Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit 7673dee

Browse files
committed
Merge PR tintinweb#169: hide inactive FleetView roster
1 parent 9c78a93 commit 7673dee

2 files changed

Lines changed: 177 additions & 47 deletions

File tree

src/ui/fleet-list.ts

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export type FleetUICtx = {
4646
type MainEntry = { kind: "main" };
4747
type AgentEntry = { kind: "agent"; record: AgentRecord };
4848
type FleetEntry = MainEntry | AgentEntry;
49+
type FleetTUI = { focusedComponent?: unknown; requestRender(force?: boolean): void };
4950

5051
/** `11s` — integer seconds, no decimal/suffix (matches Claude Code, unlike formatMs). */
5152
export function formatFleetElapsed(ms: number): string {
@@ -76,8 +77,9 @@ function rightAlign(left: string, right: string, width: number): string {
7677

7778
export class FleetList {
7879
private ui: FleetUICtx | undefined;
79-
private tui: any | undefined;
80+
private tui: FleetTUI | undefined;
8081
private inputUnsub: (() => void) | undefined;
82+
private pendingOpen: ReturnType<typeof setImmediate> | undefined;
8183
private widgetRegistered = false;
8284
private timer: ReturnType<typeof setInterval> | undefined;
8385

@@ -89,6 +91,7 @@ export class FleetList {
8991
/** Set while a conversation overlay is open; calling it closes the overlay. */
9092
private viewerClose: (() => void) | undefined;
9193
private viewingAgentId: string | undefined;
94+
private restoreActiveAfterViewer = false;
9295

9396
constructor(
9497
private manager: AgentManager,
@@ -100,13 +103,17 @@ export class FleetList {
100103
setEnabled(enabled: boolean): void {
101104
if (enabled === this.enabled) return;
102105
this.enabled = enabled;
103-
if (!enabled) this.active = false;
106+
if (!enabled) {
107+
this.active = false;
108+
this.cancelPendingOpen();
109+
}
104110
this.update();
105111
}
106112

107113
/** Capture the UI context and (re)register the global input handler. */
108114
setUICtx(ui: FleetUICtx): void {
109115
if (ui === this.ui) return;
116+
this.cancelPendingOpen();
110117
this.inputUnsub?.();
111118
this.ui = ui;
112119
this.widgetRegistered = false;
@@ -128,6 +135,7 @@ export class FleetList {
128135
}
129136

130137
dispose(): void {
138+
this.cancelPendingOpen();
131139
if (this.timer) { clearInterval(this.timer); this.timer = undefined; }
132140
this.inputUnsub?.();
133141
this.inputUnsub = undefined;
@@ -293,30 +301,48 @@ export class FleetList {
293301
}
294302
const session = record.session;
295303
const activity = this.agentActivity.get(record.id);
304+
const ui = this.ui;
305+
this.cancelPendingOpen();
306+
this.restoreActiveAfterViewer = this.active;
296307
this.viewingAgentId = record.id;
308+
this.deactivate();
309+
this.tui?.requestRender(true);
310+
311+
this.pendingOpen = setImmediate(() => {
312+
this.pendingOpen = undefined;
313+
if (this.ui !== ui || this.viewerClose || this.viewingAgentId !== record.id) return;
314+
315+
void ui.custom<undefined>(
316+
(tui, theme, keybindings, done) => {
317+
this.viewerClose = () => done(undefined);
318+
return new ConversationViewer(
319+
tui,
320+
session,
321+
record,
322+
activity,
323+
theme,
324+
done,
325+
() => {
326+
if (this.manager.abort(record.id)) this.ui?.notify(`Stopped "${record.description}".`, "info");
327+
},
328+
keybindings,
329+
(message: string) => this.manager.steer(record.id, message),
330+
);
331+
},
332+
{
333+
overlay: true,
334+
overlayOptions: { anchor: "center", width: "90%", maxHeight: `${VIEWPORT_HEIGHT_PCT}%` },
335+
},
336+
).then(() => this.clearViewer(), () => this.clearViewer());
337+
});
338+
}
297339

298-
void this.ui.custom<undefined>(
299-
(tui, theme, keybindings, done) => {
300-
this.viewerClose = () => done(undefined);
301-
return new ConversationViewer(
302-
tui,
303-
session,
304-
record,
305-
activity,
306-
theme,
307-
done,
308-
() => {
309-
if (this.manager.abort(record.id)) this.ui?.notify(`Stopped "${record.description}".`, "info");
310-
},
311-
keybindings,
312-
(message: string) => this.manager.steer(record.id, message),
313-
);
314-
},
315-
{
316-
overlay: true,
317-
overlayOptions: { anchor: "center", width: "90%", maxHeight: `${VIEWPORT_HEIGHT_PCT}%` },
318-
},
319-
).then(() => this.clearViewer(), () => this.clearViewer());
340+
private cancelPendingOpen(): void {
341+
if (!this.pendingOpen) return;
342+
clearImmediate(this.pendingOpen);
343+
this.pendingOpen = undefined;
344+
this.viewingAgentId = undefined;
345+
this.restoreActiveAfterViewer = false;
320346
}
321347

322348
/** Reset overlay state and return to the list (on close, auto-close, or error). */
@@ -331,21 +357,22 @@ export class FleetList {
331357
}
332358
this.viewerClose = undefined;
333359
this.viewingAgentId = undefined;
360+
this.active = this.restoreActiveAfterViewer;
361+
this.restoreActiveAfterViewer = false;
334362
this.update();
335363
}
336364

337365
// ---- Rendering ----
338366

339367
private renderBar(width: number, theme: Theme): string[] {
368+
if (!this.active || this.viewerClose || !this.editorHasFocus()) return [];
340369
const agents = this.roster().slice(1) as AgentEntry[];
341370
if (agents.length === 0) return [];
342371
// Clamp locally so a render between a roster shrink and the next update()
343372
// (e.g. on terminal resize) never loses the selection marker.
344373
const sel = Math.min(this.selectedIndex, agents.length);
345374

346-
const hint = this.active
347-
? "↑↓ select · enter view · esc back"
348-
: "esc to interrupt · ← for agents · ↓ to manage";
375+
const hint = "↑↓ select · enter view · esc back";
349376
const lines: string[] = [];
350377
lines.push(truncateToWidth(" " + theme.fg("dim", hint), width));
351378
lines.push("");

0 commit comments

Comments
 (0)