pi-tui-footer 1.2.6 → 1.2.7
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 +15 -0
- package/extensions/rounded-tools.ts +146 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,6 +43,21 @@ pi update --extensions
|
|
|
43
43
|
|
|
44
44
|
也可在 pi 内通过 `/pi-tui` 命令调整。
|
|
45
45
|
|
|
46
|
+
### 圆角工具框与第三方工具
|
|
47
|
+
|
|
48
|
+
`roundedTools` 开启时,除 8 个内置工具(read、write、edit、bash、powershell、grep、find、ls)外,也会自动拦截并为第三方扩展注册的工具(如 pi-fff 的 `ffgrep`、`fffind`、`fff-multi-grep`,或其 override 模式接管的 `grep`/`find`)添加圆角边框。
|
|
49
|
+
|
|
50
|
+
**加载顺序**:需要在 `~/.pi/agent/settings.json` 的 `packages` 中将 `pi-tui-footer` 排在第三方扩展(如 `@ff-labs/pi-fff`)**之前**,以确保 proxy 在工具注册前生效:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"packages": [
|
|
55
|
+
"npm:pi-tui-footer",
|
|
56
|
+
"npm:@ff-labs/pi-fff"
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
46
61
|
## 致谢
|
|
47
62
|
|
|
48
63
|
本项目是以下两个开源项目的深度融合集成:
|
|
@@ -31,10 +31,11 @@ import {
|
|
|
31
31
|
createFindToolDefinition,
|
|
32
32
|
createGrepToolDefinition,
|
|
33
33
|
createLsToolDefinition,
|
|
34
|
+
createPowerShellToolDefinition,
|
|
34
35
|
createReadToolDefinition,
|
|
35
36
|
createWriteToolDefinition,
|
|
36
37
|
} from "@earendil-works/pi-coding-agent";
|
|
37
|
-
import type { Component } from "@earendil-works/pi-tui";
|
|
38
|
+
import type { Component, TuiMouseEvent, TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
38
39
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
39
40
|
|
|
40
41
|
// ─── Rounded-corner frame ────────────────────────────────────────────────
|
|
@@ -53,6 +54,10 @@ class RoundedFrame implements Component {
|
|
|
53
54
|
private readonly mode: FrameMode = "closed",
|
|
54
55
|
) {}
|
|
55
56
|
|
|
57
|
+
getInner(): Component {
|
|
58
|
+
return this.inner;
|
|
59
|
+
}
|
|
60
|
+
|
|
56
61
|
invalidate(): void {
|
|
57
62
|
this.inner.invalidate?.();
|
|
58
63
|
this.cachedWidth = undefined;
|
|
@@ -60,6 +65,32 @@ class RoundedFrame implements Component {
|
|
|
60
65
|
this.cachedLines = undefined;
|
|
61
66
|
}
|
|
62
67
|
|
|
68
|
+
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
69
|
+
if (!this.inner.handleMouse) return undefined;
|
|
70
|
+
|
|
71
|
+
const width = this.cachedWidth ?? event.width;
|
|
72
|
+
if (width < 4) {
|
|
73
|
+
return this.inner.handleMouse(event);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const innerWidth = Math.max(1, width - 4);
|
|
77
|
+
const paddingTop = this.mode !== "open-top" ? 1 : 0;
|
|
78
|
+
const contentX = event.x - 2;
|
|
79
|
+
const contentY = event.y - paddingTop;
|
|
80
|
+
const innerHeight = this.cachedInnerLines?.length ?? 0;
|
|
81
|
+
|
|
82
|
+
if (contentX < 0 || contentX >= innerWidth || contentY < 0 || contentY >= innerHeight) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return this.inner.handleMouse({
|
|
87
|
+
...event,
|
|
88
|
+
x: contentX,
|
|
89
|
+
y: contentY,
|
|
90
|
+
width: innerWidth,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
63
94
|
// pi's built-in tool renderers cache the previously-returned component
|
|
64
95
|
// via `context.lastComponent` and call mutating methods on it
|
|
65
96
|
// (`Text.setText`, `Container.clear`/`addChild`, `invalidate`) so they
|
|
@@ -177,8 +208,8 @@ const frame = (
|
|
|
177
208
|
function borderColorFor(
|
|
178
209
|
context: { isPartial?: boolean; isError?: boolean } | undefined,
|
|
179
210
|
): string {
|
|
180
|
-
if (context?.isPartial) return "warning";
|
|
181
211
|
if (context?.isError) return "error";
|
|
212
|
+
if (context?.isPartial) return "warning";
|
|
182
213
|
return "border";
|
|
183
214
|
}
|
|
184
215
|
|
|
@@ -189,81 +220,153 @@ type ToolDef = ToolDefinition<any, any, any>;
|
|
|
189
220
|
const EMPTY_COMPONENT: Component = { render: () => [], invalidate: () => {} };
|
|
190
221
|
|
|
191
222
|
/**
|
|
192
|
-
*
|
|
223
|
+
* Unwrap inner component from a RoundedFrame if context.lastComponent is wrapped,
|
|
224
|
+
* allowing underlying renderers (e.g. edit, bash, read) to preserve their component
|
|
225
|
+
* cache across renders instead of recreating on every tick.
|
|
226
|
+
*/
|
|
227
|
+
function unwrapInner(component: unknown): Component | undefined {
|
|
228
|
+
if (component instanceof RoundedFrame) {
|
|
229
|
+
return component.getInner();
|
|
230
|
+
}
|
|
231
|
+
return component as Component | undefined;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Wrap a tool definition in rounded frames.
|
|
193
236
|
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* `renderShell` are overwritten. This keeps the tool's LLM-facing prompt
|
|
198
|
-
* data intact, edit's argument pre-processing intact, etc.
|
|
237
|
+
* Preserves all metadata (promptSnippet, promptGuidelines, parameters, etc.)
|
|
238
|
+
* while wrapping renderCall and renderResult with rounded borders.
|
|
239
|
+
* Uses closed mode for tools without renderResult to avoid open bottom borders.
|
|
199
240
|
*/
|
|
200
241
|
function wrapBuiltin(def: ToolDef) {
|
|
242
|
+
const callMode: FrameMode = def.renderResult ? "open-bottom" : "closed";
|
|
201
243
|
return {
|
|
202
244
|
...def,
|
|
203
245
|
renderShell: "self" as const,
|
|
204
246
|
renderCall: (args: any, theme: any, context: any) => {
|
|
205
|
-
|
|
206
|
-
// doesn't treat a RoundedFrame as its own lastComponent — that
|
|
207
|
-
// would either throw (`Text.setText` is missing) or nest frames
|
|
208
|
-
// (`inner = previous RoundedFrame` → we re-wrap it). Forcing the
|
|
209
|
-
// inner to allocate fresh costs one Text/Container per render but
|
|
210
|
-
// keeps every render independent and CC-protect against future
|
|
211
|
-
// tool changes.
|
|
247
|
+
const unwrapped = unwrapInner(context?.lastComponent);
|
|
212
248
|
const inner: Component = def.renderCall
|
|
213
|
-
? def.renderCall(args, theme, { ...context, lastComponent:
|
|
249
|
+
? def.renderCall(args, theme, { ...context, lastComponent: unwrapped })
|
|
214
250
|
: EMPTY_COMPONENT;
|
|
215
|
-
|
|
216
|
-
// timer). Skip the frame then — keeping its borders stable avoids
|
|
217
|
-
// redraw flicker — and only wrap it once the tool settles.
|
|
218
|
-
if (context?.isPartial) return inner;
|
|
219
|
-
return frame(inner, theme, "open-bottom", borderColorFor(context));
|
|
251
|
+
return frame(inner, theme, callMode, borderColorFor(context));
|
|
220
252
|
},
|
|
221
253
|
renderResult: (result: any, options: any, theme: any, context: any) => {
|
|
254
|
+
const unwrapped = unwrapInner(context?.lastComponent);
|
|
222
255
|
const inner: Component = def.renderResult
|
|
223
|
-
? def.renderResult(result, options, theme, { ...context, lastComponent:
|
|
256
|
+
? def.renderResult(result, options, theme, { ...context, lastComponent: unwrapped })
|
|
224
257
|
: EMPTY_COMPONENT;
|
|
225
|
-
if (context?.isPartial) return inner;
|
|
226
258
|
return frame(inner, theme, "open-top", borderColorFor(context));
|
|
227
259
|
},
|
|
228
260
|
};
|
|
229
261
|
}
|
|
230
262
|
|
|
231
263
|
/**
|
|
232
|
-
* Register
|
|
233
|
-
* (`enabled`) or with
|
|
234
|
-
*
|
|
235
|
-
*
|
|
264
|
+
* Register built-in tools (read, write, edit, bash, powershell, grep, find, ls),
|
|
265
|
+
* either with rounded frames (`enabled`) or with stock definitions (`!enabled`).
|
|
266
|
+
*
|
|
267
|
+
* If a third-party extension has already replaced a built-in tool (e.g. pi-fff
|
|
268
|
+
* overriding grep/find), that tool is skipped here to prevent clobbering.
|
|
236
269
|
*/
|
|
237
|
-
export function registerRoundedTools(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
270
|
+
export function registerRoundedTools(
|
|
271
|
+
target: ExtensionAPI | ((tool: ToolDef) => void),
|
|
272
|
+
enabled: boolean,
|
|
273
|
+
cwd: string,
|
|
274
|
+
captured?: Map<string, ToolDef>,
|
|
275
|
+
): void {
|
|
276
|
+
const register = typeof target === "function" ? target : target.registerTool.bind(target);
|
|
277
|
+
|
|
278
|
+
const builtins: Record<string, () => ToolDef> = {
|
|
279
|
+
read: () => createReadToolDefinition(cwd),
|
|
280
|
+
write: () => createWriteToolDefinition(cwd),
|
|
281
|
+
edit: () => createEditToolDefinition(cwd),
|
|
282
|
+
bash: () => createBashToolDefinition(cwd),
|
|
283
|
+
powershell: () => createPowerShellToolDefinition(cwd),
|
|
284
|
+
ls: () => createLsToolDefinition(cwd),
|
|
285
|
+
grep: () => createGrepToolDefinition(cwd),
|
|
286
|
+
find: () => createFindToolDefinition(cwd),
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
for (const [name, factory] of Object.entries(builtins)) {
|
|
290
|
+
if (!captured?.has(name)) {
|
|
291
|
+
register(enabled ? wrapBuiltin(factory()) : factory());
|
|
292
|
+
}
|
|
250
293
|
}
|
|
251
294
|
}
|
|
252
295
|
|
|
253
296
|
/**
|
|
254
297
|
* Manager class encapsulating rounded tools registration and lifecycle updates.
|
|
298
|
+
*
|
|
299
|
+
* In addition to wrapping built-in tools, it installs a `pi.registerTool` proxy
|
|
300
|
+
* so that any tool registered by third-party extensions (e.g. pi-fff's ffgrep,
|
|
301
|
+
* fffind, fff-multi-grep, or overridden grep/find) automatically receives rounded
|
|
302
|
+
* frames when `roundedTools` is enabled, without maintaining a fragile hardcoded
|
|
303
|
+
* name whitelist.
|
|
304
|
+
*
|
|
305
|
+
* Load-order note: For extensions that register tools at load time, place
|
|
306
|
+
* `pi-tui-footer` before those packages in `~/.pi/agent/settings.json` so the
|
|
307
|
+
* proxy is active when they load.
|
|
255
308
|
*/
|
|
256
309
|
export class RoundedToolsManager {
|
|
257
|
-
|
|
310
|
+
private enabled = false;
|
|
311
|
+
private proxyInstalled = false;
|
|
312
|
+
/** The real `pi.registerTool` before we installed the proxy. */
|
|
313
|
+
private originalRegisterTool!: (tool: ToolDef) => void;
|
|
314
|
+
/** Original (unwrapped) definitions captured from third-party tools. */
|
|
315
|
+
private readonly captured = new Map<string, ToolDef>();
|
|
316
|
+
|
|
317
|
+
constructor(private readonly pi: ExtensionAPI) {
|
|
318
|
+
this.installProxy();
|
|
319
|
+
}
|
|
258
320
|
|
|
259
321
|
/** Register initial tools before session_start with fallback cwd. */
|
|
260
322
|
init(enabled: boolean, cwd = process.cwd()): void {
|
|
261
|
-
|
|
323
|
+
this.enabled = enabled;
|
|
324
|
+
this.installProxy();
|
|
325
|
+
registerRoundedTools(this.originalRegisterTool, enabled, cwd, this.captured);
|
|
326
|
+
this.reRegisterCaptured();
|
|
262
327
|
}
|
|
263
328
|
|
|
264
329
|
/** Apply tools with current session cwd. */
|
|
265
330
|
apply(enabled: boolean, cwd: string): void {
|
|
266
|
-
|
|
331
|
+
this.enabled = enabled;
|
|
332
|
+
this.installProxy();
|
|
333
|
+
registerRoundedTools(this.originalRegisterTool, enabled, cwd, this.captured);
|
|
334
|
+
this.reRegisterCaptured();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Install a one-time proxy on `pi.registerTool` to intercept third-party
|
|
339
|
+
* tool registrations.
|
|
340
|
+
*/
|
|
341
|
+
private installProxy(): void {
|
|
342
|
+
if (this.proxyInstalled) return;
|
|
343
|
+
this.proxyInstalled = true;
|
|
344
|
+
|
|
345
|
+
this.originalRegisterTool = this.pi.registerTool.bind(this.pi);
|
|
346
|
+
const original = this.originalRegisterTool;
|
|
347
|
+
const self = this;
|
|
348
|
+
|
|
349
|
+
this.pi.registerTool = function proxyRegisterTool(tool: ToolDef) {
|
|
350
|
+
// Don't intercept tools that already render their own shell
|
|
351
|
+
if (tool.renderShell === "self") {
|
|
352
|
+
original(tool);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Capture the unwrapped original definition
|
|
357
|
+
self.captured.set(tool.name, tool);
|
|
358
|
+
original(self.enabled ? wrapBuiltin(tool) : tool);
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Re-register all captured third-party tools with or without rounded
|
|
364
|
+
* frames based on the current `enabled` state.
|
|
365
|
+
*/
|
|
366
|
+
private reRegisterCaptured(): void {
|
|
367
|
+
for (const def of this.captured.values()) {
|
|
368
|
+
this.originalRegisterTool(this.enabled ? wrapBuiltin(def) : def);
|
|
369
|
+
}
|
|
267
370
|
}
|
|
268
371
|
}
|
|
269
372
|
|