opencode-zellij 0.0.7 → 0.0.9

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/dist/index.mjs CHANGED
@@ -1,14 +1,14 @@
1
1
  import process from "node:process";
2
2
  import { execFile, spawn, spawnSync } from "node:child_process";
3
- import { randomUUID } from "node:crypto";
4
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
- import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
4
+ import { readFile, rename, rm } from "node:fs/promises";
6
5
  import path, { basename, dirname, join } from "node:path";
7
6
  import { fileURLToPath } from "node:url";
8
7
  import { promisify } from "node:util";
9
8
  import { homedir, tmpdir } from "node:os";
10
9
  import { parseJSON, parseJSONC } from "confbox";
11
10
  import { z } from "zod";
11
+ import { randomUUID } from "node:crypto";
12
12
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
13
13
  import { tool } from "@opencode-ai/plugin";
14
14
  import { Buffer } from "node:buffer";
@@ -18,19 +18,20 @@ function debug(message, ...details) {
18
18
  console.warn(`[opencode-zellij] ${message}`, ...details);
19
19
  }
20
20
  //#endregion
21
+ //#region src/utils/errors.ts
22
+ function errorMessage(error) {
23
+ return error instanceof Error ? error.message : String(error);
24
+ }
25
+ //#endregion
21
26
  //#region src/auto-update.ts
22
27
  const PACKAGE_NAME = "opencode-zellij";
23
28
  const NPM_REGISTRY_URL = "https://registry.npmjs.org/-/package/opencode-zellij/dist-tags";
24
29
  const FETCH_TIMEOUT_MS = 5e3;
25
30
  const INSTALL_TIMEOUT_MS = 6e4;
26
- const LOCK_STALE_MS = INSTALL_TIMEOUT_MS * 2 + FETCH_TIMEOUT_MS + 3e4;
27
31
  const defaultExecFile = promisify(execFile);
28
32
  function packageDir(installRoot) {
29
33
  return join(installRoot, "node_modules", PACKAGE_NAME);
30
34
  }
31
- function lockDir(installRoot) {
32
- return join(installRoot, ".opencode-zellij-update.lock");
33
- }
34
35
  function backupDir(installRoot) {
35
36
  return join(installRoot, "node_modules", `${PACKAGE_NAME}.update-backup`);
36
37
  }
@@ -43,7 +44,9 @@ async function installedPackageMetadata(installRoot) {
43
44
  version: typeof pkg.version === "string" ? pkg.version : void 0,
44
45
  main: typeof pkg.main === "string" ? pkg.main : void 0
45
46
  };
46
- } catch {}
47
+ } catch (error) {
48
+ debug("installedPackageMetadata failed", errorMessage(error));
49
+ }
47
50
  }
48
51
  function isExpectedPackage(metadata, version) {
49
52
  return metadata?.name === "opencode-zellij" && metadata.version === version;
@@ -64,52 +67,6 @@ async function removeInstalledPackage(installRoot) {
64
67
  recursive: true
65
68
  });
66
69
  }
67
- async function installRootLockIsStale(installRoot) {
68
- try {
69
- const content = await readFile(join(lockDir(installRoot), "owner.json"), "utf8");
70
- const owner = JSON.parse(content);
71
- if (isRecord$1(owner) && typeof owner.createdAt === "number") return Date.now() - owner.createdAt > LOCK_STALE_MS;
72
- } catch {
73
- return true;
74
- }
75
- return false;
76
- }
77
- async function acquireInstallLock(installRoot) {
78
- const dir = lockDir(installRoot);
79
- const token = randomUUID();
80
- try {
81
- await mkdir(dir);
82
- } catch {
83
- if (!await installRootLockIsStale(installRoot)) return void 0;
84
- await rm(dir, {
85
- force: true,
86
- recursive: true
87
- });
88
- try {
89
- await mkdir(dir);
90
- } catch {
91
- return;
92
- }
93
- }
94
- await writeFile(join(dir, "owner.json"), JSON.stringify({
95
- pid: process.pid,
96
- token,
97
- createdAt: Date.now()
98
- }));
99
- return async () => {
100
- try {
101
- const content = await readFile(join(dir, "owner.json"), "utf8");
102
- const owner = JSON.parse(content);
103
- if (isRecord$1(owner) && owner.token !== token) return;
104
- } catch {
105
- return;
106
- }
107
- await rm(dir, {
108
- force: true,
109
- recursive: true
110
- });
111
- };
112
- }
113
70
  async function backupInstalledPackage(installRoot) {
114
71
  const source = packageDir(installRoot);
115
72
  if (!existsSync(source)) return void 0;
@@ -158,7 +115,9 @@ async function findInstallContext(importMetaUrl) {
158
115
  currentVersion: pkg.version
159
116
  };
160
117
  }
161
- } catch {}
118
+ } catch (error) {
119
+ debug("findInstallContext package.json read failed", errorMessage(error));
120
+ }
162
121
  }
163
122
  const parent = dirname(dir);
164
123
  if (parent === dir) break;
@@ -253,48 +212,36 @@ async function checkAndUpdate(options) {
253
212
  reason: `cache spec is pinned or unknown (${context.cacheSpec})`
254
213
  };
255
214
  }
