opencode-zellij 0.0.15 → 0.0.17

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,262 +1,26 @@
1
1
  import process from "node:process";
2
- import { execFile, spawn, spawnSync } from "node:child_process";
3
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
- import { readFile, rename, rm } from "node:fs/promises";
5
- import path, { basename, dirname, join } from "node:path";
6
- import { fileURLToPath } from "node:url";
7
- import { promisify } from "node:util";
3
+ import { readFile } from "node:fs/promises";
8
4
  import { homedir, tmpdir } from "node:os";
5
+ import path, { dirname, join } from "node:path";
9
6
  import { parseJSON, parseJSONC } from "confbox";
10
7
  import { z } from "zod";
11
8
  import { randomUUID } from "node:crypto";
12
9
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
13
10
  import { tool } from "@opencode-ai/plugin";
11
+ import { execFile, spawn, spawnSync } from "node:child_process";
12
+ import { promisify } from "node:util";
13
+ import { fileURLToPath } from "node:url";
14
14
  import { Buffer } from "node:buffer";
15
- //#region src/utils/debug.ts
16
- function debug(message, ...details) {
17
- if (!process.env.ZELLIJ_PTY_DEBUG) return;
18
- console.warn(`[opencode-zellij] ${message}`, ...details);
19
- }
20
- //#endregion
21
- //#region src/utils/errors.ts
22
- function errorMessage(error) {
23
- return error instanceof Error ? error.message : String(error);
24
- }
25
- //#endregion
26
- //#region src/auto-update.ts
27
- const PACKAGE_NAME = "opencode-zellij";
28
- const NPM_REGISTRY_URL = "https://registry.npmjs.org/-/package/opencode-zellij/dist-tags";
29
- const FETCH_TIMEOUT_MS = 5e3;
30
- const INSTALL_TIMEOUT_MS = 6e4;
31
- const defaultExecFile = promisify(execFile);
32
- function packageDir(installRoot) {
33
- return join(installRoot, "node_modules", PACKAGE_NAME);
34
- }
35
- function backupDir(installRoot) {
36
- return join(installRoot, "node_modules", `${PACKAGE_NAME}.update-backup`);
37
- }
38
- async function installedPackageMetadata(installRoot) {
39
- try {
40
- const content = await readFile(join(packageDir(installRoot), "package.json"), "utf8");
41
- const pkg = JSON.parse(content);
42
- if (isRecord$2(pkg)) return {
43
- name: typeof pkg.name === "string" ? pkg.name : void 0,
44
- version: typeof pkg.version === "string" ? pkg.version : void 0,
45
- main: typeof pkg.main === "string" ? pkg.main : void 0
46
- };
47
- } catch (error) {
48
- debug("installedPackageMetadata failed", errorMessage(error));
49
- }
50
- }
51
- function isExpectedPackage(metadata, version) {
52
- return metadata?.name === "opencode-zellij" && metadata.version === version;
53
- }
54
- function hasRunnableEntry(installRoot, metadata) {
55
- if (!metadata) return false;
56
- const dir = packageDir(installRoot);
57
- if (metadata.main && existsSync(join(dir, metadata.main))) return true;
58
- return existsSync(join(dir, "dist", "index.mjs"));
59
- }
60
- async function isVerifiedInstall(installRoot, version) {
61
- const metadata = await installedPackageMetadata(installRoot);
62
- return isExpectedPackage(metadata, version) && hasRunnableEntry(installRoot, metadata);
63
- }
64
- async function removeInstalledPackage(installRoot) {
65
- await rm(packageDir(installRoot), {
66
- force: true,
67
- recursive: true
68
- });
69
- }
70
- async function backupInstalledPackage(installRoot) {
71
- const source = packageDir(installRoot);
72
- if (!existsSync(source)) return void 0;
73
- const backup = backupDir(installRoot);
74
- await rm(backup, {
75
- force: true,
76
- recursive: true
77
- });
78
- await rename(source, backup);
79
- return backup;
80
- }
81
- async function restoreInstalledPackage(installRoot, backup) {
82
- if (!backup || !existsSync(backup)) return;
83
- await rm(packageDir(installRoot), {
84
- force: true,
85
- recursive: true
86
- });
87
- await rename(backup, packageDir(installRoot));
88
- }
89
- async function discardBackup(backup) {
90
- if (backup) await rm(backup, {
91
- force: true,
92
- recursive: true
93
- });
94
- }
95
- async function findInstallContext(importMetaUrl) {
96
- let startPath;
97
- try {
98
- startPath = fileURLToPath(importMetaUrl);
99
- } catch (cause) {
100
- debug("invalid import.meta.url", cause instanceof Error ? cause.message : String(cause));
101
- return;
102
- }
103
- let dir = dirname(startPath);
104
- while (true) {
105
- if (dir.endsWith(`/node_modules/opencode-zellij`) || dir.endsWith(`\\node_modules\\opencode-zellij`)) {
106
- const packageJsonPath = join(dir, "package.json");
107
- try {
108
- const content = await readFile(packageJsonPath, "utf8");
109
- const pkg = JSON.parse(content);
110
- if (isRecord$2(pkg) && pkg.name === "opencode-zellij" && typeof pkg.version === "string" && pkg.version.length > 0) {
111
- const installRoot = dirname(dirname(dir));
112
- if (existsSync(join(installRoot, "package.json"))) return {
113
- installRoot,
114
- cacheSpec: basename(installRoot),
115
- currentVersion: pkg.version
116
- };
117
- }
118
- } catch (error) {
119
- debug("findInstallContext package.json read failed", errorMessage(error));
120
- }
121
- }
122
- const parent = dirname(dir);
123
- if (parent === dir) break;
124
- dir = parent;
125
- }
126
- }
127
- function isRecord$2(value) {
128
- return typeof value === "object" && value !== null;
129
- }
130
- function isAutoUpdatableSpec(spec) {
131
- if (spec === "opencode-zellij") return true;
132
- if (spec === `opencode-zellij@latest`) return true;
133
- return false;
134
- }
135
- async function fetchLatestVersion(fetchImpl = globalThis.fetch) {
136
- const controller = new AbortController();
137
- const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
138
- try {
139
- const response = await fetchImpl(NPM_REGISTRY_URL, { signal: controller.signal });
140
- clearTimeout(timeout);
141
- if (!response.ok) {
142
- debug(`npm registry returned ${response.status}`);
143
- return;
144
- }
145
- const data = await response.json();
146
- if (isRecord$2(data) && typeof data.latest === "string") return data.latest;
147
- debug("npm registry response missing latest tag");
148
- return;
149
- } catch (cause) {
150
- clearTimeout(timeout);
151
- debug("failed to fetch latest version", cause instanceof Error ? cause.message : String(cause));
152
- return;
153
- }
154
- }
155
- async function runNpmInstall(installRoot, version, execImpl = defaultExecFile) {
156
- debug(`updating ${PACKAGE_NAME} to ${version} in ${installRoot}`);
157
- try {
158
- const install = () => execImpl("npm", [
159
- "install",
160
- `${PACKAGE_NAME}@${version}`,
161
- "--save-exact",
162
- "--ignore-scripts",
163
- "--no-audit",
164
- "--no-fund",
165
- "--prefer-online"
166
- ], {
167
- cwd: installRoot,
168
- timeout: INSTALL_TIMEOUT_MS
169
- });
170
- await install();
171
- if (await isVerifiedInstall(installRoot, version)) {
172
- debug(`updated ${PACKAGE_NAME} to ${version}`);
173
- return true;
174
- }
175
- const installedPackage = await installedPackageMetadata(installRoot);
176
- debug(`npm install left stale or invalid ${PACKAGE_NAME} (${installedPackage?.name ?? "<missing>"}@${installedPackage?.version ?? "<missing>"}); reinstalling ${version}`);
177
- const backup = await backupInstalledPackage(installRoot);
178
- try {
179
- await removeInstalledPackage(installRoot);
180
- await install();
181
- if (await isVerifiedInstall(installRoot, version)) {
182
- await discardBackup(backup);
183
- debug(`updated ${PACKAGE_NAME} to ${version}`);
184
- return true;
185
- }
186
- const reinstalledPackage = await installedPackageMetadata(installRoot);
187
- debug(`npm install verification failed: expected ${PACKAGE_NAME}@${version}, found ${reinstalledPackage?.name ?? "<missing>"}@${reinstalledPackage?.version ?? "<missing>"}`);
188
- await restoreInstalledPackage(installRoot, backup);
189
- return false;
190
- } catch (cause) {
191
- await restoreInstalledPackage(installRoot, backup);
192
- throw cause;
193
- }
194
- } catch (cause) {
195
- debug("npm install failed", cause instanceof Error ? cause.message : String(cause));
196
- return false;
197
- }
198
- }
199
- async function checkAndUpdate(options) {
200
- const context = await findInstallContext(options.importMetaUrl);
201
- if (!context) {
202
- debug("skipping auto-update: not installed from npm");
203
- return {
204
- type: "skipped",
205
- reason: "not installed from npm"
206
- };
207
- }
208
- if (!isAutoUpdatableSpec(context.cacheSpec)) {
209
- debug(`skipping auto-update: cache spec is pinned or unknown (${context.cacheSpec})`);
210
- return {
211
- type: "skipped",
212
- reason: `cache spec is pinned or unknown (${context.cacheSpec})`
213
- };
214
- }
215
- const latest = await fetchLatestVersion(options.fetchImpl);
216
- if (!latest) {
217
- debug("skipping auto-update: could not determine latest version");
218
- return {
219
- type: "skipped",
220
- reason: "could not determine latest version"
221
- };
222
- }
223
- const installedVersion = (await installedPackageMetadata(context.installRoot))?.version ?? context.currentVersion;
224
- if (latest === installedVersion) {
225
- debug(`auto-update: already on latest ${latest}`);
226
- return {
227
- type: "up-to-date",
228
- currentVersion: installedVersion
229
- };
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
- };
245
- }
246
- //#endregion
15
+ import { LogFileRotationTransport } from "@loglayer/transport-log-file-rotation";
16
+ import { LogLayer } from "loglayer";
17
+ import { serializeError } from "serialize-error";
247
18
  //#region src/config.ts
