Conversation
why: Workspace builders lack a user-facing CLI. what: - Add tmuxp parsing and native document/load services. - Report partial effects and supervise bounded child output. - Check the Python shell runtime and document remaining gaps.
why: Pane IDs repeat across tmux servers. what: - Compare the current and selected daemon identities. - Reject mismatched append targets before creating windows.
why: Append validation must stop when its caller cancels. what: - Accept acquisition options for daemon identity reads. - Pass CLI cancellation through and regenerate the API reference.
why: Node color warnings can corrupt machine stderr. what: - Apply color precedence before loading command modules. - Verify every command from offline-installed tarballs. - Document the installed check and remaining CLI work.
why: Python matching needs structured input over stdin. what: - Write child input with backpressure while draining output. - Join canceled processes and their pending input streams.
why: Directory aliases caused repeated workspace entries. what: - Deduplicate candidate directories while preserving precedence. - Exclude hidden workspace files from discovery and counts.
why: Compatibility should preserve a fast native workflow. what: - Document native regular expression semantics. - Keep Python-only behavior behind the optional runtime boundary.
why: Human load must enter the final workspace and reject unavailable terminal or unrelated-server context before creating sessions. what: - Attach through the controlling terminal with redirected output - Authenticate and explicitly switch tmux's selected current client - Preserve workspace effects when attachment is interrupted - Verify real terminal behavior on Bun and Node 22
why: Readiness polling delayed blank workspace loads by two seconds even though no commands needed a shell prompt. what: - Skip shell-policy queries when no pane commands need readiness - Wait only for panes receiving commands - Prove blank panes finish without a prompt under a one-second bound
why: Keep lifecycle and script diagnostics available without losing command results when logging fails or cancellation interrupts output. what: - Append severity-filtered NDJSON and retain structured machine stdout - Preserve command outcomes on log failures and close owned handles - Verify full-file failures, cancellation, and installed Node/Bun logs
why: A blocked output consumer can hold the CLI open after interruption even after its owned child has exited. what: - Propagate cancellation through human and machine output writes - Bound final interruption results and exit after owned cleanup - Verify unread pipes and child reaping on Node and Bun
why: Waiting for an escalation timer after the owned process group has disappeared adds half a second to cancellation without improving cleanup. what: - Finish immediately when the owned process group no longer exists - Retain escalation for descendants after their direct parent exits - Verify prompt cancellation and the surviving-descendant path
why: Load accepts progress flags but gives no terminal feedback. what: - Render presets and templates from native load completion events - Bound script history and serialize cancellable terminal updates - Keep machine output clean and preserve raw stream boundaries - Verify terminal behavior, runtime floors and local loop budgets
Add index to direct and planned window creation so callers can reserve sparse layouts without creating a window elsewhere and moving it. Reject invalid indexes and relative direction before transport. Preserve occupied windows and verify direct and batched creation on tmux 3.2a and the current server.
why: Implicit windows can occupy indexes requested by later inputs. what: - Allocate around explicit indexes using native indexed creation - Preserve renumbering policy during early bootstrap removal - Report owned window and pane IDs through cleanup failures - Validate index scalars before creating workspace state
why: Supported tmux versions reject the removed -8 flag. what: - Reject 88-color mode before file lookup or tmux invocation - Explain the supported -2 alternative in help and the CLI guide - Verify human and machine diagnostics without starting tmux
why: File-only commands eagerly load the tmux and YAML implementations. what: - Import tmux at dispatch and check cancellation before the operation - Load YAML only when reading or writing that document format - Serialize output before creating its temporary file
why: Workspace capture cannot stop an option read without a typed cancellation path through the core API. what: Forward command options through own, inherited and global option reads. Keep scope and decoding unchanged, refresh generated signatures, and cover every read scope with a pre-aborted signal.
why: Interrupted capture kept reading tmux and replaced its destination before output noticed cancellation. what: Pass the invocation signal through capture reads and file saves. Check cancellation before atomic publication, retain completed writes, and prove interrupted backend clients exit without replacing the file.
why: Keep completion and offline references aligned with native options without starting a runtime on each Tab. what: - Export Commander metadata and validate native shell choices - Generate Bash, Zsh and fish scripts with reference artifacts - Check freshness, real shell insertion and installed script parity
why: Bare root and import groups must not mix human help into machine output or report success without an operation. what: - Return structured usage diagnostics and status 2 in machine modes - Preserve human help and explicit help/version handling - Cover both groups with JSON and NDJSON regressions
why: Callers need to associate topology with the daemon that answered without a second acquisition that may observe a replacement. what: - Return the immutable captured identity with each live snapshot - Verify empty-server identity, cancellation and unchanged query count - Update generated API examples and the published type contract
why: Resolving the current pane in a second acquisition can observe a replacement daemon after the selected endpoint has been checked. what: - Compare the selected snapshot identity with the inherited endpoint - Resolve the borrowed session from that same snapshot - Remove one complete graph acquisition from append preflight
why: Supported tmux versions reject 88-color mode, so accepting it in the core defers a configuration error until execution. what: - Restrict public color overrides to 256 with runtime validation - Remove legacy client flags and update docs and type contracts - Verify rejection and 256-color acquisition on both tmux lanes
why: An empty plugin array selects no extension, but JavaScript array truthiness incorrectly rejected it as requiring the Python bridge. what: - Require an actual plugin selection before leaving native loading - Verify blank panes load with empty plugins and unavailable Python
why: A completed child can leave descendants holding captured pipes open. Cancellation callbacks also discarded output already captured by the CLI. what: - Allow 100ms to drain after exit, then stop the owned process group - Close remaining capture streams after process-group cleanup - Retain captured output when a callback raises the active abort reason - Prove normal-exit and cancelled descendant cleanup in the runner tests
why: Node reports premature closure when cleanup ends inherited pipes, replacing a successful parent status with an unrelated stream error. what: - Recognize only deliberate capture closure during owned cleanup - Mark interrupted capture as truncated and preserve the parent status - Exercise the emitted runner on Node 22 with a TERM-ignoring child
why: Python plugins and custom builders need an explicit boundary while ordinary workspace loading remains native and independent of Python. what: - Validate all native plans and extension selections before execution - Run the tmuxp 1.74 builder protocol with owned bounded child streams - Retain the authenticated append target across mixed input files - Report observed effects without claiming ownership or rolling them back - Reconcile surviving topology only within the same daemon lifetime - Verify plugins, cancellation, custom builders and installed Node/Bun use
why: Chunk boundaries inserted blank panel lines into CRLF output. what: - Track a trailing carriage return separately for each script stream. - Verify empty chunks, interleaving and workspace resets.
why: Avoid restarting Bun for each command behavior check while retaining executable coverage for help and ordinary nonzero exit statuses. what: - Call the injected command entry point with fresh captured streams. - Share isolated environment setup with the executable checks. - Keep usage and editor failures at the process boundary.
…ument asks for that is not there why: Readiness was conditional on the server's default-shell being zsh, so under bash every pane showed its command twice: the text arrived before the line editor owned the terminal, the tty echoed it, and readline redrew it. The rule was never about zsh. Two quieter silences went with it: a builder option this port does not implement refused the whole document, and a start_directory that does not exist built happily with every pane in $HOME and no word about the typo. what: - Wait for a pane's prompt whatever the pane's shell is, unless the document turns readiness off - Warn, rather than refuse, for an unknown key inside workspace_builder_options; the document still loads - Warn once per declared start_directory tmux cannot change into, naming the path and the fallback
why: pane-base-index and its like are window options in tmux. Setting one through a session target lands it on whatever window is current at the time, which here is the bootstrap window the load is about to remove, so a document asking for pane-base-index got panes numbered from zero and no word about it. what: - Split a session's options: by tmux's own window-option catalog, applying the window-scoped ones to every window the load creates and the rest to the session - Order them before a window's own options:, so a window that names the same option again still wins
why: With no pane declaring focus, a load left the first pane of a window active, because every split is created detached. tmuxp leaves the last pane created. Every other layout behaviour here is held to tmuxp; this one was not, so the cursor came up somewhere else after an otherwise identical load. what: - Select the last pane created in each window when nothing declares focus; an explicit focus still wins - Update the README: the reuse comparison, the session removed on a failed create, the append that keeps and names its windows, and readiness for any shell
…where it differs why: applyWindow batched every split off the window's active pane with no layout pass between them, so halving one pane in turn ran out of rows and tmux answered "size or position no space for a new pane". A six-pane window built by the CLI failed through the published API on the same host and the same tmux. The CLI has carried the fix, and the comment explaining it, since this branch started; the library never got it, and its real-tmux suite never tried more than two panes in a window. why also: the two builders answer differently -- one converges a session that exists, the other compares it against the document -- and accept different documents, and nothing said so. what: - Split one pane at a time and rebalance between splits; a declared layout is still applied last and still has the final say - Test a six-pane window at 80x24 against real tmux - State in the README and at the entry point that the CLI and this package are separate implementations, naming the converge-versus-compare difference and the fields the strict schema rejects
…port focuses The import writes an explicit focus on the first pane unless the source names one, so that case is about explicit focus winning, not about the default the last-pane change moved.
…he CLI The library's README and entry point already say it; the command's README said nothing, so a reader arriving from either side now learns the same thing: two builders, different answers for a session that exists, different accepted documents.
why: `partial` reports retained effects. A reused session that does not satisfy the document has none -- reuse compares, it never rebuilds -- so `status: "partial"` beside `completed_stages: []` claimed progress that never happened. `invalid_workspace` was also the wrong side: the document parsed fine, the session was the one that disagreed. what: - Add `session_mismatch` to the closed `MachineCode` set: the named session exists and does not satisfy the document. - Report the reuse-mismatch load with `code: "session_mismatch"` and `status: "error"` instead of `invalid_workspace`/`partial`; nothing built, nothing changed, session left exactly as it was. - Move the existing reuse-mismatch test onto the new code and status. - Log both changed lines in the still-unreleased changelog entry.
why: The previous commit forced status to error whenever the thrown error was a session mismatch, regardless of what else load had already built. A load of two documents where the first completes and the second finds a mismatched session then reported error for a session it had just built and kept -- moving the same lie to the other direction. what: - Fold the exemption into the `changed` predicate: a result with `missing_windows` set contributes no retained effect, but a `completed` or otherwise-touched result from another input in the same load still does. Drop the blanket status override. - Add a two-input regression: first document builds and keeps a session, second mismatches; expect `status: "partial"`, the first result at `stage: "completed"`, and `session_mismatch` on the error.
why: The "<name> is already running. Attach?" prompt's answer was orthogonal to the reuse comparison: whichever way the user answered, load ran the same compare against the session and reported session_mismatch for anything the document declared that the session did not hold. Declining means the tool never went to reuse that session -- nothing was built, nothing was switched, the client stayed put -- so there is nothing to compare and no mismatch to report. The visible symptom was the wrong exit code for declining the prompt; the real defect was running the comparison at all after a decline, independent of what the exit code was mapped to. what: - Track the decline on the existing-session branch of the attach prompt (the only prompt this applies to; it names inputs.at(-1) alone) and skip the missingWindows compare for that one input when set. Accepting the prompt (or the non-interactive default) still compares, as does every other reuse -- mirrors rs's `Disposition::Decline` guard. - The skip is per-input, not a return before the loop: a multi-document load still builds every earlier document that does not yet exist: only the last input's own comparison is what a decline excuses. - Add a pty-driven terminal test: a bare session missing a declared window, attach prompt declined, asserts exit 0 and the session untouched. The existing (n) test at this prompt used a session matching the document exactly, so it could assert an exit code without ever reaching the compare -- proved by reverting the fix and watching this new test's exit assertion fail (0 vs 1).
why: The pty test asserting a declined attach skips the reuse comparison named the decision by shorthand in its own comment rather than describing the behavior, which reads like a ticket reference embedded in the test. what: - Reword the comment to state what the assertion checks without the shorthand names.
why: An interrupt during session creation was handled the same as a build failure: the before-script catch and the outer session-create catch each killed a session this load had just started, whatever raised the error. Stopping the tool is not the same as it refusing or failing something it can name, and it does not always know which of its own mutations before the interrupt landed; destroying what it built on the way out is a second unbounded operation begun at the moment the user asked to stop. what: - Guard both catches on the abort signal's state: a new session is only removed for a diagnosable failure, never for a cancellation. - Add a regression for an interrupt raised while building a window with no before_script involved: the session and the window already created are retained and reported partial, not removed. Reverting the guard reproduces the old removal. - Rework the existing before-script cancellation test the same way: the session it was building is retained rather than torn down. Reverting that guard reproduces the old failure too. - Log the changed behavior.
why: `applyWorkspace` ran to completion once begun. A caller that had already given up -- a request abandoned, a watcher restarting -- still paid for every tmux call, and there was no way to say so. The README also said nothing about two calls racing on one session name, which tmux cannot arbitrate: a session that does not exist yet has no lock, so both callers can observe none and each start one. what: - `applyWorkspace` and `planWorkspace` take a `signal`, threaded through every tmux call they make, plus a check at the loop boundaries where no call is in flight - Say in both READMEs that concurrent calls on one name are uncoordinated, and that callers should serialize them - Test: an already-fired signal rejects before any session exists, and it is the preflight that rejects, not an unknown-option guard - Changelog entries tests: workspace package typechecks; 43 tests pass. Dropping the signal in normalization fails the new test.
tony
added a commit
that referenced
this pull request
Sep 19, 2026
why: `@libtmux/workspace-cli` is new on this branch, so most of its `Unreleased` entries described the `tmux-workspace` command's own prior behaviour -- a state no user of a published release ever saw. WRITING.md's slop-prevention rule bars mentioning an intermediate state or "no longer" behaviour unless a published release's users actually experienced the old one, and files this kind of entry as a plain description of what the command does instead. what: - Fold the `@libtmux/workspace-cli` entries (`load`, `freeze`, `convert`, `import`, the machine `code` taxonomy, `--ndjson` events, terminal sizing, and packaging) into a few grouped paragraphs describing the command's behaviour, dropping "no longer"/"now"/"previously"/"was" contrast and the `**Breaking.**` marker, since nothing published broke - Keep every entry under `@libtmux/workspace` (`applyWorkspace`'s split rebalancing, the `signal` parameter, the two builders' documented differences), `libtmux` (layout validation, `ServerOptions.colors`, `ServerSnapshot.daemonIdentity`, cancellable acquisition) and `@libtmux/mcp` (`select_layout`, stdio shutdown) unchanged: each describes a real change to an already-published package
tony
added a commit
that referenced
this pull request
Sep 19, 2026
why: `@libtmux/workspace-cli` 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 package under its own heading: loading and its prompts, document handling, machine codes, events, `freeze` and imports - Leave the `libtmux`, `@libtmux/workspace` and `@libtmux/mcp` entries as the published-package changes they already record - Carry the pull request number on every entry
why: The cancellation commit left builder.test.ts with a trailing blank line; oxfmt --check failed the Gates job on it. what: - Run oxfmt over builder.test.ts; no behavior change.
…the lookup why: load refuses a session_name or window_name holding "." or ":" before it ever runs tmux, since those are target separators; freeze's own check ran only against a session it had already resolved. freezeSession matched an explicit name by plain string equality against a full snapshot, not through tmux's own "-t" parsing, so on 3.7a and later it did find a session actually named "my.proj" and refuse it correctly -- but every version at or below 3.6b rewrites that name to "my_proj" at creation, and 3.7 itself refuses to create it, so the lookup answered session_not_found before the addressability check ever ran. The regression test asserting the real contract inherited a session-existence precondition that cannot hold on any of those releases, and building its own fixture failed outright on 3.7, which also refuses a window named with a dot. what: - Check an explicit session_name argument's own syntax before asking tmux whether anything runs under it, sharing freeze's existing addressability check with the one already run against a resolved session and window name - Rewrite the freeze test to assert the refusal directly, independent of whether a session exists under the literal name; add a case addressing an existing dotted session by ID, gated on the one era that can hold one - Tighten the CHANGELOG line the earlier freeze fix landed under
… target
why: no `-t` target spelling is both exact and correct for every name.
`=name` matches exactly but tmux reads a "." or ":" in the name as a target
separator first, so `has-session -t =my.proj` answered no even when a
session was named exactly that; `name:` survives those characters but
resolves a unique prefix the same way a bare name does, so it would
silently reintroduce the ambiguity `hasSession` exists to avoid. Sending
the same request twice (freeze's `session_not_found` on a name it should
have refused on syntax alone) is the same class of bug found on this
branch's own freeze fix, in a different command.
what:
- hasSession lists every session and compares the requested name against
`#{session_name}` in JavaScript, exact for any name tmux can store
- Update the docstrings on both the internal helper and Server.hasSession
- Unit test: the mocked request now matches list-sessions, plus a case a
session named "alphabet" must not satisfy hasSession("alpha")
- Integration test: a session actually named "my.proj", spawned outside
the library since newSession refuses the name itself, must satisfy
hasSession("my.proj") -- gated on the one tmux era that stores such a
name verbatim, verified against every tagged release from 3.2a to 3.7c
- Reword the names.ts doc comment: a name holding "." or ":" needs a
target spelling ordinary lookups do not use, not "unaddressable"
- Regenerate docs/api.md; changelog entry under `libtmux` > Server
why: tmux 3.8's candidate reports its version as `3.8-rc`, and older candidates as `3.0-rc3`. The parser refused both as invalid, so any call that checks the running version threw against a candidate build. what: - Read a trailing `-rc` or `-rcN` as a build of the release it names, classified the same way as `next-X.Y` - Move `3.7-rc1` out of the rejected spellings and pin `3.8-rc`, its fields, and its ordering beside `next-3.8`; keep `-rc0` and trailing junk rejected - Record the fix in the changelog
why: A control connection sent its target to attach-session as given, and tmux matches a bare -t name as a prefix: asking to attach to `doom` when only `doomsday` existed attached to `doomsday` and streamed a session nobody named. what: - Pass a session id through as given and anchor a name with `=` - Cover it: a prefix name is refused while the exact name still attaches - Record the fix in the changelog
why: compareTmuxVersions checked development-ness before major and
minor, so next-X.Y and a release candidate both ranked above every
tagged release. server.versionAtLeast("99.0") answered true on
next-3.9, passing a feature gate for a release that build had not
shipped.
what:
- Split development builds into named-next (next-X.Y, ranks below
the release it has not shipped and above the one before it) and
untargeted (bare master, <tag>-master, which names no release and
stays unbounded)
- Rank a release candidate as the release it names, not below it: it
is feature-frozen there, unlike a next-X.Y build
- Strip each development marker independently so parseTmuxVersion
and the new classifier agree on every shape
- Update versionAtLeast's TSDoc, the README, and the changelog;
regenerate docs/api.md
why: build ran notices (write mode) before the Gates lane's test step, so notices.test.ts compared freshly regenerated content to itself and could never catch a stale THIRD-PARTY-NOTICES.md. what: - Call notices:check in build, matching how docs:check already gates staleness there
why: hasSession answered false for any list-sessions failure, not only a cold socket. A permission error or a daemon that refused the command read the same as "no such session", which a caller deciding whether to create one cannot tell apart from an actual absence. what: - Share isColdEndpoint (moved from layout.ts) so hasSession can answer false only for a socket with no daemon listening - Raise every other failure instead of folding it into "no session" - Update the TSDoc and changelog; regenerate docs/api.md - Unit test both directions: a cold socket and a non-cold failure
why: `@libtmux/workspace-cli` 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 package under its own heading: loading and its prompts, document handling, machine codes, events, `freeze` and imports - Leave the `libtmux`, `@libtmux/workspace` and `@libtmux/mcp` entries as the published-package changes they already record - Carry the pull request number on every entry - Rank a named development build and a release candidate by the release they name, and raise a real `hasSession` failure instead of reading it as "no such session" - Record that `Session.newWindow` accepts an exact `index`, and that the option readers and `daemonIdentity` accept `CommandOptions`
tony
added a commit
that referenced
this pull request
Sep 19, 2026
why: `@libtmux/workspace-cli` 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 package under its own heading: loading and its prompts, document handling, machine codes, events, `freeze` and imports - Leave the `libtmux`, `@libtmux/workspace` and `@libtmux/mcp` entries as the published-package changes they already record - Carry the pull request number on every entry - Rank a named development build and a release candidate by the release they name, and raise a real `hasSession` failure instead of reading it as "no such session"
tony
added a commit
that referenced
this pull request
Sep 19, 2026
why: `@libtmux/workspace-cli` 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 package under its own heading: loading and its prompts, document handling, machine codes, events, `freeze` and imports - Leave the `libtmux`, `@libtmux/workspace` and `@libtmux/mcp` entries as the published-package changes they already record - Carry the pull request number on every entry - Rank a named development build and a release candidate by the release they name, and raise a real `hasSession` failure instead of reading it as "no such session" - Record that `Session.newWindow` accepts an exact `index`, and that the option readers and `daemonIdentity` accept `CommandOptions`
tony
force-pushed
the
workspace-cli
branch
2 times, most recently
from
September 19, 2026 22:03
4d7d81a to
3d55c6e
Compare
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
@libtmux/workspace-cli, a package whosetmux-workspacebinary manages tmux workspaces from YAML and JSON on Node.js 22.12+ or Bun 1.3.14+. Parsing follows tmuxp (repo, v1.74.0 release notes, PyPI), and loads run on native document and tmux services built onlibtmux.libtmuxkeeps its zero-runtime-dependency contract: Commander, YAML and terminal text helpers are bundled into the CLI only.What the CLI does
loadallocates windows around explicit indexes with native indexed creation, skips readiness polling for panes without commands, and reports owned window and pane IDs through cleanup failures.loadrenders progress presets and templates;--log-fileappends severity-filtered NDJSON.importinvocations return structured usage errors in machine modes instead of human help.-8is refused before file lookup or tmux invocation.Core library changes
index, rejected when invalid or combined withdirection.daemonIdentity, so callers can tie topology to the daemon that answered without a second acquisition.showOptionsandshowResolvedOptionson every scope acceptCommandOptions, so a caller's abort signal reaches them.colorsnarrows to256; 88-colour mode is rejected at runtime and no longer forwards-8. A caller passing88stops type-checking.Server.hasSessionlists sessions and compares the name directly, exact for any name tmux can store, including one holding.or:. It returnsfalsewhen no exact name exists or no server is running; other failures to list sessions raise.Server.connect({ target })rejects session-name prefixes; names containing.or:retain tmux target parsing.Server.versionAtLeastranks a named development build (next-X.Y) below the release it names and above the one before it, and reads a release candidate (X.Y-rc) as that release. An untargeted build (baremaster, or<tag>-master) still satisfies any minimum.Server.validateLayoutsandWindow.selectLayoutcheck a window plan's layout strings — preset names, mirrored variants, and a serialized tree's checksum and grammar — before either workspace builder applies it.packages/libtmux/docs/api.mdis regenerated for the new signatures.Verification
packages/workspace-cli/testscovers parsing, documents, load, progress, completions and terminal behaviour against real tmux. The install check packs the packages and runs every command from offline-installed tarballs on Node and Bun:$ bun run --cwd packages/workspace-cli test:installPlatform scope
Native lifecycle and terminal tests cover Linux. The macOS lane verifies package installation; native lifecycle behavior there is unverified.