howtofixcli 0.7.8 → 0.7.9
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/chunk-27TRKD65.js +2 -0
- package/dist/chunk-3FYCFLNK.js +1 -0
- package/dist/chunk-56SEAGAQ.js +1 -0
- package/dist/chunk-63ZCQDXH.js +6 -0
- package/dist/chunk-6FUWMLWO.js +1 -0
- package/dist/chunk-7CHDNTH6.js +1 -0
- package/dist/chunk-ACOMNDUL.js +185 -0
- package/dist/chunk-AEVPET7X.js +2 -0
- package/dist/chunk-AGGIYXN7.js +2 -0
- package/dist/chunk-BHVBO4SC.js +1 -0
- package/dist/chunk-FPXVDP32.js +8 -0
- package/dist/chunk-GIUYIKH3.js +109 -0
- package/dist/chunk-GQCAJ56I.js +5 -0
- package/dist/chunk-GXB7RZHH.js +1 -0
- package/dist/chunk-IGEY3AWF.js +1 -0
- package/dist/chunk-ILDDDLRC.js +1 -0
- package/dist/chunk-JFYVRWJI.js +2 -0
- package/dist/chunk-MVLCGU3G.js +1 -0
- package/dist/chunk-N345KWWK.js +1 -0
- package/dist/chunk-NBQFPCRT.js +1 -0
- package/dist/chunk-OIPSLIZE.js +5 -0
- package/dist/chunk-ORJ2QZBG.js +3 -0
- package/dist/chunk-PCALBZOO.js +2 -0
- package/dist/chunk-PDNBGUYP.js +2 -0
- package/dist/chunk-S656RE4A.js +1 -0
- package/dist/chunk-TSEKPGK4.js +1 -0
- package/dist/chunk-U6CI6KXN.js +34 -0
- package/dist/chunk-UTYFLLSM.js +1 -0
- package/dist/chunk-V3R4FYTJ.js +1 -0
- package/dist/chunk-V656XVXV.js +8 -0
- package/dist/chunk-WKUJ7CFO.js +1 -0
- package/dist/chunk-WOT6VMZA.js +1 -0
- package/dist/chunk-WUPU6LTD.js +4 -0
- package/dist/chunk-WX3V5P7U.js +5 -0
- package/dist/chunk-ZKZ6WCI3.js +2 -0
- package/dist/main.js +94 -536
- package/package.json +15 -62
- package/dist/account-credential.js +0 -136
- package/dist/account-credential.test.js +0 -38
- package/dist/activity.js +0 -124
- package/dist/agent-defs.js +0 -237
- package/dist/agent-limiter.js +0 -68
- package/dist/agents.js +0 -554
- package/dist/anim.js +0 -361
- package/dist/args.js +0 -228
- package/dist/at-file.js +0 -28
- package/dist/brand-extension.js +0 -87
- package/dist/brand.js +0 -76
- package/dist/browser.js +0 -58
- package/dist/cache-report.js +0 -139
- package/dist/checkpoint.js +0 -437
- package/dist/claims.js +0 -167
- package/dist/cli-auth.js +0 -142
- package/dist/cli-checkin.js +0 -120
- package/dist/cli-credentials.js +0 -254
- package/dist/clipboard.js +0 -149
- package/dist/collapse.js +0 -129
- package/dist/commands.js +0 -301
- package/dist/compaction-guard.js +0 -81
- package/dist/context-extension.js +0 -17
- package/dist/dcp/commands.js +0 -172
- package/dist/dcp/compress-state.js +0 -139
- package/dist/dcp/compress.js +0 -329
- package/dist/dcp/dcp-extension.js +0 -288
- package/dist/dcp/dedup.js +0 -93
- package/dist/dcp/ids.js +0 -189
- package/dist/dcp/index.js +0 -25
- package/dist/dcp/prompts.js +0 -68
- package/dist/dcp/protected-content.js +0 -221
- package/dist/dcp/protected.js +0 -161
- package/dist/dcp/prune.js +0 -418
- package/dist/dcp/state.js +0 -176
- package/dist/device-id.js +0 -90
- package/dist/device-id.test.js +0 -23
- package/dist/errors.js +0 -63
- package/dist/extensions.js +0 -48
- package/dist/fatal.js +0 -35
- package/dist/feed.js +0 -498
- package/dist/fs-atomic.js +0 -54
- package/dist/gateway-errors.js +0 -90
- package/dist/git.js +0 -230
- package/dist/goal-extension.js +0 -88
- package/dist/goal.js +0 -222
- package/dist/hooks-extension.js +0 -176
- package/dist/hooks.js +0 -443
- package/dist/howtofix-account.js +0 -715
- package/dist/hud-line.js +0 -153
- package/dist/hwid.js +0 -130
- package/dist/interrupt.js +0 -60
- package/dist/json-events.js +0 -50
- package/dist/level-style.js +0 -112
- package/dist/loop-guard.js +0 -95
- package/dist/lsp-tools.js +0 -32
- package/dist/lsp.js +0 -347
- package/dist/mcp-config.js +0 -85
- package/dist/mcp-lifecycle.js +0 -147
- package/dist/model-catalog.js +0 -217
- package/dist/model-resolve.js +0 -65
- package/dist/notes.js +0 -78
- package/dist/oauth-login.js +0 -568
- package/dist/paths.js +0 -61
- package/dist/permissions.js +0 -298
- package/dist/plan-mode.js +0 -363
- package/dist/preset-command.js +0 -227
- package/dist/preset-extension.js +0 -35
- package/dist/preset-mcp.js +0 -292
- package/dist/presets.js +0 -727
- package/dist/prompt-files.js +0 -103
- package/dist/prompt-text.js +0 -41
- package/dist/proxy.js +0 -137
- package/dist/quality/ast.js +0 -196
- package/dist/quality/cognitive.js +0 -83
- package/dist/quality/cyclomatic.js +0 -30
- package/dist/quality/halstead.js +0 -83
- package/dist/quality/index.js +0 -18
- package/dist/quality/length.js +0 -3
- package/dist/quality/maintainability.js +0 -15
- package/dist/quality/nesting.js +0 -57
- package/dist/quality/report.js +0 -803
- package/dist/quality/smells.js +0 -67
- package/dist/quality-tool.js +0 -63
- package/dist/read-only-commands.js +0 -597
- package/dist/repo-map.js +0 -292
- package/dist/rtk.js +0 -193
- package/dist/sandbox.js +0 -105
- package/dist/schema.js +0 -214
- package/dist/session-lifecycle.js +0 -19
- package/dist/session-retention.js +0 -111
- package/dist/session.js +0 -446
- package/dist/sessions.js +0 -100
- package/dist/settings.js +0 -148
- package/dist/skills.js +0 -35
- package/dist/slop-tool.js +0 -96
- package/dist/spend-cap.js +0 -43
- package/dist/subagent.js +0 -171
- package/dist/tar.js +0 -146
- package/dist/theme.js +0 -79
- package/dist/thinking.js +0 -45
- package/dist/todo.js +0 -114
- package/dist/tool-loadout.js +0 -97
- package/dist/tool-render.js +0 -769
- package/dist/tools.js +0 -176
- package/dist/ui.js +0 -4757
- package/dist/update.js +0 -533
- package/dist/usage-segment.js +0 -69
- package/dist/web-tools.js +0 -46
- package/dist/web.js +0 -265
- package/dist/welcome.js +0 -179
- package/dist/workflow-engine.js +0 -246
- package/dist/workflow-progress.js +0 -446
- package/dist/workflow-view.js +0 -195
- package/dist/workflow.js +0 -349
package/dist/tool-render.js
DELETED
|
@@ -1,769 +0,0 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
2
|
-
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
-
import { formatElapsed } from "./workflow-progress.js";
|
|
4
|
-
/**
|
|
5
|
-
* Tool call/result rendering, in the shape Claude Code uses:
|
|
6
|
-
*
|
|
7
|
-
* ⏺ Read(src/ui.ts)
|
|
8
|
-
* ⎿ 120 lines
|
|
9
|
-
* ⏺ Edit(src/ui.ts)
|
|
10
|
-
* ⎿ +12 / -3
|
|
11
|
-
* ⏺ Bash(git status)
|
|
12
|
-
* ⎿ exit 0
|
|
13
|
-
*
|
|
14
|
-
* Everything here is pure text, so it can be exercised without a session, a
|
|
15
|
-
* model, or a terminal. `ui.ts` feeds it the SDK's tool events; `feed.ts` colours
|
|
16
|
-
* and indents the result.
|
|
17
|
-
*/
|
|
18
|
-
/**
|
|
19
|
-
* Marks a tool invocation. `●` (U+25CF) is deliberate: unlike `⏺` (U+23FA) it
|
|
20
|
-
* carries neither the Unicode Emoji property nor a coverage gap — all 12
|
|
21
|
-
* monospace fonts shipped on this box have it, while `⏺` is present in only one.
|
|
22
|
-
*/
|
|
23
|
-
export const TOOL_MARKER = "●";
|
|
24
|
-
/**
|
|
25
|
-
* Marks the result belonging to the invocation above it. `└` (U+2514) rather
|
|
26
|
-
* than Claude Code's `⎿` (U+23BF): the box-drawing elbow is present in DejaVu,
|
|
27
|
-
* Liberation and FreeMono, whereas `⎿` is missing from every monospace font
|
|
28
|
-
* here except FreeMono and renders as tofu almost everywhere.
|
|
29
|
-
*/
|
|
30
|
-
export const RESULT_MARKER = "└";
|
|
31
|
-
/**
|
|
32
|
-
* Marks the collapsed body of a result. `⋯` (U+22EF) rather than an ellipsis
|
|
33
|
-
* character alone: the row reads as "there is more here, press ctrl+e", and the
|
|
34
|
-
* midline dots sit inside the same monospace cell width as the other markers.
|
|
35
|
-
*/
|
|
36
|
-
export const EXPAND_MARKER = "⋯";
|
|
37
|
-
/**
|
|
38
|
-
* Braille spinner frames. Braille is outside the Unicode Emoji set, and this is
|
|
39
|
-
* the same frame set the `ora` package made ubiquitous, so any font that shows
|
|
40
|
-
* a CLI spinner at all has these.
|
|
41
|
-
*/
|
|
42
|
-
export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
43
|
-
/** The current spinner frame, derived from the clock so callers keep no state. */
|
|
44
|
-
export function spinnerFrame(intervalMs = 90) {
|
|
45
|
-
const index = Math.floor(Date.now() / intervalMs) % SPINNER_FRAMES.length;
|
|
46
|
-
return SPINNER_FRAMES[index] ?? SPINNER_FRAMES[0];
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Marks a call that built something, as opposed to one that read something.
|
|
50
|
-
*
|
|
51
|
-
* A chat turn and a build are not the same event, and until they were marked
|
|
52
|
-
* apart a session read as an undifferentiated wall of `● Mcp__robloxstudio__…`
|
|
53
|
-
* lines: the mesh that took a minute to generate looked exactly like the sixth
|
|
54
|
-
* `get_project_structure` of the same minute. Someone scrolling back to find
|
|
55
|
-
* "the boat I made" had nothing to scroll to.
|
|
56
|
-
*
|
|
57
|
-
* `▲` (U+25B2) rather than a colour, first: colour is what a terminal strips —
|
|
58
|
-
* through `NO_COLOR`, a pipe, or a log file — and the distinction has to survive
|
|
59
|
-
* that.
|
|
60
|
-
*
|
|
61
|
-
* And then rather than the obvious `▸` (U+25B8) or `◫` (U+25EB), which is the
|
|
62
|
-
* part measured rather than guessed: of the three monospace families installed
|
|
63
|
-
* here, both of those are present in only two, so on Liberation Mono and FreeMono
|
|
64
|
-
* they render as tofu — the same coverage gap the `⏺` note above exists to avoid.
|
|
65
|
-
* `▲` is present in all three, carries no Emoji property, and keeps the same
|
|
66
|
-
* single cell width as `●`.
|
|
67
|
-
*/
|
|
68
|
-
export const BUILD_MARKER = "▲";
|
|
69
|
-
/**
|
|
70
|
-
* Marks a build that did not finish. `!` rather than a glyph: a failure is the
|
|
71
|
-
* one line someone reads carefully, and an ASCII character cannot be missing
|
|
72
|
-
* from a font.
|
|
73
|
-
*/
|
|
74
|
-
export const BUILD_FAILED_MARKER = "!";
|
|
75
|
-
/**
|
|
76
|
-
* Marks a build that produced something a person can look at.
|
|
77
|
-
*
|
|
78
|
-
* `○` (U+25CB) for the same reason as the marker above — it is in all three
|
|
79
|
-
* families on this box, where the picture-like `◫` is in two. Kept apart from
|
|
80
|
-
* `BUILD_MARKER` because it says something different: not "this wrote to the
|
|
81
|
-
* place" but "there is a picture of what it wrote", which is the line worth
|
|
82
|
-
* stopping on when scrolling back.
|
|
83
|
-
*/
|
|
84
|
-
export const BUILD_IMAGE_MARKER = "○";
|
|
85
|
-
/**
|
|
86
|
-
* Tools whose completion produced or changed something, rather than reading it.
|
|
87
|
-
*
|
|
88
|
-
* The list is explicit rather than a pattern, for the reason the record's own
|
|
89
|
-
* list is: a rule based on the name would catch whatever the next server calls
|
|
90
|
-
* its read tool, and `get_project_structure` runs on nearly every turn. A build
|
|
91
|
-
* marker on those would be noise, and noise on the lines that matter is worse
|
|
92
|
-
* than no marker at all.
|
|
93
|
-
*/
|
|
94
|
-
const BUILDING_TOOLS = [
|
|
95
|
-
// The bridge's own writes.
|
|
96
|
-
"create_object",
|
|
97
|
-
"set_properties",
|
|
98
|
-
"delete_object",
|
|
99
|
-
"clone_instance",
|
|
100
|
-
"reparent_instance",
|
|
101
|
-
"set_script_source",
|
|
102
|
-
"edit_script_lines",
|
|
103
|
-
"insert_script_lines",
|
|
104
|
-
"execute_luau",
|
|
105
|
-
// Studio's generators, reached as `mcp__roblox-studio__<name>`.
|
|
106
|
-
"generate_mesh",
|
|
107
|
-
"generate_material",
|
|
108
|
-
"generate_procedural_model",
|
|
109
|
-
];
|
|
110
|
-
/**
|
|
111
|
-
* The bare tool name, with any `mcp__<server>__` prefix removed.
|
|
112
|
-
*
|
|
113
|
-
* The CLI sees both sources prefixed, so a marker that only worked for one of
|
|
114
|
-
* them would mark half the builds.
|
|
115
|
-
*/
|
|
116
|
-
export function bareToolName(toolName) {
|
|
117
|
-
return toolName.includes("__") ? toolName.slice(toolName.lastIndexOf("__") + 2) : toolName;
|
|
118
|
-
}
|
|
119
|
-
/** Whether this call built something, as opposed to reading it. */
|
|
120
|
-
export function isBuildTool(toolName) {
|
|
121
|
-
return BUILDING_TOOLS.includes(bareToolName(toolName));
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* The marker a finished call gets.
|
|
125
|
-
*
|
|
126
|
-
* An ordinary call keeps `●`, so a build stands out by being different rather
|
|
127
|
-
* than by everything else being marked. A failed build is marked apart from a
|
|
128
|
-
* failed read: the first says the place may have been left half-changed, which
|
|
129
|
-
* is the one failure worth going back to look at.
|
|
130
|
-
*/
|
|
131
|
-
export function resultMarker(toolName, isError) {
|
|
132
|
-
if (!isBuildTool(toolName))
|
|
133
|
-
return TOOL_MARKER;
|
|
134
|
-
return isError ? BUILD_FAILED_MARKER : BUILD_MARKER;
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* The shortest path that still identifies the file.
|
|
138
|
-
*
|
|
139
|
-
* A model passes an absolute path as readily as a relative one - measured, not
|
|
140
|
-
* assumed: asked to read `src/theme.ts` by absolute path, it did - and printing
|
|
141
|
-
* `/Users/…/HowToFix-CLI/src/theme.ts` spends 40 characters of the line saying
|
|
142
|
-
* what `src/theme.ts` says in 12. Claude Code's `getDisplayPath` applies the
|
|
143
|
-
* same three rules, in the same order:
|
|
144
|
-
*
|
|
145
|
-
* 1. relative to the working directory, when that does not escape it
|
|
146
|
-
* 2. `~`-relative for a path under home
|
|
147
|
-
* 3. absolute, when neither applies
|
|
148
|
-
*
|
|
149
|
-
* This is a DISPLAY transform only. The tool receives the path the model sent;
|
|
150
|
-
* nothing here rewrites an argument.
|
|
151
|
-
*/
|
|
152
|
-
export function displayPath(filePath, cwd = process.cwd()) {
|
|
153
|
-
if (!filePath)
|
|
154
|
-
return filePath;
|
|
155
|
-
if (!isAbsolute(filePath))
|
|
156
|
-
return filePath;
|
|
157
|
-
// Normalise first. Both problems with the earlier versions came from a path
|
|
158
|
-
// that still held a `..` when it was measured: `relative()` then produced a
|
|
159
|
-
// `..`-prefixed string that lost to the absolute form, and the `~` branch
|
|
160
|
-
// produced `~/…/HowToFix-CLI/../elsewhere/x.ts`, which is longer than what it
|
|
161
|
-
// replaced. Resolving once makes both checks answer about the real location.
|
|
162
|
-
const resolved = resolve(filePath);
|
|
163
|
-
const rel = relative(cwd, resolved);
|
|
164
|
-
// The working directory itself has no relative form - `relative` returns `""`
|
|
165
|
-
// - and falling through to the home rule printed `~/Kuliah/iseng/HowToFix-CLI`
|
|
166
|
-
// for a path whose whole identity is "here". `.` is what it is.
|
|
167
|
-
if (rel === "")
|
|
168
|
-
return ".";
|
|
169
|
-
// Rule 1: inside the working directory. `..` only appears when it is not.
|
|
170
|
-
if (rel && !rel.startsWith(`..${sep}`) && rel !== "..")
|
|
171
|
-
return rel;
|
|
172
|
-
// Rule 2: under home. Checked after the working directory, so a repository in
|
|
173
|
-
// home shows `src/theme.ts` rather than `~/Kuliah/iseng/HowToFix-CLI/src/…`.
|
|
174
|
-
const home = homedir();
|
|
175
|
-
if (home && (resolved === home || resolved.startsWith(home + sep))) {
|
|
176
|
-
return `~${resolved.slice(home.length)}`;
|
|
177
|
-
}
|
|
178
|
-
return resolved;
|
|
179
|
-
}
|
|
180
|
-
/** Collapse whitespace and clip to a budget, for one-line summaries. */
|
|
181
|
-
export function clip(text, max) {
|
|
182
|
-
const one = text.replace(/\s+/g, " ").trim();
|
|
183
|
-
return one.length > max ? `${one.slice(0, Math.max(1, max - 1))}…` : one;
|
|
184
|
-
}
|
|
185
|
-
function field(args, key) {
|
|
186
|
-
if (args && typeof args === "object") {
|
|
187
|
-
const value = args[key];
|
|
188
|
-
if (typeof value === "string" && value.length > 0)
|
|
189
|
-
return value;
|
|
190
|
-
if (typeof value === "number")
|
|
191
|
-
return String(value);
|
|
192
|
-
}
|
|
193
|
-
return undefined;
|
|
194
|
-
}
|
|
195
|
-
function argsSummary(args, max = 160) {
|
|
196
|
-
if (!args || typeof args !== "object")
|
|
197
|
-
return "";
|
|
198
|
-
try {
|
|
199
|
-
const text = JSON.stringify(args);
|
|
200
|
-
return text && text !== "{}" ? clip(text, max) : "";
|
|
201
|
-
}
|
|
202
|
-
catch {
|
|
203
|
-
return "";
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* The text between `⏺ Name(` and `)`. Kept separate from the formatting so the
|
|
208
|
-
* target can be reused (for example by a future expanded view).
|
|
209
|
-
*/
|
|
210
|
-
export function toolTarget(toolName, args) {
|
|
211
|
-
switch (toolName) {
|
|
212
|
-
case "read": {
|
|
213
|
-
const path = displayPath(field(args, "path") ?? "");
|
|
214
|
-
const range = [field(args, "offset"), field(args, "limit")].filter(Boolean);
|
|
215
|
-
return range.length > 0 ? `${path} ${range.length === 1 ? `offset=${range[0]}` : `offset=${range[0]}, limit=${range[1]}`}` : path;
|
|
216
|
-
}
|
|
217
|
-
case "edit":
|
|
218
|
-
case "write":
|
|
219
|
-
return displayPath(field(args, "path") ?? "");
|
|
220
|
-
case "bash":
|
|
221
|
-
case "powershell": {
|
|
222
|
-
const command = field(args, "command") ?? "";
|
|
223
|
-
// A background command is the one thing about it a reader has to know: the
|
|
224
|
-
// call returns immediately and the work keeps going.
|
|
225
|
-
const background = args?.run_in_background === true;
|
|
226
|
-
return background ? `${command} [background]` : command;
|
|
227
|
-
}
|
|
228
|
-
case "bash_output": {
|
|
229
|
-
const id = field(args, "task_id");
|
|
230
|
-
const filter = field(args, "filter");
|
|
231
|
-
if (!id)
|
|
232
|
-
return "all tasks";
|
|
233
|
-
return filter ? `${id} filter=${filter}` : id;
|
|
234
|
-
}
|
|
235
|
-
case "kill_shell":
|
|
236
|
-
return field(args, "task_id") ?? "";
|
|
237
|
-
case "compress": {
|
|
238
|
-
// `content` is an array of ranges; the topic alone does not say how much
|
|
239
|
-
// is being collapsed, and the range count is the useful second fact.
|
|
240
|
-
const topic = field(args, "topic") ?? "";
|
|
241
|
-
const ranges = Array.isArray(args.content) ? args.content.length : 0;
|
|
242
|
-
return ranges > 1 ? `${topic} (${ranges} ranges)` : topic;
|
|
243
|
-
}
|
|
244
|
-
case "grep": {
|
|
245
|
-
const pattern = field(args, "pattern") ?? "";
|
|
246
|
-
const where = field(args, "path");
|
|
247
|
-
return where ? `${pattern} in ${displayPath(where)}` : pattern;
|
|
248
|
-
}
|
|
249
|
-
case "find":
|
|
250
|
-
case "glob": {
|
|
251
|
-
const pattern = field(args, "pattern") ?? field(args, "glob") ?? "";
|
|
252
|
-
const where = field(args, "path");
|
|
253
|
-
return where ? `${pattern} in ${displayPath(where)}` : pattern;
|
|
254
|
-
}
|
|
255
|
-
case "ls":
|
|
256
|
-
// `ls` renders a path like the others; it was the one left absolute, so
|
|
257
|
-
// the tool that lists a directory printed the longest form of its name.
|
|
258
|
-
return displayPath(field(args, "path") ?? ".");
|
|
259
|
-
case "task":
|
|
260
|
-
return field(args, "agent") ?? "task";
|
|
261
|
-
case "ask":
|
|
262
|
-
return field(args, "question") ?? "";
|
|
263
|
-
case "web_search":
|
|
264
|
-
return field(args, "query") ?? "";
|
|
265
|
-
case "web_fetch":
|
|
266
|
-
return field(args, "url") ?? "";
|
|
267
|
-
case "lsp": {
|
|
268
|
-
const action = field(args, "action") ?? "lsp";
|
|
269
|
-
const file = field(args, "file") ?? "";
|
|
270
|
-
const line = field(args, "line");
|
|
271
|
-
return line ? `${action} ${file}:${line}` : `${action} ${file}`;
|
|
272
|
-
}
|
|
273
|
-
case "quality":
|
|
274
|
-
return field(args, "path") ?? "changed files";
|
|
275
|
-
case "think":
|
|
276
|
-
return "private";
|
|
277
|
-
case "todo": {
|
|
278
|
-
const op = field(args, "op") ?? "view";
|
|
279
|
-
const task = field(args, "task");
|
|
280
|
-
const items = args?.items;
|
|
281
|
-
if (op === "init" && Array.isArray(items))
|
|
282
|
-
return `init ${items.length} task${items.length === 1 ? "" : "s"}`;
|
|
283
|
-
return task ? `${op} ${task}` : op;
|
|
284
|
-
}
|
|
285
|
-
case "context": {
|
|
286
|
-
const op = field(args, "op") ?? "list";
|
|
287
|
-
const key = field(args, "key");
|
|
288
|
-
return key ? `${op} ${key}` : op;
|
|
289
|
-
}
|
|
290
|
-
case "workflow": {
|
|
291
|
-
const path = field(args, "path");
|
|
292
|
-
if (path)
|
|
293
|
-
return path;
|
|
294
|
-
const code = field(args, "code") ?? "";
|
|
295
|
-
// Show the script's own name when it declares one; it is the useful handle.
|
|
296
|
-
const name = /export\s+const\s+meta\s*=\s*\{[\s\S]*?name\s*:\s*["'`]([^"'`]+)/.exec(code)?.[1];
|
|
297
|
-
if (name)
|
|
298
|
-
return name;
|
|
299
|
-
const first = code.trim().split("\n")[0] ?? "";
|
|
300
|
-
return first.length > 40 ? `${first.slice(0, 40)}…` : first;
|
|
301
|
-
}
|
|
302
|
-
default:
|
|
303
|
-
return argsSummary(args);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
/** `⏺ Read(src/ui.ts)` — the invocation line. */
|
|
307
|
-
export function formatToolCall(toolName, args, max = 100) {
|
|
308
|
-
// An MCP name arrives as `mcp__roblox-studio__generate_mesh`, and title-casing
|
|
309
|
-
// that produced `Mcp__roblox-studio__generate_mesh` — the server's name twice,
|
|
310
|
-
// in the one line meant to be scanned. The bare name is what the person
|
|
311
|
-
// recognises, and it is also what decides whether this is a build.
|
|
312
|
-
const bare = bareToolName(toolName);
|
|
313
|
-
const isMcp = bare !== toolName;
|
|
314
|
-
const label = isMcp
|
|
315
|
-
? bare
|
|
316
|
-
: bare.charAt(0).toUpperCase() + bare.slice(1);
|
|
317
|
-
const marker = isBuildTool(toolName) ? BUILD_MARKER : TOOL_MARKER;
|
|
318
|
-
const target = clip(toolTarget(bare, args), max);
|
|
319
|
-
return target ? `${marker} ${label}(${target})` : `${marker} ${label}`;
|
|
320
|
-
}
|
|
321
|
-
/**
|
|
322
|
-
* Counts `+`/`-` lines in a unified diff, skipping the `+++`/`---` file headers.
|
|
323
|
-
*
|
|
324
|
-
* It must be a unified diff: a caller that passes `+12 -3` (a stat, not a diff)
|
|
325
|
-
* gets zero from both counters, because no line starts with a single marker.
|
|
326
|
-
* That is not a bug in the counter but it is a silent wrong answer, and it cost
|
|
327
|
-
* a debugging round when a fixture used the wrong shape.
|
|
328
|
-
*/
|
|
329
|
-
export function diffStat(diff) {
|
|
330
|
-
let additions = 0;
|
|
331
|
-
let removals = 0;
|
|
332
|
-
for (const line of diff.split("\n")) {
|
|
333
|
-
if (line.startsWith("+") && !line.startsWith("+++"))
|
|
334
|
-
additions += 1;
|
|
335
|
-
else if (line.startsWith("-") && !line.startsWith("---"))
|
|
336
|
-
removals += 1;
|
|
337
|
-
}
|
|
338
|
-
return { additions, removals };
|
|
339
|
-
}
|
|
340
|
-
function resultText(result) {
|
|
341
|
-
const content = result?.content;
|
|
342
|
-
if (typeof content === "string")
|
|
343
|
-
return content;
|
|
344
|
-
if (!Array.isArray(content))
|
|
345
|
-
return "";
|
|
346
|
-
return content.map((part) => (part && typeof part === "object" && part.type === "text" ? String(part.text ?? "") : "")).join("");
|
|
347
|
-
}
|
|
348
|
-
/**
|
|
349
|
-
* The `details` bag of a tool result, or an empty object.
|
|
350
|
-
*
|
|
351
|
-
* Exported because the workbench needs the same shape to build a live panel from
|
|
352
|
-
* a partial result, and two readings of the same field would drift.
|
|
353
|
-
*/
|
|
354
|
-
export function detailsOf(result) {
|
|
355
|
-
const details = result?.details;
|
|
356
|
-
return details && typeof details === "object" ? details : {};
|
|
357
|
-
}
|
|
358
|
-
/**
|
|
359
|
-
* `structuredContent` is where the SDK puts machine-readable tool output. For
|
|
360
|
-
* bash that is `{ output, truncated, exit_code, wall_time_seconds }` — `details`
|
|
361
|
-
* is undefined for shell tools, so this is the only reliable exit code.
|
|
362
|
-
*/
|
|
363
|
-
function structuredOf(result) {
|
|
364
|
-
const structured = result?.structuredContent;
|
|
365
|
-
return structured && typeof structured === "object" ? structured : {};
|
|
366
|
-
}
|
|
367
|
-
function exitCodeOf(structured) {
|
|
368
|
-
const code = structured.exit_code;
|
|
369
|
-
return typeof code === "number" && Number.isFinite(code) ? code : undefined;
|
|
370
|
-
}
|
|
371
|
-
/**
|
|
372
|
-
* What a build produced, as one line, or nothing when the result does not say.
|
|
373
|
-
*
|
|
374
|
-
* Both real shapes are read because both are real: the bridge answers with an
|
|
375
|
-
* object, and Studio answers with content blocks whose text is JSON. Reading
|
|
376
|
-
* only the top level would leave every generator result blank — the half where
|
|
377
|
-
* the line matters most, since a generated mesh is the slow thing worth seeing
|
|
378
|
-
* the outcome of.
|
|
379
|
-
*
|
|
380
|
-
* Nothing is invented. An absent count stays absent, because `0 instances` and
|
|
381
|
-
* "nobody counted" are different facts and only one of them is a problem. A
|
|
382
|
-
* result that says nothing recognisable returns nothing, and the caller falls
|
|
383
|
-
* back to the ordinary summary rather than to a guess.
|
|
384
|
-
*/
|
|
385
|
-
function buildSummary(result) {
|
|
386
|
-
const candidates = [];
|
|
387
|
-
if (result && typeof result === "object") {
|
|
388
|
-
candidates.push(result);
|
|
389
|
-
const content = result.content;
|
|
390
|
-
if (Array.isArray(content)) {
|
|
391
|
-
for (const part of content) {
|
|
392
|
-
const text = part?.text;
|
|
393
|
-
if (typeof text !== "string")
|
|
394
|
-
continue;
|
|
395
|
-
try {
|
|
396
|
-
const parsed = JSON.parse(text);
|
|
397
|
-
if (parsed && typeof parsed === "object")
|
|
398
|
-
candidates.push(parsed);
|
|
399
|
-
}
|
|
400
|
-
catch {
|
|
401
|
-
// A plain sentence from Studio carries no counts. The call still
|
|
402
|
-
// happened; it just has nothing to add here.
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
const parts = [];
|
|
408
|
-
for (const candidate of candidates) {
|
|
409
|
-
const instances = ["instances", "instanceCount", "count", "parts", "partsCreated"]
|
|
410
|
-
.map((key) => candidate[key])
|
|
411
|
-
.find((value) => typeof value === "number" && Number.isFinite(value));
|
|
412
|
-
// "1 instances" reads as machine output, and this line is shown to a person.
|
|
413
|
-
if (typeof instances === "number") {
|
|
414
|
-
parts.push(`${instances} instance${instances === 1 ? "" : "s"}`);
|
|
415
|
-
}
|
|
416
|
-
const files = firstStringList(candidate, ["files", "written", "paths"]);
|
|
417
|
-
if (files)
|
|
418
|
-
parts.push(files.join(", "));
|
|
419
|
-
const images = firstStringList(candidate, ["images", "renders", "screenshots"]);
|
|
420
|
-
// The marker says a picture exists; the name says which. Both, because a
|
|
421
|
-
// marker with no name leaves nothing to open. The word "renders" is spelled
|
|
422
|
-
// out beside it so the glyph is never the only thing carrying the meaning.
|
|
423
|
-
if (images)
|
|
424
|
-
parts.push(`renders ${BUILD_IMAGE_MARKER} ${images.join(", ")}`);
|
|
425
|
-
const findings = firstStringList(candidate, ["findings", "warnings", "errors"]);
|
|
426
|
-
// The count only, here: the feed is a summary line, and the findings
|
|
427
|
-
// themselves are long enough to bury it. They are what a check reports in
|
|
428
|
-
// full elsewhere; this line's job is to say there is something to look at.
|
|
429
|
-
if (findings)
|
|
430
|
-
parts.push(`${findings.length} finding${findings.length === 1 ? "" : "s"}`);
|
|
431
|
-
if (parts.length > 0)
|
|
432
|
-
break;
|
|
433
|
-
}
|
|
434
|
-
return parts.length > 0 ? parts.join(" · ") : undefined;
|
|
435
|
-
}
|
|
436
|
-
function firstStringList(record, keys) {
|
|
437
|
-
for (const key of keys) {
|
|
438
|
-
const value = record[key];
|
|
439
|
-
if (!Array.isArray(value))
|
|
440
|
-
continue;
|
|
441
|
-
const strings = value.filter((entry) => typeof entry === "string" && entry !== "");
|
|
442
|
-
if (strings.length > 0)
|
|
443
|
-
return strings;
|
|
444
|
-
}
|
|
445
|
-
return undefined;
|
|
446
|
-
}
|
|
447
|
-
function firstLine(text) {
|
|
448
|
-
return (text
|
|
449
|
-
.trim()
|
|
450
|
-
.split("\n")
|
|
451
|
-
.find((line) => line.trim()) ?? "");
|
|
452
|
-
}
|
|
453
|
-
/** ` (N lines)` for a shell call, preferring the structured output over the text. */
|
|
454
|
-
function shellOutputLines(structured, text) {
|
|
455
|
-
const output = typeof structured.output === "string" ? structured.output : text.replace(/\n*Command exited with code \d+\s*$/, "");
|
|
456
|
-
const lines = lineCount(output);
|
|
457
|
-
return lines > 0 ? ` (${lines} line${lines === 1 ? "" : "s"})` : "";
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* `1 line` / `2 lines`.
|
|
461
|
-
*
|
|
462
|
-
* Every count label in this file needs it, and the one that was written inline
|
|
463
|
-
* (`write`) was the one that got it wrong. Claude Code reaches the same
|
|
464
|
-
* conclusion from the other direction: its collapse summary carries the
|
|
465
|
-
* singular/plural branch six times, once per category.
|
|
466
|
-
*/
|
|
467
|
-
function plural(count, one, many) {
|
|
468
|
-
return count === 1 ? one : many;
|
|
469
|
-
}
|
|
470
|
-
function lineCount(text) {
|
|
471
|
-
const trimmed = text.trim();
|
|
472
|
-
return trimmed ? trimmed.split("\n").length : 0;
|
|
473
|
-
}
|
|
474
|
-
/**
|
|
475
|
-
* The summary after `⎿`. This is the line that makes a tool call readable at a
|
|
476
|
-
* glance: line counts for reads, a diff stat for edits, an exit code for shell.
|
|
477
|
-
*/
|
|
478
|
-
/** Token counts are read at a glance, so thousands collapse to one decimal. */
|
|
479
|
-
function formatTokens(count) {
|
|
480
|
-
return count < 1000 ? `${count} tokens` : `${(count / 1000).toFixed(1)}k tokens`;
|
|
481
|
-
}
|
|
482
|
-
export function formatToolResult(toolName, result, isError) {
|
|
483
|
-
const text = resultText(result);
|
|
484
|
-
const details = detailsOf(result);
|
|
485
|
-
const structured = structuredOf(result);
|
|
486
|
-
// A failed call reports its own reason, which beats any generic summary. The
|
|
487
|
-
// SDK wraps a nonzero exit as "<output>\n\n\nCommand exited with code N"; the
|
|
488
|
-
// code itself is more useful as a summary, so prefer it when present.
|
|
489
|
-
if (isError) {
|
|
490
|
-
const code = exitCodeOf(structured);
|
|
491
|
-
if (code !== undefined && (toolName === "bash" || toolName === "powershell"))
|
|
492
|
-
return `exit ${code}`;
|
|
493
|
-
const first = text
|
|
494
|
-
.trim()
|
|
495
|
-
.split("\n")
|
|
496
|
-
.find((line) => line.trim());
|
|
497
|
-
return clip(first ?? "failed", 120);
|
|
498
|
-
}
|
|
499
|
-
// A build reports what it produced, before any generic handling. "Done" on a
|
|
500
|
-
// mesh that produced eight instances says nothing a person can use, and the
|
|
501
|
-
// whole reason a build is marked apart is so the line can be read for what it
|
|
502
|
-
// made.
|
|
503
|
-
if (isBuildTool(toolName)) {
|
|
504
|
-
const summary = buildSummary(result);
|
|
505
|
-
if (summary)
|
|
506
|
-
return summary;
|
|
507
|
-
// A result with no counts still said something happened to something. The
|
|
508
|
-
// tool's own name is the honest summary — `set_properties` on a part is what
|
|
509
|
-
// occurred — where a generic "done" would spend the line saying nothing.
|
|
510
|
-
const bare = bareToolName(toolName);
|
|
511
|
-
if (bare === "execute_luau")
|
|
512
|
-
return "ran";
|
|
513
|
-
return clip(bare.replace(/_/g, " "), 120);
|
|
514
|
-
}
|
|
515
|
-
switch (toolName) {
|
|
516
|
-
case "task": {
|
|
517
|
-
// The first line of the result is the child's own summary
|
|
518
|
-
// (`## scout#id done (17 tool uses · 56.6k · 1m 34.3s)`), so the feed shows
|
|
519
|
-
// that rather than the whole report: what the child produced belongs to the
|
|
520
|
-
// model, and the reader needs the shape of the work. Several children come
|
|
521
|
-
// back as one summary per line.
|
|
522
|
-
const heads = text
|
|
523
|
-
.split("\n")
|
|
524
|
-
.filter((line) => line.startsWith("## "))
|
|
525
|
-
.map((line) => line.replace(/^##\s*/, "").trim());
|
|
526
|
-
if (heads.length === 0) {
|
|
527
|
-
const first = text
|
|
528
|
-
.trim()
|
|
529
|
-
.split("\n")
|
|
530
|
-
.find((line) => line.trim());
|
|
531
|
-
return clip(first ?? "done", 120);
|
|
532
|
-
}
|
|
533
|
-
return heads.map((head) => clip(head, 120)).join("\n");
|
|
534
|
-
}
|
|
535
|
-
case "read": {
|
|
536
|
-
const truncation = details.truncation;
|
|
537
|
-
// The truncation notice is appended to the content, so it must not count
|
|
538
|
-
// towards the line total.
|
|
539
|
-
const lines = lineCount(text.replace(/\n*\[[^\]]*\]\s*$/, ""));
|
|
540
|
-
const base = lines === 0 ? "empty" : `${lines} line${lines === 1 ? "" : "s"}`;
|
|
541
|
-
return truncation?.truncated ? `${base} (truncated from ${truncation.totalLines ?? "?"})` : base;
|
|
542
|
-
}
|
|
543
|
-
case "compress": {
|
|
544
|
-
// The number the user otherwise has to type /dcp status to see. Without
|
|
545
|
-
// it a compression is invisible: it changes what the provider is sent and
|
|
546
|
-
// nothing on screen, so there is no way to tell it helped.
|
|
547
|
-
const blockIds = Array.isArray(details.blockIds) ? details.blockIds : [];
|
|
548
|
-
const replaced = typeof details.compressedTokens === "number" ? details.compressedTokens : 0;
|
|
549
|
-
const summaryTokens = typeof details.summaryTokens === "number" ? details.summaryTokens : 0;
|
|
550
|
-
const label = blockIds.length === 1 ? `b${String(blockIds[0])}` : `${blockIds.length} blocks`;
|
|
551
|
-
if (replaced === 0)
|
|
552
|
-
return label;
|
|
553
|
-
// A summary can be LARGER than what it replaced - a model that copies the
|
|
554
|
-
// file contents into it does exactly that. Clamping at zero printed
|
|
555
|
-
// "0 tokens saved" for a compression that added 161 tokens, which reads as
|
|
556
|
-
// a no-op rather than as the regression it is. The sign is the whole
|
|
557
|
-
// message here.
|
|
558
|
-
const delta = replaced - summaryTokens;
|
|
559
|
-
if (delta < 0)
|
|
560
|
-
return `${label} · ${formatTokens(-delta)} LARGER than the range`;
|
|
561
|
-
return `${label} · ${formatTokens(delta)} saved`;
|
|
562
|
-
}
|
|
563
|
-
case "edit": {
|
|
564
|
-
const diff = typeof details.diff === "string" ? details.diff : "";
|
|
565
|
-
if (!diff)
|
|
566
|
-
return "applied";
|
|
567
|
-
const { additions, removals } = diffStat(diff);
|
|
568
|
-
return `+${additions} / -${removals}`;
|
|
569
|
-
}
|
|
570
|
-
case "write": {
|
|
571
|
-
const lines = lineCount(text);
|
|
572
|
-
if (lines === 0)
|
|
573
|
-
return "written";
|
|
574
|
-
// `written (1 lines)` was the only label in this file that did not agree
|
|
575
|
-
// with its own count. `read`, `grep` and `web_search` all handle the
|
|
576
|
-
// singular; this one did not, because the plural was written inline here
|
|
577
|
-
// rather than through the helper below.
|
|
578
|
-
return `written (${lines} ${plural(lines, "line", "lines")})`;
|
|
579
|
-
}
|
|
580
|
-
case "bash":
|
|
581
|
-
case "powershell": {
|
|
582
|
-
// Ground truth (dist/core/tools/bash.js): `details` is always undefined and
|
|
583
|
-
// the exit code lives in `structuredContent.exit_code`. The earlier version
|
|
584
|
-
// of this formatter matched /exit code:/ against the content, which never
|
|
585
|
-
// matches the SDK's "Command exited with code N" wording, so every shell
|
|
586
|
-
// call summarised as a bare "done".
|
|
587
|
-
const code = exitCodeOf(structured);
|
|
588
|
-
if (code === undefined)
|
|
589
|
-
return text.trim() ? clip(firstLine(text), 120) : "done";
|
|
590
|
-
const suffix = shellOutputLines(structured, text);
|
|
591
|
-
return code === 0 ? `exit 0${suffix}` : `exit ${code}`;
|
|
592
|
-
}
|
|
593
|
-
case "grep": {
|
|
594
|
-
const lines = lineCount(text);
|
|
595
|
-
return lines === 0 ? "no matches" : `${lines} match${lines === 1 ? "" : "es"}`;
|
|
596
|
-
}
|
|
597
|
-
case "web_search": {
|
|
598
|
-
const count = typeof details.count === "number" ? details.count : lineCount(text);
|
|
599
|
-
return count === 0 ? "no results" : `${count} result${count === 1 ? "" : "s"}`;
|
|
600
|
-
}
|
|
601
|
-
case "web_fetch": {
|
|
602
|
-
const chars = typeof details.chars === "number" ? details.chars : text.length;
|
|
603
|
-
return `${chars} chars`;
|
|
604
|
-
}
|
|
605
|
-
case "lsp": {
|
|
606
|
-
const count = typeof details.count === "number" ? details.count : undefined;
|
|
607
|
-
if (typeof details.error === "string")
|
|
608
|
-
return clip(details.error, 120);
|
|
609
|
-
return count === undefined ? clip(firstLine(text), 120) : `${count} location${count === 1 ? "" : "s"}`;
|
|
610
|
-
}
|
|
611
|
-
case "quality": {
|
|
612
|
-
if (typeof details.error === "string")
|
|
613
|
-
return clip(details.error, 120);
|
|
614
|
-
if (details.off === true)
|
|
615
|
-
return "off";
|
|
616
|
-
const files = typeof details.files === "number" ? details.files : 0;
|
|
617
|
-
const functions = typeof details.functions === "number" ? details.functions : 0;
|
|
618
|
-
const findings = typeof details.findings === "number" ? details.findings : 0;
|
|
619
|
-
const score = typeof details.score === "number" ? ` · ${Math.round(details.score)}` : "";
|
|
620
|
-
if (files === 0)
|
|
621
|
-
return "no files";
|
|
622
|
-
return `${files} file${files === 1 ? "" : "s"} · ${functions} fn · ${findings} finding${findings === 1 ? "" : "s"}${score}`;
|
|
623
|
-
}
|
|
624
|
-
case "todo": {
|
|
625
|
-
const todos = (details.todos ?? []);
|
|
626
|
-
if (!Array.isArray(todos) || todos.length === 0)
|
|
627
|
-
return "list cleared";
|
|
628
|
-
const done = todos.filter((item) => item.status === "completed").length;
|
|
629
|
-
const open = todos.filter((item) => item.status === "pending" || item.status === "in_progress").length;
|
|
630
|
-
return `${done}/${todos.length} done, ${open} open`;
|
|
631
|
-
}
|
|
632
|
-
case "context": {
|
|
633
|
-
const notes = details.notes;
|
|
634
|
-
const count = notes ? Object.keys(notes).length : 0;
|
|
635
|
-
return count === 0 ? "no notes" : `${count} note${count === 1 ? "" : "s"} held`;
|
|
636
|
-
}
|
|
637
|
-
case "workflow": {
|
|
638
|
-
const agents = typeof details.agents === "number" ? details.agents : 0;
|
|
639
|
-
const tokens = typeof details.outputTokens === "number" ? details.outputTokens : 0;
|
|
640
|
-
const failed = Array.isArray(details.steps) ? details.steps.filter((step) => step.status === "failed").length : 0;
|
|
641
|
-
if (typeof details.error === "string")
|
|
642
|
-
return `failed after ${agents} agent(s)`;
|
|
643
|
-
const elapsed = typeof details.startedAt === "number" && typeof details.endedAt === "number" ? `, ${formatElapsed(details.endedAt - details.startedAt)}` : "";
|
|
644
|
-
return `${agents} agent(s), ${tokens} tokens${elapsed}${failed > 0 ? `, ${failed} failed` : ""}`;
|
|
645
|
-
}
|
|
646
|
-
case "bash_output": {
|
|
647
|
-
// The first line the tool writes is the task and its state; the rest is the
|
|
648
|
-
// task's own output, which the summary should not repeat.
|
|
649
|
-
const head = text.trim().split("\n")[0] ?? "";
|
|
650
|
-
const state = /running/.test(head) ? "running" : /finished/.test(head) ? "finished" : "task";
|
|
651
|
-
const body = text
|
|
652
|
-
.trim()
|
|
653
|
-
.split("\n")
|
|
654
|
-
.slice(2)
|
|
655
|
-
.filter((line) => line.trim()).length;
|
|
656
|
-
return body === 0 ? state : `${state} · ${body} line${body === 1 ? "" : "s"}`;
|
|
657
|
-
}
|
|
658
|
-
case "kill_shell": {
|
|
659
|
-
const first = text.trim().split("\n")[0] ?? "";
|
|
660
|
-
return first.startsWith("Stopped") ? "stopped" : clip(first, 120);
|
|
661
|
-
}
|
|
662
|
-
default: {
|
|
663
|
-
const first = text
|
|
664
|
-
.trim()
|
|
665
|
-
.split("\n")
|
|
666
|
-
.find((line) => line.trim());
|
|
667
|
-
return first ? clip(first, 120) : "done";
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
/**
|
|
672
|
-
* Body lines under a result. Deliberately narrow: the summary line already
|
|
673
|
-
* carries the gist, so only a diff (edits) or a few output lines (shell) are
|
|
674
|
-
* worth repeating. Everything else stays collapsed until there is an expand
|
|
675
|
-
* toggle.
|
|
676
|
-
*/
|
|
677
|
-
export function toolResultDetail(toolName, result) {
|
|
678
|
-
const details = detailsOf(result);
|
|
679
|
-
if (toolName === "edit" && typeof details.diff === "string") {
|
|
680
|
-
return details.diff
|
|
681
|
-
.split("\n")
|
|
682
|
-
.filter((line) => line.length > 0)
|
|
683
|
-
.slice(0, 12);
|
|
684
|
-
}
|
|
685
|
-
if (toolName === "bash" || toolName === "powershell") {
|
|
686
|
-
// A background start has no command output yet; its content is the task
|
|
687
|
-
// header, which the summary already carries.
|
|
688
|
-
const body0 = resultText(result);
|
|
689
|
-
if (body0.startsWith("Started bash_"))
|
|
690
|
-
return [];
|
|
691
|
-
// The exit code is already the summary; show the command's own output. The
|
|
692
|
-
// SDK appends "Command exited with code N" to the content on failure, which
|
|
693
|
-
// would only repeat the summary line.
|
|
694
|
-
const structured = structuredOf(result);
|
|
695
|
-
const body = typeof structured.output === "string" ? structured.output : resultText(result).replace(/\n*Command exited with code \d+\s*$/, "");
|
|
696
|
-
const lines = body
|
|
697
|
-
.trimEnd()
|
|
698
|
-
.split("\n")
|
|
699
|
-
.filter((line) => line.trim());
|
|
700
|
-
return lines.slice(-4);
|
|
701
|
-
}
|
|
702
|
-
if (toolName === "bash_output") {
|
|
703
|
-
// The task's own output is the point, so show its last lines.
|
|
704
|
-
const lines = resultText(result)
|
|
705
|
-
.split("\n")
|
|
706
|
-
.slice(2)
|
|
707
|
-
.filter((line) => line.trim());
|
|
708
|
-
return lines.slice(-6);
|
|
709
|
-
}
|
|
710
|
-
if (toolName === "todo") {
|
|
711
|
-
// The list is the point of the tool, so show it rather than only counting.
|
|
712
|
-
const todos = (details.todos ?? []);
|
|
713
|
-
const mark = { pending: "[ ]", in_progress: "[>]", completed: "[x]", dropped: "[-]" };
|
|
714
|
-
return todos.slice(0, 6).map((item) => `${mark[item.status ?? "pending"] ?? "[ ]"} ${item.content ?? ""}`);
|
|
715
|
-
}
|
|
716
|
-
if (toolName === "task") {
|
|
717
|
-
// The summary carries each child's header, so the detail is the report under
|
|
718
|
-
// it — the child's own words, which is what the parent reads next and what a
|
|
719
|
-
// reader wants when they open a call. The header is dropped rather than
|
|
720
|
-
// repeated, so the two lines do not say the same thing twice.
|
|
721
|
-
return resultText(result)
|
|
722
|
-
.split("\n")
|
|
723
|
-
.filter((line) => line.trim() && !line.startsWith("## "))
|
|
724
|
-
.slice(0, 6);
|
|
725
|
-
}
|
|
726
|
-
if (toolName === "workflow") {
|
|
727
|
-
// A workflow is only legible if you can see the agents it ran and where each
|
|
728
|
-
// one failed, so the steps are the detail rather than a count.
|
|
729
|
-
const steps = (details.steps ?? []);
|
|
730
|
-
const lines = steps.map((step) => {
|
|
731
|
-
const glyph = step.status === "running" ? "▸" : step.status === "failed" ? "×" : "✓";
|
|
732
|
-
const where = step.phase ? `${step.phase} · ` : "";
|
|
733
|
-
return `${glyph} ${where}${step.label ?? "agent"}`;
|
|
734
|
-
});
|
|
735
|
-
return lines.slice(-6);
|
|
736
|
-
}
|
|
737
|
-
return [];
|
|
738
|
-
}
|
|
739
|
-
/**
|
|
740
|
-
* The whole output of a tool call, for the expand toggle.
|
|
741
|
-
*
|
|
742
|
-
* `toolResultDetail` is deliberately narrow — the summary line already carries
|
|
743
|
-
* the gist, and the workbench is a transcript, not a log viewer. That is the
|
|
744
|
-
* right default and the wrong *only* option: when a call needs to be read in
|
|
745
|
-
* full (a diff that did not apply, the tail of a failing suite, the file a
|
|
746
|
-
* `read` actually returned), the user wants the whole thing without leaving the
|
|
747
|
-
* screen or asking the agent to repeat it.
|
|
748
|
-
*
|
|
749
|
-
* Capped so a stray `cat` of a huge file cannot wedge the render: the cap is
|
|
750
|
-
* large enough to read a normal result and small enough that one keypress
|
|
751
|
-
* cannot paint a megabyte.
|
|
752
|
-
*/
|
|
753
|
-
export const EXPANDED_LINE_CAP = 400;
|
|
754
|
-
export function toolResultFull(toolName, result) {
|
|
755
|
-
const details = detailsOf(result);
|
|
756
|
-
if (toolName === "edit" && typeof details.diff === "string") {
|
|
757
|
-
return details.diff
|
|
758
|
-
.split("\n")
|
|
759
|
-
.filter((line) => line.length > 0)
|
|
760
|
-
.slice(0, EXPANDED_LINE_CAP);
|
|
761
|
-
}
|
|
762
|
-
const structured = structuredOf(result);
|
|
763
|
-
const body = typeof structured.output === "string" ? structured.output : resultText(result).replace(/\n*Command exited with code \d+\s*$/, "");
|
|
764
|
-
const lines = body.trimEnd().split("\n");
|
|
765
|
-
const kept = lines.slice(0, EXPANDED_LINE_CAP);
|
|
766
|
-
if (lines.length > EXPANDED_LINE_CAP)
|
|
767
|
-
kept.push(`… ${lines.length - EXPANDED_LINE_CAP} more line(s)`);
|
|
768
|
-
return kept;
|
|
769
|
-
}
|