248
19
  const sudoPaneSchema = z.enum([
249
20
  "allow",
250
21
  "deny",
251
22
  "hide"
252
23
  ]);
253
- const completionNotificationModeSchema = z.enum([
254
- "off",
255
- "queue",
256
- "toast",
257
- "queue+toast",
258
- "prompt"
259
- ]);
260
24
  const configFilenames = ["opencode-zellij.config.jsonc", "opencode-zellij.config.json"];
261
25
  const tabTitleLayerSchema = z.object({
262
26
  enabled: z.boolean().optional().describe("Enable dynamic Zellij tab title updates."),
@@ -269,22 +33,12 @@ const tabTitleLayerSchema = z.object({
269
33
  const ptyLayerSchema = z.object({
270
34
  enabled: z.boolean().optional().describe("Enable Zellij-backed PTY tools."),
271
35
  cleanupExitedPaneOnRead: z.boolean().optional().describe("Remove exited PTY panes after they are read."),
272
- sudoPane: sudoPaneSchema.optional().describe("Controls whether the sudo pane tool is available, denied, or hidden."),
273
- completionNotification: z.object({
274
- mode: completionNotificationModeSchema.optional().describe("Controls how completion notifications are delivered."),
275
- prompt: z.object({
276
- requireIdle: z.boolean().optional().describe("Require the plugin to be idle before prompting."),
277
- cooldownMs: z.number().finite().min(0).optional().describe("Cooldown time before prompting again in milliseconds."),
278
- maxAttempts: z.number().finite().int().min(0).optional().describe("Maximum prompt attempts before backing off.")
279
- }).strict().optional().describe("Prompt-specific completion notification settings.")
280
- }).strict().optional().describe("Completion notification delivery settings.")
36
+ sudoPane: sudoPaneSchema.optional().describe("Controls whether the sudo pane tool is available, denied, or hidden.")
281
37
  }).strict();
282
- const autoUpdateLayerSchema = z.boolean().optional().describe("Enable automatic update checks for the opencode-zellij plugin.");
283
38
  const sidecarConfigSchema = z.object({
284
39
  $schema: z.string().optional().describe("JSON Schema URI for editor completion."),
285
40
  tabTitle: tabTitleLayerSchema.optional(),
286
- pty: ptyLayerSchema.optional(),
287
- autoUpdate: autoUpdateLayerSchema.optional()
41
+ pty: ptyLayerSchema.optional()
288
42
  }).strict();
289
43
  const defaultConfig = {
290
44
  tabTitle: {
@@ -298,25 +52,15 @@ const defaultConfig = {
298
52
  pty: {
299
53
  enabled: true,
300
54
  cleanupExitedPaneOnRead: true,
301
- sudoPane: "allow",
302
- completionNotification: {
303
- mode: "queue+toast",
304
- prompt: {
305
- requireIdle: true,
306
- cooldownMs: 3e4,
307
- maxAttempts: 1
308
- }
309
- }
310
- },
311
- autoUpdate: true
55
+ sudoPane: "allow"
56
+ }
312
57
  };
313
58
  function validConfigLayer(value) {
314
59
  const result = sidecarConfigSchema.safeParse(value);
315
60
  if (!result.success) return void 0;
316
61
  return {
317
62
  tabTitle: result.data.tabTitle,
318
- pty: result.data.pty,
319
- autoUpdate: result.data.autoUpdate
63
+ pty: result.data.pty
320
64
  };
321
65
  }
322
66
  function mergeConfig(user, project) {
@@ -332,17 +76,8 @@ function mergeConfig(user, project) {
332
76
  pty: {
333
77
  enabled: project?.pty?.enabled ?? user?.pty?.enabled ?? defaultConfig.pty.enabled,
334
78
  cleanupExitedPaneOnRead: project?.pty?.cleanupExitedPaneOnRead ?? user?.pty?.cleanupExitedPaneOnRead ?? defaultConfig.pty.cleanupExitedPaneOnRead,
335
- sudoPane: project?.pty?.sudoPane ?? user?.pty?.sudoPane ?? defaultConfig.pty.sudoPane,
336
- completionNotification: {
337
- mode: project?.pty?.completionNotification?.mode ?? user?.pty?.completionNotification?.mode ?? defaultConfig.pty.completionNotification.mode,
338
- prompt: {
339
- requireIdle: project?.pty?.completionNotification?.prompt?.requireIdle ?? user?.pty?.completionNotification?.prompt?.requireIdle ?? defaultConfig.pty.completionNotification.prompt.requireIdle,
340
- cooldownMs: project?.pty?.completionNotification?.prompt?.cooldownMs ?? user?.pty?.completionNotification?.prompt?.cooldownMs ?? defaultConfig.pty.completionNotification.prompt.cooldownMs,
341
- maxAttempts: project?.pty?.completionNotification?.prompt?.maxAttempts ?? user?.pty?.completionNotification?.prompt?.maxAttempts ?? defaultConfig.pty.completionNotification.prompt.maxAttempts
342
- }
343
- }
344
- },
345
- autoUpdate: project?.autoUpdate ?? user?.autoUpdate ?? defaultConfig.autoUpdate
79
+ sudoPane: project?.pty?.sudoPane ?? user?.pty?.sudoPane ?? defaultConfig.pty.sudoPane
80
+ }
346
81
  };
347
82
  }
348
83
  async function loadConfigLayer(directory, warnings) {
@@ -490,8 +225,7 @@ var SessionManager = class {
490
225
  reason: input.reason,
491
226
  terminalAt: now,
492
227
  tail: (input.tail ?? []).slice(-tombstoneTailLimit),
493
- paneClosedAt: null,
494
- notificationSentAt: null
228
+ paneClosedAt: null
495
229
  };
496
230
  session.status = "terminal";
497
231
  session.tombstone = tombstone;
@@ -521,16 +255,6 @@ var SessionManager = class {
521
255
  }
522
256
  return session;
523
257
  }
524
- markTerminalNotificationSent(id) {
525
- const session = this.get(id);
526
- const now = (/* @__PURE__ */ new Date()).toISOString();
527
- if (!session.tombstone) return session;
528
- if (!session.tombstone.notificationSentAt) {
529
- session.tombstone.notificationSentAt = now;
530
- session.updatedAt = now;
531
- }
532
- return session;
533
- }
534
258
  listByOpenCodeSession(openCodeSessionId) {
535
259
  return this.list().filter((session) => session.openCodeSessionId === openCodeSessionId);
536
260
  }
@@ -573,6 +297,17 @@ function buildCommandArgv(input, options = {}) {
573
297
  ];
574
298
  }
575
299
  //#endregion
300
+ //#region src/utils/debug.ts
301
+ function debug(message, ...details) {
302
+ if (!process.env.ZELLIJ_PTY_DEBUG) return;
303
+ console.warn(`[opencode-zellij] ${message}`, ...details);
304
+ }
305
+ //#endregion
306
+ //#region src/utils/errors.ts
307
+ function errorMessage(error) {
308
+ return error instanceof Error ? error.message : String(error);
309
+ }
310
+ //#endregion
576
311
  //#region src/zellij/parse.ts
577
312
  function numericProperty(object, keys) {
578
313
  for (const key of keys) {
@@ -663,6 +398,28 @@ function findTabName(value, tabId) {
663
398
  if (found !== void 0) return found;
664
399
  }
665
400
  }
401
+ function activeTabNameProperty(object) {
402
+ if (object.active !== true || object.is_plugin === true) return void 0;
403
+ const name = stringProperty$2(object, ["name", "title"]);
404
+ return typeof name === "string" ? name : void 0;
405
+ }
406
+ function findActiveTabName(value) {
407
+ if (Array.isArray(value)) {
408
+ for (const item of value) {
409
+ const found = findActiveTabName(item);
410
+ if (found !== void 0) return found;
411
+ }
412
+ return;
413
+ }
414
+ if (typeof value !== "object" || value === null) return void 0;
415
+ const object = value;
416
+ const name = activeTabNameProperty(object);
417
+ if (name !== void 0) return name;
418
+ for (const nested of Object.values(object)) {
419
+ const found = findActiveTabName(nested);
420
+ if (found !== void 0) return found;
421
+ }
422
+ }
666
423
  function parseTabName(listTabsJson, tabId) {
667
424
  try {
668
425
  return findTabName(JSON.parse(listTabsJson), tabId);
@@ -671,6 +428,14 @@ function parseTabName(listTabsJson, tabId) {
671
428
  return;
672
429
  }
673
430
  }
431
+ function parseActiveTabName(listTabsJson) {
432
+ try {
433
+ return findActiveTabName(JSON.parse(listTabsJson));
434
+ } catch (error) {
435
+ debug("parseActiveTabName failed", errorMessage(error));
436
+ return;
437
+ }
438
+ }
674
439
  //#endregion
675
440
  //#region src/zellij/cli.ts
676
441
  const execFileAsync$1 = promisify(execFile);
@@ -738,25 +503,28 @@ async function runZellij(actionArgs, options = {}) {
738
503
  }
739
504
  }
740
505
  var ZellijCli = class {
506
+ constructor(run = runZellij) {
507
+ this.run = run;
508
+ }
741
509
  async newPane(options) {
742
- return parsePaneId((await runZellij(buildNewPaneActionArgs(options))).stdout);
510
+ return parsePaneId((await this.run(buildNewPaneActionArgs(options))).stdout);
743
511
  }
744
512
  async writeChars(paneId, data) {
745
- await runZellij(zellijActionArgs("write-chars", [
513
+ await this.run(zellijActionArgs("write-chars", [
746
514
  "--pane-id",
747
515
  paneId,
748
516
  data
749
517
  ]));
750
518
  }
751
519
  async sendCtrlC(paneId) {
752
- await runZellij(zellijActionArgs("send-keys", [
520
+ await this.run(zellijActionArgs("send-keys", [
753
521
  "--pane-id",
754
522
  paneId,
755
523
  "Ctrl c"
756
524
  ]));
757
525
  }
758
526
  async closePane(paneId) {
759
- await runZellij(zellijActionArgs("close-pane", ["--pane-id", paneId]));
527
+ await this.run(zellijActionArgs("close-pane", ["--pane-id", paneId]));
760
528
  }
761
529
  closePaneSync(paneId) {
762
530
  ensureZellijTarget();
@@ -767,10 +535,10 @@ var ZellijCli = class {
767
535
  });
768
536
  }
769
537
  async focusPane(paneId) {
770
- await runZellij(zellijActionArgs("focus-pane-id", [paneId]));
538
+ await this.run(zellijActionArgs("focus-pane-id", [paneId]));
771
539
  }
772
540
  async dumpScreen(paneId) {
773
- return (await runZellij(zellijActionArgs("dump-screen", [
541
+ return (await this.run(zellijActionArgs("dump-screen", [
774
542
  "--pane-id",
775
543
  paneId,
776
544
  "--full"
@@ -779,21 +547,24 @@ var ZellijCli = class {
779
547
  async currentPaneTabId() {
780
548
  const paneId = process.env.ZELLIJ_PANE_ID;
781
549
  if (!paneId) return void 0;
782
- return parseCurrentPaneTabId((await runZellij(zellijActionArgs("list-panes", ["--json"]), { timeoutMs: 5e3 })).stdout, paneId);
550
+ return parseCurrentPaneTabId((await this.run(zellijActionArgs("list-panes", ["--json"]), { timeoutMs: 5e3 })).stdout, paneId);
783
551
  }
784
552
  async paneExists(paneId) {
785
- return parsePaneExists((await runZellij(zellijActionArgs("list-panes", ["--json"]), { timeoutMs: 5e3 })).stdout, paneId);
553
+ return parsePaneExists((await this.run(zellijActionArgs("list-panes", ["--json"]), { timeoutMs: 5e3 })).stdout, paneId);
786
554
  }
787
555
  async renameTab(title) {
788
556
  const tabId = await this.currentPaneTabId();
789
557
  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 }));
558
+ await this.run(tabId === void 0 ? buildRenameTabActionArgs(title) : buildRenameTabActionArgs(title, { tabId }));
791
559
  }
792
560
  async currentTabTitle() {
793
- if (!process.env.ZELLIJ_PANE_ID) return void 0;
561
+ if (!process.env.ZELLIJ_PANE_ID) {
562
+ if (!process.env.ZELLIJ_SESSION_NAME?.trim()) return void 0;
563
+ return parseActiveTabName((await this.run(zellijActionArgs("list-tabs", ["--json"]), { timeoutMs: 5e3 })).stdout);
564
+ }
794
565
  const tabId = await this.currentPaneTabId();
795
566
  if (tabId === void 0) return void 0;
796
- return parseTabName((await runZellij(zellijActionArgs("list-tabs", ["--json"]), { timeoutMs: 5e3 })).stdout, tabId);
567
+ return parseTabName((await this.run(zellijActionArgs("list-tabs", ["--json"]), { timeoutMs: 5e3 })).stdout, tabId);
797
568
  }
798
569
  };
799
570
  const zellijCli = new ZellijCli();
@@ -1038,13 +809,20 @@ function createExitCodeToken() {
1038
809
  return randomUUID().replaceAll("-", "");
1039
810
  }
1040
811
  function parseExitCodeMarker(line) {
1041
- const match = line.replace(ansiPattern, "").trim().match(markerPattern);
812
+ const match = line.replace(ansiPattern, "").replace(/\r?\n/g, "").trim().match(markerPattern);
1042
813
  if (!match?.[1] || !match[2]) return null;
1043
814
  return {
1044
815
  token: match[1],
1045
816
  exitCode: Number(match[2])
1046
817
  };
1047
818
  }
819
+ function parseExitCodeMarkerLines(lines, maxWindowLines = 8) {
820
+ for (let start = 0; start < lines.length; start += 1) for (let size = 1; size <= maxWindowLines && start + size <= lines.length; size += 1) {
821
+ const marker = parseExitCodeMarker(lines.slice(start, start + size).join("\n"));
822
+ if (marker) return marker;
823
+ }
824
+ return null;
825
+ }
1048
826
  //#endregion
1049
827
  //#region src/zellij/subscribe.ts
1050
828
  const maxStderrLines = 200;
@@ -1107,9 +885,9 @@ var SubscriberManager = class {
1107
885
  this.sessions = sessions;
1108
886
  this.maxBufferLines = maxBufferLines;
1109
887
  this.spawnProcess = dependencies.spawn ?? spawn;
1110
- this.dumpScreen = dependencies.dumpScreen ?? zellijCli.dumpScreen;
1111
- this.paneExists = dependencies.paneExists ?? zellijCli.paneExists;
1112
- this.closePane = dependencies.closePane ?? zellijCli.closePane;
888
+ this.dumpScreen = dependencies.dumpScreen ?? ((paneId) => zellijCli.dumpScreen(paneId));
889
+ this.paneExists = dependencies.paneExists ?? ((paneId) => zellijCli.paneExists(paneId));
890
+ this.closePane = dependencies.closePane ?? ((paneId) => zellijCli.closePane(paneId));
1113
891
  this.lifecycleHooks = dependencies.lifecycleHooks;
1114
892
  this.terminalTailLines = dependencies.terminalTailLines ?? 200;
1115
893
  }
@@ -1268,12 +1046,9 @@ var SubscriberManager = class {
1268
1046
  captureExitCode(sessionId, lines) {
1269
1047
  const session = this.sessions.get(sessionId);
1270
1048
  if (!session.exitCodeToken) return;
1271
- for (const line of lines) {
1272
- const marker = parseExitCodeMarker(line);
1273
- if (!marker || marker.token !== session.exitCodeToken) continue;
1274
- this.markSessionTerminal(sessionId, "exit_marker", { exitCode: marker.exitCode });
1275
- return;
1276
- }
1049
+ const marker = parseExitCodeMarkerLines(lines);
1050
+ if (!marker || marker.token !== session.exitCodeToken) return;
1051
+ this.markSessionTerminal(sessionId, "exit_marker", { exitCode: marker.exitCode });
1277
1052
  }
1278
1053
  handleStderr(sessionId, child, chunk) {
1279
1054
  const state = this.subscribers.get(sessionId);
@@ -1373,8 +1148,7 @@ function publicSession(session) {
1373
1148
  reason: session.tombstone.reason,
1374
1149
  terminalAt: session.tombstone.terminalAt,
1375
1150
  tailLines: session.tombstone.tail.length,
1376
- paneClosedAt: session.tombstone.paneClosedAt,
1377
- notificationSentAt: session.tombstone.notificationSentAt
1151
+ paneClosedAt: session.tombstone.paneClosedAt
1378
1152
  } : null
1379
1153
  };
1380
1154
  }
@@ -1538,7 +1312,7 @@ async function executeZellijPtyRead(args, dependencies = {}) {
1538
1312
  const nextAdviceApi = dependencies.nextAdvice ?? nextAdvice;
1539
1313
  const readOutputSnapshotApi = dependencies.readOutputSnapshot ?? readOutputSnapshot;
1540
1314
  const validateGrepApi = dependencies.validateGrep ?? validateGrep;
1541
- const paneExistsApi = dependencies.paneExists ?? zellijCli.paneExists;
1315
+ const paneExistsApi = dependencies.paneExists ?? ((paneId) => zellijCli.paneExists(paneId));
1542
1316
  const session = sessionManagerApi.get(args.id);
1543
1317
  const grepError = validateGrepApi(args.grep);
1544
1318
  if (grepError) return {
@@ -1714,13 +1488,6 @@ const requestSudoTool = tool({
1714
1488
  floating: true,
1715
1489
  exitCodeToken
1716
1490
  });
1717
- const warnings = [];
1718
- try {
1719
- await zellijCli.focusPane(paneId);
1720
- } catch (error) {
1721
- if (!(error instanceof Error ? error.message : String(error)).includes("already focused")) throw error;
1722
- warnings.push("Pane was already focused after creation.");
1723
- }
1724
1491
  const session = sessionManager.create({
1725
1492
  openCodeSessionId: context.sessionID,
1726
1493
  paneId,
@@ -1738,7 +1505,7 @@ const requestSudoTool = tool({
1738
1505
  session: publicSession(session),
1739
1506
  output: readOutputSnapshot(session.id),
1740
1507
  next: nextAdvice(false, "The user must review the summary and commands in Zellij, then type YES and any required credentials directly in the pane."),
1741
- warnings
1508
+ warnings: []
1742
1509
  });
1743
1510
  }
1744
1511
  });
@@ -1942,281 +1709,149 @@ const zellijPtyWriteTool = tool({
1942
1709
  }
1943
1710
  });
1944
1711
  //#endregion
1945
- //#region src/zellij/completion-notifications.ts
1946
- /** Fetches the prompt-mode idle guard status; unrelated to tab title state. */
1947
- async function fetchPromptIdleStatusSnapshot(client, workspaceRoot) {
1712
+ //#region src/utils/runtime.ts
1713
+ /**
1714
+ * Detect whether the plugin process is running inside an OpenCode TUI
1715
+ * session, as opposed to a headless `opencode run` invocation.
1716
+ *
1717
+ * OpenCode spawns the TUI's renderer as a worker child process and
1718
+ * explicitly sets `OPENCODE_PROCESS_ROLE=worker`
1719
+ * (see `packages/opencode/src/cli/cmd/tui/thread.ts` in opencode). The
1720
+ * headless `opencode run` command keeps the default `main` role set by
1721
+ * the CLI entry point, so this is the most reliable signal to tell TUI
1722
+ * from headless.
1723
+ *
1724
+ * Outside the TUI there is no surface for toasts, prompts, or Zellij
1725
+ * panes, and the plugin's lifecycle hooks (watchdogs, tab title actor,
1726
+ * completion notifications) misbehave. The plugin short-circuits to a
1727
+ * no-op in headless mode to avoid leaking side effects.
1728
+ */
1729
+ function isOpencodeTuiMode() {
1730
+ return process.env.OPENCODE_PROCESS_ROLE === "worker";
1731
+ }
1732
+ //#endregion
1733
+ //#region src/utils/logger.ts
1734
+ const defaultDebugLogPath = `${process.env.XDG_CACHE_HOME?.trim() || `${homedir()}/.cache`}/opencode-zellij/debug.log`;
1735
+ function buildRootLogger() {
1736
+ const filename = process.env.OPENCODE_ZELLIJ_DEBUG_LOG?.trim() || defaultDebugLogPath;
1737
+ if (!filename) return null;
1948
1738
  try {
1949
- if (!client.session?.status) {
1950
- debug("fetchPromptIdleStatusSnapshot: client.session.status not available");
1951
- return;
1952
- }
1953
- const result = await client.session.status({ query: { directory: workspaceRoot } });
1954
- const payload = result && typeof result === "object" && "data" in result ? result.data : result;
1955
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
1956
- debug("fetchPromptIdleStatusSnapshot received non-object payload");
1957
- return;
1958
- }
1959
- const entries = Object.entries(payload);
1960
- if (entries.length === 0) return {};
1961
- const snapshot = {};
1962
- for (const [sessionID, status] of entries) {
1963
- const parsed = parseSessionStatus(status);
1964
- if (parsed === void 0) {
1965
- debug("fetchPromptIdleStatusSnapshot received invalid status entry, rejecting entire snapshot");
1966
- return;
1967
- }
1968
- snapshot[sessionID] = parsed;
1969
- }
1970
- return snapshot;
1971
- } catch (err) {
1972
- debug("fetchPromptIdleStatusSnapshot failed", errorMessage(err));
1973
- return;
1739
+ mkdirSync(dirname(filename), { recursive: true });
1740
+ } catch {
1741
+ return null;
1742
+ }
1743
+ try {
1744
+ return new LogLayer({
1745
+ errorSerializer: serializeError,
1746
+ transport: new LogFileRotationTransport({
1747
+ filename: filename.includes("%DATE%") ? filename : `${filename}-%DATE%`,
1748
+ dateFormat: "YMD",
1749
+ size: "1M",
1750
+ maxLogs: "7d",
1751
+ frequency: "daily",
1752
+ compressOnRotate: true
1753
+ })
1754
+ });
1755
+ } catch {
1756
+ return null;
1974
1757
  }
1975
1758
  }
1976
- function parseSessionStatus(value) {
1977
- if (!value || typeof value !== "object" || !("type" in value)) return void 0;
1978
- const status = value;
1979
- if (status.type === "idle" || status.type === "busy") return { type: status.type };
1980
- if (status.type === "retry") return {
1981
- type: "retry",
1982
- attempt: typeof status.attempt === "number" ? status.attempt : 0,
1983
- message: typeof status.message === "string" ? status.message : "",
1984
- next: typeof status.next === "number" ? status.next : 0
1985
- };
1759
+ const rootLogger = buildRootLogger();
1760
+ /**
1761
+ * Returns a child logger that prefixes every message with `[name]`, or
1762
+ * null if the root logger failed to initialize.
1763
+ */
1764
+ function getChildLogger(name) {
1765
+ return rootLogger?.withPrefix(`[${name}]`) ?? null;
1766
+ }
1767
+ //#endregion
1768
+ //#region src/zellij/completion-notifications.ts
1769
+ const logger = getChildLogger("completion-notifications");
1770
+ async function postPromptAsync(serverUrl, sessionID, body) {
1771
+ const url = new URL(`/session/${encodeURIComponent(sessionID)}/prompt_async`, serverUrl);
1772
+ const headers = { "Content-Type": "application/json" };
1773
+ const directory = process.env.OPENCODE_DIRECTORY?.trim();
1774
+ if (directory) headers["x-opencode-directory"] = encodeURIComponent(directory);
1775
+ return fetch(url, {
1776
+ method: "POST",
1777
+ headers,
1778
+ body: JSON.stringify(body)
1779
+ });
1780
+ }
1781
+ function formatExitCode(exitCode) {
1782
+ return exitCode === null ? "?" : String(exitCode);
1986
1783
  }
1987
- const completionTitle = "Zellij PTY session completed";
1988
- const completionMessage = "A Zellij PTY session completed. Review the finished pane if needed.";
1989
- const queuedNoticeHeader = "[OpenCode] Zellij PTY completion notice";
1990
- function buildQueuedCompletionNotice(events) {
1991
- return [queuedNoticeHeader, ...events.map((event) => `- ${event.session.id} (${event.session.paneId}) 已完成,請使用 zellij_pty_read 讀取最終輸出並清理 pane。`)].join("\n");
1784
+ function buildCompletionPromptText(event) {
1785
+ const { paneId, exitCode } = event.session;
1786
+ return `[zellij_pty] pane ${paneId} exit=${formatExitCode(exitCode)} — call zellij_pty_read to read, then zellij_pty_kill to close.`;
1992
1787
  }
1993
1788
  function buildCompletionPromptRequest(event) {
1994
1789
  return {
1995
- path: { id: event.session.openCodeSessionId },
1996
- body: { parts: [{
1997
- type: "text",
1998
- text: completionMessage
1999
- }] }
2000
- };
2001
- }
2002
- function injectQueuedCompletionNotice(input, notice) {
2003
- if (typeof input === "string") return `${notice}\n\n${input}`;
2004
- if (!input || typeof input !== "object") return input;
2005
- const record = input;
2006
- if (Array.isArray(record.parts)) return {
2007
- ...record,
1790
+ sessionID: event.session.openCodeSessionId,
2008
1791
  parts: [{
2009
1792
  type: "text",
2010
- text: notice
2011
- }, ...record.parts]
2012
- };
2013
- if (typeof record.message === "string") return {
2014
- ...record,
2015
- message: `${notice}\n\n${record.message}`
2016
- };
2017
- if (typeof record.content === "string") return {
2018
- ...record,
2019
- content: `${notice}\n\n${record.content}`
2020
- };
2021
- if (typeof record.text === "string") return {
2022
- ...record,
2023
- text: `${notice}\n\n${record.text}`
2024
- };
2025
- return {
2026
- ...record,
2027
- message: notice
2028
- };
2029
- }
2030
- function evaluateCompletionPromptDecision(input) {
2031
- if (input.config.mode !== "prompt") return {
2032
- shouldPrompt: false,
2033
- shouldQueue: false,
2034
- reason: "prompt mode disabled"
2035
- };
2036
- if (input.event.session.humanInputOnly || !input.event.session.allowAgentInput) return {
2037
- shouldPrompt: false,
2038
- shouldQueue: true,
2039
- reason: "human-input-only session"
2040
- };
2041
- if (!input.promptClientAvailable) return {
2042
- shouldPrompt: false,
2043
- shouldQueue: true,
2044
- reason: "prompt client unavailable"
2045
- };
2046
- if (!input.event.session.openCodeSessionId) return {
2047
- shouldPrompt: false,
2048
- shouldQueue: true,
2049
- reason: "session id unavailable"
2050
- };
2051
- if (!input.snapshotAvailable) return {
2052
- shouldPrompt: false,
2053
- shouldQueue: true,
2054
- reason: "session status snapshot unavailable"
2055
- };
2056
- if (input.config.prompt.maxAttempts <= 0 || input.promptAttemptCount >= input.config.prompt.maxAttempts) return {
2057
- shouldPrompt: false,
2058
- shouldQueue: true,
2059
- reason: "prompt max attempts reached"
2060
- };
2061
- if (input.config.prompt.cooldownMs > 0 && input.lastPromptAttemptAt !== null && input.now - input.lastPromptAttemptAt < input.config.prompt.cooldownMs) return {
2062
- shouldPrompt: false,
2063
- shouldQueue: true,
2064
- reason: "prompt cooldown active"
2065
- };
2066
- if (input.config.prompt.requireIdle) {
2067
- const sessionId = input.event.session.openCodeSessionId;
2068
- if (!sessionId) return {
2069
- shouldPrompt: false,
2070
- shouldQueue: true,
2071
- reason: "session status unavailable"
2072
- };
2073
- const status = sessionId ? input.snapshot?.[sessionId] : void 0;
2074
- if (!status) return {
2075
- shouldPrompt: false,
2076
- shouldQueue: true,
2077
- reason: "session status unavailable"
2078
- };
2079
- if (status && status.type !== "idle") return {
2080
- shouldPrompt: false,
2081
- shouldQueue: true,
2082
- reason: "session not idle"
2083
- };
2084
- }
2085
- return {
2086
- shouldPrompt: true,
2087
- shouldQueue: false,
2088
- reason: "prompt allowed"
1793
+ text: buildCompletionPromptText(event)
1794
+ }]
2089
1795
  };
2090
1796
  }
2091
- var SessionCompletionNotificationQueue = class {
2092
- states = /* @__PURE__ */ new Map();
2093
- constructor(context, hooks = {}, clock = () => Date.now()) {
1797
+ var SessionCompletionNotificationManager = class {
1798
+ seen = /* @__PURE__ */ new Set();
1799
+ constructor(context) {
2094
1800
  this.context = context;
2095
- this.hooks = hooks;
2096
- this.clock = clock;
2097
- }
2098
- hasPending(sessionId) {
2099
- return this.states.get(sessionId)?.queued ?? false;
2100
- }
2101
- clearSession(sessionId) {
2102
- this.states.delete(sessionId);
2103
- }
2104
- clearAll() {
2105
- this.states.clear();
2106
1801
  }
2107
1802
  dispose() {
2108
- this.clearAll();
1803
+ this.seen.clear();
2109
1804
  }
2110
1805
  async handleSessionTerminal(event) {
2111
- if (this.context.config.mode === "off") return;
2112
- if (this.states.has(event.sessionId) || event.session.tombstone?.notificationSentAt) return;
2113
- const state = {
2114
- event,
2115
- queued: false,
2116
- toastSent: false,
2117
- promptAttempts: 0,
2118
- promptAttemptedAt: null
2119
- };
2120
- this.states.set(event.sessionId, state);
2121
- switch (this.context.config.mode) {
2122
- case "queue":
2123
- state.queued = true;
2124
- return;
2125
- case "toast":
2126
- await this.sendToast(state);
2127
- this.finalize(state);
2128
- return;
2129
- case "queue+toast":
2130
- state.queued = true;
2131
- await this.sendToast(state);
2132
- return;
2133
- case "prompt":
2134
- await this.tryPromptOrQueue(state);
2135
- break;
2136
- default:
2137
- }
2138
- }
2139
- injectQueuedChatMessage(input) {
2140
- const pending = [...this.states.values()].filter((state) => state.queued);
2141
- if (pending.length === 0) return input;
2142
- const notice = buildQueuedCompletionNotice(pending.map((state) => state.event));
2143
- for (const state of pending) {
2144
- if (!state.toastSent) this.context.markSent(state.event.sessionId);
2145
- this.finalize(state);
2146
- }
2147
- return injectQueuedCompletionNotice(input, notice);
2148
- }
2149
- async tryPromptOrQueue(state) {
2150
- if (!state.event.session.openCodeSessionId) {
2151
- state.queued = true;
1806
+ logger?.withMetadata({
1807
+ session: event.sessionId,
1808
+ reason: event.reason,
1809
+ paneId: event.session.paneId,
1810
+ openCodeSessionId: event.session.openCodeSessionId ?? "null"
1811
+ }).info("handleSessionTerminal");
1812
+ if (this.seen.has(event.sessionId)) return;
1813
+ this.seen.add(event.sessionId);
1814
+ if (!event.session.openCodeSessionId) {
1815
+ logger?.withMetadata({ session: event.sessionId }).info("skipped: no openCodeSessionId");
2152
1816
  return;
2153
1817
  }
2154
1818
  const session = this.context.client.session;
2155
- const prompt = session?.prompt ?? session?.promptAsync;
2156
- const statusSnapshot = await fetchPromptIdleStatusSnapshot(this.context.client, this.context.workspaceRoot);
2157
- const decision = evaluateCompletionPromptDecision({
2158
- event: state.event,
2159
- config: this.context.config,
2160
- snapshot: statusSnapshot,
2161
- snapshotAvailable: statusSnapshot !== void 0,
2162
- now: this.clock(),
2163
- lastPromptAttemptAt: state.promptAttemptedAt,
2164
- promptAttemptCount: state.promptAttempts,
2165
- promptClientAvailable: Boolean(prompt)
2166
- });
2167
- if (!decision.shouldPrompt) {
2168
- state.queued = decision.shouldQueue;
1819
+ const sdkPrompt = session?.promptAsync ?? session?.prompt;
1820
+ const request = buildCompletionPromptRequest(event);
1821
+ const sessionID = request.sessionID;
1822
+ if (sdkPrompt) try {
1823
+ logger?.withMetadata({ sessionID }).info("SDK prompt attempt");
1824
+ await sdkPrompt(request);
1825
+ logger?.withMetadata({ sessionID }).info("SDK prompt ok");
2169
1826
  return;
2170
- }
2171
- if (this.hooks.prompt) try {
2172
- const maybePromise = this.hooks.prompt(state.event);
2173
- if (maybePromise && typeof maybePromise.then === "function") await maybePromise;
2174
- } catch (error) {
2175
- debug("completion notification prompt hook failed", errorMessage(error));
2176
- }
2177
- if (!session || !prompt) {
2178
- state.queued = true;
2179
- return;
2180
- }
2181
- state.promptAttempts += 1;
2182
- state.promptAttemptedAt = this.clock();
2183
- try {
2184
- if (session.prompt) await session.prompt(buildCompletionPromptRequest(state.event));
2185
- else if (session.promptAsync) await session.promptAsync(buildCompletionPromptRequest(state.event));
2186
- else {
2187
- state.queued = true;
2188
- return;
2189
- }
2190
- this.context.markSent(state.event.sessionId);
2191
- this.finalize(state);
2192
1827
  } catch (error) {
2193
- debug("completion notification prompt failed", errorMessage(error));
2194
- state.queued = true;
1828
+ logger?.withMetadata({
1829
+ sessionID,
1830
+ error: errorMessage(error)
1831
+ }).warn("SDK prompt failed, falling back to HTTP");
2195
1832
  }
2196
- }
2197
- async sendToast(state) {
2198
- const toast = this.context.client.tui?.showToast;
2199
- if (!toast) {
2200
- debug("completion notification toast skipped: client.tui.showToast unavailable");
1833
+ else logger?.withMetadata({
1834
+ sessionID,
1835
+ sessionKeys: session ? Object.keys(session).join(",") : "null"
1836
+ }).info("no SDK prompt on client, using HTTP fallback");
1837
+ if (!this.context.serverUrl) {
1838
+ logger?.withMetadata({ sessionID }).error("no serverUrl for HTTP fallback");
2201
1839
  return;
2202
1840
  }
2203
1841
  try {
2204
- await toast({ body: {
2205
- title: completionTitle,
2206
- message: completionMessage,
2207
- variant: "success",
2208
- duration: 1e4
2209
- } });
2210
- state.toastSent = true;
2211
- this.context.markSent(state.event.sessionId);
2212
- if (!state.queued) this.finalize(state);
1842
+ const response = await postPromptAsync(this.context.serverUrl, sessionID, { parts: request.parts });
1843
+ logger?.withMetadata({
1844
+ sessionID,
1845
+ status: response.status,
1846
+ ok: response.ok
1847
+ }).info("HTTP fallback response");
2213
1848
  } catch (error) {
2214
- debug("completion notification toast failed", errorMessage(error));
1849
+ logger?.withMetadata({
1850
+ sessionID,
1851
+ error: errorMessage(error)
1852
+ }).error("HTTP fallback threw");
2215
1853
  }
2216
1854
  }
2217
- finalize(state) {
2218
- this.states.delete(state.event.sessionId);
2219
- }
2220
1855
  };
2221
1856
  //#endregion
2222
1857
  //#region src/zellij/shutdown-cleanup.ts
@@ -2558,9 +2193,6 @@ var TabTitleManager = class {
2558
2193
  emojis;
2559
2194
  enabled;
2560
2195
  destroyed = false;
2561
- originalTabTitle;
2562
- originalTabTitleLoaded = false;
2563
- originalTabTitlePromise;
2564
2196
  destroyPromise;
2565
2197
  actor;
2566
2198
  constructor(options) {
@@ -2586,8 +2218,6 @@ var TabTitleManager = class {
2586
2218
  }
2587
2219
  async renderImmediate() {
2588
2220
  if (!this.enabled || this.destroyed) return;
2589
- await this.ensureOriginalTabTitle();
2590
- if (this.destroyed) return;
2591
2221
  this.desiredTitle = this.buildTitle();
2592
2222
  this.clearDebounceTimer();
2593
2223
  await this.syncDesiredTitle();
@@ -2609,7 +2239,6 @@ var TabTitleManager = class {
2609
2239
  async syncDesiredTitle() {
2610
2240
  if (!this.enabled || this.destroyed) return;
2611
2241
  const generation = this.syncGeneration;
2612
- await this.ensureOriginalTabTitle();
2613
2242
  if (this.destroyed || generation !== this.syncGeneration) return;
2614
2243
  if (this.syncInFlight) return this.syncPromise;
2615
2244
  this.syncInFlight = true;
@@ -2659,23 +2288,6 @@ var TabTitleManager = class {
2659
2288
  if (this.debounceTimer) clearTimeout(this.debounceTimer);
2660
2289
  this.debounceTimer = void 0;
2661
2290
  }
2662
- async ensureOriginalTabTitle() {
2663
- if (!this.enabled || this.originalTabTitleLoaded) return;
2664
- if (this.originalTabTitlePromise) return this.originalTabTitlePromise;
2665
- this.originalTabTitlePromise = this.saveOriginalTabTitle();
2666
- return this.originalTabTitlePromise;
2667
- }
2668
- async saveOriginalTabTitle() {
2669
- try {
2670
- const title = await this.cli.currentTabTitle();
2671
- if (title !== void 0) this.originalTabTitle = title;
2672
- } catch (error) {
2673
- debug("TabTitleManager failed to save original tab title", errorMessage(error));
2674
- } finally {
2675
- this.originalTabTitleLoaded = true;
2676
- this.originalTabTitlePromise = void 0;
2677
- }
2678
- }
2679
2291
  destroy() {
2680
2292
  if (this.destroyed) return this.destroyPromise ?? Promise.resolve();
2681
2293
  this.destroyed = true;
@@ -2683,21 +2295,12 @@ var TabTitleManager = class {
2683
2295
  this.desiredTitle = void 0;
2684
2296
  this.clearDebounceTimer();
2685
2297
  this.clearRetryTimer();
2686
- if (!this.enabled) return Promise.resolve();
2687
- this.destroyPromise = this.restoreOriginalTabTitle().catch((error) => debug("TabTitleManager failed to restore original tab title", errorMessage(error)));
2688
- return this.destroyPromise;
2689
- }
2690
- async restoreOriginalTabTitle() {
2691
- await this.originalTabTitlePromise;
2692
- await this.syncPromise;
2693
- const originalTitle = this.originalTabTitle;
2694
- this.originalTabTitle = void 0;
2695
- if (originalTitle === void 0) return;
2696
- await this.cli.renameTab(originalTitle);
2298
+ return Promise.resolve();
2697
2299
  }
2698
2300
  };
2699
2301
  //#endregion
2700
2302
  //#region src/plugin.ts
2303
+ const PLUGIN_ID = "opencode-zellij";
2701
2304
  function createPtyTools(defaultCleanupExitedPaneOnRead) {
2702
2305
  return {
2703
2306
  zellij_pty_spawn: zellijPtySpawnTool,
@@ -2713,29 +2316,6 @@ function getProjectName(path) {
2713
2316
  function getWorkspaceRoot(input) {
2714
2317
  return input.worktree || input.directory || process.cwd();
2715
2318
  }
2716
- function showUpdateToast(client, result) {
2717
- if (result.type === "updated") client.tui.showToast({ body: {
2718
- title: "opencode-zellij updated",
2719
- message: `Updated to ${result.toVersion}. Restart OpenCode to apply the changes.`,
2720
- variant: "success",
2721
- duration: 1e4
2722
- } }).catch((error) => debug("show update toast for successful update failed", errorMessage(error)));
2723
- else if (result.type === "failed") client.tui.showToast({ body: {
2724
- title: "opencode-zellij update failed",
2725
- message: `Failed to update to ${result.latestVersion}.`,
2726
- variant: "error",
2727
- duration: 8e3
2728
- } }).catch((error) => debug("show update toast for failed update failed", errorMessage(error)));
2729
- }
2730
- function startAutoUpdateCheck(client, importMetaUrl, check = checkAndUpdate) {
2731
- (async () => {
2732
- try {
2733
- showUpdateToast(client, await check({ importMetaUrl }));
2734
- } catch (cause) {
2735
- debug("auto-update check failed", errorMessage(cause));
2736
- }
2737
- })();
2738
- }
2739
2319
  async function cleanupStep(stepName, sessionId, step) {
2740
2320
  try {
2741
2321
  await step();
@@ -2751,6 +2331,10 @@ async function cleanupDeletedSession(sessionId) {
2751
2331
  }
2752
2332
  function createZellijPtyPlugin(dependencies = {}) {
2753
2333
  return async (input) => {
2334
+ if (!isOpencodeTuiMode()) {
2335
+ debug("opencode-zellij disabled: not running inside an OpenCode TUI session");
2336
+ return {};
2337
+ }
2754
2338
  const { config, warnings } = await loadConfig(input);
2755
2339
  for (const warning of warnings) debug(warning);
2756
2340
  configureSudoPane(config.pty.sudoPane === "allow");
@@ -2778,60 +2362,35 @@ function createZellijPtyPlugin(dependencies = {}) {
2778
2362
  branch: config.tabTitle.emojiBranch
2779
2363
  }
2780
2364
  }) : void 0;
2781
- const client = input.client;
2782
- const completionNotifications = config.pty.completionNotification.mode === "off" ? void 0 : dependencies.createCompletionNotifications?.({
2783
- client,
2784
- workspaceRoot,
2785
- config: config.pty.completionNotification,
2786
- markSent(sessionId) {
2787
- try {
2788
- sessionManager.markTerminalNotificationSent(sessionId);
2789
- } catch (error) {
2790
- debug("mark terminal notification sent failed", errorMessage(error));
2791
- }
2792
- }
2793
- }) ?? new SessionCompletionNotificationQueue({
2794
- client,
2795
- workspaceRoot,
2796
- config: config.pty.completionNotification,
2797
- markSent(sessionId) {
2798
- try {
2799
- sessionManager.markTerminalNotificationSent(sessionId);
2800
- } catch (error) {
2801
- debug("mark terminal notification sent failed", errorMessage(error));
2802
- }
2803
- }
2365
+ const completionNotifications = dependencies.createCompletionNotifications?.({
2366
+ client: { session: input.client?.session },
2367
+ serverUrl: input.serverUrl
2368
+ }) ?? new SessionCompletionNotificationManager({
2369
+ client: { session: input.client?.session },
2370
+ serverUrl: input.serverUrl
2804
2371
  });
2805
- subscriberManager.setLifecycleHooks(completionNotifications ? { onSessionTerminal: (event) => void completionNotifications.handleSessionTerminal(event).catch((error) => debug("completion notification lifecycle hook failed", errorMessage(error))) } : void 0);
2372
+ subscriberManager.setLifecycleHooks({ onSessionTerminal: (event) => void completionNotifications.handleSessionTerminal(event).catch((error) => debug("completion notification lifecycle hook failed", errorMessage(error))) });
2806
2373
  if (actor) await actor.ready;
2807
2374
  tabTitleManager?.renderImmediate().catch((error) => debug("initial tab title render failed", errorMessage(error)));
2808
- if (config.autoUpdate) (dependencies.startAutoUpdateCheck ?? startAutoUpdateCheck)(client, dependencies.importMetaUrl ?? import.meta.url);
2809
2375
  return {
2810
2376
  async event(input) {
2811
2377
  const event = input.event;
2812
2378
  if (actor && tabTitleManager) {
2813
2379
  await actor.handleEvent(event);
2814
- if (event.type === "server.instance.disposed" || event.type === "global.disposed") await tabTitleManager.destroy();
2815
- else tabTitleManager.scheduleUpdate();
2380
+ tabTitleManager.scheduleUpdate();
2816
2381
  }
2817
2382
  if (event.type === "server.instance.disposed" || event.type === "global.disposed") {
2818
- completionNotifications?.clearAll();
2819
- completionNotifications?.dispose();
2383
+ completionNotifications.dispose();
2820
2384
  subscriberManager.setLifecycleHooks(void 0);
2821
2385
  }
2822
2386
  if (event.type === "session.deleted") {
2823
2387
  const sessionID = deletedSessionID$1(event);
2824
2388
  if (!sessionID) return;
2825
2389
  const sessions = sessionManager.listByOpenCodeSession(sessionID);
2826
- for (const session of sessions) completionNotifications?.clearSession(session.id);
2827
2390
  await Promise.all(sessions.map((session) => cleanupDeletedSession(session.id)));
2828
2391
  }
2829
2392
  },
2830
- "chat.message": async (_input, output) => {
2831
- const injected = completionNotifications?.injectQueuedChatMessage(output) ?? output;
2832
- if (injected !== output && injected && typeof injected === "object" && Array.isArray(injected.parts)) output.parts = injected.parts;
2833
- },
2834
- "tool": config.pty.enabled ? {
2393
+ tool: config.pty.enabled ? {
2835
2394
  ...createPtyTools(config.pty.cleanupExitedPaneOnRead),
2836
2395
  ...config.pty.sudoPane === "hide" ? {} : { zellij_pty_request_sudo: requestSudoTool }
2837
2396
  } : {}
@@ -2839,7 +2398,11 @@ function createZellijPtyPlugin(dependencies = {}) {
2839
2398
  };
2840
2399
  }
2841
2400
  const ZellijPtyPlugin = createZellijPtyPlugin();
2401
+ var plugin_default = {
2402
+ id: PLUGIN_ID,
2403
+ server: ZellijPtyPlugin
2404
+ };
2842
2405
  //#endregion
2843
- export { ZellijPtyPlugin, ZellijPtyPlugin as default, createZellijPtyPlugin, showUpdateToast, startAutoUpdateCheck };
2406
+ export { ZellijPtyPlugin, createZellijPtyPlugin, plugin_default as default };
2844
2407
 
2845
2408
  //# sourceMappingURL=index.mjs.map