Conversation
tony
added a commit
that referenced
this pull request
Sep 13, 2026
tony
force-pushed
the
workspace-cli
branch
2 times, most recently
from
September 14, 2026 00:13
9c2ff60 to
d38dc3b
Compare
tony
added a commit
that referenced
this pull request
Sep 14, 2026
tony
added a commit
that referenced
this pull request
Sep 14, 2026
tony
added a commit
that referenced
this pull request
Sep 14, 2026
tony
added a commit
that referenced
this pull request
Sep 16, 2026
Twelve commits on this branch changed what a user sees and none of them reached the changelog. Two are breaking: panes with no start_directory now start where the command was run rather than where the file lives, and load --ndjson spells the input count inputs rather than input_count. The freeze entries say what the old output did wrong, because a user may have documents saved from it -- a shell named for a pane that was only sitting at one, and on macOS bash named for every plain pane.
tony
added a commit
that referenced
this pull request
Sep 16, 2026
Six commits on this branch changed what a user or a --json consumer sees and none of them reached the changelog. Two are breaking: the --append focus fix changes which window the client ends up on, and the four new error codes replace the generic operation_failed a script may already be branching on.
tony
added a commit
that referenced
this pull request
Sep 16, 2026
Follow-up to the previous changelog entry: the remaining six codes, the stderr record's schema_version, and the trailing-separator message fix all reached the changelog together since they are the same S14 completion.
tony
added a commit
that referenced
this pull request
Sep 16, 2026
tony
added a commit
that referenced
this pull request
Sep 19, 2026
tony
added a commit
that referenced
this pull request
Sep 19, 2026
Twelve commits on this branch changed what a user sees and none of them reached the changelog. Two are breaking: panes with no start_directory now start where the command was run rather than where the file lives, and load --ndjson spells the input count inputs rather than input_count. The freeze entries say what the old output did wrong, because a user may have documents saved from it -- a shell named for a pane that was only sitting at one, and on macOS bash named for every plain pane.
tony
added a commit
that referenced
this pull request
Sep 19, 2026
Six commits on this branch changed what a user or a --json consumer sees and none of them reached the changelog. Two are breaking: the --append focus fix changes which window the client ends up on, and the four new error codes replace the generic operation_failed a script may already be branching on.
tony
added a commit
that referenced
this pull request
Sep 19, 2026
Follow-up to the previous changelog entry: the remaining six codes, the stderr record's schema_version, and the trailing-separator message fix all reached the changelog together as pieces of the same shared-code-table work.
tony
added a commit
that referenced
this pull request
Sep 19, 2026
tony
added a commit
that referenced
this pull request
Sep 19, 2026
why: Most bullets under workspace described `tmux-workspace` command behavior -- flags, prompts, machine codes, NDJSON events -- that no published release ever had in another state, since the command is new on this branch. WRITING.md's changelog rule bars mentioning old behavior a reader never experienced, so those entries needed to state the final behavior, not the branch's own before/after. what: - Fold the `tmux-workspace` entries (load, freeze, import/convert, ls/search, edit/shell, machine codes, NDJSON events, progress, signals, CLI.md) into grouped bullets under workspace, one per subcommand or shared concern, dropping "no longer"/"now"/"was"/ "instead of" contrast and every "breaking change" note, since nothing published is broken by an unreleased command - Keep the entries that change behavior a caller of the published `workspace` library already has: `Parse`/`Build`/`BuildInto`'s layout checks, `Build`'s pane-focus selection, map-iteration ordering, split rebalancing, literal command delivery, `Workspace.SuppressHistory`'s type, `Workspace.Validate`'s layout error, and the README/package-doc description of the library and the CLI - Drop the entries about this branch's own documentation draft (a corrected difference table) and an internal parser benchmark, neither observable outside the branch
tony
added a commit
that referenced
this pull request
Sep 19, 2026
why: `tmux-workspace` ships whole in this pull request, so what a caller gets belongs in one set of entries rather than the running record kept while it was written. what: - Describe the command under `workspace`: loading and its prompts, document handling, machine codes, events, `freeze`, imports and search - Keep the module's own published symbols in their own entries, naming the symbol rather than a subcommand - Carry the pull request number on every entry
tony
force-pushed
the
workspace-cli
branch
4 times, most recently
from
September 19, 2026 16:54
78a03de to
496cc6f
Compare
tony
added a commit
that referenced
this pull request
Sep 19, 2026
why: `tmux-workspace` ships whole in this pull request, so what a caller gets belongs in one set of entries rather than the running record kept while it was written. what: - Summarize the command under `workspace`, linking to CLI.md for its reference tables instead of restating them entry by entry - Keep the module's own published symbols in their own entries, naming the symbol rather than a subcommand - Record `Running.Kill`'s new return semantics under `tmux` - Carry the pull request number on every entry
tony
added a commit
that referenced
this pull request
Sep 19, 2026
why: `tmux-workspace` ships whole in this pull request, so what a caller gets belongs in one set of entries rather than the running record kept while it was written. The branch also carries public behaviour changes to already-published symbols that had no entry at all. what: - Summarize the command under `workspace`, linking to CLI.md for its reference tables instead of restating them entry by entry - Keep the module's own published symbols in their own entries, naming the symbol rather than a subcommand - Record `Running.Kill`'s new return semantics under `tmux` - Record `Server.SwitchClient` and `kill_session` anchoring their target by exact match, and `Version.AtLeast` ranking a `next-X.Y` build below the release it names - Separate freeze's two session-name refusals: an unsafe file name, which `--save-to` works around, from an unloadable one, which it does not - Carry the pull request number on every entry
why: Workspace documents need a command interface that preserves full configuration data and separates machine output from terminal work. what: - Add the tmuxp command tree with inherited JSON and NDJSON output. - Load, append, capture and convert through native workspace services. - Preserve Python expression and shell behavior through checked bridges. - Cover parsing, process streams and isolated tmux lifecycle regressions.
…nothing why: The comparison against a reused session reported status partial under session_not_found. Both were wrong. partial describes a load that left something behind, and this one creates nothing and changes nothing -- the session it found was already there. And the session was found, so session_not_found named the wrong condition: what failed is that the session does not satisfy the document. what: - Report session_mismatch, status error and exit 1 for a reused session that does not match, marking the failure as leaving no effect behind - Name the first window the session does not have rather than listing them - Assert the code on both the summary entry and the stderr record
why: Parsing returned at the first refusal, so a document with a mistake in two windows reported one of them and the user found the other by rerunning. The library beside this one has reported every problem at once all along. The classification had to move with it: a joined error hands the first embedded failure to anything reaching in for one, and every other window's problem then disappears at the process boundary. what: - Lift the window body into normalizeWindow and collect each window's first problem, so one mistake does not hide the next - Flatten the refusal into a single failure whose message carries every problem and whose code is the first one's - Keep document-level refusals stopping at the first, as there is nothing to carry on into - Assert both problems reach the machine envelope, not only the parser
why: The builder ranged over the option and environment maps directly, so Go randomised the order: three runs of one document applied six session options as f e d c b a, f e d c b a, then d c b a f e. A tool whose value is reproducible workspaces should not order its own writes by chance, and the command beside it already sorts. what: - Sort every option and environment map by name before applying it, in the workspace, window and pane passes alike - Cover it with six named options through a recording tmux - Drop the row it added to the README's difference table
why: terminal_required, unsupported_color_mode, unsupported_combination, confirmation_required and unsafe_destination each named a condition that is, without this port's vocabulary, "the user invoked this wrongly" -- every one of them already ends its message with the flag that fixes it (use -d, use --save-to, use explicit noninteractive options). A machine consumer branching on a code was seeing five local spellings of the same thing usage already says, which is exactly what naming a shared code across ports exists to prevent. what: - Route all five through the existing usage() helper: exit 2, code usage - Rename the test asserting confirmation_required to assert the invocation classification instead, and update the four call sites' test assertions - Leave destination_exists (a real destination already existing) and client_changed (the handoff's own race, not an invocation mistake) unchanged -- neither restates a code the port has any other name for - Correct a changelog line that still listed confirmation_required among the shipped codes
why: the ts port keyed its envelope status off whether any error carried session_mismatch, so a command loading two documents where the first built a session and the second mismatched came out status: error -- the built session was a retained effect the check never looked at. This port derives status from what survived per result across the whole command (the "Load(fix): Remove a session the build created when it fails partway" commit), which is the shape that does not make that mistake, but nothing exercised two inputs together to prove it. what: - Load a document that builds a fresh session alongside one that names an existing, mismatched session; assert partial, exit 1, session_mismatch on the failed entry, both result records present, and the first session actually built - Confirmed this test fails under the ts-style derivation (keying status off the error code instead of accumulated retained effects) before restoring the correct one
…s window why: The wording "a client is attached to it" did not say whether *it* is the invoking pane or that pane's session, and go read the stricter one: refuse unless a client's active pane is the exact pane the load was typed into. An attached load ends in switch-client, and what that needs is a client it can move, not a client parked on the pane the command happened to be typed into. The stricter reading refused a legitimate case tmuxp does not: a load sent to a background pane by a script or send-keys while the user is looking at another window of the same session. what: - In the client-matching loop, a client on the pane's session but a different window no longer disqualifies itself outright: it becomes the fallback candidate instead - Prefer the client whose active pane is the invoking pane, as before; fall back to the session's other client only when none matches exactly; refuse only when the session has no attached client at all - Reword the final refusal to name the condition it now actually checks - Add a test: a second window and its own client, with the load pointed at the first window's pane, must switch that client rather than refuse. Confirmed it fails under the stricter window-only match before restoring the fix
why: "<name> is already running. Attach?" is asked about the load's last input only -- existingTargetSession checks only that name -- but answering "n" returned from the entire load before the per-input loop ever started, so `load a.yaml b.yaml` with only b's session already running silently skipped building a too. A single-input probe cannot see this: with one input, "abort the whole load" and "leave this one session alone" produce the same result, and only two inputs separate them. Declining is answering a question, not a failure: no reuse was attempted, so comparing that session to its document would report a mismatch nobody asked for. what: - Carry the declined session's name forward instead of returning; the per-input loop skips comparison and build only for the plan matching that name, recording it as declined rather than completed or failed -- every input before it still builds and still counts toward the envelope - Set o.detached on decline so nothing is attached to at the end, same as the sibling "already inside tmux" prompt's own "n" - Print "Not attached to <name>." in human mode, and emit a declined warning event in machine mode - Split prompt() into a thin wrapper over promptDefault, which takes the bracketed hint separately from the value an empty answer falls back to; the attach prompt now shows "[Y/n]", naming both choices with the default capitalized, instead of repeating the bare default alone - Add a two-input test: only the last input's session exists and mismatches, decline it, assert the first session was still built and the second untouched. Confirmed it fails under the old whole-load return before restoring the fix
… row why: The table said the command reports "the first, with no position" for every problem in a document. That was true when it was written but a later fix gave the command a line number and made it report every window's problem rather than just the first, and the table was never updated to match. what: - State that the command reports one problem per window, each with its line, and that a document-level refusal still stops at the first
why: commandFailure fell back to tmux_failed for any unclassified error, including a failed stdout/NDJSON write and a failed terminal restore on exit -- neither is tmux failing, and a code that says so sends a reader to the wrong place. Confirmed with a real load: killing the read end of stdout mid-write reported tmux_failed. what: - Give the output-writer and terminal-restore fallbacks their own output_failed code, leaving a genuine unclassified tmux command failure as tmux_failed - Document every machine code outside the shared set in the CLI reference, including the ones that already existed beside it - Fix a changelog entry that had lost its own opening line to an earlier edit, leaving a later bullet's body attached to it
… codes why: A hand-rolled linear-search contains duplicated slices.Contains, already used in the same package for the same job. styleFor built a fresh role-to-ANSI- code map on every styled fragment, and ls/search style several per row. what: - Replace every contains(...) call with slices.Contains and drop the helper - Hoist styleFor's code table to a package-level var
why: The commandFailure fallback covered only the case where a write failure surfaced through a deferred cleanup with a nil primary error, and its message claimed a real-load verification that was never run. Checked directly: 'tmux-workspace load x.yaml -d --json' piped into a full disk still reported tmux_failed, because encode(), event()'s NDJSON branch, the human warning line, the progress presenter delegation and streamed script output all returned the raw write error through an ordinary "return err" before commandFailure's fallback ever saw it. Watched fail: reverted encode()'s wrap, ran the same load into /dev/full, got tmux_failed back; restored, got output_failed. what: - Add outputFailure, applied where invocation and tmux.go consume an encoder, the NDJSON event writer, the human warning line, and the progress presenter's event/close/script return values -- not inside those types themselves, so a caller reading their return value directly (an existing test does, on the progress presenter) still sees the underlying error through errors.Is - Correct CLI.md's child_failed row: a plugin or before_script bridge failure during load is script_failed, not this
why: Three test comments named decision IDs from a QA tracker with no copy in this repository, so they read as unexplained codes to a reader; describing the behaviour directly does not depend on it. what: - Reword the three comments to describe the behaviour under test with no ID
why: The package had no benchmark anywhere, so a regression in the document parser -- the code every load and convert runs first -- had nothing to catch it. Parsing needs neither tmux nor the disk, so it does not need the cross-path equivalence machinery the benchmarks module gates; an ordinary Benchmark function is the right size for it. what: - Add BenchmarkDecodeDocument and BenchmarkNormalize over a fixed six-window, three-pane document - Record allocation counts in BENCHMARKS.md with the reproducing command
why: The information a machine consumer needs -- exit codes, the shared codes, the codes outside that set, NDJSON events, environment variables -- was present in CLI.md only as prose, and the codes-outside-the-set table had nothing keeping it in sync with the source. Also found while building the gate: a defensive, unreachable branch would have emitted the retired generic load_failed code had it ever fired. what: - Add exit-code, shared-code, NDJSON-event and environment-variable reference tables to CLI.md - Add a test that scans the package's own failure literals and fails if one is not named in the reference; watched it fail against a planted undocumented code, confirmed it passes clean, then removed the plant - Change the unreachable empty-code fallback to tmux_failed instead of the retired load_failed
why: findSession compares tmux's own session list against the literal argument, so a name a session actually holds under a rewritten or otherwise different spelling read as "no such session" instead of the real reason: load reads a period or colon in a target as a separator, so an ordinary "-t name" can never address a session_name holding one by itself. The old test only exercised this by creating a session under that literal name first, which only tmux 3.7a and later actually keeps, so the check went untested everywhere else and the precondition itself could never hold in the sense the test assumed. what: - Add rejectUnloadableSessionName and call it on freeze's raw target before any session lookup, and again on an auto-detected session's resolved name - Rewrite TestFreezeRefusesASessionNameLoadWouldReject to assert the refusal on the argument alone, against an unstarted server, with no version gate or precondition
why: tmux applies the "=" exact-match prefix to the session part left after splitting a target on a period or colon, so "=my.proj" fails on exactly the names it looks like it should protect -- the same misreading that makes an ordinary "-t name" miss such a session. kill_session already resolves its public session_id argument to a materialized name safely; the internal kill then threw that away and rebuilt an "=name" target from it, so a session named with a period or colon could be listed and selected by id but not actually killed. what: - Resolve the session by exact name (a Go comparison against the materialized list, not tmux target syntax) and kill it by the resolved Session, which addresses by id - Add TestKillSessionAddressesADottedName: create the session and check the actual result before asserting, since [tmux.Version.AtLeast] compares feature level and treats 3.7 and 3.7a alike, so it cannot gate the one release (3.7) that refuses such a name outright between the two that rewrite or keep it
why: ParseVersion's regexp captured the leading "next-" marker but never read the capture back, so "next-3.8" parsed identically to a plain "3.8" -- same major, minor, and development flag. AtLeast for a feature gated on 3.8 then reported a development snapshot as already having it, when a "next-3.8" build by definition has not reached 3.8 yet and may not carry everything that release ends up shipping. what: - Decrement the parsed development level for a "next-" token, keeping its numeric core so it still orders above every release before the one it names and below master, which has no numeric core of its own - Add next-3.8 vs 3.8 and next-3.8 vs 3.7 cases to the comparison table
why: SwitchClient validated targetSession as a session name and then sent it as a bare "-t name", which tmux resolves by prefix: a name that matches no session exactly but starts a different one's name switches there silently instead of failing. With only a session named "doomsday", SwitchClient(ctx, "doom") landed the client on it. The control attach path already addresses by session id and is unaffected. what: - Send the target anchored as "=" + targetSession; safe here because validateLifecycleSessionName already refuses the period or colon that would make the anchor itself misread - Update the exact-argument test for the new anchored form A real-tmux negative test needs an attached client to observe: with none, switch-client refuses both "doom" and "=doom" identically with "no current client" on every version checked (3.2a, 3.7c, and the locally installed 3.7d), so a client-free assertion cannot tell the anchored and unanchored forms apart. A pty-attached client made the distinction observable locally but left the wait for it unbounded in CI, where no client ever registered within the timeout -- exactly the open-ended wait this project treats as a structural bug rather than something to extend. The exact-argument test is the regression cover for this fix.
why: cd9dd77 made the CLI select the last pane created when no pane asks for focus, matching tmuxp. CLI.md still described the old first-pane default as a deliberate difference from the reference, which TestDefaultActivePaneIsTheLastOneCreated now contradicts. what: - State that the last pane stays active, matching the reference - Keep only the tiled-vs-halving-splits difference as deliberate
why: No workflow, justfile, or Makefile runs verify_cli.py, and it needs tmuxp 1.74 plus a Python runtime that nothing here installs. It duplicates the integration suite's run(t, ...) helper and the CI-gated benchmarks module. what: - Remove workspace/scripts/verify_cli.py - Remove CLI.md's "Local verification" section, which documented only that script
why: workspace/internal/cli/{files,logging,process,prompt,
handoff_terminal}_other.go carry "!unix" build tags. The only Windows
job vets `./tmux/internal/tmuxcmd`, so those files never compile
anywhere in CI. A deliberately undefined identifier in one of them
left `go vet ./workspace/...` on Linux at exit 0 while `GOOS=windows
go vet ./workspace/...` reported it.
what:
- Add a step vetting the workspace module with GOOS=windows
- Add workspace/go.mod to the job's cache-dependency-path
why: `tmux-workspace` ships whole in this pull request, so what a caller gets belongs in one set of entries rather than the running record kept while it was written. The branch also carries public behaviour changes to already-published symbols that had no entry at all. what: - Summarize the command under `workspace`, linking to CLI.md for its reference tables instead of restating them entry by entry - Keep the module's own published symbols in their own entries, naming the symbol rather than a subcommand - Record `Running.Kill`'s new return semantics under `tmux` - Record `Server.SwitchClient` and `kill_session` anchoring their target by exact match, and `Version.AtLeast` ranking a `next-X.Y` build below the release it names - Separate freeze's two session-name refusals: an unsafe file name, which `--save-to` works around, from an unloadable one, which it does not - Carry the pull request number on every entry
why: The rebased CLI still used the removed pane-presence result and the old YAML module path. what: Propagate the core lookup error, use the maintained YAML module, and update the MCP regression fixture to its current runtime constructor.
why: Separate layout checks let ambiguous prefixes pass preflight and rejected uppercase checksums after validation. The merged resolver also lost typed errors for mirrored presets on older daemons. what: Share trunk's name resolution with static validation, preserve JSON version gates, and use the checksummed parser for classic layouts. Extend existing dispatch and preflight tests; remove coverage of the deleted MCP validator now handled by core layout tests.
why: The trunk prefix test used a placeholder checksum that the workspace layout preflight correctly rejects. what: Keep the classic-layout acceptance assertion with its actual checksum. Malformed-layout rejection remains covered separately.
why: JSON layouts retain trunk's tmux-owned validation, while the workspace preflight parses classic checksummed trees. what: State that boundary and remove the already-released direct prefix support from this branch's changelog entry.
why: JSON layout availability is a feature boundary, while the workspace continues to support the package-wide tmux floor. what: Name the JSON format introduction in 3.8-rc and retain tmux as the authority for its contents.
why: tmux 3.8 removes independent client pane focus and ignores the active-pane flag, so its projection follows the window again. what: - Assert native client flags against the running version - Retain independent-focus refusal checks on older tmux - Verify ordinary handoff when the flag no longer takes effect
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
tmux-workspace, a Go command that manages tmuxp YAML and JSON workspaces through native Go services, plus the core-library changes it needed. The command tree follows tmuxp (repo, v1.74.0 release notes, PyPI):load,ls,search,edit,freeze,convert,import teamocil,import tmuxinator,shellanddebug-info, each accepting inherited--json,--ndjsonand--color. workspace/CLI.md documents its exit codes, machinecodevalues, NDJSON event vocabulary and environment variables.Cobra and the presentation dependencies stay in the
workspacemodule, outside the coretmuxmodule. This branch builds against the local workspace graph; publishing the CLI requires a core release containing the APIs it uses. Discovery, conversion, import, help and completion generation need no tmux daemon.shelland plugin/custom-builder loads require tmuxp 1.74.0. Search uses Go regular expressions by default, with Python matching explicitly selectable. Plugin/custom-builder append with a documentbefore_scriptis rejected during preflight. The CLI and exported workspace library support different document subsets, described inworkspace/README.md.What the CLI does
loadcreates a session per input, or reuses one whose name matches the document after checking for missing named windows. A build that fails partway removes the session it created.--appendadds a document's windows to the invoking tmux session instead of creating one.x-, which every level accepts and ignores so an anchor holder such asx-pane-defaults: &shellloads.convertpreserves arbitrary document fields. Native imports preserve source command grouping, pane order, focus, synchronization phases and directory context;import tmuxinatorrefuses unexpanded ERB markup, andimport teamociltakes the session name from the source file and accepts a pane written as a plain string.freezecaptures a session as a document, requiring--save-tounless an output flag returns it instead, and refuses a session nameloadcould never address again — one holding a period, colon, NUL or newline.loadrenders progress from lifecycle events and prompts for tmuxp's inside-tmux choices; machine output (--json/--ndjson) instead carries a specific failurecodeand never reads implicit prompts.man,markdown,yaml) and shell completion are generated from the same Cobra command tree that serves the CLI.Core library changes
Workspace.SuppressHistorychanges fromBoolto*Bool; nil enables suppression, and callers assigning the field must pass a pointer. The exported workspace builder sends command strings literally.SelectLayoutRequest.Validatechecks layout syntax without tmux I/O.Server.ValidateLayoutschecks requested pane counts and version-sensitive names against the selected daemon.Window.SelectLayoutandPlan.SelectLayoutvalidate syntax and names, while tmux checks live pane capacity and geometry. Workspace parsing and the MCPselect_layouttool share this validation.tmux.Color88;ServerOptions.Colorsnow rejects mode 88 before looking up the executable instead of deferring the failure to tmux.Running.Killdispatches its SIGKILL through a backgroundedrun-shelland returns once tmux has queued the request, not once it ran;Waitis what proves the process died.Server.SwitchClientuses tmux's exact-match prefix. MCPkill_sessionresolves the exact session name and kills by ID, including names containing periods or colons.Version.AtLeastranks anext-X.Ydevelopment build below the release it names, instead of treating it as already there.focus, a build leaves the last pane created active, matching tmuxp; an explicitfocus: trueis unchanged.workspace.Buildapplies environment variables and options in a fixed order and rebalances a window between splits instead of failing tmux's pane-capacity limit.Verification
Workspace and core integration suites use private tmux sockets. Linux workspace tests use a controlling PTY for attach, detach and client switching:
$ go test ./workspace/...$ go test ./tmux/...See CONTRIBUTING for the complete gate list: formatting, linting, vet, race, generated-code, and the tmux version matrix, run per module.
Remaining work
The CLI has no published release yet; install it from a checkout with
go install ./workspace/cmd/tmux-workspace. Standalone consumers require the new core APIs to be published first. Windows checks cover vetting and executable resolution; interactive runtime parity remains unverified.