@yagni-app/code-staging 1.0.0-staging.1184.1 → 1.0.0-staging.1185.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/dist/extension/askAdvisorTool.d.ts +7 -0
- package/dist/extension/askAdvisorTool.js +11 -3
- package/dist/extension/askYagniTool.js +2 -0
- package/dist/extension/condensedTools.d.ts +93 -0
- package/dist/extension/condensedTools.js +392 -0
- package/dist/extension/index.d.ts +6 -0
- package/dist/extension/index.js +42 -2
- package/dist/extension/subagents.d.ts +10 -0
- package/dist/extension/subagents.js +11 -3
- package/dist/extension/toolRuns.d.ts +92 -0
- package/dist/extension/toolRuns.js +201 -0
- package/dist/extension/webFetchTool.js +2 -0
- package/dist/extension/workingLine.d.ts +49 -0
- package/dist/extension/workingLine.js +116 -0
- package/package.json +2 -2
|
@@ -29,6 +29,7 @@ import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-age
|
|
|
29
29
|
import { type Component } from "@earendil-works/pi-tui";
|
|
30
30
|
import { Type } from "typebox";
|
|
31
31
|
import { type AdvisorLimits, type AdvisorStateHandle } from "./advisor.js";
|
|
32
|
+
import type { WorkingLineHandle } from "./workingLine.js";
|
|
32
33
|
import { runStage as defaultRunStage } from "./pipeline/runner.js";
|
|
33
34
|
import { type PipelineStage } from "./pipeline/types.js";
|
|
34
35
|
import { type RenderTheme, type SubagentTaskProgress } from "./subagentRender.js";
|
|
@@ -49,6 +50,12 @@ export interface MakeAskAdvisorToolOptions {
|
|
|
49
50
|
limits?: AdvisorLimits;
|
|
50
51
|
/** Injectable so tests never spawn a child. */
|
|
51
52
|
runStage?: typeof defaultRunStage;
|
|
53
|
+
/**
|
|
54
|
+
* The session working-line manager (workingLine.ts). When present, live
|
|
55
|
+
* consult progress goes through it (so the elapsed/token suffix survives);
|
|
56
|
+
* absent, the tool falls back to ui.setWorkingMessage directly.
|
|
57
|
+
*/
|
|
58
|
+
workingLine?: WorkingLineHandle;
|
|
52
59
|
}
|
|
53
60
|
/**
|
|
54
61
|
* Assemble the consult brief. The advisor's persona already tells it not to take
|
|
@@ -125,6 +125,8 @@ export function makeAskAdvisorTool(opts) {
|
|
|
125
125
|
"The advice comes back as plain text: act on it, and call record_decision when it settles a product-intent call so the next agent inherits it.",
|
|
126
126
|
],
|
|
127
127
|
parameters,
|
|
128
|
+
// Self-framed: the condensed transcript look has no tinted tool boxes.
|
|
129
|
+
renderShell: "self",
|
|
128
130
|
renderCall: renderAdvisorCall,
|
|
129
131
|
renderResult: renderSubagentResult,
|
|
130
132
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
@@ -159,9 +161,12 @@ export function makeAskAdvisorTool(opts) {
|
|
|
159
161
|
},
|
|
160
162
|
});
|
|
161
163
|
const working = formatWorkingMessage([progress], now);
|
|
162
|
-
if (
|
|
164
|
+
if (working !== lastWorking) {
|
|
163
165
|
lastWorking = working;
|
|
164
|
-
|
|
166
|
+
if (opts.workingLine)
|
|
167
|
+
opts.workingLine.setActivity(working);
|
|
168
|
+
else
|
|
169
|
+
ui?.setWorkingMessage?.(working);
|
|
165
170
|
}
|
|
166
171
|
};
|
|
167
172
|
emit();
|
|
@@ -181,7 +186,10 @@ export function makeAskAdvisorTool(opts) {
|
|
|
181
186
|
}
|
|
182
187
|
finally {
|
|
183
188
|
// Restore the default "Working…" text whether we resolved or threw.
|
|
184
|
-
|
|
189
|
+
if (opts.workingLine)
|
|
190
|
+
opts.workingLine.setActivity(undefined);
|
|
191
|
+
else
|
|
192
|
+
ui?.setWorkingMessage?.();
|
|
185
193
|
}
|
|
186
194
|
const cost = result.usage?.cost ?? 0;
|
|
187
195
|
const state = opts.state.record(cost);
|
|
@@ -56,6 +56,8 @@ export function makeAskYagniTool(opts) {
|
|
|
56
56
|
"Answers carry a standing: treat a confirmed decision as settled; when you lean on an unverified assumption or an inference, say so where the work is reviewed; when there is no recorded position, follow the answer's instruction to record the assumption you proceed on.",
|
|
57
57
|
],
|
|
58
58
|
parameters,
|
|
59
|
+
// Self-framed: the condensed transcript look has no tinted tool boxes.
|
|
60
|
+
renderShell: "self",
|
|
59
61
|
renderCall(args, theme) {
|
|
60
62
|
const t = theme;
|
|
61
63
|
let text = `${t.fg("toolTitle", t.bold("ask_yagni"))} ${t.fg("dim", clipLine(args?.question ?? "…", 100))}`;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Condensed, Claude Code-style rendering for pi's seven built-in tools.
|
|
3
|
+
*
|
|
4
|
+
* pi resolves renderers per slot (`toolDefinition.renderCall ?? builtIn.renderCall`),
|
|
5
|
+
* so re-registering a built-in by name with the SAME factory-made definition
|
|
6
|
+
* spread underneath swaps ONLY the presentation: name, description, parameters,
|
|
7
|
+
* prompt metadata, and execution semantics stay byte-identical with the
|
|
8
|
+
* built-in (execution delegates to the same `create*ToolDefinition` the session
|
|
9
|
+
* would have built, including the settings-driven bash shellPath/commandPrefix
|
|
10
|
+
* and read image auto-resize). pi 0.84.1 registers such overrides silently —
|
|
11
|
+
* there is no built-in-override startup warning in this version (verified
|
|
12
|
+
* against dist; the extensions.md claim is stale).
|
|
13
|
+
*
|
|
14
|
+
* Rendering contract per row (renderShell "self", so no tinted Box):
|
|
15
|
+
* - renderCall paints the TITLE slot: a live "● Tool(arg)" line while running,
|
|
16
|
+
* a "● Tool(arg)" line for visible rows, the run summary line for the tail
|
|
17
|
+
* of a completed quiet run, or nothing (a zero-line component removes the
|
|
18
|
+
* row entirely, including its spacer).
|
|
19
|
+
* - renderResult paints the BODY slot: condensed write/edit previews, bash
|
|
20
|
+
* error/partial output tails, or nothing. Expanded (ctrl+o) shows full
|
|
21
|
+
* output for every row.
|
|
22
|
+
*
|
|
23
|
+
* String assembly is pure over {@link RenderTheme} (subagentRender.ts's
|
|
24
|
+
* pattern) so tests run against plain text. Renderer exceptions are swallowed
|
|
25
|
+
* by pi (degrading to the built-in fallback), so everything stays total.
|
|
26
|
+
*/
|
|
27
|
+
import { SettingsManager, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
28
|
+
import type { RenderTheme } from "./subagentRender.js";
|
|
29
|
+
import { ToolRunTracker } from "./toolRuns.js";
|
|
30
|
+
/** Lines of write content shown collapsed (mirrors Claude Code's preview). */
|
|
31
|
+
export declare const WRITE_PREVIEW_LINES = 8;
|
|
32
|
+
/** Visual lines of a diff shown collapsed. */
|
|
33
|
+
export declare const DIFF_PREVIEW_LINES = 12;
|
|
34
|
+
/** Output tail lines shown under a failed shell command. */
|
|
35
|
+
export declare const ERROR_TAIL_LINES = 5;
|
|
36
|
+
/** Output tail lines shown under a still-running shell command. */
|
|
37
|
+
export declare const PARTIAL_TAIL_LINES = 3;
|
|
38
|
+
declare const BUILTIN_NAMES: readonly ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
39
|
+
type BuiltinName = (typeof BUILTIN_NAMES)[number];
|
|
40
|
+
export type RowStatus = "running" | "ok" | "error";
|
|
41
|
+
/** Home-collapse, and prefer cwd-relative for paths inside the project. */
|
|
42
|
+
export declare function displayPath(rawPath: string, cwd?: string): string;
|
|
43
|
+
/** Whether a tool-call path targets the session scratchpad. */
|
|
44
|
+
export declare function isScratchpadPath(rawPath: unknown, scratchpadDir: string | undefined, cwd?: string): boolean;
|
|
45
|
+
/** The one argument worth showing in a title: path, command, or pattern. */
|
|
46
|
+
export declare function primaryArg(name: BuiltinName, args: Record<string, unknown> | undefined, cwd?: string): string;
|
|
47
|
+
/** `● Tool(arg)` — the visible-row (and live-row) title line. */
|
|
48
|
+
export declare function formatRowTitle(name: BuiltinName, args: Record<string, unknown> | undefined, status: RowStatus, theme: RenderTheme, cwd?: string): string;
|
|
49
|
+
/** Extract "Command exited with code N" from a bash error body, if present. */
|
|
50
|
+
export declare function splitBashError(outputText: string): {
|
|
51
|
+
output: string;
|
|
52
|
+
exitLine: string | undefined;
|
|
53
|
+
};
|
|
54
|
+
/** `⎿ exit line` + a dim tail of output under a failed shell command. */
|
|
55
|
+
export declare function formatBashErrorBody(outputText: string, theme: RenderTheme): string[];
|
|
56
|
+
/** A dim tail of live output under a still-running shell command. */
|
|
57
|
+
export declare function formatBashPartialBody(outputText: string, theme: RenderTheme): string[];
|
|
58
|
+
/** Count added lines in a unified patch (edit summaries surface "+N"). */
|
|
59
|
+
export declare function countPatchAdditions(patch: string | undefined): number;
|
|
60
|
+
/**
|
|
61
|
+
* `⎿ Wrote N lines to path` + a numbered, syntax-highlighted preview.
|
|
62
|
+
* `highlight` is injectable so tests stay independent of pi's theme state.
|
|
63
|
+
*/
|
|
64
|
+
export declare function formatWriteBody(rawPath: string, content: string, expanded: boolean, theme: RenderTheme, cwd?: string, highlight?: (code: string, filePath: string) => string[]): string[];
|
|
65
|
+
/**
|
|
66
|
+
* `⎿ Updated path (+A -R)` + a colored diff preview.
|
|
67
|
+
* `paintDiff` is injectable for the same reason as `highlight` above.
|
|
68
|
+
*/
|
|
69
|
+
export declare function formatEditBody(rawPath: string, details: {
|
|
70
|
+
diff?: string;
|
|
71
|
+
patch?: string;
|
|
72
|
+
} | undefined, expanded: boolean, theme: RenderTheme, cwd?: string, paintDiff?: (diffText: string) => string): string[];
|
|
73
|
+
/** Expanded body: the raw output, dimmed and capped. */
|
|
74
|
+
export declare function formatExpandedOutput(outputText: string, theme: RenderTheme): string[];
|
|
75
|
+
export interface RegisterCondensedToolsDeps {
|
|
76
|
+
/** Session scratchpad dir; writes/edits under it aggregate instead of rendering. */
|
|
77
|
+
scratchpadDir?: string;
|
|
78
|
+
/** Registration-time cwd (defaults to process.cwd()). */
|
|
79
|
+
cwd?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Settings loader seam. The default mirrors what pi's session does when it
|
|
82
|
+
* builds base tools: bash gets shellPath + shellCommandPrefix, read gets
|
|
83
|
+
* image autoResize. Injectable so tests never touch the config dir.
|
|
84
|
+
*/
|
|
85
|
+
loadSettings?: (cwd: string) => SettingsManager;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Re-register the seven built-ins with condensed renderers. Returns the
|
|
89
|
+
* tracker so callers (and tests) can feed message boundaries into it.
|
|
90
|
+
*/
|
|
91
|
+
export declare function registerCondensedTools(pi: ExtensionAPI, deps?: RegisterCondensedToolsDeps): ToolRunTracker;
|
|
92
|
+
export {};
|
|
93
|
+
//# sourceMappingURL=condensedTools.d.ts.map
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Condensed, Claude Code-style rendering for pi's seven built-in tools.
|
|
3
|
+
*
|
|
4
|
+
* pi resolves renderers per slot (`toolDefinition.renderCall ?? builtIn.renderCall`),
|
|
5
|
+
* so re-registering a built-in by name with the SAME factory-made definition
|
|
6
|
+
* spread underneath swaps ONLY the presentation: name, description, parameters,
|
|
7
|
+
* prompt metadata, and execution semantics stay byte-identical with the
|
|
8
|
+
* built-in (execution delegates to the same `create*ToolDefinition` the session
|
|
9
|
+
* would have built, including the settings-driven bash shellPath/commandPrefix
|
|
10
|
+
* and read image auto-resize). pi 0.84.1 registers such overrides silently —
|
|
11
|
+
* there is no built-in-override startup warning in this version (verified
|
|
12
|
+
* against dist; the extensions.md claim is stale).
|
|
13
|
+
*
|
|
14
|
+
* Rendering contract per row (renderShell "self", so no tinted Box):
|
|
15
|
+
* - renderCall paints the TITLE slot: a live "● Tool(arg)" line while running,
|
|
16
|
+
* a "● Tool(arg)" line for visible rows, the run summary line for the tail
|
|
17
|
+
* of a completed quiet run, or nothing (a zero-line component removes the
|
|
18
|
+
* row entirely, including its spacer).
|
|
19
|
+
* - renderResult paints the BODY slot: condensed write/edit previews, bash
|
|
20
|
+
* error/partial output tails, or nothing. Expanded (ctrl+o) shows full
|
|
21
|
+
* output for every row.
|
|
22
|
+
*
|
|
23
|
+
* String assembly is pure over {@link RenderTheme} (subagentRender.ts's
|
|
24
|
+
* pattern) so tests run against plain text. Renderer exceptions are swallowed
|
|
25
|
+
* by pi (degrading to the built-in fallback), so everything stays total.
|
|
26
|
+
*/
|
|
27
|
+
import * as os from "node:os";
|
|
28
|
+
import * as path from "node:path";
|
|
29
|
+
import { createBashToolDefinition, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, getAgentDir, getLanguageFromPath, highlightCode, renderDiff, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
30
|
+
import { Container, Text } from "@earendil-works/pi-tui";
|
|
31
|
+
import { isQuiet, kindForTool, ToolRunTracker } from "./toolRuns.js";
|
|
32
|
+
/** Lines of write content shown collapsed (mirrors Claude Code's preview). */
|
|
33
|
+
export const WRITE_PREVIEW_LINES = 8;
|
|
34
|
+
/** Visual lines of a diff shown collapsed. */
|
|
35
|
+
export const DIFF_PREVIEW_LINES = 12;
|
|
36
|
+
/** Output tail lines shown under a failed shell command. */
|
|
37
|
+
export const ERROR_TAIL_LINES = 5;
|
|
38
|
+
/** Output tail lines shown under a still-running shell command. */
|
|
39
|
+
export const PARTIAL_TAIL_LINES = 3;
|
|
40
|
+
/** Hard cap on expanded body lines so a huge output cannot stall a repaint. */
|
|
41
|
+
const EXPANDED_MAX_LINES = 1000;
|
|
42
|
+
/** Title arg preview width. */
|
|
43
|
+
const ARG_PREVIEW_MAX = 96;
|
|
44
|
+
const BUILTIN_NAMES = ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
45
|
+
const TITLE_BY_NAME = {
|
|
46
|
+
read: "Read",
|
|
47
|
+
bash: "Bash",
|
|
48
|
+
edit: "Edit",
|
|
49
|
+
write: "Write",
|
|
50
|
+
grep: "Grep",
|
|
51
|
+
find: "Find",
|
|
52
|
+
ls: "List",
|
|
53
|
+
};
|
|
54
|
+
/** Collapse whitespace and clip to `max`, appending an ellipsis when cut. */
|
|
55
|
+
function clip(text, max) {
|
|
56
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
57
|
+
if (collapsed.length <= max)
|
|
58
|
+
return collapsed;
|
|
59
|
+
return `${collapsed.slice(0, max - 1)}…`;
|
|
60
|
+
}
|
|
61
|
+
/** Forward slashes for comparison and display, on every platform. */
|
|
62
|
+
function slashed(p) {
|
|
63
|
+
return p.replace(/\\/g, "/");
|
|
64
|
+
}
|
|
65
|
+
/** Home-collapse, and prefer cwd-relative for paths inside the project. */
|
|
66
|
+
export function displayPath(rawPath, cwd) {
|
|
67
|
+
if (!rawPath)
|
|
68
|
+
return rawPath;
|
|
69
|
+
let display = slashed(rawPath);
|
|
70
|
+
if (cwd) {
|
|
71
|
+
// Resolve BOTH sides so a POSIX-style cwd and Windows' drive-letter
|
|
72
|
+
// absolute paths compare in the same space.
|
|
73
|
+
const absolute = slashed(path.resolve(cwd, rawPath));
|
|
74
|
+
const base = slashed(path.resolve(cwd)).replace(/\/+$/, "");
|
|
75
|
+
if (absolute.startsWith(`${base}/`))
|
|
76
|
+
return absolute.slice(base.length + 1);
|
|
77
|
+
display = absolute;
|
|
78
|
+
}
|
|
79
|
+
const home = slashed(os.homedir());
|
|
80
|
+
return display.startsWith(home) ? `~${display.slice(home.length)}` : display;
|
|
81
|
+
}
|
|
82
|
+
/** Whether a tool-call path targets the session scratchpad. */
|
|
83
|
+
export function isScratchpadPath(rawPath, scratchpadDir, cwd) {
|
|
84
|
+
if (typeof rawPath !== "string" || !rawPath || !scratchpadDir)
|
|
85
|
+
return false;
|
|
86
|
+
const absolute = slashed(path.resolve(cwd ?? process.cwd(), rawPath));
|
|
87
|
+
const base = slashed(path.resolve(scratchpadDir)).replace(/\/+$/, "");
|
|
88
|
+
return absolute === base || absolute.startsWith(`${base}/`);
|
|
89
|
+
}
|
|
90
|
+
/** The one argument worth showing in a title: path, command, or pattern. */
|
|
91
|
+
export function primaryArg(name, args, cwd) {
|
|
92
|
+
const a = args ?? {};
|
|
93
|
+
const str = (v) => (typeof v === "string" ? v : "");
|
|
94
|
+
switch (name) {
|
|
95
|
+
case "bash":
|
|
96
|
+
return clip(str(a.command) || "…", ARG_PREVIEW_MAX);
|
|
97
|
+
case "grep": {
|
|
98
|
+
const pattern = str(a.pattern) || "…";
|
|
99
|
+
const where = str(a.path);
|
|
100
|
+
return clip(where ? `${pattern} in ${displayPath(where, cwd)}` : pattern, ARG_PREVIEW_MAX);
|
|
101
|
+
}
|
|
102
|
+
case "find":
|
|
103
|
+
return clip(str(a.pattern) || "…", ARG_PREVIEW_MAX);
|
|
104
|
+
case "ls":
|
|
105
|
+
return clip(displayPath(str(a.path) || ".", cwd), ARG_PREVIEW_MAX);
|
|
106
|
+
default: {
|
|
107
|
+
const p = str(a.path);
|
|
108
|
+
let display = p ? displayPath(p, cwd) : "…";
|
|
109
|
+
if (name === "read" && (a.offset !== undefined || a.limit !== undefined)) {
|
|
110
|
+
const start = typeof a.offset === "number" ? a.offset : 1;
|
|
111
|
+
const end = typeof a.limit === "number" ? start + a.limit - 1 : undefined;
|
|
112
|
+
display += `:${start}${end !== undefined ? `-${end}` : ""}`;
|
|
113
|
+
}
|
|
114
|
+
return clip(display, ARG_PREVIEW_MAX);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function statusDot(status, theme) {
|
|
119
|
+
if (status === "error")
|
|
120
|
+
return theme.fg("error", "●");
|
|
121
|
+
if (status === "running")
|
|
122
|
+
return theme.fg("accent", "●");
|
|
123
|
+
return theme.fg("success", "●");
|
|
124
|
+
}
|
|
125
|
+
/** `● Tool(arg)` — the visible-row (and live-row) title line. */
|
|
126
|
+
export function formatRowTitle(name, args, status, theme, cwd) {
|
|
127
|
+
const title = theme.bold(theme.fg("toolTitle", TITLE_BY_NAME[name]));
|
|
128
|
+
return `${statusDot(status, theme)} ${title}(${theme.fg("accent", primaryArg(name, args, cwd))})`;
|
|
129
|
+
}
|
|
130
|
+
/** Extract "Command exited with code N" from a bash error body, if present. */
|
|
131
|
+
export function splitBashError(outputText) {
|
|
132
|
+
const match = outputText.match(/\n*(Command exited with code \d+)\s*$/);
|
|
133
|
+
if (!match)
|
|
134
|
+
return { output: outputText.trimEnd(), exitLine: undefined };
|
|
135
|
+
return { output: outputText.slice(0, match.index).trimEnd(), exitLine: match[1] };
|
|
136
|
+
}
|
|
137
|
+
function indent(lines, pad) {
|
|
138
|
+
return lines.map((line) => pad + line);
|
|
139
|
+
}
|
|
140
|
+
/** `⎿ exit line` + a dim tail of output under a failed shell command. */
|
|
141
|
+
export function formatBashErrorBody(outputText, theme) {
|
|
142
|
+
const { output, exitLine } = splitBashError(outputText);
|
|
143
|
+
const lines = [];
|
|
144
|
+
lines.push(` ${theme.fg("muted", "⎿")} ${theme.fg("error", exitLine ?? "Command failed")}`);
|
|
145
|
+
const tail = output.split("\n").filter((l) => l.length > 0).slice(-ERROR_TAIL_LINES);
|
|
146
|
+
lines.push(...indent(tail.map((l) => theme.fg("toolOutput", l)), " "));
|
|
147
|
+
return lines;
|
|
148
|
+
}
|
|
149
|
+
/** A dim tail of live output under a still-running shell command. */
|
|
150
|
+
export function formatBashPartialBody(outputText, theme) {
|
|
151
|
+
const tail = outputText.split("\n").filter((l) => l.length > 0).slice(-PARTIAL_TAIL_LINES);
|
|
152
|
+
return indent(tail.map((l) => theme.fg("dim", l)), " ");
|
|
153
|
+
}
|
|
154
|
+
/** Count added lines in a unified patch (edit summaries surface "+N"). */
|
|
155
|
+
export function countPatchAdditions(patch) {
|
|
156
|
+
if (!patch)
|
|
157
|
+
return 0;
|
|
158
|
+
let added = 0;
|
|
159
|
+
for (const line of patch.split("\n")) {
|
|
160
|
+
if (line.startsWith("+") && !line.startsWith("+++"))
|
|
161
|
+
added += 1;
|
|
162
|
+
}
|
|
163
|
+
return added;
|
|
164
|
+
}
|
|
165
|
+
function countPatchRemovals(patch) {
|
|
166
|
+
if (!patch)
|
|
167
|
+
return 0;
|
|
168
|
+
let removed = 0;
|
|
169
|
+
for (const line of patch.split("\n")) {
|
|
170
|
+
if (line.startsWith("-") && !line.startsWith("---"))
|
|
171
|
+
removed += 1;
|
|
172
|
+
}
|
|
173
|
+
return removed;
|
|
174
|
+
}
|
|
175
|
+
const EXPAND_HINT = "(ctrl+o to expand)";
|
|
176
|
+
/**
|
|
177
|
+
* `⎿ Wrote N lines to path` + a numbered, syntax-highlighted preview.
|
|
178
|
+
* `highlight` is injectable so tests stay independent of pi's theme state.
|
|
179
|
+
*/
|
|
180
|
+
export function formatWriteBody(rawPath, content, expanded, theme, cwd, highlight = highlightForPath) {
|
|
181
|
+
const allLines = content.replace(/\n$/, "").split("\n");
|
|
182
|
+
const total = allLines.length;
|
|
183
|
+
const shownCount = expanded ? Math.min(total, EXPANDED_MAX_LINES) : Math.min(total, WRITE_PREVIEW_LINES);
|
|
184
|
+
const head = ` ${theme.fg("muted", "⎿")} Wrote ${total} ${total === 1 ? "line" : "lines"} to ${theme.fg("accent", displayPath(rawPath, cwd))}`;
|
|
185
|
+
const shown = allLines.slice(0, shownCount);
|
|
186
|
+
const painted = highlight(shown.join("\n"), rawPath);
|
|
187
|
+
const numberWidth = String(shownCount).length;
|
|
188
|
+
const body = painted
|
|
189
|
+
.slice(0, shownCount)
|
|
190
|
+
.map((line, i) => ` ${theme.fg("dim", String(i + 1).padStart(numberWidth))} ${line}`);
|
|
191
|
+
const lines = [head, ...body];
|
|
192
|
+
if (total > shownCount) {
|
|
193
|
+
lines.push(` ${theme.fg("muted", `… +${total - shownCount} lines ${EXPAND_HINT}`)}`);
|
|
194
|
+
}
|
|
195
|
+
return lines;
|
|
196
|
+
}
|
|
197
|
+
/** Default highlighter: pi's theme-backed syntax highlighting, failing to plain lines. */
|
|
198
|
+
function highlightForPath(code, filePath) {
|
|
199
|
+
try {
|
|
200
|
+
return highlightCode(code, getLanguageFromPath(filePath));
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return code.split("\n");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* `⎿ Updated path (+A -R)` + a colored diff preview.
|
|
208
|
+
* `paintDiff` is injectable for the same reason as `highlight` above.
|
|
209
|
+
*/
|
|
210
|
+
export function formatEditBody(rawPath, details, expanded, theme, cwd, paintDiff = defaultPaintDiff) {
|
|
211
|
+
const added = countPatchAdditions(details?.patch);
|
|
212
|
+
const removed = countPatchRemovals(details?.patch);
|
|
213
|
+
const counts = added || removed
|
|
214
|
+
? ` (${[added ? theme.fg("success", `+${added}`) : "", removed ? theme.fg("error", `-${removed}`) : ""]
|
|
215
|
+
.filter(Boolean)
|
|
216
|
+
.join(" ")})`
|
|
217
|
+
: "";
|
|
218
|
+
const head = ` ${theme.fg("muted", "⎿")} Updated ${theme.fg("accent", displayPath(rawPath, cwd))}${counts}`;
|
|
219
|
+
const diffText = details?.diff;
|
|
220
|
+
if (!diffText)
|
|
221
|
+
return [head];
|
|
222
|
+
const diffLines = paintDiff(diffText).split("\n");
|
|
223
|
+
const cap = expanded ? EXPANDED_MAX_LINES : DIFF_PREVIEW_LINES;
|
|
224
|
+
const lines = [head, ...indent(diffLines.slice(0, cap), " ")];
|
|
225
|
+
if (diffLines.length > cap) {
|
|
226
|
+
lines.push(` ${theme.fg("muted", `… +${diffLines.length - cap} lines ${EXPAND_HINT}`)}`);
|
|
227
|
+
}
|
|
228
|
+
return lines;
|
|
229
|
+
}
|
|
230
|
+
function defaultPaintDiff(diffText) {
|
|
231
|
+
try {
|
|
232
|
+
return renderDiff(diffText);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return diffText;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/** Expanded body: the raw output, dimmed and capped. */
|
|
239
|
+
export function formatExpandedOutput(outputText, theme) {
|
|
240
|
+
const lines = outputText.replace(/\n$/, "").split("\n");
|
|
241
|
+
const shown = lines.slice(0, EXPANDED_MAX_LINES).map((l) => theme.fg("toolOutput", l));
|
|
242
|
+
if (lines.length > EXPANDED_MAX_LINES) {
|
|
243
|
+
shown.push(theme.fg("muted", `… +${lines.length - EXPANDED_MAX_LINES} lines`));
|
|
244
|
+
}
|
|
245
|
+
return shown;
|
|
246
|
+
}
|
|
247
|
+
function textOf(result) {
|
|
248
|
+
return (result?.content ?? [])
|
|
249
|
+
.filter((c) => c.type === "text" && typeof c.text === "string")
|
|
250
|
+
.map((c) => c.text)
|
|
251
|
+
.join("\n");
|
|
252
|
+
}
|
|
253
|
+
function hasImages(result) {
|
|
254
|
+
return (result?.content ?? []).some((c) => c.type === "image");
|
|
255
|
+
}
|
|
256
|
+
function empty() {
|
|
257
|
+
return new Container();
|
|
258
|
+
}
|
|
259
|
+
function textComponent(lines) {
|
|
260
|
+
return lines.length === 0 ? empty() : new Text(lines.join("\n"), 0, 0);
|
|
261
|
+
}
|
|
262
|
+
function defaultLoadSettings(cwd) {
|
|
263
|
+
return SettingsManager.create(cwd, getAgentDir());
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Re-register the seven built-ins with condensed renderers. Returns the
|
|
267
|
+
* tracker so callers (and tests) can feed message boundaries into it.
|
|
268
|
+
*/
|
|
269
|
+
export function registerCondensedTools(pi, deps = {}) {
|
|
270
|
+
const tracker = new ToolRunTracker();
|
|
271
|
+
const registrationCwd = deps.cwd ?? process.cwd();
|
|
272
|
+
// Assistant/user prose breaks a run; toolResult messages must not (they are
|
|
273
|
+
// interleaved between the very rows a run aggregates).
|
|
274
|
+
pi.on("message_start", (event) => {
|
|
275
|
+
const role = event.message?.role;
|
|
276
|
+
if (role === "assistant" || role === "user")
|
|
277
|
+
tracker.markBreak();
|
|
278
|
+
});
|
|
279
|
+
let toolOptions;
|
|
280
|
+
try {
|
|
281
|
+
const settings = (deps.loadSettings ?? defaultLoadSettings)(registrationCwd);
|
|
282
|
+
toolOptions = {
|
|
283
|
+
bash: { shellPath: settings.getShellPath(), commandPrefix: settings.getShellCommandPrefix() },
|
|
284
|
+
read: { autoResizeImages: settings.getImageAutoResize() },
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
toolOptions = { bash: {}, read: {} };
|
|
289
|
+
}
|
|
290
|
+
const buildDefinitions = (cwd) => ({
|
|
291
|
+
read: createReadToolDefinition(cwd, toolOptions.read),
|
|
292
|
+
bash: createBashToolDefinition(cwd, toolOptions.bash),
|
|
293
|
+
edit: createEditToolDefinition(cwd),
|
|
294
|
+
write: createWriteToolDefinition(cwd),
|
|
295
|
+
grep: createGrepToolDefinition(cwd),
|
|
296
|
+
find: createFindToolDefinition(cwd),
|
|
297
|
+
ls: createLsToolDefinition(cwd),
|
|
298
|
+
});
|
|
299
|
+
const definitionsByCwd = new Map();
|
|
300
|
+
const definitionsFor = (cwd) => {
|
|
301
|
+
let defs = definitionsByCwd.get(cwd);
|
|
302
|
+
if (!defs) {
|
|
303
|
+
defs = buildDefinitions(cwd);
|
|
304
|
+
definitionsByCwd.set(cwd, defs);
|
|
305
|
+
}
|
|
306
|
+
return defs;
|
|
307
|
+
};
|
|
308
|
+
definitionsByCwd.set(registrationCwd, buildDefinitions(registrationCwd));
|
|
309
|
+
for (const name of BUILTIN_NAMES) {
|
|
310
|
+
const base = definitionsFor(registrationCwd)[name];
|
|
311
|
+
const kind = kindForTool(name);
|
|
312
|
+
const upsertFromCall = (context) => {
|
|
313
|
+
const args = context.args;
|
|
314
|
+
return tracker.upsert(context.toolCallId, {
|
|
315
|
+
kind,
|
|
316
|
+
scratchpad: isScratchpadPath(args?.path, deps.scratchpadDir, context.cwd),
|
|
317
|
+
error: context.isError,
|
|
318
|
+
final: !context.isPartial,
|
|
319
|
+
invalidate: context.invalidate,
|
|
320
|
+
});
|
|
321
|
+
};
|
|
322
|
+
pi.registerTool({
|
|
323
|
+
...base,
|
|
324
|
+
renderShell: "self",
|
|
325
|
+
execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
326
|
+
const cwd = ctx?.cwd ?? registrationCwd;
|
|
327
|
+
return definitionsFor(cwd)[name].execute(toolCallId, params, signal, onUpdate, ctx);
|
|
328
|
+
},
|
|
329
|
+
renderCall(args, theme, context) {
|
|
330
|
+
const slice = context;
|
|
331
|
+
const row = upsertFromCall(slice);
|
|
332
|
+
const status = slice.isError ? "error" : slice.isPartial ? "running" : "ok";
|
|
333
|
+
const argsRecord = args;
|
|
334
|
+
if (slice.expanded || !isQuiet(row)) {
|
|
335
|
+
return new Text(formatRowTitle(name, argsRecord, status, theme, slice.cwd), 0, 0);
|
|
336
|
+
}
|
|
337
|
+
if (!row.final) {
|
|
338
|
+
return new Text(formatRowTitle(name, argsRecord, "running", theme, slice.cwd), 0, 0);
|
|
339
|
+
}
|
|
340
|
+
const summary = tracker.summaryFor(row.id);
|
|
341
|
+
return summary ? new Text(theme.fg("muted", summary), 0, 0) : empty();
|
|
342
|
+
},
|
|
343
|
+
renderResult(result, options, theme, context) {
|
|
344
|
+
const slice = context;
|
|
345
|
+
const args = (slice.args ?? {});
|
|
346
|
+
const row = tracker.upsert(slice.toolCallId, {
|
|
347
|
+
kind,
|
|
348
|
+
final: !options.isPartial,
|
|
349
|
+
error: slice.isError,
|
|
350
|
+
images: hasImages(result),
|
|
351
|
+
added: name === "write" && typeof args.content === "string"
|
|
352
|
+
? args.content.replace(/\n$/, "").split("\n").length
|
|
353
|
+
: name === "edit"
|
|
354
|
+
? countPatchAdditions(result.details?.patch)
|
|
355
|
+
: 0,
|
|
356
|
+
});
|
|
357
|
+
if (options.expanded) {
|
|
358
|
+
if (name === "write" && typeof args.path === "string" && typeof args.content === "string" && !slice.isError) {
|
|
359
|
+
return textComponent(formatWriteBody(args.path, args.content, true, theme, slice.cwd));
|
|
360
|
+
}
|
|
361
|
+
if (name === "edit" && typeof args.path === "string" && !slice.isError && result.details) {
|
|
362
|
+
return textComponent(formatEditBody(args.path, result.details, true, theme, slice.cwd));
|
|
363
|
+
}
|
|
364
|
+
const output = textOf(result);
|
|
365
|
+
return output ? textComponent(formatExpandedOutput(output, theme)) : empty();
|
|
366
|
+
}
|
|
367
|
+
if (name === "bash" && options.isPartial) {
|
|
368
|
+
return textComponent(formatBashPartialBody(textOf(result), theme));
|
|
369
|
+
}
|
|
370
|
+
if (isQuiet(row))
|
|
371
|
+
return empty();
|
|
372
|
+
if (slice.isError) {
|
|
373
|
+
if (name === "bash")
|
|
374
|
+
return textComponent(formatBashErrorBody(textOf(result), theme));
|
|
375
|
+
const message = textOf(result).split("\n").filter(Boolean).slice(-ERROR_TAIL_LINES);
|
|
376
|
+
return textComponent(message.map((l, i) => (i === 0 ? ` ${theme.fg("muted", "⎿")} ${theme.fg("error", l)}` : ` ${theme.fg("toolOutput", l)}`)));
|
|
377
|
+
}
|
|
378
|
+
if (name === "write" && typeof args.path === "string" && typeof args.content === "string") {
|
|
379
|
+
return textComponent(formatWriteBody(args.path, args.content, false, theme, slice.cwd));
|
|
380
|
+
}
|
|
381
|
+
if (name === "edit" && typeof args.path === "string") {
|
|
382
|
+
return textComponent(formatEditBody(args.path, result.details, false, theme, slice.cwd));
|
|
383
|
+
}
|
|
384
|
+
// Visible for another reason (e.g. an image read): title already
|
|
385
|
+
// painted by renderCall; images render themselves below the row.
|
|
386
|
+
return empty();
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
return tracker;
|
|
391
|
+
}
|
|
392
|
+
//# sourceMappingURL=condensedTools.js.map
|
|
@@ -186,6 +186,12 @@ export { makeTokenProvider, makeAuthedFetch, persistRotationToProfile, PROACTIVE
|
|
|
186
186
|
export type { TokenProvider, TokenProviderDeps, ScheduleFn } from "./tokenProvider.js";
|
|
187
187
|
export { appendToSpool, loadSpool, flushSpool, sendOrSpool, spoolFile, MAX_SPOOL_AGE_MS, _setSpoolHomeForTest, } from "./spool.js";
|
|
188
188
|
export type { SpoolEntry, SpoolClientOpts, FlushOutcome, JudgmentWriteOutcome } from "./spool.js";
|
|
189
|
+
export { registerCondensedTools, displayPath, isScratchpadPath, primaryArg, formatRowTitle, formatWriteBody, formatEditBody, formatBashErrorBody, formatBashPartialBody, formatExpandedOutput, splitBashError, countPatchAdditions, WRITE_PREVIEW_LINES, DIFF_PREVIEW_LINES, } from "./condensedTools.js";
|
|
190
|
+
export type { RegisterCondensedToolsDeps, RowStatus } from "./condensedTools.js";
|
|
191
|
+
export { ToolRunTracker, summarizeRun, isQuiet, kindForTool } from "./toolRuns.js";
|
|
192
|
+
export type { RowKind, ToolRow, ToolRowPatch } from "./toolRuns.js";
|
|
193
|
+
export { registerWorkingLine, composeWorkingMessage, NULL_WORKING_LINE, WORKING_INDICATOR_FRAMES, WORKING_VERBS, } from "./workingLine.js";
|
|
194
|
+
export type { WorkingLineHandle, RegisterWorkingLineDeps } from "./workingLine.js";
|
|
189
195
|
export { crashReportsDisabled, installUncaughtExceptionMonitor, makeCrashReporter, reportFatalCrash, sanitizeCrashError, sanitizeCrashText, } from "./crashReport.js";
|
|
190
196
|
export type { CrashReporter, CrashReporterOpts, FatalCrashOpts, SanitizedCrash } from "./crashReport.js";
|
|
191
197
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/extension/index.js
CHANGED
|
@@ -49,6 +49,9 @@ import { registerChipEditor } from "./chipEditor.js";
|
|
|
49
49
|
import { defaultMineBeatGit, fileMineBeatMarkers, maybeOfferMiningBeat as defaultMaybeOfferMiningBeat, } from "./mineBeat.js";
|
|
50
50
|
import { makeFlywheelState } from "./flywheel.js";
|
|
51
51
|
import { registerSilentTurnReminder } from "./silentTurnReminder.js";
|
|
52
|
+
import { registerCondensedTools } from "./condensedTools.js";
|
|
53
|
+
import { isDesktopSurface } from "./surface.js";
|
|
54
|
+
import { registerWorkingLine, WORKING_INDICATOR_FRAMES } from "./workingLine.js";
|
|
52
55
|
function isEvalMode(env = process.env) {
|
|
53
56
|
return env.YAGNI_CODE_EVAL_MODE === "1";
|
|
54
57
|
}
|
|
@@ -161,6 +164,11 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
161
164
|
// no-position record suggestions reach the model) and record_decision
|
|
162
165
|
// (flywheel-attributed records send dedupe: true). Run 7.
|
|
163
166
|
const flywheelState = makeFlywheelState();
|
|
167
|
+
// The composed streaming status line ("Shaping… (12m 54s · ↓ 47.5k tokens)").
|
|
168
|
+
// Registered before the subagent/advisor tools: they publish their live
|
|
169
|
+
// progress through this handle so the elapsed/token suffix survives their
|
|
170
|
+
// overrides. TUI-gated internally (agent_start checks ctx.mode).
|
|
171
|
+
const workingLine = registerWorkingLine(pi);
|
|
164
172
|
pi.registerTool(makeAskYagniTool({ ...toolOpts, flywheel: flywheelState, getRepo: () => sessionRepo }));
|
|
165
173
|
// WebFetch (YAG-578): read an arbitrary URL as clean markdown + a
|
|
166
174
|
// standard-tier extraction, replacing the bash + curl + python dance.
|
|
@@ -178,7 +186,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
178
186
|
// hide it forever from a session that switched TO Advanced. One state handle
|
|
179
187
|
// per session, shared with /advise so they draw on the same cap.
|
|
180
188
|
const advisorState = makeAdvisorState();
|
|
181
|
-
const askAdvisorTool = makeAskAdvisorTool({ state: advisorState });
|
|
189
|
+
const askAdvisorTool = makeAskAdvisorTool({ state: advisorState, workingLine });
|
|
182
190
|
pi.registerTool(askAdvisorTool);
|
|
183
191
|
// /advise runs the SAME tool, sharing the state handle, so a manual consult
|
|
184
192
|
// draws on the same cap rather than opening a side channel around it.
|
|
@@ -218,7 +226,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
218
226
|
// driver's delegation identity for the diamond directive (see the
|
|
219
227
|
// before_agent_start handler below) and widens the tool's fan-out ceiling.
|
|
220
228
|
const ultraHolder = createUltraHolder();
|
|
221
|
-
registerSubagents(pi, { isUltra: () => ultraHolder.get() });
|
|
229
|
+
registerSubagents(pi, { isUltra: () => ultraHolder.get(), workingLine });
|
|
222
230
|
registerUltraCommand(pi, ultraHolder);
|
|
223
231
|
// Shared fetch timeout for the small, interactive display-path reads below
|
|
224
232
|
// (/cost's spend + headroom, and Task 8's per-run spend for /go's summary):
|
|
@@ -620,6 +628,25 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
620
628
|
});
|
|
621
629
|
}
|
|
622
630
|
}
|
|
631
|
+
// The condensed Claude Code-style transcript: the seven built-ins re-render
|
|
632
|
+
// as flat "● Tool(arg)" rows, quiet runs collapse into one "Read 2 files,
|
|
633
|
+
// ran 2 shell commands" summary line, and project writes/edits keep visible
|
|
634
|
+
// previews. Execution delegates to the same factory-made built-ins pi would
|
|
635
|
+
// have used (settings-honoring), so ONLY presentation changes — which is why
|
|
636
|
+
// eval mode keeps pi's stock registration (measured behavior stays
|
|
637
|
+
// byte-identical) and the desktop surface keeps the structured pipeline.
|
|
638
|
+
// YAGNI_CLASSIC_TOOL_ROWS=1 is the debugging escape hatch back to pi's
|
|
639
|
+
// boxed renderers.
|
|
640
|
+
if (!evalMode && !isDesktopSurface() && env.YAGNI_CLASSIC_TOOL_ROWS !== "1") {
|
|
641
|
+
try {
|
|
642
|
+
registerCondensedTools(pi, {
|
|
643
|
+
...(scratchpadDirPath ? { scratchpadDir: scratchpadDirPath } : {}),
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
// Rendering must never break activation; pi's built-ins remain.
|
|
648
|
+
}
|
|
649
|
+
}
|
|
623
650
|
// Own the identity + inject live company context (and repo rules) on every
|
|
624
651
|
// turn. The extension loads identically in every pi process this app spawns —
|
|
625
652
|
// the interactive driver AND every `/go` stage child, subagent, and advisor
|
|
@@ -847,6 +874,14 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
847
874
|
catch {
|
|
848
875
|
// A label must never disrupt session start.
|
|
849
876
|
}
|
|
877
|
+
// The pulsing star indicator that pairs with the composed working line.
|
|
878
|
+
// Fails closed to pi's default spinner.
|
|
879
|
+
try {
|
|
880
|
+
ctx.ui?.setWorkingIndicator?.({ frames: [...WORKING_INDICATOR_FRAMES], intervalMs: 140 });
|
|
881
|
+
}
|
|
882
|
+
catch {
|
|
883
|
+
// An indicator must never disrupt session start.
|
|
884
|
+
}
|
|
850
885
|
// One-time hint naming what changed now that reasoning is collapsed by
|
|
851
886
|
// default (option 2). Gated on the launcher's YAGNI_HIDE_THINKING_SEEDED
|
|
852
887
|
// marker so it fires only on the launch that actually seeded the key — it
|
|
@@ -1043,5 +1078,10 @@ export { makeTokenProvider, makeAuthedFetch, persistRotationToProfile, PROACTIVE
|
|
|
1043
1078
|
// W4 trust plumbing: the durable write-spool for the judgment-capture tools
|
|
1044
1079
|
// (idempotencyKey-replayed; server-side dedup makes flush safe).
|
|
1045
1080
|
export { appendToSpool, loadSpool, flushSpool, sendOrSpool, spoolFile, MAX_SPOOL_AGE_MS, _setSpoolHomeForTest, } from "./spool.js";
|
|
1081
|
+
// The condensed Claude Code-style transcript: flat tool rows, run summaries,
|
|
1082
|
+
// and the composed streaming status line.
|
|
1083
|
+
export { registerCondensedTools, displayPath, isScratchpadPath, primaryArg, formatRowTitle, formatWriteBody, formatEditBody, formatBashErrorBody, formatBashPartialBody, formatExpandedOutput, splitBashError, countPatchAdditions, WRITE_PREVIEW_LINES, DIFF_PREVIEW_LINES, } from "./condensedTools.js";
|
|
1084
|
+
export { ToolRunTracker, summarizeRun, isQuiet, kindForTool } from "./toolRuns.js";
|
|
1085
|
+
export { registerWorkingLine, composeWorkingMessage, NULL_WORKING_LINE, WORKING_INDICATOR_FRAMES, WORKING_VERBS, } from "./workingLine.js";
|
|
1046
1086
|
export { crashReportsDisabled, installUncaughtExceptionMonitor, makeCrashReporter, reportFatalCrash, sanitizeCrashError, sanitizeCrashText, } from "./crashReport.js";
|
|
1047
1087
|
//# sourceMappingURL=index.js.map
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
21
21
|
import { Type } from "typebox";
|
|
22
|
+
import type { WorkingLineHandle } from "./workingLine.js";
|
|
22
23
|
import { runStage } from "./pipeline/runner.js";
|
|
23
24
|
import { type ModelTier, type PipelineStage } from "./pipeline/types.js";
|
|
24
25
|
import { renderSubagentCall, renderSubagentResult } from "./subagentRender.js";
|
|
@@ -96,6 +97,12 @@ export interface MakeSubagentToolDeps {
|
|
|
96
97
|
homeDir?: string;
|
|
97
98
|
/** Live ultra-mode probe (/ultra): widens the per-call fan-out ceiling. */
|
|
98
99
|
isUltra?: () => boolean;
|
|
100
|
+
/**
|
|
101
|
+
* The session working-line manager (workingLine.ts). When present, live
|
|
102
|
+
* progress goes through it (so the elapsed/token suffix survives); absent,
|
|
103
|
+
* the tool falls back to setting ui.setWorkingMessage directly.
|
|
104
|
+
*/
|
|
105
|
+
workingLine?: WorkingLineHandle;
|
|
99
106
|
}
|
|
100
107
|
export declare function makeSubagentTool(deps?: MakeSubagentToolDeps): {
|
|
101
108
|
name: string;
|
|
@@ -110,6 +117,7 @@ export declare function makeSubagentTool(deps?: MakeSubagentToolDeps): {
|
|
|
110
117
|
agent: Type.TOptional<Type.TString>;
|
|
111
118
|
}>>>;
|
|
112
119
|
}>;
|
|
120
|
+
renderShell: "self";
|
|
113
121
|
renderCall: typeof renderSubagentCall;
|
|
114
122
|
renderResult: typeof renderSubagentResult;
|
|
115
123
|
execute(_toolCallId: string, params: SubagentParams, signal?: AbortSignal, onUpdate?: (update: {
|
|
@@ -141,6 +149,8 @@ export interface RegisterSubagentsDeps {
|
|
|
141
149
|
homeDir?: string;
|
|
142
150
|
/** Live ultra-mode probe (/ultra): widens the per-call fan-out ceiling. */
|
|
143
151
|
isUltra?: () => boolean;
|
|
152
|
+
/** Session working-line manager; see MakeSubagentToolDeps.workingLine. */
|
|
153
|
+
workingLine?: WorkingLineHandle;
|
|
144
154
|
}
|
|
145
155
|
/** Wire the subagent tool and the /agents listing command. */
|
|
146
156
|
export declare function registerSubagents(pi: ExtensionAPI, deps?: RegisterSubagentsDeps): void;
|
|
@@ -350,6 +350,8 @@ export function makeSubagentTool(deps = {}) {
|
|
|
350
350
|
"(list them with /agents); omit `agent` for the general-purpose one.",
|
|
351
351
|
promptSnippet: "subagent: delegate a self-contained task (or parallel tasks) to a fresh-context agent; returns its report.",
|
|
352
352
|
parameters,
|
|
353
|
+
// Self-framed: the condensed transcript look has no tinted tool boxes.
|
|
354
|
+
renderShell: "self",
|
|
353
355
|
renderCall: renderSubagentCall,
|
|
354
356
|
renderResult: renderSubagentResult,
|
|
355
357
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
@@ -397,9 +399,12 @@ export function makeSubagentTool(deps = {}) {
|
|
|
397
399
|
},
|
|
398
400
|
});
|
|
399
401
|
const working = formatWorkingMessage(progresses, now);
|
|
400
|
-
if (
|
|
402
|
+
if (working !== lastWorking) {
|
|
401
403
|
lastWorking = working;
|
|
402
|
-
|
|
404
|
+
if (deps.workingLine)
|
|
405
|
+
deps.workingLine.setActivity(working);
|
|
406
|
+
else
|
|
407
|
+
ui?.setWorkingMessage?.(working);
|
|
403
408
|
}
|
|
404
409
|
};
|
|
405
410
|
emit();
|
|
@@ -429,7 +434,10 @@ export function makeSubagentTool(deps = {}) {
|
|
|
429
434
|
}
|
|
430
435
|
finally {
|
|
431
436
|
// Restore the default "Working…" text whether we resolved or threw.
|
|
432
|
-
|
|
437
|
+
if (deps.workingLine)
|
|
438
|
+
deps.workingLine.setActivity(undefined);
|
|
439
|
+
else
|
|
440
|
+
ui?.setWorkingMessage?.();
|
|
433
441
|
}
|
|
434
442
|
const allFailed = outcomes.every((o) => o.result.exitCode !== 0);
|
|
435
443
|
const sections = outcomes.map((o) => {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run model for the condensed tool transcript (Claude Code-style).
|
|
3
|
+
*
|
|
4
|
+
* A "run" is a maximal stretch of consecutive QUIET tool rows (reads, searches,
|
|
5
|
+
* listings, successful shell commands, scratchpad edits) uninterrupted by an
|
|
6
|
+
* assistant/user message or a visible row (errors, project writes/edits, image
|
|
7
|
+
* reads). Collapsed, every row in a completed run renders zero lines except the
|
|
8
|
+
* run's tail, which paints one summary line: "Read 2 files, ran 2 shell
|
|
9
|
+
* commands". Expanded (ctrl+o) bypasses this model entirely.
|
|
10
|
+
*
|
|
11
|
+
* The tracker is built purely from render calls (idempotent upserts keyed by
|
|
12
|
+
* toolCallId, in first-render order, which matches display order both live and
|
|
13
|
+
* on session replay). Message boundaries arrive via {@link ToolRunTracker.markBreak}
|
|
14
|
+
* from live `message_start` events only — after a resume, runs that were
|
|
15
|
+
* separated by prose may merge into one summary. That is a deliberate trade:
|
|
16
|
+
* render-derived state is the only state that survives replay.
|
|
17
|
+
*
|
|
18
|
+
* Everything here is PURE (no pi imports) so tests run against plain objects;
|
|
19
|
+
* the pi wiring lives in condensedTools.ts.
|
|
20
|
+
*/
|
|
21
|
+
/** Aggregation category of one tool row. */
|
|
22
|
+
export type RowKind = "read" | "shell" | "search" | "list" | "write" | "edit";
|
|
23
|
+
/** Facts about one tool row; `quiet` is derived, never stored. */
|
|
24
|
+
export interface ToolRow {
|
|
25
|
+
id: string;
|
|
26
|
+
kind: RowKind;
|
|
27
|
+
/** The row's target path lives under the session scratchpad dir. */
|
|
28
|
+
scratchpad: boolean;
|
|
29
|
+
/** The tool call errored (visible regardless of kind). */
|
|
30
|
+
error: boolean;
|
|
31
|
+
/** The result carries inline images (a read screenshot must stay visible). */
|
|
32
|
+
images: boolean;
|
|
33
|
+
/** The result is final (not partial/streaming). */
|
|
34
|
+
final: boolean;
|
|
35
|
+
/** Added lines (scratchpad edits surface as "+N" in the summary). */
|
|
36
|
+
added: number;
|
|
37
|
+
/** An assistant/user message landed between the previous row and this one. */
|
|
38
|
+
breakBefore: boolean;
|
|
39
|
+
/** Repaint hook for this row's component (captured from the render context). */
|
|
40
|
+
invalidate?: () => void;
|
|
41
|
+
/** A deferred repaint is already queued for this row. */
|
|
42
|
+
invalidatePending?: boolean;
|
|
43
|
+
}
|
|
44
|
+
export type ToolRowPatch = Partial<Pick<ToolRow, "kind" | "scratchpad" | "error" | "images" | "final" | "added" | "invalidate">>;
|
|
45
|
+
/** Aggregation kind for a built-in tool name (undefined for non-built-ins). */
|
|
46
|
+
export declare function kindForTool(toolName: string): RowKind | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Whether a row folds into a run summary. Reads, searches, listings, and
|
|
49
|
+
* successful shell commands always do; writes/edits only when they target the
|
|
50
|
+
* scratchpad (project mutations must stay visible). Errors and image-bearing
|
|
51
|
+
* results are always visible.
|
|
52
|
+
*/
|
|
53
|
+
export declare function isQuiet(row: Pick<ToolRow, "kind" | "scratchpad" | "error" | "images">): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* One summary line for a completed run, phrases in first-occurrence order:
|
|
56
|
+
* "Read 2 files, ran 2 shell commands, made 1 scratchpad edit +20".
|
|
57
|
+
*/
|
|
58
|
+
export declare function summarizeRun(rows: readonly Pick<ToolRow, "kind" | "scratchpad" | "added">[]): string;
|
|
59
|
+
/**
|
|
60
|
+
* Ordered row registry. Upserts are idempotent and diff-aware: only a change
|
|
61
|
+
* invalidates the affected run's tail (the one component whose output depends
|
|
62
|
+
* on neighbors), so repaints converge instead of looping.
|
|
63
|
+
*/
|
|
64
|
+
export declare class ToolRunTracker {
|
|
65
|
+
private rows;
|
|
66
|
+
private indexById;
|
|
67
|
+
private breakPending;
|
|
68
|
+
/** Record an assistant/user message boundary; the next new row starts a fresh run. */
|
|
69
|
+
markBreak(): void;
|
|
70
|
+
get(id: string): ToolRow | undefined;
|
|
71
|
+
upsert(id: string, patch: ToolRowPatch): ToolRow;
|
|
72
|
+
/**
|
|
73
|
+
* The one summary line for `id`, present only when `id` is the tail of a
|
|
74
|
+
* fully-final quiet run. Every other member of the run gets undefined.
|
|
75
|
+
*/
|
|
76
|
+
summaryFor(id: string): string | undefined;
|
|
77
|
+
/** The contiguous quiet run containing `row` (just `[row]` when visible). */
|
|
78
|
+
private runOf;
|
|
79
|
+
private invalidateTailOf;
|
|
80
|
+
/**
|
|
81
|
+
* Defer a row's repaint to a microtask, deduped per row. Upserts run INSIDE
|
|
82
|
+
* pi's synchronous `updateDisplay` pass (renderers call them), and the
|
|
83
|
+
* component's `invalidate()` re-enters `updateDisplay` immediately — a
|
|
84
|
+
* synchronous call from a renderer would rebuild the container while the
|
|
85
|
+
* outer frame is still appending to it, stacking duplicate children.
|
|
86
|
+
* Deferring means every repaint runs as its own clean top-level pass; it
|
|
87
|
+
* still cannot loop, because the diff-aware upsert only schedules on an
|
|
88
|
+
* actual change.
|
|
89
|
+
*/
|
|
90
|
+
private scheduleInvalidate;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=toolRuns.d.ts.map
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run model for the condensed tool transcript (Claude Code-style).
|
|
3
|
+
*
|
|
4
|
+
* A "run" is a maximal stretch of consecutive QUIET tool rows (reads, searches,
|
|
5
|
+
* listings, successful shell commands, scratchpad edits) uninterrupted by an
|
|
6
|
+
* assistant/user message or a visible row (errors, project writes/edits, image
|
|
7
|
+
* reads). Collapsed, every row in a completed run renders zero lines except the
|
|
8
|
+
* run's tail, which paints one summary line: "Read 2 files, ran 2 shell
|
|
9
|
+
* commands". Expanded (ctrl+o) bypasses this model entirely.
|
|
10
|
+
*
|
|
11
|
+
* The tracker is built purely from render calls (idempotent upserts keyed by
|
|
12
|
+
* toolCallId, in first-render order, which matches display order both live and
|
|
13
|
+
* on session replay). Message boundaries arrive via {@link ToolRunTracker.markBreak}
|
|
14
|
+
* from live `message_start` events only — after a resume, runs that were
|
|
15
|
+
* separated by prose may merge into one summary. That is a deliberate trade:
|
|
16
|
+
* render-derived state is the only state that survives replay.
|
|
17
|
+
*
|
|
18
|
+
* Everything here is PURE (no pi imports) so tests run against plain objects;
|
|
19
|
+
* the pi wiring lives in condensedTools.ts.
|
|
20
|
+
*/
|
|
21
|
+
const KIND_BY_TOOL = {
|
|
22
|
+
read: "read",
|
|
23
|
+
bash: "shell",
|
|
24
|
+
grep: "search",
|
|
25
|
+
find: "list",
|
|
26
|
+
ls: "list",
|
|
27
|
+
write: "write",
|
|
28
|
+
edit: "edit",
|
|
29
|
+
};
|
|
30
|
+
/** Aggregation kind for a built-in tool name (undefined for non-built-ins). */
|
|
31
|
+
export function kindForTool(toolName) {
|
|
32
|
+
return KIND_BY_TOOL[toolName];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Whether a row folds into a run summary. Reads, searches, listings, and
|
|
36
|
+
* successful shell commands always do; writes/edits only when they target the
|
|
37
|
+
* scratchpad (project mutations must stay visible). Errors and image-bearing
|
|
38
|
+
* results are always visible.
|
|
39
|
+
*/
|
|
40
|
+
export function isQuiet(row) {
|
|
41
|
+
if (row.error || row.images)
|
|
42
|
+
return false;
|
|
43
|
+
if (row.kind === "write" || row.kind === "edit")
|
|
44
|
+
return row.scratchpad;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
function plural(n, singular, pluralForm = `${singular}s`) {
|
|
48
|
+
return n === 1 ? singular : pluralForm;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* One summary line for a completed run, phrases in first-occurrence order:
|
|
52
|
+
* "Read 2 files, ran 2 shell commands, made 1 scratchpad edit +20".
|
|
53
|
+
*/
|
|
54
|
+
export function summarizeRun(rows) {
|
|
55
|
+
const order = [];
|
|
56
|
+
const counts = new Map();
|
|
57
|
+
for (const row of rows) {
|
|
58
|
+
const category = row.kind === "write" || row.kind === "edit" ? "scratch" : row.kind;
|
|
59
|
+
let entry = counts.get(category);
|
|
60
|
+
if (!entry) {
|
|
61
|
+
entry = { n: 0, added: 0 };
|
|
62
|
+
counts.set(category, entry);
|
|
63
|
+
order.push(category);
|
|
64
|
+
}
|
|
65
|
+
entry.n += 1;
|
|
66
|
+
entry.added += row.added;
|
|
67
|
+
}
|
|
68
|
+
const phrases = order.map((category) => {
|
|
69
|
+
const { n, added } = counts.get(category);
|
|
70
|
+
switch (category) {
|
|
71
|
+
case "read":
|
|
72
|
+
return `read ${n} ${plural(n, "file")}`;
|
|
73
|
+
case "shell":
|
|
74
|
+
return `ran ${n} shell ${plural(n, "command")}`;
|
|
75
|
+
case "search":
|
|
76
|
+
return `searched for ${n} ${plural(n, "pattern")}`;
|
|
77
|
+
case "list":
|
|
78
|
+
return `listed ${n} ${plural(n, "path")}`;
|
|
79
|
+
default:
|
|
80
|
+
return `made ${n} scratchpad ${plural(n, "edit")}${added > 0 ? ` +${added}` : ""}`;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
const line = phrases.join(", ");
|
|
84
|
+
return line.charAt(0).toUpperCase() + line.slice(1);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Ordered row registry. Upserts are idempotent and diff-aware: only a change
|
|
88
|
+
* invalidates the affected run's tail (the one component whose output depends
|
|
89
|
+
* on neighbors), so repaints converge instead of looping.
|
|
90
|
+
*/
|
|
91
|
+
export class ToolRunTracker {
|
|
92
|
+
rows = [];
|
|
93
|
+
indexById = new Map();
|
|
94
|
+
breakPending = true;
|
|
95
|
+
/** Record an assistant/user message boundary; the next new row starts a fresh run. */
|
|
96
|
+
markBreak() {
|
|
97
|
+
this.breakPending = true;
|
|
98
|
+
}
|
|
99
|
+
get(id) {
|
|
100
|
+
const i = this.indexById.get(id);
|
|
101
|
+
return i === undefined ? undefined : this.rows[i];
|
|
102
|
+
}
|
|
103
|
+
upsert(id, patch) {
|
|
104
|
+
const existingIndex = this.indexById.get(id);
|
|
105
|
+
if (existingIndex === undefined) {
|
|
106
|
+
const row = {
|
|
107
|
+
id,
|
|
108
|
+
kind: patch.kind ?? "shell",
|
|
109
|
+
scratchpad: patch.scratchpad ?? false,
|
|
110
|
+
error: patch.error ?? false,
|
|
111
|
+
images: patch.images ?? false,
|
|
112
|
+
final: patch.final ?? false,
|
|
113
|
+
added: patch.added ?? 0,
|
|
114
|
+
breakBefore: this.breakPending,
|
|
115
|
+
...(patch.invalidate ? { invalidate: patch.invalidate } : {}),
|
|
116
|
+
};
|
|
117
|
+
this.breakPending = false;
|
|
118
|
+
this.indexById.set(id, this.rows.length);
|
|
119
|
+
this.rows.push(row);
|
|
120
|
+
// The previous row may have been its run's tail (painting a summary);
|
|
121
|
+
// now that the run extends past it, repaint it as a hidden member.
|
|
122
|
+
// (invalidateTailOf(prev) would be wrong here: prev's run now includes
|
|
123
|
+
// this new row, so its tail is the row currently painting, not prev.)
|
|
124
|
+
const prev = this.rows[this.rows.length - 2];
|
|
125
|
+
if (prev && isQuiet(prev) && !row.breakBefore)
|
|
126
|
+
this.scheduleInvalidate(prev);
|
|
127
|
+
return row;
|
|
128
|
+
}
|
|
129
|
+
const row = this.rows[existingIndex];
|
|
130
|
+
if (patch.invalidate)
|
|
131
|
+
row.invalidate = patch.invalidate;
|
|
132
|
+
let changed = false;
|
|
133
|
+
for (const key of ["kind", "scratchpad", "error", "images", "final", "added"]) {
|
|
134
|
+
const next = patch[key];
|
|
135
|
+
if (next !== undefined && row[key] !== next) {
|
|
136
|
+
row[key] = next;
|
|
137
|
+
changed = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (changed)
|
|
141
|
+
this.invalidateTailOf(row);
|
|
142
|
+
return row;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The one summary line for `id`, present only when `id` is the tail of a
|
|
146
|
+
* fully-final quiet run. Every other member of the run gets undefined.
|
|
147
|
+
*/
|
|
148
|
+
summaryFor(id) {
|
|
149
|
+
const row = this.get(id);
|
|
150
|
+
if (!row || !isQuiet(row) || !row.final)
|
|
151
|
+
return undefined;
|
|
152
|
+
const run = this.runOf(row);
|
|
153
|
+
if (run[run.length - 1] !== row)
|
|
154
|
+
return undefined;
|
|
155
|
+
if (run.some((r) => !r.final))
|
|
156
|
+
return undefined;
|
|
157
|
+
return summarizeRun(run);
|
|
158
|
+
}
|
|
159
|
+
/** The contiguous quiet run containing `row` (just `[row]` when visible). */
|
|
160
|
+
runOf(row) {
|
|
161
|
+
if (!isQuiet(row))
|
|
162
|
+
return [row];
|
|
163
|
+
const i = this.indexById.get(row.id);
|
|
164
|
+
let start = i;
|
|
165
|
+
while (start > 0 && !this.rows[start].breakBefore && isQuiet(this.rows[start - 1]))
|
|
166
|
+
start--;
|
|
167
|
+
let end = i;
|
|
168
|
+
while (end < this.rows.length - 1 &&
|
|
169
|
+
!this.rows[end + 1].breakBefore &&
|
|
170
|
+
isQuiet(this.rows[end + 1])) {
|
|
171
|
+
end++;
|
|
172
|
+
}
|
|
173
|
+
return this.rows.slice(start, end + 1);
|
|
174
|
+
}
|
|
175
|
+
invalidateTailOf(row) {
|
|
176
|
+
const run = this.runOf(row);
|
|
177
|
+
const tail = run[run.length - 1];
|
|
178
|
+
if (tail)
|
|
179
|
+
this.scheduleInvalidate(tail);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Defer a row's repaint to a microtask, deduped per row. Upserts run INSIDE
|
|
183
|
+
* pi's synchronous `updateDisplay` pass (renderers call them), and the
|
|
184
|
+
* component's `invalidate()` re-enters `updateDisplay` immediately — a
|
|
185
|
+
* synchronous call from a renderer would rebuild the container while the
|
|
186
|
+
* outer frame is still appending to it, stacking duplicate children.
|
|
187
|
+
* Deferring means every repaint runs as its own clean top-level pass; it
|
|
188
|
+
* still cannot loop, because the diff-aware upsert only schedules on an
|
|
189
|
+
* actual change.
|
|
190
|
+
*/
|
|
191
|
+
scheduleInvalidate(row) {
|
|
192
|
+
if (row.invalidatePending)
|
|
193
|
+
return;
|
|
194
|
+
row.invalidatePending = true;
|
|
195
|
+
queueMicrotask(() => {
|
|
196
|
+
row.invalidatePending = false;
|
|
197
|
+
row.invalidate?.();
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=toolRuns.js.map
|
|
@@ -44,6 +44,8 @@ export function makeWebFetchTool(opts) {
|
|
|
44
44
|
"Use web_fetch to read a URL's content instead of bash + curl when you need a page or a summary of it.",
|
|
45
45
|
],
|
|
46
46
|
parameters,
|
|
47
|
+
// Self-framed: the condensed transcript look has no tinted tool boxes.
|
|
48
|
+
renderShell: "self",
|
|
47
49
|
renderCall(args, theme) {
|
|
48
50
|
const t = theme;
|
|
49
51
|
const url = clipLine(args?.url ?? "…", 80);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Claude Code-style streaming status line: `Shaping… (12m 54s · ↓ 47.5k
|
|
3
|
+
* tokens)` in place of pi's static "Working...".
|
|
4
|
+
*
|
|
5
|
+
* One manager owns `ctx.ui.setWorkingMessage` for the whole session so the
|
|
6
|
+
* verb, the elapsed clock, and the token counter never fight the subagent /
|
|
7
|
+
* advisor progress text: those tools publish their activity line through
|
|
8
|
+
* {@link WorkingLineHandle.setActivity} (instead of calling setWorkingMessage
|
|
9
|
+
* directly), and the manager splices it in as the head of the same composed
|
|
10
|
+
* message. Elapsed time spans the whole agent loop (agent_start → agent_end);
|
|
11
|
+
* the token counter accumulates assistant output tokens across the loop's
|
|
12
|
+
* messages (message_end), which is when pi learns usage — subagent tokens live
|
|
13
|
+
* in the subagent's own activity text, not this counter.
|
|
14
|
+
*
|
|
15
|
+
* TUI-only by the agent_start guard; a headless /go child or desktop surface
|
|
16
|
+
* never gets a working line. Everything is fail-soft: a status line must never
|
|
17
|
+
* break a turn.
|
|
18
|
+
*/
|
|
19
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
20
|
+
/** What subagents/advisor publish instead of calling setWorkingMessage. */
|
|
21
|
+
export interface WorkingLineHandle {
|
|
22
|
+
setActivity(text?: string): void;
|
|
23
|
+
}
|
|
24
|
+
/** A no-op handle for callers wired without a manager (tests, children). */
|
|
25
|
+
export declare const NULL_WORKING_LINE: WorkingLineHandle;
|
|
26
|
+
/**
|
|
27
|
+
* The verb pool. Neutral gerunds — one is picked per agent loop, so long
|
|
28
|
+
* sessions read as a person at work rather than a stuck spinner.
|
|
29
|
+
*/
|
|
30
|
+
export declare const WORKING_VERBS: readonly string[];
|
|
31
|
+
/** Pulse frames for the streaming indicator (pi renders them verbatim). */
|
|
32
|
+
export declare const WORKING_INDICATOR_FRAMES: string[];
|
|
33
|
+
export interface ComposeWorkingOpts {
|
|
34
|
+
/** Override head from a running subagent/advisor (verb used when absent). */
|
|
35
|
+
activity?: string | undefined;
|
|
36
|
+
verb: string;
|
|
37
|
+
elapsedMs: number;
|
|
38
|
+
outputTokens: number;
|
|
39
|
+
}
|
|
40
|
+
/** `Shaping… (12m 54s · ↓ 47.5k tokens)` — pure, exported for tests. */
|
|
41
|
+
export declare function composeWorkingMessage(opts: ComposeWorkingOpts): string;
|
|
42
|
+
export interface RegisterWorkingLineDeps {
|
|
43
|
+
now?: () => number;
|
|
44
|
+
pickVerb?: (verbs: readonly string[]) => string;
|
|
45
|
+
/** Refresh cadence for the elapsed clock. */
|
|
46
|
+
tickMs?: number;
|
|
47
|
+
}
|
|
48
|
+
export declare function registerWorkingLine(pi: ExtensionAPI, deps?: RegisterWorkingLineDeps): WorkingLineHandle;
|
|
49
|
+
//# sourceMappingURL=workingLine.d.ts.map
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Claude Code-style streaming status line: `Shaping… (12m 54s · ↓ 47.5k
|
|
3
|
+
* tokens)` in place of pi's static "Working...".
|
|
4
|
+
*
|
|
5
|
+
* One manager owns `ctx.ui.setWorkingMessage` for the whole session so the
|
|
6
|
+
* verb, the elapsed clock, and the token counter never fight the subagent /
|
|
7
|
+
* advisor progress text: those tools publish their activity line through
|
|
8
|
+
* {@link WorkingLineHandle.setActivity} (instead of calling setWorkingMessage
|
|
9
|
+
* directly), and the manager splices it in as the head of the same composed
|
|
10
|
+
* message. Elapsed time spans the whole agent loop (agent_start → agent_end);
|
|
11
|
+
* the token counter accumulates assistant output tokens across the loop's
|
|
12
|
+
* messages (message_end), which is when pi learns usage — subagent tokens live
|
|
13
|
+
* in the subagent's own activity text, not this counter.
|
|
14
|
+
*
|
|
15
|
+
* TUI-only by the agent_start guard; a headless /go child or desktop surface
|
|
16
|
+
* never gets a working line. Everything is fail-soft: a status line must never
|
|
17
|
+
* break a turn.
|
|
18
|
+
*/
|
|
19
|
+
import { usageFromMessage } from "./costHud.js";
|
|
20
|
+
import { formatDuration, formatTokens } from "./subagentRender.js";
|
|
21
|
+
/** A no-op handle for callers wired without a manager (tests, children). */
|
|
22
|
+
export const NULL_WORKING_LINE = { setActivity: () => { } };
|
|
23
|
+
/**
|
|
24
|
+
* The verb pool. Neutral gerunds — one is picked per agent loop, so long
|
|
25
|
+
* sessions read as a person at work rather than a stuck spinner.
|
|
26
|
+
*/
|
|
27
|
+
export const WORKING_VERBS = [
|
|
28
|
+
"Working",
|
|
29
|
+
"Thinking",
|
|
30
|
+
"Exploring",
|
|
31
|
+
"Tracing",
|
|
32
|
+
"Shaping",
|
|
33
|
+
"Wiring",
|
|
34
|
+
"Weighing",
|
|
35
|
+
"Sketching",
|
|
36
|
+
"Assembling",
|
|
37
|
+
"Distilling",
|
|
38
|
+
"Untangling",
|
|
39
|
+
"Polishing",
|
|
40
|
+
];
|
|
41
|
+
/** Pulse frames for the streaming indicator (pi renders them verbatim). */
|
|
42
|
+
export const WORKING_INDICATOR_FRAMES = ["·", "✢", "✳", "✶", "✳", "✢"];
|
|
43
|
+
/** `Shaping… (12m 54s · ↓ 47.5k tokens)` — pure, exported for tests. */
|
|
44
|
+
export function composeWorkingMessage(opts) {
|
|
45
|
+
const head = opts.activity ?? `${opts.verb}…`;
|
|
46
|
+
const stats = [
|
|
47
|
+
formatDuration(opts.elapsedMs),
|
|
48
|
+
opts.outputTokens > 0 ? `↓ ${formatTokens(opts.outputTokens)} tokens` : undefined,
|
|
49
|
+
]
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
.join(" · ");
|
|
52
|
+
return `${head} (${stats})`;
|
|
53
|
+
}
|
|
54
|
+
export function registerWorkingLine(pi, deps = {}) {
|
|
55
|
+
const now = deps.now ?? (() => Date.now());
|
|
56
|
+
const pickVerb = deps.pickVerb ?? ((verbs) => verbs[Math.floor(Math.random() * verbs.length)]);
|
|
57
|
+
const tickMs = deps.tickMs ?? 1_000;
|
|
58
|
+
let ui;
|
|
59
|
+
let timer;
|
|
60
|
+
let startedAt;
|
|
61
|
+
let outputTokens = 0;
|
|
62
|
+
let verb = WORKING_VERBS[0];
|
|
63
|
+
let activity;
|
|
64
|
+
const refresh = () => {
|
|
65
|
+
if (!ui || startedAt === undefined)
|
|
66
|
+
return;
|
|
67
|
+
try {
|
|
68
|
+
ui.setWorkingMessage?.(composeWorkingMessage({ activity, verb, elapsedMs: now() - startedAt, outputTokens }));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// The status line must never break a turn.
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const stop = () => {
|
|
75
|
+
if (timer)
|
|
76
|
+
clearInterval(timer);
|
|
77
|
+
timer = undefined;
|
|
78
|
+
startedAt = undefined;
|
|
79
|
+
activity = undefined;
|
|
80
|
+
try {
|
|
81
|
+
ui?.setWorkingMessage?.();
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Restoring the default label is best-effort.
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
88
|
+
if (ctx.mode !== "tui" || !ctx.hasUI)
|
|
89
|
+
return;
|
|
90
|
+
ui = ctx.ui;
|
|
91
|
+
startedAt = now();
|
|
92
|
+
outputTokens = 0;
|
|
93
|
+
activity = undefined;
|
|
94
|
+
verb = pickVerb(WORKING_VERBS);
|
|
95
|
+
refresh();
|
|
96
|
+
if (timer)
|
|
97
|
+
clearInterval(timer);
|
|
98
|
+
timer = setInterval(refresh, tickMs);
|
|
99
|
+
timer.unref?.();
|
|
100
|
+
});
|
|
101
|
+
pi.on("agent_end", () => stop());
|
|
102
|
+
pi.on("message_end", (event) => {
|
|
103
|
+
const message = event.message;
|
|
104
|
+
if (message?.role !== "assistant")
|
|
105
|
+
return;
|
|
106
|
+
outputTokens += usageFromMessage(message).output;
|
|
107
|
+
refresh();
|
|
108
|
+
});
|
|
109
|
+
return {
|
|
110
|
+
setActivity(text) {
|
|
111
|
+
activity = text;
|
|
112
|
+
refresh();
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=workingLine.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "1.0.0-staging.
|
|
3
|
+
"version": "1.0.0-staging.1185.1",
|
|
4
4
|
"description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
|
|
@@ -41,5 +41,5 @@
|
|
|
41
41
|
"turndown": "^7.2.4",
|
|
42
42
|
"typebox": "^1.3.15"
|
|
43
43
|
},
|
|
44
|
-
"yagniSourceSha": "
|
|
44
|
+
"yagniSourceSha": "970cc138faff85585c7d21243aeb553937df2a40"
|
|
45
45
|
}
|