256
- const releaseLock = await acquireInstallLock(context.installRoot);
257
- if (!releaseLock) {
258
- debug(`skipping auto-update: update already in progress for ${context.installRoot}`);
215
+ const latest = await fetchLatestVersion(options.fetchImpl);
216
+ if (!latest) {
217
+ debug("skipping auto-update: could not determine latest version");
259
218
  return {
260
219
  type: "skipped",
261
- reason: "update already in progress"
220
+ reason: "could not determine latest version"
262
221
  };
263
222
  }
264
- try {
265
- const latest = await fetchLatestVersion(options.fetchImpl);
266
- if (!latest) {
267
- debug("skipping auto-update: could not determine latest version");
268
- return {
269
- type: "skipped",
270
- reason: "could not determine latest version"
271
- };
272
- }
273
- const installedVersion = (await installedPackageMetadata(context.installRoot))?.version ?? context.currentVersion;
274
- if (latest === installedVersion) {
275
- debug(`auto-update: already on latest ${latest}`);
276
- return {
277
- type: "up-to-date",
278
- currentVersion: installedVersion
279
- };
280
- }
281
- if (await runNpmInstall(context.installRoot, latest, options.execImpl)) {
282
- debug(`updated ${PACKAGE_NAME} from ${installedVersion} to ${latest}`);
283
- return {
284
- type: "updated",
285
- fromVersion: installedVersion,
286
- toVersion: latest
287
- };
288
- }
223
+ const installedVersion = (await installedPackageMetadata(context.installRoot))?.version ?? context.currentVersion;
224
+ if (latest === installedVersion) {
225
+ debug(`auto-update: already on latest ${latest}`);
289
226
  return {
290
- type: "failed",
291
- currentVersion: installedVersion,
292
- latestVersion: latest,
293
- reason: "npm install failed"
227
+ type: "up-to-date",
228
+ currentVersion: installedVersion
294
229
  };
295
- } finally {
296
- await releaseLock();
297
230
  }
231
+ if (await runNpmInstall(context.installRoot, latest, options.execImpl)) {
232
+ debug(`updated ${PACKAGE_NAME} from ${installedVersion} to ${latest}`);
233
+ return {
234
+ type: "updated",
235
+ fromVersion: installedVersion,
236
+ toVersion: latest
237
+ };
238
+ }
239
+ return {
240
+ type: "failed",
241
+ currentVersion: installedVersion,
242
+ latestVersion: latest,
243
+ reason: "npm install failed"
244
+ };
298
245
  }
299
246
  //#endregion
300
247
  //#region src/config.ts
@@ -316,7 +263,7 @@ const ptyLayerSchema = z.object({
316
263
  enabled: z.boolean().optional().describe("Enable Zellij-backed PTY tools."),
317
264
  sudoPane: sudoPaneSchema.optional().describe("Controls whether the sudo pane tool is available, denied, or hidden.")
318
265
  }).strict();
319
- const autoUpdateLayerSchema = z.object({ enabled: z.boolean().optional().describe("Enable automatic update checks for the opencode-zellij plugin.") }).strict();
266
+ const autoUpdateLayerSchema = z.boolean().optional().describe("Enable automatic update checks for the opencode-zellij plugin.");
320
267
  const sidecarConfigSchema = z.object({
321
268
  $schema: z.string().optional().describe("JSON Schema URI for editor completion."),
322
269
  tabTitle: tabTitleLayerSchema.optional(),
@@ -336,7 +283,7 @@ const defaultConfig = {
336
283
  enabled: true,
337
284
  sudoPane: "allow"
338
285
  },
339
- autoUpdate: { enabled: true }
286
+ autoUpdate: true
340
287
  };
341
288
  function validConfigLayer(value) {
342
289
  const result = sidecarConfigSchema.safeParse(value);
@@ -361,7 +308,7 @@ function mergeConfig(user, project) {
361
308
  enabled: project?.pty?.enabled ?? user?.pty?.enabled ?? defaultConfig.pty.enabled,
362
309
  sudoPane: project?.pty?.sudoPane ?? user?.pty?.sudoPane ?? defaultConfig.pty.sudoPane
363
310
  },
364
- autoUpdate: { enabled: project?.autoUpdate?.enabled ?? user?.autoUpdate?.enabled ?? defaultConfig.autoUpdate.enabled }
311
+ autoUpdate: project?.autoUpdate ?? user?.autoUpdate ?? defaultConfig.autoUpdate
365
312
  };
366
313
  }
367
314
  async function loadConfigLayer(directory, warnings) {
@@ -416,80 +363,13 @@ async function loadConfig(input) {
416
363
  };
417
364
  }
418
365
  //#endregion
419
- //#region src/utils/shell-args.ts
420
- const directCommandExitWrapper = "token=\"$1\"; shift; set +e; \"$@\"; code=$?; printf \"\\n[zellij-pty:%s] exit-code=%s\\n\" \"$token\" \"$code\"; exit \"$code\"";
421
- const shellCommandExitWrapper = "token=\"$1\"; command=\"$2\"; set +e; bash -lc \"$command\"; code=$?; printf \"\\n[zellij-pty:%s] exit-code=%s\\n\" \"$token\" \"$code\"; exit \"$code\"";
422
- function buildCommandArgv(input, options = {}) {
423
- const command = input.command.trim();
424
- if (!command) throw new Error("command is required");
425
- if (options.exitCodeToken) {
426
- if (input.args && input.args.length > 0) return [
427
- "bash",
428
- "-lc",
429
- directCommandExitWrapper,
430
- "zellij-pty",
431
- options.exitCodeToken,
432
- command,
433
- ...input.args
434
- ];
435
- return [
436
- "bash",
437
- "-lc",
438
- shellCommandExitWrapper,
439
- "zellij-pty",
440
- options.exitCodeToken,
441
- command
442
- ];
443
- }
444
- if (input.args && input.args.length > 0) return [command, ...input.args];
445
- return [
446
- "bash",
447
- "-lc",
448
- command
449
- ];
366
+ //#region src/permissions/sudo-pane.ts
367
+ let sudoPaneAllowed = true;
368
+ function configureSudoPane(allowed) {
369
+ sudoPaneAllowed = allowed;
450
370
  }
451
- function commandLineForPolicy(input) {
452
- if (!input.args || input.args.length === 0) return input.command.trim();
453
- return [input.command, ...input.args].join(" ").trim();
454
- }
455
- //#endregion
456
- //#region src/permissions/policy.ts
457
- const denyPatterns = [
458
- /(^|\s)rm\s+-[^\n&;r|]*r[^\n&;|]*f\s+\//,
459
- /(^|\s)mkfs(?:\s|$)/,
460
- /(^|\s)dd\s+(?:[^\s&;|][^\n;|&]*)?\bof=\/dev\//,
461
- /:\(\)\s*\{\s*:\|:\s*&\s*\}\s*;/
462
- ];
463
- const sudoPattern = /(?:^|[\s;&|])sudo(?:[\s;&|]|$)/;
464
- let configuredDenyCommands = [];
465
- let configuredAllowCommands = [];
466
- let allowSudoPane = true;
467
- function isStringArray(value) {
468
- return Array.isArray(value) && value.every((item) => typeof item === "string");
469
- }
470
- function escapeRegex(value) {
471
- return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
472
- }
473
- function wildcardMatches(pattern, commandLine) {
474
- return new RegExp(`^${pattern.split("*").map(escapeRegex).join(".*")}$`).test(commandLine);
475
- }
476
- function configurePolicy(config) {
477
- configuredDenyCommands = [];
478
- configuredAllowCommands = [];
479
- allowSudoPane = true;
480
- if (!config || typeof config !== "object") return;
481
- const object = config;
482
- if (isStringArray(object.denyCommands)) configuredDenyCommands = object.denyCommands;
483
- if (isStringArray(object.allowCommands)) configuredAllowCommands = object.allowCommands;
484
- if (typeof object.allowSudoPane === "boolean") allowSudoPane = object.allowSudoPane;
485
- }
486
- function assertCommandAllowed(input) {
487
- const commandLine = commandLineForPolicy(input);
488
- for (const pattern of denyPatterns) if (pattern.test(commandLine)) throw new Error(`Command denied by zellij-pty policy: ${commandLine}`);
489
- for (const pattern of configuredDenyCommands) if (wildcardMatches(pattern, commandLine)) throw new Error(`Command denied by zellij-pty configured deny rule: ${commandLine}`);
490
- if (configuredAllowCommands.length > 0 && !configuredAllowCommands.some((pattern) => wildcardMatches(pattern, commandLine))) throw new Error(`Command denied by zellij-pty allow list: ${commandLine}`);
491
- if (!input.humanInputOnly && sudoPattern.test(commandLine)) throw new Error("sudo commands must use zellij_pty_request_sudo so credentials stay human-input-only and never pass through agent tool input.");
492
- if (input.humanInputOnly && !allowSudoPane) throw new Error("sudo pane is disabled by zellij-pty policy.");
371
+ function assertSudoPaneAllowed() {
372
+ if (!sudoPaneAllowed) throw new Error("sudo pane is disabled by zellij-pty config.");
493
373
  }
494
374
  //#endregion
495
375
  //#region src/utils/ids.ts
@@ -540,6 +420,9 @@ var SessionManager = class {
540
420
  if (!session) throw new Error(`Unknown zellij PTY session: ${id}`);
541
421
  return session;
542
422
  }
423
+ find(id) {
424
+ return this.sessions.get(id);
425
+ }
543
426
  list() {
544
427
  return Array.from(this.sessions.values()).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
545
428
  }
@@ -572,6 +455,39 @@ var SessionManager = class {
572
455
  };
573
456
  const sessionManager = new SessionManager();
574
457
  //#endregion
458
+ //#region src/utils/shell-args.ts
459
+ const directCommandExitWrapper = "token=\"$1\"; shift; set +e; \"$@\"; code=$?; printf \"\\n[zellij-pty:%s] exit-code=%s\\n\" \"$token\" \"$code\"; exit \"$code\"";
460
+ const shellCommandExitWrapper = "token=\"$1\"; command=\"$2\"; set +e; bash -lc \"$command\"; code=$?; printf \"\\n[zellij-pty:%s] exit-code=%s\\n\" \"$token\" \"$code\"; exit \"$code\"";
461
+ function buildCommandArgv(input, options = {}) {
462
+ const command = input.command.trim();
463
+ if (!command) throw new Error("command is required");
464
+ if (options.exitCodeToken) {
465
+ if (input.args && input.args.length > 0) return [
466
+ "bash",
467
+ "-lc",
468
+ directCommandExitWrapper,
469
+ "zellij-pty",
470
+ options.exitCodeToken,
471
+ command,
472
+ ...input.args
473
+ ];
474
+ return [
475
+ "bash",
476
+ "-lc",
477
+ shellCommandExitWrapper,
478
+ "zellij-pty",
479
+ options.exitCodeToken,
480
+ command
481
+ ];
482
+ }
483
+ if (input.args && input.args.length > 0) return [command, ...input.args];
484
+ return [
485
+ "bash",
486
+ "-lc",
487
+ command
488
+ ];
489
+ }
490
+ //#endregion
575
491
  //#region src/zellij/cli.ts
576
492
  const execFileAsync$1 = promisify(execFile);
577
493
  function zellijCommandArgs(actionArgs) {
@@ -652,7 +568,8 @@ function parseCurrentPaneTabId(listPanesJson, paneId) {
652
568
  if (!Number.isInteger(parsedPaneId)) return void 0;
653
569
  try {
654
570
  return findPaneTabId(JSON.parse(listPanesJson), parsedPaneId);
655
- } catch {
571
+ } catch (error) {
572
+ debug("parseCurrentPaneTabId failed", errorMessage(error));
656
573
  return;
657
574
  }
658
575
  }
@@ -735,6 +652,7 @@ const zellijCli = new ZellijCli();
735
652
  //#region src/zellij/pane-watchdog.ts
736
653
  const instanceId = randomUUID();
737
654
  let watchdogStarted = false;
655
+ let watchdogChild = null;
738
656
  function registryDirectory() {
739
657
  const base = process.env.XDG_RUNTIME_DIR || tmpdir();
740
658
  return path.join(base, `opencode-zellij-${process.getuid?.() ?? "user"}`);
@@ -748,7 +666,8 @@ function parseLinuxProcessStartTime(stat) {
748
666
  function linuxProcessStartTime(pid) {
749
667
  try {
750
668
  return parseLinuxProcessStartTime(readFileSync(`/proc/${pid}/stat`, "utf8"));
751
- } catch {
669
+ } catch (error) {
670
+ debug("linuxProcessStartTime failed", errorMessage(error));
752
671
  return null;
753
672
  }
754
673
  }
@@ -769,7 +688,8 @@ function readRegistry() {
769
688
  const parsed = JSON.parse(readFileSync(file, "utf8"));
770
689
  if (parsed.version !== 1 || parsed.instanceId !== instanceId || parsed.ownerPid !== process.pid || !Array.isArray(parsed.panes)) return emptyRegistry();
771
690
  return parsed;
772
- } catch {
691
+ } catch (error) {
692
+ debug("readRegistry failed", errorMessage(error));
773
693
  return emptyRegistry();
774
694
  }
775
695
  }
@@ -784,13 +704,24 @@ function writeRegistry(registry) {
784
704
  renameSync(tempFile, file);
785
705
  }
786
706
  function ensureWatchdog() {
787
- if (watchdogStarted) return;
707
+ if (watchdogStarted && watchdogChild) return;
788
708
  watchdogStarted = true;
789
- spawn("node", [watchdogRunnerPath(), watchdogRegistryPath()], {
709
+ const child = spawn(process.execPath, [watchdogRunnerPath(), watchdogRegistryPath()], {
790
710
  detached: true,
791
711
  stdio: "ignore",
792
712
  env: process.env
793
- }).unref();
713
+ });
714
+ watchdogChild = child;
715
+ child.unref();
716
+ child.on("error", () => {
717
+ watchdogStarted = false;
718
+ if (watchdogChild === child) watchdogChild = null;
719
+ });
720
+ child.on("exit", () => {
721
+ watchdogStarted = false;
722
+ if (watchdogChild === child) watchdogChild = null;
723
+ if (existsSync(watchdogRegistryPath())) ensureWatchdog();
724
+ });
794
725
  }
795
726
  function watchdogRunnerPath() {
796
727
  return fileURLToPath(new URL("./pane-watchdog-runner.mjs", import.meta.url));
@@ -806,7 +737,8 @@ function cleanupStaleWatchdogRegistries() {
806
737
  if (registry.version !== 1 || ownerStillMatches(registry)) continue;
807
738
  closeRegistryPanes(registry);
808
739
  rmSync(file, { force: true });
809
- } catch {
740
+ } catch (error) {
741
+ debug("cleanupStaleWatchdogRegistries failed", errorMessage(error));
810
742
  rmSync(file, { force: true });
811
743
  }
812
744
  }
@@ -814,7 +746,8 @@ function cleanupStaleWatchdogRegistries() {
814
746
  function ownerStillMatches(registry) {
815
747
  try {
816
748
  process.kill(registry.ownerPid, 0);
817
- } catch {
749
+ } catch (error) {
750
+ debug("ownerStillMatches kill check failed", errorMessage(error));
818
751
  return false;
819
752
  }
820
753
  return !registry.ownerStartTime || linuxProcessStartTime(registry.ownerPid) === registry.ownerStartTime;
@@ -866,7 +799,10 @@ function unregisterPaneFromWatchdog(sessionId) {
866
799
  function removeWatchdogRegistry() {
867
800
  try {
868
801
  rmSync(watchdogRegistryPath(), { force: true });
869
- } catch {}
802
+ } catch (error) {
803
+ debug("removeWatchdogRegistry failed", errorMessage(error));
804
+ }
805
+ if (!watchdogChild) watchdogStarted = false;
870
806
  }
871
807
  //#endregion
872
808
  //#region src/pty/ring-buffer.ts
@@ -1011,14 +947,26 @@ function extractRenderedLines(event) {
1011
947
  }
1012
948
  var SubscriberManager = class {
1013
949
  subscribers = /* @__PURE__ */ new Map();
950
+ startingSessions = /* @__PURE__ */ new Map();
1014
951
  constructor(sessions, maxBufferLines = Number(process.env.PTY_MAX_BUFFER_LINES ?? 5e4)) {
1015
952
  this.sessions = sessions;
1016
953
  this.maxBufferLines = maxBufferLines;
1017
954
  }
1018
955
  async start(session) {
1019
- const existing = this.subscribers.get(session.id);
1020
- if (existing?.child) return;
956
+ if (this.subscribers.get(session.id)?.child) return;
957
+ const inProgress = this.startingSessions.get(session.id);
958
+ if (inProgress) return inProgress;
1021
959
  ensureZellijTarget();
960
+ const startPromise = this.doStart(session);
961
+ this.startingSessions.set(session.id, startPromise);
962
+ try {
963
+ await startPromise;
964
+ } finally {
965
+ this.startingSessions.delete(session.id);
966
+ }
967
+ }
968
+ async doStart(session) {
969
+ const existing = this.subscribers.get(session.id);
1022
970
  const state = existing ?? {
1023
971
  child: null,
1024
972
  buffer: new RingBuffer(this.maxBufferLines),
@@ -1027,13 +975,7 @@ var SubscriberManager = class {
1027
975
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1028
976
  lastExitedAt: null
1029
977
  };
1030
- if (!existing) {
1031
- try {
1032
- state.buffer.appendSnapshot(await zellijCli.dumpScreen(session.paneId));
1033
- this.sessions.updateLineCount(session.id, state.buffer.lineCount);
1034
- } catch {}
1035
- this.subscribers.set(session.id, state);
1036
- }
978
+ if (!existing) this.subscribers.set(session.id, state);
1037
979
  const child = spawn("zellij", zellijCommandArgs([
1038
980
  "subscribe",
1039
981
  "--pane-id",
@@ -1048,14 +990,26 @@ var SubscriberManager = class {
1048
990
  "pipe"
1049
991
  ] });
1050
992
  child.stdin.end();
993
+ if (this.subscribers.get(session.id) !== state) {
994
+ child.kill("SIGTERM");
995
+ return;
996
+ }
1051
997
  state.child = child;
1052
998
  state.lastExitedAt = null;
1053
999
  child.stdout.setEncoding("utf8");
1054
- child.stdout.on("data", (chunk) => this.handleStdout(session.id, chunk));
1000
+ child.stdout.on("data", (chunk) => this.handleStdout(session.id, child, chunk));
1055
1001
  child.stderr.setEncoding("utf8");
1056
- child.stderr.on("data", (chunk) => this.handleStderr(session.id, chunk));
1057
- child.on("exit", () => this.handleSubscriberExit(session.id));
1058
- child.on("error", (error) => this.handleSubscriberError(session.id, error));
1002
+ child.stderr.on("data", (chunk) => this.handleStderr(session.id, child, chunk));
1003
+ child.on("exit", () => this.handleSubscriberExit(session.id, child));
1004
+ child.on("error", (error) => this.handleSubscriberError(session.id, child, error));
1005
+ if (!existing) try {
1006
+ const snapshot = await zellijCli.dumpScreen(session.paneId);
1007
+ if (this.subscribers.get(session.id) !== state || state.child !== child) return;
1008
+ state.buffer.appendSnapshot(snapshot);
1009
+ this.sessions.updateLineCount(session.id, state.buffer.lineCount);
1010
+ } catch (error) {
1011
+ debug("dumpScreen failed", errorMessage(error));
1012
+ }
1059
1013
  }
1060
1014
  read(sessionId, input) {
1061
1015
  const state = this.subscribers.get(sessionId);
@@ -1095,18 +1049,20 @@ var SubscriberManager = class {
1095
1049
  this.stop(sessionId);
1096
1050
  try {
1097
1051
  await zellijCli.closePane(session.paneId);
1098
- } catch {}
1052
+ } catch (error) {
1053
+ debug("closePane failed", errorMessage(error));
1054
+ }
1099
1055
  }
1100
- handleStdout(sessionId, chunk) {
1056
+ handleStdout(sessionId, child, chunk) {
1101
1057
  const state = this.subscribers.get(sessionId);
1102
- if (!state) return;
1058
+ if (!state || state.child !== child) return;
1103
1059
  const parts = `${state.stdoutRemainder}${chunk}`.split("\n");
1104
1060
  state.stdoutRemainder = parts.pop() ?? "";
1105
- for (const part of parts) this.handleJsonLine(sessionId, part);
1061
+ for (const part of parts) this.handleJsonLine(sessionId, child, part);
1106
1062
  }
1107
- handleJsonLine(sessionId, line) {
1063
+ handleJsonLine(sessionId, child, line) {
1108
1064
  const state = this.subscribers.get(sessionId);
1109
- if (!state) return;
1065
+ if (!state || state.child !== child) return;
1110
1066
  const trimmed = line.trim();
1111
1067
  if (!trimmed) return;
1112
1068
  let event;
@@ -1114,12 +1070,20 @@ var SubscriberManager = class {
1114
1070
  const parsed = JSON.parse(trimmed);
1115
1071
  if (!parsed || typeof parsed !== "object") return;
1116
1072
  event = parsed;
1117
- } catch {
1073
+ } catch (error) {
1118
1074
  state.buffer.append(trimmed);
1119
1075
  this.sessions.updateLineCount(sessionId, state.buffer.lineCount);
1076
+ debug("JSON parse of subscriber event failed, treating as raw text", errorMessage(error));
1077
+ return;
1078
+ }
1079
+ let session;
1080
+ try {
1081
+ session = this.sessions.get(sessionId);
1082
+ } catch (error) {
1083
+ this.forget(sessionId);
1084
+ debug("session lookup by id failed", errorMessage(error));
1120
1085
  return;
1121
1086
  }
1122
- const session = this.sessions.get(sessionId);
1123
1087
  const paneId = eventPaneId(event);
1124
1088
  if (paneId && paneId !== session.paneId) return;
1125
1089
  const type = eventType(event);
@@ -1147,24 +1111,29 @@ var SubscriberManager = class {
1147
1111
  return;
1148
1112
  }
1149
1113
  }
1150
- handleStderr(sessionId, chunk) {
1114
+ handleStderr(sessionId, child, chunk) {
1151
1115
  const state = this.subscribers.get(sessionId);
1152
- if (!state) return;
1116
+ if (!state || state.child !== child) return;
1153
1117
  state.stderr.push(...splitLines(chunk));
1154
1118
  if (state.stderr.length > maxStderrLines) state.stderr = state.stderr.slice(state.stderr.length - maxStderrLines);
1155
1119
  }
1156
- handleSubscriberExit(sessionId) {
1120
+ handleSubscriberExit(sessionId, child) {
1157
1121
  const state = this.subscribers.get(sessionId);
1158
1122
  if (!state) return;
1123
+ if (state.child !== child) return;
1159
1124
  state.child = null;
1160
1125
  state.lastExitedAt = (/* @__PURE__ */ new Date()).toISOString();
1161
1126
  state.stderr.push(`[zellij-pty] subscriber exited at ${state.lastExitedAt}; last buffered output is retained.`);
1162
1127
  if (state.stderr.length > maxStderrLines) state.stderr = state.stderr.slice(state.stderr.length - maxStderrLines);
1163
1128
  }
1164
- handleSubscriberError(sessionId, error) {
1129
+ handleSubscriberError(sessionId, child, error) {
1165
1130
  const state = this.subscribers.get(sessionId);
1166
- if (state) state.stderr.push(error.message);
1167
- this.sessions.updateStatus(sessionId, "unknown");
1131
+ if (state?.child === child) {
1132
+ state.stderr.push(error.message);
1133
+ state.child = null;
1134
+ state.lastExitedAt = (/* @__PURE__ */ new Date()).toISOString();
1135
+ this.sessions.updateStatus(sessionId, "unknown");
1136
+ }
1168
1137
  }
1169
1138
  };
1170
1139
  const subscriberManager = new SubscriberManager(sessionManager);
@@ -1214,7 +1183,7 @@ function validateGrep(grep) {
1214
1183
  new RegExp(grep).test("");
1215
1184
  return null;
1216
1185
  } catch (error) {
1217
- return error instanceof Error ? error.message : String(error);
1186
+ return errorMessage(error);
1218
1187
  }
1219
1188
  }
1220
1189
  function readOutputSnapshot(sessionId, options = {}) {
@@ -1327,11 +1296,12 @@ const zellijPtyReadTool = tool({
1327
1296
  next: nextAdvice(false, `Invalid grep regex: ${grepError}`),
1328
1297
  warnings: []
1329
1298
  });
1330
- if (!subscriberManager.has(session.id)) await subscriberManager.start(session);
1331
1299
  const subscriberStatus = subscriberManager.status(session.id);
1300
+ if (!subscriberStatus.hasBuffer || !subscriberStatus.active && (session.status === "running" || session.status === "unknown")) await subscriberManager.start(session);
1301
+ const statusAfterStart = subscriberManager.status(session.id);
1332
1302
  const warnings = [];
1333
1303
  if (session.humanInputOnly) warnings.push("This pane is human-input-only: agent writes are forbidden, but rendered output is visible to the agent.");
1334
- if (!subscriberStatus.active) {
1304
+ if (!statusAfterStart.active) {
1335
1305
  warnings.push("Subscriber is inactive; returned output may be stale.");
1336
1306
  if (session.status === "running") sessionManager.updateStatus(session.id, "unknown");
1337
1307
  }
@@ -1344,8 +1314,8 @@ const zellijPtyReadTool = tool({
1344
1314
  session: publicSession(session),
1345
1315
  output,
1346
1316
  next: nextAdvice(session.status !== "exited" && session.status !== "killed", nextReadReason(session.status)),
1347
- subscriberActive: subscriberStatus.active,
1348
- subscriberLastExitedAt: subscriberStatus.lastExitedAt,
1317
+ subscriberActive: statusAfterStart.active,
1318
+ subscriberLastExitedAt: statusAfterStart.lastExitedAt,
1349
1319
  subscriberErrors: subscriberManager.stderr(session.id),
1350
1320
  warnings
1351
1321
  });
@@ -1407,10 +1377,7 @@ const requestSudoTool = tool({
1407
1377
  async execute(args, context) {
1408
1378
  const cwd = context.directory;
1409
1379
  const exitCodeToken = createExitCodeToken();
1410
- for (const script of args.scripts) assertCommandAllowed({
1411
- command: script.command,
1412
- humanInputOnly: true
1413
- });
1380
+ assertSudoPaneAllowed();
1414
1381
  const command = buildReviewScript(args.summary, args.scripts);
1415
1382
  const title = createOpenCodePaneTitle("zellij_pty_request_sudo");
1416
1383
  const paneId = await zellijCli.newPane({
@@ -1536,11 +1503,6 @@ const zellijPtySpawnTool = tool({
1536
1503
  async execute(args, context) {
1537
1504
  const cwd = args.cwd ?? context.directory;
1538
1505
  const exitCodeToken = createExitCodeToken();
1539
- assertCommandAllowed({
1540
- command: args.command,
1541
- args: args.args,
1542
- humanInputOnly: false
1543
- });
1544
1506
  const grepError = args.probe?.type === "output" ? validateGrep(args.probe.grep) : null;
1545
1507
  if (grepError) throw new Error(`Invalid probe.grep regex: ${grepError}`);
1546
1508
  const title = createOpenCodePaneTitle(args.title ?? args.command);
@@ -1633,16 +1595,23 @@ const zellijPtyWriteTool = tool({
1633
1595
  session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1634
1596
  if (args.interruptAfterSeconds) {
1635
1597
  await setTimeout$1(args.interruptAfterSeconds * 1e3);
1636
- if (sessionManager.get(session.id).status === "running") {
1598
+ if (sessionManager.find(session.id)?.status === "running") {
1637
1599
  await zellijCli.sendCtrlC(session.paneId);
1638
1600
  await setTimeout$1(500);
1639
1601
  }
1640
1602
  } else await setTimeout$1(1e3);
1603
+ const warnings = [];
1604
+ let output = emptyOutputSnapshot(session.lineCount);
1605
+ try {
1606
+ output = readOutputSnapshot(session.id, { maxLines: args.maxLines });
1607
+ } catch (error) {
1608
+ warnings.push(`Session output was unavailable before the write response completed: ${errorMessage(error)}`);
1609
+ }
1641
1610
  return jsonResponse({
1642
1611
  session: publicSession(session),
1643
- output: readOutputSnapshot(session.id, { maxLines: args.maxLines }),
1612
+ output,
1644
1613
  next: nextAdvice(true, args.interruptAfterSeconds ? "Input was sent; Ctrl-C was sent after the requested interrupt timeout if the session was still running." : "Input was sent and recent output was observed."),
1645
- warnings: []
1614
+ warnings
1646
1615
  });
1647
1616
  }
1648
1617
  });
@@ -1656,11 +1625,15 @@ function cleanupPanesOnShutdown(sessions = sessionManager, subscribers = subscri
1656
1625
  for (const session of sessions.list()) {
1657
1626
  try {
1658
1627
  zellijCli.closePaneSync(session.paneId);
1659
- } catch {}
1628
+ } catch (error) {
1629
+ debug("cleanupPanesOnShutdown closePane failed", errorMessage(error));
1630
+ }
1660
1631
  subscribers.forget(session.id);
1661
1632
  try {
1662
1633
  sessions.remove(session.id);
1663
- } catch {}
1634
+ } catch (error) {
1635
+ debug("cleanupPanesOnShutdown sessions.remove failed", errorMessage(error));
1636
+ }
1664
1637
  }
1665
1638
  }
1666
1639
  function registerShutdownCleanup() {
@@ -1730,7 +1703,8 @@ async function readGitBranch(worktree) {
1730
1703
  async function getInitialBranch(worktree, readBranch = readGitBranch) {
1731
1704
  try {
1732
1705
  return (await readBranch(worktree)).trim() || void 0;
1733
- } catch {
1706
+ } catch (error) {
1707
+ debug("getInitialBranch failed", errorMessage(error));
1734
1708
  return;
1735
1709
  }
1736
1710
  }
@@ -1915,7 +1889,7 @@ var TabTitleManager = class {
1915
1889
  this.clearDebounceTimer();
1916
1890
  this.debounceTimer = setTimeout(() => {
1917
1891
  this.debounceTimer = void 0;
1918
- this.syncDesiredTitle().catch(() => {});
1892
+ this.syncDesiredTitle().catch((error) => debug("debounced tab title sync failed", errorMessage(error)));
1919
1893
  }, this.debounceMs);
1920
1894
  this.unrefTimer(this.debounceTimer);
1921
1895
  }
@@ -1947,7 +1921,7 @@ var TabTitleManager = class {
1947
1921
  this.retryAttempt += 1;
1948
1922
  this.retryTimer = setTimeout(() => {
1949
1923
  this.retryTimer = void 0;
1950
- this.syncDesiredTitle().catch(() => {});
1924
+ this.syncDesiredTitle().catch((error) => debug("retry tab title sync failed", errorMessage(error)));
1951
1925
  }, delay);
1952
1926
  this.unrefTimer(this.retryTimer);
1953
1927
  }
@@ -1989,28 +1963,41 @@ function showUpdateToast(client, result) {
1989
1963
  message: `Updated to ${result.toVersion}. Restart OpenCode to apply the changes.`,
1990
1964
  variant: "success",
1991
1965
  duration: 1e4
1992
- } }).catch(() => {});
1966
+ } }).catch((error) => debug("show update toast for successful update failed", errorMessage(error)));
1993
1967
  else if (result.type === "failed") client.tui.showToast({ body: {
1994
1968
  title: "opencode-zellij update failed",
1995
1969
  message: `Failed to update to ${result.latestVersion}.`,
1996
1970
  variant: "error",
1997
1971
  duration: 8e3
1998
- } }).catch(() => {});
1972
+ } }).catch((error) => debug("show update toast for failed update failed", errorMessage(error)));
1999
1973
  }
2000
1974
  function startAutoUpdateCheck(client, importMetaUrl, check = checkAndUpdate) {
2001
1975
  (async () => {
2002
1976
  try {
2003
1977
  showUpdateToast(client, await check({ importMetaUrl }));
2004
1978
  } catch (cause) {
2005
- debug("auto-update check failed", cause instanceof Error ? cause.message : String(cause));
1979
+ debug("auto-update check failed", errorMessage(cause));
2006
1980
  }
2007
1981
  })();
2008
1982
  }
1983
+ async function cleanupStep(stepName, sessionId, step) {
1984
+ try {
1985
+ await step();
1986
+ } catch (error) {
1987
+ debug(`session.deleted cleanup failed: ${stepName} for ${sessionId}`, errorMessage(error));
1988
+ }
1989
+ }
1990
+ async function cleanupDeletedSession(sessionId) {
1991
+ await cleanupStep("close pane", sessionId, () => subscriberManager.closeSessionPane(sessionId));
1992
+ await cleanupStep("forget subscriber", sessionId, () => subscriberManager.forget(sessionId));
1993
+ await cleanupStep("unregister watchdog", sessionId, () => unregisterPaneFromWatchdog(sessionId));
1994
+ await cleanupStep("remove session", sessionId, () => sessionManager.remove(sessionId));
1995
+ }
2009
1996
  function createZellijPtyPlugin(dependencies = {}) {
2010
1997
  return async (input) => {
2011
1998
  const { config, warnings } = await loadConfig(input);
2012
1999
  for (const warning of warnings) debug(warning);
2013
- configurePolicy({ allowSudoPane: config.pty.sudoPane === "allow" });
2000
+ configureSudoPane(config.pty.sudoPane === "allow");
2014
2001
  cleanupStaleWatchdogRegistries();
2015
2002
  registerShutdownCleanup();
2016
2003
  const workspaceRoot = getWorkspaceRoot(input);
@@ -2027,9 +2014,9 @@ function createZellijPtyPlugin(dependencies = {}) {
2027
2014
  branch: config.tabTitle.emojiBranch
2028
2015
  }
2029
2016
  }) : void 0;
2030
- tabTitleManager?.renderImmediate().catch(() => {});
2017
+ tabTitleManager?.renderImmediate().catch((error) => debug("initial tab title render failed", errorMessage(error)));
2031
2018
  const client = input.client;
2032
- if (config.autoUpdate.enabled) (dependencies.startAutoUpdateCheck ?? startAutoUpdateCheck)(client, dependencies.importMetaUrl ?? import.meta.url);
2019
+ if (config.autoUpdate) (dependencies.startAutoUpdateCheck ?? startAutoUpdateCheck)(client, dependencies.importMetaUrl ?? import.meta.url);
2033
2020
  return {
2034
2021
  async event(input) {
2035
2022
  const event = input.event;
@@ -2038,12 +2025,7 @@ function createZellijPtyPlugin(dependencies = {}) {
2038
2025
  const sessionID = deletedSessionID(event);
2039
2026
  if (!sessionID) return;
2040
2027
  const sessions = sessionManager.listByOpenCodeSession(sessionID);
2041
- await Promise.all(sessions.map(async (session) => {
2042
- await subscriberManager.closeSessionPane(session.id);
2043
- subscriberManager.forget(session.id);
2044
- unregisterPaneFromWatchdog(session.id);
2045
- sessionManager.remove(session.id);
2046
- }));
2028
+ await Promise.all(sessions.map((session) => cleanupDeletedSession(session.id)));
2047
2029
  }
2048
2030
  },
2049
2031
  tool: config.pty.enabled ? {