pi-tui-footer 1.2.9 → 1.3.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 +11 -13
- package/extensions/index.ts +11 -16
- package/extensions/rounded-tools.ts +66 -116
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ pi install git:github.com/onenora/pi-tui-footer
|
|
|
17
17
|
固定版本:
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
pi install git:github.com/onenora/pi-tui-footer@v1.
|
|
20
|
+
pi install git:github.com/onenora/pi-tui-footer@v1.3.0
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
更新:
|
|
@@ -43,24 +43,22 @@ pi update --extensions
|
|
|
43
43
|
|
|
44
44
|
也可在 pi 内通过 `/pi-tui` 命令调整。
|
|
45
45
|
|
|
46
|
-
###
|
|
46
|
+
### 圆角工具框
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
需要 pi >= 1.0.4(依赖 `pi.registerToolRenderer()`)。旧版本不会报错,但圆角工具框不生效。
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
`roundedTools` 通过渲染器解析器包装工具调用/结果的显示,不重新注册工具,不改变工具定义、执行逻辑和激活状态。覆盖范围:
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
```
|
|
52
|
+
- 内置工具:read、write、bash、powershell、grep、find、ls(edit 自带外框,保持原样)
|
|
53
|
+
- pi-fff 工具:ffgrep、fffind、fff-multi-grep
|
|
54
|
+
|
|
55
|
+
按工具名匹配,与扩展加载顺序无关。其他第三方工具不会自动添加圆角边框;需要时将工具名加入 `extensions/rounded-tools.ts` 的 `ROUNDED_TOOL_NAMES`。
|
|
56
|
+
|
|
57
|
+
切换 `roundedTools` 后,新的工具调用立即生效,已渲染的历史记录在下次重建(`/reload`、恢复会话、压缩)时更新。
|
|
60
58
|
|
|
61
59
|
## 致谢
|
|
62
60
|
|
|
63
|
-
|
|
61
|
+
本项目can以下两个开源项目的集成:
|
|
64
62
|
|
|
65
63
|
- [pi-open-tui](https://github.com/OldSuns/pi-open-tui) — 头部、底部、编辑器、遥测和设置界面的整体结构
|
|
66
64
|
- [pi-rounded-tools](https://github.com/orionpax1997/pi-rounded-tools) — 圆角工具调用/结果框
|
package/extensions/index.ts
CHANGED
|
@@ -224,21 +224,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
224
224
|
requestFooterRender?.();
|
|
225
225
|
};
|
|
226
226
|
|
|
227
|
-
const applyRoundedTools = (
|
|
228
|
-
roundedTools.
|
|
227
|
+
const applyRoundedTools = () => {
|
|
228
|
+
roundedTools.setEnabled(config.enabled && config.roundedTools);
|
|
229
229
|
};
|
|
230
230
|
|
|
231
|
-
//
|
|
232
|
-
// /reload transcript rebuild
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
// components keep their current frames until the next transcript rebuild
|
|
238
|
-
// (compaction / resume / tree navigation / thinking-block toggle).
|
|
239
|
-
const initialConfig = loadConfig();
|
|
240
|
-
config = initialConfig;
|
|
241
|
-
roundedTools.init(initialConfig.enabled && initialConfig.roundedTools);
|
|
231
|
+
// The renderer resolver is installed when RoundedToolsManager is constructed
|
|
232
|
+
// (before session_start), so a /reload transcript rebuild already uses it.
|
|
233
|
+
// Toggling the setting affects components created afterwards; already-rendered
|
|
234
|
+
// historical components keep their frames until the next transcript rebuild.
|
|
235
|
+
config = loadConfig();
|
|
236
|
+
applyRoundedTools();
|
|
242
237
|
|
|
243
238
|
const refreshInteractiveState = (ctx: ExtensionContext, project = false) => {
|
|
244
239
|
if (!sessionLifecycle.isCurrent() || !ctx.hasUI) return;
|
|
@@ -280,7 +275,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
280
275
|
clearPeekLabel(ctx);
|
|
281
276
|
|
|
282
277
|
applyUi(ctx);
|
|
283
|
-
applyRoundedTools(
|
|
278
|
+
applyRoundedTools();
|
|
284
279
|
|
|
285
280
|
refreshInteractiveState(ctx, true);
|
|
286
281
|
});
|
|
@@ -437,8 +432,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
437
432
|
if (lastCtx) {
|
|
438
433
|
pendingUiChange = getPendingUiChange(newConfig.enabled, active);
|
|
439
434
|
}
|
|
440
|
-
if (
|
|
441
|
-
applyRoundedTools(
|
|
435
|
+
if (wasRoundedTools !== newConfig.roundedTools || wasEnabled !== newConfig.enabled) {
|
|
436
|
+
applyRoundedTools();
|
|
442
437
|
}
|
|
443
438
|
const gitNeeded = newConfig.footerSegments.gitBranch || newConfig.footerSegments.gitStatus || newConfig.footerSegments.gitCommit;
|
|
444
439
|
if (lastCtx && gitNeeded) {
|
|
@@ -1,31 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Rounded tool frames — rounded frames for tool calls and results.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Uses `pi.registerToolRenderer()` to wrap the renderers of built-in tools
|
|
5
|
+
* (read, write, bash, powershell, grep, find, ls, plus pi-fff's ffgrep, fffind,
|
|
6
|
+
* fff-multi-grep) with `renderShell: "self"`
|
|
7
|
+
* and a frame drawn with Unicode rounded-corner characters (╭ ╮ ╰ ╯ ─ │).
|
|
8
|
+
* Tool definitions, execution and active-tool state are left untouched.
|
|
7
9
|
*
|
|
8
|
-
* Tools that already render their own shell (e.g. `edit`
|
|
9
|
-
*
|
|
10
|
+
* Tools that already render their own shell (e.g. `edit`) are preserved as-is
|
|
11
|
+
* to avoid duplicate nesting and UI glitches.
|
|
10
12
|
*
|
|
11
13
|
* Border color follows `theme.fg("border", ...)` so it adapts to your theme,
|
|
12
14
|
* using warning (yellow) during pending/streaming execution and error (red) on failure.
|
|
13
|
-
*
|
|
14
|
-
* Non-default tools (grep, find, ls, powershell) preserve `defaultActive: false`
|
|
15
|
-
* to avoid polluting the model's active toolset.
|
|
16
15
|
*/
|
|
17
16
|
|
|
18
|
-
import type { ExtensionAPI,
|
|
19
|
-
import {
|
|
20
|
-
createBashToolDefinition,
|
|
21
|
-
createEditToolDefinition,
|
|
22
|
-
createFindToolDefinition,
|
|
23
|
-
createGrepToolDefinition,
|
|
24
|
-
createLsToolDefinition,
|
|
25
|
-
createPowerShellToolDefinition,
|
|
26
|
-
createReadToolDefinition,
|
|
27
|
-
createWriteToolDefinition,
|
|
28
|
-
} from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import type { ExtensionAPI, ToolRenderers } from "@earendil-works/pi-coding-agent";
|
|
29
18
|
import type { Component, TuiMouseEvent, TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
30
19
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
31
20
|
|
|
@@ -38,6 +27,18 @@ import { visibleWidth } from "@earendil-works/pi-tui";
|
|
|
38
27
|
|
|
39
28
|
export type FrameMode = "closed" | "open-bottom" | "open-top";
|
|
40
29
|
|
|
30
|
+
// ─── Border color ────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
type RenderContext = { isPartial?: boolean; isError?: boolean };
|
|
33
|
+
|
|
34
|
+
function borderColorFor(context: RenderContext | undefined): string {
|
|
35
|
+
if (context?.isError) return "error";
|
|
36
|
+
if (context?.isPartial) return "warning";
|
|
37
|
+
return "border";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── RoundedFrame component ─────────────────────────────────────────────
|
|
41
|
+
|
|
41
42
|
export class RoundedFrame implements Component {
|
|
42
43
|
private cachedWidth: number | undefined;
|
|
43
44
|
private cachedInnerLines: string[] | undefined;
|
|
@@ -142,9 +143,8 @@ export class RoundedFrame implements Component {
|
|
|
142
143
|
return this.cachedLines;
|
|
143
144
|
}
|
|
144
145
|
|
|
145
|
-
const horizontal = "─".repeat(
|
|
146
|
+
const horizontal = "─".repeat(width - 2);
|
|
146
147
|
const side = this.border("│");
|
|
147
|
-
|
|
148
148
|
const out: string[] = [];
|
|
149
149
|
|
|
150
150
|
if (this.mode !== "open-top") {
|
|
@@ -153,8 +153,7 @@ export class RoundedFrame implements Component {
|
|
|
153
153
|
|
|
154
154
|
if (innerLines.length > 0) {
|
|
155
155
|
for (const line of innerLines) {
|
|
156
|
-
const
|
|
157
|
-
const pad = " ".repeat(Math.max(0, innerWidth - vis));
|
|
156
|
+
const pad = " ".repeat(Math.max(0, innerWidth - visibleWidth(line)));
|
|
158
157
|
out.push(side + " " + line + pad + " " + side);
|
|
159
158
|
}
|
|
160
159
|
} else if (this.mode === "closed") {
|
|
@@ -164,6 +163,7 @@ export class RoundedFrame implements Component {
|
|
|
164
163
|
if (this.mode !== "open-bottom") {
|
|
165
164
|
out.push(this.border("╰" + horizontal + "╯"));
|
|
166
165
|
}
|
|
166
|
+
|
|
167
167
|
this.cachedWidth = width;
|
|
168
168
|
this.cachedInnerLines = innerLines;
|
|
169
169
|
this.cachedLines = out;
|
|
@@ -171,35 +171,38 @@ export class RoundedFrame implements Component {
|
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
|
|
174
|
+
// ─── Frame factories ────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
const createFrame = (
|
|
175
177
|
inner: Component,
|
|
176
178
|
theme: { fg: (color: string, text: string) => string },
|
|
177
179
|
mode: FrameMode = "closed",
|
|
178
180
|
colorKey: string = "border",
|
|
179
181
|
): RoundedFrame => new RoundedFrame(inner, (t) => theme.fg(colorKey, t), mode, colorKey);
|
|
180
182
|
|
|
181
|
-
function borderColorFor(
|
|
182
|
-
context: { isPartial?: boolean; isError?: boolean } | undefined,
|
|
183
|
-
): string {
|
|
184
|
-
if (context?.isError) return "error";
|
|
185
|
-
if (context?.isPartial) return "warning";
|
|
186
|
-
return "border";
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
190
|
-
|
|
191
|
-
type ToolDef = ToolDefinition<any, any, any>;
|
|
192
|
-
|
|
193
183
|
const EMPTY_COMPONENT: Component = { render: () => [], invalidate: () => {} };
|
|
194
184
|
|
|
185
|
+
// ─── Tool wrapping ──────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
const ROUNDED_TOOL_NAMES: ReadonlySet<string> = new Set([
|
|
188
|
+
"read",
|
|
189
|
+
"write",
|
|
190
|
+
"bash",
|
|
191
|
+
"powershell",
|
|
192
|
+
"grep",
|
|
193
|
+
"find",
|
|
194
|
+
"ls",
|
|
195
|
+
// pi-fff (@ff-labs/pi-fff) tools; resolvers match by name, so load order is irrelevant.
|
|
196
|
+
"ffgrep",
|
|
197
|
+
"fffind",
|
|
198
|
+
"fff-multi-grep",
|
|
199
|
+
]);
|
|
200
|
+
|
|
195
201
|
function unwrapInner(component: unknown): Component | undefined {
|
|
196
|
-
|
|
197
|
-
return component.getInner();
|
|
198
|
-
}
|
|
199
|
-
return component as Component | undefined;
|
|
202
|
+
return component instanceof RoundedFrame ? component.getInner() : (component as Component | undefined);
|
|
200
203
|
}
|
|
201
204
|
|
|
202
|
-
export function
|
|
205
|
+
export function wrapRenderers(def: ToolRenderers): ToolRenderers {
|
|
203
206
|
// Tools that already manage their own shell (e.g. edit) should remain untouched.
|
|
204
207
|
if (def.renderShell === "self") {
|
|
205
208
|
return def;
|
|
@@ -218,13 +221,14 @@ export function wrapBuiltin(def: ToolDef): ToolDef {
|
|
|
218
221
|
// close the bottom border so it renders as a complete box instead of an open frame.
|
|
219
222
|
const isPendingWithoutResult = Boolean(context?.isPartial && !context?.state?.__hasResult);
|
|
220
223
|
const mode: FrameMode = def.renderResult && !isPendingWithoutResult ? "open-bottom" : "closed";
|
|
221
|
-
const
|
|
224
|
+
const colorKey = borderColorFor(context);
|
|
225
|
+
const borderFn = (t: string) => theme.fg(colorKey, t);
|
|
222
226
|
|
|
223
227
|
let callFrame: RoundedFrame;
|
|
224
228
|
if (context?.lastComponent instanceof RoundedFrame) {
|
|
225
|
-
callFrame = context.lastComponent.update(inner, borderFn, mode,
|
|
229
|
+
callFrame = context.lastComponent.update(inner, borderFn, mode, colorKey);
|
|
226
230
|
} else {
|
|
227
|
-
callFrame =
|
|
231
|
+
callFrame = createFrame(inner, theme, mode, colorKey);
|
|
228
232
|
}
|
|
229
233
|
|
|
230
234
|
if (context?.state) {
|
|
@@ -246,91 +250,37 @@ export function wrapBuiltin(def: ToolDef): ToolDef {
|
|
|
246
250
|
const inner: Component = def.renderResult
|
|
247
251
|
? def.renderResult(result, options, theme, { ...context, lastComponent: unwrapped })
|
|
248
252
|
: EMPTY_COMPONENT;
|
|
249
|
-
const
|
|
253
|
+
const colorKey = borderColorFor(context);
|
|
254
|
+
const borderFn = (t: string) => theme.fg(colorKey, t);
|
|
250
255
|
|
|
251
256
|
if (context?.lastComponent instanceof RoundedFrame) {
|
|
252
|
-
return context.lastComponent.update(inner, borderFn, "open-top",
|
|
257
|
+
return context.lastComponent.update(inner, borderFn, "open-top", colorKey);
|
|
253
258
|
}
|
|
254
|
-
return
|
|
259
|
+
return createFrame(inner, theme, "open-top", colorKey);
|
|
255
260
|
},
|
|
256
261
|
};
|
|
257
262
|
}
|
|
258
263
|
|
|
259
|
-
|
|
260
|
-
target: ExtensionAPI | ((tool: ToolDef) => void),
|
|
261
|
-
enabled: boolean,
|
|
262
|
-
cwd: string,
|
|
263
|
-
_captured?: Map<string, ToolDef>,
|
|
264
|
-
): void {
|
|
265
|
-
const register = typeof target === "function" ? target : target.registerTool.bind(target);
|
|
266
|
-
|
|
267
|
-
let settings: any;
|
|
268
|
-
if (typeof target !== "function" && target.getSettings) {
|
|
269
|
-
try {
|
|
270
|
-
settings = target.getSettings();
|
|
271
|
-
} catch {
|
|
272
|
-
// runtime not initialized yet during early load
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
const autoResizeImages = settings?.images?.autoResize ?? true;
|
|
276
|
-
const shellCommandPrefix = settings?.shellCommandPrefix;
|
|
277
|
-
const shellPath = settings?.shellPath;
|
|
278
|
-
|
|
279
|
-
let activeToolNames: Set<string> | undefined;
|
|
280
|
-
if (typeof target !== "function" && target.getActiveTools) {
|
|
281
|
-
try {
|
|
282
|
-
const active = target.getActiveTools();
|
|
283
|
-
if (Array.isArray(active)) {
|
|
284
|
-
activeToolNames = new Set(active);
|
|
285
|
-
}
|
|
286
|
-
} catch {
|
|
287
|
-
// runtime not initialized yet during early load
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
const defaultActiveNames = activeToolNames ?? new Set(["read", "bash", "edit", "write"]);
|
|
291
|
-
|
|
292
|
-
const builtins: Record<string, () => ToolDef> = {
|
|
293
|
-
read: () => createReadToolDefinition(cwd, { autoResizeImages }),
|
|
294
|
-
write: () => createWriteToolDefinition(cwd),
|
|
295
|
-
edit: () => createEditToolDefinition(cwd),
|
|
296
|
-
bash: () => createBashToolDefinition(cwd, { commandPrefix: shellCommandPrefix, shellPath }),
|
|
297
|
-
powershell: () => createPowerShellToolDefinition(cwd),
|
|
298
|
-
ls: () => createLsToolDefinition(cwd),
|
|
299
|
-
grep: () => createGrepToolDefinition(cwd),
|
|
300
|
-
find: () => createFindToolDefinition(cwd),
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
for (const [name, factory] of Object.entries(builtins)) {
|
|
304
|
-
const rawDef = factory();
|
|
305
|
-
// Skip tools that already manage their own shell framing (e.g. edit)
|
|
306
|
-
if (rawDef.renderShell === "self") {
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const shouldBeActive = defaultActiveNames.has(name);
|
|
311
|
-
const baseDef: ToolDef = {
|
|
312
|
-
...rawDef,
|
|
313
|
-
defaultActive: shouldBeActive,
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
register(enabled ? wrapBuiltin(baseDef) : baseDef);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
264
|
+
// ─── Manager ─────────────────────────────────────────────────────────────
|
|
319
265
|
|
|
320
266
|
export class RoundedToolsManager {
|
|
321
267
|
private enabled = false;
|
|
322
268
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
269
|
+
/**
|
|
270
|
+
* Install the renderer resolver once at extension load. Toggling `enabled`
|
|
271
|
+
* affects tool components created afterwards (resume / reload / new calls).
|
|
272
|
+
*/
|
|
273
|
+
constructor(pi: ExtensionAPI) {
|
|
274
|
+
pi.registerToolRenderer?.((toolName, next) => {
|
|
275
|
+
const base = next();
|
|
276
|
+
if (!base || !this.enabled || !ROUNDED_TOOL_NAMES.has(toolName)) return base;
|
|
277
|
+
// Nothing to frame: let pi use its own fallback rendering.
|
|
278
|
+
if (!base.renderCall && !base.renderResult) return base;
|
|
279
|
+
return wrapRenderers(base);
|
|
280
|
+
});
|
|
329
281
|
}
|
|
330
282
|
|
|
331
|
-
|
|
332
|
-
apply(enabled: boolean, cwd: string): void {
|
|
283
|
+
setEnabled(enabled: boolean): void {
|
|
333
284
|
this.enabled = enabled;
|
|
334
|
-
registerRoundedTools(this.pi, enabled, cwd);
|
|
335
285
|
}
|
|
336
286
|
}
|