pi-better-subagents 0.1.18 → 0.1.20
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/index.ts +62 -7
- package/package.json +1 -1
- package/parse.ts +116 -1
- package/shared-navigator.ts +204 -40
package/index.ts
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
import { execSync } from "node:child_process";
|
|
15
15
|
import { writeFileSync, mkdirSync, statSync } from "node:fs";
|
|
16
16
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import * as PiCodingAgent from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import * as PiTui from "@earendil-works/pi-tui";
|
|
17
19
|
import { CustomEditor } from "@earendil-works/pi-coding-agent";
|
|
18
20
|
import { matchesKey, Key, truncateToWidth } from "@earendil-works/pi-tui";
|
|
19
21
|
import { Type } from "@earendil-works/pi-ai";
|
|
@@ -30,7 +32,7 @@ import {
|
|
|
30
32
|
type BackgroundWorkRow,
|
|
31
33
|
} from "./shared-navigator.ts";
|
|
32
34
|
import { spawnDetached, type SpawnResult } from "./spawn.ts";
|
|
33
|
-
import { parseRun,
|
|
35
|
+
import { parseRun, readRunTranscript, resetParseRunCursor, type Usage } from "./parse.ts";
|
|
34
36
|
import { finalizeRun as finalizeRunCore } from "./finalization.ts";
|
|
35
37
|
import { loadConfig, normalizeTools, resolveExtensionPath, SAFE_DEFAULT_TOOLS, SAFE_CLEAN_TOOLS, DEFAULT_MAX_CONCURRENT } from "./config.ts";
|
|
36
38
|
import { resolveExtensions, extensionArgs } from "./extensions.ts";
|
|
@@ -836,7 +838,8 @@ function mainAgentWorkRow(now: number): BackgroundWorkRow {
|
|
|
836
838
|
function subagentWorkDetail(id: string, now: number, options?: { logTailLines?: number }): BackgroundWorkDetail | null {
|
|
837
839
|
const detail = navigatorDetail(id, now);
|
|
838
840
|
if (!detail) return null;
|
|
839
|
-
|
|
841
|
+
void options;
|
|
842
|
+
const transcript = readRunTranscript(id);
|
|
840
843
|
const metadata = [
|
|
841
844
|
{ label: "provider", value: "Subagents" },
|
|
842
845
|
{ label: "model", value: detail.effort ? `${detail.model} · effort ${detail.effort}` : detail.model },
|
|
@@ -854,15 +857,65 @@ function subagentWorkDetail(id: string, now: number, options?: { logTailLines?:
|
|
|
854
857
|
statusTone: statusTone(detail.status),
|
|
855
858
|
subtitle: detail.currentTool ? `current tool ${detail.currentTool}` : undefined,
|
|
856
859
|
metadata,
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
// activity/result snapshot.
|
|
861
|
-
evidence: { label: "log tail", text: tailLog(id, logTailLines) },
|
|
860
|
+
evidence: { label: "transcript", text: detail.output || "(no transcript yet)" },
|
|
861
|
+
transcript: transcript.entries,
|
|
862
|
+
transcriptDiagnostic: transcript.diagnostic,
|
|
862
863
|
footerActions: [detail.status === "running" || detail.status === "orphaned" ? "x stop" : "x dismiss"],
|
|
863
864
|
};
|
|
864
865
|
}
|
|
865
866
|
|
|
867
|
+
function createSubagentTranscriptComponent(detail: BackgroundWorkDetail, theme: unknown) {
|
|
868
|
+
const ContainerComponent = (PiTui as any).Container;
|
|
869
|
+
const AssistantComponent = (PiCodingAgent as any).AssistantMessageComponent;
|
|
870
|
+
const ToolComponent = (PiCodingAgent as any).ToolExecutionComponent;
|
|
871
|
+
const markdownTheme = typeof (PiCodingAgent as any).getMarkdownTheme === "function"
|
|
872
|
+
? (PiCodingAgent as any).getMarkdownTheme()
|
|
873
|
+
: {};
|
|
874
|
+
if (!ContainerComponent || !AssistantComponent || !ToolComponent) {
|
|
875
|
+
return {
|
|
876
|
+
render: () => (detail.transcript ?? []).flatMap((entry) => entry.type === "assistant"
|
|
877
|
+
? entry.content.filter((block) => block.type === "text").flatMap((block) => String(block.text ?? "").split("\n"))
|
|
878
|
+
: [`[${entry.state}] ${entry.name}`]),
|
|
879
|
+
invalidate() {},
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
const container = new ContainerComponent();
|
|
883
|
+
const ui = { requestRender: () => { try { (uiCtx?.ui as any)?.requestRender?.(); } catch { /* ignore */ } } };
|
|
884
|
+
for (const entry of detail.transcript ?? []) {
|
|
885
|
+
if (entry.type === "assistant") {
|
|
886
|
+
const message = {
|
|
887
|
+
role: "assistant" as const,
|
|
888
|
+
content: entry.content,
|
|
889
|
+
stopReason: entry.streaming ? undefined : "stop",
|
|
890
|
+
timestamp: Date.now(),
|
|
891
|
+
};
|
|
892
|
+
const component = new AssistantComponent(message as any, true, markdownTheme, "Thinking...", 1);
|
|
893
|
+
component.updateContent(message as any, entry.streaming);
|
|
894
|
+
container.addChild(component);
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
const component = new ToolComponent(
|
|
898
|
+
entry.name,
|
|
899
|
+
entry.id ?? `transcript-${entry.name}`,
|
|
900
|
+
entry.args ?? {},
|
|
901
|
+
{ showImages: false },
|
|
902
|
+
undefined,
|
|
903
|
+
ui as any,
|
|
904
|
+
uiCtx?.cwd ?? process.cwd(),
|
|
905
|
+
);
|
|
906
|
+
component.markExecutionStarted();
|
|
907
|
+
component.setArgsComplete();
|
|
908
|
+
if (entry.state === "completed") {
|
|
909
|
+
const result = entry.result && typeof entry.result === "object"
|
|
910
|
+
? entry.result as any
|
|
911
|
+
: { content: entry.result == null ? [] : [{ type: "text", text: String(entry.result) }] };
|
|
912
|
+
component.updateResult({ ...result, isError: entry.isError });
|
|
913
|
+
}
|
|
914
|
+
container.addChild(component);
|
|
915
|
+
}
|
|
916
|
+
return container;
|
|
917
|
+
}
|
|
918
|
+
|
|
866
919
|
function ensureSubagentProvider(): void {
|
|
867
920
|
if (unregisterSubagentProvider) return;
|
|
868
921
|
const provider: BackgroundWorkProvider = {
|
|
@@ -870,6 +923,7 @@ function ensureSubagentProvider(): void {
|
|
|
870
923
|
label: "Subagents",
|
|
871
924
|
priority: 10,
|
|
872
925
|
visibleCount: () => navigatorRunningCount(),
|
|
926
|
+
showSection: (rows) => rows.some((row) => row.status === "running"),
|
|
873
927
|
parentRow: (now) => mainAgentWorkRow(now),
|
|
874
928
|
listRows: (now) => subagentWorkRows(now),
|
|
875
929
|
detail: (id, now, options) => subagentWorkDetail(id, now, options),
|
|
@@ -961,6 +1015,7 @@ function ensureNavigator(ctx: ExtensionContext): void {
|
|
|
961
1015
|
isOpenTrigger: (data: string) => matchesKey(data, Key.left),
|
|
962
1016
|
matchKey: (data: string, keyId: string) => matchesKey(data, keyId),
|
|
963
1017
|
truncate: truncateToWidth,
|
|
1018
|
+
createTranscriptComponent: createSubagentTranscriptComponent,
|
|
964
1019
|
});
|
|
965
1020
|
} catch { /* ignore */ }
|
|
966
1021
|
}
|
package/package.json
CHANGED
package/parse.ts
CHANGED
|
@@ -30,7 +30,7 @@ import { readBoundedTail, tailTerminalDisplay } from "./shared-log-utils.ts";
|
|
|
30
30
|
import { readAppendedLines, type LogCursor } from "./log-cursor.ts";
|
|
31
31
|
import { logPathFor } from "./registry.ts";
|
|
32
32
|
|
|
33
|
-
interface ContentBlock { type: string; text?: string; name?: string }
|
|
33
|
+
interface ContentBlock { type: string; text?: string; thinking?: string; name?: string }
|
|
34
34
|
interface Cost { total?: number }
|
|
35
35
|
interface MsgUsage { input?: number; output?: number; cacheRead?: number; cost?: Cost }
|
|
36
36
|
interface Msg { role?: string; content?: string | ContentBlock[]; usage?: MsgUsage }
|
|
@@ -130,6 +130,121 @@ export interface ParsedRun {
|
|
|
130
130
|
diagnostics: string[];
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
export type TranscriptEntry =
|
|
134
|
+
| { type: "assistant"; content: ContentBlock[]; streaming: boolean }
|
|
135
|
+
| {
|
|
136
|
+
type: "tool";
|
|
137
|
+
id?: string;
|
|
138
|
+
name: string;
|
|
139
|
+
args?: unknown;
|
|
140
|
+
result?: unknown;
|
|
141
|
+
isError: boolean;
|
|
142
|
+
state: "running" | "completed";
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export interface RunTranscript {
|
|
146
|
+
entries: TranscriptEntry[];
|
|
147
|
+
truncated: boolean;
|
|
148
|
+
diagnostic?: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const DEFAULT_TRANSCRIPT_TAIL_BYTES = 2 * 1024 * 1024;
|
|
152
|
+
const DEFAULT_TRANSCRIPT_ENTRIES = 40;
|
|
153
|
+
|
|
154
|
+
function transcriptContent(msg: Msg | undefined): ContentBlock[] {
|
|
155
|
+
if (!msg) return [];
|
|
156
|
+
if (typeof msg.content === "string") {
|
|
157
|
+
return msg.content.trim() ? [{ type: "text", text: msg.content }] : [];
|
|
158
|
+
}
|
|
159
|
+
if (!Array.isArray(msg.content)) return [];
|
|
160
|
+
return msg.content.filter((block) =>
|
|
161
|
+
block && (
|
|
162
|
+
(block.type === "text" && typeof block.text === "string" && block.text.trim()) ||
|
|
163
|
+
(block.type === "thinking" && typeof (block as { thinking?: unknown }).thinking === "string")
|
|
164
|
+
),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Bounded, display-oriented transcript fold for the subagent detail view. */
|
|
169
|
+
export function readRunTranscript(
|
|
170
|
+
id: string,
|
|
171
|
+
options: { maxBytes?: number; maxEntries?: number } = {},
|
|
172
|
+
): RunTranscript {
|
|
173
|
+
const maxBytes = Math.max(1024, options.maxBytes ?? DEFAULT_TRANSCRIPT_TAIL_BYTES);
|
|
174
|
+
const maxEntries = Math.max(1, options.maxEntries ?? DEFAULT_TRANSCRIPT_ENTRIES);
|
|
175
|
+
const tail = readTail(logPathFor(id), maxBytes);
|
|
176
|
+
if (tail.error) return { entries: [], truncated: false, diagnostic: `Log unreadable: ${tail.error}` };
|
|
177
|
+
|
|
178
|
+
const lines = tail.text.split(/\r?\n/);
|
|
179
|
+
if (tail.truncated) lines.shift();
|
|
180
|
+
const entries: TranscriptEntry[] = [];
|
|
181
|
+
const tools = new Map<string, Extract<TranscriptEntry, { type: "tool" }>>();
|
|
182
|
+
let anonymousTool = 0;
|
|
183
|
+
let liveAssistant: Extract<TranscriptEntry, { type: "assistant" }> | undefined;
|
|
184
|
+
|
|
185
|
+
for (const line of lines) {
|
|
186
|
+
const event = tryParse(line.trim());
|
|
187
|
+
if (!event) continue;
|
|
188
|
+
const type = event.type;
|
|
189
|
+
if (type === "message_end") {
|
|
190
|
+
const message = event.message as Msg | undefined;
|
|
191
|
+
if (message?.role !== "assistant") continue;
|
|
192
|
+
const content = transcriptContent(message);
|
|
193
|
+
if (content.length) entries.push({ type: "assistant", content, streaming: false });
|
|
194
|
+
liveAssistant = undefined;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (type === "message_update") {
|
|
198
|
+
const message = event.message as Msg | undefined;
|
|
199
|
+
if (message?.role !== "assistant") continue;
|
|
200
|
+
const content = transcriptContent(message);
|
|
201
|
+
if (content.length) liveAssistant = { type: "assistant", content, streaming: true };
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (type === "tool_execution_start") {
|
|
205
|
+
const idValue = typeof event.toolCallId === "string" ? event.toolCallId : undefined;
|
|
206
|
+
const key = idValue ?? `anonymous:${anonymousTool++}`;
|
|
207
|
+
const tool: Extract<TranscriptEntry, { type: "tool" }> = {
|
|
208
|
+
type: "tool",
|
|
209
|
+
id: idValue,
|
|
210
|
+
name: typeof event.toolName === "string" ? event.toolName : "unknown",
|
|
211
|
+
args: event.args,
|
|
212
|
+
isError: false,
|
|
213
|
+
state: "running",
|
|
214
|
+
};
|
|
215
|
+
entries.push(tool);
|
|
216
|
+
tools.set(key, tool);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (type === "tool_execution_end") {
|
|
220
|
+
const idValue = typeof event.toolCallId === "string" ? event.toolCallId : undefined;
|
|
221
|
+
let tool = idValue ? tools.get(idValue) : undefined;
|
|
222
|
+
if (!tool && typeof event.toolName === "string") {
|
|
223
|
+
tool = [...tools.values()].reverse().find((candidate) => candidate.name === event.toolName && candidate.state === "running");
|
|
224
|
+
}
|
|
225
|
+
if (!tool) {
|
|
226
|
+
tool = {
|
|
227
|
+
type: "tool",
|
|
228
|
+
id: idValue,
|
|
229
|
+
name: typeof event.toolName === "string" ? event.toolName : "unknown",
|
|
230
|
+
isError: event.isError === true,
|
|
231
|
+
state: "completed",
|
|
232
|
+
};
|
|
233
|
+
entries.push(tool);
|
|
234
|
+
}
|
|
235
|
+
tool.result = event.result;
|
|
236
|
+
tool.isError = event.isError === true;
|
|
237
|
+
tool.state = "completed";
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (liveAssistant) entries.push(liveAssistant);
|
|
241
|
+
return {
|
|
242
|
+
entries: entries.slice(-maxEntries),
|
|
243
|
+
truncated: tail.truncated || entries.length > maxEntries,
|
|
244
|
+
diagnostic: tail.truncated ? `Showing the latest ${fmtBytes(maxBytes)} of the transcript.` : undefined,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
133
248
|
/**
|
|
134
249
|
* Authoritative lifecycle evidence scanned from the complete NDJSON stream.
|
|
135
250
|
* Kept separate from parseRun()'s bounded tail so large-log result parsing stays
|
package/shared-navigator.ts
CHANGED
|
@@ -39,6 +39,11 @@ export type BackgroundWorkDetail = {
|
|
|
39
39
|
metadata: Array<{ label: string; value: string }>;
|
|
40
40
|
foldedSections?: Array<{ id: string; label: string; text: string; collapsedText?: string; expandedByDefault?: boolean }>;
|
|
41
41
|
evidence: { label: string; text: string };
|
|
42
|
+
transcript?: Array<
|
|
43
|
+
| { type: "assistant"; content: Array<{ type: string; text?: string; thinking?: string }>; streaming: boolean }
|
|
44
|
+
| { type: "tool"; id?: string; name: string; args?: unknown; result?: unknown; isError: boolean; state: "running" | "completed" }
|
|
45
|
+
>;
|
|
46
|
+
transcriptDiagnostic?: string;
|
|
42
47
|
footerActions?: string[];
|
|
43
48
|
};
|
|
44
49
|
|
|
@@ -54,6 +59,7 @@ export type BackgroundWorkProvider = {
|
|
|
54
59
|
label: string;
|
|
55
60
|
priority: number;
|
|
56
61
|
visibleCount(): number;
|
|
62
|
+
showSection?(rows: BackgroundWorkRow[], now: number): boolean;
|
|
57
63
|
parentRow?(now: number): BackgroundWorkRow | null;
|
|
58
64
|
listRows(now: number): BackgroundWorkRow[];
|
|
59
65
|
detail(id: string, now: number, options?: { logTailLines?: number }): BackgroundWorkDetail | null;
|
|
@@ -67,6 +73,7 @@ type HostDeps = {
|
|
|
67
73
|
isOpenTrigger: (data: string) => boolean;
|
|
68
74
|
matchKey: (data: string, keyId: string) => boolean;
|
|
69
75
|
truncate: (s: string, width: number) => string;
|
|
76
|
+
createTranscriptComponent?: (detail: BackgroundWorkDetail, theme: unknown) => Component;
|
|
70
77
|
};
|
|
71
78
|
|
|
72
79
|
type NavigatorState = {
|
|
@@ -76,6 +83,7 @@ type NavigatorState = {
|
|
|
76
83
|
deps?: HostDeps;
|
|
77
84
|
lastHint?: string | null;
|
|
78
85
|
lastMainListLines?: string[];
|
|
86
|
+
lastMainListSignature?: string;
|
|
79
87
|
mainListWidgetInstalled?: boolean;
|
|
80
88
|
mainListRequestRender?: () => void;
|
|
81
89
|
mainListSelectedId?: string;
|
|
@@ -83,6 +91,7 @@ type NavigatorState = {
|
|
|
83
91
|
mainListCloseArm?: { id: string; armedAt: number };
|
|
84
92
|
mainListCloseArmTimer?: ReturnType<typeof setTimeout>;
|
|
85
93
|
mainListDeadlineScheduler?: RenderScheduler;
|
|
94
|
+
editorComponent?: Component;
|
|
86
95
|
detailOverlayRows?: number;
|
|
87
96
|
dispose?: () => void;
|
|
88
97
|
};
|
|
@@ -99,11 +108,9 @@ export const CLOSE_ARM_MS = 3000;
|
|
|
99
108
|
export const DEFAULT_LOG_TAIL_ROWS = 10;
|
|
100
109
|
export const LOG_TAIL_ROW_CHOICES = [10, 25] as const;
|
|
101
110
|
const MAIN_LIST_FALLBACK_WIDTH = 100;
|
|
102
|
-
const
|
|
103
|
-
const DETAIL_OVERLAY_FOOTER_MARGIN_ROWS = 3;
|
|
111
|
+
const DETAIL_OVERLAY_FOOTER_ROWS = 3;
|
|
104
112
|
const EVIDENCE_SECTION_ID = "__evidence__";
|
|
105
113
|
const RUNNING_DOT_GLYPH = "●";
|
|
106
|
-
const RUNNING_DOT_FRAMES = ["dim", "accent", "accent", "dim"] as const;
|
|
107
114
|
|
|
108
115
|
function state(): NavigatorState {
|
|
109
116
|
const g = globalThis as typeof globalThis & { [GLOBAL_KEY]?: NavigatorState };
|
|
@@ -192,6 +199,7 @@ export function disposeBackgroundWorkNavigator(ctx?: ExtensionContext): void {
|
|
|
192
199
|
}
|
|
193
200
|
s.lastHint = undefined;
|
|
194
201
|
s.lastMainListLines = undefined;
|
|
202
|
+
s.lastMainListSignature = undefined;
|
|
195
203
|
s.mainListDeadlineScheduler?.dispose();
|
|
196
204
|
s.mainListDeadlineScheduler = undefined;
|
|
197
205
|
s.mainListWidgetInstalled = false;
|
|
@@ -256,6 +264,12 @@ function refreshMainListWidget(): void {
|
|
|
256
264
|
s.mainListDeadlineScheduler?.schedule(nextExpiry - now);
|
|
257
265
|
}
|
|
258
266
|
syncMainListSelection(rows);
|
|
267
|
+
const nextSignature = rows.length ? mainListSignature(rows, {
|
|
268
|
+
selectedId: s.mainListFocused ? s.mainListSelectedId : undefined,
|
|
269
|
+
focused: s.mainListFocused === true,
|
|
270
|
+
}) : undefined;
|
|
271
|
+
const changed = nextSignature !== s.lastMainListSignature;
|
|
272
|
+
s.lastMainListSignature = nextSignature;
|
|
259
273
|
s.lastMainListLines = rows.length ? buildMainListLines(rows, MAIN_LIST_FALLBACK_WIDTH, deps.truncate, themeFg(ctx), {
|
|
260
274
|
selectedId: s.mainListFocused ? s.mainListSelectedId : undefined,
|
|
261
275
|
focused: s.mainListFocused === true,
|
|
@@ -265,6 +279,7 @@ function refreshMainListWidget(): void {
|
|
|
265
279
|
try { (ctx.ui as any).setWidget?.(MAIN_LIST_WIDGET_KEY, undefined); } catch { /* ignore */ }
|
|
266
280
|
s.mainListWidgetInstalled = false;
|
|
267
281
|
s.mainListRequestRender = undefined;
|
|
282
|
+
s.lastMainListSignature = undefined;
|
|
268
283
|
return;
|
|
269
284
|
}
|
|
270
285
|
if (!s.mainListWidgetInstalled) {
|
|
@@ -273,7 +288,9 @@ function refreshMainListWidget(): void {
|
|
|
273
288
|
s.mainListWidgetInstalled = true;
|
|
274
289
|
} catch { /* ignore */ }
|
|
275
290
|
}
|
|
276
|
-
|
|
291
|
+
if (changed) {
|
|
292
|
+
try { s.mainListRequestRender?.(); } catch { /* ignore */ }
|
|
293
|
+
}
|
|
277
294
|
}
|
|
278
295
|
|
|
279
296
|
function themeFg(ctx: ExtensionContext): (color: string, value: string) => string {
|
|
@@ -315,7 +332,7 @@ function renderWidth(width: number): number {
|
|
|
315
332
|
return Number.isFinite(width) && width > 0 ? Math.floor(width) : MAIN_LIST_FALLBACK_WIDTH;
|
|
316
333
|
}
|
|
317
334
|
|
|
318
|
-
type InternalRow = BackgroundWorkRow & { navigatorId: string; providerLabel: string };
|
|
335
|
+
type InternalRow = BackgroundWorkRow & { navigatorId: string; providerLabel: string; parentRow?: boolean };
|
|
319
336
|
|
|
320
337
|
function rowKey(providerId: string, id: string): string {
|
|
321
338
|
return `${providerId}:${id}`;
|
|
@@ -333,6 +350,19 @@ function listRows(now = Date.now()): InternalRow[] {
|
|
|
333
350
|
let providerRows: BackgroundWorkRow[] = [];
|
|
334
351
|
try { providerRows = provider.listRows(now) ?? []; } catch { providerRows = []; }
|
|
335
352
|
const orderedProviderRows = [...providerRows].sort((a, b) => b.sortStartedAt - a.sortStartedAt || rowDisplayName(a).localeCompare(rowDisplayName(b)));
|
|
353
|
+
let showSection = orderedProviderRows.length > 0 || provider.parentRow !== undefined;
|
|
354
|
+
try { showSection = provider.showSection?.(orderedProviderRows, now) ?? showSection; } catch { showSection = false; }
|
|
355
|
+
if (!showSection) continue;
|
|
356
|
+
let parentRow: BackgroundWorkRow | null = null;
|
|
357
|
+
try { parentRow = provider.parentRow?.(now) ?? null; } catch { parentRow = null; }
|
|
358
|
+
if (parentRow) {
|
|
359
|
+
rows.push({
|
|
360
|
+
...parentRow,
|
|
361
|
+
navigatorId: rowKey(parentRow.providerId, parentRow.id),
|
|
362
|
+
providerLabel: provider.label,
|
|
363
|
+
parentRow: true,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
336
366
|
for (const row of orderedProviderRows) {
|
|
337
367
|
rows.push({ ...row, navigatorId: rowKey(provider.id, row.id), providerLabel: provider.label });
|
|
338
368
|
}
|
|
@@ -378,7 +408,7 @@ function focusMainList(): void {
|
|
|
378
408
|
const rows = listRows();
|
|
379
409
|
if (rows.length === 0) return;
|
|
380
410
|
const s = state();
|
|
381
|
-
|
|
411
|
+
s.mainListSelectedId = rows.find((row) => row.parentRow && row.id === "main")?.navigatorId ?? rows[0]!.navigatorId;
|
|
382
412
|
s.mainListFocused = true;
|
|
383
413
|
refreshMainListWidget();
|
|
384
414
|
}
|
|
@@ -412,16 +442,6 @@ function buildMainListLines(
|
|
|
412
442
|
const group = grouped.get(label)!;
|
|
413
443
|
if (i > 0) lines.push("");
|
|
414
444
|
lines.push(providerGroupLabel(label, fg));
|
|
415
|
-
const provider = state().providers.get(group[0]!.providerId);
|
|
416
|
-
let parentRow: BackgroundWorkRow | null = null;
|
|
417
|
-
try { parentRow = provider?.parentRow?.(Date.now()) ?? null; } catch { parentRow = null; }
|
|
418
|
-
if (parentRow) {
|
|
419
|
-
lines.push(formatMainListRow({
|
|
420
|
-
...parentRow,
|
|
421
|
-
navigatorId: rowKey(parentRow.providerId, parentRow.id),
|
|
422
|
-
providerLabel: label,
|
|
423
|
-
}, false, fg, width));
|
|
424
|
-
}
|
|
425
445
|
for (const row of group) {
|
|
426
446
|
const selected = options.focused && row.navigatorId === options.selectedId;
|
|
427
447
|
lines.push(formatMainListRow(row, selected === true, fg, width));
|
|
@@ -433,13 +453,13 @@ function buildMainListLines(
|
|
|
433
453
|
}
|
|
434
454
|
|
|
435
455
|
function shortcutsLine(focused: boolean, fg: (color: string, value: string) => string): string {
|
|
436
|
-
const keys = focused ? "↑↓
|
|
456
|
+
const keys = focused ? "↑↓ switch · Enter detail · x stop · Esc unfocus" : "← to navigate";
|
|
437
457
|
return dim(keys, fg);
|
|
438
458
|
}
|
|
439
459
|
|
|
440
460
|
function providerGroupLabel(label: string, fg: (color: string, value: string) => string): string {
|
|
441
461
|
const normalized = singleLine(label).toLowerCase();
|
|
442
|
-
return
|
|
462
|
+
return fg("warning", normalized);
|
|
443
463
|
}
|
|
444
464
|
|
|
445
465
|
function formatMainListRow(row: InternalRow, selected: boolean, fg: (color: string, value: string) => string, width: number): string {
|
|
@@ -471,9 +491,40 @@ function statusGlyph(row: InternalRow): string {
|
|
|
471
491
|
}
|
|
472
492
|
|
|
473
493
|
function statusIndicator(row: InternalRow, now = Date.now()): { glyph: string; color: string } {
|
|
494
|
+
void now;
|
|
474
495
|
if (row.statusTone !== "running") return { glyph: statusGlyph(row), color: toneColor(row.statusTone, row.status) };
|
|
475
|
-
|
|
476
|
-
|
|
496
|
+
return { glyph: RUNNING_DOT_GLYPH, color: "accent" };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function mainListSignature(rows: InternalRow[], options: { selectedId?: string; focused?: boolean } = {}): string {
|
|
500
|
+
return JSON.stringify({
|
|
501
|
+
focused: options.focused === true,
|
|
502
|
+
selectedId: options.focused === true ? options.selectedId ?? null : null,
|
|
503
|
+
rows: rows.map((row) => ({
|
|
504
|
+
navigatorId: row.navigatorId,
|
|
505
|
+
providerLabel: row.providerLabel,
|
|
506
|
+
parentRow: row.parentRow === true,
|
|
507
|
+
providerId: row.providerId,
|
|
508
|
+
id: row.id,
|
|
509
|
+
name: row.name ?? null,
|
|
510
|
+
model: row.model ?? null,
|
|
511
|
+
effort: row.effort ?? null,
|
|
512
|
+
tool: row.tool ?? null,
|
|
513
|
+
tokens: row.tokens ?? null,
|
|
514
|
+
command: row.command ?? null,
|
|
515
|
+
status: row.status,
|
|
516
|
+
statusTone: row.statusTone,
|
|
517
|
+
kind: row.kind,
|
|
518
|
+
primary: row.primary,
|
|
519
|
+
secondary: row.secondary ?? null,
|
|
520
|
+
facts: stableFacts(row.facts),
|
|
521
|
+
sortStartedAt: row.sortStartedAt,
|
|
522
|
+
})),
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function stableFacts(facts: string[] | undefined): string[] {
|
|
527
|
+
return (facts ?? []).filter((fact) => !/\bleft$/.test(singleLine(fact)));
|
|
477
528
|
}
|
|
478
529
|
|
|
479
530
|
function rowSummary(row: InternalRow): string {
|
|
@@ -527,13 +578,14 @@ function detailFor(navigatorId: string, now = Date.now(), options?: { logTailLin
|
|
|
527
578
|
}
|
|
528
579
|
|
|
529
580
|
function closeFor(row: InternalRow): BackgroundWorkCloseOutcome {
|
|
581
|
+
if (row.parentRow) return { action: "not-closable", providerId: row.providerId, id: row.id };
|
|
530
582
|
const provider = state().providers.get(row.providerId);
|
|
531
583
|
if (!provider) return { action: "missing", providerId: row.providerId, id: row.id };
|
|
532
584
|
try { return provider.close(row.id); } catch { return { action: "missing", providerId: row.providerId, id: row.id }; }
|
|
533
585
|
}
|
|
534
586
|
|
|
535
587
|
function closeHintFor(row: InternalRow | undefined): string | null {
|
|
536
|
-
if (!row) return null;
|
|
588
|
+
if (!row || row.parentRow) return null;
|
|
537
589
|
const provider = state().providers.get(row.providerId);
|
|
538
590
|
if (!provider) return null;
|
|
539
591
|
try { return `${provider.armCloseLabel(row)} ${row.name || row.id}`; } catch { return null; }
|
|
@@ -558,6 +610,7 @@ function installNavigatorEditor(ui: any, deps: HostDeps): unknown {
|
|
|
558
610
|
}
|
|
559
611
|
|
|
560
612
|
function wrapEditor(inner: any, deps: HostDeps): unknown {
|
|
613
|
+
if (inner && typeof inner.render === "function") state().editorComponent = inner as Component;
|
|
561
614
|
return new Proxy(inner, {
|
|
562
615
|
get(target, prop) {
|
|
563
616
|
if (prop === "handleInput") {
|
|
@@ -588,16 +641,24 @@ function handleMainListInput(data: string, deps: HostDeps): boolean {
|
|
|
588
641
|
}
|
|
589
642
|
if (deps.matchKey(data, "up")) {
|
|
590
643
|
clearMainListCloseArm();
|
|
591
|
-
if (moveMainListSelection(-1))
|
|
644
|
+
if (moveMainListSelection(-1)) {
|
|
645
|
+
refreshMainListWidget();
|
|
646
|
+
if (!selectedMainListRow()?.parentRow) openNavigator();
|
|
647
|
+
}
|
|
592
648
|
return true;
|
|
593
649
|
}
|
|
594
650
|
if (deps.matchKey(data, "down")) {
|
|
595
651
|
clearMainListCloseArm();
|
|
596
|
-
if (moveMainListSelection(1))
|
|
652
|
+
if (moveMainListSelection(1)) {
|
|
653
|
+
refreshMainListWidget();
|
|
654
|
+
if (!selectedMainListRow()?.parentRow) openNavigator();
|
|
655
|
+
}
|
|
597
656
|
return true;
|
|
598
657
|
}
|
|
599
658
|
if (deps.matchKey(data, "enter")) {
|
|
600
|
-
|
|
659
|
+
const selected = selectedMainListRow();
|
|
660
|
+
if (selected?.parentRow) unfocusMainList();
|
|
661
|
+
else openNavigator();
|
|
601
662
|
return true;
|
|
602
663
|
}
|
|
603
664
|
if (data === "x" || data === "X" || deps.matchKey(data, "x") || deps.matchKey(data, "X")) {
|
|
@@ -613,7 +674,7 @@ function handleMainListInput(data: string, deps: HostDeps): boolean {
|
|
|
613
674
|
|
|
614
675
|
function handleMainListCloseKey(): void {
|
|
615
676
|
const row = selectedMainListRow();
|
|
616
|
-
if (!row) return;
|
|
677
|
+
if (!row || row.parentRow) return;
|
|
617
678
|
const s = state();
|
|
618
679
|
const now = Date.now();
|
|
619
680
|
const arm = s.mainListCloseArm;
|
|
@@ -689,6 +750,8 @@ function createOverlayComponent(
|
|
|
689
750
|
let closeArm: { id: string; armedAt: number } | undefined;
|
|
690
751
|
let closeArmTimer: ReturnType<typeof setTimeout> | undefined;
|
|
691
752
|
let closed = false;
|
|
753
|
+
let transcriptDetail: BackgroundWorkDetail | null = null;
|
|
754
|
+
let transcriptComponent: Component | null = null;
|
|
692
755
|
|
|
693
756
|
const fg = (color: string, value: string) => theme?.fg ? theme.fg(color, value) : value;
|
|
694
757
|
|
|
@@ -704,12 +767,24 @@ function createOverlayComponent(
|
|
|
704
767
|
});
|
|
705
768
|
|
|
706
769
|
function refreshRows(): void {
|
|
707
|
-
const selectedId = overlayState.rows[overlayState.selected]?.navigatorId;
|
|
770
|
+
const selectedId = state().mainListSelectedId ?? overlayState.rows[overlayState.selected]?.navigatorId;
|
|
708
771
|
overlayState.rows = listRows();
|
|
709
772
|
const nextIdx = selectedId ? overlayState.rows.findIndex((row) => row.navigatorId === selectedId) : -1;
|
|
710
773
|
overlayState.selected = nextIdx >= 0 ? nextIdx : Math.min(overlayState.selected, Math.max(0, overlayState.rows.length - 1));
|
|
711
774
|
}
|
|
712
775
|
|
|
776
|
+
function selectOverlayRow(next: number): void {
|
|
777
|
+
overlayState.selected = Math.min(Math.max(0, overlayState.rows.length - 1), Math.max(0, next));
|
|
778
|
+
state().mainListSelectedId = overlayState.rows[overlayState.selected]?.navigatorId;
|
|
779
|
+
clearCloseArm();
|
|
780
|
+
refreshMainListWidget();
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function activateSelectedRow(): void {
|
|
784
|
+
if (selectedRow()?.parentRow) close();
|
|
785
|
+
else openDetail();
|
|
786
|
+
}
|
|
787
|
+
|
|
713
788
|
function clearCloseArm(): void {
|
|
714
789
|
if (closeArmTimer) clearTimeout(closeArmTimer);
|
|
715
790
|
closeArmTimer = undefined;
|
|
@@ -731,13 +806,16 @@ function createOverlayComponent(
|
|
|
731
806
|
}
|
|
732
807
|
|
|
733
808
|
function selectedRow(): InternalRow | undefined {
|
|
734
|
-
if (mode === "detail" && detailId) return overlayState.rows.find((row) => row.navigatorId === detailId);
|
|
735
809
|
return overlayState.rows[overlayState.selected];
|
|
736
810
|
}
|
|
737
811
|
|
|
738
812
|
function openDetail(): void {
|
|
739
813
|
const row = selectedRow();
|
|
740
814
|
if (!row) return;
|
|
815
|
+
if (row.parentRow) {
|
|
816
|
+
close();
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
741
819
|
clearCloseArm();
|
|
742
820
|
detailId = row.navigatorId;
|
|
743
821
|
expandedSections.clear();
|
|
@@ -808,10 +886,43 @@ function createOverlayComponent(
|
|
|
808
886
|
|
|
809
887
|
return {
|
|
810
888
|
render(width: number) {
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
889
|
+
refreshRows();
|
|
890
|
+
const railLines = mode === "detail"
|
|
891
|
+
? buildMainListLines(overlayState.rows, width, deps.truncate, fg, {
|
|
892
|
+
selectedId: selectedRow()?.navigatorId,
|
|
893
|
+
focused: true,
|
|
894
|
+
})
|
|
895
|
+
: [];
|
|
896
|
+
const editorLines = mode === "detail" ? renderEditorLines(width) : [];
|
|
897
|
+
const bottomLines = [...railLines, ...editorLines];
|
|
898
|
+
const overlayRows = state().detailOverlayRows;
|
|
899
|
+
const detailRows = overlayRows === undefined ? undefined : Math.max(1, overlayRows - bottomLines.length);
|
|
900
|
+
let contentLines: string[];
|
|
901
|
+
if (mode === "detail" && detail?.transcript && deps.createTranscriptComponent) {
|
|
902
|
+
if (transcriptDetail !== detail || !transcriptComponent) {
|
|
903
|
+
transcriptDetail = detail;
|
|
904
|
+
transcriptComponent = deps.createTranscriptComponent(detail, theme);
|
|
905
|
+
}
|
|
906
|
+
contentLines = buildTranscriptDetailLines(
|
|
907
|
+
detail,
|
|
908
|
+
transcriptComponent.render(width),
|
|
909
|
+
width,
|
|
910
|
+
deps.truncate,
|
|
911
|
+
fg,
|
|
912
|
+
{ minRows: detailRows },
|
|
913
|
+
);
|
|
914
|
+
} else {
|
|
915
|
+
transcriptDetail = null;
|
|
916
|
+
transcriptComponent = null;
|
|
917
|
+
contentLines = mode === "detail"
|
|
918
|
+
? buildDetailLines(detail, width, deps.truncate, fg, { expandedSections, logTailRows, minRows: detailRows })
|
|
919
|
+
: buildListLines(overlayState, width, deps.truncate, fg);
|
|
920
|
+
}
|
|
921
|
+
if (mode !== "detail") return contentLines;
|
|
922
|
+
if (detailRows === undefined) return [...contentLines, ...bottomLines];
|
|
923
|
+
const fittedContent = contentLines.slice(0, detailRows);
|
|
924
|
+
while (fittedContent.length < detailRows) fittedContent.push("");
|
|
925
|
+
return [...fittedContent, ...bottomLines];
|
|
815
926
|
},
|
|
816
927
|
handleInput(data: string) {
|
|
817
928
|
if (closed) return;
|
|
@@ -820,10 +931,25 @@ function createOverlayComponent(
|
|
|
820
931
|
return;
|
|
821
932
|
}
|
|
822
933
|
if (mode === "detail") {
|
|
823
|
-
if (deps.matchKey(data, "
|
|
934
|
+
if (deps.matchKey(data, "up")) {
|
|
935
|
+
selectOverlayRow(overlayState.selected - 1);
|
|
936
|
+
activateSelectedRow();
|
|
937
|
+
}
|
|
938
|
+
else if (deps.matchKey(data, "down")) {
|
|
939
|
+
selectOverlayRow(overlayState.selected + 1);
|
|
940
|
+
activateSelectedRow();
|
|
941
|
+
}
|
|
942
|
+
else if (deps.matchKey(data, "left")) {
|
|
943
|
+
const mainIdx = overlayState.rows.findIndex((row) => row.parentRow && row.id === "main");
|
|
944
|
+
if (mainIdx >= 0) selectOverlayRow(mainIdx);
|
|
945
|
+
close();
|
|
946
|
+
}
|
|
824
947
|
else if (deps.matchKey(data, "enter")) {
|
|
825
|
-
const
|
|
826
|
-
if (
|
|
948
|
+
const row = selectedRow();
|
|
949
|
+
if (row?.parentRow || row?.navigatorId !== detailId) openDetail();
|
|
950
|
+
else {
|
|
951
|
+
const sectionId = firstToggleableSectionId(detail);
|
|
952
|
+
if (!sectionId) return;
|
|
827
953
|
if (expandedSections.has(sectionId)) expandedSections.delete(sectionId);
|
|
828
954
|
else expandedSections.add(sectionId);
|
|
829
955
|
requestRender();
|
|
@@ -834,7 +960,10 @@ function createOverlayComponent(
|
|
|
834
960
|
if (detailId) detail = detailFor(detailId, Date.now(), { logTailLines: logTailRows }) ?? detail;
|
|
835
961
|
requestRender();
|
|
836
962
|
}
|
|
837
|
-
else if (deps.matchKey(data, "escape"))
|
|
963
|
+
else if (deps.matchKey(data, "escape")) {
|
|
964
|
+
unfocusMainList();
|
|
965
|
+
close();
|
|
966
|
+
}
|
|
838
967
|
return;
|
|
839
968
|
}
|
|
840
969
|
if (deps.matchKey(data, "up")) {
|
|
@@ -851,7 +980,7 @@ function createOverlayComponent(
|
|
|
851
980
|
close();
|
|
852
981
|
}
|
|
853
982
|
},
|
|
854
|
-
invalidate() {},
|
|
983
|
+
invalidate() { transcriptComponent?.invalidate(); },
|
|
855
984
|
dispose() {
|
|
856
985
|
clearCloseArm();
|
|
857
986
|
detailScheduler.dispose();
|
|
@@ -859,26 +988,61 @@ function createOverlayComponent(
|
|
|
859
988
|
};
|
|
860
989
|
}
|
|
861
990
|
|
|
991
|
+
function buildTranscriptDetailLines(
|
|
992
|
+
detail: BackgroundWorkDetail,
|
|
993
|
+
transcriptLines: string[],
|
|
994
|
+
width: number,
|
|
995
|
+
truncate: (s: string, width: number) => string,
|
|
996
|
+
fg: (color: string, value: string) => string,
|
|
997
|
+
options: { minRows?: number } = {},
|
|
998
|
+
): string[] {
|
|
999
|
+
const actions = [...(detail.footerActions ?? ["x close"]), "Esc close"].join(" · ");
|
|
1000
|
+
const lines: string[] = [
|
|
1001
|
+
fg("accent", rule(detail.title, width)),
|
|
1002
|
+
dim(` ← main · ${actions}`, fg),
|
|
1003
|
+
"",
|
|
1004
|
+
` status ${fg(toneColor(detail.statusTone, detail.status), detail.status)}`,
|
|
1005
|
+
];
|
|
1006
|
+
if (detail.subtitle) lines.push(` summary ${detail.subtitle}`);
|
|
1007
|
+
for (const item of detail.metadata) lines.push(` ${item.label.padEnd(8, " ").slice(0, 8)} ${item.value}`);
|
|
1008
|
+
lines.push("", dim(section("transcript", width), fg));
|
|
1009
|
+
if (detail.transcriptDiagnostic) lines.push(` ${dim(detail.transcriptDiagnostic, fg)}`);
|
|
1010
|
+
lines.push(...(transcriptLines.length ? transcriptLines : [" (no transcript yet)"]));
|
|
1011
|
+
lines.push("");
|
|
1012
|
+
const footerLines = [dim(` ← main · ${actions}`, fg), dim(rule("", width), fg)];
|
|
1013
|
+
padBeforeFooter(lines, footerLines.length, options.minRows);
|
|
1014
|
+
lines.push(...footerLines);
|
|
1015
|
+
return lines.map((line) => safeTruncate(line, width, truncate));
|
|
1016
|
+
}
|
|
1017
|
+
|
|
862
1018
|
function detailOverlayOptions() {
|
|
863
|
-
const
|
|
864
|
-
const marginBottom = DETAIL_OVERLAY_FOOTER_MARGIN_ROWS + navigatorRows;
|
|
1019
|
+
const marginBottom = DETAIL_OVERLAY_FOOTER_ROWS;
|
|
865
1020
|
return {
|
|
866
1021
|
anchor: "top-left" as const,
|
|
867
1022
|
width: "100%" as const,
|
|
868
1023
|
maxHeight: "100%" as const,
|
|
869
1024
|
margin: {
|
|
870
|
-
top:
|
|
1025
|
+
top: 0,
|
|
871
1026
|
right: 0,
|
|
872
1027
|
bottom: marginBottom,
|
|
873
1028
|
left: 0,
|
|
874
1029
|
},
|
|
875
1030
|
visible: (_termWidth: number, termHeight: number) => {
|
|
876
|
-
state().detailOverlayRows = Math.max(1, termHeight -
|
|
1031
|
+
state().detailOverlayRows = Math.max(1, termHeight - marginBottom);
|
|
877
1032
|
return true;
|
|
878
1033
|
},
|
|
879
1034
|
};
|
|
880
1035
|
}
|
|
881
1036
|
|
|
1037
|
+
function renderEditorLines(width: number): string[] {
|
|
1038
|
+
try {
|
|
1039
|
+
const lines = state().editorComponent?.render(width);
|
|
1040
|
+
if (lines?.length) return lines;
|
|
1041
|
+
} catch { /* use an empty editor-shaped fallback */ }
|
|
1042
|
+
const border = "─".repeat(Math.max(1, width));
|
|
1043
|
+
return [border, "", border];
|
|
1044
|
+
}
|
|
1045
|
+
|
|
882
1046
|
function fallbackDetail(row: InternalRow): BackgroundWorkDetail {
|
|
883
1047
|
return {
|
|
884
1048
|
providerId: row.providerId,
|