@yagni-app/code 0.3.5 → 1.0.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/README.md +42 -0
- package/dist/cli.js +231 -6
- package/dist/crashReport.d.ts +8 -0
- package/dist/crashReport.js +13 -1
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +33 -0
- package/dist/extension/askAdvisorTool.d.ts +7 -0
- package/dist/extension/askAdvisorTool.js +13 -3
- package/dist/extension/askUserQuestionTool.d.ts +54 -0
- package/dist/extension/askUserQuestionTool.js +621 -0
- package/dist/extension/askYagniTool.js +2 -0
- package/dist/extension/branding.d.ts +15 -0
- package/dist/extension/branding.js +76 -0
- package/dist/extension/chipEditor.d.ts +22 -1
- package/dist/extension/chipEditor.js +58 -5
- package/dist/extension/cmux/state.js +9 -16
- package/dist/extension/condensedTools.d.ts +93 -0
- package/dist/extension/condensedTools.js +392 -0
- package/dist/extension/crashReport.js +12 -0
- package/dist/extension/decisionCapture.js +3 -0
- package/dist/extension/decisions.js +4 -0
- package/dist/extension/diagnostics.d.ts +31 -0
- package/dist/extension/diagnostics.js +53 -55
- package/dist/extension/diffStat.d.ts +62 -0
- package/dist/extension/diffStat.js +158 -0
- package/dist/extension/errorSink.d.ts +64 -0
- package/dist/extension/errorSink.js +180 -0
- package/dist/extension/feedbackCommand.d.ts +38 -0
- package/dist/extension/feedbackCommand.js +151 -0
- package/dist/extension/footer.d.ts +2 -0
- package/dist/extension/footer.js +21 -8
- package/dist/extension/hooks.js +12 -12
- package/dist/extension/index.d.ts +7 -0
- package/dist/extension/index.js +161 -42
- package/dist/extension/mineBeat.js +13 -0
- package/dist/extension/permission/execPolicy.js +47 -0
- package/dist/extension/pipeline/goCommand.js +2 -0
- package/dist/extension/pipeline/invocation.d.ts +7 -0
- package/dist/extension/pipeline/invocation.js +7 -0
- package/dist/extension/pipeline/personas.js +4 -4
- package/dist/extension/pipeline/runner.d.ts +1 -0
- package/dist/extension/pipeline/runner.js +24 -3
- package/dist/extension/pipeline/sessionWorktree.d.ts +64 -0
- package/dist/extension/pipeline/sessionWorktree.js +225 -0
- package/dist/extension/scratchpad.d.ts +66 -0
- package/dist/extension/scratchpad.js +93 -0
- package/dist/extension/silentTurnReminder.js +18 -14
- package/dist/extension/subagents.d.ts +10 -0
- package/dist/extension/subagents.js +18 -4
- package/dist/extension/todos.d.ts +1 -0
- package/dist/extension/todos.js +15 -0
- package/dist/extension/toolRuns.d.ts +92 -0
- package/dist/extension/toolRuns.js +201 -0
- package/dist/extension/turnLog.js +17 -46
- package/dist/extension/webFetch.d.ts +85 -0
- package/dist/extension/webFetch.js +192 -0
- package/dist/extension/webFetchTool.d.ts +34 -0
- package/dist/extension/webFetchTool.js +106 -0
- package/dist/extension/workingLine.d.ts +49 -0
- package/dist/extension/workingLine.js +116 -0
- package/dist/feedback.d.ts +77 -0
- package/dist/feedback.js +500 -0
- package/dist/goHeadless.d.ts +3 -0
- package/dist/goHeadless.js +13 -0
- package/dist/launch.d.ts +8 -0
- package/dist/launch.js +6 -0
- package/dist/otel.d.ts +150 -0
- package/dist/otel.js +291 -0
- package/dist/outputFormat.d.ts +83 -0
- package/dist/outputFormat.js +207 -0
- package/dist/paths.d.ts +10 -0
- package/dist/paths.js +13 -0
- package/dist/worktreeArgs.d.ts +43 -0
- package/dist/worktreeArgs.js +96 -0
- package/package.json +4 -2
|
@@ -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
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
import { spawn } from "node:child_process";
|
|
28
28
|
import { scrubSecrets } from "./pipeline/scrubSecrets.js";
|
|
29
29
|
import { isDesktopSurface } from "./surface.js";
|
|
30
|
+
import { logEvent } from "./errorSink.js";
|
|
30
31
|
const defaultSpawn = (command, args, options) => spawn(command, args, options);
|
|
31
32
|
export const CRASH_REPORT_DISABLE_ENV = "YAGNI_DISABLE_CRASH_REPORTS";
|
|
32
33
|
export const CRASH_REPORT_TIMEOUT_MS = 1_500;
|
|
@@ -267,6 +268,17 @@ export function reportFatalCrash(error, opts, context) {
|
|
|
267
268
|
export function installUncaughtExceptionMonitor(opts, proc = process) {
|
|
268
269
|
proc.on("uncaughtExceptionMonitor", (err) => {
|
|
269
270
|
reportFatalCrash(err, opts, "uncaught-exception");
|
|
271
|
+
// Also seed the local error trail (best-effort): the crash report is a
|
|
272
|
+
// sanitized POST, but the ON-DISK trail is what /feedback binds for a
|
|
273
|
+
// report someone files next session.
|
|
274
|
+
logEvent({
|
|
275
|
+
source: "tool",
|
|
276
|
+
level: "error",
|
|
277
|
+
event: "uncaught_exception",
|
|
278
|
+
sessionId: process.env.YAGNI_SESSION_ID ?? undefined,
|
|
279
|
+
flush: "sync",
|
|
280
|
+
fields: { errorClass: err instanceof Error ? err.name || "Error" : "Error" },
|
|
281
|
+
});
|
|
270
282
|
});
|
|
271
283
|
}
|
|
272
284
|
//# sourceMappingURL=crashReport.js.map
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* never affects the tool call that triggered it.
|
|
18
18
|
*/
|
|
19
19
|
import { bankDecision } from "./decisions.js";
|
|
20
|
+
import { logEvent } from "./errorSink.js";
|
|
20
21
|
/** At most one capture prompt per this window (spec: 10 minutes). */
|
|
21
22
|
export const CAPTURE_DEBOUNCE_MS = 10 * 60 * 1000;
|
|
22
23
|
/** Build a session-scoped decision capture (holds the debounce timestamp). */
|
|
@@ -54,10 +55,12 @@ export function makeDecisionCapture(deps) {
|
|
|
54
55
|
ctx.ui.notify("Saved the decision locally; it will sync automatically.", "info");
|
|
55
56
|
}
|
|
56
57
|
else {
|
|
58
|
+
logEvent({ source: "decision-capture", level: "error", event: "capture_failed", fields: { kind: outcome.kind } });
|
|
57
59
|
ctx.ui.notify(outcome.message, "error");
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
catch {
|
|
63
|
+
logEvent({ source: "decision-capture", level: "error", event: "capture_failed", fields: { kind: "threw" } });
|
|
61
64
|
/* fail-soft: a capture failure never affects the tool call */
|
|
62
65
|
}
|
|
63
66
|
},
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { randomUUID } from "node:crypto";
|
|
21
21
|
import { friendlyFetchError, METERED_POST_FETCH_POLICY, resilientFetch } from "./resilientFetch.js";
|
|
22
22
|
import { sendOrSpool } from "./spool.js";
|
|
23
|
+
import { logEvent } from "./errorSink.js";
|
|
23
24
|
/**
|
|
24
25
|
* Bank a `cli_correction` decision durably. Every logical write carries a
|
|
25
26
|
* generated idempotencyKey; the backend dedups on it, so a spool replay or a
|
|
@@ -151,6 +152,7 @@ export function registerDecisionCommands(pi, opts) {
|
|
|
151
152
|
notify(notice.message, notice.type);
|
|
152
153
|
}
|
|
153
154
|
catch (err) {
|
|
155
|
+
logEvent({ source: "decisions", level: "error", event: "decide_failed", fields: { op: "decide" } });
|
|
154
156
|
notify(`Could not record the decision: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
155
157
|
}
|
|
156
158
|
},
|
|
@@ -183,6 +185,7 @@ export function registerDecisionCommands(pi, opts) {
|
|
|
183
185
|
notify(`Superseded decision ${shortId(id)}.`, "info");
|
|
184
186
|
}
|
|
185
187
|
catch (err) {
|
|
188
|
+
logEvent({ source: "decisions", level: "error", event: "supersede_failed", fields: { op: "supersede" } });
|
|
186
189
|
notify(`Could not supersede decision: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
187
190
|
}
|
|
188
191
|
return;
|
|
@@ -192,6 +195,7 @@ export function registerDecisionCommands(pi, opts) {
|
|
|
192
195
|
await pi.sendUserMessage(formatDecisionsList(items));
|
|
193
196
|
}
|
|
194
197
|
catch (err) {
|
|
198
|
+
logEvent({ source: "decisions", level: "error", event: "list_failed", fields: { op: "list" } });
|
|
195
199
|
notify(`Could not list decisions: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
196
200
|
}
|
|
197
201
|
},
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* A support flow can tail this file and POST it with the user's consent;
|
|
13
13
|
* nothing is uploaded automatically.
|
|
14
14
|
*/
|
|
15
|
+
import { readSessionTrail } from "./errorSink.js";
|
|
15
16
|
export declare function _setDiagnosticsHomeForTest(dir: string | null): void;
|
|
17
|
+
/** Active unified-sink path (one rotating per-day JSONL for all sources). */
|
|
16
18
|
export declare function diagnosticsLogPath(): string;
|
|
17
19
|
/** Layer A: verbose mode. `YAGNI_DEBUG=1` (or "true") turns on extra detail. */
|
|
18
20
|
export declare function isDebug(env?: NodeJS.ProcessEnv): boolean;
|
|
@@ -34,8 +36,37 @@ export interface ImagePasteEvent {
|
|
|
34
36
|
* prompt. `detail` is included only when YAGNI_DEBUG is on.
|
|
35
37
|
*/
|
|
36
38
|
export declare function logImagePaste(ev: ImagePasteEvent): void;
|
|
39
|
+
export interface AskQuestionEvent {
|
|
40
|
+
/** Stable event name: "key" | "finish" | "render" | "navigate" | "other_key". */
|
|
41
|
+
event: string;
|
|
42
|
+
/** Human-readable description; NEVER user-typed text or question content. */
|
|
43
|
+
detail?: string;
|
|
44
|
+
selectedIndex?: number;
|
|
45
|
+
otherIndex?: number;
|
|
46
|
+
otherLen?: number;
|
|
47
|
+
/** Raw key bytes, escaped (only for debug). */
|
|
48
|
+
key?: string;
|
|
49
|
+
/** Resolution status when the tool finishes: selected | other | cancelled. */
|
|
50
|
+
status?: string;
|
|
51
|
+
/** Whether the question is multiSelect at the time of the event. */
|
|
52
|
+
multi?: boolean;
|
|
53
|
+
/** 0-based index of the question within this tool call (multi-question runs). */
|
|
54
|
+
qIndex?: number;
|
|
55
|
+
/** Whether the "Other" row auto-check is on (multi-select). */
|
|
56
|
+
checked?: boolean;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Append one sanitized ask-user-question event to the unified sink
|
|
60
|
+
* (source:"ask-question").
|
|
61
|
+
* Fail-soft and hermetically gated under `node --test` exactly like
|
|
62
|
+
* `logImagePaste`. Never logs user-typed text, question text, or option labels
|
|
63
|
+
* — only indices, lengths, key bytes (escaped), and resolution status.
|
|
64
|
+
*/
|
|
65
|
+
export declare function logAskQuestion(ev: AskQuestionEvent): void;
|
|
37
66
|
/** Read the most recent log content (for a user-triggered report). */
|
|
38
67
|
export declare function readRecentDiagnostics(maxBytes?: number): string;
|
|
39
68
|
/** List existing diagnostic log files (active + rotations), for a report. */
|
|
40
69
|
export declare function listDiagnosticFiles(): string[];
|
|
70
|
+
/** Re-export the session-scoped trail reader for the /feedback flow. */
|
|
71
|
+
export { readSessionTrail };
|
|
41
72
|
//# sourceMappingURL=diagnostics.d.ts.map
|
|
@@ -12,22 +12,19 @@
|
|
|
12
12
|
* A support flow can tail this file and POST it with the user's consent;
|
|
13
13
|
* nothing is uploaded automatically.
|
|
14
14
|
*/
|
|
15
|
-
import {
|
|
15
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
16
16
|
import { dirname, join, basename } from "node:path";
|
|
17
|
-
import {
|
|
17
|
+
import { _setErrorSinkHomeForTest, logEvent, errorSinkPath, readSessionTrail, } from "./errorSink.js";
|
|
18
18
|
/** Test seam (mirrors `_setYagniCodeHomeForTest`): point the log at a tmpdir. */
|
|
19
19
|
let homeOverride = null;
|
|
20
20
|
export function _setDiagnosticsHomeForTest(dir) {
|
|
21
21
|
homeOverride = dir;
|
|
22
|
+
_setErrorSinkHomeForTest(dir);
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
-
return codeStateHome(homeOverride);
|
|
25
|
-
}
|
|
24
|
+
/** Active unified-sink path (one rotating per-day JSONL for all sources). */
|
|
26
25
|
export function diagnosticsLogPath() {
|
|
27
|
-
return
|
|
26
|
+
return errorSinkPath();
|
|
28
27
|
}
|
|
29
|
-
const MAX_LOG_BYTES = 256 * 1024; // rotate the active file past this
|
|
30
|
-
const KEEP_ROTATIONS = 2; // keep image-paste.log.1 and .2 alongside the active file
|
|
31
28
|
/** Layer A: verbose mode. `YAGNI_DEBUG=1` (or "true") turns on extra detail. */
|
|
32
29
|
export function isDebug(env = process.env) {
|
|
33
30
|
const v = env.YAGNI_DEBUG;
|
|
@@ -38,56 +35,55 @@ export function isDebug(env = process.env) {
|
|
|
38
35
|
* prompt. `detail` is included only when YAGNI_DEBUG is on.
|
|
39
36
|
*/
|
|
40
37
|
export function logImagePaste(ev) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
};
|
|
58
|
-
const path = diagnosticsLogPath();
|
|
59
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
60
|
-
rotateIfNeeded(path);
|
|
61
|
-
appendFileSync(path, JSON.stringify(line) + "\n", "utf8");
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
/* logging must never throw into the editor */
|
|
65
|
-
}
|
|
38
|
+
// The unified sink: content-free fields are always-on, the debug-only
|
|
39
|
+
// `detail` rides level:debug (never bound by /feedback).
|
|
40
|
+
const fields = {
|
|
41
|
+
...(ev.outcome !== undefined ? { outcome: ev.outcome } : {}),
|
|
42
|
+
...(ev.mimeType !== undefined ? { mimeType: ev.mimeType } : {}),
|
|
43
|
+
...(ev.bytes !== undefined ? { bytes: ev.bytes } : {}),
|
|
44
|
+
...(ev.imageCount !== undefined ? { imageCount: ev.imageCount } : {}),
|
|
45
|
+
...(ev.file !== undefined ? { file: basename(ev.file) } : {}),
|
|
46
|
+
...(ev.detail !== undefined && isDebug() ? { detail: ev.detail } : {}),
|
|
47
|
+
};
|
|
48
|
+
logEvent({
|
|
49
|
+
source: "image-paste",
|
|
50
|
+
level: ev.detail !== undefined && isDebug() ? "debug" : "info",
|
|
51
|
+
event: ev.event,
|
|
52
|
+
fields,
|
|
53
|
+
});
|
|
66
54
|
}
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Append one sanitized ask-user-question event to the unified sink
|
|
57
|
+
* (source:"ask-question").
|
|
58
|
+
* Fail-soft and hermetically gated under `node --test` exactly like
|
|
59
|
+
* `logImagePaste`. Never logs user-typed text, question text, or option labels
|
|
60
|
+
* — only indices, lengths, key bytes (escaped), and resolution status.
|
|
61
|
+
*/
|
|
62
|
+
export function logAskQuestion(ev) {
|
|
63
|
+
// Raw key bytes (`key`) and `detail` are content-ish: gate them behind DEBUG.
|
|
64
|
+
const debug = isDebug();
|
|
65
|
+
const fields = {
|
|
66
|
+
...(ev.selectedIndex !== undefined ? { selectedIndex: ev.selectedIndex } : {}),
|
|
67
|
+
...(ev.otherIndex !== undefined ? { otherIndex: ev.otherIndex } : {}),
|
|
68
|
+
...(ev.otherLen !== undefined ? { otherLen: ev.otherLen } : {}),
|
|
69
|
+
...(ev.status !== undefined ? { status: ev.status } : {}),
|
|
70
|
+
...(ev.multi !== undefined ? { multi: ev.multi } : {}),
|
|
71
|
+
...(ev.qIndex !== undefined ? { qIndex: ev.qIndex } : {}),
|
|
72
|
+
...(ev.checked !== undefined ? { checked: ev.checked } : {}),
|
|
73
|
+
...(ev.key !== undefined && debug ? { key: ev.key } : {}),
|
|
74
|
+
...(ev.detail !== undefined && debug ? { detail: ev.detail } : {}),
|
|
75
|
+
};
|
|
76
|
+
logEvent({
|
|
77
|
+
source: "ask-question",
|
|
78
|
+
level: debug ? "debug" : "info",
|
|
79
|
+
event: ev.event,
|
|
80
|
+
fields,
|
|
81
|
+
});
|
|
86
82
|
}
|
|
87
83
|
/** Read the most recent log content (for a user-triggered report). */
|
|
88
84
|
export function readRecentDiagnostics(maxBytes = 64 * 1024) {
|
|
89
85
|
try {
|
|
90
|
-
const data = readFileSync(
|
|
86
|
+
const data = readFileSync(errorSinkPath(), "utf8");
|
|
91
87
|
return data.length > maxBytes ? data.slice(data.length - maxBytes) : data;
|
|
92
88
|
}
|
|
93
89
|
catch {
|
|
@@ -97,9 +93,9 @@ export function readRecentDiagnostics(maxBytes = 64 * 1024) {
|
|
|
97
93
|
/** List existing diagnostic log files (active + rotations), for a report. */
|
|
98
94
|
export function listDiagnosticFiles() {
|
|
99
95
|
try {
|
|
100
|
-
const dir = dirname(
|
|
96
|
+
const dir = dirname(errorSinkPath());
|
|
101
97
|
return readdirSync(dir)
|
|
102
|
-
.filter((f) => f.startsWith("
|
|
98
|
+
.filter((f) => f.startsWith("errors-"))
|
|
103
99
|
.sort()
|
|
104
100
|
.map((f) => join(dir, f));
|
|
105
101
|
}
|
|
@@ -107,4 +103,6 @@ export function listDiagnosticFiles() {
|
|
|
107
103
|
return [];
|
|
108
104
|
}
|
|
109
105
|
}
|
|
106
|
+
/** Re-export the session-scoped trail reader for the /feedback flow. */
|
|
107
|
+
export { readSessionTrail };
|
|
110
108
|
//# sourceMappingURL=diagnostics.js.map
|