@xiliumz/pi-minimal 1.0.0 → 1.1.0
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 +17 -1
- package/extensions/index.ts +49 -0
- package/package.json +4 -4
- package/src/editor.ts +13 -0
- package/src/tool-runs.ts +66 -0
- package/src/tool-view.ts +192 -0
package/README.md
CHANGED
|
@@ -32,6 +32,16 @@ Restart pi or run `/reload`.
|
|
|
32
32
|
| Spinner | Shown in the prompt while the agent runs |
|
|
33
33
|
| Header / footer | Removed |
|
|
34
34
|
| `/info` / `F8` | Model, thinking level, context %, tokens, cost, session file/id |
|
|
35
|
+
| Tool rows / `Ctrl+O` | Cycles **grouped** → **title** → **collapsed** → **full**. Starts grouped |
|
|
36
|
+
|
|
37
|
+
Tool views:
|
|
38
|
+
|
|
39
|
+
- **grouped** — back-to-back tool calls collapse into one `N tools` line; a lone call shows its name
|
|
40
|
+
- **title** — one line per call, tool name only (red when the call failed)
|
|
41
|
+
- **collapsed** — pi's default truncated preview
|
|
42
|
+
- **full** — pi's normal expanded output
|
|
43
|
+
|
|
44
|
+
Assistant text, thinking, and user messages end a group.
|
|
35
45
|
|
|
36
46
|
Multiline input and slash-command autocomplete start at column 0 (under `$ `).
|
|
37
47
|
Soft-wrapped continuations use the full terminal width — only the first visual line shares space with `$ `.
|
|
@@ -42,6 +52,10 @@ Soft-wrapped continuations use the full terminal width — only the first visual
|
|
|
42
52
|
Pi resets extension UI before the old session context goes stale, then re-runs
|
|
43
53
|
`session_start` so this layout comes back. Extension-side fix is not available.
|
|
44
54
|
|
|
55
|
+
`Ctrl+O` follows your `app.tools.expand` keybinding and shows the new view as
|
|
56
|
+
`Tool output: <view>`. `/export` renders tools in
|
|
57
|
+
the current view — switch to **full** first for complete HTML.
|
|
58
|
+
|
|
45
59
|
## Develop
|
|
46
60
|
|
|
47
61
|
```bash
|
|
@@ -58,7 +72,9 @@ Layout:
|
|
|
58
72
|
|
|
59
73
|
```
|
|
60
74
|
extensions/index.ts # pi entry (registers hooks + commands)
|
|
61
|
-
src/editor.ts # bash prompt CustomEditor
|
|
75
|
+
src/editor.ts # bash prompt CustomEditor (+ Ctrl+O hook)
|
|
76
|
+
src/tool-view.ts # grouped / title / full tool renderers
|
|
77
|
+
src/tool-runs.ts # tracks back-to-back tool calls
|
|
62
78
|
src/info.ts # /info UI + token stats
|
|
63
79
|
src/git.ts # branch + dirty
|
|
64
80
|
src/format.ts # cwd/tokens helpers
|
package/extensions/index.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* $ |
|
|
7
7
|
*
|
|
8
8
|
* Header/footer chrome removed. Session stats via /info.
|
|
9
|
+
* Ctrl+O cycles tool rows: grouped (`N tools`) → title → collapsed → full.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
@@ -13,6 +14,8 @@ import type { TUI } from "@earendil-works/pi-tui";
|
|
|
13
14
|
import { createBashPromptEditor } from "../src/editor.js";
|
|
14
15
|
import { fetchGitState, type GitState } from "../src/git.js";
|
|
15
16
|
import { showInfoUi } from "../src/info.js";
|
|
17
|
+
import { ToolRuns } from "../src/tool-runs.js";
|
|
18
|
+
import { createToolViewRenderer, nextToolView, type ToolView } from "../src/tool-view.js";
|
|
16
19
|
import { EmptyComponent } from "../src/ui.js";
|
|
17
20
|
|
|
18
21
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
@@ -23,6 +26,28 @@ export default function bashLayoutExtension(pi: ExtensionAPI): void {
|
|
|
23
26
|
let isWorking = false;
|
|
24
27
|
let spinnerIndex = 0;
|
|
25
28
|
let spinnerTimer: ReturnType<typeof setInterval> | undefined;
|
|
29
|
+
let activeCtx: ExtensionContext | undefined;
|
|
30
|
+
|
|
31
|
+
const toolState = { view: "grouped" as ToolView, runs: new ToolRuns() };
|
|
32
|
+
const toolRenderer = createToolViewRenderer(toolState);
|
|
33
|
+
pi.registerToolRenderer(toolRenderer.resolve);
|
|
34
|
+
|
|
35
|
+
// pi only redraws rows when the expanded flag changes, so flip it twice.
|
|
36
|
+
const repaintTools = (ctx: ExtensionContext) => {
|
|
37
|
+
const expanded = toolState.view === "full";
|
|
38
|
+
ctx.ui.setToolsExpanded(!expanded);
|
|
39
|
+
ctx.ui.setToolsExpanded(expanded);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const rebuildToolRuns = (ctx: ExtensionContext) => {
|
|
43
|
+
toolState.runs.clear();
|
|
44
|
+
toolRenderer.clear();
|
|
45
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
46
|
+
if (entry.type === "message") toolState.runs.add(entry.message);
|
|
47
|
+
else toolState.runs.break();
|
|
48
|
+
}
|
|
49
|
+
repaintTools(ctx);
|
|
50
|
+
};
|
|
26
51
|
|
|
27
52
|
const stopSpinner = () => {
|
|
28
53
|
if (spinnerTimer) {
|
|
@@ -49,9 +74,30 @@ export default function bashLayoutExtension(pi: ExtensionAPI): void {
|
|
|
49
74
|
onMount(tui: TUI) {
|
|
50
75
|
activeTui = tui;
|
|
51
76
|
},
|
|
77
|
+
onToolsExpand() {
|
|
78
|
+
if (!activeCtx) return;
|
|
79
|
+
toolState.view = nextToolView(toolState.view);
|
|
80
|
+
repaintTools(activeCtx);
|
|
81
|
+
// Info notices reuse pi's status line, replacing its `collapsed/expanded` text.
|
|
82
|
+
activeCtx.ui.notify(`Tool output: ${toolState.view}`, "info");
|
|
83
|
+
},
|
|
52
84
|
};
|
|
53
85
|
|
|
86
|
+
pi.on("message_end", (event) => {
|
|
87
|
+
const changed = toolState.runs.add(event.message);
|
|
88
|
+
if (toolState.view === "grouped") toolRenderer.redraw(changed);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
92
|
+
if (ctx.mode === "tui") rebuildToolRuns(ctx);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
pi.on("session_compact", (_event, ctx) => {
|
|
96
|
+
if (ctx.mode === "tui") rebuildToolRuns(ctx);
|
|
97
|
+
});
|
|
98
|
+
|
|
54
99
|
pi.on("agent_start", () => {
|
|
100
|
+
toolState.runs.break();
|
|
55
101
|
isWorking = true;
|
|
56
102
|
stopSpinner();
|
|
57
103
|
spinnerTimer = setInterval(() => {
|
|
@@ -73,6 +119,7 @@ export default function bashLayoutExtension(pi: ExtensionAPI): void {
|
|
|
73
119
|
pi.on("session_shutdown", () => {
|
|
74
120
|
stopSpinner();
|
|
75
121
|
activeTui = undefined;
|
|
122
|
+
activeCtx = undefined;
|
|
76
123
|
});
|
|
77
124
|
|
|
78
125
|
pi.on("session_start", (_event, ctx) => {
|
|
@@ -81,6 +128,8 @@ export default function bashLayoutExtension(pi: ExtensionAPI): void {
|
|
|
81
128
|
ctx.ui.setHeader(() => new EmptyComponent());
|
|
82
129
|
ctx.ui.setFooter(() => new EmptyComponent());
|
|
83
130
|
ctx.ui.setWorkingVisible(false);
|
|
131
|
+
activeCtx = ctx;
|
|
132
|
+
rebuildToolRuns(ctx);
|
|
84
133
|
|
|
85
134
|
void refreshGit(ctx.cwd);
|
|
86
135
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiliumz/pi-minimal",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Bash-style prompt layout for pi — ~/path (branch*): with /info",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
"typecheck": "tsc --noEmit"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@earendil-works/pi-ai": "^
|
|
46
|
-
"@earendil-works/pi-coding-agent": "^
|
|
47
|
-
"@earendil-works/pi-tui": "^
|
|
45
|
+
"@earendil-works/pi-ai": "^1.3.0",
|
|
46
|
+
"@earendil-works/pi-coding-agent": "^1.3.0",
|
|
47
|
+
"@earendil-works/pi-tui": "^1.3.0",
|
|
48
48
|
"@types/node": "^26.1.2",
|
|
49
49
|
"typescript": "^7.0.2"
|
|
50
50
|
}
|
package/src/editor.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface BashPromptState {
|
|
|
21
21
|
spinnerFrame: string;
|
|
22
22
|
/** Called when the editor mounts so the extension can request re-renders. */
|
|
23
23
|
onMount: (tui: TUI) => void;
|
|
24
|
+
/** Replaces pi's `app.tools.expand` (Ctrl+O) toggle. Extensions cannot bind that key directly. */
|
|
25
|
+
onToolsExpand: () => void;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
/** Subset of Editor internals used for dual-width soft-wrap. */
|
|
@@ -52,9 +54,12 @@ export function createBashPromptEditor(
|
|
|
52
54
|
return class BashPromptEditor extends CustomEditor {
|
|
53
55
|
/** Columns reserved on the first visual line for `$ `. */
|
|
54
56
|
private promptInset = 0;
|
|
57
|
+
/** CustomEditor keeps its own copy private. */
|
|
58
|
+
private readonly keys: KeybindingsManager;
|
|
55
59
|
|
|
56
60
|
constructor(tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) {
|
|
57
61
|
super(tui, theme, keybindings, { paddingX: 0 });
|
|
62
|
+
this.keys = keybindings;
|
|
58
63
|
// pi-tui's Editor declares `private layoutText`; TS forbids a subclass
|
|
59
64
|
// member with that name (TS2415/TS2322), but JS ignores `private` at
|
|
60
65
|
// runtime — so install the override as an instance property instead.
|
|
@@ -63,6 +68,14 @@ export function createBashPromptEditor(
|
|
|
63
68
|
state.onMount(tui);
|
|
64
69
|
}
|
|
65
70
|
|
|
71
|
+
override handleInput(data: string): void {
|
|
72
|
+
if (this.keys.matches(data, "app.tools.expand")) {
|
|
73
|
+
state.onToolsExpand();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
super.handleInput(data);
|
|
77
|
+
}
|
|
78
|
+
|
|
66
79
|
/** Host may push settings padding — keep 0 so prompt math stays correct. */
|
|
67
80
|
override setPaddingX(_paddingX: number): void {
|
|
68
81
|
super.setPaddingX(0);
|
package/src/tool-runs.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { MessageEndEvent } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
type TranscriptMessage = MessageEndEvent["message"];
|
|
4
|
+
|
|
5
|
+
export interface ToolRunPosition {
|
|
6
|
+
/** First call of the run — the one that draws `N tools`. */
|
|
7
|
+
isHead: boolean;
|
|
8
|
+
/** Number of calls in the run so far. */
|
|
9
|
+
count: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Groups tool calls that appear back to back in the transcript. A run ends at
|
|
14
|
+
* any visible non-tool content: assistant text, thinking, user or custom
|
|
15
|
+
* messages. Tool results never end a run.
|
|
16
|
+
*/
|
|
17
|
+
export class ToolRuns {
|
|
18
|
+
private current: string[] = [];
|
|
19
|
+
private readonly runById = new Map<string, string[]>();
|
|
20
|
+
|
|
21
|
+
clear(): void {
|
|
22
|
+
this.current = [];
|
|
23
|
+
this.runById.clear();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Feed one finished message. Returns tool call ids whose run changed. */
|
|
27
|
+
add(message: TranscriptMessage): string[] {
|
|
28
|
+
if (message.role === "toolResult") return [];
|
|
29
|
+
if (message.role !== "assistant") {
|
|
30
|
+
this.current = [];
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const changed: string[] = [];
|
|
35
|
+
for (const block of message.content) {
|
|
36
|
+
if (block.type !== "toolCall") {
|
|
37
|
+
if (isVisible(block)) this.current = [];
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (this.runById.has(block.id)) continue;
|
|
41
|
+
const head = this.current[0];
|
|
42
|
+
if (head) changed.push(head);
|
|
43
|
+
this.current.push(block.id);
|
|
44
|
+
this.runById.set(block.id, this.current);
|
|
45
|
+
changed.push(block.id);
|
|
46
|
+
}
|
|
47
|
+
return changed;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Ends the open run, e.g. at a compaction or other non-message entry. */
|
|
51
|
+
break(): void {
|
|
52
|
+
this.current = [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
position(toolCallId: string): ToolRunPosition | undefined {
|
|
56
|
+
const run = this.runById.get(toolCallId);
|
|
57
|
+
if (!run) return undefined;
|
|
58
|
+
return { isHead: run[0] === toolCallId, count: run.length };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isVisible(block: { type: string; text?: string; thinking?: string }): boolean {
|
|
63
|
+
if (block.type === "text") return Boolean(block.text?.trim());
|
|
64
|
+
if (block.type === "thinking") return Boolean(block.thinking?.trim());
|
|
65
|
+
return true;
|
|
66
|
+
}
|
package/src/tool-view.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import {
|
|
2
|
+
keyHint,
|
|
3
|
+
type AgentToolResult,
|
|
4
|
+
type Theme,
|
|
5
|
+
type ToolRendererResolver,
|
|
6
|
+
type ToolRenderers,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { Box, Text, type Component } from "@earendil-works/pi-tui";
|
|
9
|
+
import type { ToolRuns } from "./tool-runs.js";
|
|
10
|
+
import { EmptyComponent } from "./ui.js";
|
|
11
|
+
|
|
12
|
+
/** pi does not export `ToolRenderContext` from its entry point. */
|
|
13
|
+
type ToolRenderContext = Parameters<NonNullable<ToolRenderers["renderCall"]>>[2];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* How tool rows are drawn; Ctrl+O cycles them in this order.
|
|
17
|
+
* - grouped: back-to-back calls collapse into one `N tools` line
|
|
18
|
+
* - title: one line per call with just the tool name
|
|
19
|
+
* - collapsed: pi's default truncated preview
|
|
20
|
+
* - full: pi's normal expanded output
|
|
21
|
+
*/
|
|
22
|
+
export type ToolView = "grouped" | "title" | "collapsed" | "full";
|
|
23
|
+
|
|
24
|
+
const NEXT_VIEW: Record<ToolView, ToolView> = {
|
|
25
|
+
grouped: "title",
|
|
26
|
+
title: "collapsed",
|
|
27
|
+
collapsed: "full",
|
|
28
|
+
full: "grouped",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function nextToolView(view: ToolView): ToolView {
|
|
32
|
+
return NEXT_VIEW[view];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Views drawn by pi-minimal itself rather than the tool's own renderers. */
|
|
36
|
+
function isSummaryView(view: ToolView): boolean {
|
|
37
|
+
return view === "grouped" || view === "title";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Same limits pi uses for tools without renderers. */
|
|
41
|
+
const PREVIEW_LINES = 10;
|
|
42
|
+
const PREVIEW_ARGS_CHARS = 100;
|
|
43
|
+
|
|
44
|
+
export interface ToolViewState {
|
|
45
|
+
readonly view: ToolView;
|
|
46
|
+
readonly runs: ToolRuns;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Per-row bookkeeping: the row's redraw hook, plus the base renderer's components and our frame for collapsed/full view. */
|
|
50
|
+
interface RowCache {
|
|
51
|
+
redraw?: () => void;
|
|
52
|
+
call?: Component;
|
|
53
|
+
result?: Component;
|
|
54
|
+
frame?: Box;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ToolViewRenderer {
|
|
58
|
+
resolve: ToolRendererResolver;
|
|
59
|
+
/** Redraw rows already on screen, e.g. after their run grew. */
|
|
60
|
+
redraw(toolCallIds: string[]): void;
|
|
61
|
+
/** Drop per-row caches, e.g. when the transcript is rebuilt. */
|
|
62
|
+
clear(): void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Renderer resolver that lets pi-minimal decide what every tool row shows,
|
|
67
|
+
* including MCP and other extensions' tools.
|
|
68
|
+
*/
|
|
69
|
+
export function createToolViewRenderer(state: ToolViewState): ToolViewRenderer {
|
|
70
|
+
const rows = new Map<string, RowCache>();
|
|
71
|
+
return {
|
|
72
|
+
resolve: (toolName, next) => wrapRenderers(toolName, next(), state, rows),
|
|
73
|
+
redraw: (toolCallIds) => {
|
|
74
|
+
for (const id of toolCallIds) rows.get(id)?.redraw?.();
|
|
75
|
+
},
|
|
76
|
+
clear: () => rows.clear(),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Rows always use `renderShell: "self"` — pi fixes the shell when a row is
|
|
82
|
+
* created, and only "self" lets a hidden row take zero lines. Full view
|
|
83
|
+
* redraws pi's default box itself.
|
|
84
|
+
*/
|
|
85
|
+
function wrapRenderers(
|
|
86
|
+
toolName: string,
|
|
87
|
+
base: ToolRenderers | undefined,
|
|
88
|
+
state: ToolViewState,
|
|
89
|
+
rows: Map<string, RowCache>,
|
|
90
|
+
): ToolRenderers {
|
|
91
|
+
const row = (id: string) => {
|
|
92
|
+
let cache = rows.get(id);
|
|
93
|
+
if (!cache) rows.set(id, (cache = {}));
|
|
94
|
+
return cache;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
renderShell: "self",
|
|
99
|
+
|
|
100
|
+
renderCall(args, theme, context) {
|
|
101
|
+
const cache = row(context.toolCallId);
|
|
102
|
+
cache.redraw = context.invalidate;
|
|
103
|
+
if (isSummaryView(state.view)) return summaryLine(toolName, theme, context, state);
|
|
104
|
+
|
|
105
|
+
const baseContext = { ...context, lastComponent: cache.call };
|
|
106
|
+
cache.call = base?.renderCall
|
|
107
|
+
? base.renderCall(args, theme, baseContext)
|
|
108
|
+
: new Text(formatArgs(toolName, args, theme, context.expanded), 0, 0);
|
|
109
|
+
if (base?.renderShell === "self") return cache.call;
|
|
110
|
+
|
|
111
|
+
cache.frame = new Box(context.outputPad, 1, rowBackground(theme, context));
|
|
112
|
+
cache.frame.addChild(cache.call);
|
|
113
|
+
return cache.frame;
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
renderResult(result, options, theme, context) {
|
|
117
|
+
if (isSummaryView(state.view)) return new EmptyComponent();
|
|
118
|
+
|
|
119
|
+
const cache = row(context.toolCallId);
|
|
120
|
+
const baseContext = { ...context, lastComponent: cache.result };
|
|
121
|
+
cache.result = base?.renderResult
|
|
122
|
+
? base.renderResult(result, options, theme, baseContext)
|
|
123
|
+
: fallbackResult(result, theme, options.expanded);
|
|
124
|
+
if (base?.renderShell === "self" || !cache.frame) return cache.result;
|
|
125
|
+
|
|
126
|
+
// Default-shell tools draw call + result inside one box, like pi does.
|
|
127
|
+
cache.frame.addChild(cache.result);
|
|
128
|
+
return new EmptyComponent();
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function summaryLine(
|
|
134
|
+
toolName: string,
|
|
135
|
+
theme: Theme,
|
|
136
|
+
context: ToolRenderContext,
|
|
137
|
+
state: ToolViewState,
|
|
138
|
+
): Component {
|
|
139
|
+
const background = rowBackground(theme, context);
|
|
140
|
+
const position = state.view === "grouped" ? state.runs.position(context.toolCallId) : undefined;
|
|
141
|
+
if (position && position.count > 1) {
|
|
142
|
+
if (!position.isHead) return new EmptyComponent();
|
|
143
|
+
const label = theme.fg("toolTitle", theme.bold(`${position.count} tools`));
|
|
144
|
+
return new Text(label, context.outputPad, 1, background);
|
|
145
|
+
}
|
|
146
|
+
const color = context.isError ? "error" : "toolTitle";
|
|
147
|
+
return new Text(theme.fg(color, theme.bold(toolName)), context.outputPad, 1, background);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function rowBackground(theme: Theme, context: ToolRenderContext): (text: string) => string {
|
|
151
|
+
if (context.isPartial) return (text) => theme.bg("toolPendingBg", text);
|
|
152
|
+
if (context.isError) return (text) => theme.bg("toolErrorBg", text);
|
|
153
|
+
return (text) => theme.bg("toolSuccessBg", text);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Tools without their own renderer: name plus one `key: value` line per
|
|
158
|
+
* argument, or a one-line `key=value` preview when collapsed.
|
|
159
|
+
*/
|
|
160
|
+
function formatArgs(toolName: string, args: unknown, theme: Theme, expanded: boolean): string {
|
|
161
|
+
const header = theme.fg("toolTitle", theme.bold(toolName));
|
|
162
|
+
if (!args || typeof args !== "object") return header;
|
|
163
|
+
if (!expanded) {
|
|
164
|
+
const pairs = Object.entries(args)
|
|
165
|
+
.map(([key, value]) => `${key}=${JSON.stringify(value) ?? String(value)}`)
|
|
166
|
+
.join(" ");
|
|
167
|
+
if (!pairs) return header;
|
|
168
|
+
const preview = pairs.length > PREVIEW_ARGS_CHARS ? `${pairs.slice(0, PREVIEW_ARGS_CHARS - 3)}...` : pairs;
|
|
169
|
+
return `${header} ${theme.fg("muted", preview)}`;
|
|
170
|
+
}
|
|
171
|
+
const lines = Object.entries(args).map(([key, value]) => {
|
|
172
|
+
const text = typeof value === "string" ? value : (JSON.stringify(value, null, 2) ?? String(value));
|
|
173
|
+
return ` ${key}: ${text.replace(/\r/g, "").split("\n").join("\n ")}`;
|
|
174
|
+
});
|
|
175
|
+
return lines.length > 0 ? `${header}\n${theme.fg("muted", lines.join("\n"))}` : header;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function fallbackResult(result: AgentToolResult<unknown>, theme: Theme, expanded: boolean): Component {
|
|
179
|
+
const output = result.content
|
|
180
|
+
.flatMap((block) => (block.type === "text" ? [block.text] : []))
|
|
181
|
+
.join("\n")
|
|
182
|
+
.trim();
|
|
183
|
+
if (!output) return new EmptyComponent();
|
|
184
|
+
const lines = output.split("\n");
|
|
185
|
+
const shown = expanded ? lines : lines.slice(0, PREVIEW_LINES);
|
|
186
|
+
let text = shown.map((line) => theme.fg("toolOutput", line)).join("\n");
|
|
187
|
+
const remaining = lines.length - shown.length;
|
|
188
|
+
if (remaining > 0) {
|
|
189
|
+
text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")}${theme.fg("muted", ")")}`;
|
|
190
|
+
}
|
|
191
|
+
return new Text(text, 0, 0);
|
|
192
|
+
}
|