pi-zentui 0.1.2 → 0.1.3
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 +26 -12
- package/extensions/zentui/compact-tools.ts +565 -0
- package/extensions/zentui/config.ts +44 -11
- package/extensions/zentui/index.ts +10 -30
- package/extensions/zentui/ui.ts +65 -33
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -86,7 +86,7 @@ On first run, Zentui creates a config file at:
|
|
|
86
86
|
{
|
|
87
87
|
"icons": {
|
|
88
88
|
"cwd": "",
|
|
89
|
-
"git": "",
|
|
89
|
+
"git": "",
|
|
90
90
|
"ahead": "↑",
|
|
91
91
|
"behind": "↓",
|
|
92
92
|
"diverged": "⇕",
|
|
@@ -109,10 +109,19 @@ On first run, Zentui creates a config file at:
|
|
|
109
109
|
"tokens": "muted",
|
|
110
110
|
"cost": "success",
|
|
111
111
|
"separator": "borderMuted"
|
|
112
|
+
},
|
|
113
|
+
"tools": {
|
|
114
|
+
"style": "compact"
|
|
112
115
|
}
|
|
113
116
|
}
|
|
114
117
|
```
|
|
115
118
|
|
|
119
|
+
`tools.style` controls the initial built-in tool output style:
|
|
120
|
+
|
|
121
|
+
- `compact` — one-line tool calls by default
|
|
122
|
+
- `truncated` — expanded preview with long output truncated
|
|
123
|
+
- `full` — expanded full output
|
|
124
|
+
|
|
116
125
|
### Color values
|
|
117
126
|
|
|
118
127
|
Colors can be:
|
|
@@ -129,23 +138,28 @@ This means Zentui works with any Pi theme — it uses your theme's colors by def
|
|
|
129
138
|
|
|
130
139
|
## Development
|
|
131
140
|
|
|
132
|
-
|
|
141
|
+
```bash
|
|
142
|
+
npm install
|
|
143
|
+
npm run verify
|
|
144
|
+
npm run fmt
|
|
145
|
+
npm run pack:check
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Test in Pi
|
|
149
|
+
|
|
150
|
+
The project keeps Pi core packages as peer dependencies for runtime and dev dependencies for
|
|
151
|
+
typechecking. To avoid accidentally running the local `node_modules/.bin/pi` shim, the dev scripts use
|
|
152
|
+
the globally installed Pi binary by default:
|
|
133
153
|
|
|
134
154
|
```bash
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
mise run verify
|
|
138
|
-
mise run fmt
|
|
139
|
-
mise run ci
|
|
155
|
+
npm run pi:dev
|
|
156
|
+
npm run pi:install-local
|
|
140
157
|
```
|
|
141
158
|
|
|
142
|
-
|
|
159
|
+
Override the binary if your Pi install is somewhere else:
|
|
143
160
|
|
|
144
161
|
```bash
|
|
145
|
-
npm
|
|
146
|
-
npm run verify
|
|
147
|
-
npm run fmt
|
|
148
|
-
npm run pack:check
|
|
162
|
+
PI_BIN=/path/to/pi npm run pi:dev
|
|
149
163
|
```
|
|
150
164
|
|
|
151
165
|
## Credits
|
|
@@ -0,0 +1,565 @@
|
|
|
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
|
+
}
|
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
4
4
|
|
|
5
5
|
export type ColorSpec = string;
|
|
6
|
+
export type ToolOutputStyle = "compact" | "truncated" | "full";
|
|
6
7
|
|
|
7
8
|
export type PolishedTuiConfig = {
|
|
8
9
|
icons: {
|
|
@@ -31,6 +32,9 @@ export type PolishedTuiConfig = {
|
|
|
31
32
|
cost: ColorSpec;
|
|
32
33
|
separator: ColorSpec;
|
|
33
34
|
};
|
|
35
|
+
tools: {
|
|
36
|
+
style: ToolOutputStyle;
|
|
37
|
+
};
|
|
34
38
|
};
|
|
35
39
|
|
|
36
40
|
export const configPath = join(getAgentDir(), "zentui.json");
|
|
@@ -110,6 +114,9 @@ export const defaultConfig: PolishedTuiConfig = {
|
|
|
110
114
|
cost: "success",
|
|
111
115
|
separator: "borderMuted",
|
|
112
116
|
},
|
|
117
|
+
tools: {
|
|
118
|
+
style: "compact",
|
|
119
|
+
},
|
|
113
120
|
};
|
|
114
121
|
|
|
115
122
|
function isHexColor(value: string): boolean {
|
|
@@ -128,6 +135,18 @@ type ThemeLike = {
|
|
|
128
135
|
fg(color: string, text: string): string;
|
|
129
136
|
};
|
|
130
137
|
|
|
138
|
+
type ConfigRecord = Record<string, unknown>;
|
|
139
|
+
|
|
140
|
+
function isRecord(value: unknown): value is ConfigRecord {
|
|
141
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function parseToolOutputStyle(value: unknown): ToolOutputStyle {
|
|
145
|
+
return value === "truncated" || value === "full" || value === "compact"
|
|
146
|
+
? value
|
|
147
|
+
: defaultConfig.tools.style;
|
|
148
|
+
}
|
|
149
|
+
|
|
131
150
|
export function colorize(theme: ThemeLike, color: ColorSpec, text: string): string {
|
|
132
151
|
if (themeColorTokens.has(color)) {
|
|
133
152
|
return theme.fg(color, text);
|
|
@@ -148,20 +167,34 @@ export function ensureConfigExists(): void {
|
|
|
148
167
|
}
|
|
149
168
|
}
|
|
150
169
|
|
|
170
|
+
export function mergeConfig(parsed: unknown): PolishedTuiConfig {
|
|
171
|
+
const config = isRecord(parsed) ? parsed : {};
|
|
172
|
+
const icons = isRecord(config.icons) ? (config.icons as Partial<PolishedTuiConfig["icons"]>) : {};
|
|
173
|
+
const colors = isRecord(config.colors)
|
|
174
|
+
? (config.colors as Partial<PolishedTuiConfig["colors"]>)
|
|
175
|
+
: {};
|
|
176
|
+
const tools = isRecord(config.tools) ? (config.tools as Partial<PolishedTuiConfig["tools"]>) : {};
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
icons: {
|
|
180
|
+
...defaultConfig.icons,
|
|
181
|
+
...icons,
|
|
182
|
+
},
|
|
183
|
+
colors: {
|
|
184
|
+
...defaultConfig.colors,
|
|
185
|
+
...colors,
|
|
186
|
+
},
|
|
187
|
+
tools: {
|
|
188
|
+
...defaultConfig.tools,
|
|
189
|
+
style: parseToolOutputStyle(tools.style),
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
151
194
|
export function loadConfig(): PolishedTuiConfig {
|
|
152
195
|
try {
|
|
153
196
|
if (!existsSync(configPath)) return defaultConfig;
|
|
154
|
-
|
|
155
|
-
return {
|
|
156
|
-
icons: {
|
|
157
|
-
...defaultConfig.icons,
|
|
158
|
-
...(parsed.icons ?? {}),
|
|
159
|
-
},
|
|
160
|
-
colors: {
|
|
161
|
-
...defaultConfig.colors,
|
|
162
|
-
...(parsed.colors ?? {}),
|
|
163
|
-
},
|
|
164
|
-
};
|
|
197
|
+
return mergeConfig(JSON.parse(readFileSync(configPath, "utf8")));
|
|
165
198
|
} catch {
|
|
166
199
|
return defaultConfig;
|
|
167
200
|
}
|
|
@@ -6,13 +6,13 @@ 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";
|
|
9
10
|
import { type PolishedTuiConfig, colorize, ensureConfigExists, loadConfig } from "./config";
|
|
10
11
|
import { type GitStatusSummary, emptyGitStatus, readGitStatus } from "./git";
|
|
11
12
|
import { type RuntimeInfo, readRuntimeInfo } from "./runtime";
|
|
12
|
-
import { PolishedEditor, patchUserMessageComponent } from "./ui";
|
|
13
|
+
import { PolishedEditor, patchUserMessageComponent, restoreUserMessageComponent } from "./ui";
|
|
13
14
|
|
|
14
15
|
type FooterState = GitStatusSummary & {
|
|
15
|
-
busy: boolean;
|
|
16
16
|
modelLabel: string;
|
|
17
17
|
providerLabel: string;
|
|
18
18
|
contextLabel: string;
|
|
@@ -127,8 +127,9 @@ function formatCwdLabel(cwd: string, cwdIcon: string): string {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
export default function (pi: ExtensionAPI) {
|
|
130
|
+
registerCompactTools(pi);
|
|
131
|
+
|
|
130
132
|
const state: FooterState = {
|
|
131
|
-
busy: false,
|
|
132
133
|
modelLabel: "no-model",
|
|
133
134
|
providerLabel: "Unknown",
|
|
134
135
|
contextLabel: "--",
|
|
@@ -267,15 +268,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
267
268
|
const installEditor = (ctx: ExtensionContext) => {
|
|
268
269
|
syncState(ctx);
|
|
269
270
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
type AutocompleteEditorInternals = {
|
|
274
|
-
autocompleteProvider?: unknown;
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
const editorFactory = (tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => {
|
|
278
|
-
const editor = new PolishedEditor(
|
|
271
|
+
const editorFactory = (tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) =>
|
|
272
|
+
new PolishedEditor(
|
|
279
273
|
tui,
|
|
280
274
|
theme,
|
|
281
275
|
keybindings,
|
|
@@ -287,22 +281,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
287
281
|
].join(ctx.ui.theme.fg("borderMuted", " ")),
|
|
288
282
|
() => pi.getThinkingLevel(),
|
|
289
283
|
);
|
|
290
|
-
currentEditor = editor;
|
|
291
|
-
|
|
292
|
-
const originalHandleInput = editor.handleInput.bind(editor);
|
|
293
|
-
editor.handleInput = (data: string) => {
|
|
294
|
-
const editorInternals = editor as unknown as AutocompleteEditorInternals;
|
|
295
|
-
if (!autocompleteFixed && !editorInternals.autocompleteProvider) {
|
|
296
|
-
autocompleteFixed = true;
|
|
297
|
-
ctx.ui.setEditorComponent(editorFactory);
|
|
298
|
-
currentEditor?.handleInput(data);
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
originalHandleInput(data);
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
return editor;
|
|
305
|
-
};
|
|
306
284
|
|
|
307
285
|
ctx.ui.setEditorComponent(editorFactory);
|
|
308
286
|
};
|
|
@@ -321,14 +299,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
321
299
|
installUi(ctx);
|
|
322
300
|
});
|
|
323
301
|
|
|
302
|
+
pi.on("session_shutdown", async () => {
|
|
303
|
+
restoreUserMessageComponent();
|
|
304
|
+
});
|
|
305
|
+
|
|
324
306
|
pi.on("agent_start", async (_event, ctx) => {
|
|
325
|
-
state.busy = true;
|
|
326
307
|
syncState(ctx);
|
|
327
308
|
refresh();
|
|
328
309
|
});
|
|
329
310
|
|
|
330
311
|
pi.on("agent_end", async (_event, ctx) => {
|
|
331
|
-
state.busy = false;
|
|
332
312
|
syncState(ctx);
|
|
333
313
|
scheduleProjectRefresh(ctx);
|
|
334
314
|
refresh();
|
package/extensions/zentui/ui.ts
CHANGED
|
@@ -16,13 +16,22 @@ import {
|
|
|
16
16
|
const OSC133_ZONE_START = "\x1b]133;A\x07";
|
|
17
17
|
const OSC133_ZONE_END = "\x1b]133;B\x07";
|
|
18
18
|
const OSC133_ZONE_FINAL = "\x1b]133;C\x07";
|
|
19
|
-
const
|
|
19
|
+
const userMessagePatchKey = "__zentuiUserMessagePatch";
|
|
20
20
|
|
|
21
21
|
type AutocompleteEditorInternals = {
|
|
22
22
|
autocompleteList?: Pick<Component, "render">;
|
|
23
23
|
isShowingAutocomplete?: () => boolean;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
type UserMessageRender = (this: UserMessageComponent, width: number) => string[];
|
|
27
|
+
type UserMessagePatchState = {
|
|
28
|
+
originalRender: UserMessageRender;
|
|
29
|
+
patchedRender: UserMessageRender;
|
|
30
|
+
};
|
|
31
|
+
type PatchableUserMessagePrototype = typeof UserMessageComponent.prototype & {
|
|
32
|
+
[userMessagePatchKey]?: UserMessagePatchState;
|
|
33
|
+
};
|
|
34
|
+
|
|
26
35
|
let currentUiTheme: Theme | undefined;
|
|
27
36
|
|
|
28
37
|
const TRUECOLOR_BACKGROUND_ANSI = /\x1b\[48;2;\d+;\d+;\d+m/g;
|
|
@@ -36,51 +45,74 @@ function stripBackgroundAnsi(text: string): string {
|
|
|
36
45
|
.replace(SIMPLE_BACKGROUND_ANSI, "");
|
|
37
46
|
}
|
|
38
47
|
|
|
39
|
-
function fillStyledLine(
|
|
40
|
-
content: string,
|
|
41
|
-
width: number,
|
|
42
|
-
background?: (text: string) => string,
|
|
43
|
-
): string {
|
|
48
|
+
function fillStyledLine(content: string, width: number): string {
|
|
44
49
|
const truncated = truncateToWidth(stripBackgroundAnsi(content), width, "");
|
|
45
50
|
const padWidth = Math.max(0, width - visibleWidth(truncated));
|
|
46
|
-
const pad =
|
|
47
|
-
padWidth > 0 ? (background ? background(" ".repeat(padWidth)) : " ".repeat(padWidth)) : "";
|
|
51
|
+
const pad = padWidth > 0 ? " ".repeat(padWidth) : "";
|
|
48
52
|
return `${truncated}${pad}`;
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
function userMessagePrototype(): PatchableUserMessagePrototype {
|
|
56
|
+
return UserMessageComponent.prototype as PatchableUserMessagePrototype;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function renderPatchedUserMessage(this: UserMessageComponent, width: number): string[] {
|
|
60
|
+
const originalRender = userMessagePrototype()[userMessagePatchKey]?.originalRender;
|
|
61
|
+
if (!currentUiTheme || !originalRender) {
|
|
62
|
+
return originalRender
|
|
63
|
+
? originalRender.call(this, width)
|
|
64
|
+
: (Container.prototype.render.call(this, width) as string[]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const railWidth = 2;
|
|
68
|
+
const innerWidth = Math.max(1, width - railWidth);
|
|
69
|
+
const baseLines = Container.prototype.render.call(this, innerWidth) as string[];
|
|
70
|
+
if (baseLines.length === 0) return baseLines;
|
|
71
|
+
|
|
72
|
+
const hasLeadingSpacer = baseLines.length > 1 && visibleWidth(baseLines[0] ?? "") === 0;
|
|
73
|
+
const leadingLines = hasLeadingSpacer ? [baseLines[0] ?? ""] : [];
|
|
74
|
+
const contentLines = hasLeadingSpacer ? baseLines.slice(1) : baseLines;
|
|
75
|
+
const rail = `${currentUiTheme.fg("accent", "│")}\x1b[0m `;
|
|
76
|
+
const border = currentUiTheme.fg("border", "─".repeat(width));
|
|
77
|
+
const styledLines = contentLines.map((line) => `${rail}${fillStyledLine(line, innerWidth)}`);
|
|
78
|
+
|
|
79
|
+
if (styledLines.length === 0) {
|
|
80
|
+
return leadingLines;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const framedLines = [border, ...styledLines, border];
|
|
84
|
+
framedLines[0] = OSC133_ZONE_START + framedLines[0];
|
|
85
|
+
framedLines[framedLines.length - 1] =
|
|
86
|
+
framedLines[framedLines.length - 1] + OSC133_ZONE_END + OSC133_ZONE_FINAL;
|
|
87
|
+
return [...leadingLines, ...framedLines];
|
|
88
|
+
}
|
|
89
|
+
|
|
51
90
|
export function patchUserMessageComponent(uiTheme: Theme): void {
|
|
52
91
|
currentUiTheme = uiTheme;
|
|
53
92
|
|
|
54
|
-
const prototype =
|
|
55
|
-
|
|
93
|
+
const prototype = userMessagePrototype();
|
|
94
|
+
const patchState = prototype[userMessagePatchKey] ?? {
|
|
95
|
+
originalRender: prototype.render,
|
|
96
|
+
patchedRender: renderPatchedUserMessage,
|
|
56
97
|
};
|
|
57
|
-
|
|
58
|
-
if (!currentUiTheme) {
|
|
59
|
-
return originalUserMessageRender.call(this, width);
|
|
60
|
-
}
|
|
98
|
+
patchState.patchedRender = renderPatchedUserMessage;
|
|
61
99
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
100
|
+
Object.defineProperty(prototype, userMessagePatchKey, {
|
|
101
|
+
value: patchState,
|
|
102
|
+
configurable: true,
|
|
103
|
+
});
|
|
104
|
+
prototype.render = renderPatchedUserMessage;
|
|
105
|
+
}
|
|
66
106
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const contentLines = hasLeadingSpacer ? baseLines.slice(1) : baseLines;
|
|
70
|
-
const rail = `${currentUiTheme.fg("accent", "│")}\x1b[0m `;
|
|
71
|
-
const border = currentUiTheme.fg("border", "─".repeat(width));
|
|
72
|
-
const styledLines = contentLines.map((line) => `${rail}${fillStyledLine(line, innerWidth)}`);
|
|
107
|
+
export function restoreUserMessageComponent(): void {
|
|
108
|
+
currentUiTheme = undefined;
|
|
73
109
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
110
|
+
const prototype = userMessagePrototype();
|
|
111
|
+
const patchState = prototype[userMessagePatchKey];
|
|
112
|
+
if (!patchState || prototype.render !== patchState.patchedRender) return;
|
|
77
113
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
framedLines[framedLines.length - 1] =
|
|
81
|
-
framedLines[framedLines.length - 1] + OSC133_ZONE_END + OSC133_ZONE_FINAL;
|
|
82
|
-
return [...leadingLines, ...framedLines];
|
|
83
|
-
};
|
|
114
|
+
prototype.render = patchState.originalRender;
|
|
115
|
+
delete prototype[userMessagePatchKey];
|
|
84
116
|
}
|
|
85
117
|
|
|
86
118
|
export class PolishedEditor extends CustomEditor {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-zentui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "A Starship-inspired statusline and Opencode-style TUI for Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
"lint": "biome check .",
|
|
19
19
|
"typecheck": "tsc --noEmit",
|
|
20
20
|
"test": "vitest run",
|
|
21
|
+
"pi:dev": "${PI_BIN:-/opt/homebrew/bin/pi} --no-extensions -e ./extensions/zentui/index.ts",
|
|
22
|
+
"pi:install-local": "${PI_BIN:-/opt/homebrew/bin/pi} install ./ -l",
|
|
21
23
|
"fmt": "biome format --write .",
|
|
22
24
|
"fmt:check": "biome format --check .",
|
|
23
25
|
"verify": "npm run lint && npm run typecheck && npm run test",
|
|
@@ -38,9 +40,9 @@
|
|
|
38
40
|
},
|
|
39
41
|
"devDependencies": {
|
|
40
42
|
"@biomejs/biome": "^1.9.4",
|
|
41
|
-
"@mariozechner/pi-ai": "^0.
|
|
42
|
-
"@mariozechner/pi-coding-agent": "^0.
|
|
43
|
-
"@mariozechner/pi-tui": "^0.
|
|
43
|
+
"@mariozechner/pi-ai": "^0.73.0",
|
|
44
|
+
"@mariozechner/pi-coding-agent": "^0.73.0",
|
|
45
|
+
"@mariozechner/pi-tui": "^0.73.0",
|
|
44
46
|
"@types/node": "^25.5.2",
|
|
45
47
|
"typescript": "^6.0.2",
|
|
46
48
|
"vitest": "^3.2.4"
|