brl-subagent 2.3.9 → 2.5.0
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/AGENT.md +5 -0
- package/README.md +20 -8
- package/package.json +11 -9
- package/src/concurrency.ts +1 -0
- package/src/diff.ts +1 -0
- package/src/event-bus.ts +8 -2
- package/src/git.ts +68 -0
- package/src/history.ts +59 -1
- package/src/index.ts +244 -406
- package/src/kill-escalation.ts +94 -0
- package/src/logging.ts +58 -4
- package/src/messaging.ts +1 -0
- package/src/metrics.ts +1 -0
- package/src/model-availability.ts +1 -0
- package/src/notify-completion.ts +1 -0
- package/src/params.ts +1 -0
- package/src/paths.ts +1 -0
- package/src/preflight.ts +30 -14
- package/src/prelude.ts +1 -0
- package/src/presets.ts +1 -0
- package/src/proc.ts +140 -0
- package/src/prompt.ts +1 -0
- package/src/recovery-engine.ts +262 -0
- package/src/recovery.ts +325 -0
- package/src/reports.ts +64 -8
- package/src/router.ts +1 -0
- package/src/run-registry.ts +269 -0
- package/src/runner.ts +207 -16
- package/src/sanitize.ts +54 -3
- package/src/scheduler.ts +1 -0
- package/src/schema.ts +254 -0
- package/src/session-manager.ts +142 -111
- package/src/state.ts +52 -7
- package/src/templates.ts +1 -0
- package/src/transcript-path.ts +41 -0
- package/src/transcript-tail.ts +1 -0
- package/src/transcript.ts +14 -12
- package/src/tui-format.ts +1 -0
- package/src/tui.ts +231 -296
- package/src/types.ts +130 -2
- package/src/unit-run.ts +9 -0
- package/src/validate.ts +16 -2
package/src/tui.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Purpose: All TUI surfaces: config menus, preset/template managers, history, live monitor, and result rendering.
|
|
1
2
|
/**
|
|
2
3
|
* brl-subagent — TUI Components
|
|
3
4
|
*
|
|
@@ -13,11 +14,10 @@ import type {
|
|
|
13
14
|
ToolRenderResultOptions,
|
|
14
15
|
} from "@earendil-works/pi-coding-agent";
|
|
15
16
|
import { DynamicBorder, getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
16
|
-
import { Container, type SelectItem, SelectList, Spacer, Text, Markdown } from "@earendil-works/pi-tui";
|
|
17
|
+
import { Container, type Component, type SelectItem, SelectList, Spacer, Text, Markdown, matchesKey } from "@earendil-works/pi-tui";
|
|
17
18
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
18
19
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
19
20
|
import type {
|
|
20
|
-
SubagentPreset,
|
|
21
21
|
TaskTemplate,
|
|
22
22
|
SubagentResult,
|
|
23
23
|
SubagentRun,
|
|
@@ -28,7 +28,6 @@ import type {
|
|
|
28
28
|
GraphDetails,
|
|
29
29
|
DelegateTaskDetails,
|
|
30
30
|
GraphTask,
|
|
31
|
-
SubTaskResult,
|
|
32
31
|
ThinkingLevel,
|
|
33
32
|
UsageStats,
|
|
34
33
|
FileDiff,
|
|
@@ -42,8 +41,7 @@ import {
|
|
|
42
41
|
COLLAPSED_OUTPUT_LINES,
|
|
43
42
|
COLLAPSED_DIFF_FILES_PREVIEW,
|
|
44
43
|
EXPANDED_HUNKS_PER_FILE,
|
|
45
|
-
|
|
46
|
-
RESERVED_COMMAND_NAMES,
|
|
44
|
+
DEFAULT_OUTPUT_CAP_BYTES,
|
|
47
45
|
formatTokens,
|
|
48
46
|
formatUsageStats,
|
|
49
47
|
formatModel,
|
|
@@ -52,15 +50,16 @@ import {
|
|
|
52
50
|
isSubagentError,
|
|
53
51
|
isMultiSubagentDetails,
|
|
54
52
|
isGraphDetails,
|
|
53
|
+
isUnsettledPartial,
|
|
55
54
|
} from "./types";
|
|
56
55
|
import { buildFileAccessReport, buildSecretsExposureReport, generateComplianceSummary } from "./reports";
|
|
57
|
-
import { extractParamNames
|
|
56
|
+
import { extractParamNames } from "./templates";
|
|
58
57
|
import { parseDiff } from "./diff";
|
|
59
|
-
import { formatPresetSummary, getPreset,
|
|
60
|
-
import { formatRunDuration } from "./history";
|
|
58
|
+
import { formatPresetSummary, getPreset, loadCustomPresets } from "./presets";
|
|
59
|
+
import { buildOutputHonestyLines, formatRunDuration } from "./history";
|
|
61
60
|
import * as fs from "node:fs";
|
|
62
61
|
import * as path from "node:path";
|
|
63
|
-
import { sweepStaleLiveSubagents, type SessionState } from "./state";
|
|
62
|
+
import { collapseRunsForHistory, sweepStaleLiveSubagents, type SessionState } from "./state";
|
|
64
63
|
import { getAgent } from "./session-manager";
|
|
65
64
|
import { computeSLAMetrics, computeCostTrend, formatSparkline } from "./metrics";
|
|
66
65
|
import { formatElapsed, liveRowName, liveSpinner, formatLiveRowDim } from "./tui-format";
|
|
@@ -638,185 +637,6 @@ export async function showComplianceMenu(
|
|
|
638
637
|
|
|
639
638
|
// Preset management UI
|
|
640
639
|
|
|
641
|
-
export async function showAddPreset(
|
|
642
|
-
ctx: ExtensionContext,
|
|
643
|
-
state: SessionState,
|
|
644
|
-
): Promise<void> {
|
|
645
|
-
// 1. Prompt for name
|
|
646
|
-
const name = await ctx.ui.input("Preset name (e.g., read-only-audit):");
|
|
647
|
-
if (!name?.trim()) return;
|
|
648
|
-
const trimmedName = name.trim();
|
|
649
|
-
|
|
650
|
-
// 2. Validate name (reserved patterns, existing presets)
|
|
651
|
-
if (RESERVED_NAME_PATTERN.test(trimmedName)) {
|
|
652
|
-
ctx.ui.notify("Names starting and ending with '__' are reserved.", "error");
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
655
|
-
if (RESERVED_COMMAND_NAMES.has(trimmedName)) {
|
|
656
|
-
ctx.ui.notify("Name '" + trimmedName + "' is reserved for a command.", "error");
|
|
657
|
-
return;
|
|
658
|
-
}
|
|
659
|
-
if (getPreset(trimmedName, state.builtinPresets, state.customPresets)) {
|
|
660
|
-
ctx.ui.notify(`Preset "${trimmedName}" already exists.`, "error");
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// 3. Description
|
|
665
|
-
const description = await ctx.ui.input("Description (optional):");
|
|
666
|
-
|
|
667
|
-
// 4. Thinking level
|
|
668
|
-
const thinkingItems: SelectItem[] = [
|
|
669
|
-
{ value: "", label: "(not set — use conductor's choice)" },
|
|
670
|
-
...THINKING_LEVELS.map((level) => ({ value: level, label: level })),
|
|
671
|
-
];
|
|
672
|
-
const thinkingResult = await showSelectList(ctx, "Default Thinking Level", thinkingItems, 8);
|
|
673
|
-
|
|
674
|
-
// 5. Tool scope
|
|
675
|
-
const scopeItems: SelectItem[] = [
|
|
676
|
-
{ value: "all", label: "All tools (default)" },
|
|
677
|
-
{ value: "readonly", label: "Read-only (read, grep, find, ls)" },
|
|
678
|
-
{ value: "custom", label: "Custom tool list..." },
|
|
679
|
-
];
|
|
680
|
-
const scopeResult = await showSelectList(ctx, "Tool Scope", scopeItems, 5);
|
|
681
|
-
|
|
682
|
-
let tools: string[] | undefined;
|
|
683
|
-
let excludeTools: string[] | undefined;
|
|
684
|
-
let noBuiltinTools: boolean | undefined;
|
|
685
|
-
|
|
686
|
-
if (scopeResult === "readonly") {
|
|
687
|
-
tools = ["read", "grep", "find", "ls"];
|
|
688
|
-
excludeTools = ["write", "edit", "bash"];
|
|
689
|
-
} else if (scopeResult === "custom") {
|
|
690
|
-
const toolsStr = await ctx.ui.input("Tools (comma-separated):");
|
|
691
|
-
if (toolsStr?.trim()) tools = toolsStr.split(",").map((t) => t.trim()).filter(Boolean);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
// 6. Inheritance
|
|
695
|
-
const inheritItems: SelectItem[] = [
|
|
696
|
-
{ value: "true", label: "Inherit system prompt (default)" },
|
|
697
|
-
{ value: "false", label: "No inheritance (standalone)" },
|
|
698
|
-
];
|
|
699
|
-
const inheritResult = await showSelectList(ctx, "System Prompt Inheritance", inheritItems, 3);
|
|
700
|
-
|
|
701
|
-
// 7. System prompt body
|
|
702
|
-
const systemPrompt = await ctx.ui.input("System prompt (optional, multi-line not supported yet):");
|
|
703
|
-
|
|
704
|
-
// 8. Build preset object
|
|
705
|
-
const preset: SubagentPreset = {
|
|
706
|
-
name: trimmedName,
|
|
707
|
-
description: description?.trim() || undefined,
|
|
708
|
-
thinkingLevel: thinkingResult || undefined,
|
|
709
|
-
inheritSystemPrompt: inheritResult === "false" ? false : undefined,
|
|
710
|
-
tools,
|
|
711
|
-
excludeTools,
|
|
712
|
-
noBuiltinTools,
|
|
713
|
-
systemPrompt: systemPrompt?.trim() || undefined,
|
|
714
|
-
};
|
|
715
|
-
|
|
716
|
-
// 9. Ask where to save
|
|
717
|
-
const locationItems: SelectItem[] = [
|
|
718
|
-
{ value: "project", label: "Project (.pi/brl-subagent/presets/)" },
|
|
719
|
-
{ value: "global", label: "Global (~/.pi/agent/brl-subagent/presets/)" },
|
|
720
|
-
];
|
|
721
|
-
const location = await showSelectList(ctx, "Save Location", locationItems, 3);
|
|
722
|
-
if (!location) return;
|
|
723
|
-
|
|
724
|
-
const homedir = process.env.HOME || process.env.USERPROFILE || "";
|
|
725
|
-
const targetDir = location === "project"
|
|
726
|
-
? path.join(ctx.cwd, ".pi", "brl-subagent", "presets")
|
|
727
|
-
: path.join(homedir, ".pi", "agent", "brl-subagent", "presets");
|
|
728
|
-
|
|
729
|
-
// 10. Write file
|
|
730
|
-
try {
|
|
731
|
-
writePresetFile(preset, targetDir);
|
|
732
|
-
// 11. Refresh custom presets
|
|
733
|
-
state.customPresets = loadCustomPresets(ctx.cwd, state.log);
|
|
734
|
-
// Reload templates alongside presets — both are file-derived, and a
|
|
735
|
-
// preset add/remove may fix (or break) a template's `preset:` reference.
|
|
736
|
-
// Full stack: custom (project+global) merged over builtins.
|
|
737
|
-
state.config.templates = loadAllTemplates(ctx.cwd, state.log);
|
|
738
|
-
// Issue #81: re-check template `preset:` refs after the mutation — warn
|
|
739
|
-
// (never skip) for dangling references instead of preset-less runs.
|
|
740
|
-
validateTemplatePresetRefs(
|
|
741
|
-
state.config.templates,
|
|
742
|
-
getAllPresets(state.builtinPresets, state.customPresets),
|
|
743
|
-
state.log,
|
|
744
|
-
);
|
|
745
|
-
ctx.ui.notify(`Preset "${trimmedName}" saved to ${location === "project" ? "project" : "global"} directory`, "info");
|
|
746
|
-
} catch (err) {
|
|
747
|
-
ctx.ui.notify(`Failed to save preset: ${(err as Error).message}`, "error");
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
export async function showRemovePreset(
|
|
752
|
-
ctx: ExtensionContext,
|
|
753
|
-
state: SessionState,
|
|
754
|
-
): Promise<void> {
|
|
755
|
-
const homedir = process.env.HOME || process.env.USERPROFILE || "";
|
|
756
|
-
const projectDir = path.join(ctx.cwd, ".pi", "brl-subagent", "presets");
|
|
757
|
-
const globalDir = path.join(homedir, ".pi", "agent", "brl-subagent", "presets");
|
|
758
|
-
|
|
759
|
-
// Scan both directories for .md files
|
|
760
|
-
const entries: Array<{ name: string; source: "project" | "global"; filePath: string }> = [];
|
|
761
|
-
|
|
762
|
-
for (const dir of [projectDir, globalDir]) {
|
|
763
|
-
const source = dir === projectDir ? "project" : "global";
|
|
764
|
-
try {
|
|
765
|
-
const files = fs.readdirSync(dir);
|
|
766
|
-
for (const file of files) {
|
|
767
|
-
if (!file.endsWith(".md")) continue;
|
|
768
|
-
const filePath = path.join(dir, file);
|
|
769
|
-
try {
|
|
770
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
771
|
-
const { meta } = parseFrontmatter(content);
|
|
772
|
-
if (meta.name && typeof meta.name === "string") {
|
|
773
|
-
entries.push({ name: meta.name as string, source, filePath });
|
|
774
|
-
}
|
|
775
|
-
} catch {
|
|
776
|
-
// Skip unparseable files
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
} catch {
|
|
780
|
-
// Directory doesn't exist — fine
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
if (entries.length === 0) {
|
|
785
|
-
ctx.ui.notify("No custom presets to remove.", "info");
|
|
786
|
-
return;
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// Show list with source indicators
|
|
790
|
-
const items: SelectItem[] = entries.map((e) => ({
|
|
791
|
-
value: e.filePath,
|
|
792
|
-
label: `[${e.source === "project" ? "P" : "G"}] ${e.name}`,
|
|
793
|
-
description: e.source === "project" ? "Project-local" : "Global",
|
|
794
|
-
}));
|
|
795
|
-
|
|
796
|
-
const result = await showSelectList(ctx, "Remove Preset", items, 10);
|
|
797
|
-
if (!result) return;
|
|
798
|
-
|
|
799
|
-
// Delete the file
|
|
800
|
-
try {
|
|
801
|
-
fs.unlinkSync(result);
|
|
802
|
-
state.customPresets = loadCustomPresets(ctx.cwd, state.log);
|
|
803
|
-
// Reload templates too — a removed preset may orphan a template's
|
|
804
|
-
// `preset:` reference, so both file-derived collections refresh together.
|
|
805
|
-
// Full stack: custom (project+global) merged over builtins.
|
|
806
|
-
state.config.templates = loadAllTemplates(ctx.cwd, state.log);
|
|
807
|
-
// Issue #81: re-check template `preset:` refs after the mutation — warn
|
|
808
|
-
// (never skip) for dangling references instead of preset-less runs.
|
|
809
|
-
validateTemplatePresetRefs(
|
|
810
|
-
state.config.templates,
|
|
811
|
-
getAllPresets(state.builtinPresets, state.customPresets),
|
|
812
|
-
state.log,
|
|
813
|
-
);
|
|
814
|
-
ctx.ui.notify("Preset removed", "info");
|
|
815
|
-
} catch (err) {
|
|
816
|
-
ctx.ui.notify(`Failed to remove preset: ${(err as Error).message}`, "error");
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
|
|
820
640
|
export async function showPresetManager(
|
|
821
641
|
ctx: ExtensionContext,
|
|
822
642
|
state: SessionState,
|
|
@@ -1270,127 +1090,253 @@ export async function showRunHistory(
|
|
|
1270
1090
|
state: SessionState,
|
|
1271
1091
|
persistState: () => void,
|
|
1272
1092
|
): Promise<void> {
|
|
1273
|
-
const runs = state.getRunEntries(ctx).reverse();
|
|
1274
|
-
|
|
1275
|
-
if (runs.length === 0) {
|
|
1276
|
-
ctx.ui.notify("No subagent runs recorded yet. Delegate a task to see history.", "info");
|
|
1277
|
-
return;
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
1093
|
const statusIcon: Record<string, string> = {
|
|
1281
1094
|
running: ctx.ui.theme.fg("accent", "◉"),
|
|
1282
1095
|
done: ctx.ui.theme.fg("success", "✓"),
|
|
1283
1096
|
failed: ctx.ui.theme.fg("error", "✗"),
|
|
1284
1097
|
};
|
|
1285
1098
|
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
const
|
|
1293
|
-
|
|
1294
|
-
|
|
1099
|
+
// Browse loop (issue #260): list → detail → list … until the LIST is exited
|
|
1100
|
+
// with Esc. Closing a detail returns here, not to the /brl-subagent menu.
|
|
1101
|
+
while (true) {
|
|
1102
|
+
// One row per SETTLED run (issue #259): collapse each run's spawn+terminal
|
|
1103
|
+
// entry pair to its terminal-preferred record; in-flight runs are omitted
|
|
1104
|
+
// (the live monitor owns running state).
|
|
1105
|
+
const runs = collapseRunsForHistory(state.getRunEntries(ctx));
|
|
1106
|
+
|
|
1107
|
+
if (runs.length === 0) {
|
|
1108
|
+
ctx.ui.notify("No subagent runs recorded yet. Delegate a task to see history.", "info");
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1295
1111
|
|
|
1296
|
-
|
|
1297
|
-
|
|
1112
|
+
const items: SelectItem[] = runs.map((r) => {
|
|
1113
|
+
const icon = statusIcon[r.status] || "·";
|
|
1114
|
+
const name = r.label || r.task.slice(0, 60);
|
|
1115
|
+
const model = r.model.split("/").pop() || r.model;
|
|
1116
|
+
const when = r.finishedAt ? formatRunDuration(r.durationMs || 0) : "running...";
|
|
1117
|
+
const cost = r.cost ? ` $${r.cost.toFixed(4)}` : "";
|
|
1118
|
+
const desc = `${r.thinkingLevel} · ${model} · ${when}${cost}`;
|
|
1119
|
+
return { value: r.id, label: `${icon} ${name}`, description: desc };
|
|
1120
|
+
});
|
|
1298
1121
|
|
|
1299
|
-
|
|
1300
|
-
|
|
1122
|
+
const selectedId = await showSelectList(ctx, "Subagent History", items, 15);
|
|
1123
|
+
if (!selectedId) return; // Esc at the list exits to the menu
|
|
1301
1124
|
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1125
|
+
const run = runs.find((r) => r.id === selectedId);
|
|
1126
|
+
if (!run) continue; // selected run pruned since listing — re-show the list
|
|
1127
|
+
|
|
1128
|
+
// Mark as seen
|
|
1129
|
+
if (state.markRunSeen(run.id)) {
|
|
1130
|
+
persistState();
|
|
1131
|
+
import("./concurrency").then(({ updateProgressStatus }) => {
|
|
1132
|
+
updateProgressStatus(state, ctx);
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
await showRunDetail(ctx, run);
|
|
1308
1137
|
}
|
|
1138
|
+
}
|
|
1309
1139
|
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1140
|
+
/**
|
|
1141
|
+
* Number of output lines a run-detail viewport shows at once (issue #261):
|
|
1142
|
+
* the stored text can be up to the 100 KB cap, so the panel is height-capped
|
|
1143
|
+
* and scrolled rather than dumped whole (which would push the footer and
|
|
1144
|
+
* header off-screen).
|
|
1145
|
+
*/
|
|
1146
|
+
const RUN_DETAIL_OUTPUT_VIEWPORT_LINES = 12;
|
|
1314
1147
|
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
)
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1148
|
+
/**
|
|
1149
|
+
* Detail view for one settled run (issue #260, #261): ↑/↓ and PgUp/PgDn scroll
|
|
1150
|
+
* the stored output; any OTHER key — including Esc — closes it. The
|
|
1151
|
+
* showRunHistory browse loop re-shows the list on return.
|
|
1152
|
+
*
|
|
1153
|
+
* The output region follows the approval dialog's full-diff view structure
|
|
1154
|
+
* (see `showApprovalDialog.buildDiffView` / `withDiffKeybinding`: a bordered
|
|
1155
|
+
* Container with a title, the content, and a return footer) but adds the
|
|
1156
|
+
* height cap + scroll state that view lacks. Output is wrapped through the
|
|
1157
|
+
* same `Text` component those views use, so wrapping/padding stay consistent.
|
|
1158
|
+
*/
|
|
1159
|
+
async function showRunDetail(ctx: ExtensionContext, run: SubagentRun): Promise<void> {
|
|
1160
|
+
const outputText = run.fullOutput ?? run.outputSummary ?? "";
|
|
1161
|
+
await ctx.ui.custom<void>((tui, theme, kb, done) => {
|
|
1162
|
+
// First line of the viewport (0 = top). Mutated by scroll keys and
|
|
1163
|
+
// clamped to the wrapped-content length on every render.
|
|
1164
|
+
let scrollTop = 0;
|
|
1165
|
+
// Wrapped output lines for the current width, and the width they were
|
|
1166
|
+
// produced at, so a resize re-wraps and a redraw does not.
|
|
1167
|
+
let wrappedLines: string[] = [];
|
|
1168
|
+
let wrappedWidth = -1;
|
|
1169
|
+
|
|
1170
|
+
const wrapOutput = (width: number): string[] => {
|
|
1171
|
+
if (width === wrappedWidth) return wrappedLines;
|
|
1172
|
+
wrappedLines = new Text(theme.fg("toolOutput", outputText), 1, 0).render(width);
|
|
1173
|
+
wrappedWidth = width;
|
|
1174
|
+
return wrappedLines;
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1177
|
+
const buildOutputSection = (width: number): Container => {
|
|
1178
|
+
const section = new Container();
|
|
1179
|
+
const lines = wrapOutput(width);
|
|
1180
|
+
const total = lines.length;
|
|
1181
|
+
const maxScroll = Math.max(0, total - RUN_DETAIL_OUTPUT_VIEWPORT_LINES);
|
|
1182
|
+
if (scrollTop > maxScroll) scrollTop = maxScroll;
|
|
1183
|
+
if (scrollTop < 0) scrollTop = 0;
|
|
1184
|
+
const from = total === 0 ? 0 : Math.min(scrollTop + 1, total);
|
|
1185
|
+
const to = Math.min(scrollTop + RUN_DETAIL_OUTPUT_VIEWPORT_LINES, total);
|
|
1186
|
+
const indicator =
|
|
1187
|
+
total > RUN_DETAIL_OUTPUT_VIEWPORT_LINES
|
|
1188
|
+
? ` (lines ${from}\u2013${to} of ${total} \u00b7 \u2191\u2193 / PgUp\u00b7PgDn)`
|
|
1189
|
+
: ` (${total} line${total === 1 ? "" : "s"})`;
|
|
1190
|
+
|
|
1191
|
+
section.addChild(
|
|
1341
1192
|
new Text(
|
|
1342
|
-
theme.fg(
|
|
1343
|
-
"dim",
|
|
1344
|
-
`Finished: ${run.finishedAt} (${formatRunDuration(run.durationMs || 0)})`,
|
|
1345
|
-
),
|
|
1193
|
+
theme.fg("muted", "\u2500\u2500\u2500 Output \u2500\u2500\u2500") + theme.fg("dim", indicator),
|
|
1346
1194
|
1,
|
|
1347
1195
|
0,
|
|
1348
1196
|
),
|
|
1349
1197
|
);
|
|
1350
|
-
|
|
1351
|
-
|
|
1198
|
+
|
|
1199
|
+
if (total === 0) {
|
|
1200
|
+
section.addChild(new Text(theme.fg("dim", "(no output recorded)"), 1, 0));
|
|
1201
|
+
return section;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// Height-capped viewport: the output was already wrapped to `width`
|
|
1205
|
+
// above, so hand the sliced lines back directly instead of feeding them
|
|
1206
|
+
// through another Text (which would re-wrap at the padded width).
|
|
1207
|
+
const viewport: Component = {
|
|
1208
|
+
render: () => lines.slice(scrollTop, scrollTop + RUN_DETAIL_OUTPUT_VIEWPORT_LINES),
|
|
1209
|
+
invalidate: () => {
|
|
1210
|
+
wrappedWidth = -1;
|
|
1211
|
+
},
|
|
1212
|
+
};
|
|
1213
|
+
section.addChild(viewport);
|
|
1214
|
+
return section;
|
|
1215
|
+
};
|
|
1216
|
+
|
|
1217
|
+
const buildDetailView = (width: number): Container => {
|
|
1218
|
+
const container = new Container();
|
|
1219
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
1220
|
+
|
|
1221
|
+
const statusLabel =
|
|
1222
|
+
run.status === "done" ? "Completed" : run.status === "failed" ? "Failed" : "Running";
|
|
1352
1223
|
container.addChild(
|
|
1353
1224
|
new Text(
|
|
1354
1225
|
theme.fg(
|
|
1355
|
-
"
|
|
1356
|
-
|
|
1226
|
+
"toolTitle",
|
|
1227
|
+
theme.bold(`${run.label || "Subagent"} — ${statusLabel}`),
|
|
1357
1228
|
),
|
|
1358
1229
|
1,
|
|
1359
1230
|
0,
|
|
1360
1231
|
),
|
|
1361
1232
|
);
|
|
1362
|
-
|
|
1363
|
-
if (run.errorMessage) {
|
|
1364
|
-
container.addChild(new Spacer(1));
|
|
1233
|
+
container.addChild(new Text("", 0, 0));
|
|
1365
1234
|
container.addChild(
|
|
1366
|
-
new Text(theme.fg("
|
|
1235
|
+
new Text(theme.fg("dim", `Task: ${run.task.slice(0, 200)}`), 1, 0),
|
|
1367
1236
|
);
|
|
1368
|
-
}
|
|
1369
|
-
if (run.outputSummary) {
|
|
1370
|
-
container.addChild(new Spacer(1));
|
|
1371
1237
|
container.addChild(
|
|
1372
1238
|
new Text(
|
|
1373
|
-
theme.fg("
|
|
1239
|
+
theme.fg("dim", `Model: ${run.model} · Thinking: ${run.thinkingLevel}`),
|
|
1374
1240
|
1,
|
|
1375
1241
|
0,
|
|
1376
1242
|
),
|
|
1377
1243
|
);
|
|
1378
|
-
container.addChild(
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1244
|
+
container.addChild(new Text(theme.fg("dim", `Started: ${run.startedAt}`), 1, 0));
|
|
1245
|
+
if (run.finishedAt) {
|
|
1246
|
+
container.addChild(
|
|
1247
|
+
new Text(
|
|
1248
|
+
theme.fg(
|
|
1249
|
+
"dim",
|
|
1250
|
+
`Finished: ${run.finishedAt} (${formatRunDuration(run.durationMs || 0)})`,
|
|
1251
|
+
),
|
|
1252
|
+
1,
|
|
1253
|
+
0,
|
|
1254
|
+
),
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
if (run.cost) {
|
|
1258
|
+
container.addChild(
|
|
1259
|
+
new Text(
|
|
1260
|
+
theme.fg(
|
|
1261
|
+
"dim",
|
|
1262
|
+
`Cost: $${run.cost.toFixed(4)} · ↑${formatTokens(run.tokensIn || 0)} ↓${formatTokens(run.tokensOut || 0)}`,
|
|
1263
|
+
),
|
|
1264
|
+
1,
|
|
1265
|
+
0,
|
|
1266
|
+
),
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
if (run.errorMessage) {
|
|
1270
|
+
container.addChild(new Spacer(1));
|
|
1271
|
+
container.addChild(
|
|
1272
|
+
new Text(theme.fg("error", `Error: ${run.errorMessage}`), 1, 0),
|
|
1273
|
+
);
|
|
1383
1274
|
}
|
|
1384
|
-
}
|
|
1385
1275
|
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1276
|
+
container.addChild(new Spacer(1));
|
|
1277
|
+
container.addChild(buildOutputSection(width));
|
|
1278
|
+
// Honest disclosures (issue #261): the transcript is the only place the
|
|
1279
|
+
// uncapped text lives, and the cap is stated when it was hit.
|
|
1280
|
+
for (const line of buildOutputHonestyLines(run, DEFAULT_OUTPUT_CAP_BYTES)) {
|
|
1281
|
+
container.addChild(new Text(theme.fg("dim", line), 1, 0));
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
container.addChild(new Spacer(1));
|
|
1285
|
+
container.addChild(
|
|
1286
|
+
new Text(
|
|
1287
|
+
theme.fg("dim", "\u2191\u2193 scroll \u00b7 alt+\u2191\u2193 page \u00b7 any other key to close"),
|
|
1288
|
+
1,
|
|
1289
|
+
0,
|
|
1290
|
+
),
|
|
1291
|
+
);
|
|
1292
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
1293
|
+
return container;
|
|
1294
|
+
};
|
|
1389
1295
|
|
|
1390
1296
|
return {
|
|
1391
|
-
render: (w: number) =>
|
|
1392
|
-
invalidate: () =>
|
|
1393
|
-
handleInput: (
|
|
1297
|
+
render: (w: number) => buildDetailView(w).render(w),
|
|
1298
|
+
invalidate: () => {},
|
|
1299
|
+
handleInput: (data: string) => {
|
|
1300
|
+
// Paging: alt+↑ / alt+↓. Checked BEFORE the plain arrows so an
|
|
1301
|
+
// alt-modified sequence can never be consumed as a line step.
|
|
1302
|
+
// PgUp/PgDn are unusable here: the host binds them to the
|
|
1303
|
+
// alt-screen transcript scroll (`tui.altScreen.pageUp/pageDown`)
|
|
1304
|
+
// and consumes them before a `ctx.ui.custom` overlay sees input —
|
|
1305
|
+
// pi 1.0.2 has no viewport-owner registration for overlays. The
|
|
1306
|
+
// PgUp/PgDn checks below stay for inline mode, where they do
|
|
1307
|
+
// reach the overlay.
|
|
1308
|
+
if (matchesKey(data, "alt+up")) {
|
|
1309
|
+
scrollTop -= RUN_DETAIL_OUTPUT_VIEWPORT_LINES;
|
|
1310
|
+
tui.requestRender();
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
if (matchesKey(data, "alt+down")) {
|
|
1314
|
+
scrollTop += RUN_DETAIL_OUTPUT_VIEWPORT_LINES;
|
|
1315
|
+
tui.requestRender();
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
if (kb.matches(data, "tui.select.up")) {
|
|
1319
|
+
scrollTop -= 1;
|
|
1320
|
+
tui.requestRender();
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
if (kb.matches(data, "tui.select.down")) {
|
|
1324
|
+
scrollTop += 1;
|
|
1325
|
+
tui.requestRender();
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
if (kb.matches(data, "tui.select.pageUp")) {
|
|
1329
|
+
scrollTop -= RUN_DETAIL_OUTPUT_VIEWPORT_LINES;
|
|
1330
|
+
tui.requestRender();
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
if (kb.matches(data, "tui.select.pageDown")) {
|
|
1334
|
+
scrollTop += RUN_DETAIL_OUTPUT_VIEWPORT_LINES;
|
|
1335
|
+
tui.requestRender();
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
done();
|
|
1339
|
+
},
|
|
1394
1340
|
};
|
|
1395
1341
|
});
|
|
1396
1342
|
}
|
|
@@ -2179,29 +2125,6 @@ function renderCollapsedText(
|
|
|
2179
2125
|
return text;
|
|
2180
2126
|
}
|
|
2181
2127
|
|
|
2182
|
-
// ---------------------------------------------------------------------------
|
|
2183
|
-
// Chain / parallel helper: one-line subtask summary
|
|
2184
|
-
// ---------------------------------------------------------------------------
|
|
2185
|
-
|
|
2186
|
-
function renderSubTaskSummary(
|
|
2187
|
-
result: SubTaskResult,
|
|
2188
|
-
theme: {
|
|
2189
|
-
fg: (color: string, text: string) => string;
|
|
2190
|
-
},
|
|
2191
|
-
maxLines: number,
|
|
2192
|
-
): string {
|
|
2193
|
-
const isError = isSubagentError(result);
|
|
2194
|
-
const icon = isError
|
|
2195
|
-
? theme.fg("error", "\u2717")
|
|
2196
|
-
: theme.fg("success", "\u2713");
|
|
2197
|
-
const label = result.label
|
|
2198
|
-
? theme.fg("accent", `${result.label}: `)
|
|
2199
|
-
: "";
|
|
2200
|
-
const output = getFinalOutput(result.messages);
|
|
2201
|
-
const preview = output.split("\n").slice(0, maxLines).join("\n");
|
|
2202
|
-
return `${icon} ${label}${theme.fg("toolOutput", preview)}`;
|
|
2203
|
-
}
|
|
2204
|
-
|
|
2205
2128
|
// ---------------------------------------------------------------------------
|
|
2206
2129
|
// Chain / parallel collapsed rendering
|
|
2207
2130
|
// ---------------------------------------------------------------------------
|
|
@@ -3008,7 +2931,19 @@ export function renderDelegateResult(
|
|
|
3008
2931
|
}
|
|
3009
2932
|
|
|
3010
2933
|
// Single subagent mode (original)
|
|
3011
|
-
|
|
2934
|
+
// Issue #298 (C2): the partial-vs-settled decision lives in ONE place —
|
|
2935
|
+
// `isUnsettledPartial` (src/types.ts) owns the overloaded `exitCode === -1`
|
|
2936
|
+
// disambiguation (the #206 streaming sentinel vs the #295 settled signal
|
|
2937
|
+
// death). Do not re-derive it here; the terminal-status structural ratchet
|
|
2938
|
+
// fails a local copy. A genuine partial (or a result with no details yet)
|
|
2939
|
+
// renders the raw running text; a classified -1 is a settled FAILURE and
|
|
2940
|
+
// takes the verdict branch below (✗ + error line + expanded-transcript
|
|
2941
|
+
// binding), never raw text.
|
|
2942
|
+
//
|
|
2943
|
+
// The leading `!details` is a type-narrowing aid for the settled branch below
|
|
2944
|
+
// (TS cannot narrow through a function call); it is NOT a second copy of the
|
|
2945
|
+
// decision — the predicate owns the no-details case too.
|
|
2946
|
+
if (!details || isUnsettledPartial(details)) {
|
|
3012
2947
|
const text = result.content[0];
|
|
3013
2948
|
return new Text(text?.type === "text" ? text.text : "(running\u2026)", 0, 0);
|
|
3014
2949
|
}
|