opencode-zellij 0.0.15 → 0.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -2
- package/README.zh.md +13 -2
- package/dist/index.d.mts +9 -107
- package/dist/index.mjs +227 -664
- package/dist/index.mjs.map +1 -1
- package/dist/pane-watchdog-runner.mjs +1 -1
- package/package.json +4 -1
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
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
337
|
-
mode: project?.pty?.completionNotification?.mode ?? user?.pty?.completionNotification?.mode ?? defaultConfig.pty.completionNotification.mode,
|
|
338
|
-
prompt: {
|
|
339
|
-
requireIdle: project?.pty?.completionNotification?.prompt?.requireIdle ?? user?.pty?.completionNotification?.prompt?.requireIdle ?? defaultConfig.pty.completionNotification.prompt.requireIdle,
|
|
340
|
-
cooldownMs: project?.pty?.completionNotification?.prompt?.cooldownMs ?? user?.pty?.completionNotification?.prompt?.cooldownMs ?? defaultConfig.pty.completionNotification.prompt.cooldownMs,
|
|
341
|
-
maxAttempts: project?.pty?.completionNotification?.prompt?.maxAttempts ?? user?.pty?.completionNotification?.prompt?.maxAttempts ?? defaultConfig.pty.completionNotification.prompt.maxAttempts
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
},
|
|
345
|
-
autoUpdate: project?.autoUpdate ?? user?.autoUpdate ?? defaultConfig.autoUpdate
|
|
79
|
+
sudoPane: project?.pty?.sudoPane ?? user?.pty?.sudoPane ?? defaultConfig.pty.sudoPane
|
|
80
|
+
}
|
|
346
81
|
};
|
|
347
82
|
}
|
|
348
83
|
async function loadConfigLayer(directory, warnings) {
|
|
@@ -490,8 +225,7 @@ var SessionManager = class {
|
|
|
490
225
|
reason: input.reason,
|
|
491
226
|
terminalAt: now,
|
|
492
227
|
tail: (input.tail ?? []).slice(-tombstoneTailLimit),
|
|
493
|
-
paneClosedAt: null
|
|
494
|
-
notificationSentAt: null
|
|
228
|
+
paneClosedAt: null
|
|
495
229
|
};
|
|
496
230
|
session.status = "terminal";
|
|
497
231
|
session.tombstone = tombstone;
|
|
@@ -521,16 +255,6 @@ var SessionManager = class {
|
|
|
521
255
|
}
|
|
522
256
|
return session;
|
|
523
257
|
}
|
|
524
|
-
markTerminalNotificationSent(id) {
|
|
525
|
-
const session = this.get(id);
|
|
526
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
527
|
-
if (!session.tombstone) return session;
|
|
528
|
-
if (!session.tombstone.notificationSentAt) {
|
|
529
|
-
session.tombstone.notificationSentAt = now;
|
|
530
|
-
session.updatedAt = now;
|
|
531
|
-
}
|
|
532
|
-
return session;
|
|
533
|
-
}
|
|
534
258
|
listByOpenCodeSession(openCodeSessionId) {
|
|
535
259
|
return this.list().filter((session) => session.openCodeSessionId === openCodeSessionId);
|
|
536
260
|
}
|
|
@@ -573,6 +297,17 @@ function buildCommandArgv(input, options = {}) {
|
|
|
573
297
|
];
|
|
574
298
|
}
|
|
575
299
|
//#endregion
|
|
300
|
+
//#region src/utils/debug.ts
|
|
301
|
+
function debug(message, ...details) {
|
|
302
|
+
if (!process.env.ZELLIJ_PTY_DEBUG) return;
|
|
303
|
+
console.warn(`[opencode-zellij] ${message}`, ...details);
|
|
304
|
+
}
|
|
305
|
+
//#endregion
|
|
306
|
+
//#region src/utils/errors.ts
|
|
307
|
+
function errorMessage(error) {
|
|
308
|
+
return error instanceof Error ? error.message : String(error);
|
|
309
|
+
}
|
|
310
|
+
//#endregion
|
|
576
311
|
//#region src/zellij/parse.ts
|
|
577
312
|
function numericProperty(object, keys) {
|
|
578
313
|
for (const key of keys) {
|
|
@@ -663,6 +398,28 @@ function findTabName(value, tabId) {
|
|
|
663
398
|
if (found !== void 0) return found;
|
|
664
399
|
}
|
|
665
400
|
}
|
|
401
|
+
function activeTabNameProperty(object) {
|
|
402
|
+
if (object.active !== true || object.is_plugin === true) return void 0;
|
|
403
|
+
const name = stringProperty$2(object, ["name", "title"]);
|
|
404
|
+
return typeof name === "string" ? name : void 0;
|
|
405
|
+
}
|
|
406
|
+
function findActiveTabName(value) {
|
|
407
|
+
if (Array.isArray(value)) {
|
|
408
|
+
for (const item of value) {
|
|
409
|
+
const found = findActiveTabName(item);
|
|
410
|
+
if (found !== void 0) return found;
|
|
411
|
+
}
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
415
|
+
const object = value;
|
|
416
|
+
const name = activeTabNameProperty(object);
|
|
417
|
+
if (name !== void 0) return name;
|
|
418
|
+
for (const nested of Object.values(object)) {
|
|
419
|
+
const found = findActiveTabName(nested);
|
|
420
|
+
if (found !== void 0) return found;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
666
423
|
function parseTabName(listTabsJson, tabId) {
|
|
667
424
|
try {
|
|
668
425
|
return findTabName(JSON.parse(listTabsJson), tabId);
|
|
@@ -671,6 +428,14 @@ function parseTabName(listTabsJson, tabId) {
|
|
|
671
428
|
return;
|
|
672
429
|
}
|
|
673
430
|
}
|
|
431
|
+
function parseActiveTabName(listTabsJson) {
|
|
432
|
+
try {
|
|
433
|
+
return findActiveTabName(JSON.parse(listTabsJson));
|
|
434
|
+
} catch (error) {
|
|
435
|
+
debug("parseActiveTabName failed", errorMessage(error));
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
674
439
|
//#endregion
|
|
675
440
|
//#region src/zellij/cli.ts
|
|
676
441
|
const execFileAsync$1 = promisify(execFile);
|
|
@@ -738,25 +503,28 @@ async function runZellij(actionArgs, options = {}) {
|
|
|
738
503
|
}
|
|
739
504
|
}
|
|
740
505
|
var ZellijCli = class {
|
|
506
|
+
constructor(run = runZellij) {
|
|
507
|
+
this.run = run;
|
|
508
|
+
}
|
|
741
509
|
async newPane(options) {
|
|
742
|
-
return parsePaneId((await
|
|
510
|
+
return parsePaneId((await this.run(buildNewPaneActionArgs(options))).stdout);
|
|
743
511
|
}
|
|
744
512
|
async writeChars(paneId, data) {
|
|
745
|
-
await
|
|
513
|
+
await this.run(zellijActionArgs("write-chars", [
|
|
746
514
|
"--pane-id",
|
|
747
515
|
paneId,
|
|
748
516
|
data
|
|
749
517
|
]));
|
|
750
518
|
}
|
|
751
519
|
async sendCtrlC(paneId) {
|
|
752
|
-
await
|
|
520
|
+
await this.run(zellijActionArgs("send-keys", [
|
|
753
521
|
"--pane-id",
|
|
754
522
|
paneId,
|
|
755
523
|
"Ctrl c"
|
|
756
524
|
]));
|
|
757
525
|
}
|
|
758
526
|
async closePane(paneId) {
|
|
759
|
-
await
|
|
527
|
+
await this.run(zellijActionArgs("close-pane", ["--pane-id", paneId]));
|
|
760
528
|
}
|
|
761
529
|
closePaneSync(paneId) {
|
|
762
530
|
ensureZellijTarget();
|
|
@@ -767,10 +535,10 @@ var ZellijCli = class {
|
|
|
767
535
|
});
|
|
768
536
|
}
|
|
769
537
|
async focusPane(paneId) {
|
|
770
|
-
await
|
|
538
|
+
await this.run(zellijActionArgs("focus-pane-id", [paneId]));
|
|
771
539
|
}
|
|
772
540
|
async dumpScreen(paneId) {
|
|
773
|
-
return (await
|
|
541
|
+
return (await this.run(zellijActionArgs("dump-screen", [
|
|
774
542
|
"--pane-id",
|
|
775
543
|
paneId,
|
|
776
544
|
"--full"
|
|
@@ -779,21 +547,24 @@ var ZellijCli = class {
|
|
|
779
547
|
async currentPaneTabId() {
|
|
780
548
|
const paneId = process.env.ZELLIJ_PANE_ID;
|
|
781
549
|
if (!paneId) return void 0;
|
|
782
|
-
return parseCurrentPaneTabId((await
|
|
550
|
+
return parseCurrentPaneTabId((await this.run(zellijActionArgs("list-panes", ["--json"]), { timeoutMs: 5e3 })).stdout, paneId);
|
|
783
551
|
}
|
|
784
552
|
async paneExists(paneId) {
|
|
785
|
-
return parsePaneExists((await
|
|
553
|
+
return parsePaneExists((await this.run(zellijActionArgs("list-panes", ["--json"]), { timeoutMs: 5e3 })).stdout, paneId);
|
|
786
554
|
}
|
|
787
555
|
async renameTab(title) {
|
|
788
556
|
const tabId = await this.currentPaneTabId();
|
|
789
557
|
if (tabId === void 0 && process.env.ZELLIJ) throw new Error(`Could not resolve Zellij tab id for pane ${process.env.ZELLIJ_PANE_ID ?? "<missing>"}`);
|
|
790
|
-
await
|
|
558
|
+
await this.run(tabId === void 0 ? buildRenameTabActionArgs(title) : buildRenameTabActionArgs(title, { tabId }));
|
|
791
559
|
}
|
|
792
560
|
async currentTabTitle() {
|
|
793
|
-
if (!process.env.ZELLIJ_PANE_ID)
|
|
561
|
+
if (!process.env.ZELLIJ_PANE_ID) {
|
|
562
|
+
if (!process.env.ZELLIJ_SESSION_NAME?.trim()) return void 0;
|
|
563
|
+
return parseActiveTabName((await this.run(zellijActionArgs("list-tabs", ["--json"]), { timeoutMs: 5e3 })).stdout);
|
|
564
|
+
}
|
|
794
565
|
const tabId = await this.currentPaneTabId();
|
|
795
566
|
if (tabId === void 0) return void 0;
|
|
796
|
-
return parseTabName((await
|
|
567
|
+
return parseTabName((await this.run(zellijActionArgs("list-tabs", ["--json"]), { timeoutMs: 5e3 })).stdout, tabId);
|
|
797
568
|
}
|
|
798
569
|
};
|
|
799
570
|
const zellijCli = new ZellijCli();
|
|
@@ -1038,13 +809,20 @@ function createExitCodeToken() {
|
|
|
1038
809
|
return randomUUID().replaceAll("-", "");
|
|
1039
810
|
}
|
|
1040
811
|
function parseExitCodeMarker(line) {
|
|
1041
|
-
const match = line.replace(ansiPattern, "").trim().match(markerPattern);
|
|
812
|
+
const match = line.replace(ansiPattern, "").replace(/\r?\n/g, "").trim().match(markerPattern);
|
|
1042
813
|
if (!match?.[1] || !match[2]) return null;
|
|
1043
814
|
return {
|
|
1044
815
|
token: match[1],
|
|
1045
816
|
exitCode: Number(match[2])
|
|
1046
817
|
};
|
|
1047
818
|
}
|
|
819
|
+
function parseExitCodeMarkerLines(lines, maxWindowLines = 8) {
|
|
820
|
+
for (let start = 0; start < lines.length; start += 1) for (let size = 1; size <= maxWindowLines && start + size <= lines.length; size += 1) {
|
|
821
|
+
const marker = parseExitCodeMarker(lines.slice(start, start + size).join("\n"));
|
|
822
|
+
if (marker) return marker;
|
|
823
|
+
}
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
1048
826
|
//#endregion
|
|
1049
827
|
//#region src/zellij/subscribe.ts
|
|
1050
828
|
const maxStderrLines = 200;
|
|
@@ -1107,9 +885,9 @@ var SubscriberManager = class {
|
|
|
1107
885
|
this.sessions = sessions;
|
|
1108
886
|
this.maxBufferLines = maxBufferLines;
|
|
1109
887
|
this.spawnProcess = dependencies.spawn ?? spawn;
|
|
1110
|
-
this.dumpScreen = dependencies.dumpScreen ?? zellijCli.dumpScreen;
|
|
1111
|
-
this.paneExists = dependencies.paneExists ?? zellijCli.paneExists;
|
|
1112
|
-
this.closePane = dependencies.closePane ?? zellijCli.closePane;
|
|
888
|
+
this.dumpScreen = dependencies.dumpScreen ?? ((paneId) => zellijCli.dumpScreen(paneId));
|
|
889
|
+
this.paneExists = dependencies.paneExists ?? ((paneId) => zellijCli.paneExists(paneId));
|
|
890
|
+
this.closePane = dependencies.closePane ?? ((paneId) => zellijCli.closePane(paneId));
|
|
1113
891
|
this.lifecycleHooks = dependencies.lifecycleHooks;
|
|
1114
892
|
this.terminalTailLines = dependencies.terminalTailLines ?? 200;
|
|
1115
893
|
}
|
|
@@ -1268,12 +1046,9 @@ var SubscriberManager = class {
|
|
|
1268
1046
|
captureExitCode(sessionId, lines) {
|
|
1269
1047
|
const session = this.sessions.get(sessionId);
|
|
1270
1048
|
if (!session.exitCodeToken) return;
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
this.markSessionTerminal(sessionId, "exit_marker", { exitCode: marker.exitCode });
|
|
1275
|
-
return;
|
|
1276
|
-
}
|
|
1049
|
+
const marker = parseExitCodeMarkerLines(lines);
|
|
1050
|
+
if (!marker || marker.token !== session.exitCodeToken) return;
|
|
1051
|
+
this.markSessionTerminal(sessionId, "exit_marker", { exitCode: marker.exitCode });
|
|
1277
1052
|
}
|
|
1278
1053
|
handleStderr(sessionId, child, chunk) {
|
|
1279
1054
|
const state = this.subscribers.get(sessionId);
|
|
@@ -1373,8 +1148,7 @@ function publicSession(session) {
|
|
|
1373
1148
|
reason: session.tombstone.reason,
|
|
1374
1149
|
terminalAt: session.tombstone.terminalAt,
|
|
1375
1150
|
tailLines: session.tombstone.tail.length,
|
|
1376
|
-
paneClosedAt: session.tombstone.paneClosedAt
|
|
1377
|
-
notificationSentAt: session.tombstone.notificationSentAt
|
|
1151
|
+
paneClosedAt: session.tombstone.paneClosedAt
|
|
1378
1152
|
} : null
|
|
1379
1153
|
};
|
|
1380
1154
|
}
|
|
@@ -1538,7 +1312,7 @@ async function executeZellijPtyRead(args, dependencies = {}) {
|
|
|
1538
1312
|
const nextAdviceApi = dependencies.nextAdvice ?? nextAdvice;
|
|
1539
1313
|
const readOutputSnapshotApi = dependencies.readOutputSnapshot ?? readOutputSnapshot;
|
|
1540
1314
|
const validateGrepApi = dependencies.validateGrep ?? validateGrep;
|
|
1541
|
-
const paneExistsApi = dependencies.paneExists ?? zellijCli.paneExists;
|
|
1315
|
+
const paneExistsApi = dependencies.paneExists ?? ((paneId) => zellijCli.paneExists(paneId));
|
|
1542
1316
|
const session = sessionManagerApi.get(args.id);
|
|
1543
1317
|
const grepError = validateGrepApi(args.grep);
|
|
1544
1318
|
if (grepError) return {
|
|
@@ -1714,13 +1488,6 @@ const requestSudoTool = tool({
|
|
|
1714
1488
|
floating: true,
|
|
1715
1489
|
exitCodeToken
|
|
1716
1490
|
});
|
|
1717
|
-
const warnings = [];
|
|
1718
|
-
try {
|
|
1719
|
-
await zellijCli.focusPane(paneId);
|
|
1720
|
-
} catch (error) {
|
|
1721
|
-
if (!(error instanceof Error ? error.message : String(error)).includes("already focused")) throw error;
|
|
1722
|
-
warnings.push("Pane was already focused after creation.");
|
|
1723
|
-
}
|
|
1724
1491
|
const session = sessionManager.create({
|
|
1725
1492
|
openCodeSessionId: context.sessionID,
|
|
1726
1493
|
paneId,
|
|
@@ -1738,7 +1505,7 @@ const requestSudoTool = tool({
|
|
|
1738
1505
|
session: publicSession(session),
|
|
1739
1506
|
output: readOutputSnapshot(session.id),
|
|
1740
1507
|
next: nextAdvice(false, "The user must review the summary and commands in Zellij, then type YES and any required credentials directly in the pane."),
|
|
1741
|
-
warnings
|
|
1508
|
+
warnings: []
|
|
1742
1509
|
});
|
|
1743
1510
|
}
|
|
1744
1511
|
});
|
|
@@ -1942,281 +1709,149 @@ const zellijPtyWriteTool = tool({
|
|
|
1942
1709
|
}
|
|
1943
1710
|
});
|
|
1944
1711
|
//#endregion
|
|
1945
|
-
//#region src/
|
|
1946
|
-
/**
|
|
1947
|
-
|
|
1712
|
+
//#region src/utils/runtime.ts
|
|
1713
|
+
/**
|
|
1714
|
+
* Detect whether the plugin process is running inside an OpenCode TUI
|
|
1715
|
+
* session, as opposed to a headless `opencode run` invocation.
|
|
1716
|
+
*
|
|
1717
|
+
* OpenCode spawns the TUI's renderer as a worker child process and
|
|
1718
|
+
* explicitly sets `OPENCODE_PROCESS_ROLE=worker`
|
|
1719
|
+
* (see `packages/opencode/src/cli/cmd/tui/thread.ts` in opencode). The
|
|
1720
|
+
* headless `opencode run` command keeps the default `main` role set by
|
|
1721
|
+
* the CLI entry point, so this is the most reliable signal to tell TUI
|
|
1722
|
+
* from headless.
|
|
1723
|
+
*
|
|
1724
|
+
* Outside the TUI there is no surface for toasts, prompts, or Zellij
|
|
1725
|
+
* panes, and the plugin's lifecycle hooks (watchdogs, tab title actor,
|
|
1726
|
+
* completion notifications) misbehave. The plugin short-circuits to a
|
|
1727
|
+
* no-op in headless mode to avoid leaking side effects.
|
|
1728
|
+
*/
|
|
1729
|
+
function isOpencodeTuiMode() {
|
|
1730
|
+
return process.env.OPENCODE_PROCESS_ROLE === "worker";
|
|
1731
|
+
}
|
|
1732
|
+
//#endregion
|
|
1733
|
+
//#region src/utils/logger.ts
|
|
1734
|
+
const defaultDebugLogPath = `${process.env.XDG_CACHE_HOME?.trim() || `${homedir()}/.cache`}/opencode-zellij/debug.log`;
|
|
1735
|
+
function buildRootLogger() {
|
|
1736
|
+
const filename = process.env.OPENCODE_ZELLIJ_DEBUG_LOG?.trim() || defaultDebugLogPath;
|
|
1737
|
+
if (!filename) return null;
|
|
1948
1738
|
try {
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
}
|
|
1968
|
-
snapshot[sessionID] = parsed;
|
|
1969
|
-
}
|
|
1970
|
-
return snapshot;
|
|
1971
|
-
} catch (err) {
|
|
1972
|
-
debug("fetchPromptIdleStatusSnapshot failed", errorMessage(err));
|
|
1973
|
-
return;
|
|
1739
|
+
mkdirSync(dirname(filename), { recursive: true });
|
|
1740
|
+
} catch {
|
|
1741
|
+
return null;
|
|
1742
|
+
}
|
|
1743
|
+
try {
|
|
1744
|
+
return new LogLayer({
|
|
1745
|
+
errorSerializer: serializeError,
|
|
1746
|
+
transport: new LogFileRotationTransport({
|
|
1747
|
+
filename: filename.includes("%DATE%") ? filename : `${filename}-%DATE%`,
|
|
1748
|
+
dateFormat: "YMD",
|
|
1749
|
+
size: "1M",
|
|
1750
|
+
maxLogs: "7d",
|
|
1751
|
+
frequency: "daily",
|
|
1752
|
+
compressOnRotate: true
|
|
1753
|
+
})
|
|
1754
|
+
});
|
|
1755
|
+
} catch {
|
|
1756
|
+
return null;
|
|
1974
1757
|
}
|
|
1975
1758
|
}
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1759
|
+
const rootLogger = buildRootLogger();
|
|
1760
|
+
/**
|
|
1761
|
+
* Returns a child logger that prefixes every message with `[name]`, or
|
|
1762
|
+
* null if the root logger failed to initialize.
|
|
1763
|
+
*/
|
|
1764
|
+
function getChildLogger(name) {
|
|
1765
|
+
return rootLogger?.withPrefix(`[${name}]`) ?? null;
|
|
1766
|
+
}
|
|
1767
|
+
//#endregion
|
|
1768
|
+
//#region src/zellij/completion-notifications.ts
|
|
1769
|
+
const logger = getChildLogger("completion-notifications");
|
|
1770
|
+
async function postPromptAsync(serverUrl, sessionID, body) {
|
|
1771
|
+
const url = new URL(`/session/${encodeURIComponent(sessionID)}/prompt_async`, serverUrl);
|
|
1772
|
+
const headers = { "Content-Type": "application/json" };
|
|
1773
|
+
const directory = process.env.OPENCODE_DIRECTORY?.trim();
|
|
1774
|
+
if (directory) headers["x-opencode-directory"] = encodeURIComponent(directory);
|
|
1775
|
+
return fetch(url, {
|
|
1776
|
+
method: "POST",
|
|
1777
|
+
headers,
|
|
1778
|
+
body: JSON.stringify(body)
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
function formatExitCode(exitCode) {
|
|
1782
|
+
return exitCode === null ? "?" : String(exitCode);
|
|
1986
1783
|
}
|
|
1987
|
-
|
|
1988
|
-
const
|
|
1989
|
-
|
|
1990
|
-
function buildQueuedCompletionNotice(events) {
|
|
1991
|
-
return [queuedNoticeHeader, ...events.map((event) => `- ${event.session.id} (${event.session.paneId}) 已完成,請使用 zellij_pty_read 讀取最終輸出並清理 pane。`)].join("\n");
|
|
1784
|
+
function buildCompletionPromptText(event) {
|
|
1785
|
+
const { paneId, exitCode } = event.session;
|
|
1786
|
+
return `[zellij_pty] pane ${paneId} exit=${formatExitCode(exitCode)} — call zellij_pty_read to read, then zellij_pty_kill to close.`;
|
|
1992
1787
|
}
|
|
1993
1788
|
function buildCompletionPromptRequest(event) {
|
|
1994
1789
|
return {
|
|
1995
|
-
|
|
1996
|
-
body: { parts: [{
|
|
1997
|
-
type: "text",
|
|
1998
|
-
text: completionMessage
|
|
1999
|
-
}] }
|
|
2000
|
-
};
|
|
2001
|
-
}
|
|
2002
|
-
function injectQueuedCompletionNotice(input, notice) {
|
|
2003
|
-
if (typeof input === "string") return `${notice}\n\n${input}`;
|
|
2004
|
-
if (!input || typeof input !== "object") return input;
|
|
2005
|
-
const record = input;
|
|
2006
|
-
if (Array.isArray(record.parts)) return {
|
|
2007
|
-
...record,
|
|
1790
|
+
sessionID: event.session.openCodeSessionId,
|
|
2008
1791
|
parts: [{
|
|
2009
1792
|
type: "text",
|
|
2010
|
-
text:
|
|
2011
|
-
}
|
|
2012
|
-
};
|
|
2013
|
-
if (typeof record.message === "string") return {
|
|
2014
|
-
...record,
|
|
2015
|
-
message: `${notice}\n\n${record.message}`
|
|
2016
|
-
};
|
|
2017
|
-
if (typeof record.content === "string") return {
|
|
2018
|
-
...record,
|
|
2019
|
-
content: `${notice}\n\n${record.content}`
|
|
2020
|
-
};
|
|
2021
|
-
if (typeof record.text === "string") return {
|
|
2022
|
-
...record,
|
|
2023
|
-
text: `${notice}\n\n${record.text}`
|
|
2024
|
-
};
|
|
2025
|
-
return {
|
|
2026
|
-
...record,
|
|
2027
|
-
message: notice
|
|
2028
|
-
};
|
|
2029
|
-
}
|
|
2030
|
-
function evaluateCompletionPromptDecision(input) {
|
|
2031
|
-
if (input.config.mode !== "prompt") return {
|
|
2032
|
-
shouldPrompt: false,
|
|
2033
|
-
shouldQueue: false,
|
|
2034
|
-
reason: "prompt mode disabled"
|
|
2035
|
-
};
|
|
2036
|
-
if (input.event.session.humanInputOnly || !input.event.session.allowAgentInput) return {
|
|
2037
|
-
shouldPrompt: false,
|
|
2038
|
-
shouldQueue: true,
|
|
2039
|
-
reason: "human-input-only session"
|
|
2040
|
-
};
|
|
2041
|
-
if (!input.promptClientAvailable) return {
|
|
2042
|
-
shouldPrompt: false,
|
|
2043
|
-
shouldQueue: true,
|
|
2044
|
-
reason: "prompt client unavailable"
|
|
2045
|
-
};
|
|
2046
|
-
if (!input.event.session.openCodeSessionId) return {
|
|
2047
|
-
shouldPrompt: false,
|
|
2048
|
-
shouldQueue: true,
|
|
2049
|
-
reason: "session id unavailable"
|
|
2050
|
-
};
|
|
2051
|
-
if (!input.snapshotAvailable) return {
|
|
2052
|
-
shouldPrompt: false,
|
|
2053
|
-
shouldQueue: true,
|
|
2054
|
-
reason: "session status snapshot unavailable"
|
|
2055
|
-
};
|
|
2056
|
-
if (input.config.prompt.maxAttempts <= 0 || input.promptAttemptCount >= input.config.prompt.maxAttempts) return {
|
|
2057
|
-
shouldPrompt: false,
|
|
2058
|
-
shouldQueue: true,
|
|
2059
|
-
reason: "prompt max attempts reached"
|
|
2060
|
-
};
|
|
2061
|
-
if (input.config.prompt.cooldownMs > 0 && input.lastPromptAttemptAt !== null && input.now - input.lastPromptAttemptAt < input.config.prompt.cooldownMs) return {
|
|
2062
|
-
shouldPrompt: false,
|
|
2063
|
-
shouldQueue: true,
|
|
2064
|
-
reason: "prompt cooldown active"
|
|
2065
|
-
};
|
|
2066
|
-
if (input.config.prompt.requireIdle) {
|
|
2067
|
-
const sessionId = input.event.session.openCodeSessionId;
|
|
2068
|
-
if (!sessionId) return {
|
|
2069
|
-
shouldPrompt: false,
|
|
2070
|
-
shouldQueue: true,
|
|
2071
|
-
reason: "session status unavailable"
|
|
2072
|
-
};
|
|
2073
|
-
const status = sessionId ? input.snapshot?.[sessionId] : void 0;
|
|
2074
|
-
if (!status) return {
|
|
2075
|
-
shouldPrompt: false,
|
|
2076
|
-
shouldQueue: true,
|
|
2077
|
-
reason: "session status unavailable"
|
|
2078
|
-
};
|
|
2079
|
-
if (status && status.type !== "idle") return {
|
|
2080
|
-
shouldPrompt: false,
|
|
2081
|
-
shouldQueue: true,
|
|
2082
|
-
reason: "session not idle"
|
|
2083
|
-
};
|
|
2084
|
-
}
|
|
2085
|
-
return {
|
|
2086
|
-
shouldPrompt: true,
|
|
2087
|
-
shouldQueue: false,
|
|
2088
|
-
reason: "prompt allowed"
|
|
1793
|
+
text: buildCompletionPromptText(event)
|
|
1794
|
+
}]
|
|
2089
1795
|
};
|
|
2090
1796
|
}
|
|
2091
|
-
var
|
|
2092
|
-
|
|
2093
|
-
constructor(context
|
|
1797
|
+
var SessionCompletionNotificationManager = class {
|
|
1798
|
+
seen = /* @__PURE__ */ new Set();
|
|
1799
|
+
constructor(context) {
|
|
2094
1800
|
this.context = context;
|
|
2095
|
-
this.hooks = hooks;
|
|
2096
|
-
this.clock = clock;
|
|
2097
|
-
}
|
|
2098
|
-
hasPending(sessionId) {
|
|
2099
|
-
return this.states.get(sessionId)?.queued ?? false;
|
|
2100
|
-
}
|
|
2101
|
-
clearSession(sessionId) {
|
|
2102
|
-
this.states.delete(sessionId);
|
|
2103
|
-
}
|
|
2104
|
-
clearAll() {
|
|
2105
|
-
this.states.clear();
|
|
2106
1801
|
}
|
|
2107
1802
|
dispose() {
|
|
2108
|
-
this.
|
|
1803
|
+
this.seen.clear();
|
|
2109
1804
|
}
|
|
2110
1805
|
async handleSessionTerminal(event) {
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
event,
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
switch (this.context.config.mode) {
|
|
2122
|
-
case "queue":
|
|
2123
|
-
state.queued = true;
|
|
2124
|
-
return;
|
|
2125
|
-
case "toast":
|
|
2126
|
-
await this.sendToast(state);
|
|
2127
|
-
this.finalize(state);
|
|
2128
|
-
return;
|
|
2129
|
-
case "queue+toast":
|
|
2130
|
-
state.queued = true;
|
|
2131
|
-
await this.sendToast(state);
|
|
2132
|
-
return;
|
|
2133
|
-
case "prompt":
|
|
2134
|
-
await this.tryPromptOrQueue(state);
|
|
2135
|
-
break;
|
|
2136
|
-
default:
|
|
2137
|
-
}
|
|
2138
|
-
}
|
|
2139
|
-
injectQueuedChatMessage(input) {
|
|
2140
|
-
const pending = [...this.states.values()].filter((state) => state.queued);
|
|
2141
|
-
if (pending.length === 0) return input;
|
|
2142
|
-
const notice = buildQueuedCompletionNotice(pending.map((state) => state.event));
|
|
2143
|
-
for (const state of pending) {
|
|
2144
|
-
if (!state.toastSent) this.context.markSent(state.event.sessionId);
|
|
2145
|
-
this.finalize(state);
|
|
2146
|
-
}
|
|
2147
|
-
return injectQueuedCompletionNotice(input, notice);
|
|
2148
|
-
}
|
|
2149
|
-
async tryPromptOrQueue(state) {
|
|
2150
|
-
if (!state.event.session.openCodeSessionId) {
|
|
2151
|
-
state.queued = true;
|
|
1806
|
+
logger?.withMetadata({
|
|
1807
|
+
session: event.sessionId,
|
|
1808
|
+
reason: event.reason,
|
|
1809
|
+
paneId: event.session.paneId,
|
|
1810
|
+
openCodeSessionId: event.session.openCodeSessionId ?? "null"
|
|
1811
|
+
}).info("handleSessionTerminal");
|
|
1812
|
+
if (this.seen.has(event.sessionId)) return;
|
|
1813
|
+
this.seen.add(event.sessionId);
|
|
1814
|
+
if (!event.session.openCodeSessionId) {
|
|
1815
|
+
logger?.withMetadata({ session: event.sessionId }).info("skipped: no openCodeSessionId");
|
|
2152
1816
|
return;
|
|
2153
1817
|
}
|
|
2154
1818
|
const session = this.context.client.session;
|
|
2155
|
-
const
|
|
2156
|
-
const
|
|
2157
|
-
const
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
now: this.clock(),
|
|
2163
|
-
lastPromptAttemptAt: state.promptAttemptedAt,
|
|
2164
|
-
promptAttemptCount: state.promptAttempts,
|
|
2165
|
-
promptClientAvailable: Boolean(prompt)
|
|
2166
|
-
});
|
|
2167
|
-
if (!decision.shouldPrompt) {
|
|
2168
|
-
state.queued = decision.shouldQueue;
|
|
1819
|
+
const sdkPrompt = session?.promptAsync ?? session?.prompt;
|
|
1820
|
+
const request = buildCompletionPromptRequest(event);
|
|
1821
|
+
const sessionID = request.sessionID;
|
|
1822
|
+
if (sdkPrompt) try {
|
|
1823
|
+
logger?.withMetadata({ sessionID }).info("SDK prompt attempt");
|
|
1824
|
+
await sdkPrompt(request);
|
|
1825
|
+
logger?.withMetadata({ sessionID }).info("SDK prompt ok");
|
|
2169
1826
|
return;
|
|
2170
|
-
}
|
|
2171
|
-
if (this.hooks.prompt) try {
|
|
2172
|
-
const maybePromise = this.hooks.prompt(state.event);
|
|
2173
|
-
if (maybePromise && typeof maybePromise.then === "function") await maybePromise;
|
|
2174
|
-
} catch (error) {
|
|
2175
|
-
debug("completion notification prompt hook failed", errorMessage(error));
|
|
2176
|
-
}
|
|
2177
|
-
if (!session || !prompt) {
|
|
2178
|
-
state.queued = true;
|
|
2179
|
-
return;
|
|
2180
|
-
}
|
|
2181
|
-
state.promptAttempts += 1;
|
|
2182
|
-
state.promptAttemptedAt = this.clock();
|
|
2183
|
-
try {
|
|
2184
|
-
if (session.prompt) await session.prompt(buildCompletionPromptRequest(state.event));
|
|
2185
|
-
else if (session.promptAsync) await session.promptAsync(buildCompletionPromptRequest(state.event));
|
|
2186
|
-
else {
|
|
2187
|
-
state.queued = true;
|
|
2188
|
-
return;
|
|
2189
|
-
}
|
|
2190
|
-
this.context.markSent(state.event.sessionId);
|
|
2191
|
-
this.finalize(state);
|
|
2192
1827
|
} catch (error) {
|
|
2193
|
-
|
|
2194
|
-
|
|
1828
|
+
logger?.withMetadata({
|
|
1829
|
+
sessionID,
|
|
1830
|
+
error: errorMessage(error)
|
|
1831
|
+
}).warn("SDK prompt failed, falling back to HTTP");
|
|
2195
1832
|
}
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
1833
|
+
else logger?.withMetadata({
|
|
1834
|
+
sessionID,
|
|
1835
|
+
sessionKeys: session ? Object.keys(session).join(",") : "null"
|
|
1836
|
+
}).info("no SDK prompt on client, using HTTP fallback");
|
|
1837
|
+
if (!this.context.serverUrl) {
|
|
1838
|
+
logger?.withMetadata({ sessionID }).error("no serverUrl for HTTP fallback");
|
|
2201
1839
|
return;
|
|
2202
1840
|
}
|
|
2203
1841
|
try {
|
|
2204
|
-
await
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
}
|
|
2210
|
-
state.toastSent = true;
|
|
2211
|
-
this.context.markSent(state.event.sessionId);
|
|
2212
|
-
if (!state.queued) this.finalize(state);
|
|
1842
|
+
const response = await postPromptAsync(this.context.serverUrl, sessionID, { parts: request.parts });
|
|
1843
|
+
logger?.withMetadata({
|
|
1844
|
+
sessionID,
|
|
1845
|
+
status: response.status,
|
|
1846
|
+
ok: response.ok
|
|
1847
|
+
}).info("HTTP fallback response");
|
|
2213
1848
|
} catch (error) {
|
|
2214
|
-
|
|
1849
|
+
logger?.withMetadata({
|
|
1850
|
+
sessionID,
|
|
1851
|
+
error: errorMessage(error)
|
|
1852
|
+
}).error("HTTP fallback threw");
|
|
2215
1853
|
}
|
|
2216
1854
|
}
|
|
2217
|
-
finalize(state) {
|
|
2218
|
-
this.states.delete(state.event.sessionId);
|
|
2219
|
-
}
|
|
2220
1855
|
};
|
|
2221
1856
|
//#endregion
|
|
2222
1857
|
//#region src/zellij/shutdown-cleanup.ts
|
|
@@ -2558,9 +2193,6 @@ var TabTitleManager = class {
|
|
|
2558
2193
|
emojis;
|
|
2559
2194
|
enabled;
|
|
2560
2195
|
destroyed = false;
|
|
2561
|
-
originalTabTitle;
|
|
2562
|
-
originalTabTitleLoaded = false;
|
|
2563
|
-
originalTabTitlePromise;
|
|
2564
2196
|
destroyPromise;
|
|
2565
2197
|
actor;
|
|
2566
2198
|
constructor(options) {
|
|
@@ -2586,8 +2218,6 @@ var TabTitleManager = class {
|
|
|
2586
2218
|
}
|
|
2587
2219
|
async renderImmediate() {
|
|
2588
2220
|
if (!this.enabled || this.destroyed) return;
|
|
2589
|
-
await this.ensureOriginalTabTitle();
|
|
2590
|
-
if (this.destroyed) return;
|
|
2591
2221
|
this.desiredTitle = this.buildTitle();
|
|
2592
2222
|
this.clearDebounceTimer();
|
|
2593
2223
|
await this.syncDesiredTitle();
|
|
@@ -2609,7 +2239,6 @@ var TabTitleManager = class {
|
|
|
2609
2239
|
async syncDesiredTitle() {
|
|
2610
2240
|
if (!this.enabled || this.destroyed) return;
|
|
2611
2241
|
const generation = this.syncGeneration;
|
|
2612
|
-
await this.ensureOriginalTabTitle();
|
|
2613
2242
|
if (this.destroyed || generation !== this.syncGeneration) return;
|
|
2614
2243
|
if (this.syncInFlight) return this.syncPromise;
|
|
2615
2244
|
this.syncInFlight = true;
|
|
@@ -2659,23 +2288,6 @@ var TabTitleManager = class {
|
|
|
2659
2288
|
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
2660
2289
|
this.debounceTimer = void 0;
|
|
2661
2290
|
}
|
|
2662
|
-
async ensureOriginalTabTitle() {
|
|
2663
|
-
if (!this.enabled || this.originalTabTitleLoaded) return;
|
|
2664
|
-
if (this.originalTabTitlePromise) return this.originalTabTitlePromise;
|
|
2665
|
-
this.originalTabTitlePromise = this.saveOriginalTabTitle();
|
|
2666
|
-
return this.originalTabTitlePromise;
|
|
2667
|
-
}
|
|
2668
|
-
async saveOriginalTabTitle() {
|
|
2669
|
-
try {
|
|
2670
|
-
const title = await this.cli.currentTabTitle();
|
|
2671
|
-
if (title !== void 0) this.originalTabTitle = title;
|
|
2672
|
-
} catch (error) {
|
|
2673
|
-
debug("TabTitleManager failed to save original tab title", errorMessage(error));
|
|
2674
|
-
} finally {
|
|
2675
|
-
this.originalTabTitleLoaded = true;
|
|
2676
|
-
this.originalTabTitlePromise = void 0;
|
|
2677
|
-
}
|
|
2678
|
-
}
|
|
2679
2291
|
destroy() {
|
|
2680
2292
|
if (this.destroyed) return this.destroyPromise ?? Promise.resolve();
|
|
2681
2293
|
this.destroyed = true;
|
|
@@ -2683,21 +2295,12 @@ var TabTitleManager = class {
|
|
|
2683
2295
|
this.desiredTitle = void 0;
|
|
2684
2296
|
this.clearDebounceTimer();
|
|
2685
2297
|
this.clearRetryTimer();
|
|
2686
|
-
|
|
2687
|
-
this.destroyPromise = this.restoreOriginalTabTitle().catch((error) => debug("TabTitleManager failed to restore original tab title", errorMessage(error)));
|
|
2688
|
-
return this.destroyPromise;
|
|
2689
|
-
}
|
|
2690
|
-
async restoreOriginalTabTitle() {
|
|
2691
|
-
await this.originalTabTitlePromise;
|
|
2692
|
-
await this.syncPromise;
|
|
2693
|
-
const originalTitle = this.originalTabTitle;
|
|
2694
|
-
this.originalTabTitle = void 0;
|
|
2695
|
-
if (originalTitle === void 0) return;
|
|
2696
|
-
await this.cli.renameTab(originalTitle);
|
|
2298
|
+
return Promise.resolve();
|
|
2697
2299
|
}
|
|
2698
2300
|
};
|
|
2699
2301
|
//#endregion
|
|
2700
2302
|
//#region src/plugin.ts
|
|
2303
|
+
const PLUGIN_ID = "opencode-zellij";
|
|
2701
2304
|
function createPtyTools(defaultCleanupExitedPaneOnRead) {
|
|
2702
2305
|
return {
|
|
2703
2306
|
zellij_pty_spawn: zellijPtySpawnTool,
|
|
@@ -2713,29 +2316,6 @@ function getProjectName(path) {
|
|
|
2713
2316
|
function getWorkspaceRoot(input) {
|
|
2714
2317
|
return input.worktree || input.directory || process.cwd();
|
|
2715
2318
|
}
|
|
2716
|
-
function showUpdateToast(client, result) {
|
|
2717
|
-
if (result.type === "updated") client.tui.showToast({ body: {
|
|
2718
|
-
title: "opencode-zellij updated",
|
|
2719
|
-
message: `Updated to ${result.toVersion}. Restart OpenCode to apply the changes.`,
|
|
2720
|
-
variant: "success",
|
|
2721
|
-
duration: 1e4
|
|
2722
|
-
} }).catch((error) => debug("show update toast for successful update failed", errorMessage(error)));
|
|
2723
|
-
else if (result.type === "failed") client.tui.showToast({ body: {
|
|
2724
|
-
title: "opencode-zellij update failed",
|
|
2725
|
-
message: `Failed to update to ${result.latestVersion}.`,
|
|
2726
|
-
variant: "error",
|
|
2727
|
-
duration: 8e3
|
|
2728
|
-
} }).catch((error) => debug("show update toast for failed update failed", errorMessage(error)));
|
|
2729
|
-
}
|
|
2730
|
-
function startAutoUpdateCheck(client, importMetaUrl, check = checkAndUpdate) {
|
|
2731
|
-
(async () => {
|
|
2732
|
-
try {
|
|
2733
|
-
showUpdateToast(client, await check({ importMetaUrl }));
|
|
2734
|
-
} catch (cause) {
|
|
2735
|
-
debug("auto-update check failed", errorMessage(cause));
|
|
2736
|
-
}
|
|
2737
|
-
})();
|
|
2738
|
-
}
|
|
2739
2319
|
async function cleanupStep(stepName, sessionId, step) {
|
|
2740
2320
|
try {
|
|
2741
2321
|
await step();
|
|
@@ -2751,6 +2331,10 @@ async function cleanupDeletedSession(sessionId) {
|
|
|
2751
2331
|
}
|
|
2752
2332
|
function createZellijPtyPlugin(dependencies = {}) {
|
|
2753
2333
|
return async (input) => {
|
|
2334
|
+
if (!isOpencodeTuiMode()) {
|
|
2335
|
+
debug("opencode-zellij disabled: not running inside an OpenCode TUI session");
|
|
2336
|
+
return {};
|
|
2337
|
+
}
|
|
2754
2338
|
const { config, warnings } = await loadConfig(input);
|
|
2755
2339
|
for (const warning of warnings) debug(warning);
|
|
2756
2340
|
configureSudoPane(config.pty.sudoPane === "allow");
|
|
@@ -2778,60 +2362,35 @@ function createZellijPtyPlugin(dependencies = {}) {
|
|
|
2778
2362
|
branch: config.tabTitle.emojiBranch
|
|
2779
2363
|
}
|
|
2780
2364
|
}) : void 0;
|
|
2781
|
-
const
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
try {
|
|
2788
|
-
sessionManager.markTerminalNotificationSent(sessionId);
|
|
2789
|
-
} catch (error) {
|
|
2790
|
-
debug("mark terminal notification sent failed", errorMessage(error));
|
|
2791
|
-
}
|
|
2792
|
-
}
|
|
2793
|
-
}) ?? new SessionCompletionNotificationQueue({
|
|
2794
|
-
client,
|
|
2795
|
-
workspaceRoot,
|
|
2796
|
-
config: config.pty.completionNotification,
|
|
2797
|
-
markSent(sessionId) {
|
|
2798
|
-
try {
|
|
2799
|
-
sessionManager.markTerminalNotificationSent(sessionId);
|
|
2800
|
-
} catch (error) {
|
|
2801
|
-
debug("mark terminal notification sent failed", errorMessage(error));
|
|
2802
|
-
}
|
|
2803
|
-
}
|
|
2365
|
+
const completionNotifications = dependencies.createCompletionNotifications?.({
|
|
2366
|
+
client: { session: input.client?.session },
|
|
2367
|
+
serverUrl: input.serverUrl
|
|
2368
|
+
}) ?? new SessionCompletionNotificationManager({
|
|
2369
|
+
client: { session: input.client?.session },
|
|
2370
|
+
serverUrl: input.serverUrl
|
|
2804
2371
|
});
|
|
2805
|
-
subscriberManager.setLifecycleHooks(
|
|
2372
|
+
subscriberManager.setLifecycleHooks({ onSessionTerminal: (event) => void completionNotifications.handleSessionTerminal(event).catch((error) => debug("completion notification lifecycle hook failed", errorMessage(error))) });
|
|
2806
2373
|
if (actor) await actor.ready;
|
|
2807
2374
|
tabTitleManager?.renderImmediate().catch((error) => debug("initial tab title render failed", errorMessage(error)));
|
|
2808
|
-
if (config.autoUpdate) (dependencies.startAutoUpdateCheck ?? startAutoUpdateCheck)(client, dependencies.importMetaUrl ?? import.meta.url);
|
|
2809
2375
|
return {
|
|
2810
2376
|
async event(input) {
|
|
2811
2377
|
const event = input.event;
|
|
2812
2378
|
if (actor && tabTitleManager) {
|
|
2813
2379
|
await actor.handleEvent(event);
|
|
2814
|
-
|
|
2815
|
-
else tabTitleManager.scheduleUpdate();
|
|
2380
|
+
tabTitleManager.scheduleUpdate();
|
|
2816
2381
|
}
|
|
2817
2382
|
if (event.type === "server.instance.disposed" || event.type === "global.disposed") {
|
|
2818
|
-
completionNotifications
|
|
2819
|
-
completionNotifications?.dispose();
|
|
2383
|
+
completionNotifications.dispose();
|
|
2820
2384
|
subscriberManager.setLifecycleHooks(void 0);
|
|
2821
2385
|
}
|
|
2822
2386
|
if (event.type === "session.deleted") {
|
|
2823
2387
|
const sessionID = deletedSessionID$1(event);
|
|
2824
2388
|
if (!sessionID) return;
|
|
2825
2389
|
const sessions = sessionManager.listByOpenCodeSession(sessionID);
|
|
2826
|
-
for (const session of sessions) completionNotifications?.clearSession(session.id);
|
|
2827
2390
|
await Promise.all(sessions.map((session) => cleanupDeletedSession(session.id)));
|
|
2828
2391
|
}
|
|
2829
2392
|
},
|
|
2830
|
-
|
|
2831
|
-
const injected = completionNotifications?.injectQueuedChatMessage(output) ?? output;
|
|
2832
|
-
if (injected !== output && injected && typeof injected === "object" && Array.isArray(injected.parts)) output.parts = injected.parts;
|
|
2833
|
-
},
|
|
2834
|
-
"tool": config.pty.enabled ? {
|
|
2393
|
+
tool: config.pty.enabled ? {
|
|
2835
2394
|
...createPtyTools(config.pty.cleanupExitedPaneOnRead),
|
|
2836
2395
|
...config.pty.sudoPane === "hide" ? {} : { zellij_pty_request_sudo: requestSudoTool }
|
|
2837
2396
|
} : {}
|
|
@@ -2839,7 +2398,11 @@ function createZellijPtyPlugin(dependencies = {}) {
|
|
|
2839
2398
|
};
|
|
2840
2399
|
}
|
|
2841
2400
|
const ZellijPtyPlugin = createZellijPtyPlugin();
|
|
2401
|
+
var plugin_default = {
|
|
2402
|
+
id: PLUGIN_ID,
|
|
2403
|
+
server: ZellijPtyPlugin
|
|
2404
|
+
};
|
|
2842
2405
|
//#endregion
|
|
2843
|
-
export { ZellijPtyPlugin,
|
|
2406
|
+
export { ZellijPtyPlugin, createZellijPtyPlugin, plugin_default as default };
|
|
2844
2407
|
|
|
2845
2408
|
//# sourceMappingURL=index.mjs.map
|