opencode-zellij 0.0.10 → 0.0.12
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.d.mts +106 -0
- package/dist/index.mjs +847 -136
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -250,6 +250,13 @@ const sudoPaneSchema = z.enum([
|
|
|
250
250
|
"deny",
|
|
251
251
|
"hide"
|
|
252
252
|
]);
|
|
253
|
+
const completionNotificationModeSchema = z.enum([
|
|
254
|
+
"off",
|
|
255
|
+
"queue",
|
|
256
|
+
"toast",
|
|
257
|
+
"queue+toast",
|
|
258
|
+
"prompt"
|
|
259
|
+
]);
|
|
253
260
|
const configFilenames = ["opencode-zellij.config.jsonc", "opencode-zellij.config.json"];
|
|
254
261
|
const tabTitleLayerSchema = z.object({
|
|
255
262
|
enabled: z.boolean().optional().describe("Enable dynamic Zellij tab title updates."),
|
|
@@ -261,7 +268,16 @@ const tabTitleLayerSchema = z.object({
|
|
|
261
268
|
}).strict();
|
|
262
269
|
const ptyLayerSchema = z.object({
|
|
263
270
|
enabled: z.boolean().optional().describe("Enable Zellij-backed PTY tools."),
|
|
264
|
-
|
|
271
|
+
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.")
|
|
265
281
|
}).strict();
|
|
266
282
|
const autoUpdateLayerSchema = z.boolean().optional().describe("Enable automatic update checks for the opencode-zellij plugin.");
|
|
267
283
|
const sidecarConfigSchema = z.object({
|
|
@@ -281,7 +297,16 @@ const defaultConfig = {
|
|
|
281
297
|
},
|
|
282
298
|
pty: {
|
|
283
299
|
enabled: true,
|
|
284
|
-
|
|
300
|
+
cleanupExitedPaneOnRead: true,
|
|
301
|
+
sudoPane: "allow",
|
|
302
|
+
completionNotification: {
|
|
303
|
+
mode: "queue+toast",
|
|
304
|
+
prompt: {
|
|
305
|
+
requireIdle: true,
|
|
306
|
+
cooldownMs: 3e4,
|
|
307
|
+
maxAttempts: 1
|
|
308
|
+
}
|
|
309
|
+
}
|
|
285
310
|
},
|
|
286
311
|
autoUpdate: true
|
|
287
312
|
};
|
|
@@ -306,7 +331,16 @@ function mergeConfig(user, project) {
|
|
|
306
331
|
},
|
|
307
332
|
pty: {
|
|
308
333
|
enabled: project?.pty?.enabled ?? user?.pty?.enabled ?? defaultConfig.pty.enabled,
|
|
309
|
-
|
|
334
|
+
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
|
+
}
|
|
310
344
|
},
|
|
311
345
|
autoUpdate: project?.autoUpdate ?? user?.autoUpdate ?? defaultConfig.autoUpdate
|
|
312
346
|
};
|
|
@@ -390,6 +424,7 @@ function parsePaneId(output) {
|
|
|
390
424
|
}
|
|
391
425
|
//#endregion
|
|
392
426
|
//#region src/pty/manager.ts
|
|
427
|
+
const tombstoneTailLimit = 200;
|
|
393
428
|
var SessionManager = class {
|
|
394
429
|
sessions = /* @__PURE__ */ new Map();
|
|
395
430
|
create(input) {
|
|
@@ -410,7 +445,8 @@ var SessionManager = class {
|
|
|
410
445
|
humanInputOnly: input.humanInputOnly,
|
|
411
446
|
exitCode: null,
|
|
412
447
|
exitedAt: null,
|
|
413
|
-
exitCodeToken: input.exitCodeToken ?? null
|
|
448
|
+
exitCodeToken: input.exitCodeToken ?? null,
|
|
449
|
+
tombstone: null
|
|
414
450
|
};
|
|
415
451
|
this.sessions.set(session.id, session);
|
|
416
452
|
return session;
|
|
@@ -434,16 +470,65 @@ var SessionManager = class {
|
|
|
434
470
|
}
|
|
435
471
|
updateStatus(id, status) {
|
|
436
472
|
const session = this.get(id);
|
|
473
|
+
if (session.status === "terminal" && status !== "terminal") return session;
|
|
437
474
|
session.status = status;
|
|
438
475
|
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
439
476
|
return session;
|
|
440
477
|
}
|
|
441
478
|
markExited(id, exitCode) {
|
|
479
|
+
return this.markTerminal(id, {
|
|
480
|
+
reason: "exit_marker",
|
|
481
|
+
exitCode
|
|
482
|
+
}).session;
|
|
483
|
+
}
|
|
484
|
+
markTerminal(id, input) {
|
|
485
|
+
const session = this.get(id);
|
|
486
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
487
|
+
const created = session.status !== "terminal" || !session.tombstone;
|
|
488
|
+
if (created) {
|
|
489
|
+
const tombstone = {
|
|
490
|
+
reason: input.reason,
|
|
491
|
+
terminalAt: now,
|
|
492
|
+
tail: (input.tail ?? []).slice(-tombstoneTailLimit),
|
|
493
|
+
paneClosedAt: null,
|
|
494
|
+
notificationSentAt: null
|
|
495
|
+
};
|
|
496
|
+
session.status = "terminal";
|
|
497
|
+
session.tombstone = tombstone;
|
|
498
|
+
session.updatedAt = now;
|
|
499
|
+
}
|
|
500
|
+
if (input.exitCode !== void 0 && session.exitCode === null) {
|
|
501
|
+
session.exitCode = input.exitCode;
|
|
502
|
+
session.exitedAt = now;
|
|
503
|
+
session.updatedAt = now;
|
|
504
|
+
}
|
|
505
|
+
if (session.tombstone) {
|
|
506
|
+
if (input.tail?.length && session.tombstone.tail.length === 0) session.tombstone.tail = input.tail.slice(-tombstoneTailLimit);
|
|
507
|
+
if (!session.tombstone.reason) session.tombstone.reason = input.reason;
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
session,
|
|
511
|
+
created
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
markTerminalPaneClosed(id) {
|
|
515
|
+
const session = this.get(id);
|
|
516
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
517
|
+
if (!session.tombstone) return session;
|
|
518
|
+
if (!session.tombstone.paneClosedAt) {
|
|
519
|
+
session.tombstone.paneClosedAt = now;
|
|
520
|
+
session.updatedAt = now;
|
|
521
|
+
}
|
|
522
|
+
return session;
|
|
523
|
+
}
|
|
524
|
+
markTerminalNotificationSent(id) {
|
|
442
525
|
const session = this.get(id);
|
|
443
|
-
|
|
444
|
-
session.
|
|
445
|
-
|
|
446
|
-
|
|
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
|
+
}
|
|
447
532
|
return session;
|
|
448
533
|
}
|
|
449
534
|
listByOpenCodeSession(openCodeSessionId) {
|
|
@@ -512,19 +597,19 @@ function paneMatches(object, paneId) {
|
|
|
512
597
|
"paneId"
|
|
513
598
|
]) === paneId && object.is_plugin !== true;
|
|
514
599
|
}
|
|
515
|
-
function
|
|
600
|
+
function findPaneRecord(value, paneId) {
|
|
516
601
|
if (Array.isArray(value)) {
|
|
517
602
|
for (const item of value) {
|
|
518
|
-
const found =
|
|
603
|
+
const found = findPaneRecord(item, paneId);
|
|
519
604
|
if (found !== void 0) return found;
|
|
520
605
|
}
|
|
521
606
|
return;
|
|
522
607
|
}
|
|
523
608
|
if (typeof value !== "object" || value === null) return void 0;
|
|
524
609
|
const object = value;
|
|
525
|
-
if (paneMatches(object, paneId)) return
|
|
610
|
+
if (paneMatches(object, paneId)) return object;
|
|
526
611
|
for (const nested of Object.values(object)) {
|
|
527
|
-
const found =
|
|
612
|
+
const found = findPaneRecord(nested, paneId);
|
|
528
613
|
if (found !== void 0) return found;
|
|
529
614
|
}
|
|
530
615
|
}
|
|
@@ -533,12 +618,28 @@ function parseCurrentPaneTabId(listPanesJson, paneId) {
|
|
|
533
618
|
const parsedPaneId = Number(paneId);
|
|
534
619
|
if (!Number.isInteger(parsedPaneId)) return void 0;
|
|
535
620
|
try {
|
|
536
|
-
|
|
621
|
+
const pane = findPaneRecord(JSON.parse(listPanesJson), parsedPaneId);
|
|
622
|
+
return pane ? numericProperty(pane, ["tab_id", "tabId"]) : void 0;
|
|
537
623
|
} catch (error) {
|
|
538
624
|
debug("parseCurrentPaneTabId failed", errorMessage(error));
|
|
539
625
|
return;
|
|
540
626
|
}
|
|
541
627
|
}
|
|
628
|
+
function parsePaneExists(listPanesJson, paneId) {
|
|
629
|
+
if (!paneId) return void 0;
|
|
630
|
+
let parsedPaneId;
|
|
631
|
+
try {
|
|
632
|
+
parsedPaneId = Number(normalizePaneId(paneId).slice(9));
|
|
633
|
+
} catch {
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
try {
|
|
637
|
+
return findPaneRecord(JSON.parse(listPanesJson), parsedPaneId) !== void 0;
|
|
638
|
+
} catch (error) {
|
|
639
|
+
debug("parsePaneExists failed", errorMessage(error));
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
542
643
|
function tabNameProperty(object, tabId) {
|
|
543
644
|
if (tabId === void 0) return void 0;
|
|
544
645
|
if (numericProperty(object, ["tab_id", "tabId"]) !== tabId) return void 0;
|
|
@@ -680,6 +781,9 @@ var ZellijCli = class {
|
|
|
680
781
|
if (!paneId) return void 0;
|
|
681
782
|
return parseCurrentPaneTabId((await runZellij(zellijActionArgs("list-panes", ["--json"]), { timeoutMs: 5e3 })).stdout, paneId);
|
|
682
783
|
}
|
|
784
|
+
async paneExists(paneId) {
|
|
785
|
+
return parsePaneExists((await runZellij(zellijActionArgs("list-panes", ["--json"]), { timeoutMs: 5e3 })).stdout, paneId);
|
|
786
|
+
}
|
|
683
787
|
async renameTab(title) {
|
|
684
788
|
const tabId = await this.currentPaneTabId();
|
|
685
789
|
if (tabId === void 0 && process.env.ZELLIJ) throw new Error(`Could not resolve Zellij tab id for pane ${process.env.ZELLIJ_PANE_ID ?? "<missing>"}`);
|
|
@@ -993,11 +1097,25 @@ function extractRenderedLines(event) {
|
|
|
993
1097
|
var SubscriberManager = class {
|
|
994
1098
|
subscribers = /* @__PURE__ */ new Map();
|
|
995
1099
|
startingSessions = /* @__PURE__ */ new Map();
|
|
996
|
-
|
|
1100
|
+
spawnProcess;
|
|
1101
|
+
dumpScreen;
|
|
1102
|
+
closePane;
|
|
1103
|
+
lifecycleHooks;
|
|
1104
|
+
terminalTailLines;
|
|
1105
|
+
constructor(sessions, maxBufferLines = Number(process.env.PTY_MAX_BUFFER_LINES ?? 5e4), dependencies = {}) {
|
|
997
1106
|
this.sessions = sessions;
|
|
998
1107
|
this.maxBufferLines = maxBufferLines;
|
|
1108
|
+
this.spawnProcess = dependencies.spawn ?? spawn;
|
|
1109
|
+
this.dumpScreen = dependencies.dumpScreen ?? zellijCli.dumpScreen;
|
|
1110
|
+
this.closePane = dependencies.closePane ?? zellijCli.closePane;
|
|
1111
|
+
this.lifecycleHooks = dependencies.lifecycleHooks;
|
|
1112
|
+
this.terminalTailLines = dependencies.terminalTailLines ?? 200;
|
|
1113
|
+
}
|
|
1114
|
+
setLifecycleHooks(hooks) {
|
|
1115
|
+
this.lifecycleHooks = hooks;
|
|
999
1116
|
}
|
|
1000
1117
|
async start(session) {
|
|
1118
|
+
if ((this.sessions.find(session.id) ?? session).status === "terminal") return;
|
|
1001
1119
|
if (this.subscribers.get(session.id)?.child) return;
|
|
1002
1120
|
const inProgress = this.startingSessions.get(session.id);
|
|
1003
1121
|
if (inProgress) return inProgress;
|
|
@@ -1021,7 +1139,7 @@ var SubscriberManager = class {
|
|
|
1021
1139
|
lastExitedAt: null
|
|
1022
1140
|
};
|
|
1023
1141
|
if (!existing) this.subscribers.set(session.id, state);
|
|
1024
|
-
const child =
|
|
1142
|
+
const child = this.spawnProcess("zellij", zellijCommandArgs([
|
|
1025
1143
|
"subscribe",
|
|
1026
1144
|
"--pane-id",
|
|
1027
1145
|
session.paneId,
|
|
@@ -1048,7 +1166,7 @@ var SubscriberManager = class {
|
|
|
1048
1166
|
child.on("exit", () => this.handleSubscriberExit(session.id, child));
|
|
1049
1167
|
child.on("error", (error) => this.handleSubscriberError(session.id, child, error));
|
|
1050
1168
|
if (!existing) try {
|
|
1051
|
-
const snapshot = await
|
|
1169
|
+
const snapshot = await this.dumpScreen(session.paneId);
|
|
1052
1170
|
if (this.subscribers.get(session.id) !== state || state.child !== child) return;
|
|
1053
1171
|
state.buffer.appendSnapshot(snapshot);
|
|
1054
1172
|
this.sessions.updateLineCount(session.id, state.buffer.lineCount);
|
|
@@ -1069,7 +1187,8 @@ var SubscriberManager = class {
|
|
|
1069
1187
|
return {
|
|
1070
1188
|
hasBuffer: Boolean(state),
|
|
1071
1189
|
active: Boolean(state?.child),
|
|
1072
|
-
lastExitedAt: state?.lastExitedAt ?? null
|
|
1190
|
+
lastExitedAt: state?.lastExitedAt ?? null,
|
|
1191
|
+
terminal: this.sessions.find(sessionId)?.status === "terminal"
|
|
1073
1192
|
};
|
|
1074
1193
|
}
|
|
1075
1194
|
stderr(sessionId) {
|
|
@@ -1089,13 +1208,14 @@ var SubscriberManager = class {
|
|
|
1089
1208
|
stopAll() {
|
|
1090
1209
|
for (const sessionId of this.subscribers.keys()) this.forget(sessionId);
|
|
1091
1210
|
}
|
|
1092
|
-
async closeSessionPane(sessionId) {
|
|
1211
|
+
async closeSessionPane(sessionId, options = {}) {
|
|
1093
1212
|
const session = this.sessions.get(sessionId);
|
|
1094
1213
|
this.stop(sessionId);
|
|
1095
1214
|
try {
|
|
1096
|
-
await
|
|
1215
|
+
await this.closePane(session.paneId);
|
|
1097
1216
|
} catch (error) {
|
|
1098
1217
|
debug("closePane failed", errorMessage(error));
|
|
1218
|
+
if (options.throwOnFailure) throw error;
|
|
1099
1219
|
}
|
|
1100
1220
|
}
|
|
1101
1221
|
handleStdout(sessionId, child, chunk) {
|
|
@@ -1133,11 +1253,8 @@ var SubscriberManager = class {
|
|
|
1133
1253
|
if (paneId && paneId !== session.paneId) return;
|
|
1134
1254
|
const type = eventType(event);
|
|
1135
1255
|
if (type === "pane_closed" || type === "PaneClosed") {
|
|
1136
|
-
|
|
1137
|
-
this.sessions.updateLineCount(sessionId, state.buffer.lineCount);
|
|
1138
|
-
this.sessions.updateStatus(sessionId, session.status === "killed" ? "killed" : "exited");
|
|
1256
|
+
this.markSessionTerminal(sessionId, "pane_closed");
|
|
1139
1257
|
unregisterPaneFromWatchdog(sessionId);
|
|
1140
|
-
this.stop(sessionId);
|
|
1141
1258
|
return;
|
|
1142
1259
|
}
|
|
1143
1260
|
const lines = extractRenderedLines(event);
|
|
@@ -1152,7 +1269,7 @@ var SubscriberManager = class {
|
|
|
1152
1269
|
for (const line of lines) {
|
|
1153
1270
|
const marker = parseExitCodeMarker(line);
|
|
1154
1271
|
if (!marker || marker.token !== session.exitCodeToken) continue;
|
|
1155
|
-
this.
|
|
1272
|
+
this.markSessionTerminal(sessionId, "exit_marker", { exitCode: marker.exitCode });
|
|
1156
1273
|
return;
|
|
1157
1274
|
}
|
|
1158
1275
|
}
|
|
@@ -1168,7 +1285,7 @@ var SubscriberManager = class {
|
|
|
1168
1285
|
if (state.child !== child) return;
|
|
1169
1286
|
state.child = null;
|
|
1170
1287
|
state.lastExitedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1171
|
-
state.stderr.push(`[zellij-pty] subscriber exited at ${state.lastExitedAt}; last buffered output is retained.`);
|
|
1288
|
+
if (this.sessions.find(sessionId)?.status !== "terminal") state.stderr.push(`[zellij-pty] subscriber exited at ${state.lastExitedAt}; last buffered output is retained.`);
|
|
1172
1289
|
if (state.stderr.length > maxStderrLines) state.stderr = state.stderr.slice(state.stderr.length - maxStderrLines);
|
|
1173
1290
|
}
|
|
1174
1291
|
handleSubscriberError(sessionId, child, error) {
|
|
@@ -1180,6 +1297,29 @@ var SubscriberManager = class {
|
|
|
1180
1297
|
this.sessions.updateStatus(sessionId, "unknown");
|
|
1181
1298
|
}
|
|
1182
1299
|
}
|
|
1300
|
+
markSessionTerminal(sessionId, reason, input = {}) {
|
|
1301
|
+
const state = this.subscribers.get(sessionId);
|
|
1302
|
+
if (!state) return;
|
|
1303
|
+
const tail = state.buffer.read({ limit: this.terminalTailLines }).lines;
|
|
1304
|
+
const result = this.sessions.markTerminal(sessionId, {
|
|
1305
|
+
reason,
|
|
1306
|
+
tail,
|
|
1307
|
+
exitCode: input.exitCode
|
|
1308
|
+
});
|
|
1309
|
+
if (result.created) try {
|
|
1310
|
+
const maybePromise = this.lifecycleHooks?.onSessionTerminal?.({
|
|
1311
|
+
sessionId,
|
|
1312
|
+
reason,
|
|
1313
|
+
session: result.session
|
|
1314
|
+
});
|
|
1315
|
+
if (maybePromise && typeof maybePromise.then === "function") maybePromise.catch((error) => {
|
|
1316
|
+
debug("onSessionTerminal hook failed", errorMessage(error));
|
|
1317
|
+
});
|
|
1318
|
+
} catch (error) {
|
|
1319
|
+
debug("onSessionTerminal hook failed", errorMessage(error));
|
|
1320
|
+
}
|
|
1321
|
+
this.stop(sessionId);
|
|
1322
|
+
}
|
|
1183
1323
|
};
|
|
1184
1324
|
const subscriberManager = new SubscriberManager(sessionManager);
|
|
1185
1325
|
//#endregion
|
|
@@ -1192,14 +1332,21 @@ function publicSession(session) {
|
|
|
1192
1332
|
command: session.command,
|
|
1193
1333
|
args: session.args,
|
|
1194
1334
|
cwd: session.cwd,
|
|
1195
|
-
status: session.status,
|
|
1335
|
+
status: session.status === "terminal" ? "exited" : session.status,
|
|
1196
1336
|
lineCount: session.lineCount,
|
|
1197
1337
|
createdAt: session.createdAt,
|
|
1198
1338
|
updatedAt: session.updatedAt,
|
|
1199
1339
|
agentWritable: session.allowAgentInput,
|
|
1200
1340
|
humanInputOnly: session.humanInputOnly,
|
|
1201
1341
|
exitCode: session.exitCode,
|
|
1202
|
-
exitedAt: session.exitedAt
|
|
1342
|
+
exitedAt: session.exitedAt,
|
|
1343
|
+
tombstone: session.tombstone ? {
|
|
1344
|
+
reason: session.tombstone.reason,
|
|
1345
|
+
terminalAt: session.tombstone.terminalAt,
|
|
1346
|
+
tailLines: session.tombstone.tail.length,
|
|
1347
|
+
paneClosedAt: session.tombstone.paneClosedAt,
|
|
1348
|
+
notificationSentAt: session.tombstone.notificationSentAt
|
|
1349
|
+
} : null
|
|
1203
1350
|
};
|
|
1204
1351
|
}
|
|
1205
1352
|
function nextAdvice(retryable, reason) {
|
|
@@ -1256,53 +1403,90 @@ function outputMatches(sessionId, grep, ignoreCase) {
|
|
|
1256
1403
|
}).returned > 0;
|
|
1257
1404
|
}
|
|
1258
1405
|
//#endregion
|
|
1406
|
+
//#region src/tools/pane-cleanup.ts
|
|
1407
|
+
async function closePaneOrVerifyGone(input) {
|
|
1408
|
+
try {
|
|
1409
|
+
await input.closePane();
|
|
1410
|
+
return {
|
|
1411
|
+
cleanupReady: true,
|
|
1412
|
+
alreadyGone: false
|
|
1413
|
+
};
|
|
1414
|
+
} catch (error) {
|
|
1415
|
+
const closeErrorMessage = error instanceof Error ? error.message : String(error);
|
|
1416
|
+
if (await isPaneGone(input.paneId, input.paneExists)) return {
|
|
1417
|
+
cleanupReady: true,
|
|
1418
|
+
alreadyGone: true,
|
|
1419
|
+
closeErrorMessage
|
|
1420
|
+
};
|
|
1421
|
+
return {
|
|
1422
|
+
cleanupReady: false,
|
|
1423
|
+
alreadyGone: false,
|
|
1424
|
+
closeErrorMessage
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
async function isPaneGone(paneId, paneExists) {
|
|
1429
|
+
if (!paneExists) return false;
|
|
1430
|
+
try {
|
|
1431
|
+
return await paneExists(paneId) === false;
|
|
1432
|
+
} catch {
|
|
1433
|
+
return false;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
//#endregion
|
|
1259
1437
|
//#region src/tools/kill.ts
|
|
1260
1438
|
const schema$4 = tool.schema;
|
|
1261
|
-
function
|
|
1262
|
-
|
|
1439
|
+
async function executeZellijPtyKill(args, dependencies = {}) {
|
|
1440
|
+
const zellijCliApi = dependencies.zellijCli ?? zellijCli;
|
|
1441
|
+
const session = sessionManager.get(args.id);
|
|
1442
|
+
const warnings = [];
|
|
1443
|
+
const output = subscriberManager.has(session.id) ? readOutputSnapshot(session.id) : void 0;
|
|
1444
|
+
try {
|
|
1445
|
+
await zellijCliApi.sendCtrlC(session.paneId);
|
|
1446
|
+
await setTimeout$1(500);
|
|
1447
|
+
} catch (error) {
|
|
1448
|
+
warnings.push(`Ctrl-C failed or pane was already gone: ${error instanceof Error ? error.message : String(error)}`);
|
|
1449
|
+
}
|
|
1450
|
+
const closeResult = await closePaneOrVerifyGone({
|
|
1451
|
+
paneId: session.paneId,
|
|
1452
|
+
closePane: () => zellijCliApi.closePane(session.paneId),
|
|
1453
|
+
paneExists: zellijCliApi.paneExists
|
|
1454
|
+
});
|
|
1455
|
+
if (!closeResult.cleanupReady) {
|
|
1456
|
+
warnings.push(`close-pane failed: ${closeResult.closeErrorMessage ?? "unknown error"}`);
|
|
1457
|
+
return {
|
|
1458
|
+
killed: false,
|
|
1459
|
+
cleanedUp: false,
|
|
1460
|
+
session: publicSession(sessionManager.updateStatus(session.id, "unknown")),
|
|
1461
|
+
output,
|
|
1462
|
+
next: nextAdvice(true, "close-pane failed and the pane may still be running; the session was kept so kill can be retried."),
|
|
1463
|
+
warnings
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
return finalizeKilledSession(session.id, session.paneId, output, warnings);
|
|
1263
1467
|
}
|
|
1264
1468
|
const zellijPtyKillTool = tool({
|
|
1265
1469
|
description: "Terminate a known Zellij PTY session by sending Ctrl-C, then closing its pane.",
|
|
1266
1470
|
args: { id: schema$4.string().describe("zellij-pty session id.") },
|
|
1267
1471
|
async execute(args) {
|
|
1268
|
-
|
|
1269
|
-
const warnings = [];
|
|
1270
|
-
const output = subscriberManager.has(session.id) ? readOutputSnapshot(session.id) : void 0;
|
|
1271
|
-
try {
|
|
1272
|
-
await zellijCli.sendCtrlC(session.paneId);
|
|
1273
|
-
await setTimeout$1(500);
|
|
1274
|
-
} catch (error) {
|
|
1275
|
-
warnings.push(`Ctrl-C failed or pane was already gone: ${error instanceof Error ? error.message : String(error)}`);
|
|
1276
|
-
}
|
|
1277
|
-
try {
|
|
1278
|
-
await zellijCli.closePane(session.paneId);
|
|
1279
|
-
} catch (error) {
|
|
1280
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1281
|
-
warnings.push(`close-pane failed: ${message}`);
|
|
1282
|
-
if (!closeFailureMeansGone(message)) return jsonResponse({
|
|
1283
|
-
killed: false,
|
|
1284
|
-
cleanedUp: false,
|
|
1285
|
-
session: publicSession(sessionManager.updateStatus(session.id, "unknown")),
|
|
1286
|
-
output,
|
|
1287
|
-
next: nextAdvice(true, "close-pane failed and the pane may still be running; the session was kept so kill can be retried."),
|
|
1288
|
-
warnings
|
|
1289
|
-
});
|
|
1290
|
-
}
|
|
1291
|
-
subscriberManager.stop(session.id);
|
|
1292
|
-
subscriberManager.forget(session.id);
|
|
1293
|
-
unregisterPaneFromWatchdog(session.id);
|
|
1294
|
-
sessionManager.remove(session.id);
|
|
1295
|
-
return jsonResponse({
|
|
1296
|
-
killed: true,
|
|
1297
|
-
cleanedUp: true,
|
|
1298
|
-
id: session.id,
|
|
1299
|
-
paneId: session.paneId,
|
|
1300
|
-
output,
|
|
1301
|
-
next: nextAdvice(false, "Session was closed and removed from the in-memory registry."),
|
|
1302
|
-
warnings
|
|
1303
|
-
});
|
|
1472
|
+
return jsonResponse(await executeZellijPtyKill(args));
|
|
1304
1473
|
}
|
|
1305
1474
|
});
|
|
1475
|
+
async function finalizeKilledSession(sessionId, paneId, output, warnings) {
|
|
1476
|
+
subscriberManager.stop(sessionId);
|
|
1477
|
+
subscriberManager.forget(sessionId);
|
|
1478
|
+
unregisterPaneFromWatchdog(sessionId);
|
|
1479
|
+
sessionManager.remove(sessionId);
|
|
1480
|
+
return {
|
|
1481
|
+
killed: true,
|
|
1482
|
+
cleanedUp: true,
|
|
1483
|
+
id: sessionId,
|
|
1484
|
+
paneId,
|
|
1485
|
+
output,
|
|
1486
|
+
next: nextAdvice(false, "Session was closed and removed from the in-memory registry."),
|
|
1487
|
+
warnings
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1306
1490
|
//#endregion
|
|
1307
1491
|
//#region src/tools/list.ts
|
|
1308
1492
|
const zellijPtyListTool = tool({
|
|
@@ -1318,55 +1502,123 @@ const zellijPtyListTool = tool({
|
|
|
1318
1502
|
//#endregion
|
|
1319
1503
|
//#region src/tools/read.ts
|
|
1320
1504
|
const schema$3 = tool.schema;
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
if (!statusAfterStart.active) {
|
|
1350
|
-
warnings.push("Subscriber is inactive; returned output may be stale.");
|
|
1351
|
-
if (session.status === "running") sessionManager.updateStatus(session.id, "unknown");
|
|
1505
|
+
async function executeZellijPtyRead(args, dependencies = {}) {
|
|
1506
|
+
const sessionManagerApi = dependencies.sessionManager ?? sessionManager;
|
|
1507
|
+
const subscriberManagerApi = dependencies.subscriberManager ?? subscriberManager;
|
|
1508
|
+
const publicSessionApi = dependencies.publicSession ?? publicSession;
|
|
1509
|
+
const nextAdviceApi = dependencies.nextAdvice ?? nextAdvice;
|
|
1510
|
+
const readOutputSnapshotApi = dependencies.readOutputSnapshot ?? readOutputSnapshot;
|
|
1511
|
+
const validateGrepApi = dependencies.validateGrep ?? validateGrep;
|
|
1512
|
+
const paneExistsApi = dependencies.paneExists ?? zellijCli.paneExists;
|
|
1513
|
+
const session = sessionManagerApi.get(args.id);
|
|
1514
|
+
const grepError = validateGrepApi(args.grep);
|
|
1515
|
+
if (grepError) return {
|
|
1516
|
+
session: publicSessionApi(session),
|
|
1517
|
+
output: {
|
|
1518
|
+
text: "",
|
|
1519
|
+
lines: [],
|
|
1520
|
+
lineCount: session.lineCount,
|
|
1521
|
+
returned: 0,
|
|
1522
|
+
truncated: false
|
|
1523
|
+
},
|
|
1524
|
+
next: nextAdviceApi(false, `Invalid grep regex: ${grepError}`),
|
|
1525
|
+
subscriberActive: false,
|
|
1526
|
+
subscriberLastExitedAt: null,
|
|
1527
|
+
subscriberErrors: [],
|
|
1528
|
+
warnings: [],
|
|
1529
|
+
cleanup: {
|
|
1530
|
+
requested: false,
|
|
1531
|
+
performed: false,
|
|
1532
|
+
alreadyClosed: false
|
|
1352
1533
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1534
|
+
};
|
|
1535
|
+
const subscriberStatus = subscriberManagerApi.status(session.id);
|
|
1536
|
+
if (!subscriberStatus.hasBuffer || !subscriberStatus.active && (session.status === "running" || session.status === "unknown")) await subscriberManagerApi.start(session);
|
|
1537
|
+
const statusAfterStart = subscriberManagerApi.status(session.id);
|
|
1538
|
+
const warnings = [];
|
|
1539
|
+
if (session.humanInputOnly) warnings.push("This pane is human-input-only: agent writes are forbidden, but rendered output is visible to the agent.");
|
|
1540
|
+
if (!statusAfterStart.active && session.status === "running") {
|
|
1541
|
+
warnings.push("Subscriber is inactive; returned output may be stale.");
|
|
1542
|
+
sessionManagerApi.updateStatus(session.id, "unknown");
|
|
1543
|
+
}
|
|
1544
|
+
const output = readOutputSnapshotApi(session.id, {
|
|
1545
|
+
maxLines: args.maxLines,
|
|
1546
|
+
grep: args.grep,
|
|
1547
|
+
ignoreCase: args.ignoreCase
|
|
1548
|
+
});
|
|
1549
|
+
const cleanup = await cleanupExitedPaneOnRead(session.id, session.status, args.cleanupExitedPaneOnRead ?? dependencies.defaultCleanupExitedPaneOnRead ?? true, {
|
|
1550
|
+
sessionManager: sessionManagerApi,
|
|
1551
|
+
subscriberManager: subscriberManagerApi,
|
|
1552
|
+
paneExists: paneExistsApi
|
|
1553
|
+
});
|
|
1554
|
+
if (cleanup.warning) warnings.push(cleanup.warning);
|
|
1555
|
+
return {
|
|
1556
|
+
session: publicSessionApi(session),
|
|
1557
|
+
output,
|
|
1558
|
+
next: nextAdviceApi(!isCompletedSession(session.status), nextReadReason(session.status)),
|
|
1559
|
+
subscriberActive: statusAfterStart.active,
|
|
1560
|
+
subscriberLastExitedAt: statusAfterStart.lastExitedAt,
|
|
1561
|
+
subscriberErrors: subscriberManagerApi.stderr(session.id),
|
|
1562
|
+
warnings,
|
|
1563
|
+
cleanup
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
async function cleanupExitedPaneOnRead(sessionId, status, enabled, dependencies) {
|
|
1567
|
+
if (!(enabled && isCompletedSession(status))) return {
|
|
1568
|
+
requested: false,
|
|
1569
|
+
performed: false,
|
|
1570
|
+
alreadyClosed: false
|
|
1571
|
+
};
|
|
1572
|
+
const session = dependencies.sessionManager.get(sessionId);
|
|
1573
|
+
if (session.tombstone?.paneClosedAt) return {
|
|
1574
|
+
requested: true,
|
|
1575
|
+
performed: false,
|
|
1576
|
+
alreadyClosed: true
|
|
1577
|
+
};
|
|
1578
|
+
const closeResult = await closePaneOrVerifyGone({
|
|
1579
|
+
paneId: session.paneId,
|
|
1580
|
+
closePane: () => dependencies.subscriberManager.closeSessionPane(sessionId, { throwOnFailure: true }),
|
|
1581
|
+
paneExists: dependencies.paneExists
|
|
1582
|
+
});
|
|
1583
|
+
if (closeResult.cleanupReady) {
|
|
1584
|
+
dependencies.sessionManager.markTerminalPaneClosed(sessionId);
|
|
1585
|
+
return {
|
|
1586
|
+
requested: true,
|
|
1587
|
+
performed: true,
|
|
1588
|
+
alreadyClosed: closeResult.alreadyGone
|
|
1589
|
+
};
|
|
1367
1590
|
}
|
|
1368
|
-
|
|
1591
|
+
return {
|
|
1592
|
+
requested: true,
|
|
1593
|
+
performed: false,
|
|
1594
|
+
alreadyClosed: false,
|
|
1595
|
+
warning: `Completed pane cleanup failed: ${closeResult.closeErrorMessage ?? "unknown error"}`
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
function createZellijPtyReadTool(options = {}) {
|
|
1599
|
+
return tool({
|
|
1600
|
+
description: "Read recent rendered output from a Zellij PTY session. Supports regex grep filtering.",
|
|
1601
|
+
args: {
|
|
1602
|
+
id: schema$3.string().describe("zellij-pty session id."),
|
|
1603
|
+
maxLines: schema$3.number().int().positive().max(5e3).optional().describe("Maximum recent output lines to return. Defaults to 200."),
|
|
1604
|
+
grep: schema$3.string().optional().describe("Regex used to filter returned lines."),
|
|
1605
|
+
ignoreCase: schema$3.boolean().optional().describe("Use case-insensitive regex matching."),
|
|
1606
|
+
cleanupExitedPaneOnRead: schema$3.boolean().optional().describe("Close completed panes after returning the final output. Defaults to true.")
|
|
1607
|
+
},
|
|
1608
|
+
async execute(args) {
|
|
1609
|
+
return jsonResponse(await executeZellijPtyRead(args, {
|
|
1610
|
+
...options.dependencies,
|
|
1611
|
+
defaultCleanupExitedPaneOnRead: options.defaultCleanupExitedPaneOnRead
|
|
1612
|
+
}));
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
createZellijPtyReadTool();
|
|
1617
|
+
function isCompletedSession(status) {
|
|
1618
|
+
return status === "terminal" || status === "exited" || status === "killed";
|
|
1619
|
+
}
|
|
1369
1620
|
function nextReadReason(status) {
|
|
1621
|
+
if (status === "terminal") return "Session has finished; the final output is retained until the completed pane is read and cleaned up.";
|
|
1370
1622
|
if (status === "running") return "Session is still running; read again later if more output is expected.";
|
|
1371
1623
|
if (status === "unknown") return "Session state is unknown because the subscriber is inactive; output may be stale, but retrying read may restart observation.";
|
|
1372
1624
|
return "Session is no longer running.";
|
|
@@ -1661,6 +1913,395 @@ const zellijPtyWriteTool = tool({
|
|
|
1661
1913
|
}
|
|
1662
1914
|
});
|
|
1663
1915
|
//#endregion
|
|
1916
|
+
//#region src/zellij/tab-title-status-snapshot.ts
|
|
1917
|
+
/**
|
|
1918
|
+
* Events that should trigger a debounced refresh of the tab title base status
|
|
1919
|
+
* via the `/session/status` snapshot API.
|
|
1920
|
+
*
|
|
1921
|
+
* Base state (running vs idle) is sourced from the snapshot rather than
|
|
1922
|
+
* individual `session.idle` events because testing showed that both parent and
|
|
1923
|
+
* child sessions report busy during subagent execution, and the parent remains
|
|
1924
|
+
* busy even after the child completes. The snapshot gives a consistent,
|
|
1925
|
+
* server-authoritative view. `needs-input` has no REST API, so it continues
|
|
1926
|
+
* to be managed purely through events.
|
|
1927
|
+
*
|
|
1928
|
+
* The snapshot reconciliation is intentionally *not* optimistic for idle-like
|
|
1929
|
+
* transitions: a lone parent/child idle event can be stale during subagent
|
|
1930
|
+
* handoff. The debounce coalesces high-frequency event streams to avoid
|
|
1931
|
+
* hammering the API on every individual status change.
|
|
1932
|
+
*/
|
|
1933
|
+
function shouldRefreshTabTitleStatusSnapshot(event) {
|
|
1934
|
+
switch (event.type) {
|
|
1935
|
+
case "session.status":
|
|
1936
|
+
case "session.idle":
|
|
1937
|
+
case "session.error":
|
|
1938
|
+
case "session.created":
|
|
1939
|
+
case "session.deleted":
|
|
1940
|
+
case "question.asked":
|
|
1941
|
+
case "question.replied":
|
|
1942
|
+
case "question.rejected":
|
|
1943
|
+
case "permission.asked":
|
|
1944
|
+
case "permission.replied":
|
|
1945
|
+
case "permission.updated": return true;
|
|
1946
|
+
default: return false;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
/**
|
|
1950
|
+
* Best-effort fetch of session statuses for a workspace.
|
|
1951
|
+
*
|
|
1952
|
+
* Only accepts object-keyed maps: `{ [sessionID]: SessionStatus }` directly,
|
|
1953
|
+
* or the generated-client envelope `{ data: { [sessionID]: SessionStatus } }`.
|
|
1954
|
+
*
|
|
1955
|
+
* An empty map `{}` is a valid snapshot (all sessions ended / none tracked).
|
|
1956
|
+
* Arrays are never accepted — they always return undefined.
|
|
1957
|
+
* If any single status entry fails to parse, the entire snapshot is rejected
|
|
1958
|
+
* (no partial apply) to avoid incorrectly clearing session states.
|
|
1959
|
+
*
|
|
1960
|
+
* Failures are swallowed and return undefined so the caller never throws.
|
|
1961
|
+
*/
|
|
1962
|
+
async function fetchSessionStatusSnapshot(client, workspaceRoot) {
|
|
1963
|
+
try {
|
|
1964
|
+
if (!client.session?.status) {
|
|
1965
|
+
debug("fetchSessionStatusSnapshot: client.session.status not available");
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
const result = await client.session.status({ query: { directory: workspaceRoot } });
|
|
1969
|
+
const payload = result && typeof result === "object" && "data" in result ? result.data : result;
|
|
1970
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
1971
|
+
debug("fetchSessionStatusSnapshot received non-object payload");
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
const entries = Object.entries(payload);
|
|
1975
|
+
if (entries.length === 0) return {};
|
|
1976
|
+
const snapshot = {};
|
|
1977
|
+
for (const [sessionID, status] of entries) {
|
|
1978
|
+
const parsed = parseSessionStatus(status);
|
|
1979
|
+
if (parsed === void 0) {
|
|
1980
|
+
debug("fetchSessionStatusSnapshot received invalid status entry, rejecting entire snapshot");
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
snapshot[sessionID] = parsed;
|
|
1984
|
+
}
|
|
1985
|
+
return snapshot;
|
|
1986
|
+
} catch (err) {
|
|
1987
|
+
debug("fetchSessionStatusSnapshot failed", errorMessage(err));
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
function parseSessionStatus(value) {
|
|
1992
|
+
if (!value || typeof value !== "object" || !("type" in value)) return void 0;
|
|
1993
|
+
const status = value;
|
|
1994
|
+
if (status.type === "idle" || status.type === "busy") return { type: status.type };
|
|
1995
|
+
if (status.type === "retry") return {
|
|
1996
|
+
type: "retry",
|
|
1997
|
+
attempt: typeof status.attempt === "number" ? status.attempt : 0,
|
|
1998
|
+
message: typeof status.message === "string" ? status.message : "",
|
|
1999
|
+
next: typeof status.next === "number" ? status.next : 0
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
const DEFAULT_DEBOUNCE_MS = 1e3;
|
|
2003
|
+
/**
|
|
2004
|
+
* Encapsulates tab title session-status snapshot fetching with debounced refresh.
|
|
2005
|
+
*
|
|
2006
|
+
* This class replaces the nested `refreshTabTitleSnapshot` /
|
|
2007
|
+
* `scheduleTabTitleSnapshotRefresh` functions that previously lived inside
|
|
2008
|
+
* `createZellijPtyPlugin`. It manages its own timer so the plugin factory
|
|
2009
|
+
* remains a simple composition root.
|
|
2010
|
+
*
|
|
2011
|
+
* Usage:
|
|
2012
|
+
* ```
|
|
2013
|
+
* const refresher = tabTitleManager
|
|
2014
|
+
* ? new TabTitleStatusSnapshotRefresher({ client, workspaceRoot, manager: tabTitleManager })
|
|
2015
|
+
* : undefined
|
|
2016
|
+
*
|
|
2017
|
+
* await refresher?.refreshNow() // initial snapshot
|
|
2018
|
+
* refresher?.scheduleRefresh() // on relevant events
|
|
2019
|
+
* refresher?.dispose() // on shutdown
|
|
2020
|
+
* ```
|
|
2021
|
+
*/
|
|
2022
|
+
var TabTitleStatusSnapshotRefresher = class {
|
|
2023
|
+
client;
|
|
2024
|
+
workspaceRoot;
|
|
2025
|
+
manager;
|
|
2026
|
+
debounceMs;
|
|
2027
|
+
timer;
|
|
2028
|
+
constructor(options) {
|
|
2029
|
+
this.client = options.client;
|
|
2030
|
+
this.workspaceRoot = options.workspaceRoot;
|
|
2031
|
+
this.manager = options.manager;
|
|
2032
|
+
this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
2033
|
+
}
|
|
2034
|
+
/**
|
|
2035
|
+
* Fetches and applies the snapshot immediately, cancelling any pending
|
|
2036
|
+
* debounced refresh.
|
|
2037
|
+
*/
|
|
2038
|
+
async refreshNow() {
|
|
2039
|
+
this.clearTimer();
|
|
2040
|
+
const snapshot = await fetchSessionStatusSnapshot(this.client, this.workspaceRoot);
|
|
2041
|
+
if (snapshot !== void 0) this.manager.applySessionStatusSnapshot(snapshot);
|
|
2042
|
+
}
|
|
2043
|
+
/**
|
|
2044
|
+
* Schedules a debounced snapshot refresh. Subsequent calls while a timer
|
|
2045
|
+
* is pending coalesce into a single refresh.
|
|
2046
|
+
*/
|
|
2047
|
+
scheduleRefresh() {
|
|
2048
|
+
if (this.timer) return;
|
|
2049
|
+
this.timer = setTimeout(() => {
|
|
2050
|
+
this.timer = void 0;
|
|
2051
|
+
this.refreshNow().catch((err) => debug("tab title snapshot refresh failed", errorMessage(err)));
|
|
2052
|
+
}, this.debounceMs);
|
|
2053
|
+
}
|
|
2054
|
+
/**
|
|
2055
|
+
* Clears any pending debounced refresh. Use this during shutdown so a
|
|
2056
|
+
* pending timer does not fire after the manager has been destroyed.
|
|
2057
|
+
*/
|
|
2058
|
+
dispose() {
|
|
2059
|
+
this.clearTimer();
|
|
2060
|
+
}
|
|
2061
|
+
clearTimer() {
|
|
2062
|
+
if (this.timer) {
|
|
2063
|
+
clearTimeout(this.timer);
|
|
2064
|
+
this.timer = void 0;
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
};
|
|
2068
|
+
//#endregion
|
|
2069
|
+
//#region src/zellij/completion-notifications.ts
|
|
2070
|
+
const completionTitle = "Zellij PTY session completed";
|
|
2071
|
+
const completionMessage = "A Zellij PTY session completed. Review the finished pane if needed.";
|
|
2072
|
+
const queuedNoticeHeader = "[OpenCode] Zellij PTY completion notice";
|
|
2073
|
+
function buildQueuedCompletionNotice(events) {
|
|
2074
|
+
return [queuedNoticeHeader, ...events.map((event) => `- ${event.session.id} (${event.session.paneId}) 已完成,請使用 zellij_pty_read 讀取最終輸出並清理 pane。`)].join("\n");
|
|
2075
|
+
}
|
|
2076
|
+
function buildCompletionPromptRequest(event) {
|
|
2077
|
+
return {
|
|
2078
|
+
path: { id: event.session.openCodeSessionId },
|
|
2079
|
+
body: { parts: [{
|
|
2080
|
+
type: "text",
|
|
2081
|
+
text: completionMessage
|
|
2082
|
+
}] }
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
function injectQueuedCompletionNotice(input, notice) {
|
|
2086
|
+
if (typeof input === "string") return `${notice}\n\n${input}`;
|
|
2087
|
+
if (!input || typeof input !== "object") return input;
|
|
2088
|
+
const record = input;
|
|
2089
|
+
if (Array.isArray(record.parts)) return {
|
|
2090
|
+
...record,
|
|
2091
|
+
parts: [{
|
|
2092
|
+
type: "text",
|
|
2093
|
+
text: notice
|
|
2094
|
+
}, ...record.parts]
|
|
2095
|
+
};
|
|
2096
|
+
if (typeof record.message === "string") return {
|
|
2097
|
+
...record,
|
|
2098
|
+
message: `${notice}\n\n${record.message}`
|
|
2099
|
+
};
|
|
2100
|
+
if (typeof record.content === "string") return {
|
|
2101
|
+
...record,
|
|
2102
|
+
content: `${notice}\n\n${record.content}`
|
|
2103
|
+
};
|
|
2104
|
+
if (typeof record.text === "string") return {
|
|
2105
|
+
...record,
|
|
2106
|
+
text: `${notice}\n\n${record.text}`
|
|
2107
|
+
};
|
|
2108
|
+
return {
|
|
2109
|
+
...record,
|
|
2110
|
+
message: notice
|
|
2111
|
+
};
|
|
2112
|
+
}
|
|
2113
|
+
function evaluateCompletionPromptDecision(input) {
|
|
2114
|
+
if (input.config.mode !== "prompt") return {
|
|
2115
|
+
shouldPrompt: false,
|
|
2116
|
+
shouldQueue: false,
|
|
2117
|
+
reason: "prompt mode disabled"
|
|
2118
|
+
};
|
|
2119
|
+
if (input.event.session.humanInputOnly || !input.event.session.allowAgentInput) return {
|
|
2120
|
+
shouldPrompt: false,
|
|
2121
|
+
shouldQueue: true,
|
|
2122
|
+
reason: "human-input-only session"
|
|
2123
|
+
};
|
|
2124
|
+
if (!input.promptClientAvailable) return {
|
|
2125
|
+
shouldPrompt: false,
|
|
2126
|
+
shouldQueue: true,
|
|
2127
|
+
reason: "prompt client unavailable"
|
|
2128
|
+
};
|
|
2129
|
+
if (!input.event.session.openCodeSessionId) return {
|
|
2130
|
+
shouldPrompt: false,
|
|
2131
|
+
shouldQueue: true,
|
|
2132
|
+
reason: "session id unavailable"
|
|
2133
|
+
};
|
|
2134
|
+
if (!input.snapshotAvailable) return {
|
|
2135
|
+
shouldPrompt: false,
|
|
2136
|
+
shouldQueue: true,
|
|
2137
|
+
reason: "session status snapshot unavailable"
|
|
2138
|
+
};
|
|
2139
|
+
if (input.config.prompt.maxAttempts <= 0 || input.promptAttemptCount >= input.config.prompt.maxAttempts) return {
|
|
2140
|
+
shouldPrompt: false,
|
|
2141
|
+
shouldQueue: true,
|
|
2142
|
+
reason: "prompt max attempts reached"
|
|
2143
|
+
};
|
|
2144
|
+
if (input.config.prompt.cooldownMs > 0 && input.lastPromptAttemptAt !== null && input.now - input.lastPromptAttemptAt < input.config.prompt.cooldownMs) return {
|
|
2145
|
+
shouldPrompt: false,
|
|
2146
|
+
shouldQueue: true,
|
|
2147
|
+
reason: "prompt cooldown active"
|
|
2148
|
+
};
|
|
2149
|
+
if (input.config.prompt.requireIdle) {
|
|
2150
|
+
const sessionId = input.event.session.openCodeSessionId;
|
|
2151
|
+
if (!sessionId) return {
|
|
2152
|
+
shouldPrompt: false,
|
|
2153
|
+
shouldQueue: true,
|
|
2154
|
+
reason: "session status unavailable"
|
|
2155
|
+
};
|
|
2156
|
+
const status = sessionId ? input.snapshot?.[sessionId] : void 0;
|
|
2157
|
+
if (!status) return {
|
|
2158
|
+
shouldPrompt: false,
|
|
2159
|
+
shouldQueue: true,
|
|
2160
|
+
reason: "session status unavailable"
|
|
2161
|
+
};
|
|
2162
|
+
if (status && status.type !== "idle") return {
|
|
2163
|
+
shouldPrompt: false,
|
|
2164
|
+
shouldQueue: true,
|
|
2165
|
+
reason: "session not idle"
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
return {
|
|
2169
|
+
shouldPrompt: true,
|
|
2170
|
+
shouldQueue: false,
|
|
2171
|
+
reason: "prompt allowed"
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
var SessionCompletionNotificationQueue = class {
|
|
2175
|
+
states = /* @__PURE__ */ new Map();
|
|
2176
|
+
constructor(context, hooks = {}, clock = () => Date.now()) {
|
|
2177
|
+
this.context = context;
|
|
2178
|
+
this.hooks = hooks;
|
|
2179
|
+
this.clock = clock;
|
|
2180
|
+
}
|
|
2181
|
+
hasPending(sessionId) {
|
|
2182
|
+
return this.states.get(sessionId)?.queued ?? false;
|
|
2183
|
+
}
|
|
2184
|
+
clearSession(sessionId) {
|
|
2185
|
+
this.states.delete(sessionId);
|
|
2186
|
+
}
|
|
2187
|
+
clearAll() {
|
|
2188
|
+
this.states.clear();
|
|
2189
|
+
}
|
|
2190
|
+
dispose() {
|
|
2191
|
+
this.clearAll();
|
|
2192
|
+
}
|
|
2193
|
+
async handleSessionTerminal(event) {
|
|
2194
|
+
if (this.context.config.mode === "off") return;
|
|
2195
|
+
if (this.states.has(event.sessionId) || event.session.tombstone?.notificationSentAt) return;
|
|
2196
|
+
const state = {
|
|
2197
|
+
event,
|
|
2198
|
+
queued: false,
|
|
2199
|
+
toastSent: false,
|
|
2200
|
+
promptAttempts: 0,
|
|
2201
|
+
promptAttemptedAt: null
|
|
2202
|
+
};
|
|
2203
|
+
this.states.set(event.sessionId, state);
|
|
2204
|
+
switch (this.context.config.mode) {
|
|
2205
|
+
case "queue":
|
|
2206
|
+
state.queued = true;
|
|
2207
|
+
return;
|
|
2208
|
+
case "toast":
|
|
2209
|
+
await this.sendToast(state);
|
|
2210
|
+
this.finalize(state);
|
|
2211
|
+
return;
|
|
2212
|
+
case "queue+toast":
|
|
2213
|
+
state.queued = true;
|
|
2214
|
+
await this.sendToast(state);
|
|
2215
|
+
return;
|
|
2216
|
+
case "prompt":
|
|
2217
|
+
await this.tryPromptOrQueue(state);
|
|
2218
|
+
break;
|
|
2219
|
+
default:
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
injectQueuedChatMessage(input) {
|
|
2223
|
+
const pending = [...this.states.values()].filter((state) => state.queued);
|
|
2224
|
+
if (pending.length === 0) return input;
|
|
2225
|
+
const notice = buildQueuedCompletionNotice(pending.map((state) => state.event));
|
|
2226
|
+
for (const state of pending) {
|
|
2227
|
+
if (!state.toastSent) this.context.markSent(state.event.sessionId);
|
|
2228
|
+
this.finalize(state);
|
|
2229
|
+
}
|
|
2230
|
+
return injectQueuedCompletionNotice(input, notice);
|
|
2231
|
+
}
|
|
2232
|
+
async tryPromptOrQueue(state) {
|
|
2233
|
+
if (!state.event.session.openCodeSessionId) {
|
|
2234
|
+
state.queued = true;
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
const session = this.context.client.session;
|
|
2238
|
+
const prompt = session?.prompt ?? session?.promptAsync;
|
|
2239
|
+
const statusSnapshot = await fetchSessionStatusSnapshot(this.context.client, this.context.workspaceRoot);
|
|
2240
|
+
const decision = evaluateCompletionPromptDecision({
|
|
2241
|
+
event: state.event,
|
|
2242
|
+
config: this.context.config,
|
|
2243
|
+
snapshot: statusSnapshot,
|
|
2244
|
+
snapshotAvailable: statusSnapshot !== void 0,
|
|
2245
|
+
now: this.clock(),
|
|
2246
|
+
lastPromptAttemptAt: state.promptAttemptedAt,
|
|
2247
|
+
promptAttemptCount: state.promptAttempts,
|
|
2248
|
+
promptClientAvailable: Boolean(prompt)
|
|
2249
|
+
});
|
|
2250
|
+
if (!decision.shouldPrompt) {
|
|
2251
|
+
state.queued = decision.shouldQueue;
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
if (this.hooks.prompt) try {
|
|
2255
|
+
const maybePromise = this.hooks.prompt(state.event);
|
|
2256
|
+
if (maybePromise && typeof maybePromise.then === "function") await maybePromise;
|
|
2257
|
+
} catch (error) {
|
|
2258
|
+
debug("completion notification prompt hook failed", errorMessage(error));
|
|
2259
|
+
}
|
|
2260
|
+
if (!session || !prompt) {
|
|
2261
|
+
state.queued = true;
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
state.promptAttempts += 1;
|
|
2265
|
+
state.promptAttemptedAt = this.clock();
|
|
2266
|
+
try {
|
|
2267
|
+
if (session.prompt) await session.prompt(buildCompletionPromptRequest(state.event));
|
|
2268
|
+
else if (session.promptAsync) await session.promptAsync(buildCompletionPromptRequest(state.event));
|
|
2269
|
+
else {
|
|
2270
|
+
state.queued = true;
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
this.context.markSent(state.event.sessionId);
|
|
2274
|
+
this.finalize(state);
|
|
2275
|
+
} catch (error) {
|
|
2276
|
+
debug("completion notification prompt failed", errorMessage(error));
|
|
2277
|
+
state.queued = true;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
async sendToast(state) {
|
|
2281
|
+
const toast = this.context.client.tui?.showToast;
|
|
2282
|
+
if (!toast) {
|
|
2283
|
+
debug("completion notification toast skipped: client.tui.showToast unavailable");
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2286
|
+
try {
|
|
2287
|
+
await toast({ body: {
|
|
2288
|
+
title: completionTitle,
|
|
2289
|
+
message: completionMessage,
|
|
2290
|
+
variant: "success",
|
|
2291
|
+
duration: 1e4
|
|
2292
|
+
} });
|
|
2293
|
+
state.toastSent = true;
|
|
2294
|
+
this.context.markSent(state.event.sessionId);
|
|
2295
|
+
if (!state.queued) this.finalize(state);
|
|
2296
|
+
} catch (error) {
|
|
2297
|
+
debug("completion notification toast failed", errorMessage(error));
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
finalize(state) {
|
|
2301
|
+
this.states.delete(state.event.sessionId);
|
|
2302
|
+
}
|
|
2303
|
+
};
|
|
2304
|
+
//#endregion
|
|
1664
2305
|
//#region src/zellij/shutdown-cleanup.ts
|
|
1665
2306
|
let registered = false;
|
|
1666
2307
|
let cleanedUp = false;
|
|
@@ -1764,19 +2405,11 @@ function handleTabTitleEvent(tabTitleManager, event) {
|
|
|
1764
2405
|
case "session.status": {
|
|
1765
2406
|
const sessionID = stringProperty(properties, "sessionID");
|
|
1766
2407
|
const status = sessionStatusProperty(properties);
|
|
1767
|
-
if (sessionID && status) tabTitleManager.updateSessionStatus(sessionID, status);
|
|
1768
|
-
break;
|
|
1769
|
-
}
|
|
1770
|
-
case "session.idle": {
|
|
1771
|
-
const sessionID = stringProperty(properties, "sessionID");
|
|
1772
|
-
if (sessionID) tabTitleManager.markSessionIdle(sessionID);
|
|
1773
|
-
break;
|
|
1774
|
-
}
|
|
1775
|
-
case "session.error": {
|
|
1776
|
-
const sessionID = stringProperty(properties, "sessionID");
|
|
1777
|
-
if (sessionID) tabTitleManager.markSessionIdle(sessionID);
|
|
2408
|
+
if (sessionID && status && status.type !== "idle") tabTitleManager.updateSessionStatus(sessionID, status);
|
|
1778
2409
|
break;
|
|
1779
2410
|
}
|
|
2411
|
+
case "session.idle": break;
|
|
2412
|
+
case "session.error": break;
|
|
1780
2413
|
case "vcs.branch.updated":
|
|
1781
2414
|
tabTitleManager.setBranch(stringProperty(properties, "branch"));
|
|
1782
2415
|
break;
|
|
@@ -1784,22 +2417,32 @@ function handleTabTitleEvent(tabTitleManager, event) {
|
|
|
1784
2417
|
case "permission.asked": {
|
|
1785
2418
|
const id = inputRequestID(properties);
|
|
1786
2419
|
const sessionID = stringProperty(properties, "sessionID");
|
|
1787
|
-
if (id && sessionID)
|
|
2420
|
+
if (id && sessionID) {
|
|
2421
|
+
tabTitleManager.markNeedsInput(id, sessionID);
|
|
2422
|
+
tabTitleManager.updateSessionStatus(sessionID, { type: "busy" });
|
|
2423
|
+
}
|
|
1788
2424
|
break;
|
|
1789
2425
|
}
|
|
1790
2426
|
case "permission.updated": {
|
|
1791
2427
|
const id = inputRequestID(properties);
|
|
1792
2428
|
const sessionID = stringProperty(properties, "sessionID");
|
|
1793
2429
|
const state = inputState(properties);
|
|
1794
|
-
if (id && isResolvedInputState(state))
|
|
1795
|
-
|
|
2430
|
+
if (id && isResolvedInputState(state)) {
|
|
2431
|
+
tabTitleManager.clearNeedsInput(id);
|
|
2432
|
+
if (sessionID) tabTitleManager.updateSessionStatus(sessionID, { type: "busy" });
|
|
2433
|
+
} else if (id && sessionID) {
|
|
2434
|
+
tabTitleManager.markNeedsInput(id, sessionID);
|
|
2435
|
+
tabTitleManager.updateSessionStatus(sessionID, { type: "busy" });
|
|
2436
|
+
}
|
|
1796
2437
|
break;
|
|
1797
2438
|
}
|
|
1798
2439
|
case "question.replied":
|
|
1799
2440
|
case "question.rejected":
|
|
1800
2441
|
case "permission.replied": {
|
|
1801
2442
|
const id = inputRequestID(properties);
|
|
2443
|
+
const sessionID = stringProperty(properties, "sessionID");
|
|
1802
2444
|
if (id) tabTitleManager.clearNeedsInput(id);
|
|
2445
|
+
if (sessionID) tabTitleManager.updateSessionStatus(sessionID, { type: "busy" });
|
|
1803
2446
|
break;
|
|
1804
2447
|
}
|
|
1805
2448
|
case "session.deleted": {
|
|
@@ -1869,6 +2512,27 @@ var TabTitleManager = class {
|
|
|
1869
2512
|
this.branchName = trimmed;
|
|
1870
2513
|
this.scheduleUpdate();
|
|
1871
2514
|
}
|
|
2515
|
+
/**
|
|
2516
|
+
* Applies a snapshot of session statuses from the server.
|
|
2517
|
+
*
|
|
2518
|
+
* This replaces the entire base status map. Sessions absent from the snapshot
|
|
2519
|
+
* (e.g. because they ended) are removed so stale busy/idle entries do not
|
|
2520
|
+
* persist. This is the authoritative source for the "running vs idle" base
|
|
2521
|
+
* state; individual session.status events still perform optimistic updates
|
|
2522
|
+
* for immediacy but the snapshot corrects drift.
|
|
2523
|
+
*
|
|
2524
|
+
* The `needs-input` overlay remains independent — it is managed purely by
|
|
2525
|
+
* events (question/permission asked/replied/etc.) and always takes priority
|
|
2526
|
+
* over the snapshot base when computing the displayed title.
|
|
2527
|
+
*/
|
|
2528
|
+
applySessionStatusSnapshot(statuses) {
|
|
2529
|
+
for (const sessionID of this.sessionStatuses.keys()) if (!(sessionID in statuses)) this.sessionStatuses.delete(sessionID);
|
|
2530
|
+
for (const [sessionID, status] of Object.entries(statuses)) {
|
|
2531
|
+
const activity = status.type === "idle" ? "idle" : "running";
|
|
2532
|
+
if (this.sessionStatuses.get(sessionID) !== activity) this.sessionStatuses.set(sessionID, activity);
|
|
2533
|
+
}
|
|
2534
|
+
this.scheduleUpdate();
|
|
2535
|
+
}
|
|
1872
2536
|
updateSessionStatus(sessionID, status) {
|
|
1873
2537
|
const activity = status.type === "idle" ? "idle" : "running";
|
|
1874
2538
|
if (this.sessionStatuses.get(sessionID) === activity) return;
|
|
@@ -2029,13 +2693,15 @@ var TabTitleManager = class {
|
|
|
2029
2693
|
};
|
|
2030
2694
|
//#endregion
|
|
2031
2695
|
//#region src/plugin.ts
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2696
|
+
function createPtyTools(defaultCleanupExitedPaneOnRead) {
|
|
2697
|
+
return {
|
|
2698
|
+
zellij_pty_spawn: zellijPtySpawnTool,
|
|
2699
|
+
zellij_pty_list: zellijPtyListTool,
|
|
2700
|
+
zellij_pty_write: zellijPtyWriteTool,
|
|
2701
|
+
zellij_pty_read: createZellijPtyReadTool({ defaultCleanupExitedPaneOnRead }),
|
|
2702
|
+
zellij_pty_kill: zellijPtyKillTool
|
|
2703
|
+
};
|
|
2704
|
+
}
|
|
2039
2705
|
function getProjectName(path) {
|
|
2040
2706
|
return path.split(/[/\\]/).filter(Boolean).pop() || "opencode";
|
|
2041
2707
|
}
|
|
@@ -2099,22 +2765,67 @@ function createZellijPtyPlugin(dependencies = {}) {
|
|
|
2099
2765
|
branch: config.tabTitle.emojiBranch
|
|
2100
2766
|
}
|
|
2101
2767
|
}) : void 0;
|
|
2102
|
-
tabTitleManager?.renderImmediate().catch((error) => debug("initial tab title render failed", errorMessage(error)));
|
|
2103
2768
|
const client = input.client;
|
|
2769
|
+
const completionNotifications = config.pty.completionNotification.mode === "off" ? void 0 : dependencies.createCompletionNotifications?.({
|
|
2770
|
+
client,
|
|
2771
|
+
workspaceRoot,
|
|
2772
|
+
config: config.pty.completionNotification,
|
|
2773
|
+
markSent(sessionId) {
|
|
2774
|
+
try {
|
|
2775
|
+
sessionManager.markTerminalNotificationSent(sessionId);
|
|
2776
|
+
} catch (error) {
|
|
2777
|
+
debug("mark terminal notification sent failed", errorMessage(error));
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
}) ?? new SessionCompletionNotificationQueue({
|
|
2781
|
+
client,
|
|
2782
|
+
workspaceRoot,
|
|
2783
|
+
config: config.pty.completionNotification,
|
|
2784
|
+
markSent(sessionId) {
|
|
2785
|
+
try {
|
|
2786
|
+
sessionManager.markTerminalNotificationSent(sessionId);
|
|
2787
|
+
} catch (error) {
|
|
2788
|
+
debug("mark terminal notification sent failed", errorMessage(error));
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
});
|
|
2792
|
+
subscriberManager.setLifecycleHooks(completionNotifications ? { onSessionTerminal: (event) => void completionNotifications.handleSessionTerminal(event).catch((error) => debug("completion notification lifecycle hook failed", errorMessage(error))) } : void 0);
|
|
2793
|
+
const tabTitleSnapshotRefresher = tabTitleManager ? new TabTitleStatusSnapshotRefresher({
|
|
2794
|
+
client,
|
|
2795
|
+
workspaceRoot,
|
|
2796
|
+
manager: tabTitleManager,
|
|
2797
|
+
debounceMs: 1e3
|
|
2798
|
+
}) : void 0;
|
|
2799
|
+
tabTitleSnapshotRefresher?.refreshNow().catch((error) => debug("initial tab title snapshot refresh failed", errorMessage(error)));
|
|
2800
|
+
tabTitleManager?.renderImmediate().catch((error) => debug("initial tab title render failed", errorMessage(error)));
|
|
2104
2801
|
if (config.autoUpdate) (dependencies.startAutoUpdateCheck ?? startAutoUpdateCheck)(client, dependencies.importMetaUrl ?? import.meta.url);
|
|
2105
2802
|
return {
|
|
2106
2803
|
async event(input) {
|
|
2107
2804
|
const event = input.event;
|
|
2108
|
-
if (tabTitleManager)
|
|
2805
|
+
if (tabTitleManager) {
|
|
2806
|
+
if (event.type === "server.instance.disposed" || event.type === "global.disposed") tabTitleSnapshotRefresher?.dispose();
|
|
2807
|
+
await handleTabTitleEvent(tabTitleManager, event);
|
|
2808
|
+
if (shouldRefreshTabTitleStatusSnapshot(event)) tabTitleSnapshotRefresher?.scheduleRefresh();
|
|
2809
|
+
}
|
|
2810
|
+
if (event.type === "server.instance.disposed" || event.type === "global.disposed") {
|
|
2811
|
+
completionNotifications?.clearAll();
|
|
2812
|
+
completionNotifications?.dispose();
|
|
2813
|
+
subscriberManager.setLifecycleHooks(void 0);
|
|
2814
|
+
}
|
|
2109
2815
|
if (event.type === "session.deleted") {
|
|
2110
2816
|
const sessionID = deletedSessionID(event);
|
|
2111
2817
|
if (!sessionID) return;
|
|
2112
2818
|
const sessions = sessionManager.listByOpenCodeSession(sessionID);
|
|
2819
|
+
for (const session of sessions) completionNotifications?.clearSession(session.id);
|
|
2113
2820
|
await Promise.all(sessions.map((session) => cleanupDeletedSession(session.id)));
|
|
2114
2821
|
}
|
|
2115
2822
|
},
|
|
2116
|
-
|
|
2117
|
-
|
|
2823
|
+
"chat.message": async (_input, output) => {
|
|
2824
|
+
const injected = completionNotifications?.injectQueuedChatMessage(output) ?? output;
|
|
2825
|
+
if (injected !== output && injected && typeof injected === "object" && Array.isArray(injected.parts)) output.parts = injected.parts;
|
|
2826
|
+
},
|
|
2827
|
+
"tool": config.pty.enabled ? {
|
|
2828
|
+
...createPtyTools(config.pty.cleanupExitedPaneOnRead),
|
|
2118
2829
|
...config.pty.sudoPane === "hide" ? {} : { zellij_pty_request_sudo: requestSudoTool }
|
|
2119
2830
|
} : {}
|
|
2120
2831
|
};
|