opencode-mux 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -10
- package/dist/args.d.ts +2 -0
- package/dist/args.js +11 -1
- package/dist/args.js.map +1 -1
- package/dist/cli.d.ts +52 -1
- package/dist/cli.js +227 -25
- package/dist/cli.js.map +1 -1
- package/dist/session.d.ts +9 -0
- package/dist/session.js +7 -0
- package/dist/session.js.map +1 -1
- package/dist/tmux.d.ts +34 -1
- package/dist/tmux.js +86 -14
- package/dist/tmux.js.map +1 -1
- package/dist/tracker.d.ts +13 -1
- package/dist/tracker.js +20 -5
- package/dist/tracker.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/watch.d.ts +15 -0
- package/dist/watch.js +250 -13
- package/dist/watch.js.map +1 -1
- package/package.json +11 -6
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ matter what your tmux is configured to look like.
|
|
|
9
9
|
|
|
10
10
|
When a session running inside opencode-mux spawns a subagent, a new tmux pane
|
|
11
11
|
opens showing that session live. When the subagent finishes, the pane closes
|
|
12
|
-
itself. The subagent panes use opencode's `mini` frontend by default (configurable via `--pane-command` or `pane_command` in the config file) — no tab bar, no agent switcher.
|
|
12
|
+
itself and the remaining subagent panes re-tile to share the space evenly. The subagent panes use opencode's `mini` frontend by default (configurable via `--pane-command` or `pane_command` in the config file) — no tab bar, no agent switcher.
|
|
13
13
|
|
|
14
14
|
```
|
|
15
15
|
┌──────────────────────────────────────────┬──────────────┐
|
|
@@ -27,6 +27,8 @@ itself. The subagent panes use opencode's `mini` frontend by default (configurab
|
|
|
27
27
|
your `~/.tmux.conf`, `~/.config/opencode/cli.json`, or `tui.json`.
|
|
28
28
|
- **Coherent look** — pane borders are styled to match your active opencode
|
|
29
29
|
theme, and the session opts are scoped to the mux session only.
|
|
30
|
+
- **Concurrent instances** — one mux per project, each with its own tmux
|
|
31
|
+
session and its own subagent panes.
|
|
30
32
|
|
|
31
33
|
## Requirements
|
|
32
34
|
|
|
@@ -48,7 +50,7 @@ To use it as your default `opencode`:
|
|
|
48
50
|
alias opencode="opencode-mux"
|
|
49
51
|
```
|
|
50
52
|
|
|
51
|
-
(binaries are `opencode-mux`, version `1.
|
|
53
|
+
(binaries are `opencode-mux`, version `1.3.0`)
|
|
52
54
|
|
|
53
55
|
## Usage
|
|
54
56
|
|
|
@@ -71,6 +73,9 @@ opencode-mux --layout tiled --model fast
|
|
|
71
73
|
# Running inside an existing tmux session also works — subagent panes are
|
|
72
74
|
# opened in the session you're already in, and already-running subagents are
|
|
73
75
|
# adopted without a startup flash.
|
|
76
|
+
|
|
77
|
+
# A second instance (another project, another terminal) is fine: it gets its
|
|
78
|
+
# own tmux session and only panes that project's subagents.
|
|
74
79
|
```
|
|
75
80
|
|
|
76
81
|
### Mux options
|
|
@@ -84,7 +89,7 @@ opencode-mux --layout tiled --model fast
|
|
|
84
89
|
| `--pane-command <cmd>` | Subagent pane frontend: `mini` (minimal UI) or `tui` (full TUI) | `mini` |
|
|
85
90
|
| `--grace <seconds>` | Delay before a finished subagent's pane closes | `1` |
|
|
86
91
|
| `--parent <id>` | Only spawn panes for children of this session | inferred from `-s`/`--session` |
|
|
87
|
-
| `--session-name <name>` |
|
|
92
|
+
| `--session-name <name>` | Base name for this instance's tmux session (suffixed when the name is taken) | `mux` |
|
|
88
93
|
| `-h`, `--help` | Show help | |
|
|
89
94
|
| `--version` | Print the version | |
|
|
90
95
|
|
|
@@ -114,14 +119,52 @@ Precedence: **CLI flags > config file > defaults**.
|
|
|
114
119
|
|
|
115
120
|
- **Outside tmux**: opencode-mux creates a detached tmux session named
|
|
116
121
|
`mux` (or `--session-name`), applies session-scoped `status off` and
|
|
117
|
-
`mouse on`, re-executes itself inside, and attaches.
|
|
118
|
-
exits,
|
|
122
|
+
`mouse on`, re-executes itself inside, and attaches. Nothing kills that
|
|
123
|
+
session: it ends when its TUI pane exits, so detaching (`prefix+d`) keeps it
|
|
124
|
+
running for reattach, and any window you open inside it survives. Ownership
|
|
125
|
+
is never passed through tmux session environment, so a nested launch (a
|
|
126
|
+
shell tool, a second window in that session) can never tear its host down.
|
|
127
|
+
Session options and attach target the session id tmux printed at creation
|
|
128
|
+
(`new-session -P -F '#{session_id}'`, validated as `$N`), so a stale name can
|
|
129
|
+
never match another instance's session. When tmux prints no usable id, the
|
|
130
|
+
fallback targets the bare session name for `set-option` (the only form it
|
|
131
|
+
resolves) and `=name` for attach; appearance failures stay non-fatal.
|
|
119
132
|
- **Inside tmux**: the wrapper runs as a watcher. It connects to the opencode
|
|
120
133
|
server (same background service you're already using), listens for
|
|
121
134
|
subagent-session events, opens a pane per child session via
|
|
122
135
|
`opencode mini -s <id>`, styles the pane borders to match the active
|
|
123
136
|
opencode theme, and closes panes once their session finishes (after the
|
|
124
|
-
grace period).
|
|
137
|
+
grace period). If the opencode daemon restarts, the watcher resubscribes
|
|
138
|
+
after about a second and re-adopts the subagents that are still running.
|
|
139
|
+
Every pane split and every pane close re-applies the configured layout, so
|
|
140
|
+
the main pane keeps its configured size and the subagent panes always split
|
|
141
|
+
the remaining space evenly.
|
|
142
|
+
|
|
143
|
+
### Several instances at once
|
|
144
|
+
|
|
145
|
+
Each instance owns its tmux session, so you can run one per project
|
|
146
|
+
concurrently — in different terminals, all sharing the same opencode
|
|
147
|
+
background service:
|
|
148
|
+
|
|
149
|
+
- **Own session per instance.** Outside tmux the wrapper takes the configured
|
|
150
|
+
base name (`mux`) when it is free. If another instance (or a leftover)
|
|
151
|
+
already owns it, the next name is used and reported:
|
|
152
|
+
`mux-<project>`, then `mux-<project>-2`, and so on. A session is never
|
|
153
|
+
shared between instances, and nothing ever kills one: it ends when its own
|
|
154
|
+
TUI pane exits, so a detached session keeps running for reattach.
|
|
155
|
+
- **Own panes per instance.** Without an explicit `--parent`/`-s`, a watcher
|
|
156
|
+
only panes subagents whose parent session belongs to *its* directory (the
|
|
157
|
+
cwd, or an explicit opencode `<directory>` argument). Two instances on
|
|
158
|
+
different projects therefore never mirror each other's subagents. Inside
|
|
159
|
+
one project, pin the parent with `--parent <session-id>` when you run two
|
|
160
|
+
instances side by side. A `-s` value that is not a session id is reported
|
|
161
|
+
rather than silently widening the scope.
|
|
162
|
+
- **Own window per instance.** Each instance tags the panes it creates
|
|
163
|
+
(pane-scoped `@mux-instance`) and re-tiles only windows it has to itself.
|
|
164
|
+
Running two instances inside one window is not a supported layout — the
|
|
165
|
+
panes and the layout belong to the window, not to an instance — but it
|
|
166
|
+
degrades predictably instead of oscillating: the first instance keeps the
|
|
167
|
+
geometry, and a second instance never clears window-scoped border styles.
|
|
125
168
|
|
|
126
169
|
## Features
|
|
127
170
|
|
|
@@ -147,14 +190,17 @@ configured their setup to look like:
|
|
|
147
190
|
- A `system` theme or an unknown/unresolvable theme results in **no border
|
|
148
191
|
styling** — never wrong colors.
|
|
149
192
|
- Config changes are re-applied live (checked on the watcher tick), and
|
|
150
|
-
borders are cleared when the theme becomes unresolvable
|
|
151
|
-
|
|
193
|
+
borders are cleared when the theme becomes unresolvable — unless another
|
|
194
|
+
instance shares the window, in which case the clear is skipped and retried
|
|
195
|
+
on the next pane open/close instead. Zero tmux calls while nothing changed.
|
|
152
196
|
|
|
153
197
|
### Never touches your setup
|
|
154
198
|
|
|
155
199
|
All tmux writes are runtime-scoped: window options are applied with `-w`
|
|
156
|
-
(pane-border colors)
|
|
157
|
-
|
|
200
|
+
(pane-border colors), session options with `-t <session>` (status/mouse), and
|
|
201
|
+
every pane this instance creates is tagged with a pane-scoped `@mux-instance`
|
|
202
|
+
option, so a second instance can tell the window is shared. No `-g`, no writes
|
|
203
|
+
to `~/.tmux.conf`, no changes to your opencode configs.
|
|
158
204
|
|
|
159
205
|
## Requirements & compatibility
|
|
160
206
|
|
package/dist/args.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export interface ParsedArgs {
|
|
|
5
5
|
help: boolean;
|
|
6
6
|
version: boolean;
|
|
7
7
|
watch: boolean;
|
|
8
|
+
/** Internal `--mux-scope` paths forwarded to the watcher (project scoping). */
|
|
9
|
+
scope: string[];
|
|
8
10
|
passThrough: string[];
|
|
9
11
|
}
|
|
10
12
|
export declare function parseArgs(argv: string[]): ParsedArgs;
|
package/dist/args.js
CHANGED
|
@@ -2,7 +2,7 @@ const LAYOUTS = new Set(["main-vertical", "main-horizontal", "tiled", "even-hori
|
|
|
2
2
|
const CLOSE_MODES = new Set(["auto", "keep"]);
|
|
3
3
|
const PANE_COMMANDS = new Set(["mini", "tui"]);
|
|
4
4
|
export function parseArgs(argv) {
|
|
5
|
-
const out = { flags: {}, help: false, version: false, watch: false, passThrough: [] };
|
|
5
|
+
const out = { flags: {}, help: false, version: false, watch: false, scope: [], passThrough: [] };
|
|
6
6
|
for (let i = 0; i < argv.length; i++) {
|
|
7
7
|
const arg = argv[i];
|
|
8
8
|
if (arg === "--mux-watch") {
|
|
@@ -94,6 +94,16 @@ export function parseArgs(argv) {
|
|
|
94
94
|
out.flags.sessionName = v;
|
|
95
95
|
break;
|
|
96
96
|
}
|
|
97
|
+
case "--mux-scope": {
|
|
98
|
+
// Internal: the launcher forwards the directories this instance may be
|
|
99
|
+
// showing (cwd + an explicit opencode <directory> argument) to the
|
|
100
|
+
// watcher child, which no longer sees the pass-through args.
|
|
101
|
+
const v = value();
|
|
102
|
+
if (v === null || v === "")
|
|
103
|
+
throw new Error("opencode-mux: --mux-scope requires a path");
|
|
104
|
+
out.scope.push(v);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
97
107
|
default:
|
|
98
108
|
if (name.startsWith("--mux-")) {
|
|
99
109
|
throw new Error(`opencode-mux: unknown flag ${name}`);
|
package/dist/args.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"args.js","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"args.js","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
CHANGED
|
@@ -43,16 +43,67 @@ export declare function parseMajorVersion(version: string): number | null;
|
|
|
43
43
|
* Returns null for unsupported or unparseable versions.
|
|
44
44
|
*/
|
|
45
45
|
export declare function serviceGeneration(version: string): ServiceGeneration | null;
|
|
46
|
+
/** Project tag for auto-named sessions: tmux rejects spaces, dots and colons. */
|
|
47
|
+
export declare function sessionNameSuffix(cwd: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* tmux session names for successive attempts: the configured base name first,
|
|
50
|
+
* then "<base>-<project>", then "<base>-<project>-2, -3, …".
|
|
51
|
+
*
|
|
52
|
+
* Later attempts only matter when the earlier names are taken — by another
|
|
53
|
+
* opencode-mux instance running concurrently, or by a session left over from a
|
|
54
|
+
* crash. Colliding instead of sharing is deliberate: two TUIs must not share
|
|
55
|
+
* one window, and each instance's session is independent — nothing kills it,
|
|
56
|
+
* it simply ends when its own TUI pane exits (AGENTS.md invariant 7).
|
|
57
|
+
*/
|
|
58
|
+
export declare function sessionNameCandidate(base: string, cwd: string, attempt: number): string;
|
|
59
|
+
/** True when tmux refused `new-session` because the session name is taken. */
|
|
60
|
+
export declare function isSessionNameTaken(result: SpawnResult): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Directories this instance may be showing: its working directory plus at most
|
|
63
|
+
* one explicit opencode `<directory>` argument from the pass-through args
|
|
64
|
+
* (opencode chdirs there, so its sessions belong to that directory, not our
|
|
65
|
+
* cwd). Used to keep concurrent instances' subagents in their own windows.
|
|
66
|
+
*
|
|
67
|
+
* Only a *positional* argument counts as that directory: it must not start
|
|
68
|
+
* with "-" **and** its predecessor must not be a flag token. We cannot know
|
|
69
|
+
* which opencode flags take a value, so `--some-opt /tmp/other /real/project`
|
|
70
|
+
* must treat `/tmp/other` as that flag's value (treating it as the directory
|
|
71
|
+
* would silently scope the watcher to a foreign project — hiding our own
|
|
72
|
+
* subagents *and* paning theirs). opencode's `<directory>` is documented as a
|
|
73
|
+
* positional argument; if it could legitimately follow a flag, that flag would
|
|
74
|
+
* need an explicit rule here rather than widening this one.
|
|
75
|
+
*
|
|
76
|
+
* Only the *first* accepted directory is kept: any other positional is far
|
|
77
|
+
* more likely to be a prompt that happens to name a path, and adding it would
|
|
78
|
+
* widen the watcher's scope again.
|
|
79
|
+
*/
|
|
80
|
+
export declare function scopeDirectories(passThrough: string[], cwd: string): string[];
|
|
46
81
|
/** Re-emits resolved CLI flags as mux argv (for the watcher child process). */
|
|
47
82
|
export declare function flagsToArgv(flags: CliFlags): string[];
|
|
48
83
|
/** argv for the spawned watcher process: marker + config path + explicit flags. */
|
|
49
84
|
export declare function buildWatcherArgv(opts: {
|
|
50
85
|
configPath?: string;
|
|
51
86
|
flags: CliFlags;
|
|
87
|
+
scope?: string[];
|
|
52
88
|
}): string[];
|
|
53
89
|
/** argv the re-exec'd instance runs inside the fresh tmux session. */
|
|
54
90
|
export declare function buildReexecArgv(entryPath: string, originalArgv: string[]): string[];
|
|
55
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* tmux new-session argv that boots the detached session and its command.
|
|
93
|
+
*
|
|
94
|
+
* `-P -F '#{session_id}'` makes tmux print the new session's id (`$0`, …), which
|
|
95
|
+
* the caller uses as an exact `-t` target: a bare session *name* would
|
|
96
|
+
* prefix-match a sibling instance's session if ours disappeared in the
|
|
97
|
+
* meantime, and tmux session ids are exact by construction.
|
|
98
|
+
*
|
|
99
|
+
* Ownership is deliberately NOT propagated through the tmux session
|
|
100
|
+
* environment: `-e` would leak into every process in the session (and into any
|
|
101
|
+
* nested opencode-mux run), which would then kill the host session on exit.
|
|
102
|
+
* There is no ownership marker at all: the session owns itself and ends when
|
|
103
|
+
* its TUI pane exits, so no session is ever killed explicitly (AGENTS.md
|
|
104
|
+
* invariant 7 — a kill would also destroy user windows and can resolve to the
|
|
105
|
+
* wrong session).
|
|
106
|
+
*/
|
|
56
107
|
export declare function buildNewSessionCommand(input: {
|
|
57
108
|
entryPath: string;
|
|
58
109
|
originalArgv: string[];
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { appendFileSync, closeSync, mkdirSync, openSync } from "node:fs";
|
|
2
|
+
import { appendFileSync, closeSync, mkdirSync, openSync, statSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
5
|
import { parentFromPassThrough, parseArgs } from "./args.js";
|
|
6
6
|
import { loadConfig } from "./config.js";
|
|
7
7
|
import { nodeExec } from "./exec.js";
|
|
@@ -95,7 +95,8 @@ export function usageText() {
|
|
|
95
95
|
" --pane-command <cmd> pane frontend: mini | tui (default mini)",
|
|
96
96
|
" --grace <seconds> close delay after a session finishes (default 1)",
|
|
97
97
|
" --parent <id> only watch children of this session",
|
|
98
|
-
" --session-name <name> tmux session
|
|
98
|
+
" --session-name <name> base name for the owned tmux session (default mux;",
|
|
99
|
+
" suffixed with the project when already taken)",
|
|
99
100
|
" -h, --help show this help and exit",
|
|
100
101
|
" --version print the version and exit",
|
|
101
102
|
"",
|
|
@@ -129,6 +130,94 @@ export function serviceGeneration(version) {
|
|
|
129
130
|
const minor = Number(match[2]);
|
|
130
131
|
return minor === 0 && Number(match[3]) < 6 ? "legacy" : "new";
|
|
131
132
|
}
|
|
133
|
+
/** tmux session name candidates tried before giving up (concurrent instances). */
|
|
134
|
+
const MAX_SESSION_NAME_ATTEMPTS = 20;
|
|
135
|
+
/** Project tag for auto-named sessions: tmux rejects spaces, dots and colons. */
|
|
136
|
+
export function sessionNameSuffix(cwd) {
|
|
137
|
+
const cleaned = basename(cwd)
|
|
138
|
+
.replace(/[^A-Za-z0-9_-]+/g, "-")
|
|
139
|
+
.replace(/^-+|-+$/g, "")
|
|
140
|
+
.slice(0, 24);
|
|
141
|
+
return cleaned === "" ? "session" : cleaned;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* tmux session names for successive attempts: the configured base name first,
|
|
145
|
+
* then "<base>-<project>", then "<base>-<project>-2, -3, …".
|
|
146
|
+
*
|
|
147
|
+
* Later attempts only matter when the earlier names are taken — by another
|
|
148
|
+
* opencode-mux instance running concurrently, or by a session left over from a
|
|
149
|
+
* crash. Colliding instead of sharing is deliberate: two TUIs must not share
|
|
150
|
+
* one window, and each instance's session is independent — nothing kills it,
|
|
151
|
+
* it simply ends when its own TUI pane exits (AGENTS.md invariant 7).
|
|
152
|
+
*/
|
|
153
|
+
export function sessionNameCandidate(base, cwd, attempt) {
|
|
154
|
+
if (attempt <= 0)
|
|
155
|
+
return base;
|
|
156
|
+
const tagged = `${base}-${sessionNameSuffix(cwd)}`;
|
|
157
|
+
return attempt === 1 ? tagged : `${tagged}-${attempt}`;
|
|
158
|
+
}
|
|
159
|
+
/** True when tmux refused `new-session` because the session name is taken. */
|
|
160
|
+
export function isSessionNameTaken(result) {
|
|
161
|
+
return /duplicate session|already exists/i.test(`${result.stderr}\n${result.error ?? ""}`);
|
|
162
|
+
}
|
|
163
|
+
/** A tmux session id as printed by `-P -F '#{session_id}'` (`$0`, `$12`, …). */
|
|
164
|
+
const SESSION_ID_PATTERN = /^\$\d+$/;
|
|
165
|
+
/**
|
|
166
|
+
* The session id tmux printed at creation, or null when stdout is not exactly
|
|
167
|
+
* an id. Only trailing whitespace is ignored: leading junk, extra lines or
|
|
168
|
+
* extra text mean the format was not what we asked for, and proximity matters
|
|
169
|
+
* (a stray `$3` inside noise is not trustworthy). Measured on tmux 3.8 with an
|
|
170
|
+
* isolated server: an unsupported -F format prints *empty* stdout with rc 0,
|
|
171
|
+
* so both "no output" and "junk output" are reachable with a successful
|
|
172
|
+
* new-session.
|
|
173
|
+
*/
|
|
174
|
+
function printedSessionId(stdout) {
|
|
175
|
+
const printed = stdout.trimEnd();
|
|
176
|
+
return SESSION_ID_PATTERN.test(printed) ? printed : null;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Directories this instance may be showing: its working directory plus at most
|
|
180
|
+
* one explicit opencode `<directory>` argument from the pass-through args
|
|
181
|
+
* (opencode chdirs there, so its sessions belong to that directory, not our
|
|
182
|
+
* cwd). Used to keep concurrent instances' subagents in their own windows.
|
|
183
|
+
*
|
|
184
|
+
* Only a *positional* argument counts as that directory: it must not start
|
|
185
|
+
* with "-" **and** its predecessor must not be a flag token. We cannot know
|
|
186
|
+
* which opencode flags take a value, so `--some-opt /tmp/other /real/project`
|
|
187
|
+
* must treat `/tmp/other` as that flag's value (treating it as the directory
|
|
188
|
+
* would silently scope the watcher to a foreign project — hiding our own
|
|
189
|
+
* subagents *and* paning theirs). opencode's `<directory>` is documented as a
|
|
190
|
+
* positional argument; if it could legitimately follow a flag, that flag would
|
|
191
|
+
* need an explicit rule here rather than widening this one.
|
|
192
|
+
*
|
|
193
|
+
* Only the *first* accepted directory is kept: any other positional is far
|
|
194
|
+
* more likely to be a prompt that happens to name a path, and adding it would
|
|
195
|
+
* widen the watcher's scope again.
|
|
196
|
+
*/
|
|
197
|
+
export function scopeDirectories(passThrough, cwd) {
|
|
198
|
+
const dirs = [cwd];
|
|
199
|
+
for (let i = 0; i < passThrough.length; i++) {
|
|
200
|
+
const arg = passThrough[i];
|
|
201
|
+
if (arg.startsWith("-"))
|
|
202
|
+
continue;
|
|
203
|
+
// A token after a flag token may be that flag's value, never a positional.
|
|
204
|
+
const previous = passThrough[i - 1];
|
|
205
|
+
if (previous !== undefined && previous.startsWith("-"))
|
|
206
|
+
continue;
|
|
207
|
+
try {
|
|
208
|
+
if (!statSync(arg).isDirectory())
|
|
209
|
+
continue;
|
|
210
|
+
const dir = resolve(cwd, arg);
|
|
211
|
+
if (!dirs.includes(dir))
|
|
212
|
+
dirs.push(dir);
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// not an existing path (a prompt, a value, …)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return dirs;
|
|
220
|
+
}
|
|
132
221
|
/** Re-emits resolved CLI flags as mux argv (for the watcher child process). */
|
|
133
222
|
export function flagsToArgv(flags) {
|
|
134
223
|
const argv = [];
|
|
@@ -154,19 +243,36 @@ export function buildWatcherArgv(opts) {
|
|
|
154
243
|
"--mux-watch",
|
|
155
244
|
...(opts.configPath !== undefined ? ["--config", opts.configPath] : []),
|
|
156
245
|
...flagsToArgv(opts.flags),
|
|
246
|
+
...(opts.scope ?? []).flatMap((dir) => ["--mux-scope", dir]),
|
|
157
247
|
];
|
|
158
248
|
}
|
|
159
249
|
/** argv the re-exec'd instance runs inside the fresh tmux session. */
|
|
160
250
|
export function buildReexecArgv(entryPath, originalArgv) {
|
|
161
251
|
return [entryPath, ...originalArgv];
|
|
162
252
|
}
|
|
163
|
-
/**
|
|
253
|
+
/**
|
|
254
|
+
* tmux new-session argv that boots the detached session and its command.
|
|
255
|
+
*
|
|
256
|
+
* `-P -F '#{session_id}'` makes tmux print the new session's id (`$0`, …), which
|
|
257
|
+
* the caller uses as an exact `-t` target: a bare session *name* would
|
|
258
|
+
* prefix-match a sibling instance's session if ours disappeared in the
|
|
259
|
+
* meantime, and tmux session ids are exact by construction.
|
|
260
|
+
*
|
|
261
|
+
* Ownership is deliberately NOT propagated through the tmux session
|
|
262
|
+
* environment: `-e` would leak into every process in the session (and into any
|
|
263
|
+
* nested opencode-mux run), which would then kill the host session on exit.
|
|
264
|
+
* There is no ownership marker at all: the session owns itself and ends when
|
|
265
|
+
* its TUI pane exits, so no session is ever killed explicitly (AGENTS.md
|
|
266
|
+
* invariant 7 — a kill would also destroy user windows and can resolve to the
|
|
267
|
+
* wrong session).
|
|
268
|
+
*/
|
|
164
269
|
export function buildNewSessionCommand(input) {
|
|
165
270
|
return [
|
|
166
271
|
"new-session",
|
|
167
272
|
"-d",
|
|
168
|
-
"-
|
|
169
|
-
"
|
|
273
|
+
"-P",
|
|
274
|
+
"-F",
|
|
275
|
+
"#{session_id}",
|
|
170
276
|
"-s",
|
|
171
277
|
input.sessionName,
|
|
172
278
|
"-c",
|
|
@@ -174,11 +280,35 @@ export function buildNewSessionCommand(input) {
|
|
|
174
280
|
...buildReexecArgv(input.entryPath, input.originalArgv).map(shellQuote),
|
|
175
281
|
];
|
|
176
282
|
}
|
|
283
|
+
/** First `-s`/`--session`/`--session=<v>` value in the pass-through args, else null. */
|
|
284
|
+
function sessionPinValue(passThrough) {
|
|
285
|
+
for (let i = 0; i < passThrough.length; i++) {
|
|
286
|
+
const arg = passThrough[i];
|
|
287
|
+
if (arg === "-s" || arg === "--session") {
|
|
288
|
+
const next = passThrough[i + 1];
|
|
289
|
+
if (next !== undefined && !next.startsWith("-"))
|
|
290
|
+
return next;
|
|
291
|
+
}
|
|
292
|
+
if (arg.startsWith("--session="))
|
|
293
|
+
return arg.slice("--session=".length);
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
177
297
|
async function resolveConfig(parsed, env) {
|
|
178
298
|
if (parsed.flags.parent === undefined) {
|
|
179
299
|
const inferred = parentFromPassThrough(parsed.passThrough);
|
|
180
|
-
if (inferred !== null)
|
|
300
|
+
if (inferred !== null) {
|
|
181
301
|
parsed.flags.parent = inferred;
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
// Without a usable parent the instance watches the whole project. Say so
|
|
305
|
+
// instead of silently widening: a typo'd session id looks like "it just
|
|
306
|
+
// watches everything". Never fatal, never changes the exit code.
|
|
307
|
+
const ignored = sessionPinValue(parsed.passThrough);
|
|
308
|
+
if (ignored !== null && ignored !== "") {
|
|
309
|
+
process.stderr.write(`opencode-mux: ignoring -s value "${ignored}": not an opencode session id (watching this project instead)\n`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
182
312
|
}
|
|
183
313
|
return loadConfig({ path: parsed.configPath, flags: parsed.flags, env });
|
|
184
314
|
}
|
|
@@ -193,7 +323,8 @@ async function runWatch(parsed, env, deps, generation) {
|
|
|
193
323
|
mkdirSync(dirname(logFile), { recursive: true });
|
|
194
324
|
const append = (line) => {
|
|
195
325
|
try {
|
|
196
|
-
|
|
326
|
+
// Instances share one log file: pid + timestamp make every line attributable.
|
|
327
|
+
appendFileSync(logFile, `[${new Date().toISOString()}] [${process.pid}] ${line}\n`);
|
|
197
328
|
}
|
|
198
329
|
catch {
|
|
199
330
|
// log failures are non-fatal
|
|
@@ -211,6 +342,13 @@ async function runWatch(parsed, env, deps, generation) {
|
|
|
211
342
|
mainPaneSize: config.mainPaneSize,
|
|
212
343
|
themeStyles: createThemeStyles({ env, cwd: deps.cwd ?? process.cwd() }),
|
|
213
344
|
parentID: config.parent,
|
|
345
|
+
// Without an explicit parent we watch every child session; scope that to
|
|
346
|
+
// this instance's project so concurrent instances do not pane each other's
|
|
347
|
+
// subagents. The cwd is the watcher's own; extra directories (an explicit
|
|
348
|
+
// opencode <directory>) arrive via the internal --mux-scope flags.
|
|
349
|
+
projectDirectories: config.parent === null
|
|
350
|
+
? [...new Set([deps.cwd ?? process.cwd(), ...parsed.scope])]
|
|
351
|
+
: undefined,
|
|
214
352
|
paneArgv: (id) => defaultPaneArgv(id, config.paneCommand),
|
|
215
353
|
tickEveryMs: 500,
|
|
216
354
|
});
|
|
@@ -266,38 +404,102 @@ export async function main(argv, deps) {
|
|
|
266
404
|
const config = await resolveConfig(parsed, env);
|
|
267
405
|
const insideTmux = env.TMUX !== undefined && env.TMUX !== "" && env.TMUX_PANE !== undefined && env.TMUX_PANE !== "";
|
|
268
406
|
if (!insideTmux) {
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
407
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
408
|
+
// Concurrent instances each own their tmux session: when the configured
|
|
409
|
+
// name is taken (another instance, or a stale session), the next candidate
|
|
410
|
+
// is used. A failed `new-session` is the only race-free check — tmux
|
|
411
|
+
// refuses duplicate names atomically — so collisions are retried rather
|
|
412
|
+
// than probed.
|
|
413
|
+
let sessionName = null;
|
|
414
|
+
/** `-t` target for the session-scoped options; tmux resolves it as a target-pane. */
|
|
415
|
+
let optionTarget = null;
|
|
416
|
+
/** `-t` target for attach; the exact-match "=name" form is accepted here. */
|
|
417
|
+
let attachTarget = null;
|
|
418
|
+
let failure = null;
|
|
419
|
+
for (let attempt = 0; attempt < MAX_SESSION_NAME_ATTEMPTS; attempt++) {
|
|
420
|
+
const candidate = sessionNameCandidate(config.sessionName, cwd, attempt);
|
|
421
|
+
const created = await deps
|
|
422
|
+
.spawn(tmuxCommand, buildNewSessionCommand({
|
|
423
|
+
entryPath: deps.entryPath,
|
|
424
|
+
originalArgv: argv,
|
|
425
|
+
sessionName: candidate,
|
|
426
|
+
cwd,
|
|
427
|
+
}))
|
|
428
|
+
.done;
|
|
429
|
+
if (created.ok) {
|
|
430
|
+
sessionName = candidate;
|
|
431
|
+
// Exact handle for the later -t: tmux printed the session id (`$0`, …).
|
|
432
|
+
// When stdout is not a clean id (unsupported -F format → empty stdout
|
|
433
|
+
// with rc 0, or junk), fall back to the name itself. Measured on tmux
|
|
434
|
+
// 3.8 (isolated server): `set-option -t '=name' …` fails with
|
|
435
|
+
// "no such session: =name" (rc 1) because set-option resolves a
|
|
436
|
+
// *target-pane* and only a bare session name works there, while
|
|
437
|
+
// `attach -t '=name'` resolves fine. So the fallback uses the bare name
|
|
438
|
+
// for the options and "=name" for attach; the id works for both.
|
|
439
|
+
const id = printedSessionId(created.stdout);
|
|
440
|
+
optionTarget = id ?? candidate;
|
|
441
|
+
attachTarget = id ?? `=${candidate}`;
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
failure = created;
|
|
445
|
+
if (!isSessionNameTaken(created))
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
if (sessionName === null || optionTarget === null || attachTarget === null) {
|
|
449
|
+
// nodeSpawn reports `error` only when the command could not be started
|
|
450
|
+
// (ENOENT, …); a tmux that ran and failed exits non-zero with the reason
|
|
451
|
+
// on stderr (e.g. "duplicate session: mux"). Surface that instead of
|
|
452
|
+
// printing "unknown error".
|
|
453
|
+
const detail = failure === null
|
|
454
|
+
? `no free session name after ${MAX_SESSION_NAME_ATTEMPTS} attempts`
|
|
455
|
+
: (failure.error ??
|
|
456
|
+
(failure.stderr.trim() !== "" ? failure.stderr.trim() : `tmux exited with code ${failure.code ?? "unknown"}`));
|
|
457
|
+
process.stderr.write(`opencode-mux: failed to start tmux session: ${detail}\n`);
|
|
279
458
|
return 1;
|
|
280
459
|
}
|
|
460
|
+
if (sessionName !== config.sessionName) {
|
|
461
|
+
process.stderr.write(`opencode-mux: tmux session "${config.sessionName}" is taken; using "${sessionName}"\n`);
|
|
462
|
+
}
|
|
281
463
|
// Session-scoped appearance, so the mux looks coherent regardless of the
|
|
282
464
|
// user's global config. These touch only our own session — never -g.
|
|
283
465
|
// Failures are non-fatal: attach proceeds and the exit code is unchanged.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
466
|
+
// The target is the session id captured at creation whenever tmux printed
|
|
467
|
+
// one: exact by construction, so it can never prefix-match a sibling. With
|
|
468
|
+
// no usable id the options target the bare session name (the only form
|
|
469
|
+
// `set-option` resolves for a session) and attach uses the exact-match
|
|
470
|
+
// "=name" form; a failing set-option there stays non-fatal, as always.
|
|
471
|
+
await deps.spawn(tmuxCommand, ["set-option", "-t", optionTarget, "status", "off"]).done;
|
|
472
|
+
await deps.spawn(tmuxCommand, ["set-option", "-t", optionTarget, "mouse", "on"]).done;
|
|
473
|
+
const attached = await deps.spawn(tmuxCommand, ["attach", "-t", attachTarget], { inherit: true }).done;
|
|
474
|
+
// No cleanup here. `attach` returns on TUI exit *and* on detach
|
|
475
|
+
// (prefix+d), and a detached session must keep running. Nothing kills the
|
|
476
|
+
// session at all: it ends by itself when its TUI pane exits.
|
|
287
477
|
return attached.ok ? 0 : attached.code ?? 1;
|
|
288
478
|
}
|
|
289
479
|
const logFile = watchLogPath(env);
|
|
290
480
|
mkdirSync(dirname(logFile), { recursive: true });
|
|
291
|
-
const
|
|
481
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
482
|
+
const watcher = deps.spawn(nodeCommand, [
|
|
483
|
+
deps.entryPath,
|
|
484
|
+
...buildWatcherArgv({
|
|
485
|
+
configPath: parsed.configPath,
|
|
486
|
+
flags: parsed.flags,
|
|
487
|
+
// Openers other than the cwd: the watcher cannot see the pass-through
|
|
488
|
+
// args, so forward the directories it must consider ours.
|
|
489
|
+
scope: scopeDirectories(parsed.passThrough, cwd).filter((dir) => dir !== cwd),
|
|
490
|
+
}),
|
|
491
|
+
], {
|
|
292
492
|
stdoutFile: logFile,
|
|
293
493
|
stderrFile: logFile,
|
|
294
494
|
});
|
|
295
495
|
const tui = await deps.spawn(opencodeCommand, parsed.passThrough, { inherit: true }).done;
|
|
296
496
|
watcher.kill();
|
|
297
497
|
await watcher.done;
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
498
|
+
// Never kill a session: the session terminates by itself when its TUI pane
|
|
499
|
+
// exits, this instance may be living in the user's own session, and an
|
|
500
|
+
// id-resolving kill could even target the host it runs in (without
|
|
501
|
+
// TMUX_PANE, `display-message '#{session_id}'` means tmux's "current"
|
|
502
|
+
// session). Killing would also destroy windows the user opened here.
|
|
301
503
|
return tui.ok ? 0 : tui.code ?? 1;
|
|
302
504
|
}
|
|
303
505
|
//# sourceMappingURL=cli.js.map
|