opencode-zellij 0.0.1 → 0.0.2

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,9 +1,13 @@
1
+ import process from "node:process";
1
2
  import { randomUUID } from "node:crypto";
2
- import { setTimeout } from "node:timers/promises";
3
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
3
4
  import { tool } from "@opencode-ai/plugin";
4
- import { execFile, spawn } from "node:child_process";
5
- import process from "node:process";
5
+ import { execFile, spawn, spawnSync } from "node:child_process";
6
6
  import { promisify } from "node:util";
7
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import path from "node:path";
10
+ import { fileURLToPath } from "node:url";
7
11
  import { Buffer } from "node:buffer";
8
12
  //#region src/utils/shell-args.ts
9
13
  const directCommandExitWrapper = "token=\"$1\"; shift; set +e; \"$@\"; code=$?; printf \"\\n[zellij-pty:%s] exit-code=%s\\n\" \"$token\" \"$code\"; exit \"$code\"";
@@ -74,7 +78,7 @@ function assertCommandAllowed(input) {
74
78
  for (const pattern of denyPatterns) if (pattern.test(commandLine)) throw new Error(`Command denied by zellij-pty policy: ${commandLine}`);
75
79
  for (const pattern of configuredDenyCommands) if (wildcardMatches(pattern, commandLine)) throw new Error(`Command denied by zellij-pty configured deny rule: ${commandLine}`);
76
80
  if (configuredAllowCommands.length > 0 && !configuredAllowCommands.some((pattern) => wildcardMatches(pattern, commandLine))) throw new Error(`Command denied by zellij-pty allow list: ${commandLine}`);
77
- if (!input.humanInputOnly && sudoPattern.test(commandLine)) throw new Error("sudo commands must use request_sudo so credentials stay human-input-only and never pass through agent tool input.");
81
+ 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.");
78
82
  if (input.humanInputOnly && sudoPattern.test(commandLine) && !allowSudoPane) throw new Error("sudo pane is disabled by zellij-pty policy.");
79
83
  }
80
84
  //#endregion
@@ -159,7 +163,7 @@ var SessionManager = class {
159
163
  const sessionManager = new SessionManager();
160
164
  //#endregion
161
165
  //#region src/zellij/cli.ts
162
- const execFileAsync = promisify(execFile);
166
+ const execFileAsync$1 = promisify(execFile);
163
167
  function zellijCommandArgs(actionArgs) {
164
168
  const sessionName = process.env.ZELLIJ_SESSION_NAME?.trim();
165
169
  if (sessionName) return [
@@ -185,6 +189,13 @@ function buildNewPaneActionArgs(options) {
185
189
  args.push("--", ...buildCommandArgv(options, { exitCodeToken: options.exitCodeToken }));
186
190
  return args;
187
191
  }
192
+ function buildRenameTabActionArgs(title) {
193
+ return [
194
+ "action",
195
+ "rename-tab",
196
+ title
197
+ ];
198
+ }
188
199
  function ensureZellijTarget() {
189
200
  if (process.env.ZELLIJ || process.env.ZELLIJ_SESSION_NAME) return;
190
201
  throw new Error("Zellij context not found. Run OpenCode inside Zellij or set ZELLIJ_SESSION_NAME to an existing session.");
@@ -192,7 +203,7 @@ function ensureZellijTarget() {
192
203
  async function runZellij(actionArgs, options = {}) {
193
204
  ensureZellijTarget();
194
205
  try {
195
- const result = await execFileAsync("zellij", zellijCommandArgs(actionArgs), {
206
+ const result = await execFileAsync$1("zellij", zellijCommandArgs(actionArgs), {
196
207
  encoding: "utf8",
197
208
  timeout: options.timeoutMs ?? 1e4,
198
209
  maxBuffer: 20 * 1024 * 1024
@@ -230,6 +241,14 @@ var ZellijCli = class {
230
241
  async closePane(paneId) {
231
242
  await runZellij(zellijActionArgs("close-pane", ["--pane-id", paneId]));
232
243
  }
244
+ closePaneSync(paneId) {
245
+ ensureZellijTarget();
246
+ spawnSync("zellij", zellijCommandArgs(zellijActionArgs("close-pane", ["--pane-id", paneId])), {
247
+ encoding: "utf8",
248
+ stdio: "ignore",
249
+ timeout: 2e3
250
+ });
251
+ }
233
252
  async focusPane(paneId) {
234
253
  await runZellij(zellijActionArgs("focus-pane-id", [paneId]));
235
254
  }
@@ -240,9 +259,149 @@ var ZellijCli = class {
240
259
  "--full"
241
260
  ]), { timeoutMs: 1e4 })).stdout;
242
261
  }
262
+ async renameTab(title) {
263
+ await runZellij(buildRenameTabActionArgs(title));
264
+ }
243
265
  };
244
266
  const zellijCli = new ZellijCli();
245
267
  //#endregion
268
+ //#region src/zellij/pane-watchdog.ts
269
+ const instanceId = randomUUID();
270
+ let watchdogStarted = false;
271
+ function registryDirectory() {
272
+ const base = process.env.XDG_RUNTIME_DIR || tmpdir();
273
+ return path.join(base, `opencode-zellij-${process.getuid?.() ?? "user"}`);
274
+ }
275
+ function watchdogRegistryPath() {
276
+ return path.join(registryDirectory(), `panes-${process.pid}-${instanceId}.json`);
277
+ }
278
+ function parseLinuxProcessStartTime(stat) {
279
+ return stat.slice(stat.lastIndexOf(")") + 2).trim().split(/\s+/)[19] ?? null;
280
+ }
281
+ function linuxProcessStartTime(pid) {
282
+ try {
283
+ return parseLinuxProcessStartTime(readFileSync(`/proc/${pid}/stat`, "utf8"));
284
+ } catch {
285
+ return null;
286
+ }
287
+ }
288
+ function emptyRegistry() {
289
+ return {
290
+ version: 1,
291
+ instanceId,
292
+ ownerPid: process.pid,
293
+ ownerStartTime: linuxProcessStartTime(process.pid),
294
+ zellijSessionName: process.env.ZELLIJ_SESSION_NAME?.trim() || null,
295
+ panes: []
296
+ };
297
+ }
298
+ function readRegistry() {
299
+ const file = watchdogRegistryPath();
300
+ if (!existsSync(file)) return emptyRegistry();
301
+ try {
302
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
303
+ if (parsed.version !== 1 || parsed.instanceId !== instanceId || parsed.ownerPid !== process.pid || !Array.isArray(parsed.panes)) return emptyRegistry();
304
+ return parsed;
305
+ } catch {
306
+ return emptyRegistry();
307
+ }
308
+ }
309
+ function writeRegistry(registry) {
310
+ mkdirSync(registryDirectory(), {
311
+ recursive: true,
312
+ mode: 448
313
+ });
314
+ const file = watchdogRegistryPath();
315
+ const tempFile = `${file}.tmp-${process.pid}`;
316
+ writeFileSync(tempFile, JSON.stringify(registry, null, 2), { mode: 384 });
317
+ renameSync(tempFile, file);
318
+ }
319
+ function ensureWatchdog() {
320
+ if (watchdogStarted) return;
321
+ watchdogStarted = true;
322
+ spawn("node", [watchdogRunnerPath(), watchdogRegistryPath()], {
323
+ detached: true,
324
+ stdio: "ignore",
325
+ env: process.env
326
+ }).unref();
327
+ }
328
+ function watchdogRunnerPath() {
329
+ return fileURLToPath(new URL("./pane-watchdog-runner.mjs", import.meta.url));
330
+ }
331
+ function cleanupStaleWatchdogRegistries() {
332
+ const directory = registryDirectory();
333
+ if (!existsSync(directory)) return;
334
+ for (const fileName of readdirSync(directory)) {
335
+ if (!fileName.startsWith("panes-") || !fileName.endsWith(".json")) continue;
336
+ const file = path.join(directory, fileName);
337
+ try {
338
+ const registry = JSON.parse(readFileSync(file, "utf8"));
339
+ if (registry.version !== 1 || ownerStillMatches(registry)) continue;
340
+ closeRegistryPanes(registry);
341
+ rmSync(file, { force: true });
342
+ } catch {
343
+ rmSync(file, { force: true });
344
+ }
345
+ }
346
+ }
347
+ function ownerStillMatches(registry) {
348
+ try {
349
+ process.kill(registry.ownerPid, 0);
350
+ } catch {
351
+ return false;
352
+ }
353
+ return !registry.ownerStartTime || linuxProcessStartTime(registry.ownerPid) === registry.ownerStartTime;
354
+ }
355
+ function closeRegistryPanes(registry) {
356
+ for (const pane of registry.panes) {
357
+ const args = [];
358
+ if (registry.zellijSessionName) args.push("--session", registry.zellijSessionName);
359
+ args.push("action", "close-pane", "--pane-id", pane.paneId);
360
+ spawn("zellij", args, {
361
+ detached: true,
362
+ stdio: "ignore",
363
+ env: process.env
364
+ }).unref();
365
+ }
366
+ }
367
+ function upsertWatchdogPane(registry, session) {
368
+ return {
369
+ ...registry,
370
+ panes: [...registry.panes.filter((pane) => pane.sessionId !== session.id && pane.paneId !== session.paneId), {
371
+ sessionId: session.id,
372
+ paneId: session.paneId,
373
+ title: session.title,
374
+ openCodeSessionId: session.openCodeSessionId,
375
+ createdAt: session.createdAt
376
+ }]
377
+ };
378
+ }
379
+ function removeWatchdogPane(registry, sessionId) {
380
+ return {
381
+ ...registry,
382
+ panes: registry.panes.filter((pane) => pane.sessionId !== sessionId)
383
+ };
384
+ }
385
+ function registerPaneForWatchdog(session) {
386
+ writeRegistry(upsertWatchdogPane(readRegistry(), session));
387
+ ensureWatchdog();
388
+ }
389
+ function unregisterPaneFromWatchdog(sessionId) {
390
+ const registry = readRegistry();
391
+ const updated = removeWatchdogPane(registry, sessionId);
392
+ if (updated.panes.length === registry.panes.length) return;
393
+ if (updated.panes.length === 0) {
394
+ removeWatchdogRegistry();
395
+ return;
396
+ }
397
+ writeRegistry(updated);
398
+ }
399
+ function removeWatchdogRegistry() {
400
+ try {
401
+ rmSync(watchdogRegistryPath(), { force: true });
402
+ } catch {}
403
+ }
404
+ //#endregion
246
405
  //#region src/pty/ring-buffer.ts
247
406
  const ansiPattern$1 = new RegExp(`${String.fromCharCode(27)}\\[[0-9;?]*[a-z]`, "gi");
248
407
  function normalizeLines(input) {
@@ -501,6 +660,7 @@ var SubscriberManager = class {
501
660
  state.buffer.append(`[zellij-pty] Pane ${session.paneId} closed at ${(/* @__PURE__ */ new Date()).toISOString()}`);
502
661
  this.sessions.updateLineCount(sessionId, state.buffer.lineCount);
503
662
  this.sessions.updateStatus(sessionId, session.status === "killed" ? "killed" : "exited");
663
+ unregisterPaneFromWatchdog(sessionId);
504
664
  this.stop(sessionId);
505
665
  return;
506
666
  }
@@ -629,7 +789,7 @@ const zellijPtyKillTool = tool({
629
789
  const output = subscriberManager.has(session.id) ? readOutputSnapshot(session.id) : void 0;
630
790
  try {
631
791
  await zellijCli.sendCtrlC(session.paneId);
632
- await setTimeout(500);
792
+ await setTimeout$1(500);
633
793
  } catch (error) {
634
794
  warnings.push(`Ctrl-C failed or pane was already gone: ${error instanceof Error ? error.message : String(error)}`);
635
795
  }
@@ -649,6 +809,7 @@ const zellijPtyKillTool = tool({
649
809
  }
650
810
  subscriberManager.stop(session.id);
651
811
  subscriberManager.forget(session.id);
812
+ unregisterPaneFromWatchdog(session.id);
652
813
  sessionManager.remove(session.id);
653
814
  return jsonResponse({
654
815
  killed: true,
@@ -784,7 +945,7 @@ const requestSudoTool = tool({
784
945
  humanInputOnly: true
785
946
  });
786
947
  const command = buildReviewScript(args.summary, args.scripts);
787
- const title = createOpenCodePaneTitle("request_sudo");
948
+ const title = createOpenCodePaneTitle("zellij_pty_request_sudo");
788
949
  const paneId = await zellijCli.newPane({
789
950
  command: "bash",
790
951
  args: ["-lc", command],
@@ -804,13 +965,14 @@ const requestSudoTool = tool({
804
965
  openCodeSessionId: context.sessionID,
805
966
  paneId,
806
967
  title,
807
- command: "request_sudo",
968
+ command: "zellij_pty_request_sudo",
808
969
  args: [],
809
970
  cwd,
810
971
  allowAgentInput: false,
811
972
  humanInputOnly: true,
812
973
  exitCodeToken
813
974
  });
975
+ registerPaneForWatchdog(session);
814
976
  await subscriberManager.start(session);
815
977
  return jsonResponse({
816
978
  session: publicSession(session),
@@ -833,7 +995,7 @@ async function runProbe(probe, outputReader) {
833
995
  };
834
996
  if (effectiveProbe.type === "sleep") {
835
997
  const seconds = effectiveProbe.seconds ?? defaultSleepSeconds;
836
- await setTimeout(seconds * 1e3);
998
+ await setTimeout$1(seconds * 1e3);
837
999
  return result(effectiveProbe.type, true, `Slept for ${seconds}s.`, startedAt);
838
1000
  }
839
1001
  if (effectiveProbe.type === "output") {
@@ -841,7 +1003,7 @@ async function runProbe(probe, outputReader) {
841
1003
  const deadline = Date.now() + timeoutSeconds * 1e3;
842
1004
  while (Date.now() <= deadline) {
843
1005
  if (outputReader(effectiveProbe.grep, effectiveProbe.ignoreCase)) return result(effectiveProbe.type, true, `Observed output matching /${effectiveProbe.grep}/.`, startedAt);
844
- await setTimeout(pollIntervalMs);
1006
+ await setTimeout$1(pollIntervalMs);
845
1007
  }
846
1008
  return result(effectiveProbe.type, false, `Timed out after ${timeoutSeconds}s waiting for output matching /${effectiveProbe.grep}/.`, startedAt);
847
1009
  }
@@ -861,7 +1023,7 @@ async function runProbe(probe, outputReader) {
861
1023
  } catch (error) {
862
1024
  lastError = error instanceof Error ? error.message : String(error);
863
1025
  }
864
- await setTimeout(pollIntervalMs);
1026
+ await setTimeout$1(pollIntervalMs);
865
1027
  }
866
1028
  return result(effectiveProbe.type, false, `Timed out after ${timeoutSeconds}s probing ${effectiveProbe.url}: ${lastError}.`, startedAt);
867
1029
  }
@@ -934,6 +1096,7 @@ const zellijPtySpawnTool = tool({
934
1096
  humanInputOnly: false,
935
1097
  exitCodeToken
936
1098
  });
1099
+ registerPaneForWatchdog(session);
937
1100
  await subscriberManager.start(session);
938
1101
  const probe = await runProbe(args.probe, (grep, ignoreCase) => outputMatches(session.id, grep, ignoreCase));
939
1102
  const output = readOutputSnapshot(session.id, { maxLines: args.maxLines });
@@ -982,7 +1145,7 @@ const schema = tool.schema;
982
1145
  const zellijPtyWriteTool = tool({
983
1146
  description: "Write stdin to a Zellij PTY session. Refuses human-input-only sessions.",
984
1147
  args: {
985
- id: schema.string().describe("zellij-pty session id returned by zellij_pty_spawn or request_sudo."),
1148
+ id: schema.string().describe("zellij-pty session id returned by zellij_pty_spawn or zellij_pty_request_sudo."),
986
1149
  data: schema.string().describe("Text to write. Use  to send Ctrl-C."),
987
1150
  maxLines: schema.number().int().positive().max(5e3).optional().describe("Maximum recent output lines to return. Defaults to 200."),
988
1151
  interruptAfterSeconds: schema.number().positive().max(300).optional().describe("Blindly send Ctrl-C after this many seconds if the pane is still running; keeps the pane alive.")
@@ -1002,12 +1165,12 @@ const zellijPtyWriteTool = tool({
1002
1165
  }
1003
1166
  session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1004
1167
  if (args.interruptAfterSeconds) {
1005
- await setTimeout(args.interruptAfterSeconds * 1e3);
1168
+ await setTimeout$1(args.interruptAfterSeconds * 1e3);
1006
1169
  if (sessionManager.get(session.id).status === "running") {
1007
1170
  await zellijCli.sendCtrlC(session.paneId);
1008
- await setTimeout(500);
1171
+ await setTimeout$1(500);
1009
1172
  }
1010
- } else await setTimeout(1e3);
1173
+ } else await setTimeout$1(1e3);
1011
1174
  return jsonResponse({
1012
1175
  session: publicSession(session),
1013
1176
  output: readOutputSnapshot(session.id, { maxLines: args.maxLines }),
@@ -1017,16 +1180,310 @@ const zellijPtyWriteTool = tool({
1017
1180
  }
1018
1181
  });
1019
1182
  //#endregion
1183
+ //#region src/zellij/shutdown-cleanup.ts
1184
+ let registered = false;
1185
+ let cleanedUp = false;
1186
+ function cleanupPanesOnShutdown(sessions = sessionManager, subscribers = subscriberManager) {
1187
+ if (cleanedUp) return;
1188
+ cleanedUp = true;
1189
+ for (const session of sessions.list()) {
1190
+ try {
1191
+ zellijCli.closePaneSync(session.paneId);
1192
+ } catch {}
1193
+ subscribers.forget(session.id);
1194
+ try {
1195
+ sessions.remove(session.id);
1196
+ } catch {}
1197
+ }
1198
+ }
1199
+ function registerShutdownCleanup() {
1200
+ if (registered) return;
1201
+ registered = true;
1202
+ process.once("exit", () => cleanupPanesOnShutdown());
1203
+ process.once("SIGINT", () => exitAfterCleanup("SIGINT", 130));
1204
+ process.once("SIGTERM", () => exitAfterCleanup("SIGTERM", 143));
1205
+ process.once("SIGHUP", () => exitAfterCleanup("SIGHUP", 129));
1206
+ }
1207
+ function exitAfterCleanup(signal, code) {
1208
+ cleanupPanesOnShutdown();
1209
+ process.removeAllListeners(signal);
1210
+ process.exit(code);
1211
+ }
1212
+ //#endregion
1213
+ //#region src/zellij/tab-title-events.ts
1214
+ const execFileAsync = promisify(execFile);
1215
+ function isRecord(value) {
1216
+ return typeof value === "object" && value !== null;
1217
+ }
1218
+ function stringProperty(object, key) {
1219
+ const value = object[key];
1220
+ return typeof value === "string" ? value : void 0;
1221
+ }
1222
+ function nestedStringProperty(object, key, nestedKey) {
1223
+ const nested = object[key];
1224
+ if (!isRecord(nested)) return void 0;
1225
+ return stringProperty(nested, nestedKey);
1226
+ }
1227
+ function sessionStatusProperty(object) {
1228
+ const status = object.status;
1229
+ if (!isRecord(status)) return void 0;
1230
+ if (status.type === "idle" || status.type === "busy") return { type: status.type };
1231
+ if (status.type === "retry") return {
1232
+ type: "retry",
1233
+ attempt: typeof status.attempt === "number" ? status.attempt : 0,
1234
+ message: typeof status.message === "string" ? status.message : "",
1235
+ next: typeof status.next === "number" ? status.next : 0
1236
+ };
1237
+ }
1238
+ function inputRequestID(object) {
1239
+ return stringProperty(object, "id") ?? stringProperty(object, "requestID") ?? stringProperty(object, "permissionID");
1240
+ }
1241
+ function deletedSessionID(event) {
1242
+ if (!isRecord(event.properties)) return void 0;
1243
+ return nestedStringProperty(event.properties, "info", "id") ?? stringProperty(event.properties, "sessionID");
1244
+ }
1245
+ async function readGitBranch(worktree) {
1246
+ return (await execFileAsync("git", [
1247
+ "-C",
1248
+ worktree,
1249
+ "branch",
1250
+ "--show-current"
1251
+ ], {
1252
+ encoding: "utf8",
1253
+ timeout: 1e3,
1254
+ maxBuffer: 1024 * 1024
1255
+ })).stdout;
1256
+ }
1257
+ async function getInitialBranch(worktree, readBranch = readGitBranch) {
1258
+ try {
1259
+ return (await readBranch(worktree)).trim() || void 0;
1260
+ } catch {
1261
+ return;
1262
+ }
1263
+ }
1264
+ function shouldReadInitialBranch(zellij) {
1265
+ return Boolean(zellij);
1266
+ }
1267
+ function handleTabTitleEvent(tabTitleManager, event) {
1268
+ if (!isRecord(event.properties)) return;
1269
+ const properties = event.properties;
1270
+ switch (event.type) {
1271
+ case "session.status": {
1272
+ const sessionID = stringProperty(properties, "sessionID");
1273
+ const status = sessionStatusProperty(properties);
1274
+ if (sessionID && status) tabTitleManager.updateSessionStatus(sessionID, status);
1275
+ break;
1276
+ }
1277
+ case "session.idle": {
1278
+ const sessionID = stringProperty(properties, "sessionID");
1279
+ if (sessionID) tabTitleManager.markSessionIdle(sessionID);
1280
+ break;
1281
+ }
1282
+ case "vcs.branch.updated":
1283
+ tabTitleManager.setBranch(stringProperty(properties, "branch"));
1284
+ break;
1285
+ case "question.asked":
1286
+ case "permission.asked":
1287
+ case "permission.updated": {
1288
+ const id = inputRequestID(properties);
1289
+ const sessionID = stringProperty(properties, "sessionID");
1290
+ if (id && sessionID) tabTitleManager.markNeedsInput(id, sessionID);
1291
+ break;
1292
+ }
1293
+ case "question.replied":
1294
+ case "question.rejected":
1295
+ case "permission.replied": {
1296
+ const id = inputRequestID(properties);
1297
+ if (id) tabTitleManager.clearNeedsInput(id);
1298
+ break;
1299
+ }
1300
+ case "session.deleted": {
1301
+ const sessionID = deletedSessionID(event);
1302
+ if (sessionID) tabTitleManager.removeSession(sessionID);
1303
+ break;
1304
+ }
1305
+ }
1306
+ }
1307
+ //#endregion
1308
+ //#region src/utils/debug.ts
1309
+ function debug(message, ...details) {
1310
+ if (!process.env.ZELLIJ_PTY_DEBUG) return;
1311
+ console.warn(`[opencode-zellij] ${message}`, ...details);
1312
+ }
1313
+ //#endregion
1314
+ //#region src/zellij/tab-title.ts
1315
+ const defaultTabTitleEmojis = {
1316
+ idle: "🟢",
1317
+ running: "⚡",
1318
+ needsInput: "💬",
1319
+ branch: "🌱"
1320
+ };
1321
+ function formatTabTitle(context) {
1322
+ const branch = context.branchName ? ` ${context.emojis.branch} ${context.branchName}` : "";
1323
+ return `${context.emojis[context.status === "needs-input" ? "needsInput" : context.status]} ${context.projectName}${branch}`;
1324
+ }
1325
+ function sanitizeTitle(title, maxLength = 90) {
1326
+ let cleaned = title.replace(/[\p{Cc}\p{Cf}\p{Co}\p{Cn}]/gu, " ").replace(/\s+/g, " ").trim();
1327
+ const chars = Array.from(cleaned);
1328
+ if (chars.length > maxLength) cleaned = `${chars.slice(0, maxLength - 1).join("")}…`;
1329
+ return cleaned;
1330
+ }
1331
+ var TabTitleManager = class {
1332
+ sessionStatuses = /* @__PURE__ */ new Map();
1333
+ pendingInputs = /* @__PURE__ */ new Map();
1334
+ branchName;
1335
+ desiredTitle;
1336
+ lastSyncedTitle;
1337
+ debounceTimer;
1338
+ syncInFlight = false;
1339
+ debounceMs;
1340
+ projectName;
1341
+ cli;
1342
+ emojis;
1343
+ enabled;
1344
+ constructor(options) {
1345
+ this.projectName = options.projectName;
1346
+ this.branchName = options.branchName?.trim() || void 0;
1347
+ this.cli = options.cli ?? new ZellijCli();
1348
+ this.emojis = {
1349
+ ...defaultTabTitleEmojis,
1350
+ ...options.emojis
1351
+ };
1352
+ this.debounceMs = options.debounceMs ?? 300;
1353
+ this.enabled = Boolean(process.env.ZELLIJ);
1354
+ }
1355
+ setBranch(branch) {
1356
+ const trimmed = branch?.trim() || void 0;
1357
+ if (this.branchName === trimmed) return;
1358
+ this.branchName = trimmed;
1359
+ this.scheduleUpdate();
1360
+ }
1361
+ updateSessionStatus(sessionID, status) {
1362
+ const activity = status.type === "idle" ? "idle" : "running";
1363
+ if (this.sessionStatuses.get(sessionID) === activity) return;
1364
+ this.sessionStatuses.set(sessionID, activity);
1365
+ this.scheduleUpdate();
1366
+ }
1367
+ markSessionIdle(sessionID) {
1368
+ this.updateSessionStatus(sessionID, { type: "idle" });
1369
+ }
1370
+ removeSession(sessionID) {
1371
+ const hadSessionStatus = this.sessionStatuses.delete(sessionID);
1372
+ let hadPendingInput = false;
1373
+ for (const [id, pendingSessionID] of this.pendingInputs) if (pendingSessionID === sessionID) {
1374
+ this.pendingInputs.delete(id);
1375
+ hadPendingInput = true;
1376
+ }
1377
+ if (!hadSessionStatus && !hadPendingInput) return;
1378
+ this.scheduleUpdate();
1379
+ }
1380
+ markNeedsInput(id, sessionID) {
1381
+ if (this.pendingInputs.get(id) === sessionID) return;
1382
+ this.pendingInputs.set(id, sessionID);
1383
+ this.scheduleUpdate();
1384
+ }
1385
+ clearNeedsInput(id) {
1386
+ if (!this.pendingInputs.delete(id)) return;
1387
+ this.scheduleUpdate();
1388
+ }
1389
+ get isBusy() {
1390
+ for (const activity of this.sessionStatuses.values()) if (activity === "running") return true;
1391
+ return false;
1392
+ }
1393
+ get needsInput() {
1394
+ return this.pendingInputs.size > 0;
1395
+ }
1396
+ get status() {
1397
+ if (this.needsInput) return "needs-input";
1398
+ if (this.isBusy) return "running";
1399
+ return "idle";
1400
+ }
1401
+ buildTitle() {
1402
+ return sanitizeTitle(formatTabTitle({
1403
+ projectName: this.projectName,
1404
+ branchName: this.branchName,
1405
+ status: this.status,
1406
+ emojis: this.emojis
1407
+ }));
1408
+ }
1409
+ getCurrentTitle() {
1410
+ return this.buildTitle();
1411
+ }
1412
+ async renderImmediate() {
1413
+ if (!this.enabled) return;
1414
+ this.desiredTitle = this.buildTitle();
1415
+ this.clearDebounceTimer();
1416
+ await this.syncDesiredTitle();
1417
+ }
1418
+ scheduleUpdate() {
1419
+ if (!this.enabled) return;
1420
+ const title = this.buildTitle();
1421
+ if (title === this.desiredTitle && title === this.lastSyncedTitle) return;
1422
+ this.desiredTitle = title;
1423
+ if (this.syncInFlight) return;
1424
+ this.clearDebounceTimer();
1425
+ this.debounceTimer = setTimeout(() => {
1426
+ this.debounceTimer = void 0;
1427
+ this.syncDesiredTitle().catch(() => {});
1428
+ }, this.debounceMs);
1429
+ }
1430
+ async syncDesiredTitle() {
1431
+ if (!this.enabled) return;
1432
+ if (this.syncInFlight) return;
1433
+ this.syncInFlight = true;
1434
+ try {
1435
+ while (this.desiredTitle && this.desiredTitle !== this.lastSyncedTitle) {
1436
+ const title = this.desiredTitle;
1437
+ try {
1438
+ await this.cli.renameTab(title);
1439
+ this.lastSyncedTitle = title;
1440
+ } catch (cause) {
1441
+ debug("Failed to rename Zellij tab.", cause);
1442
+ break;
1443
+ }
1444
+ }
1445
+ } finally {
1446
+ this.syncInFlight = false;
1447
+ }
1448
+ }
1449
+ clearDebounceTimer() {
1450
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
1451
+ this.debounceTimer = void 0;
1452
+ }
1453
+ destroy() {
1454
+ this.clearDebounceTimer();
1455
+ }
1456
+ };
1457
+ //#endregion
1020
1458
  //#region src/plugin.ts
1021
- const ZellijPtyPlugin = async (_input, options) => {
1459
+ function getProjectName(path) {
1460
+ return path.split(/[/\\]/).filter(Boolean).pop() || "opencode";
1461
+ }
1462
+ function getWorkspaceRoot(input) {
1463
+ return input.worktree || input.directory || process.cwd();
1464
+ }
1465
+ const ZellijPtyPlugin = async (input, options) => {
1022
1466
  configurePolicy(options?.zellijPty ?? options);
1467
+ cleanupStaleWatchdogRegistries();
1468
+ registerShutdownCleanup();
1469
+ const workspaceRoot = getWorkspaceRoot(input);
1470
+ const tabTitleManager = new TabTitleManager({
1471
+ projectName: getProjectName(workspaceRoot),
1472
+ branchName: shouldReadInitialBranch(process.env.ZELLIJ) ? await getInitialBranch(workspaceRoot) : void 0
1473
+ });
1474
+ tabTitleManager.renderImmediate().catch(() => {});
1023
1475
  return {
1024
1476
  async event(input) {
1025
- if (input.event.type === "session.deleted") {
1026
- const sessions = sessionManager.listByOpenCodeSession(input.event.properties.info.id);
1477
+ const event = input.event;
1478
+ handleTabTitleEvent(tabTitleManager, event);
1479
+ if (event.type === "session.deleted") {
1480
+ const sessionID = deletedSessionID(event);
1481
+ if (!sessionID) return;
1482
+ const sessions = sessionManager.listByOpenCodeSession(sessionID);
1027
1483
  await Promise.all(sessions.map(async (session) => {
1028
1484
  await subscriberManager.closeSessionPane(session.id);
1029
1485
  subscriberManager.forget(session.id);
1486
+ unregisterPaneFromWatchdog(session.id);
1030
1487
  sessionManager.remove(session.id);
1031
1488
  }));
1032
1489
  }
@@ -1037,7 +1494,7 @@ const ZellijPtyPlugin = async (_input, options) => {
1037
1494
  zellij_pty_write: zellijPtyWriteTool,
1038
1495
  zellij_pty_read: zellijPtyReadTool,
1039
1496
  zellij_pty_kill: zellijPtyKillTool,
1040
- request_sudo: requestSudoTool
1497
+ zellij_pty_request_sudo: requestSudoTool
1041
1498
  }
1042
1499
  };
1043
1500
  };