Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,21 @@ story <id>`, the same annotation a sweep bundle writes. Both sprint and stories

### Fixed

- **Give psmux a working per-window option channel (#310).** psmux keeps one user-option scope
per server and returns `''` for every `-w` read of an `@`-prefixed name, so both mechanisms
riding per-window options were broken. The ctl-window project tag read the same bled value for
every `list-windows` row, letting a prune in one project `kill-window` another project's
window; the parked-return option always read empty, so an attached client was never handed
back to its origin. The window-option verbs, the `@`-prefixed columns of `list_windows` and the
parked trailer now use a session-scoped option whose key carries the window id
(`@bmad_project_@3` for window `@3` — the `_@` shape keeps foreign config options out of the
cleanup sweeps), routed with an explicit `-t <session>` (the in-pane parked trailer rides
`$TMUX` instead). Values that cannot survive psmux's
control-line transport verbatim are refused with a warning instead of stored corrupted. Keys are
freed on `kill_window` and reconciled at parked-window launch; the return move itself is restored
for the detach leg, while the `switch-client` leg stays inert until psmux/psmux#483 lands
upstream. Builtin window options keep the `-w` path; tmux is untouched.

- **Session-qualify the psmux TUI-side window ids (#291).** #254 covered the engine seam but left
the launcher's surfaces bare, and that process usually runs _outside_ any pane — where a bare
`@N` resolves through psmux's most-recent-session fallback, not the session that minted it. The
Expand Down
16 changes: 16 additions & 0 deletions docs/multiplexer-backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,22 @@ selection falls through). Native Windows is still experimental — see the
[roadmap](ROADMAP.md#native-windows-multiplexer-backend) for the remaining work. WSL is
unaffected: it _is_ Linux and uses tmux.

Two model differences matter if you port a backend or read psmux argv. psmux runs one server
per session, so window ids are minted per server and the backend session-qualifies every id it
hands out (`session:@N`). And psmux has no per-window user options — one scope exists per
server — so the window-option verbs and the `@`-prefixed columns of `list_windows` are served
by a session-scoped option whose key carries the window id (`@bmad_project_@3` for window `@3`).
Both are properties of psmux's model, not gaps awaiting an upstream release. Practical
consequence: such a value is **not** readable via `psmux show-options -w` by hand — read it with
`psmux show-options -qv -t <session> "@bmad_project_@N"` instead. One visible limit: a value
that cannot survive psmux's control-line transport verbatim is refused with a stderr warning at
every launch, and that project's windows stay untagged — the prune then scopes them through the
run-dir fallback instead of the tag. Which paths those are is counter-intuitive, because the
psmux client quotes a value only when it contains an ASCII space and `'` is literal inside those
quotes: `C:\Users\O'Brien\dev` is **refused** while `C:\Users\O'Brien Files\dev` is accepted, and
a spaced UNC path (`\\server\share\My Proj`) is refused while the spaceless `\\server\share\proj`
is accepted.

## External backends

Every backend beyond the two bundled ones is a separate package that you co-install with bmad-loop; it
Expand Down
8 changes: 7 additions & 1 deletion src/bmad_loop/adapters/multiplexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,13 @@ def select_window(self, target: str) -> None:
def set_window_option(self, target: str, option: str, value: str) -> None:
"""Set a user option on the targeted window (best-effort: a no-op on a
transport failure). ``target`` is a :meth:`target` token or a
backend-native window id."""
backend-native window id.

The contract is the (window, option) keying, not the storage: a backend
without per-window option scope may key the value however it likes
(psmux does), so read it back only through :meth:`show_window_option`
or :meth:`list_windows`, never by running the multiplexer's own option
verbs by hand."""

@abstractmethod
def unset_window_option(self, target: str, option: str) -> None:
Expand Down
Loading