@mrclrchtr/supi-lsp 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-code-runtime/node_modules/@mrclrchtr/supi-core/README.md +10 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +2 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/api.ts +2 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/config.ts +3 -2
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/progress-widget.ts +98 -17
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/report.ts +121 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +2 -2
- 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 +3 -3
- package/src/session/settings-registration.ts +8 -4
package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/README.md
CHANGED
|
@@ -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
|
package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json
CHANGED
|
@@ -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",
|
package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/api.ts
CHANGED
|
@@ -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 {
|
package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/index.ts
CHANGED
|
@@ -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
|
+
}
|
package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/report.ts
ADDED
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-code-runtime",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.1",
|
|
4
4
|
"description": "SuPi code-runtime — shared workspace context, capability contracts, and canonical types for the code-understanding stack",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"!__tests__"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@mrclrchtr/supi-core": "1.
|
|
22
|
+
"@mrclrchtr/supi-core": "1.14.1"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
25
|
"@earendil-works/pi-coding-agent": "*"
|
|
@@ -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-lsp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.1",
|
|
4
4
|
"description": "SuPi LSP extension — Language Server Protocol integration for pi",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"vscode-jsonrpc": "^9.0.0",
|
|
27
27
|
"vscode-languageserver-protocol": "^3.17.5",
|
|
28
28
|
"vscode-languageserver-types": "^3.17.5",
|
|
29
|
-
"@mrclrchtr/supi-code-runtime": "1.
|
|
30
|
-
"@mrclrchtr/supi-core": "1.
|
|
29
|
+
"@mrclrchtr/supi-code-runtime": "1.14.1",
|
|
30
|
+
"@mrclrchtr/supi-core": "1.14.1"
|
|
31
31
|
},
|
|
32
32
|
"bundledDependencies": [
|
|
33
33
|
"@mrclrchtr/supi-code-runtime",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// LSP settings registration for the supi settings registry.
|
|
2
2
|
|
|
3
|
-
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { CONFIG_DIR_NAME, getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import type { SettingItem } from "@earendil-works/pi-tui";
|
|
5
5
|
import { Container, Key, matchesKey, SettingsList, Text } from "@earendil-works/pi-tui";
|
|
6
6
|
import {
|
|
@@ -40,10 +40,10 @@ export function getLspDisabledMessage(cwd: string, homeDir?: string): string {
|
|
|
40
40
|
const project = loadSupiConfigForScope("lsp", cwd, LSP_DEFAULTS, { scope: "project", homeDir });
|
|
41
41
|
|
|
42
42
|
if (project.enabled === false) {
|
|
43
|
-
return
|
|
43
|
+
return `LSP is disabled in project settings (${CONFIG_DIR_NAME}/supi/config.json)`;
|
|
44
44
|
}
|
|
45
45
|
if (global.enabled === false) {
|
|
46
|
-
return
|
|
46
|
+
return `LSP is disabled in global settings (~/${CONFIG_DIR_NAME}/agent/supi/config.json)`;
|
|
47
47
|
}
|
|
48
48
|
return "LSP is disabled in settings";
|
|
49
49
|
}
|
|
@@ -251,7 +251,11 @@ function createExcludeSubmenu(
|
|
|
251
251
|
const header = new Text("Exclude Patterns — toggle off to remove", 0, 0);
|
|
252
252
|
container.addChild(header);
|
|
253
253
|
|
|
254
|
-
const footer = new Text(
|
|
254
|
+
const footer = new Text(
|
|
255
|
+
`Add new patterns in ${CONFIG_DIR_NAME}/supi/config.json under lsp.exclude`,
|
|
256
|
+
0,
|
|
257
|
+
0,
|
|
258
|
+
);
|
|
255
259
|
container.addChild(footer);
|
|
256
260
|
|
|
257
261
|
const settingsList = new SettingsList(
|