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 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.2.0`)
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>` | Owned tmux session name (when starting outside tmux) | `mux` |
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. When the main TUI
118
- exits, the owned session is cleaned up.
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. Zero tmux calls
151
- while nothing changed.
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) and session options with `-t <session>` (status/mouse).
157
- No `-g`, no writes to `~/.tmux.conf`, no changes to your opencode configs.
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":"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"}
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
- /** tmux new-session argv that boots the detached session and its command. */
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 name (default mux)",
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
- /** tmux new-session argv that boots the detached session and its command. */
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
- "-e",
169
- "MUX_OWNED_SESSION=1",
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
- appendFileSync(logFile, `[${new Date().toISOString()}] ${line}\n`);
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 created = await deps
270
- .spawn(tmuxCommand, buildNewSessionCommand({
271
- entryPath: deps.entryPath,
272
- originalArgv: argv,
273
- sessionName: config.sessionName,
274
- cwd: deps.cwd ?? process.cwd(),
275
- }))
276
- .done;
277
- if (!created.ok) {
278
- process.stderr.write(`opencode-mux: failed to start tmux session: ${created.error ?? "unknown error"}\n`);
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
- await deps.spawn(tmuxCommand, ["set-option", "-t", config.sessionName, "status", "off"]).done;
285
- await deps.spawn(tmuxCommand, ["set-option", "-t", config.sessionName, "mouse", "on"]).done;
286
- const attached = await deps.spawn(tmuxCommand, ["attach", "-t", config.sessionName], { inherit: true }).done;
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 watcher = deps.spawn(nodeCommand, [deps.entryPath, ...buildWatcherArgv({ configPath: parsed.configPath, flags: parsed.flags })], {
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
- if (env.MUX_OWNED_SESSION === "1") {
299
- await deps.spawn(tmuxCommand, ["kill-session", "-t", config.sessionName]).done;
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