@mrclrchtr/supi-review 1.13.0 → 1.14.1
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/node_modules/@mrclrchtr/supi-core/README.md +10 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +2 -1
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +2 -0
- package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +3 -2
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
- package/node_modules/@mrclrchtr/supi-core/src/progress-widget.ts +98 -17
- package/node_modules/@mrclrchtr/supi-core/src/report.ts +121 -0
- package/package.json +2 -2
- package/src/tool/brief-runner.ts +7 -5
- package/src/tool/review-debug.ts +0 -1
- package/src/tool/review-handlers.ts +115 -19
- package/src/tool/review-runner.ts +3 -0
- package/src/tool/runner-helpers.ts +14 -2
- package/src/tool/session-lifecycle.ts +5 -1
- package/src/types.ts +19 -4
- package/src/ui/format-content.ts +0 -3
- package/src/ui/renderer.ts +0 -3
|
@@ -21,6 +21,7 @@ pnpm add @mrclrchtr/supi-core
|
|
|
21
21
|
## Package surfaces
|
|
22
22
|
|
|
23
23
|
- `@mrclrchtr/supi-core/api` — reusable helpers for other packages and extensions
|
|
24
|
+
- `@mrclrchtr/supi-core/report` — shared text/report rendering helpers for TUI and plain-text summaries
|
|
24
25
|
|
|
25
26
|
## What you get from the API
|
|
26
27
|
|
|
@@ -71,6 +72,14 @@ The built-in settings UI supports:
|
|
|
71
72
|
- active-branch session helper: `getActiveBranchEntries()`
|
|
72
73
|
- terminal helpers such as `formatTitle()`, `signalWaiting()`, and `signalDone()`
|
|
73
74
|
|
|
75
|
+
### Report helpers
|
|
76
|
+
|
|
77
|
+
- `clampReportWidth()` — enforce a minimum readable report width
|
|
78
|
+
- `formatReportTitle()` / `formatSectionHeader()` — shared themed headers
|
|
79
|
+
- `formatDimLine()` / `formatKeyValueLine()` — common summary rows
|
|
80
|
+
- `formatOverflowHint()` — consistent preview-overflow hints
|
|
81
|
+
- `wrapReportText()` — ANSI-aware wrapped report blocks with optional indentation
|
|
82
|
+
|
|
74
83
|
## Example
|
|
75
84
|
|
|
76
85
|
```ts
|
|
@@ -101,3 +110,4 @@ const message = wrapExtensionContext("my-extension", "hello", {
|
|
|
101
110
|
- `src/config.ts` — shared config loading and writing
|
|
102
111
|
- `src/config-settings.ts` — config-backed settings registration helper
|
|
103
112
|
- `src/settings-ui.ts` — shared settings overlay
|
|
113
|
+
- `src/report.ts` — shared text/report rendering helpers
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.1",
|
|
4
4
|
"description": "SuPi core — shared infrastructure for SuPi extensions (XML context tags, config system)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"./llm": "./src/llm.ts",
|
|
49
49
|
"./package.json": "./package.json",
|
|
50
50
|
"./path": "./src/path.ts",
|
|
51
|
+
"./report": "./src/report.ts",
|
|
51
52
|
"./progress-widget": "./src/progress-widget.ts",
|
|
52
53
|
"./project": "./src/project.ts",
|
|
53
54
|
"./session": "./src/session.ts",
|
|
@@ -21,6 +21,8 @@ export * from "./path.ts";
|
|
|
21
21
|
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
22
22
|
export * from "./project.ts";
|
|
23
23
|
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
24
|
+
export * from "./report.ts";
|
|
25
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
24
26
|
export * from "./session.ts";
|
|
25
27
|
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
26
28
|
export * from "./settings.ts";
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import * as os from "node:os";
|
|
9
9
|
import * as path from "node:path";
|
|
10
|
+
import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent";
|
|
10
11
|
|
|
11
|
-
const GLOBAL_CONFIG_DIR =
|
|
12
|
-
const PROJECT_CONFIG_DIR =
|
|
12
|
+
const GLOBAL_CONFIG_DIR = `${CONFIG_DIR_NAME}/agent/supi`;
|
|
13
|
+
const PROJECT_CONFIG_DIR = `${CONFIG_DIR_NAME}/supi`;
|
|
13
14
|
const CONFIG_FILE = "config.json";
|
|
14
15
|
|
|
15
16
|
function getGlobalConfigPath(homeDir?: string): string {
|
|
@@ -19,6 +19,8 @@ export * from "./path.ts";
|
|
|
19
19
|
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
20
20
|
export * from "./project.ts";
|
|
21
21
|
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
22
|
+
export * from "./report.ts";
|
|
23
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
22
24
|
export * from "./session.ts";
|
|
23
25
|
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
24
26
|
export * from "./settings.ts";
|
|
@@ -8,16 +8,38 @@ import { CancellableLoader, Container, Text } from "@earendil-works/pi-tui";
|
|
|
8
8
|
|
|
9
9
|
// ── Types ──────────────────────────────────────────────────────────────────
|
|
10
10
|
|
|
11
|
+
/** What the reviewer is currently doing and on what. */
|
|
12
|
+
export interface CurrentFocus {
|
|
13
|
+
/** Display label for the active tool (e.g. "Reading", "Searching", "Finding"). */
|
|
14
|
+
label: string;
|
|
15
|
+
/** Context detail (e.g. file path, search pattern, directory). */
|
|
16
|
+
detail: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
/** Progress state for widget display, compatible with child-session updates. */
|
|
12
20
|
export interface WidgetProgress {
|
|
13
21
|
/** Number of agent turns completed. */
|
|
14
22
|
turns: number;
|
|
15
23
|
/** Number of tool executions started. */
|
|
16
24
|
toolUses: number;
|
|
17
|
-
/** Human-readable active tool descriptions. */
|
|
18
|
-
activities: string[];
|
|
19
25
|
/** Token usage stats, if available. */
|
|
20
|
-
tokens?: {
|
|
26
|
+
tokens?: {
|
|
27
|
+
input: number;
|
|
28
|
+
output: number;
|
|
29
|
+
total: number;
|
|
30
|
+
cacheRead?: number;
|
|
31
|
+
cacheWrite?: number;
|
|
32
|
+
};
|
|
33
|
+
/** Per-tool execution counts keyed by short display label (e.g. "diffs", "reads", "greps"). */
|
|
34
|
+
toolCounts?: Record<string, number>;
|
|
35
|
+
/** Number of distinct files inspected so far (via read_snapshot_diff / read_snapshot_file). */
|
|
36
|
+
filesInspected?: number;
|
|
37
|
+
/** Total files in the review snapshot. */
|
|
38
|
+
filesTotal?: number;
|
|
39
|
+
/** Current tool + context for the progress narrative line. */
|
|
40
|
+
currentFocus?: CurrentFocus;
|
|
41
|
+
/** Elapsed time in milliseconds since the operation started. */
|
|
42
|
+
elapsedMs?: number;
|
|
21
43
|
}
|
|
22
44
|
|
|
23
45
|
// ── Widget ─────────────────────────────────────────────────────────────────
|
|
@@ -25,12 +47,12 @@ export interface WidgetProgress {
|
|
|
25
47
|
/**
|
|
26
48
|
* TUI progress widget for long-running operations.
|
|
27
49
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
50
|
+
* Two-line layout: top line shows the narrative (current focus + file progress),
|
|
51
|
+
* bottom line shows stats (tokens, elapsed time, turns, tool counts).
|
|
30
52
|
*/
|
|
31
53
|
export class ProgressWidget extends Container {
|
|
32
54
|
private message: string;
|
|
33
|
-
private progress: WidgetProgress = { turns: 0, toolUses: 0
|
|
55
|
+
private progress: WidgetProgress = { turns: 0, toolUses: 0 };
|
|
34
56
|
private loader: CancellableLoader;
|
|
35
57
|
private tui: { requestRender(): void };
|
|
36
58
|
private theme: Theme;
|
|
@@ -79,30 +101,89 @@ export class ProgressWidget extends Container {
|
|
|
79
101
|
|
|
80
102
|
private renderContent(): void {
|
|
81
103
|
this.clear();
|
|
104
|
+
this.renderTopLine();
|
|
105
|
+
this.renderBottomLine();
|
|
106
|
+
}
|
|
82
107
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (this.progress.toolUses > 0) stats.push(`${this.progress.toolUses} tool uses`);
|
|
86
|
-
if (this.progress.tokens) stats.push(`${formatTokens(this.progress.tokens.total)} tokens`);
|
|
108
|
+
private renderTopLine(): void {
|
|
109
|
+
const topParts: string[] = [];
|
|
87
110
|
|
|
88
|
-
|
|
89
|
-
|
|
111
|
+
if (this.progress.currentFocus) {
|
|
112
|
+
const { label, detail } = this.progress.currentFocus;
|
|
113
|
+
topParts.push(detail ? `${label}: ${detail}` : label);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (this.progress.filesTotal && this.progress.filesTotal > 0) {
|
|
117
|
+
const inspected = this.progress.filesInspected ?? 0;
|
|
118
|
+
topParts.push(`${inspected}/${this.progress.filesTotal} files`);
|
|
119
|
+
}
|
|
90
120
|
|
|
121
|
+
const loaderMessage =
|
|
122
|
+
topParts.length > 0 ? `${this.message} · ${topParts.join(" · ")}` : this.message;
|
|
91
123
|
this.loader.setMessage(loaderMessage);
|
|
92
124
|
this.addChild(this.loader);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private renderBottomLine(): void {
|
|
128
|
+
const stats: string[] = [];
|
|
129
|
+
|
|
130
|
+
this.appendTokenStats(stats);
|
|
131
|
+
|
|
132
|
+
if (this.progress.elapsedMs !== undefined && this.progress.elapsedMs >= 1000) {
|
|
133
|
+
stats.push(formatElapsed(this.progress.elapsedMs));
|
|
134
|
+
}
|
|
93
135
|
|
|
94
|
-
if (this.progress.
|
|
95
|
-
this.
|
|
96
|
-
new Text(this.theme.fg("dim", ` ⎿ ${this.progress.activities.join(", ")}…`), 1, 0),
|
|
97
|
-
);
|
|
136
|
+
if (this.progress.turns > 0) {
|
|
137
|
+
stats.push(`⟳ ${this.progress.turns}`);
|
|
98
138
|
}
|
|
139
|
+
|
|
140
|
+
if (this.progress.toolCounts) {
|
|
141
|
+
const parts = Object.entries(this.progress.toolCounts)
|
|
142
|
+
.filter(([, count]) => count > 0)
|
|
143
|
+
.sort(([, a], [, b]) => b - a)
|
|
144
|
+
.map(([label, count]) => `${count} ${label}`);
|
|
145
|
+
if (parts.length > 0) stats.push(parts.join(" · "));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (stats.length > 0) {
|
|
149
|
+
this.addChild(new Text(this.theme.fg("dim", ` ${stats.join(" · ")}`), 1, 0));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private appendTokenStats(stats: string[]): void {
|
|
154
|
+
const tokens = this.progress.tokens;
|
|
155
|
+
if (!tokens) return;
|
|
156
|
+
|
|
157
|
+
stats.push(`↑ ${formatTokens(tokens.input)}`);
|
|
158
|
+
if (tokens.cacheRead !== undefined && tokens.cacheRead > 0) {
|
|
159
|
+
stats.push(`↲ ${formatTokens(tokens.cacheRead)}`);
|
|
160
|
+
}
|
|
161
|
+
if (tokens.cacheWrite !== undefined && tokens.cacheWrite > 0) {
|
|
162
|
+
stats.push(`↱ ${formatTokens(tokens.cacheWrite)}`);
|
|
163
|
+
}
|
|
164
|
+
stats.push(`↓ ${formatTokens(tokens.output)}`);
|
|
99
165
|
}
|
|
100
166
|
}
|
|
101
167
|
|
|
102
168
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
103
169
|
|
|
104
|
-
function formatTokens(count: number): string {
|
|
170
|
+
export function formatTokens(count: number): string {
|
|
105
171
|
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
|
106
172
|
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k`;
|
|
107
173
|
return String(count);
|
|
108
174
|
}
|
|
175
|
+
|
|
176
|
+
export function formatElapsed(ms: number): string {
|
|
177
|
+
const totalSec = Math.floor(ms / 1000);
|
|
178
|
+
const hours = Math.floor(totalSec / 3600);
|
|
179
|
+
const minutes = Math.floor((totalSec % 3600) / 60);
|
|
180
|
+
const seconds = totalSec % 60;
|
|
181
|
+
|
|
182
|
+
if (hours > 0) {
|
|
183
|
+
return `${hours}h ${minutes}m ${seconds}s`;
|
|
184
|
+
}
|
|
185
|
+
if (minutes > 0) {
|
|
186
|
+
return `${minutes}m ${seconds}s`;
|
|
187
|
+
}
|
|
188
|
+
return `${seconds}s`;
|
|
189
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
/** Minimal theme surface required by the shared report helpers. */
|
|
5
|
+
export type ReportTheme = Pick<Theme, "fg">;
|
|
6
|
+
|
|
7
|
+
/** Color keys accepted by the report helpers. */
|
|
8
|
+
export type ReportColor = Parameters<Theme["fg"]>[0];
|
|
9
|
+
|
|
10
|
+
/** Options for rendering a themed key/value report row. */
|
|
11
|
+
export interface KeyValueLineOptions {
|
|
12
|
+
/** The label shown on the left. */
|
|
13
|
+
label: string;
|
|
14
|
+
/** The value shown on the right. */
|
|
15
|
+
value: string;
|
|
16
|
+
/** Theme used for color formatting. */
|
|
17
|
+
theme: ReportTheme;
|
|
18
|
+
/** Maximum rendered width. */
|
|
19
|
+
width: number;
|
|
20
|
+
/** Left indentation in spaces. */
|
|
21
|
+
indent?: number;
|
|
22
|
+
/** Theme color applied to the label. Defaults to `"text"`. */
|
|
23
|
+
labelColor?: ReportColor;
|
|
24
|
+
/** Theme color applied to the value. Defaults to `"dim"`. */
|
|
25
|
+
valueColor?: ReportColor;
|
|
26
|
+
/** Separator placed between label and value. Defaults to `": "`. */
|
|
27
|
+
separator?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Options for rendering a preview-overflow hint. */
|
|
31
|
+
export interface OverflowHintOptions {
|
|
32
|
+
/** Optional follow-up hint such as `run /supi-context full`. */
|
|
33
|
+
hint?: string | null;
|
|
34
|
+
/** Left indentation in spaces. Defaults to `2`. */
|
|
35
|
+
indent?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Ensure a report width never drops below the minimum readable width. */
|
|
39
|
+
export function clampReportWidth(width: number, minWidth = 24): number {
|
|
40
|
+
return Math.max(minWidth, width);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Render a top-level report title line with theme color and truncation. */
|
|
44
|
+
export function formatReportTitle(
|
|
45
|
+
title: string,
|
|
46
|
+
theme: ReportTheme,
|
|
47
|
+
width: number,
|
|
48
|
+
color: ReportColor = "accent",
|
|
49
|
+
): string {
|
|
50
|
+
return truncateToWidth(theme.fg(color, title), width);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Render a section header with optional dimmed metadata.
|
|
55
|
+
*
|
|
56
|
+
* Example: `Usage by category 42.3k tokens`
|
|
57
|
+
*/
|
|
58
|
+
export function formatSectionHeader(
|
|
59
|
+
title: string,
|
|
60
|
+
meta: string | null,
|
|
61
|
+
theme: ReportTheme,
|
|
62
|
+
width: number,
|
|
63
|
+
): string {
|
|
64
|
+
const left = theme.fg("text", title);
|
|
65
|
+
const content = meta ? `${left}${theme.fg("dim", ` ${meta}`)}` : left;
|
|
66
|
+
return truncateToWidth(content, width);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Render a single dimmed report line with optional left indentation. */
|
|
70
|
+
export function formatDimLine(text: string, theme: ReportTheme, width: number, indent = 0): string {
|
|
71
|
+
const safeIndent = Math.max(0, indent);
|
|
72
|
+
return truncateToWidth(`${" ".repeat(safeIndent)}${theme.fg("dim", text)}`, width);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Render a dimmed preview-overflow hint such as `… and 3 more — run /foo full`. */
|
|
76
|
+
export function formatOverflowHint(
|
|
77
|
+
hiddenCount: number,
|
|
78
|
+
theme: ReportTheme,
|
|
79
|
+
width: number,
|
|
80
|
+
options: OverflowHintOptions = {},
|
|
81
|
+
): string {
|
|
82
|
+
const { hint = null, indent = 2 } = options;
|
|
83
|
+
const suffix = hint ? ` — ${hint}` : "";
|
|
84
|
+
return formatDimLine(`… and ${hiddenCount} more${suffix}`, theme, width, indent);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Render a single themed `label: value` row with truncation. */
|
|
88
|
+
export function formatKeyValueLine(options: KeyValueLineOptions): string {
|
|
89
|
+
const {
|
|
90
|
+
label,
|
|
91
|
+
value,
|
|
92
|
+
theme,
|
|
93
|
+
width,
|
|
94
|
+
indent = 2,
|
|
95
|
+
labelColor = "text",
|
|
96
|
+
valueColor = "dim",
|
|
97
|
+
separator = ": ",
|
|
98
|
+
} = options;
|
|
99
|
+
const safeIndent = Math.max(0, indent);
|
|
100
|
+
|
|
101
|
+
return truncateToWidth(
|
|
102
|
+
`${" ".repeat(safeIndent)}${theme.fg(labelColor, label)}${separator}${theme.fg(valueColor, value)}`,
|
|
103
|
+
width,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Wrap a report text block to the available width and optionally prefix each line.
|
|
109
|
+
*
|
|
110
|
+
* This is useful for wrapped bullets or explanatory notes that should align
|
|
111
|
+
* under an existing report indent.
|
|
112
|
+
*/
|
|
113
|
+
export function wrapReportText(
|
|
114
|
+
text: string,
|
|
115
|
+
width: number,
|
|
116
|
+
options: { indent?: string } = {},
|
|
117
|
+
): string[] {
|
|
118
|
+
const indent = options.indent ?? "";
|
|
119
|
+
const wrapped = wrapTextWithAnsi(text, Math.max(1, width - indent.length));
|
|
120
|
+
return indent ? wrapped.map((line) => `${indent}${line}`) : wrapped;
|
|
121
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-review",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.1",
|
|
4
4
|
"description": "SuPi Review extension — structured code review via /supi-review command",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"README.md"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@mrclrchtr/supi-core": "1.
|
|
23
|
+
"@mrclrchtr/supi-core": "1.14.1"
|
|
24
24
|
},
|
|
25
25
|
"bundledDependencies": [
|
|
26
26
|
"@mrclrchtr/supi-core"
|
package/src/tool/brief-runner.ts
CHANGED
|
@@ -84,7 +84,8 @@ function emitBriefProgress(
|
|
|
84
84
|
invocation: BriefSynthesisInvocation,
|
|
85
85
|
): void {
|
|
86
86
|
ctx.progress.tokens = buildProgressTokens(() => ctx.session.getSessionStats());
|
|
87
|
-
|
|
87
|
+
ctx.progress.elapsedMs = Date.now() - ctx.startTime;
|
|
88
|
+
invocation.onProgress?.(ctx.progress);
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
function handleAgentEnd(options: {
|
|
@@ -145,13 +146,14 @@ export async function runBriefSynthesis(
|
|
|
145
146
|
break;
|
|
146
147
|
case "tool_execution_start":
|
|
147
148
|
ctx.progress.toolUses++;
|
|
148
|
-
ctx.progress.
|
|
149
|
-
event.toolName === "submit_review_brief" ? "
|
|
150
|
-
|
|
149
|
+
ctx.progress.currentFocus = {
|
|
150
|
+
label: event.toolName === "submit_review_brief" ? "Submitting brief" : event.toolName,
|
|
151
|
+
detail: "",
|
|
152
|
+
};
|
|
151
153
|
emitBriefProgress(ctx, invocation);
|
|
152
154
|
break;
|
|
153
155
|
case "tool_execution_end":
|
|
154
|
-
ctx.progress.
|
|
156
|
+
ctx.progress.currentFocus = undefined;
|
|
155
157
|
emitBriefProgress(ctx, invocation);
|
|
156
158
|
break;
|
|
157
159
|
case "agent_end": {
|
package/src/tool/review-debug.ts
CHANGED
|
@@ -107,7 +107,6 @@ export function buildFailureDebug(input: BuildFailureDebugInput): ReviewFailureD
|
|
|
107
107
|
return {
|
|
108
108
|
turns: input.progress.turns,
|
|
109
109
|
toolUses: input.progress.toolUses,
|
|
110
|
-
activities: input.progress.activities.length > 0 ? [...input.progress.activities] : undefined,
|
|
111
110
|
tokens: input.progress.tokens,
|
|
112
111
|
recentEvents: input.recentEvents.length > 0 ? [...input.recentEvents] : undefined,
|
|
113
112
|
lastAssistantText: lastAssistant?.text,
|
|
@@ -38,27 +38,113 @@ export interface RunnerContext {
|
|
|
38
38
|
timeoutSteered: boolean;
|
|
39
39
|
graceTurnsRemaining: number | undefined;
|
|
40
40
|
debug: { recentEvents: string[] };
|
|
41
|
+
/** Accumulated per-tool-label execution counts. */
|
|
42
|
+
toolCounts: Record<string, number>;
|
|
43
|
+
/** Set of distinct file paths inspected via read_snapshot_diff / read_snapshot_file. */
|
|
44
|
+
inspectedFiles: Set<string>;
|
|
45
|
+
/** Timestamp (ms) when the runner started, for elapsed-time display. */
|
|
46
|
+
startTime: number;
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
// ---------------------------------------------------------------------------
|
|
44
50
|
// Tool and progress helpers
|
|
45
51
|
// ---------------------------------------------------------------------------
|
|
46
52
|
|
|
47
|
-
/** Maps tool names to
|
|
48
|
-
function
|
|
49
|
-
if (phase === "end") return "";
|
|
53
|
+
/** Maps tool names to compact display labels for the progress line. */
|
|
54
|
+
function toolNameToLabel(name: string): string {
|
|
50
55
|
const map: Record<string, string> = {
|
|
51
|
-
read: "
|
|
52
|
-
grep: "
|
|
53
|
-
find: "
|
|
54
|
-
ls: "
|
|
55
|
-
submit_review: "
|
|
56
|
-
read_snapshot_diff: "
|
|
57
|
-
read_snapshot_file: "
|
|
56
|
+
read: "reads",
|
|
57
|
+
grep: "greps",
|
|
58
|
+
find: "finds",
|
|
59
|
+
ls: "ls",
|
|
60
|
+
submit_review: "submits",
|
|
61
|
+
read_snapshot_diff: "diffs",
|
|
62
|
+
read_snapshot_file: "file-reads",
|
|
58
63
|
};
|
|
59
64
|
return map[name] ?? name;
|
|
60
65
|
}
|
|
61
66
|
|
|
67
|
+
/** Maps tool names to focus labels for the progress narrative line. */
|
|
68
|
+
function toolNameToFocusLabel(name: string): string {
|
|
69
|
+
const map: Record<string, string> = {
|
|
70
|
+
read: "Reading",
|
|
71
|
+
grep: "Searching",
|
|
72
|
+
find: "Finding",
|
|
73
|
+
ls: "Listing",
|
|
74
|
+
submit_review: "Submitting review",
|
|
75
|
+
read_snapshot_diff: "Reading",
|
|
76
|
+
read_snapshot_file: "Reading",
|
|
77
|
+
};
|
|
78
|
+
return map[name] ?? name;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Extract the basename from a file path, or return the path unchanged. */
|
|
82
|
+
function extractBasename(file: string): string {
|
|
83
|
+
const lastSlash = file.lastIndexOf("/");
|
|
84
|
+
return lastSlash >= 0 ? file.slice(lastSlash + 1) : file;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Truncate a focus detail string to a display-friendly length. */
|
|
88
|
+
function truncateFocusDetail(value: string): string {
|
|
89
|
+
return value.length > 40 ? `${value.slice(0, 40)}…` : value;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Extract focus detail from a read-type tool (read, read_snapshot_diff, read_snapshot_file). */
|
|
93
|
+
function tryExtractReadDetail(toolName: string, a: Record<string, unknown>): string | undefined {
|
|
94
|
+
const file = (a.file ?? a.path) as string | undefined;
|
|
95
|
+
if (!file) return undefined;
|
|
96
|
+
const name = extractBasename(file);
|
|
97
|
+
if (toolName === "read_snapshot_diff") return `${name} (diff)`;
|
|
98
|
+
if (toolName === "read_snapshot_file") return `${name} (full)`;
|
|
99
|
+
return name;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Extract a human-readable detail string from tool args for the focus display. */
|
|
103
|
+
function tryExtractFocusDetail(toolName: string, args: unknown): string | undefined {
|
|
104
|
+
if (typeof args !== "object" || args === null) return undefined;
|
|
105
|
+
const a = args as Record<string, unknown>;
|
|
106
|
+
|
|
107
|
+
// submit_review is self-explanatory — no detail needed
|
|
108
|
+
if (toolName === "submit_review") return undefined;
|
|
109
|
+
|
|
110
|
+
// Read-type tools: extract file path, show basename only
|
|
111
|
+
if (
|
|
112
|
+
toolName === "read" ||
|
|
113
|
+
toolName === "read_snapshot_diff" ||
|
|
114
|
+
toolName === "read_snapshot_file"
|
|
115
|
+
) {
|
|
116
|
+
return tryExtractReadDetail(toolName, a);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// grep: show the search pattern
|
|
120
|
+
if (toolName === "grep") {
|
|
121
|
+
const pattern = a.pattern as string | undefined;
|
|
122
|
+
return pattern ? truncateFocusDetail(pattern) : undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// find: show the glob / pattern
|
|
126
|
+
if (toolName === "find") {
|
|
127
|
+
const pattern = (a.pattern ?? a.glob) as string | undefined;
|
|
128
|
+
return pattern ? truncateFocusDetail(pattern) : undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ls: show the listing path
|
|
132
|
+
if (toolName === "ls") {
|
|
133
|
+
const path = a.path as string | undefined;
|
|
134
|
+
return path ? truncateFocusDetail(path) : undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Extract a file path from tool args when the tool is file-inspecting (for inspectedFiles tracking). */
|
|
141
|
+
function tryExtractFileArg(toolName: string, args: unknown): string | undefined {
|
|
142
|
+
if (toolName !== "read_snapshot_diff" && toolName !== "read_snapshot_file") return undefined;
|
|
143
|
+
if (typeof args !== "object" || args === null) return undefined;
|
|
144
|
+
const file = (args as Record<string, unknown>).file;
|
|
145
|
+
return typeof file === "string" ? file : undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
62
148
|
export function createSubmitReviewTool(resultHolder: {
|
|
63
149
|
value: ReviewOutputEvent | undefined;
|
|
64
150
|
}): ReturnType<typeof defineTool> {
|
|
@@ -81,7 +167,11 @@ export function createSubmitReviewTool(resultHolder: {
|
|
|
81
167
|
|
|
82
168
|
export function emitProgress(ctx: RunnerContext): void {
|
|
83
169
|
ctx.progress.tokens = buildProgressTokens(() => ctx.session.getSessionStats());
|
|
84
|
-
ctx.
|
|
170
|
+
ctx.progress.toolCounts = { ...ctx.toolCounts };
|
|
171
|
+
ctx.progress.filesInspected = ctx.inspectedFiles.size;
|
|
172
|
+
ctx.progress.filesTotal = ctx.invocation.snapshot.stats.files;
|
|
173
|
+
ctx.progress.elapsedMs = Date.now() - ctx.startTime;
|
|
174
|
+
ctx.invocation.onProgress?.(ctx.progress);
|
|
85
175
|
}
|
|
86
176
|
|
|
87
177
|
// ---------------------------------------------------------------------------
|
|
@@ -90,7 +180,6 @@ export function emitProgress(ctx: RunnerContext): void {
|
|
|
90
180
|
|
|
91
181
|
export function handleTurnEnd(ctx: RunnerContext): void {
|
|
92
182
|
ctx.progress.turns++;
|
|
93
|
-
ctx.progress.activities = [];
|
|
94
183
|
|
|
95
184
|
if (!ctx.state.settled && ctx.timeoutSteered && ctx.graceTurnsRemaining !== undefined) {
|
|
96
185
|
ctx.graceTurnsRemaining--;
|
|
@@ -108,8 +197,19 @@ export function handleToolStart(
|
|
|
108
197
|
ctx: RunnerContext,
|
|
109
198
|
): void {
|
|
110
199
|
ctx.progress.toolUses++;
|
|
111
|
-
|
|
112
|
-
|
|
200
|
+
|
|
201
|
+
const label = toolNameToLabel(event.toolName);
|
|
202
|
+
ctx.toolCounts[label] = (ctx.toolCounts[label] ?? 0) + 1;
|
|
203
|
+
|
|
204
|
+
const file = tryExtractFileArg(event.toolName, event.args);
|
|
205
|
+
if (file) ctx.inspectedFiles.add(file);
|
|
206
|
+
|
|
207
|
+
const focusDetail = tryExtractFocusDetail(event.toolName, event.args);
|
|
208
|
+
ctx.progress.currentFocus = {
|
|
209
|
+
label: toolNameToFocusLabel(event.toolName),
|
|
210
|
+
detail: focusDetail ?? "",
|
|
211
|
+
};
|
|
212
|
+
|
|
113
213
|
ctx.invocation.onToolActivity?.({ toolName: event.toolName, phase: "start" });
|
|
114
214
|
emitProgress(ctx);
|
|
115
215
|
}
|
|
@@ -118,11 +218,7 @@ export function handleToolEnd(
|
|
|
118
218
|
event: Extract<AgentSessionEvent, { type: "tool_execution_end" }>,
|
|
119
219
|
ctx: RunnerContext,
|
|
120
220
|
): void {
|
|
121
|
-
|
|
122
|
-
if (activity) {
|
|
123
|
-
const index = ctx.progress.activities.indexOf(activity);
|
|
124
|
-
if (index !== -1) ctx.progress.activities.splice(index, 1);
|
|
125
|
-
}
|
|
221
|
+
ctx.progress.currentFocus = undefined;
|
|
126
222
|
ctx.invocation.onToolActivity?.({ toolName: event.toolName, phase: "end" });
|
|
127
223
|
emitProgress(ctx);
|
|
128
224
|
}
|
|
@@ -201,6 +201,8 @@ function buildRunnerCtx(runner: ReviewerRunnerState): RunnerContext {
|
|
|
201
201
|
ctx.timeoutSteered = runner.timeoutSteered;
|
|
202
202
|
ctx.graceTurnsRemaining = runner.graceTurnsRemaining;
|
|
203
203
|
ctx.debug = runner.debug;
|
|
204
|
+
ctx.toolCounts = {};
|
|
205
|
+
ctx.inspectedFiles = new Set();
|
|
204
206
|
return ctx;
|
|
205
207
|
}
|
|
206
208
|
|
|
@@ -214,6 +216,7 @@ function syncCtxFromLifecycle(
|
|
|
214
216
|
ctx.resolve = lcCtx.resolve;
|
|
215
217
|
ctx.cleanup = lcCtx.cleanup;
|
|
216
218
|
ctx.state = lcCtx.state;
|
|
219
|
+
ctx.startTime = lcCtx.startTime;
|
|
217
220
|
ctx.submitSteered = runner.submitSteered;
|
|
218
221
|
ctx.timeoutSteered = runner.timeoutSteered;
|
|
219
222
|
ctx.graceTurnsRemaining = runner.graceTurnsRemaining;
|
|
@@ -42,8 +42,18 @@ export function extractAssistantText(content: unknown): string | undefined {
|
|
|
42
42
|
|
|
43
43
|
/** Build a truncated string representation of session stats for progress. */
|
|
44
44
|
export function buildProgressTokens(
|
|
45
|
-
getSessionStats: () => {
|
|
46
|
-
|
|
45
|
+
getSessionStats: () => {
|
|
46
|
+
tokens?: {
|
|
47
|
+
input?: number;
|
|
48
|
+
output?: number;
|
|
49
|
+
total?: number;
|
|
50
|
+
cacheRead?: number;
|
|
51
|
+
cacheWrite?: number;
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
):
|
|
55
|
+
| { input: number; output: number; total: number; cacheRead?: number; cacheWrite?: number }
|
|
56
|
+
| undefined {
|
|
47
57
|
try {
|
|
48
58
|
const stats = getSessionStats();
|
|
49
59
|
return stats?.tokens
|
|
@@ -51,6 +61,8 @@ export function buildProgressTokens(
|
|
|
51
61
|
input: stats.tokens.input ?? 0,
|
|
52
62
|
output: stats.tokens.output ?? 0,
|
|
53
63
|
total: stats.tokens.total ?? 0,
|
|
64
|
+
cacheRead: stats.tokens.cacheRead,
|
|
65
|
+
cacheWrite: stats.tokens.cacheWrite,
|
|
54
66
|
}
|
|
55
67
|
: undefined;
|
|
56
68
|
} catch {
|
|
@@ -27,6 +27,8 @@ export interface LifecycleCtx<TResult> {
|
|
|
27
27
|
* Useful for custom timers or resources set up by `onTimeout`.
|
|
28
28
|
*/
|
|
29
29
|
addTeardown: (fn: () => void) => void;
|
|
30
|
+
/** Timestamp (ms) when the lifecycle started, for elapsed-time display. */
|
|
31
|
+
startTime: number;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/** Configuration for `runWithLifecycle`. */
|
|
@@ -90,7 +92,6 @@ export function runWithLifecycle<TResult>(
|
|
|
90
92
|
const progress: ReviewProgress = {
|
|
91
93
|
turns: 0,
|
|
92
94
|
toolUses: 0,
|
|
93
|
-
activities: [],
|
|
94
95
|
tokens: undefined,
|
|
95
96
|
};
|
|
96
97
|
const state: { settled: boolean; aborting: boolean } = {
|
|
@@ -122,6 +123,8 @@ export function runWithLifecycle<TResult>(
|
|
|
122
123
|
return result;
|
|
123
124
|
};
|
|
124
125
|
|
|
126
|
+
const startTime = Date.now();
|
|
127
|
+
|
|
125
128
|
return new Promise<TResult>((resolve) => {
|
|
126
129
|
const ctx: LifecycleCtx<TResult> = {
|
|
127
130
|
resolve,
|
|
@@ -130,6 +133,7 @@ export function runWithLifecycle<TResult>(
|
|
|
130
133
|
state,
|
|
131
134
|
session,
|
|
132
135
|
addTeardown,
|
|
136
|
+
startTime,
|
|
133
137
|
};
|
|
134
138
|
|
|
135
139
|
session.subscribe((event: AgentSessionEvent) => {
|
package/src/types.ts
CHANGED
|
@@ -128,11 +128,12 @@ export interface ReviewPacket {
|
|
|
128
128
|
export interface ReviewFailureDebugInfo {
|
|
129
129
|
turns: number;
|
|
130
130
|
toolUses: number;
|
|
131
|
-
activities?: string[];
|
|
132
131
|
tokens?: {
|
|
133
132
|
input: number;
|
|
134
133
|
output: number;
|
|
135
134
|
total: number;
|
|
135
|
+
cacheRead?: number;
|
|
136
|
+
cacheWrite?: number;
|
|
136
137
|
};
|
|
137
138
|
recentEvents?: string[];
|
|
138
139
|
lastAssistantText?: string;
|
|
@@ -200,10 +201,24 @@ export interface ReviewProgress {
|
|
|
200
201
|
turns: number;
|
|
201
202
|
/** Number of tool executions started. */
|
|
202
203
|
toolUses: number;
|
|
203
|
-
/** Human-readable active tool descriptions. */
|
|
204
|
-
activities: string[];
|
|
205
204
|
/** Token usage stats, if available. */
|
|
206
|
-
tokens?: {
|
|
205
|
+
tokens?: {
|
|
206
|
+
input: number;
|
|
207
|
+
output: number;
|
|
208
|
+
total: number;
|
|
209
|
+
cacheRead?: number;
|
|
210
|
+
cacheWrite?: number;
|
|
211
|
+
};
|
|
212
|
+
/** Per-tool execution counts keyed by short display label (e.g. "diffs", "reads", "greps"). */
|
|
213
|
+
toolCounts?: Record<string, number>;
|
|
214
|
+
/** Number of distinct files inspected so far (via read_snapshot_diff / read_snapshot_file). */
|
|
215
|
+
filesInspected?: number;
|
|
216
|
+
/** Total files in the review snapshot. */
|
|
217
|
+
filesTotal?: number;
|
|
218
|
+
/** Current tool + context for the progress narrative line. */
|
|
219
|
+
currentFocus?: { label: string; detail: string };
|
|
220
|
+
/** Elapsed time in milliseconds since the operation started. */
|
|
221
|
+
elapsedMs?: number;
|
|
207
222
|
}
|
|
208
223
|
|
|
209
224
|
export type BriefSynthesisRunResult =
|
package/src/ui/format-content.ts
CHANGED
|
@@ -87,9 +87,6 @@ function appendDebugContent(parts: string[], debug: ReviewFailureDebugInfo | und
|
|
|
87
87
|
const lines = [
|
|
88
88
|
`- Turns: ${debug.turns}`,
|
|
89
89
|
`- Tool uses: ${debug.toolUses}`,
|
|
90
|
-
debug.activities && debug.activities.length > 0
|
|
91
|
-
? `- Active: ${debug.activities.join(", ")}`
|
|
92
|
-
: undefined,
|
|
93
90
|
debug.tokens
|
|
94
91
|
? `- Tokens: ${debug.tokens.input} in / ${debug.tokens.output} out / ${debug.tokens.total} total`
|
|
95
92
|
: undefined,
|
package/src/ui/renderer.ts
CHANGED
|
@@ -210,9 +210,6 @@ function renderFailureDebug(
|
|
|
210
210
|
|
|
211
211
|
const lines = [
|
|
212
212
|
`Turns: ${debug.turns} · Tool uses: ${debug.toolUses}`,
|
|
213
|
-
debug.activities && debug.activities.length > 0
|
|
214
|
-
? `Active: ${debug.activities.join(", ")}`
|
|
215
|
-
: undefined,
|
|
216
213
|
debug.tokens
|
|
217
214
|
? `Tokens: ${debug.tokens.input} in / ${debug.tokens.output} out / ${debug.tokens.total} total`
|
|
218
215
|
: undefined,
|