Skip to content

Workspace: add the tmux-workspace command - #15

Open
tony wants to merge 156 commits into
masterfrom
workspace-cli
Open

tony wants to merge 156 commits into
masterfrom
workspace-cli

Conversation

@tony

@tony tony commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

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, shell and debug-info, each accepting inherited --json, --ndjson and --color. workspace/CLI.md documents its exit codes, machine code values, NDJSON event vocabulary and environment variables.

Cobra and the presentation dependencies stay in the workspace module, outside the core tmux module. 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.

shell and 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 document before_script is rejected during preflight. The CLI and exported workspace library support different document subsets, described in workspace/README.md.

What the CLI does

  • load creates 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. --append adds a document's windows to the invoking tmux session instead of creating one.
  • Native loads reject unknown workspace, window, pane, command and readiness keys before opening logs, running scripts or touching tmux, except a key starting with x-, which every level accepts and ignores so an anchor holder such as x-pane-defaults: &shell loads.
  • convert preserves arbitrary document fields. Native imports preserve source command grouping, pane order, focus, synchronization phases and directory context; import tmuxinator refuses unexpanded ERB markup, and import teamocil takes the session name from the source file and accepts a pane written as a plain string.
  • freeze captures a session as a document, requiring --save-to unless an output flag returns it instead, and refuses a session name load could never address again — one holding a period, colon, NUL or newline.
  • Human load renders progress from lifecycle events and prompts for tmuxp's inside-tmux choices; machine output (--json/--ndjson) instead carries a specific failure code and never reads implicit prompts.
  • SIGINT and SIGTERM cancel active scripts with status 130. On Unix, interactive prompts also cancel while stdin remains open.
  • Command metadata, manuals (man, markdown, yaml) and shell completion are generated from the same Cobra command tree that serves the CLI.

Core library changes

Workspace.SuppressHistory changes from Bool to *Bool; nil enables suppression, and callers assigning the field must pass a pointer. The exported workspace builder sends command strings literally.

  • SelectLayoutRequest.Validate checks layout syntax without tmux I/O. Server.ValidateLayouts checks requested pane counts and version-sensitive names against the selected daemon. Window.SelectLayout and Plan.SelectLayout validate syntax and names, while tmux checks live pane capacity and geometry. Workspace parsing and the MCP select_layout tool share this validation.
  • Remove tmux.Color88; ServerOptions.Colors now rejects mode 88 before looking up the executable instead of deferring the failure to tmux.
  • Running.Kill dispatches its SIGKILL through a backgrounded run-shell and returns once tmux has queued the request, not once it ran; Wait is what proves the process died.
  • Server.SwitchClient uses tmux's exact-match prefix. MCP kill_session resolves the exact session name and kills by ID, including names containing periods or colons.
  • Version.AtLeast ranks a next-X.Y development build below the release it names, instead of treating it as already there.
  • With no pane declaring focus, a build leaves the last pane created active, matching tmuxp; an explicit focus: true is unchanged. workspace.Build applies 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.

@tony
tony force-pushed the workspace-cli branch 2 times, most recently from 9c2ff60 to d38dc3b Compare September 14, 2026 00:13
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
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
tony force-pushed the workspace-cli branch 4 times, most recently from 78a03de to 496cc6f Compare September 19, 2026 16:54
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.
tony added 24 commits September 20, 2026 08:20
…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

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant