opencode-zellij 0.0.14 → 0.0.16

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
@@ -663,6 +663,28 @@ function findTabName(value, tabId) {
663
663
  if (found !== void 0) return found;
664
664
  }
665
665
  }
666
+ function activeTabNameProperty(object) {
667
+ if (object.active !== true || object.is_plugin === true) return void 0;
668
+ const name = stringProperty$2(object, ["name", "title"]);
669
+ return typeof name === "string" ? name : void 0;
670
+ }
671
+ function findActiveTabName(value) {
672
+ if (Array.isArray(value)) {
673
+ for (const item of value) {
674
+ const found = findActiveTabName(item);
675
+ if (found !== void 0) return found;
676
+ }
677
+ return;
678
+ }
679
+ if (typeof value !== "object" || value === null) return void 0;
680
+ const object = value;
681
+ const name = activeTabNameProperty(object);
682
+ if (name !== void 0) return name;
683
+ for (const nested of Object.values(object)) {
684
+ const found = findActiveTabName(nested);
685
+ if (found !== void 0) return found;
686
+ }
687
+ }
666
688
  function parseTabName(listTabsJson, tabId) {
667
689
  try {
668
690
  return findTabName(JSON.parse(listTabsJson), tabId);
@@ -671,6 +693,14 @@ function parseTabName(listTabsJson, tabId) {
671
693
  return;
672
694
  }
673
695
  }
696
+ function parseActiveTabName(listTabsJson) {
697
+ try {
698
+ return findActiveTabName(JSON.parse(listTabsJson));
699
+ } catch (error) {
700
+ debug("parseActiveTabName failed", errorMessage(error));
701
+ return;
702
+ }
703
+ }
674
704
  //#endregion
675
705
  //#region src/zellij/cli.ts
676
706
  const execFileAsync$1 = promisify(execFile);
@@ -738,25 +768,28 @@ async function runZellij(actionArgs, options = {}) {
738
768
  }
739
769
  }
740
770
  var ZellijCli = class {
771
+ constructor(run = runZellij) {
772
+ this.run = run;
773
+ }
741
774
  async newPane(options) {
742
- return parsePaneId((await runZellij(buildNewPaneActionArgs(options))).stdout);
775
+ return parsePaneId((await this.run(buildNewPaneActionArgs(options))).stdout);
743
776
  }
744
777
  async writeChars(paneId, data) {
745
- await runZellij(zellijActionArgs("write-chars", [
778
+ await this.run(zellijActionArgs("write-chars", [
746
779
  "--pane-id",
747
780
  paneId,
748
781
  data
749
782
  ]));
750
783
  }
751
784
  async sendCtrlC(paneId) {
752
- await runZellij(zellijActionArgs("send-keys", [
785
+ await this.run(zellijActionArgs("send-keys", [
753
786
  "--pane-id",
754
787
  paneId,
755
788
  "Ctrl c"
756
789
  ]));
757
790
  }
758
791
  async closePane(paneId) {
759
- await runZellij(zellijActionArgs("close-pane", ["--pane-id", paneId]));
792
+ await this.run(zellijActionArgs("close-pane", ["--pane-id", paneId]));
760
793
  }
761
794
  closePaneSync(paneId) {
762
795
  ensureZellijTarget();
@@ -767,10 +800,10 @@ var ZellijCli = class {
767
800
  });
768
801
  }
769
802
  async focusPane(paneId) {
770
- await runZellij(zellijActionArgs("focus-pane-id", [paneId]));
803
+ await this.run(zellijActionArgs("focus-pane-id", [paneId]));
771
804
  }
772
805
  async dumpScreen(paneId) {
773
- return (await runZellij(zellijActionArgs("dump-screen", [
806
+ return (await this.run(zellijActionArgs("dump-screen", [
774
807
  "--pane-id",
775
808
  paneId,
776
809
  "--full"
@@ -779,21 +812,24 @@ var ZellijCli = class {
779
812
  async currentPaneTabId() {
780
813
  const paneId = process.env.ZELLIJ_PANE_ID;
781
814
  if (!paneId) return void 0;
782
- return parseCurrentPaneTabId((await runZellij(zellijActionArgs("list-panes", ["--json"]), { timeoutMs: 5e3 })).stdout, paneId);
815
+ return parseCurrentPaneTabId((await this.run(zellijActionArgs("list-panes", ["--json"]), { timeoutMs: 5e3 })).stdout, paneId);
783
816
  }
784
817
  async paneExists(paneId) {
785
- return parsePaneExists((await runZellij(zellijActionArgs("list-panes", ["--json"]), { timeoutMs: 5e3 })).stdout, paneId);
818
+ return parsePaneExists((await this.run(zellijActionArgs("list-panes", ["--json"]), { timeoutMs: 5e3 })).stdout, paneId);
786
819
  }
787
820
  async renameTab(title) {
788
821
  const tabId = await this.currentPaneTabId();
789
822
  if (tabId === void 0 && process.env.ZELLIJ) throw new Error(`Could not resolve Zellij tab id for pane ${process.env.ZELLIJ_PANE_ID ?? "<missing>"}`);
790
- await runZellij(tabId === void 0 ? buildRenameTabActionArgs(title) : buildRenameTabActionArgs(title, { tabId }));
823
+ await this.run(tabId === void 0 ? buildRenameTabActionArgs(title) : buildRenameTabActionArgs(title, { tabId }));
791
824
  }
792
825
  async currentTabTitle() {
793
- if (!process.env.ZELLIJ_PANE_ID) return void 0;
826
+ if (!process.env.ZELLIJ_PANE_ID) {
827
+ if (!process.env.ZELLIJ_SESSION_NAME?.trim()) return void 0;
828
+ return parseActiveTabName((await this.run(zellijActionArgs("list-tabs", ["--json"]), { timeoutMs: 5e3 })).stdout);
829
+ }
794
830
  const tabId = await this.currentPaneTabId();
795
831
  if (tabId === void 0) return void 0;
796
- return parseTabName((await runZellij(zellijActionArgs("list-tabs", ["--json"]), { timeoutMs: 5e3 })).stdout, tabId);
832
+ return parseTabName((await this.run(zellijActionArgs("list-tabs", ["--json"]), { timeoutMs: 5e3 })).stdout, tabId);
797
833
  }
798
834
  };
799
835
  const zellijCli = new ZellijCli();
@@ -1038,13 +1074,20 @@ function createExitCodeToken() {
1038
1074
  return randomUUID().replaceAll("-", "");
1039
1075
  }
1040
1076
  function parseExitCodeMarker(line) {
1041
- const match = line.replace(ansiPattern, "").trim().match(markerPattern);
1077
+ const match = line.replace(ansiPattern, "").replace(/\r?\n/g, "").trim().match(markerPattern);
1042
1078
  if (!match?.[1] || !match[2]) return null;
1043
1079
  return {
1044
1080
  token: match[1],
1045
1081
  exitCode: Number(match[2])
1046
1082
  };
1047
1083
  }
1084
+ function parseExitCodeMarkerLines(lines, maxWindowLines = 8) {
1085
+ for (let start = 0; start < lines.length; start += 1) for (let size = 1; size <= maxWindowLines && start + size <= lines.length; size += 1) {
1086
+ const marker = parseExitCodeMarker(lines.slice(start, start + size).join("\n"));
1087
+ if (marker) return marker;
1088
+ }
1089
+ return null;
1090
+ }
1048
1091
  //#endregion
1049
1092
  //#region src/zellij/subscribe.ts
1050
1093
  const maxStderrLines = 200;
@@ -1099,6 +1142,7 @@ var SubscriberManager = class {
1099
1142
  startingSessions = /* @__PURE__ */ new Map();
1100
1143
  spawnProcess;
1101
1144
  dumpScreen;
1145
+ paneExists;
1102
1146
  closePane;
1103
1147
  lifecycleHooks;
1104
1148
  terminalTailLines;
@@ -1106,8 +1150,9 @@ var SubscriberManager = class {
1106
1150
  this.sessions = sessions;
1107
1151
  this.maxBufferLines = maxBufferLines;
1108
1152
  this.spawnProcess = dependencies.spawn ?? spawn;
1109
- this.dumpScreen = dependencies.dumpScreen ?? zellijCli.dumpScreen;
1110
- this.closePane = dependencies.closePane ?? zellijCli.closePane;
1153
+ this.dumpScreen = dependencies.dumpScreen ?? ((paneId) => zellijCli.dumpScreen(paneId));
1154
+ this.paneExists = dependencies.paneExists ?? ((paneId) => zellijCli.paneExists(paneId));
1155
+ this.closePane = dependencies.closePane ?? ((paneId) => zellijCli.closePane(paneId));
1111
1156
  this.lifecycleHooks = dependencies.lifecycleHooks;
1112
1157
  this.terminalTailLines = dependencies.terminalTailLines ?? 200;
1113
1158
  }
@@ -1266,18 +1311,14 @@ var SubscriberManager = class {
1266
1311
  captureExitCode(sessionId, lines) {
1267
1312
  const session = this.sessions.get(sessionId);
1268
1313
  if (!session.exitCodeToken) return;
1269
- for (const line of lines) {
1270
- const marker = parseExitCodeMarker(line);
1271
- if (!marker || marker.token !== session.exitCodeToken) continue;
1272
- this.markSessionTerminal(sessionId, "exit_marker", { exitCode: marker.exitCode });
1273
- return;
1274
- }
1314
+ const marker = parseExitCodeMarkerLines(lines);
1315
+ if (!marker || marker.token !== session.exitCodeToken) return;
1316
+ this.markSessionTerminal(sessionId, "exit_marker", { exitCode: marker.exitCode });
1275
1317
  }
1276
1318
  handleStderr(sessionId, child, chunk) {
1277
1319
  const state = this.subscribers.get(sessionId);
1278
1320
  if (!state || state.child !== child) return;
1279
- state.stderr.push(...splitLines(chunk));
1280
- if (state.stderr.length > maxStderrLines) state.stderr = state.stderr.slice(state.stderr.length - maxStderrLines);
1321
+ this.appendStderr(state, ...splitLines(chunk));
1281
1322
  }
1282
1323
  handleSubscriberExit(sessionId, child) {
1283
1324
  const state = this.subscribers.get(sessionId);
@@ -1285,17 +1326,45 @@ var SubscriberManager = class {
1285
1326
  if (state.child !== child) return;
1286
1327
  state.child = null;
1287
1328
  state.lastExitedAt = (/* @__PURE__ */ new Date()).toISOString();
1288
- if (this.sessions.find(sessionId)?.status !== "terminal") state.stderr.push(`[zellij-pty] subscriber exited at ${state.lastExitedAt}; last buffered output is retained.`);
1289
- if (state.stderr.length > maxStderrLines) state.stderr = state.stderr.slice(state.stderr.length - maxStderrLines);
1329
+ if (this.sessions.find(sessionId)?.status !== "terminal") this.appendStderr(state, `[zellij-pty] subscriber exited at ${state.lastExitedAt}; last buffered output is retained.`);
1330
+ this.reconcileSubscriberTermination(sessionId, state, "subscriber_exit");
1290
1331
  }
1291
1332
  handleSubscriberError(sessionId, child, error) {
1292
1333
  const state = this.subscribers.get(sessionId);
1293
1334
  if (state?.child === child) {
1294
- state.stderr.push(error.message);
1335
+ this.appendStderr(state, error.message);
1295
1336
  state.child = null;
1296
1337
  state.lastExitedAt = (/* @__PURE__ */ new Date()).toISOString();
1297
- this.sessions.updateStatus(sessionId, "unknown");
1338
+ if (this.sessions.find(sessionId)?.status !== "terminal") this.sessions.updateStatus(sessionId, "unknown");
1339
+ this.reconcileSubscriberTermination(sessionId, state, "subscriber_error");
1340
+ }
1341
+ }
1342
+ appendStderr(state, ...lines) {
1343
+ state.stderr.push(...lines);
1344
+ if (state.stderr.length > maxStderrLines) state.stderr = state.stderr.slice(state.stderr.length - maxStderrLines);
1345
+ }
1346
+ async reconcileSubscriberTermination(sessionId, state, reason) {
1347
+ const session = this.sessions.find(sessionId);
1348
+ if (!session || session.status === "terminal" || this.subscribers.get(sessionId) !== state || state.child) return;
1349
+ let paneExists;
1350
+ try {
1351
+ paneExists = await this.paneExists(session.paneId);
1352
+ } catch (error) {
1353
+ const latestState = this.subscribers.get(sessionId);
1354
+ const latestSession = this.sessions.find(sessionId);
1355
+ if (!latestState || latestState !== state || latestState.child || !latestSession || latestSession.status === "terminal") return;
1356
+ this.appendStderr(latestState, `[zellij-pty] ${reason} reconciliation could not verify pane ${latestSession.paneId}: ${errorMessage(error)}`);
1357
+ return;
1358
+ }
1359
+ const latestState = this.subscribers.get(sessionId);
1360
+ const latestSession = this.sessions.find(sessionId);
1361
+ if (!latestState || latestState !== state || latestState.child || !latestSession || latestSession.status === "terminal") return;
1362
+ if (paneExists === false) {
1363
+ this.markSessionTerminal(sessionId, reason);
1364
+ unregisterPaneFromWatchdog(sessionId);
1365
+ return;
1298
1366
  }
1367
+ if (paneExists === void 0) this.appendStderr(latestState, `[zellij-pty] ${reason} reconciliation could not confirm whether pane ${latestSession.paneId} still exists; leaving session non-terminal.`);
1299
1368
  }
1300
1369
  markSessionTerminal(sessionId, reason, input = {}) {
1301
1370
  const state = this.subscribers.get(sessionId);
@@ -1509,7 +1578,7 @@ async function executeZellijPtyRead(args, dependencies = {}) {
1509
1578
  const nextAdviceApi = dependencies.nextAdvice ?? nextAdvice;
1510
1579
  const readOutputSnapshotApi = dependencies.readOutputSnapshot ?? readOutputSnapshot;
1511
1580
  const validateGrepApi = dependencies.validateGrep ?? validateGrep;
1512
- const paneExistsApi = dependencies.paneExists ?? zellijCli.paneExists;
1581
+ const paneExistsApi = dependencies.paneExists ?? ((paneId) => zellijCli.paneExists(paneId));
1513
1582
  const session = sessionManagerApi.get(args.id);
1514
1583
  const grepError = validateGrepApi(args.grep);
1515
1584
  if (grepError) return {
@@ -1685,13 +1754,6 @@ const requestSudoTool = tool({
1685
1754
  floating: true,
1686
1755
  exitCodeToken
1687
1756
  });
1688
- const warnings = [];
1689
- try {
1690
- await zellijCli.focusPane(paneId);
1691
- } catch (error) {
1692
- if (!(error instanceof Error ? error.message : String(error)).includes("already focused")) throw error;
1693
- warnings.push("Pane was already focused after creation.");
1694
- }
1695
1757
  const session = sessionManager.create({
1696
1758
  openCodeSessionId: context.sessionID,
1697
1759
  paneId,
@@ -1709,7 +1771,7 @@ const requestSudoTool = tool({
1709
1771
  session: publicSession(session),
1710
1772
  output: readOutputSnapshot(session.id),
1711
1773
  next: nextAdvice(false, "The user must review the summary and commands in Zellij, then type YES and any required credentials directly in the pane."),
1712
- warnings
1774
+ warnings: []
1713
1775
  });
1714
1776
  }
1715
1777
  });
@@ -1958,6 +2020,9 @@ function parseSessionStatus(value) {
1958
2020
  const completionTitle = "Zellij PTY session completed";
1959
2021
  const completionMessage = "A Zellij PTY session completed. Review the finished pane if needed.";
1960
2022
  const queuedNoticeHeader = "[OpenCode] Zellij PTY completion notice";
2023
+ function supportsActivePrompt(mode) {
2024
+ return mode === "prompt" || mode === "queue+toast";
2025
+ }
1961
2026
  function buildQueuedCompletionNotice(events) {
1962
2027
  return [queuedNoticeHeader, ...events.map((event) => `- ${event.session.id} (${event.session.paneId}) 已完成,請使用 zellij_pty_read 讀取最終輸出並清理 pane。`)].join("\n");
1963
2028
  }
@@ -1999,7 +2064,7 @@ function injectQueuedCompletionNotice(input, notice) {
1999
2064
  };
2000
2065
  }
2001
2066
  function evaluateCompletionPromptDecision(input) {
2002
- if (input.config.mode !== "prompt") return {
2067
+ if (!supportsActivePrompt(input.config.mode)) return {
2003
2068
  shouldPrompt: false,
2004
2069
  shouldQueue: false,
2005
2070
  reason: "prompt mode disabled"
@@ -2084,6 +2149,7 @@ var SessionCompletionNotificationQueue = class {
2084
2149
  const state = {
2085
2150
  event,
2086
2151
  queued: false,
2152
+ sent: false,
2087
2153
  toastSent: false,
2088
2154
  promptAttempts: 0,
2089
2155
  promptAttemptedAt: null
@@ -2098,7 +2164,7 @@ var SessionCompletionNotificationQueue = class {
2098
2164
  this.finalize(state);
2099
2165
  return;
2100
2166
  case "queue+toast":
2101
- state.queued = true;
2167
+ await this.tryPromptOrQueue(state);
2102
2168
  await this.sendToast(state);
2103
2169
  return;
2104
2170
  case "prompt":
@@ -2112,7 +2178,7 @@ var SessionCompletionNotificationQueue = class {
2112
2178
  if (pending.length === 0) return input;
2113
2179
  const notice = buildQueuedCompletionNotice(pending.map((state) => state.event));
2114
2180
  for (const state of pending) {
2115
- if (!state.toastSent) this.context.markSent(state.event.sessionId);
2181
+ this.markStateSent(state);
2116
2182
  this.finalize(state);
2117
2183
  }
2118
2184
  return injectQueuedCompletionNotice(input, notice);
@@ -2158,7 +2224,7 @@ var SessionCompletionNotificationQueue = class {
2158
2224
  state.queued = true;
2159
2225
  return;
2160
2226
  }
2161
- this.context.markSent(state.event.sessionId);
2227
+ this.markStateSent(state);
2162
2228
  this.finalize(state);
2163
2229
  } catch (error) {
2164
2230
  debug("completion notification prompt failed", errorMessage(error));
@@ -2179,12 +2245,17 @@ var SessionCompletionNotificationQueue = class {
2179
2245
  duration: 1e4
2180
2246
  } });
2181
2247
  state.toastSent = true;
2182
- this.context.markSent(state.event.sessionId);
2248
+ this.markStateSent(state);
2183
2249
  if (!state.queued) this.finalize(state);
2184
2250
  } catch (error) {
2185
2251
  debug("completion notification toast failed", errorMessage(error));
2186
2252
  }
2187
2253
  }
2254
+ markStateSent(state) {
2255
+ if (state.sent) return;
2256
+ this.context.markSent(state.event.sessionId);
2257
+ state.sent = true;
2258
+ }
2188
2259
  finalize(state) {
2189
2260
  this.states.delete(state.event.sessionId);
2190
2261
  }
@@ -2516,6 +2587,7 @@ var TabTitleActor = class {
2516
2587
  var TabTitleManager = class {
2517
2588
  desiredTitle;
2518
2589
  lastSyncedTitle;
2590
+ syncGeneration = 0;
2519
2591
  debounceTimer;
2520
2592
  retryTimer;
2521
2593
  retryAttempt = 0;
@@ -2578,24 +2650,27 @@ var TabTitleManager = class {
2578
2650
  }
2579
2651
  async syncDesiredTitle() {
2580
2652
  if (!this.enabled || this.destroyed) return;
2653
+ const generation = this.syncGeneration;
2581
2654
  await this.ensureOriginalTabTitle();
2582
- if (this.destroyed) return;
2655
+ if (this.destroyed || generation !== this.syncGeneration) return;
2583
2656
  if (this.syncInFlight) return this.syncPromise;
2584
2657
  this.syncInFlight = true;
2585
- this.syncPromise = this.runTitleSync();
2658
+ this.syncPromise = this.runTitleSync(generation);
2586
2659
  return this.syncPromise;
2587
2660
  }
2588
- async runTitleSync() {
2661
+ async runTitleSync(generation) {
2589
2662
  try {
2590
- while (this.desiredTitle && this.desiredTitle !== this.lastSyncedTitle) {
2663
+ while (generation === this.syncGeneration && this.desiredTitle && this.desiredTitle !== this.lastSyncedTitle) {
2591
2664
  const title = this.desiredTitle;
2592
2665
  try {
2593
2666
  await this.cli.renameTab(title);
2667
+ if (generation !== this.syncGeneration || this.destroyed) return;
2594
2668
  this.lastSyncedTitle = title;
2595
2669
  this.retryAttempt = 0;
2596
2670
  this.clearRetryTimer();
2597
2671
  } catch (cause) {
2598
2672
  debug("Failed to rename Zellij tab.", cause);
2673
+ if (generation !== this.syncGeneration || this.destroyed) break;
2599
2674
  this.scheduleRetry();
2600
2675
  break;
2601
2676
  }
@@ -2646,6 +2721,8 @@ var TabTitleManager = class {
2646
2721
  destroy() {
2647
2722
  if (this.destroyed) return this.destroyPromise ?? Promise.resolve();
2648
2723
  this.destroyed = true;
2724
+ this.syncGeneration += 1;
2725
+ this.desiredTitle = void 0;
2649
2726
  this.clearDebounceTimer();
2650
2727
  this.clearRetryTimer();
2651
2728
  if (!this.enabled) return Promise.resolve();