opencode-zellij 0.0.16 → 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) {
@@ -1413,8 +1148,7 @@ function publicSession(session) {
1413
1148
  reason: session.tombstone.reason,
1414
1149
  terminalAt: session.tombstone.terminalAt,
1415
1150
  tailLines: session.tombstone.tail.length,
1416
- paneClosedAt: session.tombstone.paneClosedAt,
1417
- notificationSentAt: session.tombstone.notificationSentAt
1151
+ paneClosedAt: session.tombstone.paneClosedAt
1418
1152
  } : null
1419
1153
  };
1420
1154
  }
@@ -1975,290 +1709,149 @@ const zellijPtyWriteTool = tool({
1975
1709
  }
1976
1710
  });
1977
1711
  //#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) {
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;
1981
1738
  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;
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;
2007
1757
  }
2008
1758
  }
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
- };
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;
2019
1766
  }
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";
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
+ });
2025
1780
  }
2026
- function buildQueuedCompletionNotice(events) {
2027
- return [queuedNoticeHeader, ...events.map((event) => `- ${event.session.id} (${event.session.paneId}) 已完成,請使用 zellij_pty_read 讀取最終輸出並清理 pane。`)].join("\n");
1781
+ function formatExitCode(exitCode) {
1782
+ return exitCode === null ? "?" : String(exitCode);
1783
+ }
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.`;
2028
1787
  }
2029
1788
  function buildCompletionPromptRequest(event) {
2030
1789
  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,
1790
+ sessionID: event.session.openCodeSessionId,
2044
1791
  parts: [{
2045
1792
  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"
1793
+ text: buildCompletionPromptText(event)
1794
+ }]
2125
1795
  };
2126
1796
  }
2127
- var SessionCompletionNotificationQueue = class {
2128
- states = /* @__PURE__ */ new Map();
2129
- constructor(context, hooks = {}, clock = () => Date.now()) {
1797
+ var SessionCompletionNotificationManager = class {
1798
+ seen = /* @__PURE__ */ new Set();
1799
+ constructor(context) {
2130
1800
  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
1801
  }
2143
1802
  dispose() {
2144
- this.clearAll();
1803
+ this.seen.clear();
2145
1804
  }
2146
1805
  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;
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");
2189
1816
  return;
2190
1817
  }
2191
1818
  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;
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");
2206
1826
  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
1827
  } catch (error) {
2212
- debug("completion notification prompt hook failed", errorMessage(error));
1828
+ logger?.withMetadata({
1829
+ sessionID,
1830
+ error: errorMessage(error)
1831
+ }).warn("SDK prompt failed, falling back to HTTP");
2213
1832
  }
2214
- if (!session || !prompt) {
2215
- state.queued = true;
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");
2216
1839
  return;
2217
1840
  }
2218
- state.promptAttempts += 1;
2219
- state.promptAttemptedAt = this.clock();
2220
1841
  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);
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");
2229
1848
  } catch (error) {
2230
- debug("completion notification prompt failed", errorMessage(error));
2231
- state.queued = true;
1849
+ logger?.withMetadata({
1850
+ sessionID,
1851
+ error: errorMessage(error)
1852
+ }).error("HTTP fallback threw");
2232
1853
  }
2233
1854
  }
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");
2238
- return;
2239
- }
2240
- 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);
2250
- } catch (error) {
2251
- debug("completion notification toast failed", errorMessage(error));
2252
- }
2253
- }
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
1855
  };
2263
1856
  //#endregion
2264
1857
  //#region src/zellij/shutdown-cleanup.ts
@@ -2600,9 +2193,6 @@ var TabTitleManager = class {
2600
2193
  emojis;
2601
2194
  enabled;
2602
2195
  destroyed = false;
2603
- originalTabTitle;
2604
- originalTabTitleLoaded = false;
2605
- originalTabTitlePromise;
2606
2196
  destroyPromise;
2607
2197
  actor;
2608
2198
  constructor(options) {
@@ -2628,8 +2218,6 @@ var TabTitleManager = class {
2628
2218
  }
2629
2219
  async renderImmediate() {
2630
2220
  if (!this.enabled || this.destroyed) return;
2631
- await this.ensureOriginalTabTitle();
2632
- if (this.destroyed) return;
2633
2221
  this.desiredTitle = this.buildTitle();
2634
2222
  this.clearDebounceTimer();
2635
2223
  await this.syncDesiredTitle();
@@ -2651,7 +2239,6 @@ var TabTitleManager = class {
2651
2239
  async syncDesiredTitle() {
2652
2240
  if (!this.enabled || this.destroyed) return;
2653
2241
  const generation = this.syncGeneration;
2654
- await this.ensureOriginalTabTitle();
2655
2242
  if (this.destroyed || generation !== this.syncGeneration) return;
2656
2243
  if (this.syncInFlight) return this.syncPromise;
2657
2244
  this.syncInFlight = true;
@@ -2701,23 +2288,6 @@ var TabTitleManager = class {
2701
2288
  if (this.debounceTimer) clearTimeout(this.debounceTimer);
2702
2289
  this.debounceTimer = void 0;
2703
2290
  }
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
2291
  destroy() {
2722
2292
  if (this.destroyed) return this.destroyPromise ?? Promise.resolve();
2723
2293
  this.destroyed = true;
@@ -2725,21 +2295,12 @@ var TabTitleManager = class {
2725
2295
  this.desiredTitle = void 0;
2726
2296
  this.clearDebounceTimer();
2727
2297
  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);
2298
+ return Promise.resolve();
2739
2299
  }
2740
2300
  };
2741
2301
  //#endregion
2742
2302
  //#region src/plugin.ts
2303
+ const PLUGIN_ID = "opencode-zellij";
2743
2304
  function createPtyTools(defaultCleanupExitedPaneOnRead) {
2744
2305
  return {
2745
2306
  zellij_pty_spawn: zellijPtySpawnTool,
@@ -2755,29 +2316,6 @@ function getProjectName(path) {
2755
2316
  function getWorkspaceRoot(input) {
2756
2317
  return input.worktree || input.directory || process.cwd();
2757
2318
  }
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
2319
  async function cleanupStep(stepName, sessionId, step) {
2782
2320
  try {
2783
2321
  await step();
@@ -2793,6 +2331,10 @@ async function cleanupDeletedSession(sessionId) {
2793
2331
  }
2794
2332
  function createZellijPtyPlugin(dependencies = {}) {
2795
2333
  return async (input) => {
2334
+ if (!isOpencodeTuiMode()) {
2335
+ debug("opencode-zellij disabled: not running inside an OpenCode TUI session");
2336
+ return {};
2337
+ }
2796
2338
  const { config, warnings } = await loadConfig(input);
2797
2339
  for (const warning of warnings) debug(warning);
2798
2340
  configureSudoPane(config.pty.sudoPane === "allow");
@@ -2820,60 +2362,35 @@ function createZellijPtyPlugin(dependencies = {}) {
2820
2362
  branch: config.tabTitle.emojiBranch
2821
2363
  }
2822
2364
  }) : 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
- }
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
2846
2371
  });
2847
- 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))) });
2848
2373
  if (actor) await actor.ready;
2849
2374
  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
2375
  return {
2852
2376
  async event(input) {
2853
2377
  const event = input.event;
2854
2378
  if (actor && tabTitleManager) {
2855
2379
  await actor.handleEvent(event);
2856
- if (event.type === "server.instance.disposed" || event.type === "global.disposed") await tabTitleManager.destroy();
2857
- else tabTitleManager.scheduleUpdate();
2380
+ tabTitleManager.scheduleUpdate();
2858
2381
  }
2859
2382
  if (event.type === "server.instance.disposed" || event.type === "global.disposed") {
2860
- completionNotifications?.clearAll();
2861
- completionNotifications?.dispose();
2383
+ completionNotifications.dispose();
2862
2384
  subscriberManager.setLifecycleHooks(void 0);
2863
2385
  }
2864
2386
  if (event.type === "session.deleted") {
2865
2387
  const sessionID = deletedSessionID$1(event);
2866
2388
  if (!sessionID) return;
2867
2389
  const sessions = sessionManager.listByOpenCodeSession(sessionID);
2868
- for (const session of sessions) completionNotifications?.clearSession(session.id);
2869
2390
  await Promise.all(sessions.map((session) => cleanupDeletedSession(session.id)));
2870
2391
  }
2871
2392
  },
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 ? {
2393
+ tool: config.pty.enabled ? {
2877
2394
  ...createPtyTools(config.pty.cleanupExitedPaneOnRead),
2878
2395
  ...config.pty.sudoPane === "hide" ? {} : { zellij_pty_request_sudo: requestSudoTool }
2879
2396
  } : {}
@@ -2881,7 +2398,11 @@ function createZellijPtyPlugin(dependencies = {}) {
2881
2398
  };
2882
2399
  }
2883
2400
  const ZellijPtyPlugin = createZellijPtyPlugin();
2401
+ var plugin_default = {
2402
+ id: PLUGIN_ID,
2403
+ server: ZellijPtyPlugin
2404
+ };
2884
2405
  //#endregion
2885
- export { ZellijPtyPlugin, ZellijPtyPlugin as default, createZellijPtyPlugin, showUpdateToast, startAutoUpdateCheck };
2406
+ export { ZellijPtyPlugin, createZellijPtyPlugin, plugin_default as default };
2886
2407
 
2887
2408
  //# sourceMappingURL=index.mjs.map