opencode-zellij 0.0.1 → 0.0.2
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 +36 -2
- package/README.zh.md +36 -2
- package/dist/index.mjs +477 -20
- package/dist/index.mjs.map +1 -1
- package/dist/pane-watchdog-runner.d.mts +1 -0
- package/dist/pane-watchdog-runner.mjs +69 -0
- package/dist/pane-watchdog-runner.mjs.map +1 -0
- package/package.json +7 -1
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import process from "node:process";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { setTimeout } from "node:timers/promises";
|
|
3
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
3
4
|
import { tool } from "@opencode-ai/plugin";
|
|
4
|
-
import { execFile, spawn } from "node:child_process";
|
|
5
|
-
import process from "node:process";
|
|
5
|
+
import { execFile, spawn, spawnSync } from "node:child_process";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
7
11
|
import { Buffer } from "node:buffer";
|
|
8
12
|
//#region src/utils/shell-args.ts
|
|
9
13
|
const directCommandExitWrapper = "token=\"$1\"; shift; set +e; \"$@\"; code=$?; printf \"\\n[zellij-pty:%s] exit-code=%s\\n\" \"$token\" \"$code\"; exit \"$code\"";
|
|
@@ -74,7 +78,7 @@ function assertCommandAllowed(input) {
|
|
|
74
78
|
for (const pattern of denyPatterns) if (pattern.test(commandLine)) throw new Error(`Command denied by zellij-pty policy: ${commandLine}`);
|
|
75
79
|
for (const pattern of configuredDenyCommands) if (wildcardMatches(pattern, commandLine)) throw new Error(`Command denied by zellij-pty configured deny rule: ${commandLine}`);
|
|
76
80
|
if (configuredAllowCommands.length > 0 && !configuredAllowCommands.some((pattern) => wildcardMatches(pattern, commandLine))) throw new Error(`Command denied by zellij-pty allow list: ${commandLine}`);
|
|
77
|
-
if (!input.humanInputOnly && sudoPattern.test(commandLine)) throw new Error("sudo commands must use
|
|
81
|
+
if (!input.humanInputOnly && sudoPattern.test(commandLine)) throw new Error("sudo commands must use zellij_pty_request_sudo so credentials stay human-input-only and never pass through agent tool input.");
|
|
78
82
|
if (input.humanInputOnly && sudoPattern.test(commandLine) && !allowSudoPane) throw new Error("sudo pane is disabled by zellij-pty policy.");
|
|
79
83
|
}
|
|
80
84
|
//#endregion
|
|
@@ -159,7 +163,7 @@ var SessionManager = class {
|
|
|
159
163
|
const sessionManager = new SessionManager();
|
|
160
164
|
//#endregion
|
|
161
165
|
//#region src/zellij/cli.ts
|
|
162
|
-
const execFileAsync = promisify(execFile);
|
|
166
|
+
const execFileAsync$1 = promisify(execFile);
|
|
163
167
|
function zellijCommandArgs(actionArgs) {
|
|
164
168
|
const sessionName = process.env.ZELLIJ_SESSION_NAME?.trim();
|
|
165
169
|
if (sessionName) return [
|
|
@@ -185,6 +189,13 @@ function buildNewPaneActionArgs(options) {
|
|
|
185
189
|
args.push("--", ...buildCommandArgv(options, { exitCodeToken: options.exitCodeToken }));
|
|
186
190
|
return args;
|
|
187
191
|
}
|
|
192
|
+
function buildRenameTabActionArgs(title) {
|
|
193
|
+
return [
|
|
194
|
+
"action",
|
|
195
|
+
"rename-tab",
|
|
196
|
+
title
|
|
197
|
+
];
|
|
198
|
+
}
|
|
188
199
|
function ensureZellijTarget() {
|
|
189
200
|
if (process.env.ZELLIJ || process.env.ZELLIJ_SESSION_NAME) return;
|
|
190
201
|
throw new Error("Zellij context not found. Run OpenCode inside Zellij or set ZELLIJ_SESSION_NAME to an existing session.");
|
|
@@ -192,7 +203,7 @@ function ensureZellijTarget() {
|
|
|
192
203
|
async function runZellij(actionArgs, options = {}) {
|
|
193
204
|
ensureZellijTarget();
|
|
194
205
|
try {
|
|
195
|
-
const result = await execFileAsync("zellij", zellijCommandArgs(actionArgs), {
|
|
206
|
+
const result = await execFileAsync$1("zellij", zellijCommandArgs(actionArgs), {
|
|
196
207
|
encoding: "utf8",
|
|
197
208
|
timeout: options.timeoutMs ?? 1e4,
|
|
198
209
|
maxBuffer: 20 * 1024 * 1024
|
|
@@ -230,6 +241,14 @@ var ZellijCli = class {
|
|
|
230
241
|
async closePane(paneId) {
|
|
231
242
|
await runZellij(zellijActionArgs("close-pane", ["--pane-id", paneId]));
|
|
232
243
|
}
|
|
244
|
+
closePaneSync(paneId) {
|
|
245
|
+
ensureZellijTarget();
|
|
246
|
+
spawnSync("zellij", zellijCommandArgs(zellijActionArgs("close-pane", ["--pane-id", paneId])), {
|
|
247
|
+
encoding: "utf8",
|
|
248
|
+
stdio: "ignore",
|
|
249
|
+
timeout: 2e3
|
|
250
|
+
});
|
|
251
|
+
}
|
|
233
252
|
async focusPane(paneId) {
|
|
234
253
|
await runZellij(zellijActionArgs("focus-pane-id", [paneId]));
|
|
235
254
|
}
|
|
@@ -240,9 +259,149 @@ var ZellijCli = class {
|
|
|
240
259
|
"--full"
|
|
241
260
|
]), { timeoutMs: 1e4 })).stdout;
|
|
242
261
|
}
|
|
262
|
+
async renameTab(title) {
|
|
263
|
+
await runZellij(buildRenameTabActionArgs(title));
|
|
264
|
+
}
|
|
243
265
|
};
|
|
244
266
|
const zellijCli = new ZellijCli();
|
|
245
267
|
//#endregion
|
|
268
|
+
//#region src/zellij/pane-watchdog.ts
|
|
269
|
+
const instanceId = randomUUID();
|
|
270
|
+
let watchdogStarted = false;
|
|
271
|
+
function registryDirectory() {
|
|
272
|
+
const base = process.env.XDG_RUNTIME_DIR || tmpdir();
|
|
273
|
+
return path.join(base, `opencode-zellij-${process.getuid?.() ?? "user"}`);
|
|
274
|
+
}
|
|
275
|
+
function watchdogRegistryPath() {
|
|
276
|
+
return path.join(registryDirectory(), `panes-${process.pid}-${instanceId}.json`);
|
|
277
|
+
}
|
|
278
|
+
function parseLinuxProcessStartTime(stat) {
|
|
279
|
+
return stat.slice(stat.lastIndexOf(")") + 2).trim().split(/\s+/)[19] ?? null;
|
|
280
|
+
}
|
|
281
|
+
function linuxProcessStartTime(pid) {
|
|
282
|
+
try {
|
|
283
|
+
return parseLinuxProcessStartTime(readFileSync(`/proc/${pid}/stat`, "utf8"));
|
|
284
|
+
} catch {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function emptyRegistry() {
|
|
289
|
+
return {
|
|
290
|
+
version: 1,
|
|
291
|
+
instanceId,
|
|
292
|
+
ownerPid: process.pid,
|
|
293
|
+
ownerStartTime: linuxProcessStartTime(process.pid),
|
|
294
|
+
zellijSessionName: process.env.ZELLIJ_SESSION_NAME?.trim() || null,
|
|
295
|
+
panes: []
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function readRegistry() {
|
|
299
|
+
const file = watchdogRegistryPath();
|
|
300
|
+
if (!existsSync(file)) return emptyRegistry();
|
|
301
|
+
try {
|
|
302
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
303
|
+
if (parsed.version !== 1 || parsed.instanceId !== instanceId || parsed.ownerPid !== process.pid || !Array.isArray(parsed.panes)) return emptyRegistry();
|
|
304
|
+
return parsed;
|
|
305
|
+
} catch {
|
|
306
|
+
return emptyRegistry();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function writeRegistry(registry) {
|
|
310
|
+
mkdirSync(registryDirectory(), {
|
|
311
|
+
recursive: true,
|
|
312
|
+
mode: 448
|
|
313
|
+
});
|
|
314
|
+
const file = watchdogRegistryPath();
|
|
315
|
+
const tempFile = `${file}.tmp-${process.pid}`;
|
|
316
|
+
writeFileSync(tempFile, JSON.stringify(registry, null, 2), { mode: 384 });
|
|
317
|
+
renameSync(tempFile, file);
|
|
318
|
+
}
|
|
319
|
+
function ensureWatchdog() {
|
|
320
|
+
if (watchdogStarted) return;
|
|
321
|
+
watchdogStarted = true;
|
|
322
|
+
spawn("node", [watchdogRunnerPath(), watchdogRegistryPath()], {
|
|
323
|
+
detached: true,
|
|
324
|
+
stdio: "ignore",
|
|
325
|
+
env: process.env
|
|
326
|
+
}).unref();
|
|
327
|
+
}
|
|
328
|
+
function watchdogRunnerPath() {
|
|
329
|
+
return fileURLToPath(new URL("./pane-watchdog-runner.mjs", import.meta.url));
|
|
330
|
+
}
|
|
331
|
+
function cleanupStaleWatchdogRegistries() {
|
|
332
|
+
const directory = registryDirectory();
|
|
333
|
+
if (!existsSync(directory)) return;
|
|
334
|
+
for (const fileName of readdirSync(directory)) {
|
|
335
|
+
if (!fileName.startsWith("panes-") || !fileName.endsWith(".json")) continue;
|
|
336
|
+
const file = path.join(directory, fileName);
|
|
337
|
+
try {
|
|
338
|
+
const registry = JSON.parse(readFileSync(file, "utf8"));
|
|
339
|
+
if (registry.version !== 1 || ownerStillMatches(registry)) continue;
|
|
340
|
+
closeRegistryPanes(registry);
|
|
341
|
+
rmSync(file, { force: true });
|
|
342
|
+
} catch {
|
|
343
|
+
rmSync(file, { force: true });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function ownerStillMatches(registry) {
|
|
348
|
+
try {
|
|
349
|
+
process.kill(registry.ownerPid, 0);
|
|
350
|
+
} catch {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
return !registry.ownerStartTime || linuxProcessStartTime(registry.ownerPid) === registry.ownerStartTime;
|
|
354
|
+
}
|
|
355
|
+
function closeRegistryPanes(registry) {
|
|
356
|
+
for (const pane of registry.panes) {
|
|
357
|
+
const args = [];
|
|
358
|
+
if (registry.zellijSessionName) args.push("--session", registry.zellijSessionName);
|
|
359
|
+
args.push("action", "close-pane", "--pane-id", pane.paneId);
|
|
360
|
+
spawn("zellij", args, {
|
|
361
|
+
detached: true,
|
|
362
|
+
stdio: "ignore",
|
|
363
|
+
env: process.env
|
|
364
|
+
}).unref();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function upsertWatchdogPane(registry, session) {
|
|
368
|
+
return {
|
|
369
|
+
...registry,
|
|
370
|
+
panes: [...registry.panes.filter((pane) => pane.sessionId !== session.id && pane.paneId !== session.paneId), {
|
|
371
|
+
sessionId: session.id,
|
|
372
|
+
paneId: session.paneId,
|
|
373
|
+
title: session.title,
|
|
374
|
+
openCodeSessionId: session.openCodeSessionId,
|
|
375
|
+
createdAt: session.createdAt
|
|
376
|
+
}]
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function removeWatchdogPane(registry, sessionId) {
|
|
380
|
+
return {
|
|
381
|
+
...registry,
|
|
382
|
+
panes: registry.panes.filter((pane) => pane.sessionId !== sessionId)
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function registerPaneForWatchdog(session) {
|
|
386
|
+
writeRegistry(upsertWatchdogPane(readRegistry(), session));
|
|
387
|
+
ensureWatchdog();
|
|
388
|
+
}
|
|
389
|
+
function unregisterPaneFromWatchdog(sessionId) {
|
|
390
|
+
const registry = readRegistry();
|
|
391
|
+
const updated = removeWatchdogPane(registry, sessionId);
|
|
392
|
+
if (updated.panes.length === registry.panes.length) return;
|
|
393
|
+
if (updated.panes.length === 0) {
|
|
394
|
+
removeWatchdogRegistry();
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
writeRegistry(updated);
|
|
398
|
+
}
|
|
399
|
+
function removeWatchdogRegistry() {
|
|
400
|
+
try {
|
|
401
|
+
rmSync(watchdogRegistryPath(), { force: true });
|
|
402
|
+
} catch {}
|
|
403
|
+
}
|
|
404
|
+
//#endregion
|
|
246
405
|
//#region src/pty/ring-buffer.ts
|
|
247
406
|
const ansiPattern$1 = new RegExp(`${String.fromCharCode(27)}\\[[0-9;?]*[a-z]`, "gi");
|
|
248
407
|
function normalizeLines(input) {
|
|
@@ -501,6 +660,7 @@ var SubscriberManager = class {
|
|
|
501
660
|
state.buffer.append(`[zellij-pty] Pane ${session.paneId} closed at ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
502
661
|
this.sessions.updateLineCount(sessionId, state.buffer.lineCount);
|
|
503
662
|
this.sessions.updateStatus(sessionId, session.status === "killed" ? "killed" : "exited");
|
|
663
|
+
unregisterPaneFromWatchdog(sessionId);
|
|
504
664
|
this.stop(sessionId);
|
|
505
665
|
return;
|
|
506
666
|
}
|
|
@@ -629,7 +789,7 @@ const zellijPtyKillTool = tool({
|
|
|
629
789
|
const output = subscriberManager.has(session.id) ? readOutputSnapshot(session.id) : void 0;
|
|
630
790
|
try {
|
|
631
791
|
await zellijCli.sendCtrlC(session.paneId);
|
|
632
|
-
await setTimeout(500);
|
|
792
|
+
await setTimeout$1(500);
|
|
633
793
|
} catch (error) {
|
|
634
794
|
warnings.push(`Ctrl-C failed or pane was already gone: ${error instanceof Error ? error.message : String(error)}`);
|
|
635
795
|
}
|
|
@@ -649,6 +809,7 @@ const zellijPtyKillTool = tool({
|
|
|
649
809
|
}
|
|
650
810
|
subscriberManager.stop(session.id);
|
|
651
811
|
subscriberManager.forget(session.id);
|
|
812
|
+
unregisterPaneFromWatchdog(session.id);
|
|
652
813
|
sessionManager.remove(session.id);
|
|
653
814
|
return jsonResponse({
|
|
654
815
|
killed: true,
|
|
@@ -784,7 +945,7 @@ const requestSudoTool = tool({
|
|
|
784
945
|
humanInputOnly: true
|
|
785
946
|
});
|
|
786
947
|
const command = buildReviewScript(args.summary, args.scripts);
|
|
787
|
-
const title = createOpenCodePaneTitle("
|
|
948
|
+
const title = createOpenCodePaneTitle("zellij_pty_request_sudo");
|
|
788
949
|
const paneId = await zellijCli.newPane({
|
|
789
950
|
command: "bash",
|
|
790
951
|
args: ["-lc", command],
|
|
@@ -804,13 +965,14 @@ const requestSudoTool = tool({
|
|
|
804
965
|
openCodeSessionId: context.sessionID,
|
|
805
966
|
paneId,
|
|
806
967
|
title,
|
|
807
|
-
command: "
|
|
968
|
+
command: "zellij_pty_request_sudo",
|
|
808
969
|
args: [],
|
|
809
970
|
cwd,
|
|
810
971
|
allowAgentInput: false,
|
|
811
972
|
humanInputOnly: true,
|
|
812
973
|
exitCodeToken
|
|
813
974
|
});
|
|
975
|
+
registerPaneForWatchdog(session);
|
|
814
976
|
await subscriberManager.start(session);
|
|
815
977
|
return jsonResponse({
|
|
816
978
|
session: publicSession(session),
|
|
@@ -833,7 +995,7 @@ async function runProbe(probe, outputReader) {
|
|
|
833
995
|
};
|
|
834
996
|
if (effectiveProbe.type === "sleep") {
|
|
835
997
|
const seconds = effectiveProbe.seconds ?? defaultSleepSeconds;
|
|
836
|
-
await setTimeout(seconds * 1e3);
|
|
998
|
+
await setTimeout$1(seconds * 1e3);
|
|
837
999
|
return result(effectiveProbe.type, true, `Slept for ${seconds}s.`, startedAt);
|
|
838
1000
|
}
|
|
839
1001
|
if (effectiveProbe.type === "output") {
|
|
@@ -841,7 +1003,7 @@ async function runProbe(probe, outputReader) {
|
|
|
841
1003
|
const deadline = Date.now() + timeoutSeconds * 1e3;
|
|
842
1004
|
while (Date.now() <= deadline) {
|
|
843
1005
|
if (outputReader(effectiveProbe.grep, effectiveProbe.ignoreCase)) return result(effectiveProbe.type, true, `Observed output matching /${effectiveProbe.grep}/.`, startedAt);
|
|
844
|
-
await setTimeout(pollIntervalMs);
|
|
1006
|
+
await setTimeout$1(pollIntervalMs);
|
|
845
1007
|
}
|
|
846
1008
|
return result(effectiveProbe.type, false, `Timed out after ${timeoutSeconds}s waiting for output matching /${effectiveProbe.grep}/.`, startedAt);
|
|
847
1009
|
}
|
|
@@ -861,7 +1023,7 @@ async function runProbe(probe, outputReader) {
|
|
|
861
1023
|
} catch (error) {
|
|
862
1024
|
lastError = error instanceof Error ? error.message : String(error);
|
|
863
1025
|
}
|
|
864
|
-
await setTimeout(pollIntervalMs);
|
|
1026
|
+
await setTimeout$1(pollIntervalMs);
|
|
865
1027
|
}
|
|
866
1028
|
return result(effectiveProbe.type, false, `Timed out after ${timeoutSeconds}s probing ${effectiveProbe.url}: ${lastError}.`, startedAt);
|
|
867
1029
|
}
|
|
@@ -934,6 +1096,7 @@ const zellijPtySpawnTool = tool({
|
|
|
934
1096
|
humanInputOnly: false,
|
|
935
1097
|
exitCodeToken
|
|
936
1098
|
});
|
|
1099
|
+
registerPaneForWatchdog(session);
|
|
937
1100
|
await subscriberManager.start(session);
|
|
938
1101
|
const probe = await runProbe(args.probe, (grep, ignoreCase) => outputMatches(session.id, grep, ignoreCase));
|
|
939
1102
|
const output = readOutputSnapshot(session.id, { maxLines: args.maxLines });
|
|
@@ -982,7 +1145,7 @@ const schema = tool.schema;
|
|
|
982
1145
|
const zellijPtyWriteTool = tool({
|
|
983
1146
|
description: "Write stdin to a Zellij PTY session. Refuses human-input-only sessions.",
|
|
984
1147
|
args: {
|
|
985
|
-
id: schema.string().describe("zellij-pty session id returned by zellij_pty_spawn or
|
|
1148
|
+
id: schema.string().describe("zellij-pty session id returned by zellij_pty_spawn or zellij_pty_request_sudo."),
|
|
986
1149
|
data: schema.string().describe("Text to write. Use to send Ctrl-C."),
|
|
987
1150
|
maxLines: schema.number().int().positive().max(5e3).optional().describe("Maximum recent output lines to return. Defaults to 200."),
|
|
988
1151
|
interruptAfterSeconds: schema.number().positive().max(300).optional().describe("Blindly send Ctrl-C after this many seconds if the pane is still running; keeps the pane alive.")
|
|
@@ -1002,12 +1165,12 @@ const zellijPtyWriteTool = tool({
|
|
|
1002
1165
|
}
|
|
1003
1166
|
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1004
1167
|
if (args.interruptAfterSeconds) {
|
|
1005
|
-
await setTimeout(args.interruptAfterSeconds * 1e3);
|
|
1168
|
+
await setTimeout$1(args.interruptAfterSeconds * 1e3);
|
|
1006
1169
|
if (sessionManager.get(session.id).status === "running") {
|
|
1007
1170
|
await zellijCli.sendCtrlC(session.paneId);
|
|
1008
|
-
await setTimeout(500);
|
|
1171
|
+
await setTimeout$1(500);
|
|
1009
1172
|
}
|
|
1010
|
-
} else await setTimeout(1e3);
|
|
1173
|
+
} else await setTimeout$1(1e3);
|
|
1011
1174
|
return jsonResponse({
|
|
1012
1175
|
session: publicSession(session),
|
|
1013
1176
|
output: readOutputSnapshot(session.id, { maxLines: args.maxLines }),
|
|
@@ -1017,16 +1180,310 @@ const zellijPtyWriteTool = tool({
|
|
|
1017
1180
|
}
|
|
1018
1181
|
});
|
|
1019
1182
|
//#endregion
|
|
1183
|
+
//#region src/zellij/shutdown-cleanup.ts
|
|
1184
|
+
let registered = false;
|
|
1185
|
+
let cleanedUp = false;
|
|
1186
|
+
function cleanupPanesOnShutdown(sessions = sessionManager, subscribers = subscriberManager) {
|
|
1187
|
+
if (cleanedUp) return;
|
|
1188
|
+
cleanedUp = true;
|
|
1189
|
+
for (const session of sessions.list()) {
|
|
1190
|
+
try {
|
|
1191
|
+
zellijCli.closePaneSync(session.paneId);
|
|
1192
|
+
} catch {}
|
|
1193
|
+
subscribers.forget(session.id);
|
|
1194
|
+
try {
|
|
1195
|
+
sessions.remove(session.id);
|
|
1196
|
+
} catch {}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
function registerShutdownCleanup() {
|
|
1200
|
+
if (registered) return;
|
|
1201
|
+
registered = true;
|
|
1202
|
+
process.once("exit", () => cleanupPanesOnShutdown());
|
|
1203
|
+
process.once("SIGINT", () => exitAfterCleanup("SIGINT", 130));
|
|
1204
|
+
process.once("SIGTERM", () => exitAfterCleanup("SIGTERM", 143));
|
|
1205
|
+
process.once("SIGHUP", () => exitAfterCleanup("SIGHUP", 129));
|
|
1206
|
+
}
|
|
1207
|
+
function exitAfterCleanup(signal, code) {
|
|
1208
|
+
cleanupPanesOnShutdown();
|
|
1209
|
+
process.removeAllListeners(signal);
|
|
1210
|
+
process.exit(code);
|
|
1211
|
+
}
|
|
1212
|
+
//#endregion
|
|
1213
|
+
//#region src/zellij/tab-title-events.ts
|
|
1214
|
+
const execFileAsync = promisify(execFile);
|
|
1215
|
+
function isRecord(value) {
|
|
1216
|
+
return typeof value === "object" && value !== null;
|
|
1217
|
+
}
|
|
1218
|
+
function stringProperty(object, key) {
|
|
1219
|
+
const value = object[key];
|
|
1220
|
+
return typeof value === "string" ? value : void 0;
|
|
1221
|
+
}
|
|
1222
|
+
function nestedStringProperty(object, key, nestedKey) {
|
|
1223
|
+
const nested = object[key];
|
|
1224
|
+
if (!isRecord(nested)) return void 0;
|
|
1225
|
+
return stringProperty(nested, nestedKey);
|
|
1226
|
+
}
|
|
1227
|
+
function sessionStatusProperty(object) {
|
|
1228
|
+
const status = object.status;
|
|
1229
|
+
if (!isRecord(status)) return void 0;
|
|
1230
|
+
if (status.type === "idle" || status.type === "busy") return { type: status.type };
|
|
1231
|
+
if (status.type === "retry") return {
|
|
1232
|
+
type: "retry",
|
|
1233
|
+
attempt: typeof status.attempt === "number" ? status.attempt : 0,
|
|
1234
|
+
message: typeof status.message === "string" ? status.message : "",
|
|
1235
|
+
next: typeof status.next === "number" ? status.next : 0
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
function inputRequestID(object) {
|
|
1239
|
+
return stringProperty(object, "id") ?? stringProperty(object, "requestID") ?? stringProperty(object, "permissionID");
|
|
1240
|
+
}
|
|
1241
|
+
function deletedSessionID(event) {
|
|
1242
|
+
if (!isRecord(event.properties)) return void 0;
|
|
1243
|
+
return nestedStringProperty(event.properties, "info", "id") ?? stringProperty(event.properties, "sessionID");
|
|
1244
|
+
}
|
|
1245
|
+
async function readGitBranch(worktree) {
|
|
1246
|
+
return (await execFileAsync("git", [
|
|
1247
|
+
"-C",
|
|
1248
|
+
worktree,
|
|
1249
|
+
"branch",
|
|
1250
|
+
"--show-current"
|
|
1251
|
+
], {
|
|
1252
|
+
encoding: "utf8",
|
|
1253
|
+
timeout: 1e3,
|
|
1254
|
+
maxBuffer: 1024 * 1024
|
|
1255
|
+
})).stdout;
|
|
1256
|
+
}
|
|
1257
|
+
async function getInitialBranch(worktree, readBranch = readGitBranch) {
|
|
1258
|
+
try {
|
|
1259
|
+
return (await readBranch(worktree)).trim() || void 0;
|
|
1260
|
+
} catch {
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
function shouldReadInitialBranch(zellij) {
|
|
1265
|
+
return Boolean(zellij);
|
|
1266
|
+
}
|
|
1267
|
+
function handleTabTitleEvent(tabTitleManager, event) {
|
|
1268
|
+
if (!isRecord(event.properties)) return;
|
|
1269
|
+
const properties = event.properties;
|
|
1270
|
+
switch (event.type) {
|
|
1271
|
+
case "session.status": {
|
|
1272
|
+
const sessionID = stringProperty(properties, "sessionID");
|
|
1273
|
+
const status = sessionStatusProperty(properties);
|
|
1274
|
+
if (sessionID && status) tabTitleManager.updateSessionStatus(sessionID, status);
|
|
1275
|
+
break;
|
|
1276
|
+
}
|
|
1277
|
+
case "session.idle": {
|
|
1278
|
+
const sessionID = stringProperty(properties, "sessionID");
|
|
1279
|
+
if (sessionID) tabTitleManager.markSessionIdle(sessionID);
|
|
1280
|
+
break;
|
|
1281
|
+
}
|
|
1282
|
+
case "vcs.branch.updated":
|
|
1283
|
+
tabTitleManager.setBranch(stringProperty(properties, "branch"));
|
|
1284
|
+
break;
|
|
1285
|
+
case "question.asked":
|
|
1286
|
+
case "permission.asked":
|
|
1287
|
+
case "permission.updated": {
|
|
1288
|
+
const id = inputRequestID(properties);
|
|
1289
|
+
const sessionID = stringProperty(properties, "sessionID");
|
|
1290
|
+
if (id && sessionID) tabTitleManager.markNeedsInput(id, sessionID);
|
|
1291
|
+
break;
|
|
1292
|
+
}
|
|
1293
|
+
case "question.replied":
|
|
1294
|
+
case "question.rejected":
|
|
1295
|
+
case "permission.replied": {
|
|
1296
|
+
const id = inputRequestID(properties);
|
|
1297
|
+
if (id) tabTitleManager.clearNeedsInput(id);
|
|
1298
|
+
break;
|
|
1299
|
+
}
|
|
1300
|
+
case "session.deleted": {
|
|
1301
|
+
const sessionID = deletedSessionID(event);
|
|
1302
|
+
if (sessionID) tabTitleManager.removeSession(sessionID);
|
|
1303
|
+
break;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
//#endregion
|
|
1308
|
+
//#region src/utils/debug.ts
|
|
1309
|
+
function debug(message, ...details) {
|
|
1310
|
+
if (!process.env.ZELLIJ_PTY_DEBUG) return;
|
|
1311
|
+
console.warn(`[opencode-zellij] ${message}`, ...details);
|
|
1312
|
+
}
|
|
1313
|
+
//#endregion
|
|
1314
|
+
//#region src/zellij/tab-title.ts
|
|
1315
|
+
const defaultTabTitleEmojis = {
|
|
1316
|
+
idle: "🟢",
|
|
1317
|
+
running: "⚡",
|
|
1318
|
+
needsInput: "💬",
|
|
1319
|
+
branch: "🌱"
|
|
1320
|
+
};
|
|
1321
|
+
function formatTabTitle(context) {
|
|
1322
|
+
const branch = context.branchName ? ` ${context.emojis.branch} ${context.branchName}` : "";
|
|
1323
|
+
return `${context.emojis[context.status === "needs-input" ? "needsInput" : context.status]} ${context.projectName}${branch}`;
|
|
1324
|
+
}
|
|
1325
|
+
function sanitizeTitle(title, maxLength = 90) {
|
|
1326
|
+
let cleaned = title.replace(/[\p{Cc}\p{Cf}\p{Co}\p{Cn}]/gu, " ").replace(/\s+/g, " ").trim();
|
|
1327
|
+
const chars = Array.from(cleaned);
|
|
1328
|
+
if (chars.length > maxLength) cleaned = `${chars.slice(0, maxLength - 1).join("")}…`;
|
|
1329
|
+
return cleaned;
|
|
1330
|
+
}
|
|
1331
|
+
var TabTitleManager = class {
|
|
1332
|
+
sessionStatuses = /* @__PURE__ */ new Map();
|
|
1333
|
+
pendingInputs = /* @__PURE__ */ new Map();
|
|
1334
|
+
branchName;
|
|
1335
|
+
desiredTitle;
|
|
1336
|
+
lastSyncedTitle;
|
|
1337
|
+
debounceTimer;
|
|
1338
|
+
syncInFlight = false;
|
|
1339
|
+
debounceMs;
|
|
1340
|
+
projectName;
|
|
1341
|
+
cli;
|
|
1342
|
+
emojis;
|
|
1343
|
+
enabled;
|
|
1344
|
+
constructor(options) {
|
|
1345
|
+
this.projectName = options.projectName;
|
|
1346
|
+
this.branchName = options.branchName?.trim() || void 0;
|
|
1347
|
+
this.cli = options.cli ?? new ZellijCli();
|
|
1348
|
+
this.emojis = {
|
|
1349
|
+
...defaultTabTitleEmojis,
|
|
1350
|
+
...options.emojis
|
|
1351
|
+
};
|
|
1352
|
+
this.debounceMs = options.debounceMs ?? 300;
|
|
1353
|
+
this.enabled = Boolean(process.env.ZELLIJ);
|
|
1354
|
+
}
|
|
1355
|
+
setBranch(branch) {
|
|
1356
|
+
const trimmed = branch?.trim() || void 0;
|
|
1357
|
+
if (this.branchName === trimmed) return;
|
|
1358
|
+
this.branchName = trimmed;
|
|
1359
|
+
this.scheduleUpdate();
|
|
1360
|
+
}
|
|
1361
|
+
updateSessionStatus(sessionID, status) {
|
|
1362
|
+
const activity = status.type === "idle" ? "idle" : "running";
|
|
1363
|
+
if (this.sessionStatuses.get(sessionID) === activity) return;
|
|
1364
|
+
this.sessionStatuses.set(sessionID, activity);
|
|
1365
|
+
this.scheduleUpdate();
|
|
1366
|
+
}
|
|
1367
|
+
markSessionIdle(sessionID) {
|
|
1368
|
+
this.updateSessionStatus(sessionID, { type: "idle" });
|
|
1369
|
+
}
|
|
1370
|
+
removeSession(sessionID) {
|
|
1371
|
+
const hadSessionStatus = this.sessionStatuses.delete(sessionID);
|
|
1372
|
+
let hadPendingInput = false;
|
|
1373
|
+
for (const [id, pendingSessionID] of this.pendingInputs) if (pendingSessionID === sessionID) {
|
|
1374
|
+
this.pendingInputs.delete(id);
|
|
1375
|
+
hadPendingInput = true;
|
|
1376
|
+
}
|
|
1377
|
+
if (!hadSessionStatus && !hadPendingInput) return;
|
|
1378
|
+
this.scheduleUpdate();
|
|
1379
|
+
}
|
|
1380
|
+
markNeedsInput(id, sessionID) {
|
|
1381
|
+
if (this.pendingInputs.get(id) === sessionID) return;
|
|
1382
|
+
this.pendingInputs.set(id, sessionID);
|
|
1383
|
+
this.scheduleUpdate();
|
|
1384
|
+
}
|
|
1385
|
+
clearNeedsInput(id) {
|
|
1386
|
+
if (!this.pendingInputs.delete(id)) return;
|
|
1387
|
+
this.scheduleUpdate();
|
|
1388
|
+
}
|
|
1389
|
+
get isBusy() {
|
|
1390
|
+
for (const activity of this.sessionStatuses.values()) if (activity === "running") return true;
|
|
1391
|
+
return false;
|
|
1392
|
+
}
|
|
1393
|
+
get needsInput() {
|
|
1394
|
+
return this.pendingInputs.size > 0;
|
|
1395
|
+
}
|
|
1396
|
+
get status() {
|
|
1397
|
+
if (this.needsInput) return "needs-input";
|
|
1398
|
+
if (this.isBusy) return "running";
|
|
1399
|
+
return "idle";
|
|
1400
|
+
}
|
|
1401
|
+
buildTitle() {
|
|
1402
|
+
return sanitizeTitle(formatTabTitle({
|
|
1403
|
+
projectName: this.projectName,
|
|
1404
|
+
branchName: this.branchName,
|
|
1405
|
+
status: this.status,
|
|
1406
|
+
emojis: this.emojis
|
|
1407
|
+
}));
|
|
1408
|
+
}
|
|
1409
|
+
getCurrentTitle() {
|
|
1410
|
+
return this.buildTitle();
|
|
1411
|
+
}
|
|
1412
|
+
async renderImmediate() {
|
|
1413
|
+
if (!this.enabled) return;
|
|
1414
|
+
this.desiredTitle = this.buildTitle();
|
|
1415
|
+
this.clearDebounceTimer();
|
|
1416
|
+
await this.syncDesiredTitle();
|
|
1417
|
+
}
|
|
1418
|
+
scheduleUpdate() {
|
|
1419
|
+
if (!this.enabled) return;
|
|
1420
|
+
const title = this.buildTitle();
|
|
1421
|
+
if (title === this.desiredTitle && title === this.lastSyncedTitle) return;
|
|
1422
|
+
this.desiredTitle = title;
|
|
1423
|
+
if (this.syncInFlight) return;
|
|
1424
|
+
this.clearDebounceTimer();
|
|
1425
|
+
this.debounceTimer = setTimeout(() => {
|
|
1426
|
+
this.debounceTimer = void 0;
|
|
1427
|
+
this.syncDesiredTitle().catch(() => {});
|
|
1428
|
+
}, this.debounceMs);
|
|
1429
|
+
}
|
|
1430
|
+
async syncDesiredTitle() {
|
|
1431
|
+
if (!this.enabled) return;
|
|
1432
|
+
if (this.syncInFlight) return;
|
|
1433
|
+
this.syncInFlight = true;
|
|
1434
|
+
try {
|
|
1435
|
+
while (this.desiredTitle && this.desiredTitle !== this.lastSyncedTitle) {
|
|
1436
|
+
const title = this.desiredTitle;
|
|
1437
|
+
try {
|
|
1438
|
+
await this.cli.renameTab(title);
|
|
1439
|
+
this.lastSyncedTitle = title;
|
|
1440
|
+
} catch (cause) {
|
|
1441
|
+
debug("Failed to rename Zellij tab.", cause);
|
|
1442
|
+
break;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
} finally {
|
|
1446
|
+
this.syncInFlight = false;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
clearDebounceTimer() {
|
|
1450
|
+
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
1451
|
+
this.debounceTimer = void 0;
|
|
1452
|
+
}
|
|
1453
|
+
destroy() {
|
|
1454
|
+
this.clearDebounceTimer();
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1457
|
+
//#endregion
|
|
1020
1458
|
//#region src/plugin.ts
|
|
1021
|
-
|
|
1459
|
+
function getProjectName(path) {
|
|
1460
|
+
return path.split(/[/\\]/).filter(Boolean).pop() || "opencode";
|
|
1461
|
+
}
|
|
1462
|
+
function getWorkspaceRoot(input) {
|
|
1463
|
+
return input.worktree || input.directory || process.cwd();
|
|
1464
|
+
}
|
|
1465
|
+
const ZellijPtyPlugin = async (input, options) => {
|
|
1022
1466
|
configurePolicy(options?.zellijPty ?? options);
|
|
1467
|
+
cleanupStaleWatchdogRegistries();
|
|
1468
|
+
registerShutdownCleanup();
|
|
1469
|
+
const workspaceRoot = getWorkspaceRoot(input);
|
|
1470
|
+
const tabTitleManager = new TabTitleManager({
|
|
1471
|
+
projectName: getProjectName(workspaceRoot),
|
|
1472
|
+
branchName: shouldReadInitialBranch(process.env.ZELLIJ) ? await getInitialBranch(workspaceRoot) : void 0
|
|
1473
|
+
});
|
|
1474
|
+
tabTitleManager.renderImmediate().catch(() => {});
|
|
1023
1475
|
return {
|
|
1024
1476
|
async event(input) {
|
|
1025
|
-
|
|
1026
|
-
|
|
1477
|
+
const event = input.event;
|
|
1478
|
+
handleTabTitleEvent(tabTitleManager, event);
|
|
1479
|
+
if (event.type === "session.deleted") {
|
|
1480
|
+
const sessionID = deletedSessionID(event);
|
|
1481
|
+
if (!sessionID) return;
|
|
1482
|
+
const sessions = sessionManager.listByOpenCodeSession(sessionID);
|
|
1027
1483
|
await Promise.all(sessions.map(async (session) => {
|
|
1028
1484
|
await subscriberManager.closeSessionPane(session.id);
|
|
1029
1485
|
subscriberManager.forget(session.id);
|
|
1486
|
+
unregisterPaneFromWatchdog(session.id);
|
|
1030
1487
|
sessionManager.remove(session.id);
|
|
1031
1488
|
}));
|
|
1032
1489
|
}
|
|
@@ -1037,7 +1494,7 @@ const ZellijPtyPlugin = async (_input, options) => {
|
|
|
1037
1494
|
zellij_pty_write: zellijPtyWriteTool,
|
|
1038
1495
|
zellij_pty_read: zellijPtyReadTool,
|
|
1039
1496
|
zellij_pty_kill: zellijPtyKillTool,
|
|
1040
|
-
|
|
1497
|
+
zellij_pty_request_sudo: requestSudoTool
|
|
1041
1498
|
}
|
|
1042
1499
|
};
|
|
1043
1500
|
};
|