opencode-zellij 0.0.7 → 0.0.8

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";
@@ -23,14 +23,10 @@ const PACKAGE_NAME = "opencode-zellij";
23
23
  const NPM_REGISTRY_URL = "https://registry.npmjs.org/-/package/opencode-zellij/dist-tags";
24
24
  const FETCH_TIMEOUT_MS = 5e3;
25
25
  const INSTALL_TIMEOUT_MS = 6e4;
26
- const LOCK_STALE_MS = INSTALL_TIMEOUT_MS * 2 + FETCH_TIMEOUT_MS + 3e4;
27
26
  const defaultExecFile = promisify(execFile);
28
27
  function packageDir(installRoot) {
29
28
  return join(installRoot, "node_modules", PACKAGE_NAME);
30
29
  }
31
- function lockDir(installRoot) {
32
- return join(installRoot, ".opencode-zellij-update.lock");
33
- }
34
30
  function backupDir(installRoot) {
35
31
  return join(installRoot, "node_modules", `${PACKAGE_NAME}.update-backup`);
36
32
  }
@@ -64,52 +60,6 @@ async function removeInstalledPackage(installRoot) {
64
60
  recursive: true
65
61
  });
66
62
  }
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
63
  async function backupInstalledPackage(installRoot) {
114
64
  const source = packageDir(installRoot);
115
65
  if (!existsSync(source)) return void 0;
@@ -253,48 +203,36 @@ async function checkAndUpdate(options) {
253
203
  reason: `cache spec is pinned or unknown (${context.cacheSpec})`
254
204
  };
255
205
  }
256
- const releaseLock = await acquireInstallLock(context.installRoot);
257
- if (!releaseLock) {
258
- debug(`skipping auto-update: update already in progress for ${context.installRoot}`);
206
+ const latest = await fetchLatestVersion(options.fetchImpl);
207
+ if (!latest) {
208
+ debug("skipping auto-update: could not determine latest version");
259
209
  return {
260
210
  type: "skipped",
261
- reason: "update already in progress"
211
+ reason: "could not determine latest version"
262
212
  };
263
213
  }
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
- }
214
+ const installedVersion = (await installedPackageMetadata(context.installRoot))?.version ?? context.currentVersion;
215
+ if (latest === installedVersion) {
216
+ debug(`auto-update: already on latest ${latest}`);
289
217
  return {
290
- type: "failed",
291
- currentVersion: installedVersion,
292
- latestVersion: latest,
293
- reason: "npm install failed"
218
+ type: "up-to-date",
219
+ currentVersion: installedVersion
294
220
  };
295
- } finally {
296
- await releaseLock();
297
221
  }
222
+ if (await runNpmInstall(context.installRoot, latest, options.execImpl)) {
223
+ debug(`updated ${PACKAGE_NAME} from ${installedVersion} to ${latest}`);
224
+ return {
225
+ type: "updated",
226
+ fromVersion: installedVersion,
227
+ toVersion: latest
228
+ };
229
+ }
230
+ return {
231
+ type: "failed",
232
+ currentVersion: installedVersion,
233
+ latestVersion: latest,
234
+ reason: "npm install failed"
235
+ };
298
236
  }
299
237
  //#endregion
300
238
  //#region src/config.ts
@@ -416,80 +354,13 @@ async function loadConfig(input) {
416
354
  };
417
355
  }
418
356
  //#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
