opencode-zellij 0.0.16 → 0.0.18

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) {
@@ -1393,48 +1128,51 @@ var SubscriberManager = class {
1393
1128
  const subscriberManager = new SubscriberManager(sessionManager);
1394
1129
  //#endregion
1395
1130
  //#region src/tools/format.ts
1396
- function publicSession(session) {
1397
- return {
1131
+ function publicSession(session, options = {}) {
1132
+ const status = session.status === "terminal" ? "exited" : session.status;
1133
+ const summary = {
1398
1134
  id: session.id,
1399
1135
  paneId: session.paneId,
1400
1136
  title: session.title,
1401
1137
  command: session.command,
1402
- args: session.args,
1403
- cwd: session.cwd,
1404
- status: session.status === "terminal" ? "exited" : session.status,
1405
- lineCount: session.lineCount,
1406
- createdAt: session.createdAt,
1407
- updatedAt: session.updatedAt,
1408
- agentWritable: session.allowAgentInput,
1409
- humanInputOnly: session.humanInputOnly,
1410
- exitCode: session.exitCode,
1411
- exitedAt: session.exitedAt,
1412
- tombstone: session.tombstone ? {
1413
- reason: session.tombstone.reason,
1414
- terminalAt: session.tombstone.terminalAt,
1415
- tailLines: session.tombstone.tail.length,
1416
- paneClosedAt: session.tombstone.paneClosedAt,
1417
- notificationSentAt: session.tombstone.notificationSentAt
1418
- } : null
1138
+ status
1419
1139
  };
1140
+ if (options.agentWritable === false) summary.humanInputOnly = true;
1141
+ if (options.includeTombstone) summary.tombstone = session.tombstone ? {
1142
+ reason: session.tombstone.reason,
1143
+ terminalAt: session.tombstone.terminalAt,
1144
+ tailLines: session.tombstone.tail.length,
1145
+ paneClosedAt: session.tombstone.paneClosedAt
1146
+ } : null;
1147
+ return summary;
1420
1148
  }
1421
- function nextAdvice(retryable, reason) {
1149
+ function jsonResponse(value) {
1150
+ return JSON.stringify(value, null, 2);
1151
+ }
1152
+ function publicCompletedPane(session) {
1153
+ const status = session.status === "terminal" ? "exited" : session.status;
1154
+ const safeStatus = status === "exited" || status === "killed" || status === "unknown" ? status : "unknown";
1422
1155
  return {
1423
- retryable,
1424
- reason
1156
+ id: session.id,
1157
+ paneId: session.paneId,
1158
+ status: safeStatus,
1159
+ exitCode: session.exitCode,
1160
+ reason: session.tombstone?.reason ?? null
1425
1161
  };
1426
1162
  }
1427
- function jsonResponse(value) {
1428
- return JSON.stringify(value, null, 2);
1163
+ function completedPanesFromSessions(sessions) {
1164
+ const completedPanes = sessions.filter((session) => session.status === "terminal").map(publicCompletedPane);
1165
+ return {
1166
+ completedPaneIds: completedPanes.map((pane) => pane.id),
1167
+ completedPanes
1168
+ };
1429
1169
  }
1430
1170
  //#endregion
1431
1171
  //#region src/tools/output.ts
1432
1172
  function emptyOutputSnapshot(lineCount = 0) {
1433
1173
  return {
1434
1174
  text: "",
1435
- lines: [],
1436
1175
  lineCount,
1437
- returned: 0,
1438
1176
  truncated: false
1439
1177
  };
1440
1178
  }
@@ -1456,20 +1194,20 @@ function readOutputSnapshot(sessionId, options = {}) {
1456
1194
  ignoreCase: options.ignoreCase
1457
1195
  });
1458
1196
  sessionManager.updateLineCount(sessionId, buffered.lineCount);
1459
- return {
1197
+ const snapshot = {
1460
1198
  text: buffered.lines.join("\n"),
1461
- lines: buffered.lines,
1462
1199
  lineCount: buffered.lineCount,
1463
- returned: buffered.returned,
1464
1200
  truncated: buffered.offset > 0
1465
1201
  };
1202
+ if (options.grep !== void 0) snapshot.matched = buffered.returned;
1203
+ return snapshot;
1466
1204
  }
1467
1205
  function outputMatches(sessionId, grep, ignoreCase) {
1468
- return readOutputSnapshot(sessionId, {
1206
+ return (readOutputSnapshot(sessionId, {
1469
1207
  maxLines: 5e3,
1470
1208
  grep,
1471
1209
  ignoreCase
1472
- }).returned > 0;
1210
+ }).matched ?? 0) > 0;
1473
1211
  }
1474
1212
  //#endregion
1475
1213
  //#region src/tools/pane-cleanup.ts
@@ -1528,7 +1266,6 @@ async function executeZellijPtyKill(args, dependencies = {}) {
1528
1266
  cleanedUp: false,
1529
1267
  session: publicSession(sessionManager.updateStatus(session.id, "unknown")),
1530
1268
  output,
1531
- next: nextAdvice(true, "close-pane failed and the pane may still be running; the session was kept so kill can be retried."),
1532
1269
  warnings
1533
1270
  };
1534
1271
  }
@@ -1552,7 +1289,6 @@ async function finalizeKilledSession(sessionId, paneId, output, warnings) {
1552
1289
  id: sessionId,
1553
1290
  paneId,
1554
1291
  output,
1555
- next: nextAdvice(false, "Session was closed and removed from the in-memory registry."),
1556
1292
  warnings
1557
1293
  };
1558
1294
  }
@@ -1562,10 +1298,14 @@ const zellijPtyListTool = tool({
1562
1298
  description: "List known Zellij pane-backed PTY sessions created by this plugin process for the current OpenCode session.",
1563
1299
  args: {},
1564
1300
  async execute(_args, context) {
1565
- return jsonResponse({ sessions: sessionManager.listByOpenCodeSession(context.sessionID).map((session) => ({
1566
- ...publicSession(session),
1567
- subscriber: subscriberManager.status(session.id)
1568
- })) });
1301
+ const sessionList = sessionManager.listByOpenCodeSession(context.sessionID);
1302
+ return jsonResponse({
1303
+ sessions: sessionList.map((session) => ({
1304
+ ...publicSession(session, { includeTombstone: true }),
1305
+ subscriber: subscriberManager.status(session.id)
1306
+ })),
1307
+ ...completedPanesFromSessions(sessionList)
1308
+ });
1569
1309
  }
1570
1310
  });
1571
1311
  //#endregion
@@ -1575,26 +1315,22 @@ async function executeZellijPtyRead(args, dependencies = {}) {
1575
1315
  const sessionManagerApi = dependencies.sessionManager ?? sessionManager;
1576
1316
  const subscriberManagerApi = dependencies.subscriberManager ?? subscriberManager;
1577
1317
  const publicSessionApi = dependencies.publicSession ?? publicSession;
1578
- const nextAdviceApi = dependencies.nextAdvice ?? nextAdvice;
1579
1318
  const readOutputSnapshotApi = dependencies.readOutputSnapshot ?? readOutputSnapshot;
1580
1319
  const validateGrepApi = dependencies.validateGrep ?? validateGrep;
1581
1320
  const paneExistsApi = dependencies.paneExists ?? ((paneId) => zellijCli.paneExists(paneId));
1582
1321
  const session = sessionManagerApi.get(args.id);
1583
1322
  const grepError = validateGrepApi(args.grep);
1584
1323
  if (grepError) return {
1585
- session: publicSessionApi(session),
1324
+ session: publicSessionApi(session, { includeTombstone: true }),
1586
1325
  output: {
1587
1326
  text: "",
1588
- lines: [],
1589
1327
  lineCount: session.lineCount,
1590
- returned: 0,
1591
1328
  truncated: false
1592
1329
  },
1593
- next: nextAdviceApi(false, `Invalid grep regex: ${grepError}`),
1594
1330
  subscriberActive: false,
1595
1331
  subscriberLastExitedAt: null,
1596
1332
  subscriberErrors: [],
1597
- warnings: [],
1333
+ warnings: [`Invalid grep regex: ${grepError}`],
1598
1334
  cleanup: {
1599
1335
  requested: false,
1600
1336
  performed: false,
@@ -1622,9 +1358,8 @@ async function executeZellijPtyRead(args, dependencies = {}) {
1622
1358
  });
1623
1359
  if (cleanup.warning) warnings.push(cleanup.warning);
1624
1360
  return {
1625
- session: publicSessionApi(session),
1361
+ session: publicSessionApi(session, { includeTombstone: true }),
1626
1362
  output,
1627
- next: nextAdviceApi(!isCompletedSession(session.status), nextReadReason(session.status)),
1628
1363
  subscriberActive: statusAfterStart.active,
1629
1364
  subscriberLastExitedAt: statusAfterStart.lastExitedAt,
1630
1365
  subscriberErrors: subscriberManagerApi.stderr(session.id),
@@ -1686,12 +1421,6 @@ createZellijPtyReadTool();
1686
1421
  function isCompletedSession(status) {
1687
1422
  return status === "terminal" || status === "exited" || status === "killed";
1688
1423
  }
1689
- function nextReadReason(status) {
1690
- if (status === "terminal") return "Session has finished; the final output is retained until the completed pane is read and cleaned up.";
1691
- if (status === "running") return "Session is still running; read again later if more output is expected.";
1692
- if (status === "unknown") return "Session state is unknown because the subscriber is inactive; output may be stale, but retrying read may restart observation.";
1693
- return "Session is no longer running.";
1694
- }
1695
1424
  //#endregion
1696
1425
  //#region src/utils/pane-title.ts
1697
1426
  const generatedInstanceId = randomUUID().replaceAll("-", "").slice(0, 8);
@@ -1768,10 +1497,8 @@ const requestSudoTool = tool({
1768
1497
  registerPaneForWatchdog(session);
1769
1498
  await subscriberManager.start(session);
1770
1499
  return jsonResponse({
1771
- session: publicSession(session),
1772
- output: readOutputSnapshot(session.id),
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."),
1774
- warnings: []
1500
+ session: publicSession(session, { agentWritable: false }),
1501
+ output: readOutputSnapshot(session.id)
1775
1502
  });
1776
1503
  }
1777
1504
  });
@@ -1781,7 +1508,6 @@ const defaultSleepSeconds = 1;
1781
1508
  const defaultProbeTimeoutSeconds = 20;
1782
1509
  const pollIntervalMs = 250;
1783
1510
  async function runProbe(probe, outputReader) {
1784
- const startedAt = Date.now();
1785
1511
  const effectiveProbe = probe ?? {
1786
1512
  type: "sleep",
1787
1513
  seconds: defaultSleepSeconds
@@ -1789,16 +1515,16 @@ async function runProbe(probe, outputReader) {
1789
1515
  if (effectiveProbe.type === "sleep") {
1790
1516
  const seconds = effectiveProbe.seconds ?? defaultSleepSeconds;
1791
1517
  await setTimeout$1(seconds * 1e3);
1792
- return result(effectiveProbe.type, true, `Slept for ${seconds}s.`, startedAt);
1518
+ return result(effectiveProbe.type, true, `Slept for ${seconds}s.`);
1793
1519
  }
1794
1520
  if (effectiveProbe.type === "output") {
1795
1521
  const timeoutSeconds = effectiveProbe.timeoutSeconds ?? defaultProbeTimeoutSeconds;
1796
1522
  const deadline = Date.now() + timeoutSeconds * 1e3;
1797
1523
  while (Date.now() <= deadline) {
1798
- if (outputReader(effectiveProbe.grep, effectiveProbe.ignoreCase)) return result(effectiveProbe.type, true, `Observed output matching /${effectiveProbe.grep}/.`, startedAt);
1524
+ if (outputReader(effectiveProbe.grep, effectiveProbe.ignoreCase)) return result(effectiveProbe.type, true, `Observed output matching /${effectiveProbe.grep}/.`);
1799
1525
  await setTimeout$1(pollIntervalMs);
1800
1526
  }
1801
- return result(effectiveProbe.type, false, `Timed out after ${timeoutSeconds}s waiting for output matching /${effectiveProbe.grep}/.`, startedAt);
1527
+ return result(effectiveProbe.type, false, `Timed out after ${timeoutSeconds}s waiting for output matching /${effectiveProbe.grep}/.`);
1802
1528
  }
1803
1529
  const timeoutSeconds = effectiveProbe.timeoutSeconds ?? defaultProbeTimeoutSeconds;
1804
1530
  const deadline = Date.now() + timeoutSeconds * 1e3;
@@ -1810,7 +1536,7 @@ async function runProbe(probe, outputReader) {
1810
1536
  const response = await fetch(effectiveProbe.url, { signal: AbortSignal.timeout(Math.min(remainingMs, 3e3)) });
1811
1537
  if (expectStatus === void 0 ? response.status >= 200 && response.status < 400 : response.status === expectStatus) {
1812
1538
  const expected = expectStatus === void 0 ? "2xx/3xx" : String(expectStatus);
1813
- return result(effectiveProbe.type, true, `HTTP probe ${effectiveProbe.url} returned expected status ${expected}.`, startedAt);
1539
+ return result(effectiveProbe.type, true, `HTTP probe ${effectiveProbe.url} returned expected status ${expected}.`);
1814
1540
  }
1815
1541
  lastError = `HTTP ${response.status}`;
1816
1542
  } catch (error) {
@@ -1818,14 +1544,13 @@ async function runProbe(probe, outputReader) {
1818
1544
  }
1819
1545
  await setTimeout$1(pollIntervalMs);
1820
1546
  }
1821
- return result(effectiveProbe.type, false, `Timed out after ${timeoutSeconds}s probing ${effectiveProbe.url}: ${lastError}.`, startedAt);
1547
+ return result(effectiveProbe.type, false, `Timed out after ${timeoutSeconds}s probing ${effectiveProbe.url}: ${lastError}.`);
1822
1548
  }
1823
- function result(type, ok, message, startedAt) {
1549
+ function result(type, ok, message) {
1824
1550
  return {
1825
1551
  type,
1826
1552
  ok,
1827
- message,
1828
- elapsedMs: Date.now() - startedAt
1553
+ message
1829
1554
  };
1830
1555
  }
1831
1556
  //#endregion
@@ -1888,12 +1613,12 @@ const zellijPtySpawnTool = tool({
1888
1613
  await subscriberManager.start(session);
1889
1614
  const probe = await runProbe(args.probe, (grep, ignoreCase) => outputMatches(session.id, grep, ignoreCase));
1890
1615
  const output = readOutputSnapshot(session.id, { maxLines: args.maxLines });
1616
+ const completedPanes = completedPanesFromSessions(sessionManager.listByOpenCodeSession(context.sessionID));
1891
1617
  return jsonResponse({
1892
1618
  session: publicSession(session),
1893
1619
  output,
1894
1620
  probe,
1895
- next: nextAdvice(probe.ok, probe.ok ? "Probe completed; continue with this session or read later for long-running output." : probe.message),
1896
- warnings: ["Registry remains in-memory; restarting OpenCode loses plugin session records."]
1621
+ ...completedPanes
1897
1622
  });
1898
1623
  }
1899
1624
  });
@@ -1942,8 +1667,7 @@ const zellijPtyWriteTool = tool({
1942
1667
  const session = sessionManager.get(args.id);
1943
1668
  if (session.humanInputOnly || !session.allowAgentInput) return jsonResponse({
1944
1669
  session: publicSession(session),
1945
- output: subscriberManager.has(session.id) ? readOutputSnapshot(session.id, { maxLines: args.maxLines }) : emptyOutputSnapshot(session.lineCount),
1946
- next: nextAdvice(false, "This session is human-input-only; the user must type directly in the Zellij pane."),
1670
+ output: subscriberManager.has(session.id) ? readOutputSnapshot(session.id, { maxLines: args.maxLines }) : emptyOutputSnapshot(),
1947
1671
  warnings: ["Agent writes to human-input-only sessions are forbidden."]
1948
1672
  });
1949
1673
  if (args.data === "" || args.data === "") await zellijCli.sendCtrlC(session.paneId);
@@ -1969,296 +1693,155 @@ const zellijPtyWriteTool = tool({
1969
1693
  return jsonResponse({
1970
1694
  session: publicSession(session),
1971
1695
  output,
1972
- next: nextAdvice(true, args.interruptAfterSeconds ? "Input was sent; Ctrl-C was sent after the requested interrupt timeout if the session was still running." : "Input was sent and recent output was observed."),
1973
1696
  warnings
1974
1697
  });
1975
1698
  }
1976
1699
  });
1977
1700
  //#endregion
1978
- //#region src/zellij/completion-notifications.ts
1979
- /** Fetches the prompt-mode idle guard status; unrelated to tab title state. */
1980
- async function fetchPromptIdleStatusSnapshot(client, workspaceRoot) {
1701
+ //#region src/utils/runtime.ts
1702
+ /**
1703
+ * Detect whether the plugin process is running inside an OpenCode TUI
1704
+ * session, as opposed to a headless `opencode run` invocation.
1705
+ *
1706
+ * OpenCode spawns the TUI's renderer as a worker child process and
1707
+ * explicitly sets `OPENCODE_PROCESS_ROLE=worker`
1708
+ * (see `packages/opencode/src/cli/cmd/tui/thread.ts` in opencode). The
1709
+ * headless `opencode run` command keeps the default `main` role set by
1710
+ * the CLI entry point, so this is the most reliable signal to tell TUI
1711
+ * from headless.
1712
+ *
1713
+ * Outside the TUI there is no surface for toasts, prompts, or Zellij
1714
+ * panes, and the plugin's lifecycle hooks (watchdogs, tab title actor,
1715
+ * completion notifications) misbehave. The plugin short-circuits to a
1716
+ * no-op in headless mode to avoid leaking side effects.
1717
+ */
1718
+ function isOpencodeTuiMode() {
1719
+ return process.env.OPENCODE_PROCESS_ROLE === "worker";
1720
+ }
1721
+ //#endregion
1722
+ //#region src/utils/logger.ts
1723
+ const defaultDebugLogPath = `${process.env.XDG_CACHE_HOME?.trim() || `${homedir()}/.cache`}/opencode-zellij/debug.log`;
1724
+ function buildRootLogger() {
1725
+ const filename = process.env.OPENCODE_ZELLIJ_DEBUG_LOG?.trim() || defaultDebugLogPath;
1726
+ if (!filename) return null;
1981
1727
  try {
1982
- if (!client.session?.status) {
1983
- debug("fetchPromptIdleStatusSnapshot: client.session.status not available");
1984
- return;
1985
- }
1986
- const result = await client.session.status({ query: { directory: workspaceRoot } });
1987
- const payload = result && typeof result === "object" && "data" in result ? result.data : result;
1988
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
1989
- debug("fetchPromptIdleStatusSnapshot received non-object payload");
1990
- return;
1991
- }
1992
- const entries = Object.entries(payload);
1993
- if (entries.length === 0) return {};
1994
- const snapshot = {};
1995
- for (const [sessionID, status] of entries) {
1996
- const parsed = parseSessionStatus(status);
1997
- if (parsed === void 0) {
1998
- debug("fetchPromptIdleStatusSnapshot received invalid status entry, rejecting entire snapshot");
1999
- return;
2000
- }
2001
- snapshot[sessionID] = parsed;
2002
- }
2003
- return snapshot;
2004
- } catch (err) {
2005
- debug("fetchPromptIdleStatusSnapshot failed", errorMessage(err));
2006
- return;
1728
+ mkdirSync(dirname(filename), { recursive: true });
1729
+ } catch {
1730
+ return null;
1731
+ }
1732
+ try {
1733
+ return new LogLayer({
1734
+ errorSerializer: serializeError,
1735
+ transport: new LogFileRotationTransport({
1736
+ filename: filename.includes("%DATE%") ? filename : `${filename}-%DATE%`,
1737
+ dateFormat: "YMD",
1738
+ size: "1M",
1739
+ maxLogs: "7d",
1740
+ frequency: "daily",
1741
+ compressOnRotate: true,
1742
+ auditFile: join(dirname(filename), "audit.json")
1743
+ })
1744
+ });
1745
+ } catch {
1746
+ return null;
2007
1747
  }
2008
1748
  }
2009
- function parseSessionStatus(value) {
2010
- if (!value || typeof value !== "object" || !("type" in value)) return void 0;
2011
- const status = value;
2012
- if (status.type === "idle" || status.type === "busy") return { type: status.type };
2013
- if (status.type === "retry") return {
2014
- type: "retry",
2015
- attempt: typeof status.attempt === "number" ? status.attempt : 0,
2016
- message: typeof status.message === "string" ? status.message : "",
2017
- next: typeof status.next === "number" ? status.next : 0
2018
- };
1749
+ const rootLogger = buildRootLogger();
1750
+ /**
1751
+ * Returns a child logger that prefixes every message with `[name]`, or
1752
+ * null if the root logger failed to initialize.
1753
+ */
1754
+ function getChildLogger(name) {
1755
+ return rootLogger?.withPrefix(`[${name}]`) ?? null;
2019
1756
  }
2020
- const completionTitle = "Zellij PTY session completed";
2021
- const completionMessage = "A Zellij PTY session completed. Review the finished pane if needed.";
2022
- const queuedNoticeHeader = "[OpenCode] Zellij PTY completion notice";
2023
- function supportsActivePrompt(mode) {
2024
- return mode === "prompt" || mode === "queue+toast";
1757
+ //#endregion
1758
+ //#region src/zellij/completion-notifications.ts
1759
+ const logger = getChildLogger("completion-notifications");
1760
+ async function postPromptAsync(serverUrl, sessionID, body) {
1761
+ const url = new URL(`/session/${encodeURIComponent(sessionID)}/prompt_async`, serverUrl);
1762
+ const headers = { "Content-Type": "application/json" };
1763
+ const directory = process.env.OPENCODE_DIRECTORY?.trim();
1764
+ if (directory) headers["x-opencode-directory"] = encodeURIComponent(directory);
1765
+ return fetch(url, {
1766
+ method: "POST",
1767
+ headers,
1768
+ body: JSON.stringify(body)
1769
+ });
1770
+ }
1771
+ function formatExitCode(exitCode) {
1772
+ return exitCode === null ? "?" : String(exitCode);
2025
1773
  }
2026
- function buildQueuedCompletionNotice(events) {
2027
- return [queuedNoticeHeader, ...events.map((event) => `- ${event.session.id} (${event.session.paneId}) 已完成,請使用 zellij_pty_read 讀取最終輸出並清理 pane。`)].join("\n");
1774
+ function buildCompletionPromptText(event) {
1775
+ const { paneId, exitCode } = event.session;
1776
+ return `[zellij_pty] pane ${paneId} exit=${formatExitCode(exitCode)} — call zellij_pty_read to read, then zellij_pty_kill to close.`;
2028
1777
  }
2029
1778
  function buildCompletionPromptRequest(event) {
2030
1779
  return {
2031
- path: { id: event.session.openCodeSessionId },
2032
- body: { parts: [{
2033
- type: "text",
2034
- text: completionMessage
2035
- }] }
2036
- };
2037
- }
2038
- function injectQueuedCompletionNotice(input, notice) {
2039
- if (typeof input === "string") return `${notice}\n\n${input}`;
2040
- if (!input || typeof input !== "object") return input;
2041
- const record = input;
2042
- if (Array.isArray(record.parts)) return {
2043
- ...record,
1780
+ sessionID: event.session.openCodeSessionId,
2044
1781
  parts: [{
2045
1782
  type: "text",
2046
- text: notice
2047
- }, ...record.parts]
2048
- };
2049
- if (typeof record.message === "string") return {
2050
- ...record,
2051
- message: `${notice}\n\n${record.message}`
2052
- };
2053
- if (typeof record.content === "string") return {
2054
- ...record,
2055
- content: `${notice}\n\n${record.content}`
2056
- };
2057
- if (typeof record.text === "string") return {
2058
- ...record,
2059
- text: `${notice}\n\n${record.text}`
2060
- };
2061
- return {
2062
- ...record,
2063
- message: notice
2064
- };
2065
- }
2066
- function evaluateCompletionPromptDecision(input) {
2067
- if (!supportsActivePrompt(input.config.mode)) return {
2068
- shouldPrompt: false,
2069
- shouldQueue: false,
2070
- reason: "prompt mode disabled"
2071
- };
2072
- if (input.event.session.humanInputOnly || !input.event.session.allowAgentInput) return {
2073
- shouldPrompt: false,
2074
- shouldQueue: true,
2075
- reason: "human-input-only session"
2076
- };
2077
- if (!input.promptClientAvailable) return {
2078
- shouldPrompt: false,
2079
- shouldQueue: true,
2080
- reason: "prompt client unavailable"
2081
- };
2082
- if (!input.event.session.openCodeSessionId) return {
2083
- shouldPrompt: false,
2084
- shouldQueue: true,
2085
- reason: "session id unavailable"
2086
- };
2087
- if (!input.snapshotAvailable) return {
2088
- shouldPrompt: false,
2089
- shouldQueue: true,
2090
- reason: "session status snapshot unavailable"
2091
- };
2092
- if (input.config.prompt.maxAttempts <= 0 || input.promptAttemptCount >= input.config.prompt.maxAttempts) return {
2093
- shouldPrompt: false,
2094
- shouldQueue: true,
2095
- reason: "prompt max attempts reached"
2096
- };
2097
- if (input.config.prompt.cooldownMs > 0 && input.lastPromptAttemptAt !== null && input.now - input.lastPromptAttemptAt < input.config.prompt.cooldownMs) return {
2098
- shouldPrompt: false,
2099
- shouldQueue: true,
2100
- reason: "prompt cooldown active"
2101
- };
2102
- if (input.config.prompt.requireIdle) {
2103
- const sessionId = input.event.session.openCodeSessionId;
2104
- if (!sessionId) return {
2105
- shouldPrompt: false,
2106
- shouldQueue: true,
2107
- reason: "session status unavailable"
2108
- };
2109
- const status = sessionId ? input.snapshot?.[sessionId] : void 0;
2110
- if (!status) return {
2111
- shouldPrompt: false,
2112
- shouldQueue: true,
2113
- reason: "session status unavailable"
2114
- };
2115
- if (status && status.type !== "idle") return {
2116
- shouldPrompt: false,
2117
- shouldQueue: true,
2118
- reason: "session not idle"
2119
- };
2120
- }
2121
- return {
2122
- shouldPrompt: true,
2123
- shouldQueue: false,
2124
- reason: "prompt allowed"
1783
+ text: buildCompletionPromptText(event)
1784
+ }]
2125
1785
  };
2126
1786
  }
2127
- var SessionCompletionNotificationQueue = class {
2128
- states = /* @__PURE__ */ new Map();
2129
- constructor(context, hooks = {}, clock = () => Date.now()) {
1787
+ var SessionCompletionNotificationManager = class {
1788
+ seen = /* @__PURE__ */ new Set();
1789
+ constructor(context) {
2130
1790
  this.context = context;
2131
- this.hooks = hooks;
2132
- this.clock = clock;
2133
- }
2134
- hasPending(sessionId) {
2135
- return this.states.get(sessionId)?.queued ?? false;
2136
- }
2137
- clearSession(sessionId) {
2138
- this.states.delete(sessionId);
2139
- }
2140
- clearAll() {
2141
- this.states.clear();
2142
1791
  }
2143
1792
  dispose() {
2144
- this.clearAll();
1793
+ this.seen.clear();
2145
1794
  }
2146
1795
  async handleSessionTerminal(event) {
2147
- if (this.context.config.mode === "off") return;
2148
- if (this.states.has(event.sessionId) || event.session.tombstone?.notificationSentAt) return;
2149
- const state = {
2150
- event,
2151
- queued: false,
2152
- sent: false,
2153
- toastSent: false,
2154
- promptAttempts: 0,
2155
- promptAttemptedAt: null
2156
- };
2157
- this.states.set(event.sessionId, state);
2158
- switch (this.context.config.mode) {
2159
- case "queue":
2160
- state.queued = true;
2161
- return;
2162
- case "toast":
2163
- await this.sendToast(state);
2164
- this.finalize(state);
2165
- return;
2166
- case "queue+toast":
2167
- await this.tryPromptOrQueue(state);
2168
- await this.sendToast(state);
2169
- return;
2170
- case "prompt":
2171
- await this.tryPromptOrQueue(state);
2172
- break;
2173
- default:
2174
- }
2175
- }
2176
- injectQueuedChatMessage(input) {
2177
- const pending = [...this.states.values()].filter((state) => state.queued);
2178
- if (pending.length === 0) return input;
2179
- const notice = buildQueuedCompletionNotice(pending.map((state) => state.event));
2180
- for (const state of pending) {
2181
- this.markStateSent(state);
2182
- this.finalize(state);
2183
- }
2184
- return injectQueuedCompletionNotice(input, notice);
2185
- }
2186
- async tryPromptOrQueue(state) {
2187
- if (!state.event.session.openCodeSessionId) {
2188
- state.queued = true;
1796
+ logger?.withMetadata({
1797
+ session: event.sessionId,
1798
+ reason: event.reason,
1799
+ paneId: event.session.paneId,
1800
+ openCodeSessionId: event.session.openCodeSessionId ?? "null"
1801
+ }).info("handleSessionTerminal");
1802
+ if (this.seen.has(event.sessionId)) return;
1803
+ this.seen.add(event.sessionId);
1804
+ if (!event.session.openCodeSessionId) {
1805
+ logger?.withMetadata({ session: event.sessionId }).info("skipped: no openCodeSessionId");
2189
1806
  return;
2190
1807
  }
2191
1808
  const session = this.context.client.session;
2192
- const prompt = session?.prompt ?? session?.promptAsync;
2193
- const statusSnapshot = await fetchPromptIdleStatusSnapshot(this.context.client, this.context.workspaceRoot);
2194
- const decision = evaluateCompletionPromptDecision({
2195
- event: state.event,
2196
- config: this.context.config,
2197
- snapshot: statusSnapshot,
2198
- snapshotAvailable: statusSnapshot !== void 0,
2199
- now: this.clock(),
2200
- lastPromptAttemptAt: state.promptAttemptedAt,
2201
- promptAttemptCount: state.promptAttempts,
2202
- promptClientAvailable: Boolean(prompt)
2203
- });
2204
- if (!decision.shouldPrompt) {
2205
- state.queued = decision.shouldQueue;
2206
- return;
2207
- }
2208
- if (this.hooks.prompt) try {
2209
- const maybePromise = this.hooks.prompt(state.event);
2210
- if (maybePromise && typeof maybePromise.then === "function") await maybePromise;
2211
- } catch (error) {
2212
- debug("completion notification prompt hook failed", errorMessage(error));
2213
- }
2214
- if (!session || !prompt) {
2215
- state.queued = true;
1809
+ const sdkPrompt = session?.promptAsync ?? session?.prompt;
1810
+ const request = buildCompletionPromptRequest(event);
1811
+ const sessionID = request.sessionID;
1812
+ if (sdkPrompt) try {
1813
+ logger?.withMetadata({ sessionID }).info("SDK prompt attempt");
1814
+ await sdkPrompt(request);
1815
+ logger?.withMetadata({ sessionID }).info("SDK prompt ok");
2216
1816
  return;
2217
- }
2218
- state.promptAttempts += 1;
2219
- state.promptAttemptedAt = this.clock();
2220
- try {
2221
- if (session.prompt) await session.prompt(buildCompletionPromptRequest(state.event));
2222
- else if (session.promptAsync) await session.promptAsync(buildCompletionPromptRequest(state.event));
2223
- else {
2224
- state.queued = true;
2225
- return;
2226
- }
2227
- this.markStateSent(state);
2228
- this.finalize(state);
2229
1817
  } catch (error) {
2230
- debug("completion notification prompt failed", errorMessage(error));
2231
- state.queued = true;
1818
+ logger?.withMetadata({
1819
+ sessionID,
1820
+ error: errorMessage(error)
1821
+ }).warn("SDK prompt failed, falling back to HTTP");
2232
1822
  }
2233
- }
2234
- async sendToast(state) {
2235
- const toast = this.context.client.tui?.showToast;
2236
- if (!toast) {
2237
- debug("completion notification toast skipped: client.tui.showToast unavailable");
1823
+ else logger?.withMetadata({
1824
+ sessionID,
1825
+ sessionKeys: session ? Object.keys(session).join(",") : "null"
1826
+ }).info("no SDK prompt on client, using HTTP fallback");
1827
+ if (!this.context.serverUrl) {
1828
+ logger?.withMetadata({ sessionID }).error("no serverUrl for HTTP fallback");
2238
1829
  return;
2239
1830
  }
2240
1831
  try {
2241
- await toast({ body: {
2242
- title: completionTitle,
2243
- message: completionMessage,
2244
- variant: "success",
2245
- duration: 1e4
2246
- } });
2247
- state.toastSent = true;
2248
- this.markStateSent(state);
2249
- if (!state.queued) this.finalize(state);
1832
+ const response = await postPromptAsync(this.context.serverUrl, sessionID, { parts: request.parts });
1833
+ logger?.withMetadata({
1834
+ sessionID,
1835
+ status: response.status,
1836
+ ok: response.ok
1837
+ }).info("HTTP fallback response");
2250
1838
  } catch (error) {
2251
- debug("completion notification toast failed", errorMessage(error));
1839
+ logger?.withMetadata({
1840
+ sessionID,
1841
+ error: errorMessage(error)
1842
+ }).error("HTTP fallback threw");
2252
1843
  }
2253
1844
  }
2254
- markStateSent(state) {
2255
- if (state.sent) return;
2256
- this.context.markSent(state.event.sessionId);
2257
- state.sent = true;
2258
- }
2259
- finalize(state) {
2260
- this.states.delete(state.event.sessionId);
2261
- }
2262
1845
  };
2263
1846
  //#endregion
2264
1847
  //#region src/zellij/shutdown-cleanup.ts
@@ -2600,9 +2183,6 @@ var TabTitleManager = class {
2600
2183
  emojis;
2601
2184
  enabled;
2602
2185
  destroyed = false;
2603
- originalTabTitle;
2604
- originalTabTitleLoaded = false;
2605
- originalTabTitlePromise;
2606
2186
  destroyPromise;
2607
2187
  actor;
2608
2188
  constructor(options) {
@@ -2628,8 +2208,6 @@ var TabTitleManager = class {
2628
2208
  }
2629
2209
  async renderImmediate() {
2630
2210
  if (!this.enabled || this.destroyed) return;
2631
- await this.ensureOriginalTabTitle();
2632
- if (this.destroyed) return;
2633
2211
  this.desiredTitle = this.buildTitle();
2634
2212
  this.clearDebounceTimer();
2635
2213
  await this.syncDesiredTitle();
@@ -2651,7 +2229,6 @@ var TabTitleManager = class {
2651
2229
  async syncDesiredTitle() {
2652
2230
  if (!this.enabled || this.destroyed) return;
2653
2231
  const generation = this.syncGeneration;
2654
- await this.ensureOriginalTabTitle();
2655
2232
  if (this.destroyed || generation !== this.syncGeneration) return;
2656
2233
  if (this.syncInFlight) return this.syncPromise;
2657
2234
  this.syncInFlight = true;
@@ -2701,23 +2278,6 @@ var TabTitleManager = class {
2701
2278
  if (this.debounceTimer) clearTimeout(this.debounceTimer);
2702
2279
  this.debounceTimer = void 0;
2703
2280
  }
2704
- async ensureOriginalTabTitle() {
2705
- if (!this.enabled || this.originalTabTitleLoaded) return;
2706
- if (this.originalTabTitlePromise) return this.originalTabTitlePromise;
2707
- this.originalTabTitlePromise = this.saveOriginalTabTitle();
2708
- return this.originalTabTitlePromise;
2709
- }
2710
- async saveOriginalTabTitle() {
2711
- try {
2712
- const title = await this.cli.currentTabTitle();
2713
- if (title !== void 0) this.originalTabTitle = title;
2714
- } catch (error) {
2715
- debug("TabTitleManager failed to save original tab title", errorMessage(error));
2716
- } finally {
2717
- this.originalTabTitleLoaded = true;
2718
- this.originalTabTitlePromise = void 0;
2719
- }
2720
- }
2721
2281
  destroy() {
2722
2282
  if (this.destroyed) return this.destroyPromise ?? Promise.resolve();
2723
2283
  this.destroyed = true;
@@ -2725,21 +2285,12 @@ var TabTitleManager = class {
2725
2285
  this.desiredTitle = void 0;
2726
2286
  this.clearDebounceTimer();
2727
2287
  this.clearRetryTimer();
2728
- if (!this.enabled) return Promise.resolve();
2729
- this.destroyPromise = this.restoreOriginalTabTitle().catch((error) => debug("TabTitleManager failed to restore original tab title", errorMessage(error)));
2730
- return this.destroyPromise;
2731
- }
2732
- async restoreOriginalTabTitle() {
2733
- await this.originalTabTitlePromise;
2734
- await this.syncPromise;
2735
- const originalTitle = this.originalTabTitle;
2736
- this.originalTabTitle = void 0;
2737
- if (originalTitle === void 0) return;
2738
- await this.cli.renameTab(originalTitle);
2288
+ return Promise.resolve();
2739
2289
  }
2740
2290
  };
2741
2291
  //#endregion
2742
2292
  //#region src/plugin.ts
2293
+ const PLUGIN_ID = "opencode-zellij";
2743
2294
  function createPtyTools(defaultCleanupExitedPaneOnRead) {
2744
2295
  return {
2745
2296
  zellij_pty_spawn: zellijPtySpawnTool,
@@ -2755,29 +2306,6 @@ function getProjectName(path) {
2755
2306
  function getWorkspaceRoot(input) {
2756
2307
  return input.worktree || input.directory || process.cwd();
2757
2308
  }
2758
- function showUpdateToast(client, result) {
2759
- if (result.type === "updated") client.tui.showToast({ body: {
2760
- title: "opencode-zellij updated",
2761
- message: `Updated to ${result.toVersion}. Restart OpenCode to apply the changes.`,
2762
- variant: "success",
2763
- duration: 1e4
2764
- } }).catch((error) => debug("show update toast for successful update failed", errorMessage(error)));
2765
- else if (result.type === "failed") client.tui.showToast({ body: {
2766
- title: "opencode-zellij update failed",
2767
- message: `Failed to update to ${result.latestVersion}.`,
2768
- variant: "error",
2769
- duration: 8e3
2770
- } }).catch((error) => debug("show update toast for failed update failed", errorMessage(error)));
2771
- }
2772
- function startAutoUpdateCheck(client, importMetaUrl, check = checkAndUpdate) {
2773
- (async () => {
2774
- try {
2775
- showUpdateToast(client, await check({ importMetaUrl }));
2776
- } catch (cause) {
2777
- debug("auto-update check failed", errorMessage(cause));
2778
- }
2779
- })();
2780
- }
2781
2309
  async function cleanupStep(stepName, sessionId, step) {
2782
2310
  try {
2783
2311
  await step();
@@ -2793,6 +2321,10 @@ async function cleanupDeletedSession(sessionId) {
2793
2321
  }
2794
2322
  function createZellijPtyPlugin(dependencies = {}) {
2795
2323
  return async (input) => {
2324
+ if (!isOpencodeTuiMode()) {
2325
+ debug("opencode-zellij disabled: not running inside an OpenCode TUI session");
2326
+ return {};
2327
+ }
2796
2328
  const { config, warnings } = await loadConfig(input);
2797
2329
  for (const warning of warnings) debug(warning);
2798
2330
  configureSudoPane(config.pty.sudoPane === "allow");
@@ -2820,60 +2352,35 @@ function createZellijPtyPlugin(dependencies = {}) {
2820
2352
  branch: config.tabTitle.emojiBranch
2821
2353
  }
2822
2354
  }) : void 0;
2823
- const client = input.client;
2824
- const completionNotifications = config.pty.completionNotification.mode === "off" ? void 0 : dependencies.createCompletionNotifications?.({
2825
- client,
2826
- workspaceRoot,
2827
- config: config.pty.completionNotification,
2828
- markSent(sessionId) {
2829
- try {
2830
- sessionManager.markTerminalNotificationSent(sessionId);
2831
- } catch (error) {
2832
- debug("mark terminal notification sent failed", errorMessage(error));
2833
- }
2834
- }
2835
- }) ?? new SessionCompletionNotificationQueue({
2836
- client,
2837
- workspaceRoot,
2838
- config: config.pty.completionNotification,
2839
- markSent(sessionId) {
2840
- try {
2841
- sessionManager.markTerminalNotificationSent(sessionId);
2842
- } catch (error) {
2843
- debug("mark terminal notification sent failed", errorMessage(error));
2844
- }
2845
- }
2355
+ const completionNotifications = dependencies.createCompletionNotifications?.({
2356
+ client: { session: input.client?.session },
2357
+ serverUrl: input.serverUrl
2358
+ }) ?? new SessionCompletionNotificationManager({
2359
+ client: { session: input.client?.session },
2360
+ serverUrl: input.serverUrl
2846
2361
  });
2847
- subscriberManager.setLifecycleHooks(completionNotifications ? { onSessionTerminal: (event) => void completionNotifications.handleSessionTerminal(event).catch((error) => debug("completion notification lifecycle hook failed", errorMessage(error))) } : void 0);
2362
+ subscriberManager.setLifecycleHooks({ onSessionTerminal: (event) => void completionNotifications.handleSessionTerminal(event).catch((error) => debug("completion notification lifecycle hook failed", errorMessage(error))) });
2848
2363
  if (actor) await actor.ready;
2849
2364
  tabTitleManager?.renderImmediate().catch((error) => debug("initial tab title render failed", errorMessage(error)));
2850
- if (config.autoUpdate) (dependencies.startAutoUpdateCheck ?? startAutoUpdateCheck)(client, dependencies.importMetaUrl ?? import.meta.url);
2851
2365
  return {
2852
2366
  async event(input) {
2853
2367
  const event = input.event;
2854
2368
  if (actor && tabTitleManager) {
2855
2369
  await actor.handleEvent(event);
2856
- if (event.type === "server.instance.disposed" || event.type === "global.disposed") await tabTitleManager.destroy();
2857
- else tabTitleManager.scheduleUpdate();
2370
+ tabTitleManager.scheduleUpdate();
2858
2371
  }
2859
2372
  if (event.type === "server.instance.disposed" || event.type === "global.disposed") {
2860
- completionNotifications?.clearAll();
2861
- completionNotifications?.dispose();
2373
+ completionNotifications.dispose();
2862
2374
  subscriberManager.setLifecycleHooks(void 0);
2863
2375
  }
2864
2376
  if (event.type === "session.deleted") {
2865
2377
  const sessionID = deletedSessionID$1(event);
2866
2378
  if (!sessionID) return;
2867
2379
  const sessions = sessionManager.listByOpenCodeSession(sessionID);
2868
- for (const session of sessions) completionNotifications?.clearSession(session.id);
2869
2380
  await Promise.all(sessions.map((session) => cleanupDeletedSession(session.id)));
2870
2381
  }
2871
2382
  },
2872
- "chat.message": async (_input, output) => {
2873
- const injected = completionNotifications?.injectQueuedChatMessage(output) ?? output;
2874
- if (injected !== output && injected && typeof injected === "object" && Array.isArray(injected.parts)) output.parts = injected.parts;
2875
- },
2876
- "tool": config.pty.enabled ? {
2383
+ tool: config.pty.enabled ? {
2877
2384
  ...createPtyTools(config.pty.cleanupExitedPaneOnRead),
2878
2385
  ...config.pty.sudoPane === "hide" ? {} : { zellij_pty_request_sudo: requestSudoTool }
2879
2386
  } : {}
@@ -2881,7 +2388,11 @@ function createZellijPtyPlugin(dependencies = {}) {
2881
2388
  };
2882
2389
  }
2883
2390
  const ZellijPtyPlugin = createZellijPtyPlugin();
2391
+ var plugin_default = {
2392
+ id: PLUGIN_ID,
2393
+ server: ZellijPtyPlugin
2394
+ };
2884
2395
  //#endregion
2885
- export { ZellijPtyPlugin, ZellijPtyPlugin as default, createZellijPtyPlugin, showUpdateToast, startAutoUpdateCheck };
2396
+ export { ZellijPtyPlugin, createZellijPtyPlugin, plugin_default as default };
2886
2397
 
2887
2398
  //# sourceMappingURL=index.mjs.map