pi-zentui 0.1.3 → 0.1.4
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
CHANGED
|
@@ -84,6 +84,7 @@ On first run, Zentui creates a config file at:
|
|
|
84
84
|
|
|
85
85
|
```json
|
|
86
86
|
{
|
|
87
|
+
"projectRefreshIntervalMs": 30000,
|
|
87
88
|
"icons": {
|
|
88
89
|
"cwd": "",
|
|
89
90
|
"git": "",
|
|
@@ -109,18 +110,11 @@ On first run, Zentui creates a config file at:
|
|
|
109
110
|
"tokens": "muted",
|
|
110
111
|
"cost": "success",
|
|
111
112
|
"separator": "borderMuted"
|
|
112
|
-
},
|
|
113
|
-
"tools": {
|
|
114
|
-
"style": "compact"
|
|
115
113
|
}
|
|
116
114
|
}
|
|
117
115
|
```
|
|
118
116
|
|
|
119
|
-
`
|
|
120
|
-
|
|
121
|
-
- `compact` — one-line tool calls by default
|
|
122
|
-
- `truncated` — expanded preview with long output truncated
|
|
123
|
-
- `full` — expanded full output
|
|
117
|
+
`projectRefreshIntervalMs` controls how often Zentui refreshes project status (git/runtime) while Pi is idle. Set it to `0` to disable polling; invalid values or values below 5000 ms fall back to `30000`.
|
|
124
118
|
|
|
125
119
|
### Color values
|
|
126
120
|
|
|
@@ -3,9 +3,12 @@ import { join } from "node:path";
|
|
|
3
3
|
import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
4
4
|
|
|
5
5
|
export type ColorSpec = string;
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
const DEFAULT_PROJECT_REFRESH_INTERVAL_MS = 30_000;
|
|
8
|
+
const MIN_PROJECT_REFRESH_INTERVAL_MS = 5_000;
|
|
7
9
|
|
|
8
10
|
export type PolishedTuiConfig = {
|
|
11
|
+
projectRefreshIntervalMs: number;
|
|
9
12
|
icons: {
|
|
10
13
|
cwd: string;
|
|
11
14
|
git: string;
|
|
@@ -32,9 +35,6 @@ export type PolishedTuiConfig = {
|
|
|
32
35
|
cost: ColorSpec;
|
|
33
36
|
separator: ColorSpec;
|
|
34
37
|
};
|
|
35
|
-
tools: {
|
|
36
|
-
style: ToolOutputStyle;
|
|
37
|
-
};
|
|
38
38
|
};
|
|
39
39
|
|
|
40
40
|
export const configPath = join(getAgentDir(), "zentui.json");
|
|
@@ -88,6 +88,7 @@ const themeColorTokens = new Set([
|
|
|
88
88
|
]);
|
|
89
89
|
|
|
90
90
|
export const defaultConfig: PolishedTuiConfig = {
|
|
91
|
+
projectRefreshIntervalMs: DEFAULT_PROJECT_REFRESH_INTERVAL_MS,
|
|
91
92
|
icons: {
|
|
92
93
|
cwd: "",
|
|
93
94
|
git: "",
|
|
@@ -114,9 +115,6 @@ export const defaultConfig: PolishedTuiConfig = {
|
|
|
114
115
|
cost: "success",
|
|
115
116
|
separator: "borderMuted",
|
|
116
117
|
},
|
|
117
|
-
tools: {
|
|
118
|
-
style: "compact",
|
|
119
|
-
},
|
|
120
118
|
};
|
|
121
119
|
|
|
122
120
|
function isHexColor(value: string): boolean {
|
|
@@ -141,10 +139,16 @@ function isRecord(value: unknown): value is ConfigRecord {
|
|
|
141
139
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
142
140
|
}
|
|
143
141
|
|
|
144
|
-
function
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
142
|
+
function parseProjectRefreshIntervalMs(value: unknown): number {
|
|
143
|
+
if (value === 0) return 0;
|
|
144
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
145
|
+
return defaultConfig.projectRefreshIntervalMs;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const interval = Math.round(value);
|
|
149
|
+
return interval >= MIN_PROJECT_REFRESH_INTERVAL_MS
|
|
150
|
+
? interval
|
|
151
|
+
: defaultConfig.projectRefreshIntervalMs;
|
|
148
152
|
}
|
|
149
153
|
|
|
150
154
|
export function colorize(theme: ThemeLike, color: ColorSpec, text: string): string {
|
|
@@ -173,9 +177,8 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
|
|
|
173
177
|
const colors = isRecord(config.colors)
|
|
174
178
|
? (config.colors as Partial<PolishedTuiConfig["colors"]>)
|
|
175
179
|
: {};
|
|
176
|
-
const tools = isRecord(config.tools) ? (config.tools as Partial<PolishedTuiConfig["tools"]>) : {};
|
|
177
|
-
|
|
178
180
|
return {
|
|
181
|
+
projectRefreshIntervalMs: parseProjectRefreshIntervalMs(config.projectRefreshIntervalMs),
|
|
179
182
|
icons: {
|
|
180
183
|
...defaultConfig.icons,
|
|
181
184
|
...icons,
|
|
@@ -184,10 +187,6 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
|
|
|
184
187
|
...defaultConfig.colors,
|
|
185
188
|
...colors,
|
|
186
189
|
},
|
|
187
|
-
tools: {
|
|
188
|
-
...defaultConfig.tools,
|
|
189
|
-
style: parseToolOutputStyle(tools.style),
|
|
190
|
-
},
|
|
191
190
|
};
|
|
192
191
|
}
|
|
193
192
|
|
|
@@ -6,9 +6,9 @@ import type {
|
|
|
6
6
|
Theme,
|
|
7
7
|
} from "@mariozechner/pi-coding-agent";
|
|
8
8
|
import { type EditorTheme, type TUI, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
9
|
-
import { registerCompactTools } from "./compact-tools";
|
|
10
9
|
import { type PolishedTuiConfig, colorize, ensureConfigExists, loadConfig } from "./config";
|
|
11
10
|
import { type GitStatusSummary, emptyGitStatus, readGitStatus } from "./git";
|
|
11
|
+
import { type StopProjectRefreshInterval, startProjectRefreshInterval } from "./project-refresh";
|
|
12
12
|
import { type RuntimeInfo, readRuntimeInfo } from "./runtime";
|
|
13
13
|
import { PolishedEditor, patchUserMessageComponent, restoreUserMessageComponent } from "./ui";
|
|
14
14
|
|
|
@@ -127,8 +127,6 @@ function formatCwdLabel(cwd: string, cwdIcon: string): string {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
export default function (pi: ExtensionAPI) {
|
|
130
|
-
registerCompactTools(pi);
|
|
131
|
-
|
|
132
130
|
const state: FooterState = {
|
|
133
131
|
modelLabel: "no-model",
|
|
134
132
|
providerLabel: "Unknown",
|
|
@@ -141,6 +139,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
141
139
|
|
|
142
140
|
let currentConfig: PolishedTuiConfig = loadConfig();
|
|
143
141
|
let requestFooterRender: (() => void) | undefined;
|
|
142
|
+
let stopProjectRefreshInterval: StopProjectRefreshInterval = () => {};
|
|
144
143
|
let projectRefreshInFlight = false;
|
|
145
144
|
let projectRefreshPending = false;
|
|
146
145
|
|
|
@@ -291,6 +290,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
291
290
|
patchUserMessageComponent(ctx.ui.theme);
|
|
292
291
|
installFooter(ctx);
|
|
293
292
|
installEditor(ctx);
|
|
293
|
+
stopProjectRefreshInterval();
|
|
294
|
+
stopProjectRefreshInterval = startProjectRefreshInterval(
|
|
295
|
+
currentConfig.projectRefreshIntervalMs,
|
|
296
|
+
() => scheduleProjectRefresh(ctx),
|
|
297
|
+
);
|
|
294
298
|
scheduleProjectRefresh(ctx);
|
|
295
299
|
refresh();
|
|
296
300
|
};
|
|
@@ -300,6 +304,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
300
304
|
});
|
|
301
305
|
|
|
302
306
|
pi.on("session_shutdown", async () => {
|
|
307
|
+
stopProjectRefreshInterval();
|
|
308
|
+
stopProjectRefreshInterval = () => {};
|
|
303
309
|
restoreUserMessageComponent();
|
|
304
310
|
});
|
|
305
311
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type StopProjectRefreshInterval = () => void;
|
|
2
|
+
|
|
3
|
+
export function startProjectRefreshInterval(
|
|
4
|
+
intervalMs: number,
|
|
5
|
+
refresh: () => void,
|
|
6
|
+
): StopProjectRefreshInterval {
|
|
7
|
+
if (intervalMs <= 0) return () => {};
|
|
8
|
+
|
|
9
|
+
const timer = setInterval(refresh, intervalMs);
|
|
10
|
+
timer.unref?.();
|
|
11
|
+
|
|
12
|
+
return () => clearInterval(timer);
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,565 +0,0 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
2
|
-
import type {
|
|
3
|
-
AgentToolResult,
|
|
4
|
-
ExtensionAPI,
|
|
5
|
-
Theme,
|
|
6
|
-
ToolDefinition,
|
|
7
|
-
ToolRenderResultOptions,
|
|
8
|
-
} from "@mariozechner/pi-coding-agent";
|
|
9
|
-
import {
|
|
10
|
-
createBashToolDefinition,
|
|
11
|
-
createEditToolDefinition,
|
|
12
|
-
createFindToolDefinition,
|
|
13
|
-
createGrepToolDefinition,
|
|
14
|
-
createLsToolDefinition,
|
|
15
|
-
createReadToolDefinition,
|
|
16
|
-
createWriteToolDefinition,
|
|
17
|
-
getLanguageFromPath,
|
|
18
|
-
highlightCode,
|
|
19
|
-
renderDiff,
|
|
20
|
-
} from "@mariozechner/pi-coding-agent";
|
|
21
|
-
import { Box, type Component, Container, Spacer, Text } from "@mariozechner/pi-tui";
|
|
22
|
-
import { type ToolOutputStyle, loadConfig } from "./config";
|
|
23
|
-
|
|
24
|
-
type BuiltInDefinitions = ReturnType<typeof createBuiltInDefinitions>;
|
|
25
|
-
type ToolArgs = Record<string, unknown>;
|
|
26
|
-
|
|
27
|
-
type CompactState = {
|
|
28
|
-
summary?: string;
|
|
29
|
-
error?: boolean;
|
|
30
|
-
errorText?: string;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
type CompactRenderContext = {
|
|
34
|
-
args: ToolArgs;
|
|
35
|
-
state: CompactState;
|
|
36
|
-
isError: boolean;
|
|
37
|
-
lastComponent?: Component;
|
|
38
|
-
invalidate: () => void;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
type CompactRenderCall = (args: ToolArgs, theme: Theme, context: CompactRenderContext) => Component;
|
|
42
|
-
type CompactExpandedRenderer = (
|
|
43
|
-
result: AgentToolResult<unknown>,
|
|
44
|
-
theme: Theme,
|
|
45
|
-
context: CompactRenderContext,
|
|
46
|
-
error: boolean,
|
|
47
|
-
mode: ExpandedOutputMode,
|
|
48
|
-
) => string;
|
|
49
|
-
type CompactRenderResult = (
|
|
50
|
-
result: AgentToolResult<unknown>,
|
|
51
|
-
options: ToolRenderResultOptions,
|
|
52
|
-
theme: Theme,
|
|
53
|
-
context: CompactRenderContext,
|
|
54
|
-
) => Component;
|
|
55
|
-
|
|
56
|
-
type OutputMode = "one-line" | "preview" | "full";
|
|
57
|
-
type ExpandedOutputMode = Exclude<OutputMode, "one-line">;
|
|
58
|
-
type ExpandedBoxStatus = "pending" | "success" | "error";
|
|
59
|
-
type ToolBackground = Parameters<Theme["bg"]>[0];
|
|
60
|
-
|
|
61
|
-
const PREVIEW_LINES = 12;
|
|
62
|
-
const OUTPUT_MODES: OutputMode[] = ["one-line", "preview", "full"];
|
|
63
|
-
|
|
64
|
-
let outputMode: OutputMode = "one-line";
|
|
65
|
-
let observedPiExpanded = false;
|
|
66
|
-
|
|
67
|
-
const home = homedir();
|
|
68
|
-
const definitionsByCwd = new Map<string, BuiltInDefinitions>();
|
|
69
|
-
|
|
70
|
-
function createBuiltInDefinitions(cwd: string) {
|
|
71
|
-
return {
|
|
72
|
-
bash: createBashToolDefinition(cwd),
|
|
73
|
-
edit: createEditToolDefinition(cwd),
|
|
74
|
-
find: createFindToolDefinition(cwd),
|
|
75
|
-
grep: createGrepToolDefinition(cwd),
|
|
76
|
-
ls: createLsToolDefinition(cwd),
|
|
77
|
-
read: createReadToolDefinition(cwd),
|
|
78
|
-
write: createWriteToolDefinition(cwd),
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function getBuiltIns(cwd: string): BuiltInDefinitions {
|
|
83
|
-
let definitions = definitionsByCwd.get(cwd);
|
|
84
|
-
if (!definitions) {
|
|
85
|
-
definitions = createBuiltInDefinitions(cwd);
|
|
86
|
-
definitionsByCwd.set(cwd, definitions);
|
|
87
|
-
}
|
|
88
|
-
return definitions;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function stripAtPrefix(path: string): string {
|
|
92
|
-
return path.startsWith("@") ? path.slice(1) : path;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function shortPath(path: unknown, fallback = "."): string {
|
|
96
|
-
if (typeof path !== "string" || path.length === 0) return fallback;
|
|
97
|
-
const cleaned = stripAtPrefix(path);
|
|
98
|
-
return cleaned.startsWith(home) ? `~${cleaned.slice(home.length)}` : cleaned;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function quote(value: unknown): string {
|
|
102
|
-
return `"${String(value ?? "")}"`;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function truncate(value: unknown, max = 120): string {
|
|
106
|
-
const text = String(value ?? "")
|
|
107
|
-
.replace(/\s+/g, " ")
|
|
108
|
-
.trim();
|
|
109
|
-
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function plural(count: number, one: string, many = `${one}s`): string {
|
|
113
|
-
return `${count} ${count === 1 ? one : many}`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function asRecord(value: unknown): Record<string, unknown> {
|
|
117
|
-
return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function resultDetails(result: AgentToolResult<unknown>): Record<string, unknown> {
|
|
121
|
-
return asRecord(result.details);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function isResultTruncated(result: AgentToolResult<unknown>): boolean {
|
|
125
|
-
return Boolean(asRecord(resultDetails(result).truncation).truncated);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function textContent(result: AgentToolResult<unknown>): string {
|
|
129
|
-
const block = result.content.find((item) => item.type === "text");
|
|
130
|
-
return block?.type === "text" ? block.text : "";
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function hasImage(result: AgentToolResult<unknown>): boolean {
|
|
134
|
-
return result.content.some((item) => item.type === "image");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function visibleLineCount(text: string): number {
|
|
138
|
-
return text
|
|
139
|
-
.split("\n")
|
|
140
|
-
.map((line) => line.trim())
|
|
141
|
-
.filter(
|
|
142
|
-
(line) =>
|
|
143
|
-
line.length > 0 && !line.startsWith("[Showing ") && !line.startsWith("[Output truncated"),
|
|
144
|
-
).length;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function firstUsefulLine(text: string): string | undefined {
|
|
148
|
-
return text
|
|
149
|
-
.split("\n")
|
|
150
|
-
.map((line) => line.trim())
|
|
151
|
-
.find((line) => line.length > 0);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function normalizeDisplayText(text: string): string {
|
|
155
|
-
return text.replace(/\r/g, "");
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function replaceTabs(text: string): string {
|
|
159
|
-
return text.replace(/\t/g, " ");
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function trimTrailingEmptyLines(lines: string[]): string[] {
|
|
163
|
-
let end = lines.length;
|
|
164
|
-
while (end > 0 && lines[end - 1] === "") end--;
|
|
165
|
-
return lines.slice(0, end);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function limitRenderedText(text: string, mode: ExpandedOutputMode, theme: Theme): string {
|
|
169
|
-
if (mode === "full") return text;
|
|
170
|
-
|
|
171
|
-
const lines = trimTrailingEmptyLines(text.split("\n"));
|
|
172
|
-
if (lines.length <= PREVIEW_LINES) return lines.join("\n");
|
|
173
|
-
|
|
174
|
-
const hidden = lines.length - PREVIEW_LINES;
|
|
175
|
-
return `${lines.slice(0, PREVIEW_LINES).join("\n")}\n${theme.fg(
|
|
176
|
-
"dim",
|
|
177
|
-
`… ${hidden} more lines (Ctrl+O for full)`,
|
|
178
|
-
)}`;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function nextOutputMode(mode: OutputMode): OutputMode {
|
|
182
|
-
return OUTPUT_MODES[(OUTPUT_MODES.indexOf(mode) + 1) % OUTPUT_MODES.length] ?? "one-line";
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function outputModeForToolStyle(style: ToolOutputStyle): OutputMode {
|
|
186
|
-
switch (style) {
|
|
187
|
-
case "full":
|
|
188
|
-
return "full";
|
|
189
|
-
case "truncated":
|
|
190
|
-
return "preview";
|
|
191
|
-
default:
|
|
192
|
-
return "one-line";
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function configuredOutputMode(expanded: boolean): OutputMode {
|
|
197
|
-
const configured = outputModeForToolStyle(loadConfig().tools.style);
|
|
198
|
-
return configured === "one-line" && expanded ? "preview" : configured;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function syncOutputModeWithPiToggle(expanded: boolean) {
|
|
202
|
-
if (expanded === observedPiExpanded) return;
|
|
203
|
-
observedPiExpanded = expanded;
|
|
204
|
-
outputMode = nextOutputMode(outputMode);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function argPath(context: CompactRenderContext): string | undefined {
|
|
208
|
-
const rawPath = context.args.path ?? context.args.file_path;
|
|
209
|
-
return typeof rawPath === "string" ? stripAtPrefix(rawPath) : undefined;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function renderHighlightedSource(
|
|
213
|
-
source: string,
|
|
214
|
-
path: string | undefined,
|
|
215
|
-
theme: Theme,
|
|
216
|
-
error: boolean,
|
|
217
|
-
mode: ExpandedOutputMode,
|
|
218
|
-
): string {
|
|
219
|
-
const normalized = replaceTabs(normalizeDisplayText(source)).trimEnd();
|
|
220
|
-
if (!normalized) return "";
|
|
221
|
-
if (error) return limitRenderedText(theme.fg("error", normalized), mode, theme);
|
|
222
|
-
|
|
223
|
-
const language = path ? getLanguageFromPath(path) : undefined;
|
|
224
|
-
if (!language) return limitRenderedText(theme.fg("muted", normalized), mode, theme);
|
|
225
|
-
|
|
226
|
-
try {
|
|
227
|
-
return limitRenderedText(
|
|
228
|
-
trimTrailingEmptyLines(highlightCode(normalized, language)).join("\n"),
|
|
229
|
-
mode,
|
|
230
|
-
theme,
|
|
231
|
-
);
|
|
232
|
-
} catch {
|
|
233
|
-
return limitRenderedText(theme.fg("muted", normalized), mode, theme);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function highlightShellCommand(command: unknown, theme: Theme): string {
|
|
238
|
-
const normalized = truncate(command, 180);
|
|
239
|
-
if (!normalized) return theme.fg("muted", "...");
|
|
240
|
-
|
|
241
|
-
try {
|
|
242
|
-
return trimTrailingEmptyLines(highlightCode(normalized, "bash")).join(" ");
|
|
243
|
-
} catch {
|
|
244
|
-
return theme.fg("accent", normalized);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function renderExpandedBox(content: string, theme: Theme, status: ExpandedBoxStatus) {
|
|
249
|
-
const background: ToolBackground =
|
|
250
|
-
status === "error" ? "toolErrorBg" : status === "pending" ? "toolPendingBg" : "toolSuccessBg";
|
|
251
|
-
const box = new Box(3, 1, (text: string) => theme.bg(background, text));
|
|
252
|
-
box.addChild(new Text(content, 0, 0));
|
|
253
|
-
return box;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function renderPlainExpanded(
|
|
257
|
-
result: AgentToolResult<unknown>,
|
|
258
|
-
theme: Theme,
|
|
259
|
-
_context: CompactRenderContext,
|
|
260
|
-
error: boolean,
|
|
261
|
-
mode: ExpandedOutputMode,
|
|
262
|
-
): string {
|
|
263
|
-
return limitRenderedText(
|
|
264
|
-
theme.fg(error ? "error" : "muted", textContent(result).trimEnd()),
|
|
265
|
-
mode,
|
|
266
|
-
theme,
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function renderReadExpanded(
|
|
271
|
-
result: AgentToolResult<unknown>,
|
|
272
|
-
theme: Theme,
|
|
273
|
-
context: CompactRenderContext,
|
|
274
|
-
error: boolean,
|
|
275
|
-
mode: ExpandedOutputMode,
|
|
276
|
-
): string {
|
|
277
|
-
return renderHighlightedSource(textContent(result), argPath(context), theme, error, mode);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function renderBashExpanded(
|
|
281
|
-
result: AgentToolResult<unknown>,
|
|
282
|
-
theme: Theme,
|
|
283
|
-
_context: CompactRenderContext,
|
|
284
|
-
error: boolean,
|
|
285
|
-
mode: ExpandedOutputMode,
|
|
286
|
-
): string {
|
|
287
|
-
const output = textContent(result).trimEnd();
|
|
288
|
-
return output ? limitRenderedText(theme.fg(error ? "error" : "muted", output), mode, theme) : "";
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function renderWriteExpanded(
|
|
292
|
-
result: AgentToolResult<unknown>,
|
|
293
|
-
theme: Theme,
|
|
294
|
-
context: CompactRenderContext,
|
|
295
|
-
error: boolean,
|
|
296
|
-
mode: ExpandedOutputMode,
|
|
297
|
-
) {
|
|
298
|
-
if (error) return renderPlainExpanded(result, theme, context, true, mode);
|
|
299
|
-
const source =
|
|
300
|
-
typeof context.args.content === "string" ? context.args.content : textContent(result);
|
|
301
|
-
return renderHighlightedSource(source, argPath(context), theme, false, mode);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function renderEditExpanded(
|
|
305
|
-
result: AgentToolResult<unknown>,
|
|
306
|
-
theme: Theme,
|
|
307
|
-
context: CompactRenderContext,
|
|
308
|
-
error: boolean,
|
|
309
|
-
mode: ExpandedOutputMode,
|
|
310
|
-
): string {
|
|
311
|
-
if (error) return renderPlainExpanded(result, theme, context, true, mode);
|
|
312
|
-
const diff = resultDetails(result).diff;
|
|
313
|
-
if (typeof diff === "string" && diff.length > 0) {
|
|
314
|
-
return limitRenderedText(renderDiff(diff, { filePath: argPath(context) }), mode, theme);
|
|
315
|
-
}
|
|
316
|
-
return renderPlainExpanded(result, theme, context, false, mode);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function setCompactState(context: CompactRenderContext, next: CompactState) {
|
|
320
|
-
const state = context.state;
|
|
321
|
-
const changed =
|
|
322
|
-
state.summary !== next.summary ||
|
|
323
|
-
state.error !== next.error ||
|
|
324
|
-
state.errorText !== next.errorText;
|
|
325
|
-
state.summary = next.summary;
|
|
326
|
-
state.error = next.error;
|
|
327
|
-
state.errorText = next.errorText;
|
|
328
|
-
|
|
329
|
-
// renderCall runs before renderResult. Re-render once after renderResult stores
|
|
330
|
-
// the final summary so the count/status can stay on the same compact line.
|
|
331
|
-
if (changed) queueMicrotask(() => context.invalidate());
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function suffix(state: CompactState): string {
|
|
335
|
-
if (state.errorText) return ` — ${truncate(state.errorText, 100)}`;
|
|
336
|
-
if (state.summary) return ` (${state.summary})`;
|
|
337
|
-
return "";
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function compactCall(format: (args: ToolArgs, state: CompactState) => string): CompactRenderCall {
|
|
341
|
-
return (args, theme, context) => {
|
|
342
|
-
const state = context.state;
|
|
343
|
-
const component =
|
|
344
|
-
context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
345
|
-
const color = state.error || context.isError ? "error" : "muted";
|
|
346
|
-
component.setText(theme.fg(color, format(args, state)));
|
|
347
|
-
return component;
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const compactBashCall: CompactRenderCall = (args, theme, context) => {
|
|
352
|
-
const state = context.state;
|
|
353
|
-
const component =
|
|
354
|
-
context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
355
|
-
const command = truncate(args.command, 180);
|
|
356
|
-
const timeout = args.timeout !== undefined ? ` [timeout=${args.timeout}s]` : "";
|
|
357
|
-
const meta = `${timeout}${suffix(state)}`;
|
|
358
|
-
|
|
359
|
-
if (state.error || context.isError) {
|
|
360
|
-
component.setText(theme.fg("error", `→ $ ${command}${meta}`));
|
|
361
|
-
} else {
|
|
362
|
-
component.setText(
|
|
363
|
-
`${theme.fg("muted", "→ ")}${theme.fg("accent", "$")} ${highlightShellCommand(
|
|
364
|
-
command,
|
|
365
|
-
theme,
|
|
366
|
-
)}${theme.fg("dim", meta)}`,
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
return component;
|
|
371
|
-
};
|
|
372
|
-
|
|
373
|
-
function compactResult(
|
|
374
|
-
summarize: (
|
|
375
|
-
result: AgentToolResult<unknown>,
|
|
376
|
-
context: CompactRenderContext,
|
|
377
|
-
) => string | undefined,
|
|
378
|
-
renderExpanded: CompactExpandedRenderer = renderPlainExpanded,
|
|
379
|
-
gapBeforeExpanded = true,
|
|
380
|
-
): CompactRenderResult {
|
|
381
|
-
return (result, { expanded, isPartial }, theme, context) => {
|
|
382
|
-
const output = textContent(result);
|
|
383
|
-
const error = Boolean(context.isError);
|
|
384
|
-
const errorText = error ? firstUsefulLine(output) : undefined;
|
|
385
|
-
const summary = isPartial ? "running" : summarize(result, context);
|
|
386
|
-
|
|
387
|
-
setCompactState(context, { summary, error, errorText });
|
|
388
|
-
|
|
389
|
-
syncOutputModeWithPiToggle(Boolean(expanded));
|
|
390
|
-
if (outputMode === "one-line") return new Text("", 0, 0);
|
|
391
|
-
|
|
392
|
-
const mode: ExpandedOutputMode = outputMode === "full" ? "full" : "preview";
|
|
393
|
-
const expandedText = renderExpanded(result, theme, context, error, mode);
|
|
394
|
-
if (!expandedText || expandedText.trim() === "") return new Text("", 0, 0);
|
|
395
|
-
|
|
396
|
-
const status: ExpandedBoxStatus = error ? "error" : isPartial ? "pending" : "success";
|
|
397
|
-
const box = renderExpandedBox(expandedText, theme, status);
|
|
398
|
-
if (!gapBeforeExpanded) return box;
|
|
399
|
-
|
|
400
|
-
const container = new Container();
|
|
401
|
-
container.addChild(new Spacer(1));
|
|
402
|
-
container.addChild(box);
|
|
403
|
-
return container;
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function summarizeRead(result: AgentToolResult<unknown>): string | undefined {
|
|
408
|
-
if (hasImage(result)) return "image";
|
|
409
|
-
const lines = visibleLineCount(textContent(result));
|
|
410
|
-
const truncated = isResultTruncated(result);
|
|
411
|
-
return lines > 0 ? `${plural(lines, "line")}${truncated ? ", truncated" : ""}` : undefined;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function summarizeBash(
|
|
415
|
-
result: AgentToolResult<unknown>,
|
|
416
|
-
context: CompactRenderContext,
|
|
417
|
-
): string | undefined {
|
|
418
|
-
const output = textContent(result);
|
|
419
|
-
if (context.isError) {
|
|
420
|
-
const exit = output.match(/Command exited with code (\d+)/i)?.[1];
|
|
421
|
-
if (exit) return `exit ${exit}`;
|
|
422
|
-
}
|
|
423
|
-
const lines = visibleLineCount(output);
|
|
424
|
-
const truncated = isResultTruncated(result);
|
|
425
|
-
if (lines === 0 || output.trim() === "(no output)") return "done";
|
|
426
|
-
return `${plural(lines, "line")}${truncated ? ", truncated" : ""}`;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function summarizeEdit(result: AgentToolResult<unknown>): string | undefined {
|
|
430
|
-
const diff = resultDetails(result).diff;
|
|
431
|
-
if (typeof diff !== "string") return "done";
|
|
432
|
-
let additions = 0;
|
|
433
|
-
let removals = 0;
|
|
434
|
-
for (const line of diff.split("\n")) {
|
|
435
|
-
if (line.startsWith("+") && !line.startsWith("+++")) additions++;
|
|
436
|
-
if (line.startsWith("-") && !line.startsWith("---")) removals++;
|
|
437
|
-
}
|
|
438
|
-
return `+${additions}/-${removals}`;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function summarizeWrite(): string {
|
|
442
|
-
return "written";
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function summarizeCount(noun: string, many = `${noun}s`) {
|
|
446
|
-
return (result: AgentToolResult<unknown>): string | undefined => {
|
|
447
|
-
const count = visibleLineCount(textContent(result));
|
|
448
|
-
if (count === 0) return `0 ${many}`;
|
|
449
|
-
return plural(count, noun, many);
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
function registerCompactBuiltIn(
|
|
454
|
-
pi: ExtensionAPI,
|
|
455
|
-
name: keyof BuiltInDefinitions,
|
|
456
|
-
renderCall: CompactRenderCall,
|
|
457
|
-
renderResult: CompactRenderResult,
|
|
458
|
-
) {
|
|
459
|
-
const initialDefinition = getBuiltIns(process.cwd())[name] as ToolDefinition;
|
|
460
|
-
|
|
461
|
-
pi.registerTool({
|
|
462
|
-
...initialDefinition,
|
|
463
|
-
renderShell: "self",
|
|
464
|
-
async execute(
|
|
465
|
-
toolCallId: string,
|
|
466
|
-
params: Parameters<ToolDefinition["execute"]>[1],
|
|
467
|
-
signal: AbortSignal | undefined,
|
|
468
|
-
onUpdate: Parameters<ToolDefinition["execute"]>[3],
|
|
469
|
-
ctx: Parameters<ToolDefinition["execute"]>[4],
|
|
470
|
-
) {
|
|
471
|
-
const definition = getBuiltIns(ctx.cwd)[name] as ToolDefinition;
|
|
472
|
-
return definition.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
473
|
-
},
|
|
474
|
-
renderCall: renderCall as NonNullable<ToolDefinition["renderCall"]>,
|
|
475
|
-
renderResult: renderResult as NonNullable<ToolDefinition["renderResult"]>,
|
|
476
|
-
});
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
export function registerCompactTools(pi: ExtensionAPI) {
|
|
480
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
481
|
-
observedPiExpanded = ctx.ui.getToolsExpanded();
|
|
482
|
-
outputMode = configuredOutputMode(observedPiExpanded);
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
registerCompactBuiltIn(
|
|
486
|
-
pi,
|
|
487
|
-
"read",
|
|
488
|
-
compactCall((args, state) => {
|
|
489
|
-
const options: string[] = [];
|
|
490
|
-
if (args.offset !== undefined) options.push(`offset=${args.offset}`);
|
|
491
|
-
if (args.limit !== undefined) options.push(`limit=${args.limit}`);
|
|
492
|
-
return `→ Read ${shortPath(args.path)}${
|
|
493
|
-
options.length ? ` [${options.join(", ")}]` : ""
|
|
494
|
-
}${suffix(state)}`;
|
|
495
|
-
}),
|
|
496
|
-
compactResult(summarizeRead, renderReadExpanded),
|
|
497
|
-
);
|
|
498
|
-
|
|
499
|
-
registerCompactBuiltIn(
|
|
500
|
-
pi,
|
|
501
|
-
"bash",
|
|
502
|
-
compactBashCall,
|
|
503
|
-
compactResult(summarizeBash, renderBashExpanded),
|
|
504
|
-
);
|
|
505
|
-
|
|
506
|
-
registerCompactBuiltIn(
|
|
507
|
-
pi,
|
|
508
|
-
"edit",
|
|
509
|
-
compactCall((args, state) => {
|
|
510
|
-
const edits = Array.isArray(args.edits) ? ` [${plural(args.edits.length, "edit")}]` : "";
|
|
511
|
-
return `→ Edit ${shortPath(args.path)}${edits}${suffix(state)}`;
|
|
512
|
-
}),
|
|
513
|
-
compactResult(summarizeEdit, renderEditExpanded),
|
|
514
|
-
);
|
|
515
|
-
|
|
516
|
-
registerCompactBuiltIn(
|
|
517
|
-
pi,
|
|
518
|
-
"write",
|
|
519
|
-
compactCall((args, state) => {
|
|
520
|
-
const lines =
|
|
521
|
-
typeof args.content === "string"
|
|
522
|
-
? ` [${plural(args.content.split("\n").length, "line")}]`
|
|
523
|
-
: "";
|
|
524
|
-
return `→ Write ${shortPath(args.path)}${lines}${suffix(state)}`;
|
|
525
|
-
}),
|
|
526
|
-
compactResult(summarizeWrite, renderWriteExpanded),
|
|
527
|
-
);
|
|
528
|
-
|
|
529
|
-
registerCompactBuiltIn(
|
|
530
|
-
pi,
|
|
531
|
-
"find",
|
|
532
|
-
compactCall((args, state) => {
|
|
533
|
-
const limit = args.limit !== undefined ? ` [limit=${args.limit}]` : "";
|
|
534
|
-
return `* Glob ${quote(args.pattern)} in ${shortPath(args.path)}${limit}${suffix(state)}`;
|
|
535
|
-
}),
|
|
536
|
-
compactResult(summarizeCount("match", "matches")),
|
|
537
|
-
);
|
|
538
|
-
|
|
539
|
-
registerCompactBuiltIn(
|
|
540
|
-
pi,
|
|
541
|
-
"grep",
|
|
542
|
-
compactCall((args, state) => {
|
|
543
|
-
const parts: string[] = [];
|
|
544
|
-
if (args.glob) parts.push(`glob=${args.glob}`);
|
|
545
|
-
if (args.ignoreCase) parts.push("ignoreCase=true");
|
|
546
|
-
if (args.literal) parts.push("literal=true");
|
|
547
|
-
if (args.context !== undefined) parts.push(`context=${args.context}`);
|
|
548
|
-
if (args.limit !== undefined) parts.push(`limit=${args.limit}`);
|
|
549
|
-
return `* Grep ${quote(args.pattern)} in ${shortPath(args.path)}${
|
|
550
|
-
parts.length ? ` [${parts.join(", ")}]` : ""
|
|
551
|
-
}${suffix(state)}`;
|
|
552
|
-
}),
|
|
553
|
-
compactResult(summarizeCount("match", "matches")),
|
|
554
|
-
);
|
|
555
|
-
|
|
556
|
-
registerCompactBuiltIn(
|
|
557
|
-
pi,
|
|
558
|
-
"ls",
|
|
559
|
-
compactCall((args, state) => {
|
|
560
|
-
const limit = args.limit !== undefined ? ` [limit=${args.limit}]` : "";
|
|
561
|
-
return `→ List ${shortPath(args.path)}${limit}${suffix(state)}`;
|
|
562
|
-
}),
|
|
563
|
-
compactResult(summarizeCount("entry", "entries")),
|
|
564
|
-
);
|
|
565
|
-
}
|