opencode-zellij 0.0.9 → 0.0.11
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 +976 -180
- 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) {
|
|
442
515
|
const session = this.get(id);
|
|
443
|
-
|
|
444
|
-
session.
|
|
445
|
-
|
|
446
|
-
|
|
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) {
|
|
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
|
+
}
|
|
447
532
|
return session;
|
|
448
533
|
}
|
|
449
534
|
listByOpenCodeSession(openCodeSessionId) {
|
|
@@ -488,6 +573,105 @@ function buildCommandArgv(input, options = {}) {
|
|
|
488
573
|
];
|
|
489
574
|
}
|
|
490
575
|
//#endregion
|
|
576
|
+
//#region src/zellij/parse.ts
|
|
577
|
+
function numericProperty(object, keys) {
|
|
578
|
+
for (const key of keys) {
|
|
579
|
+
const value = object[key];
|
|
580
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
581
|
+
if (typeof value === "string") {
|
|
582
|
+
const parsed = Number(value);
|
|
583
|
+
if (Number.isInteger(parsed)) return parsed;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
function stringProperty$1(object, keys) {
|
|
588
|
+
for (const key of keys) {
|
|
589
|
+
const value = object[key];
|
|
590
|
+
if (typeof value === "string") return value;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function paneMatches(object, paneId) {
|
|
594
|
+
return numericProperty(object, [
|
|
595
|
+
"id",
|
|
596
|
+
"pane_id",
|
|
597
|
+
"paneId"
|
|
598
|
+
]) === paneId && object.is_plugin !== true;
|
|
599
|
+
}
|
|
600
|
+
function findPaneRecord(value, paneId) {
|
|
601
|
+
if (Array.isArray(value)) {
|
|
602
|
+
for (const item of value) {
|
|
603
|
+
const found = findPaneRecord(item, paneId);
|
|
604
|
+
if (found !== void 0) return found;
|
|
605
|
+
}
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
609
|
+
const object = value;
|
|
610
|
+
if (paneMatches(object, paneId)) return object;
|
|
611
|
+
for (const nested of Object.values(object)) {
|
|
612
|
+
const found = findPaneRecord(nested, paneId);
|
|
613
|
+
if (found !== void 0) return found;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function parseCurrentPaneTabId(listPanesJson, paneId) {
|
|
617
|
+
if (!paneId) return void 0;
|
|
618
|
+
const parsedPaneId = Number(paneId);
|
|
619
|
+
if (!Number.isInteger(parsedPaneId)) return void 0;
|
|
620
|
+
try {
|
|
621
|
+
const pane = findPaneRecord(JSON.parse(listPanesJson), parsedPaneId);
|
|
622
|
+
return pane ? numericProperty(pane, ["tab_id", "tabId"]) : void 0;
|
|
623
|
+
} catch (error) {
|
|
624
|
+
debug("parseCurrentPaneTabId failed", errorMessage(error));
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
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
|
+
}
|
|
643
|
+
function tabNameProperty(object, tabId) {
|
|
644
|
+
if (tabId === void 0) return void 0;
|
|
645
|
+
if (numericProperty(object, ["tab_id", "tabId"]) !== tabId) return void 0;
|
|
646
|
+
const name = stringProperty$1(object, ["name", "title"]);
|
|
647
|
+
return typeof name === "string" ? name : void 0;
|
|
648
|
+
}
|
|
649
|
+
function findTabName(value, tabId) {
|
|
650
|
+
if (Array.isArray(value)) {
|
|
651
|
+
for (const item of value) {
|
|
652
|
+
const found = findTabName(item, tabId);
|
|
653
|
+
if (found !== void 0) return found;
|
|
654
|
+
}
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
658
|
+
const object = value;
|
|
659
|
+
const name = tabNameProperty(object, tabId);
|
|
660
|
+
if (name !== void 0) return name;
|
|
661
|
+
for (const nested of Object.values(object)) {
|
|
662
|
+
const found = findTabName(nested, tabId);
|
|
663
|
+
if (found !== void 0) return found;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
function parseTabName(listTabsJson, tabId) {
|
|
667
|
+
try {
|
|
668
|
+
return findTabName(JSON.parse(listTabsJson), tabId);
|
|
669
|
+
} catch (error) {
|
|
670
|
+
debug("parseTabName failed", errorMessage(error));
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
//#endregion
|
|
491
675
|
//#region src/zellij/cli.ts
|
|
492
676
|
const execFileAsync$1 = promisify(execFile);
|
|
493
677
|
function zellijCommandArgs(actionArgs) {
|
|
@@ -529,50 +713,6 @@ function buildRenameTabActionArgs(title, options = {}) {
|
|
|
529
713
|
title
|
|
530
714
|
];
|
|
531
715
|
}
|
|
532
|
-
function numericProperty(object, keys) {
|
|
533
|
-
for (const key of keys) {
|
|
534
|
-
const value = object[key];
|
|
535
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
536
|
-
if (typeof value === "string") {
|
|
537
|
-
const parsed = Number(value);
|
|
538
|
-
if (Number.isInteger(parsed)) return parsed;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
function paneMatches(object, paneId) {
|
|
543
|
-
return numericProperty(object, [
|
|
544
|
-
"id",
|
|
545
|
-
"pane_id",
|
|
546
|
-
"paneId"
|
|
547
|
-
]) === paneId && object.is_plugin !== true;
|
|
548
|
-
}
|
|
549
|
-
function findPaneTabId(value, paneId) {
|
|
550
|
-
if (Array.isArray(value)) {
|
|
551
|
-
for (const item of value) {
|
|
552
|
-
const found = findPaneTabId(item, paneId);
|
|
553
|
-
if (found !== void 0) return found;
|
|
554
|
-
}
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
if (typeof value !== "object" || value === null) return void 0;
|
|
558
|
-
const object = value;
|
|
559
|
-
if (paneMatches(object, paneId)) return numericProperty(object, ["tab_id", "tabId"]);
|
|
560
|
-
for (const nested of Object.values(object)) {
|
|
561
|
-
const found = findPaneTabId(nested, paneId);
|
|
562
|
-
if (found !== void 0) return found;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
function parseCurrentPaneTabId(listPanesJson, paneId) {
|
|
566
|
-
if (!paneId) return void 0;
|
|
567
|
-
const parsedPaneId = Number(paneId);
|
|
568
|
-
if (!Number.isInteger(parsedPaneId)) return void 0;
|
|
569
|
-
try {
|
|
570
|
-
return findPaneTabId(JSON.parse(listPanesJson), parsedPaneId);
|
|
571
|
-
} catch (error) {
|
|
572
|
-
debug("parseCurrentPaneTabId failed", errorMessage(error));
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
716
|
function ensureZellijTarget() {
|
|
577
717
|
if (process.env.ZELLIJ || process.env.ZELLIJ_SESSION_NAME) return;
|
|
578
718
|
throw new Error("Zellij context not found. Run OpenCode inside Zellij or set ZELLIJ_SESSION_NAME to an existing session.");
|
|
@@ -641,11 +781,20 @@ var ZellijCli = class {
|
|
|
641
781
|
if (!paneId) return void 0;
|
|
642
782
|
return parseCurrentPaneTabId((await runZellij(zellijActionArgs("list-panes", ["--json"]), { timeoutMs: 5e3 })).stdout, paneId);
|
|
643
783
|
}
|
|
784
|
+
async paneExists(paneId) {
|
|
785
|
+
return parsePaneExists((await runZellij(zellijActionArgs("list-panes", ["--json"]), { timeoutMs: 5e3 })).stdout, paneId);
|
|
786
|
+
}
|
|
644
787
|
async renameTab(title) {
|
|
645
788
|
const tabId = await this.currentPaneTabId();
|
|
646
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>"}`);
|
|
647
790
|
await runZellij(tabId === void 0 ? buildRenameTabActionArgs(title) : buildRenameTabActionArgs(title, { tabId }));
|
|
648
791
|
}
|
|
792
|
+
async currentTabTitle() {
|
|
793
|
+
if (!process.env.ZELLIJ_PANE_ID) return void 0;
|
|
794
|
+
const tabId = await this.currentPaneTabId();
|
|
795
|
+
if (tabId === void 0) return void 0;
|
|
796
|
+
return parseTabName((await runZellij(zellijActionArgs("list-tabs", ["--json"]), { timeoutMs: 5e3 })).stdout, tabId);
|
|
797
|
+
}
|
|
649
798
|
};
|
|
650
799
|
const zellijCli = new ZellijCli();
|
|
651
800
|
//#endregion
|
|
@@ -948,11 +1097,25 @@ function extractRenderedLines(event) {
|
|
|
948
1097
|
var SubscriberManager = class {
|
|
949
1098
|
subscribers = /* @__PURE__ */ new Map();
|
|
950
1099
|
startingSessions = /* @__PURE__ */ new Map();
|
|
951
|
-
|
|
1100
|
+
spawnProcess;
|
|
1101
|
+
dumpScreen;
|
|
1102
|
+
closePane;
|
|
1103
|
+
lifecycleHooks;
|
|
1104
|
+
terminalTailLines;
|
|
1105
|
+
constructor(sessions, maxBufferLines = Number(process.env.PTY_MAX_BUFFER_LINES ?? 5e4), dependencies = {}) {
|
|
952
1106
|
this.sessions = sessions;
|
|
953
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;
|
|
954
1116
|
}
|
|
955
1117
|
async start(session) {
|
|
1118
|
+
if ((this.sessions.find(session.id) ?? session).status === "terminal") return;
|
|
956
1119
|
if (this.subscribers.get(session.id)?.child) return;
|
|
957
1120
|
const inProgress = this.startingSessions.get(session.id);
|
|
958
1121
|
if (inProgress) return inProgress;
|
|
@@ -976,7 +1139,7 @@ var SubscriberManager = class {
|
|
|
976
1139
|
lastExitedAt: null
|
|
977
1140
|
};
|
|
978
1141
|
if (!existing) this.subscribers.set(session.id, state);
|
|
979
|
-
const child =
|
|
1142
|
+
const child = this.spawnProcess("zellij", zellijCommandArgs([
|
|
980
1143
|
"subscribe",
|
|
981
1144
|
"--pane-id",
|
|
982
1145
|
session.paneId,
|
|
@@ -1003,7 +1166,7 @@ var SubscriberManager = class {
|
|
|
1003
1166
|
child.on("exit", () => this.handleSubscriberExit(session.id, child));
|
|
1004
1167
|
child.on("error", (error) => this.handleSubscriberError(session.id, child, error));
|
|
1005
1168
|
if (!existing) try {
|
|
1006
|
-
const snapshot = await
|
|
1169
|
+
const snapshot = await this.dumpScreen(session.paneId);
|
|
1007
1170
|
if (this.subscribers.get(session.id) !== state || state.child !== child) return;
|
|
1008
1171
|
state.buffer.appendSnapshot(snapshot);
|
|
1009
1172
|
this.sessions.updateLineCount(session.id, state.buffer.lineCount);
|
|
@@ -1024,7 +1187,8 @@ var SubscriberManager = class {
|
|
|
1024
1187
|
return {
|
|
1025
1188
|
hasBuffer: Boolean(state),
|
|
1026
1189
|
active: Boolean(state?.child),
|
|
1027
|
-
lastExitedAt: state?.lastExitedAt ?? null
|
|
1190
|
+
lastExitedAt: state?.lastExitedAt ?? null,
|
|
1191
|
+
terminal: this.sessions.find(sessionId)?.status === "terminal"
|
|
1028
1192
|
};
|
|
1029
1193
|
}
|
|
1030
1194
|
stderr(sessionId) {
|
|
@@ -1044,13 +1208,14 @@ var SubscriberManager = class {
|
|
|
1044
1208
|
stopAll() {
|
|
1045
1209
|
for (const sessionId of this.subscribers.keys()) this.forget(sessionId);
|
|
1046
1210
|
}
|
|
1047
|
-
async closeSessionPane(sessionId) {
|
|
1211
|
+
async closeSessionPane(sessionId, options = {}) {
|
|
1048
1212
|
const session = this.sessions.get(sessionId);
|
|
1049
1213
|
this.stop(sessionId);
|
|
1050
1214
|
try {
|
|
1051
|
-
await
|
|
1215
|
+
await this.closePane(session.paneId);
|
|
1052
1216
|
} catch (error) {
|
|
1053
1217
|
debug("closePane failed", errorMessage(error));
|
|
1218
|
+
if (options.throwOnFailure) throw error;
|
|
1054
1219
|
}
|
|
1055
1220
|
}
|
|
1056
1221
|
handleStdout(sessionId, child, chunk) {
|
|
@@ -1088,11 +1253,8 @@ var SubscriberManager = class {
|
|
|
1088
1253
|
if (paneId && paneId !== session.paneId) return;
|
|
1089
1254
|
const type = eventType(event);
|
|
1090
1255
|
if (type === "pane_closed" || type === "PaneClosed") {
|
|
1091
|
-
|
|
1092
|
-
this.sessions.updateLineCount(sessionId, state.buffer.lineCount);
|
|
1093
|
-
this.sessions.updateStatus(sessionId, session.status === "killed" ? "killed" : "exited");
|
|
1256
|
+
this.markSessionTerminal(sessionId, "pane_closed");
|
|
1094
1257
|
unregisterPaneFromWatchdog(sessionId);
|
|
1095
|
-
this.stop(sessionId);
|
|
1096
1258
|
return;
|
|
1097
1259
|
}
|
|
1098
1260
|
const lines = extractRenderedLines(event);
|
|
@@ -1107,7 +1269,7 @@ var SubscriberManager = class {
|
|
|
1107
1269
|
for (const line of lines) {
|
|
1108
1270
|
const marker = parseExitCodeMarker(line);
|
|
1109
1271
|
if (!marker || marker.token !== session.exitCodeToken) continue;
|
|
1110
|
-
this.
|
|
1272
|
+
this.markSessionTerminal(sessionId, "exit_marker", { exitCode: marker.exitCode });
|
|
1111
1273
|
return;
|
|
1112
1274
|
}
|
|
1113
1275
|
}
|
|
@@ -1123,7 +1285,7 @@ var SubscriberManager = class {
|
|
|
1123
1285
|
if (state.child !== child) return;
|
|
1124
1286
|
state.child = null;
|
|
1125
1287
|
state.lastExitedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1126
|
-
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.`);
|
|
1127
1289
|
if (state.stderr.length > maxStderrLines) state.stderr = state.stderr.slice(state.stderr.length - maxStderrLines);
|
|
1128
1290
|
}
|
|
1129
1291
|
handleSubscriberError(sessionId, child, error) {
|
|
@@ -1135,6 +1297,29 @@ var SubscriberManager = class {
|
|
|
1135
1297
|
this.sessions.updateStatus(sessionId, "unknown");
|
|
1136
1298
|
}
|
|
1137
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
|
+
}
|
|
1138
1323
|
};
|
|
1139
1324
|
const subscriberManager = new SubscriberManager(sessionManager);
|
|
1140
1325
|
//#endregion
|
|
@@ -1147,14 +1332,21 @@ function publicSession(session) {
|
|
|
1147
1332
|
command: session.command,
|
|
1148
1333
|
args: session.args,
|
|
1149
1334
|
cwd: session.cwd,
|
|
1150
|
-
status: session.status,
|
|
1335
|
+
status: session.status === "terminal" ? "exited" : session.status,
|
|
1151
1336
|
lineCount: session.lineCount,
|
|
1152
1337
|
createdAt: session.createdAt,
|
|
1153
1338
|
updatedAt: session.updatedAt,
|
|
1154
1339
|
agentWritable: session.allowAgentInput,
|
|
1155
1340
|
humanInputOnly: session.humanInputOnly,
|
|
1156
1341
|
exitCode: session.exitCode,
|
|
1157
|
-
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
|
|
1158
1350
|
};
|
|
1159
1351
|
}
|
|
1160
1352
|
function nextAdvice(retryable, reason) {
|
|
@@ -1211,53 +1403,90 @@ function outputMatches(sessionId, grep, ignoreCase) {
|
|
|
1211
1403
|
}).returned > 0;
|
|
1212
1404
|
}
|
|
1213
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
|
|
1214
1437
|
//#region src/tools/kill.ts
|
|
1215
1438
|
const schema$4 = tool.schema;
|
|
1216
|
-
function
|
|
1217
|
-
|
|
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);
|
|
1218
1467
|
}
|
|
1219
1468
|
const zellijPtyKillTool = tool({
|
|
1220
1469
|
description: "Terminate a known Zellij PTY session by sending Ctrl-C, then closing its pane.",
|
|
1221
1470
|
args: { id: schema$4.string().describe("zellij-pty session id.") },
|
|
1222
1471
|
async execute(args) {
|
|
1223
|
-
|
|
1224
|
-
const warnings = [];
|
|
1225
|
-
const output = subscriberManager.has(session.id) ? readOutputSnapshot(session.id) : void 0;
|
|
1226
|
-
try {
|
|
1227
|
-
await zellijCli.sendCtrlC(session.paneId);
|
|
1228
|
-
await setTimeout$1(500);
|
|
1229
|
-
} catch (error) {
|
|
1230
|
-
warnings.push(`Ctrl-C failed or pane was already gone: ${error instanceof Error ? error.message : String(error)}`);
|
|
1231
|
-
}
|
|
1232
|
-
try {
|
|
1233
|
-
await zellijCli.closePane(session.paneId);
|
|
1234
|
-
} catch (error) {
|
|
1235
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1236
|
-
warnings.push(`close-pane failed: ${message}`);
|
|
1237
|
-
if (!closeFailureMeansGone(message)) return jsonResponse({
|
|
1238
|
-
killed: false,
|
|
1239
|
-
cleanedUp: false,
|
|
1240
|
-
session: publicSession(sessionManager.updateStatus(session.id, "unknown")),
|
|
1241
|
-
output,
|
|
1242
|
-
next: nextAdvice(true, "close-pane failed and the pane may still be running; the session was kept so kill can be retried."),
|
|
1243
|
-
warnings
|
|
1244
|
-
});
|
|
1245
|
-
}
|
|
1246
|
-
subscriberManager.stop(session.id);
|
|
1247
|
-
subscriberManager.forget(session.id);
|
|
1248
|
-
unregisterPaneFromWatchdog(session.id);
|
|
1249
|
-
sessionManager.remove(session.id);
|
|
1250
|
-
return jsonResponse({
|
|
1251
|
-
killed: true,
|
|
1252
|
-
cleanedUp: true,
|
|
1253
|
-
id: session.id,
|
|
1254
|
-
paneId: session.paneId,
|
|
1255
|
-
output,
|
|
1256
|
-
next: nextAdvice(false, "Session was closed and removed from the in-memory registry."),
|
|
1257
|
-
warnings
|
|
1258
|
-
});
|
|
1472
|
+
return jsonResponse(await executeZellijPtyKill(args));
|
|
1259
1473
|
}
|
|
1260
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
|
+
}
|
|
1261
1490
|
//#endregion
|
|
1262
1491
|
//#region src/tools/list.ts
|
|
1263
1492
|
const zellijPtyListTool = tool({
|
|
@@ -1273,55 +1502,123 @@ const zellijPtyListTool = tool({
|
|
|
1273
1502
|
//#endregion
|
|
1274
1503
|
//#region src/tools/read.ts
|
|
1275
1504
|
const schema$3 = tool.schema;
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
if (!statusAfterStart.active) {
|
|
1305
|
-
warnings.push("Subscriber is inactive; returned output may be stale.");
|
|
1306
|
-
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
|
|
1307
1533
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
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
|
+
};
|
|
1322
1590
|
}
|
|
1323
|
-
|
|
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
|
+
}
|
|
1324
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.";
|
|
1325
1622
|
if (status === "running") return "Session is still running; read again later if more output is expected.";
|
|
1326
1623
|
if (status === "unknown") return "Session state is unknown because the subscriber is inactive; output may be stale, but retrying read may restart observation.";
|
|
1327
1624
|
return "Session is no longer running.";
|
|
@@ -1616,6 +1913,395 @@ const zellijPtyWriteTool = tool({
|
|
|
1616
1913
|
}
|
|
1617
1914
|
});
|
|
1618
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
|
|
1619
2305
|
//#region src/zellij/shutdown-cleanup.ts
|
|
1620
2306
|
let registered = false;
|
|
1621
2307
|
let cleanedUp = false;
|
|
@@ -1712,25 +2398,18 @@ function shouldReadInitialBranch(zellij) {
|
|
|
1712
2398
|
return Boolean(zellij);
|
|
1713
2399
|
}
|
|
1714
2400
|
function handleTabTitleEvent(tabTitleManager, event) {
|
|
2401
|
+
if (event.type === "server.instance.disposed" || event.type === "global.disposed") return tabTitleManager.destroy?.();
|
|
1715
2402
|
if (!isRecord(event.properties)) return;
|
|
1716
2403
|
const properties = event.properties;
|
|
1717
2404
|
switch (event.type) {
|
|
1718
2405
|
case "session.status": {
|
|
1719
2406
|
const sessionID = stringProperty(properties, "sessionID");
|
|
1720
2407
|
const status = sessionStatusProperty(properties);
|
|
1721
|
-
if (sessionID && status) tabTitleManager.updateSessionStatus(sessionID, status);
|
|
1722
|
-
break;
|
|
1723
|
-
}
|
|
1724
|
-
case "session.idle": {
|
|
1725
|
-
const sessionID = stringProperty(properties, "sessionID");
|
|
1726
|
-
if (sessionID) tabTitleManager.markSessionIdle(sessionID);
|
|
1727
|
-
break;
|
|
1728
|
-
}
|
|
1729
|
-
case "session.error": {
|
|
1730
|
-
const sessionID = stringProperty(properties, "sessionID");
|
|
1731
|
-
if (sessionID) tabTitleManager.markSessionIdle(sessionID);
|
|
2408
|
+
if (sessionID && status && status.type !== "idle") tabTitleManager.updateSessionStatus(sessionID, status);
|
|
1732
2409
|
break;
|
|
1733
2410
|
}
|
|
2411
|
+
case "session.idle": break;
|
|
2412
|
+
case "session.error": break;
|
|
1734
2413
|
case "vcs.branch.updated":
|
|
1735
2414
|
tabTitleManager.setBranch(stringProperty(properties, "branch"));
|
|
1736
2415
|
break;
|
|
@@ -1738,22 +2417,32 @@ function handleTabTitleEvent(tabTitleManager, event) {
|
|
|
1738
2417
|
case "permission.asked": {
|
|
1739
2418
|
const id = inputRequestID(properties);
|
|
1740
2419
|
const sessionID = stringProperty(properties, "sessionID");
|
|
1741
|
-
if (id && sessionID)
|
|
2420
|
+
if (id && sessionID) {
|
|
2421
|
+
tabTitleManager.markNeedsInput(id, sessionID);
|
|
2422
|
+
tabTitleManager.updateSessionStatus(sessionID, { type: "busy" });
|
|
2423
|
+
}
|
|
1742
2424
|
break;
|
|
1743
2425
|
}
|
|
1744
2426
|
case "permission.updated": {
|
|
1745
2427
|
const id = inputRequestID(properties);
|
|
1746
2428
|
const sessionID = stringProperty(properties, "sessionID");
|
|
1747
2429
|
const state = inputState(properties);
|
|
1748
|
-
if (id && isResolvedInputState(state))
|
|
1749
|
-
|
|
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
|
+
}
|
|
1750
2437
|
break;
|
|
1751
2438
|
}
|
|
1752
2439
|
case "question.replied":
|
|
1753
2440
|
case "question.rejected":
|
|
1754
2441
|
case "permission.replied": {
|
|
1755
2442
|
const id = inputRequestID(properties);
|
|
2443
|
+
const sessionID = stringProperty(properties, "sessionID");
|
|
1756
2444
|
if (id) tabTitleManager.clearNeedsInput(id);
|
|
2445
|
+
if (sessionID) tabTitleManager.updateSessionStatus(sessionID, { type: "busy" });
|
|
1757
2446
|
break;
|
|
1758
2447
|
}
|
|
1759
2448
|
case "session.deleted": {
|
|
@@ -1761,10 +2450,6 @@ function handleTabTitleEvent(tabTitleManager, event) {
|
|
|
1761
2450
|
if (sessionID) tabTitleManager.removeSession(sessionID);
|
|
1762
2451
|
break;
|
|
1763
2452
|
}
|
|
1764
|
-
case "server.instance.disposed":
|
|
1765
|
-
case "global.disposed":
|
|
1766
|
-
tabTitleManager.destroy?.();
|
|
1767
|
-
break;
|
|
1768
2453
|
}
|
|
1769
2454
|
}
|
|
1770
2455
|
//#endregion
|
|
@@ -1795,6 +2480,7 @@ var TabTitleManager = class {
|
|
|
1795
2480
|
retryTimer;
|
|
1796
2481
|
retryAttempt = 0;
|
|
1797
2482
|
syncInFlight = false;
|
|
2483
|
+
syncPromise;
|
|
1798
2484
|
debounceMs;
|
|
1799
2485
|
retryInitialMs;
|
|
1800
2486
|
retryMaxMs;
|
|
@@ -1803,6 +2489,10 @@ var TabTitleManager = class {
|
|
|
1803
2489
|
emojis;
|
|
1804
2490
|
enabled;
|
|
1805
2491
|
destroyed = false;
|
|
2492
|
+
originalTabTitle;
|
|
2493
|
+
originalTabTitleLoaded = false;
|
|
2494
|
+
originalTabTitlePromise;
|
|
2495
|
+
destroyPromise;
|
|
1806
2496
|
constructor(options) {
|
|
1807
2497
|
this.projectName = options.projectName;
|
|
1808
2498
|
this.branchName = options.branchName?.trim() || void 0;
|
|
@@ -1822,6 +2512,27 @@ var TabTitleManager = class {
|
|
|
1822
2512
|
this.branchName = trimmed;
|
|
1823
2513
|
this.scheduleUpdate();
|
|
1824
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
|
+
}
|
|
1825
2536
|
updateSessionStatus(sessionID, status) {
|
|
1826
2537
|
const activity = status.type === "idle" ? "idle" : "running";
|
|
1827
2538
|
if (this.sessionStatuses.get(sessionID) === activity) return;
|
|
@@ -1875,6 +2586,8 @@ var TabTitleManager = class {
|
|
|
1875
2586
|
}
|
|
1876
2587
|
async renderImmediate() {
|
|
1877
2588
|
if (!this.enabled || this.destroyed) return;
|
|
2589
|
+
await this.ensureOriginalTabTitle();
|
|
2590
|
+
if (this.destroyed) return;
|
|
1878
2591
|
this.desiredTitle = this.buildTitle();
|
|
1879
2592
|
this.clearDebounceTimer();
|
|
1880
2593
|
await this.syncDesiredTitle();
|
|
@@ -1895,8 +2608,14 @@ var TabTitleManager = class {
|
|
|
1895
2608
|
}
|
|
1896
2609
|
async syncDesiredTitle() {
|
|
1897
2610
|
if (!this.enabled || this.destroyed) return;
|
|
1898
|
-
|
|
2611
|
+
await this.ensureOriginalTabTitle();
|
|
2612
|
+
if (this.destroyed) return;
|
|
2613
|
+
if (this.syncInFlight) return this.syncPromise;
|
|
1899
2614
|
this.syncInFlight = true;
|
|
2615
|
+
this.syncPromise = this.runTitleSync();
|
|
2616
|
+
return this.syncPromise;
|
|
2617
|
+
}
|
|
2618
|
+
async runTitleSync() {
|
|
1900
2619
|
try {
|
|
1901
2620
|
while (this.desiredTitle && this.desiredTitle !== this.lastSyncedTitle) {
|
|
1902
2621
|
const title = this.desiredTitle;
|
|
@@ -1913,6 +2632,7 @@ var TabTitleManager = class {
|
|
|
1913
2632
|
}
|
|
1914
2633
|
} finally {
|
|
1915
2634
|
this.syncInFlight = false;
|
|
2635
|
+
this.syncPromise = void 0;
|
|
1916
2636
|
}
|
|
1917
2637
|
}
|
|
1918
2638
|
scheduleRetry() {
|
|
@@ -1936,21 +2656,52 @@ var TabTitleManager = class {
|
|
|
1936
2656
|
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
1937
2657
|
this.debounceTimer = void 0;
|
|
1938
2658
|
}
|
|
2659
|
+
async ensureOriginalTabTitle() {
|
|
2660
|
+
if (!this.enabled || this.originalTabTitleLoaded) return;
|
|
2661
|
+
if (this.originalTabTitlePromise) return this.originalTabTitlePromise;
|
|
2662
|
+
this.originalTabTitlePromise = this.saveOriginalTabTitle();
|
|
2663
|
+
return this.originalTabTitlePromise;
|
|
2664
|
+
}
|
|
2665
|
+
async saveOriginalTabTitle() {
|
|
2666
|
+
try {
|
|
2667
|
+
const title = await this.cli.currentTabTitle();
|
|
2668
|
+
if (title !== void 0) this.originalTabTitle = title;
|
|
2669
|
+
} catch (error) {
|
|
2670
|
+
debug("TabTitleManager failed to save original tab title", errorMessage(error));
|
|
2671
|
+
} finally {
|
|
2672
|
+
this.originalTabTitleLoaded = true;
|
|
2673
|
+
this.originalTabTitlePromise = void 0;
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
1939
2676
|
destroy() {
|
|
2677
|
+
if (this.destroyed) return this.destroyPromise ?? Promise.resolve();
|
|
1940
2678
|
this.destroyed = true;
|
|
1941
2679
|
this.clearDebounceTimer();
|
|
1942
2680
|
this.clearRetryTimer();
|
|
2681
|
+
if (!this.enabled) return Promise.resolve();
|
|
2682
|
+
this.destroyPromise = this.restoreOriginalTabTitle().catch((error) => debug("TabTitleManager failed to restore original tab title", errorMessage(error)));
|
|
2683
|
+
return this.destroyPromise;
|
|
2684
|
+
}
|
|
2685
|
+
async restoreOriginalTabTitle() {
|
|
2686
|
+
await this.originalTabTitlePromise;
|
|
2687
|
+
await this.syncPromise;
|
|
2688
|
+
const originalTitle = this.originalTabTitle;
|
|
2689
|
+
this.originalTabTitle = void 0;
|
|
2690
|
+
if (originalTitle === void 0) return;
|
|
2691
|
+
await this.cli.renameTab(originalTitle);
|
|
1943
2692
|
}
|
|
1944
2693
|
};
|
|
1945
2694
|
//#endregion
|
|
1946
2695
|
//#region src/plugin.ts
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
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
|
+
}
|
|
1954
2705
|
function getProjectName(path) {
|
|
1955
2706
|
return path.split(/[/\\]/).filter(Boolean).pop() || "opencode";
|
|
1956
2707
|
}
|
|
@@ -2014,22 +2765,67 @@ function createZellijPtyPlugin(dependencies = {}) {
|
|
|
2014
2765
|
branch: config.tabTitle.emojiBranch
|
|
2015
2766
|
}
|
|
2016
2767
|
}) : void 0;
|
|
2017
|
-
tabTitleManager?.renderImmediate().catch((error) => debug("initial tab title render failed", errorMessage(error)));
|
|
2018
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
|
+
await tabTitleSnapshotRefresher?.refreshNow();
|
|
2800
|
+
tabTitleManager?.renderImmediate().catch((error) => debug("initial tab title render failed", errorMessage(error)));
|
|
2019
2801
|
if (config.autoUpdate) (dependencies.startAutoUpdateCheck ?? startAutoUpdateCheck)(client, dependencies.importMetaUrl ?? import.meta.url);
|
|
2020
2802
|
return {
|
|
2021
2803
|
async event(input) {
|
|
2022
2804
|
const event = input.event;
|
|
2023
|
-
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
|
+
}
|
|
2024
2815
|
if (event.type === "session.deleted") {
|
|
2025
2816
|
const sessionID = deletedSessionID(event);
|
|
2026
2817
|
if (!sessionID) return;
|
|
2027
2818
|
const sessions = sessionManager.listByOpenCodeSession(sessionID);
|
|
2819
|
+
for (const session of sessions) completionNotifications?.clearSession(session.id);
|
|
2028
2820
|
await Promise.all(sessions.map((session) => cleanupDeletedSession(session.id)));
|
|
2029
2821
|
}
|
|
2030
2822
|
},
|
|
2031
|
-
|
|
2032
|
-
|
|
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),
|
|
2033
2829
|
...config.pty.sudoPane === "hide" ? {} : { zellij_pty_request_sudo: requestSudoTool }
|
|
2034
2830
|
} : {}
|
|
2035
2831
|
};
|