@esso0428/pi-subagents 0.15.5 → 0.15.6
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/CHANGELOG.md +4 -22
- package/README.md +5 -5
- package/dist/agent-history-list.d.ts +18 -0
- package/dist/agent-history-list.js +68 -0
- package/dist/agent-history.d.ts +1 -1
- package/dist/agent-history.js +31 -34
- package/dist/agent-manager.d.ts +22 -8
- package/dist/agent-manager.js +100 -93
- package/dist/agent-runner.js +67 -86
- package/dist/agent-types.js +27 -45
- package/dist/context.js +2 -6
- package/dist/cross-extension-rpc.js +5 -9
- package/dist/custom-agents.js +15 -18
- package/dist/default-agents.js +1 -4
- package/dist/enabled-models.js +11 -16
- package/dist/env.js +1 -4
- package/dist/group-join.js +1 -5
- package/dist/index.js +273 -286
- package/dist/invocation-config.js +2 -6
- package/dist/memory.js +24 -34
- package/dist/model-resolver.js +1 -4
- package/dist/nico-overrides.js +14 -20
- package/dist/output-file.js +16 -22
- package/dist/prompts.js +1 -4
- package/dist/schedule-store.js +16 -21
- package/dist/schedule.js +8 -12
- package/dist/settings.js +15 -23
- package/dist/skill-loader.js +20 -23
- package/dist/status-note.js +1 -4
- package/dist/types.d.ts +0 -1
- package/dist/types.js +1 -4
- package/dist/ui/agent-widget.d.ts +1 -5
- package/dist/ui/agent-widget.js +25 -49
- package/dist/ui/conversation-blocks.d.ts +18 -0
- package/dist/ui/conversation-blocks.js +214 -0
- package/dist/ui/conversation-nvim.d.ts +7 -0
- package/dist/ui/conversation-nvim.js +116 -0
- package/dist/ui/conversation-search.d.ts +38 -0
- package/dist/ui/conversation-search.js +123 -0
- package/dist/ui/conversation-viewer.d.ts +55 -19
- package/dist/ui/conversation-viewer.js +398 -215
- package/dist/ui/fleet-list.d.ts +23 -11
- package/dist/ui/fleet-list.js +85 -78
- package/dist/ui/schedule-menu.js +1 -4
- package/dist/ui/viewer-keys.js +7 -10
- package/dist/usage.js +4 -10
- package/dist/worktree.js +26 -31
- package/package.json +1 -1
- package/src/agent-history-list.ts +91 -0
- package/src/agent-history.ts +12 -17
- package/src/agent-manager.ts +108 -81
- package/src/agent-runner.ts +5 -13
- package/src/index.ts +84 -89
- package/src/output-file.ts +1 -1
- package/src/types.ts +0 -1
- package/src/ui/agent-widget.ts +1 -8
- package/src/ui/conversation-blocks.ts +270 -0
- package/src/ui/conversation-nvim.ts +127 -0
- package/src/ui/conversation-search.ts +161 -0
- package/src/ui/conversation-viewer.ts +376 -205
- package/src/ui/fleet-list.ts +73 -52
- package/test/agent-history-list.test.ts +121 -0
- package/test/agent-manager-history.test.ts +59 -58
- package/test/agent-menu-history-navigation.test.ts +86 -0
- package/test/ui/conversation-blocks.test.ts +72 -0
- package/test/ui/conversation-search.test.ts +55 -0
- package/test/ui/conversation-viewer.test.ts +132 -0
- package/test/ui/fleet-list.test.ts +131 -0
- package/dist/ui/markdown-result.d.ts +0 -3
- package/dist/ui/markdown-result.js +0 -53
- package/src/ui/markdown-result.ts +0 -56
- package/test/agent-history.test.ts +0 -63
- package/test/ui/markdown-result.test.ts +0 -45
package/src/index.ts
CHANGED
|
@@ -24,7 +24,8 @@ import { isModelInScope, readEnabledModels, resolveEnabledModels } from "./enabl
|
|
|
24
24
|
import { GroupJoinManager } from "./group-join.js";
|
|
25
25
|
import { resolveAgentInvocationConfig, resolveJoinMode } from "./invocation-config.js";
|
|
26
26
|
import { type ModelRegistry, resolveModel } from "./model-resolver.js";
|
|
27
|
-
import { agentHistoryLocator, createAgentHistoryPath,
|
|
27
|
+
import { agentHistoryLocator, createAgentHistoryPath, readAgentHistory } from "./agent-history.js";
|
|
28
|
+
import { buildAgentStatusMenuEntries, canOpenActiveAgent, canOpenAgentHistory, formatAgentHistoryOption, splitAgentRecords } from "./agent-history-list.js";
|
|
28
29
|
import { createOutputFilePath, streamToOutputFile, writeInitialEntry } from "./output-file.js";
|
|
29
30
|
import { SubagentScheduler } from "./schedule.js";
|
|
30
31
|
import { resolveStorePath, ScheduleStore } from "./schedule-store.js";
|
|
@@ -49,7 +50,6 @@ import {
|
|
|
49
50
|
type UICtx,
|
|
50
51
|
} from "./ui/agent-widget.js";
|
|
51
52
|
import { FleetList, type FleetUICtx } from "./ui/fleet-list.js";
|
|
52
|
-
import { createMarkdownResult } from "./ui/markdown-result.js";
|
|
53
53
|
import { showSchedulesMenu } from "./ui/schedule-menu.js";
|
|
54
54
|
import { addUsage, getLifetimeTotal, getSessionContextPercent, type LifetimeUsage } from "./usage.js";
|
|
55
55
|
|
|
@@ -242,9 +242,6 @@ function buildDetails(
|
|
|
242
242
|
};
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
/** Maximum expanded notification body retained in message details. */
|
|
246
|
-
const MAX_NOTIFICATION_RESULT_TEXT_LENGTH = 20_000;
|
|
247
|
-
|
|
248
245
|
/** Build notification details for the custom message renderer. */
|
|
249
246
|
function buildNotificationDetails(record: AgentRecord, resultMaxLen: number, activity?: AgentActivity): NotificationDetails {
|
|
250
247
|
const totalTokens = getLifetimeTotal(record.lifetimeUsage);
|
|
@@ -260,11 +257,6 @@ function buildNotificationDetails(record: AgentRecord, resultMaxLen: number, act
|
|
|
260
257
|
durationMs: record.completedAt ? record.completedAt - record.startedAt : 0,
|
|
261
258
|
outputFile: record.outputFile,
|
|
262
259
|
error: record.error,
|
|
263
|
-
resultText: record.result
|
|
264
|
-
? record.result.length > MAX_NOTIFICATION_RESULT_TEXT_LENGTH
|
|
265
|
-
? record.result.slice(0, MAX_NOTIFICATION_RESULT_TEXT_LENGTH) + "…"
|
|
266
|
-
: record.result
|
|
267
|
-
: undefined,
|
|
268
260
|
resultPreview: record.result
|
|
269
261
|
? record.result.length > resultMaxLen
|
|
270
262
|
? record.result.slice(0, resultMaxLen) + "…"
|
|
@@ -281,7 +273,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
281
273
|
const d = message.details;
|
|
282
274
|
if (!d) return undefined;
|
|
283
275
|
|
|
284
|
-
function renderOne(d: NotificationDetails): string
|
|
276
|
+
function renderOne(d: NotificationDetails): string {
|
|
285
277
|
const isError = d.status === "error" || d.status === "stopped" || d.status === "aborted";
|
|
286
278
|
const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
287
279
|
const statusText = isError ? d.status
|
|
@@ -301,34 +293,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
301
293
|
line += "\n " + parts.map(p => theme.fg("dim", p)).join(" " + theme.fg("dim", "·") + " ");
|
|
302
294
|
}
|
|
303
295
|
|
|
304
|
-
|
|
305
|
-
|
|
296
|
+
// Line 3: result preview (collapsed) or full (expanded)
|
|
297
|
+
if (expanded) {
|
|
298
|
+
const lines = d.resultPreview.split("\n").slice(0, 30);
|
|
299
|
+
for (const l of lines) line += "\n" + theme.fg("dim", ` ${l}`);
|
|
300
|
+
} else {
|
|
306
301
|
const preview = d.resultPreview.split("\n")[0]?.slice(0, 80) ?? "";
|
|
307
302
|
line += "\n " + theme.fg("dim", `⎿ ${preview}`);
|
|
308
|
-
if (d.outputFile) {
|
|
309
|
-
line += "\n " + theme.fg("muted", `transcript: ${d.outputFile}`);
|
|
310
|
-
}
|
|
311
|
-
return line;
|
|
312
303
|
}
|
|
313
304
|
|
|
314
|
-
|
|
315
|
-
container.addChild(new Text(line, 0, 0));
|
|
316
|
-
container.addChild(createMarkdownResult(d.resultText ?? d.resultPreview, theme, 30));
|
|
305
|
+
// Line 4: output file link (if present)
|
|
317
306
|
if (d.outputFile) {
|
|
318
|
-
|
|
307
|
+
line += "\n " + theme.fg("muted", `transcript: ${d.outputFile}`);
|
|
319
308
|
}
|
|
320
|
-
|
|
309
|
+
|
|
310
|
+
return line;
|
|
321
311
|
}
|
|
322
312
|
|
|
323
313
|
const all = [d, ...(d.others ?? [])];
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const container = new Container();
|
|
327
|
-
all.forEach((item, index) => {
|
|
328
|
-
if (index > 0) container.addChild(new Spacer(1));
|
|
329
|
-
container.addChild(renderOne(item) as Container);
|
|
330
|
-
});
|
|
331
|
-
return container;
|
|
314
|
+
return new Text(all.map(renderOne).join("\n"), 0, 0);
|
|
332
315
|
}
|
|
333
316
|
);
|
|
334
317
|
|
|
@@ -463,9 +446,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
463
446
|
pi.events.emit("subagents:completed", eventData);
|
|
464
447
|
}
|
|
465
448
|
|
|
466
|
-
// Persist final record for cross-extension history reconstruction
|
|
467
|
-
// the transcript body in a project-local file rather than inflating the
|
|
468
|
-
// parent session JSONL; only its project-relative locator is persisted.
|
|
449
|
+
// Persist final record for cross-extension history reconstruction
|
|
469
450
|
pi.appendEntry("subagents:record", {
|
|
470
451
|
id: record.id, type: record.type, description: record.description,
|
|
471
452
|
status: record.status, result: record.result, error: record.error,
|
|
@@ -574,22 +555,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
574
555
|
// bound session_start, so a filtered-out activation never advertises (#142).
|
|
575
556
|
pi.on("session_start", async (_event, ctx) => {
|
|
576
557
|
currentCtx = ctx;
|
|
577
|
-
try {
|
|
578
|
-
ensureSubagentsGitignore(ctx.cwd);
|
|
579
|
-
} catch (err) {
|
|
580
|
-
console.warn("[pi-subagents] Failed to protect project-local history:", err);
|
|
581
|
-
}
|
|
582
558
|
manager.clearCompleted(true);
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
.filter((entry: any) => entry?.type === "custom" && entry
|
|
586
|
-
.map((entry: any) => entry.data);
|
|
587
|
-
manager.restoreCompleted(historicalRecords);
|
|
588
|
-
// Bind the UI here as well as on tool execution so rehydrated terminal
|
|
589
|
-
// records are immediately visible and openable from FleetView after reload.
|
|
590
|
-
widget.setUICtx(ctx.ui as UICtx);
|
|
591
|
-
fleet.setUICtx(ctx.ui as unknown as FleetUICtx);
|
|
592
|
-
fleet.update();
|
|
559
|
+
const branch = ctx.sessionManager?.getBranch?.() ?? [];
|
|
560
|
+
manager.restoreCompleted(branch
|
|
561
|
+
.filter((entry: any) => entry?.type === "custom" && entry?.customType === "subagents:record")
|
|
562
|
+
.map((entry: any) => entry.data));
|
|
593
563
|
// Guard mirrors the `!scheduler.isActive()` pattern below: session_start
|
|
594
564
|
// fires once per activation, but a double-bind must not leak listeners.
|
|
595
565
|
if (!rpcHandle) {
|
|
@@ -640,11 +610,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
640
610
|
// everything else; "off" = hide the widget entirely. Read live at render time.
|
|
641
611
|
let widgetMode: WidgetMode = "background";
|
|
642
612
|
function getWidgetMode(): WidgetMode { return widgetMode; }
|
|
643
|
-
const widget = new AgentWidget(manager, agentActivity, getWidgetMode
|
|
613
|
+
const widget = new AgentWidget(manager, agentActivity, getWidgetMode);
|
|
644
614
|
function setWidgetMode(m: WidgetMode): void { widgetMode = m; widget.update(); }
|
|
645
615
|
|
|
646
616
|
// Claude Code-style FleetView: navigable list of main + subagents below the editor.
|
|
647
|
-
const fleet = new FleetList(manager, agentActivity, () => currentCtx?.cwd);
|
|
617
|
+
const fleet = new FleetList(manager, agentActivity, () => currentCtx?.cwd, pi, () => currentCtx);
|
|
648
618
|
let fleetViewEnabled = true;
|
|
649
619
|
function isFleetViewEnabled(): boolean { return fleetViewEnabled; }
|
|
650
620
|
function setFleetViewEnabled(b: boolean): void { fleetViewEnabled = b; fleet.setEnabled(b); }
|
|
@@ -1061,15 +1031,15 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1061
1031
|
|
|
1062
1032
|
if (expanded) {
|
|
1063
1033
|
const resultText = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
1064
|
-
const container = new Container();
|
|
1065
|
-
container.addChild(new Text(line, 0, 0));
|
|
1066
1034
|
if (resultText) {
|
|
1067
|
-
|
|
1035
|
+
const lines = resultText.split("\n").slice(0, 50);
|
|
1036
|
+
for (const l of lines) {
|
|
1037
|
+
line += "\n" + theme.fg("dim", ` ${l}`);
|
|
1038
|
+
}
|
|
1068
1039
|
if (resultText.split("\n").length > 50) {
|
|
1069
|
-
|
|
1040
|
+
line += "\n" + theme.fg("muted", " ... (use get_subagent_result with verbose for full output)");
|
|
1070
1041
|
}
|
|
1071
1042
|
}
|
|
1072
|
-
return container;
|
|
1073
1043
|
} else {
|
|
1074
1044
|
const doneText = isSteered ? "Wrapped up (turn limit)" : "Done";
|
|
1075
1045
|
line += "\n" + theme.fg("dim", ` ⎿ ${doneText}`);
|
|
@@ -1176,17 +1146,17 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1176
1146
|
rec.outputFile = createOutputFilePath(ctx.cwd, agentId, ctx.sessionManager.getSessionId());
|
|
1177
1147
|
writeInitialEntry(rec.outputFile, agentId, params.prompt, ctx.cwd);
|
|
1178
1148
|
|
|
1179
|
-
// Keep the existing /tmp output for compatibility, while also writing a
|
|
1180
|
-
// durable project-local copy for reload-safe, read-only history viewing.
|
|
1181
1149
|
try {
|
|
1182
|
-
rec.historyFile = createAgentHistoryPath(ctx.cwd, agentId
|
|
1150
|
+
rec.historyFile = createAgentHistoryPath(ctx.cwd, agentId);
|
|
1183
1151
|
rec.transcriptPath = agentHistoryLocator(ctx.cwd, rec.historyFile);
|
|
1184
1152
|
writeInitialEntry(rec.historyFile, agentId, params.prompt, ctx.cwd);
|
|
1185
1153
|
} catch (err) {
|
|
1186
|
-
// History is best effort; never disable the existing output transcript.
|
|
1187
1154
|
rec.historyFile = undefined;
|
|
1188
1155
|
rec.transcriptPath = undefined;
|
|
1189
|
-
|
|
1156
|
+
ctx.ui.notify(
|
|
1157
|
+
`Could not create durable transcript for agent ${agentId}: ${err instanceof Error ? err.message : String(err)}`,
|
|
1158
|
+
"warning",
|
|
1159
|
+
);
|
|
1190
1160
|
}
|
|
1191
1161
|
};
|
|
1192
1162
|
|
|
@@ -1667,17 +1637,10 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1667
1637
|
// Build select options
|
|
1668
1638
|
const options: string[] = [];
|
|
1669
1639
|
|
|
1670
|
-
//
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
record.session !== undefined || hasAgentHistory(ctx.cwd, record.transcriptPath),
|
|
1675
|
-
);
|
|
1676
|
-
if (agents.length > 0) {
|
|
1677
|
-
const running = agents.filter(a => a.status === "running" || a.status === "queued").length;
|
|
1678
|
-
const done = agents.filter(a => a.status === "completed" || a.status === "steered").length;
|
|
1679
|
-
options.push(`Running agents (${agents.length}) — ${running} running, ${done} done`);
|
|
1680
|
-
}
|
|
1640
|
+
// Keep active agents and terminal history in separate menu entries.
|
|
1641
|
+
const records = manager.listAgents();
|
|
1642
|
+
const { active, history } = splitAgentRecords(records, ctx.cwd);
|
|
1643
|
+
options.push(...buildAgentStatusMenuEntries(records, ctx.cwd));
|
|
1681
1644
|
|
|
1682
1645
|
// Agent types list
|
|
1683
1646
|
if (allNames.length > 0) {
|
|
@@ -1694,7 +1657,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1694
1657
|
options.push("Create new agent");
|
|
1695
1658
|
options.push("Settings");
|
|
1696
1659
|
|
|
1697
|
-
const noAgentsMsg = allNames.length === 0 &&
|
|
1660
|
+
const noAgentsMsg = allNames.length === 0 && active.length === 0 && history.length === 0
|
|
1698
1661
|
? "No agents found. Create specialized subagents that can be delegated to.\n\n" +
|
|
1699
1662
|
"Each subagent has its own context window, custom system prompt, and specific tools.\n\n" +
|
|
1700
1663
|
"Try creating: Code Reviewer, Security Auditor, Test Writer, or Documentation Writer.\n\n"
|
|
@@ -1710,6 +1673,9 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1710
1673
|
if (choice.startsWith("Running agents (")) {
|
|
1711
1674
|
await showRunningAgents(ctx);
|
|
1712
1675
|
await showAgentsMenu(ctx);
|
|
1676
|
+
} else if (choice.startsWith("Agent history (")) {
|
|
1677
|
+
await showAgentHistory(ctx);
|
|
1678
|
+
await showAgentsMenu(ctx);
|
|
1713
1679
|
} else if (choice.startsWith("Agent types (")) {
|
|
1714
1680
|
await showAllAgentsList(ctx);
|
|
1715
1681
|
await showAgentsMenu(ctx);
|
|
@@ -1793,9 +1759,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1793
1759
|
}
|
|
1794
1760
|
|
|
1795
1761
|
async function showRunningAgents(ctx: ExtensionCommandContext) {
|
|
1796
|
-
const agents = manager.listAgents().
|
|
1797
|
-
record.session !== undefined || hasAgentHistory(ctx.cwd, record.transcriptPath),
|
|
1798
|
-
);
|
|
1762
|
+
const { active: agents } = splitAgentRecords(manager.listAgents(), ctx.cwd);
|
|
1799
1763
|
if (agents.length === 0) {
|
|
1800
1764
|
ctx.ui.notify("No agents.", "info");
|
|
1801
1765
|
return;
|
|
@@ -1815,31 +1779,61 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1815
1779
|
if (idx < 0) return;
|
|
1816
1780
|
const record = agents[idx];
|
|
1817
1781
|
|
|
1818
|
-
await viewAgentConversation(ctx, record);
|
|
1782
|
+
await viewAgentConversation(ctx, record, "live");
|
|
1819
1783
|
// Back-navigation: re-show the list
|
|
1820
1784
|
await showRunningAgents(ctx);
|
|
1821
1785
|
}
|
|
1822
1786
|
|
|
1823
|
-
async function
|
|
1824
|
-
|
|
1825
|
-
|
|
1787
|
+
async function showAgentHistory(ctx: ExtensionCommandContext) {
|
|
1788
|
+
const { history } = splitAgentRecords(manager.listAgents(), ctx.cwd);
|
|
1789
|
+
if (history.length === 0) {
|
|
1790
|
+
ctx.ui.notify("No agent history.", "info");
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
const options = history.map(record => formatAgentHistoryOption(record, Date.now()));
|
|
1795
|
+
const choice = await ctx.ui.select("Agent history", options);
|
|
1796
|
+
if (!choice) return;
|
|
1797
|
+
const record = history[options.indexOf(choice)];
|
|
1798
|
+
if (!record) return;
|
|
1799
|
+
|
|
1800
|
+
await viewAgentConversation(ctx, record, "history");
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
async function viewAgentConversation(
|
|
1804
|
+
ctx: ExtensionCommandContext,
|
|
1805
|
+
record: AgentRecord,
|
|
1806
|
+
mode: "live" | "history",
|
|
1807
|
+
) {
|
|
1808
|
+
if (mode === "live" && !canOpenActiveAgent(record)) {
|
|
1809
|
+
ctx.ui.notify(`Agent is ${record.status === "queued" ? "queued" : "expired"} — no history available.`, "info");
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
if (mode === "history" && !canOpenAgentHistory(record, ctx.cwd)) {
|
|
1813
|
+
ctx.ui.notify("No agent history.", "info");
|
|
1826
1814
|
return;
|
|
1827
1815
|
}
|
|
1828
1816
|
|
|
1829
1817
|
const { ConversationViewer, VIEWPORT_HEIGHT_PCT, createStaticConversationSource } = await import("./ui/conversation-viewer.js");
|
|
1830
|
-
const session =
|
|
1831
|
-
?
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1818
|
+
const session = mode === "live"
|
|
1819
|
+
? record.session
|
|
1820
|
+
: (() => {
|
|
1821
|
+
const messages = record.transcriptPath
|
|
1822
|
+
? readAgentHistory(ctx.cwd, record.transcriptPath)
|
|
1823
|
+
: undefined;
|
|
1824
|
+
return messages
|
|
1825
|
+
? createStaticConversationSource(messages)
|
|
1826
|
+
: record.session
|
|
1827
|
+
? createStaticConversationSource(record.session.messages)
|
|
1828
|
+
: undefined;
|
|
1829
|
+
})();
|
|
1836
1830
|
if (!session) {
|
|
1837
|
-
ctx.ui.notify(
|
|
1831
|
+
ctx.ui.notify("No agent history.", "info");
|
|
1838
1832
|
return;
|
|
1839
1833
|
}
|
|
1840
|
-
const activity = agentActivity.get(record.id);
|
|
1841
|
-
const isLive = record.session !== undefined;
|
|
1842
1834
|
|
|
1835
|
+
const activity = agentActivity.get(record.id);
|
|
1836
|
+
const isLive = mode === "live";
|
|
1843
1837
|
await ctx.ui.custom<undefined>(
|
|
1844
1838
|
(tui, theme, keybindings, done) => {
|
|
1845
1839
|
return new ConversationViewer(tui, session, record, activity, theme, done,
|
|
@@ -1849,7 +1843,8 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1849
1843
|
}
|
|
1850
1844
|
} : undefined,
|
|
1851
1845
|
keybindings,
|
|
1852
|
-
isLive ? (message: string) => manager.steer(record.id, message) : undefined
|
|
1846
|
+
isLive ? (message: string) => manager.steer(record.id, message) : undefined,
|
|
1847
|
+
mode === "history" ? { pi, ctx, readOnly: true } : { pi, ctx });
|
|
1853
1848
|
},
|
|
1854
1849
|
{
|
|
1855
1850
|
overlay: true,
|
package/src/output-file.ts
CHANGED
|
@@ -65,8 +65,8 @@ export function streamToOutputFile(
|
|
|
65
65
|
cwd: string,
|
|
66
66
|
historyPath?: string,
|
|
67
67
|
): () => void {
|
|
68
|
-
const outputPaths = historyPath && historyPath !== path ? [path, historyPath] : [path];
|
|
69
68
|
let writtenCount = 1; // initial user prompt already written
|
|
69
|
+
const outputPaths = historyPath && historyPath !== path ? [path, historyPath] : [path];
|
|
70
70
|
|
|
71
71
|
const flush = () => {
|
|
72
72
|
const messages = session.messages;
|
package/src/types.ts
CHANGED
package/src/ui/agent-widget.ts
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
9
|
-
import { hasAgentHistory } from "../agent-history.js";
|
|
10
9
|
import type { AgentManager } from "../agent-manager.js";
|
|
11
10
|
import { getConfig } from "../agent-types.js";
|
|
12
11
|
import type { AgentInvocation, SubagentType, WidgetMode } from "../types.js";
|
|
@@ -238,8 +237,6 @@ export class AgentWidget {
|
|
|
238
237
|
* extension supplies one defaulting to `"background"`.
|
|
239
238
|
*/
|
|
240
239
|
private mode: () => WidgetMode = () => "all",
|
|
241
|
-
/** Current project cwd used to validate reload-restored transcripts. */
|
|
242
|
-
private getCwd: () => string | undefined = () => undefined,
|
|
243
240
|
) {}
|
|
244
241
|
|
|
245
242
|
/**
|
|
@@ -254,11 +251,7 @@ export class AgentWidget {
|
|
|
254
251
|
* - `all`: every agent.
|
|
255
252
|
*/
|
|
256
253
|
private widgetAgents() {
|
|
257
|
-
const all = this.manager.listAgents()
|
|
258
|
-
if (record.status === "running" || record.status === "queued") return true;
|
|
259
|
-
const cwd = this.getCwd();
|
|
260
|
-
return cwd !== undefined && hasAgentHistory(cwd, record.transcriptPath);
|
|
261
|
-
});
|
|
254
|
+
const all = this.manager.listAgents();
|
|
262
255
|
switch (this.mode()) {
|
|
263
256
|
case "off": return [];
|
|
264
257
|
case "background": return all.filter(a => a.isBackground !== false);
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export type ConversationBlockKind = "text" | "tool" | "meta";
|
|
4
|
+
export type ConversationBlockRole = "user" | "assistant" | "tool" | "custom" | "meta";
|
|
5
|
+
|
|
6
|
+
export interface ConversationBlock {
|
|
7
|
+
id: string;
|
|
8
|
+
kind: ConversationBlockKind;
|
|
9
|
+
role: ConversationBlockRole;
|
|
10
|
+
header: string;
|
|
11
|
+
markdown: string;
|
|
12
|
+
copyText: string;
|
|
13
|
+
fullText: string;
|
|
14
|
+
toolLine?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ConversationFormatOptions {
|
|
18
|
+
maxEntryChars?: number;
|
|
19
|
+
maxToolResultChars?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DEFAULT_MAX_CHARS = 16_000;
|
|
23
|
+
const TRUNCATION_NOTICE = "\n… [truncated in subagent viewer]";
|
|
24
|
+
|
|
25
|
+
type UnknownMessage = {
|
|
26
|
+
role?: unknown;
|
|
27
|
+
content?: unknown;
|
|
28
|
+
summary?: unknown;
|
|
29
|
+
command?: unknown;
|
|
30
|
+
output?: unknown;
|
|
31
|
+
toolName?: unknown;
|
|
32
|
+
isError?: unknown;
|
|
33
|
+
customType?: unknown;
|
|
34
|
+
display?: unknown;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function charLimit(value: number | undefined, fallback: number): number {
|
|
38
|
+
if (value === undefined || !Number.isFinite(value)) return fallback;
|
|
39
|
+
return Math.max(0, Math.floor(value));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Normalize terminal/platform line endings, then keep the result bounded. */
|
|
43
|
+
function bound(value: string, limit: number): string {
|
|
44
|
+
const text = value.replace(/\r\n?/g, "\n");
|
|
45
|
+
if (text.length <= limit) return text;
|
|
46
|
+
if (limit <= 0) return "";
|
|
47
|
+
if (limit <= TRUNCATION_NOTICE.length) return TRUNCATION_NOTICE.slice(0, limit);
|
|
48
|
+
return text.slice(0, limit - TRUNCATION_NOTICE.length) + TRUNCATION_NOTICE;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function asString(value: unknown): string | undefined {
|
|
52
|
+
return typeof value === "string" ? value : undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Extract displayable text without leaking provider-specific or unknown parts. */
|
|
56
|
+
function contentText(content: unknown): string {
|
|
57
|
+
if (typeof content === "string") return content;
|
|
58
|
+
if (!Array.isArray(content)) return "";
|
|
59
|
+
|
|
60
|
+
const parts: string[] = [];
|
|
61
|
+
for (const part of content) {
|
|
62
|
+
if (!part || typeof part !== "object") continue;
|
|
63
|
+
const item = part as Record<string, unknown>;
|
|
64
|
+
if (item.type === "text" && typeof item.text === "string") {
|
|
65
|
+
parts.push(item.text);
|
|
66
|
+
} else if (item.type === "image") {
|
|
67
|
+
parts.push("[image]");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return parts.join("\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function firstNonEmptyLine(text: string): string {
|
|
74
|
+
const line = text.split("\n").find((candidate) => candidate.trim().length > 0);
|
|
75
|
+
if (!line) return "(no output)";
|
|
76
|
+
return line.trim().replace(/\s+/g, " ");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function toolSummary(name: string, call: Record<string, unknown>): string {
|
|
80
|
+
const args = call.arguments && typeof call.arguments === "object"
|
|
81
|
+
? call.arguments as Record<string, unknown>
|
|
82
|
+
: call.input && typeof call.input === "object"
|
|
83
|
+
? call.input as Record<string, unknown>
|
|
84
|
+
: {};
|
|
85
|
+
|
|
86
|
+
const path = asString(args.path) ?? asString(args.filePath) ?? asString(args.filename);
|
|
87
|
+
if (["read", "edit", "write"].includes(name) && path) return path;
|
|
88
|
+
if (asString(args.command)) return asString(args.command)!;
|
|
89
|
+
if (path) return path;
|
|
90
|
+
try {
|
|
91
|
+
const encoded = JSON.stringify(args);
|
|
92
|
+
return encoded === "{}" ? "" : encoded;
|
|
93
|
+
} catch {
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function blockId(messageIndex: number, emitted: number, toolIndex?: number): string {
|
|
99
|
+
if (emitted === 0) return `message-${messageIndex}`;
|
|
100
|
+
return `message-${messageIndex}-tool-${toolIndex ?? emitted}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function textBlock(
|
|
104
|
+
id: string,
|
|
105
|
+
role: "user" | "assistant",
|
|
106
|
+
markdown: string,
|
|
107
|
+
): ConversationBlock {
|
|
108
|
+
return {
|
|
109
|
+
id,
|
|
110
|
+
kind: "text",
|
|
111
|
+
role,
|
|
112
|
+
header: role === "user" ? "User" : "Assistant",
|
|
113
|
+
markdown,
|
|
114
|
+
copyText: markdown,
|
|
115
|
+
fullText: markdown,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function toolCallBlock(
|
|
120
|
+
id: string,
|
|
121
|
+
name: string,
|
|
122
|
+
summary: string,
|
|
123
|
+
): ConversationBlock {
|
|
124
|
+
const toolLine = `· ${name} → ${summary || "(no details)"}`;
|
|
125
|
+
return {
|
|
126
|
+
id,
|
|
127
|
+
kind: "tool",
|
|
128
|
+
role: "assistant",
|
|
129
|
+
header: "Assistant tool call",
|
|
130
|
+
markdown: "",
|
|
131
|
+
copyText: toolLine,
|
|
132
|
+
fullText: toolLine,
|
|
133
|
+
toolLine,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function toolResultBlock(
|
|
138
|
+
id: string,
|
|
139
|
+
message: UnknownMessage,
|
|
140
|
+
maxChars: number,
|
|
141
|
+
): ConversationBlock | undefined {
|
|
142
|
+
const name = asString(message.toolName) ?? "tool";
|
|
143
|
+
const output = contentText(message.content);
|
|
144
|
+
const normalized = output.replace(/\r\n?/g, "\n");
|
|
145
|
+
if (!normalized.trim() && maxChars === 0) return undefined;
|
|
146
|
+
const success = message.isError === true ? "✗" : "✓";
|
|
147
|
+
const lineCount = normalized.length === 0 ? 0 : normalized.split("\n").length;
|
|
148
|
+
const charCount = normalized.length;
|
|
149
|
+
const summary = firstNonEmptyLine(normalized);
|
|
150
|
+
const toolLine = `· ${name} ${success} ${summary} ${lineCount}L/${charCount}c`;
|
|
151
|
+
const fullText = bound(`${name} ${success}\n${normalized}`, maxChars);
|
|
152
|
+
return {
|
|
153
|
+
id,
|
|
154
|
+
kind: "tool",
|
|
155
|
+
role: "tool",
|
|
156
|
+
header: `${name} result`,
|
|
157
|
+
markdown: "",
|
|
158
|
+
copyText: fullText,
|
|
159
|
+
fullText,
|
|
160
|
+
toolLine,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function bashBlock(
|
|
165
|
+
id: string,
|
|
166
|
+
message: UnknownMessage,
|
|
167
|
+
maxChars: number,
|
|
168
|
+
): ConversationBlock | undefined {
|
|
169
|
+
const command = asString(message.command) ?? "";
|
|
170
|
+
const output = asString(message.output) ?? contentText(message.content);
|
|
171
|
+
const body = command ? `$ ${command}${output ? `\n${output}` : ""}` : output;
|
|
172
|
+
if (!body.trim() && maxChars === 0) return undefined;
|
|
173
|
+
const toolLine = `· bash → ${command || "(no command)"}`;
|
|
174
|
+
const fullText = bound(body, maxChars);
|
|
175
|
+
return {
|
|
176
|
+
id,
|
|
177
|
+
kind: "tool",
|
|
178
|
+
role: "tool",
|
|
179
|
+
header: "bash",
|
|
180
|
+
markdown: "",
|
|
181
|
+
copyText: fullText,
|
|
182
|
+
fullText,
|
|
183
|
+
toolLine,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function metaBlock(
|
|
188
|
+
id: string,
|
|
189
|
+
header: string,
|
|
190
|
+
value: unknown,
|
|
191
|
+
maxChars: number,
|
|
192
|
+
): ConversationBlock | undefined {
|
|
193
|
+
const text = typeof value === "string" ? value : contentText(value);
|
|
194
|
+
const markdown = bound(text, maxChars);
|
|
195
|
+
if (!markdown.trim()) return undefined;
|
|
196
|
+
return {
|
|
197
|
+
id,
|
|
198
|
+
kind: "meta",
|
|
199
|
+
role: "meta",
|
|
200
|
+
header,
|
|
201
|
+
markdown,
|
|
202
|
+
copyText: markdown,
|
|
203
|
+
fullText: markdown,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function formatConversationMessages(
|
|
208
|
+
messages: AgentSession["messages"],
|
|
209
|
+
options?: ConversationFormatOptions,
|
|
210
|
+
): ConversationBlock[] {
|
|
211
|
+
const maxEntryChars = charLimit(options?.maxEntryChars, DEFAULT_MAX_CHARS);
|
|
212
|
+
const maxToolResultChars = charLimit(options?.maxToolResultChars, DEFAULT_MAX_CHARS);
|
|
213
|
+
const blocks: ConversationBlock[] = [];
|
|
214
|
+
|
|
215
|
+
for (let messageIndex = 0; messageIndex < messages.length; messageIndex++) {
|
|
216
|
+
const message = messages[messageIndex] as unknown as UnknownMessage;
|
|
217
|
+
const role = message?.role;
|
|
218
|
+
let emitted = 0;
|
|
219
|
+
|
|
220
|
+
if (role === "user") {
|
|
221
|
+
const text = bound(contentText(message.content), maxEntryChars);
|
|
222
|
+
if (text.trim()) blocks.push(textBlock(blockId(messageIndex, emitted++), "user", text));
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (role === "assistant") {
|
|
227
|
+
const parts = Array.isArray(message.content) ? message.content : [];
|
|
228
|
+
const text = bound(contentText(parts), maxEntryChars);
|
|
229
|
+
if (text.trim()) blocks.push(textBlock(blockId(messageIndex, emitted++), "assistant", text));
|
|
230
|
+
|
|
231
|
+
let toolIndex = 0;
|
|
232
|
+
for (const part of parts) {
|
|
233
|
+
if (!part || typeof part !== "object" || (part as any).type !== "toolCall") continue;
|
|
234
|
+
const call = part as Record<string, unknown>;
|
|
235
|
+
const name = asString(call.name) ?? asString(call.toolName) ?? "unknown";
|
|
236
|
+
blocks.push(toolCallBlock(blockId(messageIndex, emitted++, toolIndex), name, toolSummary(name, call)));
|
|
237
|
+
toolIndex++;
|
|
238
|
+
}
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (role === "toolResult") {
|
|
243
|
+
const block = toolResultBlock(blockId(messageIndex, emitted), message, maxToolResultChars);
|
|
244
|
+
if (block) blocks.push(block);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (role === "bashExecution") {
|
|
249
|
+
const block = bashBlock(blockId(messageIndex, emitted), message, maxToolResultChars);
|
|
250
|
+
if (block) blocks.push(block);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (role === "custom") {
|
|
255
|
+
if (message.display === false) continue;
|
|
256
|
+
const header = asString(message.customType) || "Custom";
|
|
257
|
+
const block = metaBlock(blockId(messageIndex, emitted), header, message.content, maxEntryChars);
|
|
258
|
+
if (block) blocks.push({ ...block, role: "custom" });
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (role === "compactionSummary" || role === "branchSummary" || role === "summary") {
|
|
263
|
+
const header = role === "summary" ? "Summary" : role === "compactionSummary" ? "Compaction summary" : "Branch summary";
|
|
264
|
+
const block = metaBlock(blockId(messageIndex, emitted), header, message.summary, maxEntryChars);
|
|
265
|
+
if (block) blocks.push(block);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return blocks;
|
|
270
|
+
}
|