- ];
450
- }
451
- function commandLineForPolicy(input) {
452
- if (!input.args || input.args.length === 0) return input.command.trim();
453
- return [input.command, ...input.args].join(" ").trim();
357
+ //#region src/permissions/sudo-pane.ts
358
+ let sudoPaneAllowed = true;
359
+ function configureSudoPane(allowed) {
360
+ sudoPaneAllowed = allowed;
454
361
  }
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.");
362
+ function assertSudoPaneAllowed() {
363
+ if (!sudoPaneAllowed) throw new Error("sudo pane is disabled by zellij-pty config.");
493
364
  }
494
365
  //#endregion
495
366
  //#region src/utils/ids.ts
@@ -540,6 +411,9 @@ var SessionManager = class {
540
411
  if (!session) throw new Error(`Unknown zellij PTY session: ${id}`);
541
412
  return session;
542
413
  }
414
+ find(id) {
415
+ return this.sessions.get(id);
416
+ }
543
417
  list() {
544
418
  return Array.from(this.sessions.values()).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
545
419
  }
@@ -572,6 +446,39 @@ var SessionManager = class {
572
446
  };
573
447
  const sessionManager = new SessionManager();
574
448
  //#endregion
449
+ //#region src/utils/shell-args.ts
450
+ const directCommandExitWrapper = "token=\"$1\"; shift; set +e; \"$@\"; code=$?; printf \"\\n[zellij-pty:%s] exit-code=%s\\n\" \"$token\" \"$code\"; exit \"$code\"";
451
+ 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\"";
452
+ function buildCommandArgv(input, options = {}) {
453
+ const command = input.command.trim();
454
+ if (!command) throw new Error("command is required");
455
+ if (options.exitCodeToken) {
456
+ if (input.args && input.args.length > 0) return [
457
+ "bash",
458
+ "-lc",
459
+ directCommandExitWrapper,
460
+ "zellij-pty",
461
+ options.exitCodeToken,
462
+ command,
463
+ ...input.args
464
+ ];
465
+ return [
466
+ "bash",
467
+ "-lc",
468
+ shellCommandExitWrapper,
469
+ "zellij-pty",
470
+ options.exitCodeToken,
471
+ command
472
+ ];
473
+ }
474
+ if (input.args && input.args.length > 0) return [command, ...input.args];
475
+ return [
476
+ "bash",
477
+ "-lc",
478
+ command
479
+ ];
480
+ }
481
+ //#endregion
575
482
  //#region src/zellij/cli.ts
576
483
  const execFileAsync$1 = promisify(execFile);
577
484
  function zellijCommandArgs(actionArgs) {
@@ -735,6 +642,7 @@ const zellijCli = new ZellijCli();
735
642
  //#region src/zellij/pane-watchdog.ts
736
643
  const instanceId = randomUUID();
737
644
  let watchdogStarted = false;
645
+ let watchdogChild = null;
738
646
  function registryDirectory() {
739
647
  const base = process.env.XDG_RUNTIME_DIR || tmpdir();
740
648
  return path.join(base, `opencode-zellij-${process.getuid?.() ?? "user"}`);
@@ -784,13 +692,24 @@ function writeRegistry(registry) {
784
692
  renameSync(tempFile, file);
785
693
  }
786
694
  function ensureWatchdog() {
787
- if (watchdogStarted) return;
695
+ if (watchdogStarted && watchdogChild) return;
788
696
  watchdogStarted = true;
789
- spawn("node", [watchdogRunnerPath(), watchdogRegistryPath()], {
697
+ const child = spawn(process.execPath, [watchdogRunnerPath(), watchdogRegistryPath()], {
790
698
  detached: true,
791
699
  stdio: "ignore",
792
700
  env: process.env
793
- }).unref();
701
+ });
702
+ watchdogChild = child;
703
+ child.unref();
704
+ child.on("error", () => {
705
+ watchdogStarted = false;
706
+ if (watchdogChild === child) watchdogChild = null;
707
+ });
708
+ child.on("exit", () => {
709
+ watchdogStarted = false;
710
+ if (watchdogChild === child) watchdogChild = null;
711
+ if (existsSync(watchdogRegistryPath())) ensureWatchdog();
712
+ });
794
713
  }
795
714
  function watchdogRunnerPath() {
796
715
  return fileURLToPath(new URL("./pane-watchdog-runner.mjs", import.meta.url));
@@ -867,6 +786,7 @@ function removeWatchdogRegistry() {
867
786
  try {
868
787
  rmSync(watchdogRegistryPath(), { force: true });
869
788
  } catch {}
789
+ if (!watchdogChild) watchdogStarted = false;
870
790
  }
871
791
  //#endregion
872
792
  //#region src/pty/ring-buffer.ts
@@ -1011,14 +931,26 @@ function extractRenderedLines(event) {
1011
931
  }
1012
932
  var SubscriberManager = class {
1013
933
  subscribers = /* @__PURE__ */ new Map();
934
+ startingSessions = /* @__PURE__ */ new Map();
1014
935
  constructor(sessions, maxBufferLines = Number(process.env.PTY_MAX_BUFFER_LINES ?? 5e4)) {
1015
936
  this.sessions = sessions;
1016
937
  this.maxBufferLines = maxBufferLines;
1017
938
  }
1018
939
  async start(session) {
1019
- const existing = this.subscribers.get(session.id);
1020
- if (existing?.child) return;
940
+ if (this.subscribers.get(session.id)?.child) return;
941
+ const inProgress = this.startingSessions.get(session.id);
942
+ if (inProgress) return inProgress;
1021
943
  ensureZellijTarget();
944
+ const startPromise = this.doStart(session);
945
+ this.startingSessions.set(session.id, startPromise);
946
+ try {
947
+ await startPromise;
948
+ } finally {
949
+ this.startingSessions.delete(session.id);
950
+ }
951
+ }
952
+ async doStart(session) {
953
+ const existing = this.subscribers.get(session.id);
1022
954
  const state = existing ?? {
1023
955
  child: null,
1024
956
  buffer: new RingBuffer(this.maxBufferLines),
@@ -1028,11 +960,12 @@ var SubscriberManager = class {
1028
960
  lastExitedAt: null
1029
961
  };
1030
962
  if (!existing) {
963
+ this.subscribers.set(session.id, state);
1031
964
  try {
1032
965
  state.buffer.appendSnapshot(await zellijCli.dumpScreen(session.paneId));
1033
966
  this.sessions.updateLineCount(session.id, state.buffer.lineCount);
1034
967
  } catch {}
1035
- this.subscribers.set(session.id, state);
968
+ if (this.subscribers.get(session.id) !== state) return;
1036
969
  }
1037
970
  const child = spawn("zellij", zellijCommandArgs([
1038
971
  "subscribe",
@@ -1048,14 +981,18 @@ var SubscriberManager = class {
1048
981
  "pipe"
1049
982
  ] });
1050
983
  child.stdin.end();
984
+ if (this.subscribers.get(session.id) !== state) {
985
+ child.kill("SIGTERM");
986
+ return;
987
+ }
1051
988
  state.child = child;
1052
989
  state.lastExitedAt = null;
1053
990
  child.stdout.setEncoding("utf8");
1054
- child.stdout.on("data", (chunk) => this.handleStdout(session.id, chunk));
991
+ child.stdout.on("data", (chunk) => this.handleStdout(session.id, child, chunk));
1055
992
  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));
993
+ child.stderr.on("data", (chunk) => this.handleStderr(session.id, child, chunk));
994
+ child.on("exit", () => this.handleSubscriberExit(session.id, child));
995
+ child.on("error", (error) => this.handleSubscriberError(session.id, child, error));
1059
996
  }
1060
997
  read(sessionId, input) {
1061
998
  const state = this.subscribers.get(sessionId);
@@ -1097,16 +1034,16 @@ var SubscriberManager = class {
1097
1034
  await zellijCli.closePane(session.paneId);
1098
1035
  } catch {}
1099
1036
  }
1100
- handleStdout(sessionId, chunk) {
1037
+ handleStdout(sessionId, child, chunk) {
1101
1038
  const state = this.subscribers.get(sessionId);
1102
- if (!state) return;
1039
+ if (!state || state.child !== child) return;
1103
1040
  const parts = `${state.stdoutRemainder}${chunk}`.split("\n");
1104
1041
  state.stdoutRemainder = parts.pop() ?? "";
1105
- for (const part of parts) this.handleJsonLine(sessionId, part);
1042
+ for (const part of parts) this.handleJsonLine(sessionId, child, part);
1106
1043
  }
1107
- handleJsonLine(sessionId, line) {
1044
+ handleJsonLine(sessionId, child, line) {
1108
1045
  const state = this.subscribers.get(sessionId);
1109
- if (!state) return;
1046
+ if (!state || state.child !== child) return;
1110
1047
  const trimmed = line.trim();
1111
1048
  if (!trimmed) return;
1112
1049
  let event;
@@ -1119,7 +1056,13 @@ var SubscriberManager = class {
1119
1056
  this.sessions.updateLineCount(sessionId, state.buffer.lineCount);
1120
1057
  return;
1121
1058
  }
1122
- const session = this.sessions.get(sessionId);
1059
+ let session;
1060
+ try {
1061
+ session = this.sessions.get(sessionId);
1062
+ } catch {
1063
+ this.forget(sessionId);
1064
+ return;
1065
+ }
1123
1066
  const paneId = eventPaneId(event);
1124
1067
  if (paneId && paneId !== session.paneId) return;
1125
1068
  const type = eventType(event);
@@ -1147,24 +1090,29 @@ var SubscriberManager = class {
1147
1090
  return;
1148
1091
  }
1149
1092
  }
1150
- handleStderr(sessionId, chunk) {
1093
+ handleStderr(sessionId, child, chunk) {
1151
1094
  const state = this.subscribers.get(sessionId);
1152
- if (!state) return;
1095
+ if (!state || state.child !== child) return;
1153
1096
  state.stderr.push(...splitLines(chunk));
1154
1097
  if (state.stderr.length > maxStderrLines) state.stderr = state.stderr.slice(state.stderr.length - maxStderrLines);
1155
1098
  }
1156
- handleSubscriberExit(sessionId) {
1099
+ handleSubscriberExit(sessionId, child) {
1157
1100
  const state = this.subscribers.get(sessionId);
1158
1101
  if (!state) return;
1102
+ if (state.child !== child) return;
1159
1103
  state.child = null;
1160
1104
  state.lastExitedAt = (/* @__PURE__ */ new Date()).toISOString();
1161
1105
  state.stderr.push(`[zellij-pty] subscriber exited at ${state.lastExitedAt}; last buffered output is retained.`);
1162
1106
  if (state.stderr.length > maxStderrLines) state.stderr = state.stderr.slice(state.stderr.length - maxStderrLines);
1163
1107
  }
1164
- handleSubscriberError(sessionId, error) {
1108
+ handleSubscriberError(sessionId, child, error) {
1165
1109
  const state = this.subscribers.get(sessionId);
1166
- if (state) state.stderr.push(error.message);
1167
- this.sessions.updateStatus(sessionId, "unknown");
1110
+ if (state?.child === child) {
1111
+ state.stderr.push(error.message);
1112
+ state.child = null;
1113
+ state.lastExitedAt = (/* @__PURE__ */ new Date()).toISOString();
1114
+ this.sessions.updateStatus(sessionId, "unknown");
1115
+ }
1168
1116
  }
1169
1117
  };
1170
1118
  const subscriberManager = new SubscriberManager(sessionManager);
@@ -1198,6 +1146,11 @@ function jsonResponse(value) {
1198
1146
  return JSON.stringify(value, null, 2);
1199
1147
  }
1200
1148
  //#endregion
1149
+ //#region src/utils/errors.ts
1150
+ function errorMessage(error) {
1151
+ return error instanceof Error ? error.message : String(error);
1152
+ }
1153
+ //#endregion
1201
1154
  //#region src/tools/output.ts
1202
1155
  function emptyOutputSnapshot(lineCount = 0) {
1203
1156
  return {
@@ -1214,7 +1167,7 @@ function validateGrep(grep) {
1214
1167
  new RegExp(grep).test("");
1215
1168
  return null;
1216
1169
  } catch (error) {
1217
- return error instanceof Error ? error.message : String(error);
1170
+ return errorMessage(error);
1218
1171
  }
1219
1172
  }
1220
1173
  function readOutputSnapshot(sessionId, options = {}) {
@@ -1327,11 +1280,12 @@ const zellijPtyReadTool = tool({
1327
1280
  next: nextAdvice(false, `Invalid grep regex: ${grepError}`),
1328
1281
  warnings: []
1329
1282
  });
1330
- if (!subscriberManager.has(session.id)) await subscriberManager.start(session);
1331
1283
  const subscriberStatus = subscriberManager.status(session.id);
1284
+ if (!subscriberStatus.hasBuffer || !subscriberStatus.active && (session.status === "running" || session.status === "unknown")) await subscriberManager.start(session);
1285
+ const statusAfterStart = subscriberManager.status(session.id);
1332
1286
  const warnings = [];
1333
1287
  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) {
1288
+ if (!statusAfterStart.active) {
1335
1289
  warnings.push("Subscriber is inactive; returned output may be stale.");
1336
1290
  if (session.status === "running") sessionManager.updateStatus(session.id, "unknown");
1337
1291
  }
@@ -1344,8 +1298,8 @@ const zellijPtyReadTool = tool({
1344
1298
  session: publicSession(session),
1345
1299
  output,
1346
1300
  next: nextAdvice(session.status !== "exited" && session.status !== "killed", nextReadReason(session.status)),
1347
- subscriberActive: subscriberStatus.active,
1348
- subscriberLastExitedAt: subscriberStatus.lastExitedAt,
1301
+ subscriberActive: statusAfterStart.active,
1302
+ subscriberLastExitedAt: statusAfterStart.lastExitedAt,
1349
1303
  subscriberErrors: subscriberManager.stderr(session.id),
1350
1304
  warnings
1351
1305
  });
@@ -1407,10 +1361,7 @@ const requestSudoTool = tool({
1407
1361
  async execute(args, context) {
1408
1362
  const cwd = context.directory;
1409
1363
  const exitCodeToken = createExitCodeToken();
1410
- for (const script of args.scripts) assertCommandAllowed({
1411
- command: script.command,
1412
- humanInputOnly: true
1413
- });
1364
+ assertSudoPaneAllowed();
1414
1365
  const command = buildReviewScript(args.summary, args.scripts);
1415
1366
  const title = createOpenCodePaneTitle("zellij_pty_request_sudo");
1416
1367
  const paneId = await zellijCli.newPane({
@@ -1536,11 +1487,6 @@ const zellijPtySpawnTool = tool({
1536
1487
  async execute(args, context) {
1537
1488
  const cwd = args.cwd ?? context.directory;
1538
1489
  const exitCodeToken = createExitCodeToken();
1539
- assertCommandAllowed({
1540
- command: args.command,
1541
- args: args.args,
1542
- humanInputOnly: false
1543
- });
1544
1490
  const grepError = args.probe?.type === "output" ? validateGrep(args.probe.grep) : null;
1545
1491
  if (grepError) throw new Error(`Invalid probe.grep regex: ${grepError}`);
1546
1492
  const title = createOpenCodePaneTitle(args.title ?? args.command);
@@ -1633,16 +1579,23 @@ const zellijPtyWriteTool = tool({
1633
1579
  session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1634
1580
  if (args.interruptAfterSeconds) {
1635
1581
  await setTimeout$1(args.interruptAfterSeconds * 1e3);
1636
- if (sessionManager.get(session.id).status === "running") {
1582
+ if (sessionManager.find(session.id)?.status === "running") {
1637
1583
  await zellijCli.sendCtrlC(session.paneId);
1638
1584
  await setTimeout$1(500);
1639
1585
  }
1640
1586
  } else await setTimeout$1(1e3);
1587
+ const warnings = [];
1588
+ let output = emptyOutputSnapshot(session.lineCount);
1589
+ try {
1590
+ output = readOutputSnapshot(session.id, { maxLines: args.maxLines });
1591
+ } catch (error) {
1592
+ warnings.push(`Session output was unavailable before the write response completed: ${errorMessage(error)}`);
1593
+ }
1641
1594
  return jsonResponse({
1642
1595
  session: publicSession(session),
1643
- output: readOutputSnapshot(session.id, { maxLines: args.maxLines }),
1596
+ output,
1644
1597
  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: []
1598
+ warnings
1646
1599
  });
1647
1600
  }
1648
1601
  });
@@ -2002,15 +1955,28 @@ function startAutoUpdateCheck(client, importMetaUrl, check = checkAndUpdate) {
2002
1955
  try {
2003
1956
  showUpdateToast(client, await check({ importMetaUrl }));
2004
1957
  } catch (cause) {
2005
- debug("auto-update check failed", cause instanceof Error ? cause.message : String(cause));
1958
+ debug("auto-update check failed", errorMessage(cause));
2006
1959
  }
2007
1960
  })();
2008
1961
  }
1962
+ async function cleanupStep(stepName, sessionId, step) {
1963
+ try {
1964
+ await step();
1965
+ } catch (error) {
1966
+ debug(`session.deleted cleanup failed: ${stepName} for ${sessionId}`, errorMessage(error));
1967
+ }
1968
+ }
1969
+ async function cleanupDeletedSession(sessionId) {
1970
+ await cleanupStep("close pane", sessionId, () => subscriberManager.closeSessionPane(sessionId));
1971
+ await cleanupStep("forget subscriber", sessionId, () => subscriberManager.forget(sessionId));
1972
+ await cleanupStep("unregister watchdog", sessionId, () => unregisterPaneFromWatchdog(sessionId));
1973
+ await cleanupStep("remove session", sessionId, () => sessionManager.remove(sessionId));
1974
+ }
2009
1975
  function createZellijPtyPlugin(dependencies = {}) {
2010
1976
  return async (input) => {
2011
1977
  const { config, warnings } = await loadConfig(input);
2012
1978
  for (const warning of warnings) debug(warning);
2013
- configurePolicy({ allowSudoPane: config.pty.sudoPane === "allow" });
1979
+ configureSudoPane(config.pty.sudoPane === "allow");
2014
1980
  cleanupStaleWatchdogRegistries();
2015
1981
  registerShutdownCleanup();
2016
1982
  const workspaceRoot = getWorkspaceRoot(input);
@@ -2038,12 +2004,7 @@ function createZellijPtyPlugin(dependencies = {}) {
2038
2004
  const sessionID = deletedSessionID(event);
2039
2005
  if (!sessionID) return;
2040
2006
  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
- }));
2007
+ await Promise.all(sessions.map((session) => cleanupDeletedSession(session.id)));
2047
2008
  }
2048
2009
  },
2049
2010
  tool: config.pty.enabled ? {