pi-tui-footer 1.2.8 → 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 +76 -119
- 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;
|
|
@@ -47,6 +48,7 @@ export class RoundedFrame implements Component {
|
|
|
47
48
|
private inner: Component,
|
|
48
49
|
private border: (text: string) => string,
|
|
49
50
|
private mode: FrameMode = "closed",
|
|
51
|
+
private colorKey: string = "border",
|
|
50
52
|
) {}
|
|
51
53
|
|
|
52
54
|
getInner(): Component {
|
|
@@ -64,10 +66,16 @@ export class RoundedFrame implements Component {
|
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
update(
|
|
68
|
-
|
|
69
|
+
update(
|
|
70
|
+
inner: Component,
|
|
71
|
+
border: (text: string) => string,
|
|
72
|
+
mode: FrameMode,
|
|
73
|
+
colorKey: string,
|
|
74
|
+
): this {
|
|
75
|
+
if (this.inner !== inner || this.mode !== mode || this.colorKey !== colorKey) {
|
|
69
76
|
this.inner = inner;
|
|
70
77
|
this.mode = mode;
|
|
78
|
+
this.colorKey = colorKey;
|
|
71
79
|
this.invalidate();
|
|
72
80
|
}
|
|
73
81
|
this.border = border;
|
|
@@ -135,9 +143,8 @@ export class RoundedFrame implements Component {
|
|
|
135
143
|
return this.cachedLines;
|
|
136
144
|
}
|
|
137
145
|
|
|
138
|
-
const horizontal = "─".repeat(
|
|
146
|
+
const horizontal = "─".repeat(width - 2);
|
|
139
147
|
const side = this.border("│");
|
|
140
|
-
|
|
141
148
|
const out: string[] = [];
|
|
142
149
|
|
|
143
150
|
if (this.mode !== "open-top") {
|
|
@@ -146,8 +153,7 @@ export class RoundedFrame implements Component {
|
|
|
146
153
|
|
|
147
154
|
if (innerLines.length > 0) {
|
|
148
155
|
for (const line of innerLines) {
|
|
149
|
-
const
|
|
150
|
-
const pad = " ".repeat(Math.max(0, innerWidth - vis));
|
|
156
|
+
const pad = " ".repeat(Math.max(0, innerWidth - visibleWidth(line)));
|
|
151
157
|
out.push(side + " " + line + pad + " " + side);
|
|
152
158
|
}
|
|
153
159
|
} else if (this.mode === "closed") {
|
|
@@ -157,6 +163,7 @@ export class RoundedFrame implements Component {
|
|
|
157
163
|
if (this.mode !== "open-bottom") {
|
|
158
164
|
out.push(this.border("╰" + horizontal + "╯"));
|
|
159
165
|
}
|
|
166
|
+
|
|
160
167
|
this.cachedWidth = width;
|
|
161
168
|
this.cachedInnerLines = innerLines;
|
|
162
169
|
this.cachedLines = out;
|
|
@@ -164,35 +171,38 @@ export class RoundedFrame implements Component {
|
|
|
164
171
|
}
|
|
165
172
|
}
|
|
166
173
|
|
|
167
|
-
|
|
174
|
+
// ─── Frame factories ────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
const createFrame = (
|
|
168
177
|
inner: Component,
|
|
169
178
|
theme: { fg: (color: string, text: string) => string },
|
|
170
179
|
mode: FrameMode = "closed",
|
|
171
180
|
colorKey: string = "border",
|
|
172
|
-
): RoundedFrame => new RoundedFrame(inner, (t) => theme.fg(colorKey, t), mode);
|
|
173
|
-
|
|
174
|
-
function borderColorFor(
|
|
175
|
-
context: { isPartial?: boolean; isError?: boolean } | undefined,
|
|
176
|
-
): string {
|
|
177
|
-
if (context?.isError) return "error";
|
|
178
|
-
if (context?.isPartial) return "warning";
|
|
179
|
-
return "border";
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
183
|
-
|
|
184
|
-
type ToolDef = ToolDefinition<any, any, any>;
|
|
181
|
+
): RoundedFrame => new RoundedFrame(inner, (t) => theme.fg(colorKey, t), mode, colorKey);
|
|
185
182
|
|
|
186
183
|
const EMPTY_COMPONENT: Component = { render: () => [], invalidate: () => {} };
|
|
187
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
|
+
|
|
188
201
|
function unwrapInner(component: unknown): Component | undefined {
|
|
189
|
-
|
|
190
|
-
return component.getInner();
|
|
191
|
-
}
|
|
192
|
-
return component as Component | undefined;
|
|
202
|
+
return component instanceof RoundedFrame ? component.getInner() : (component as Component | undefined);
|
|
193
203
|
}
|
|
194
204
|
|
|
195
|
-
export function
|
|
205
|
+
export function wrapRenderers(def: ToolRenderers): ToolRenderers {
|
|
196
206
|
// Tools that already manage their own shell (e.g. edit) should remain untouched.
|
|
197
207
|
if (def.renderShell === "self") {
|
|
198
208
|
return def;
|
|
@@ -211,13 +221,14 @@ export function wrapBuiltin(def: ToolDef): ToolDef {
|
|
|
211
221
|
// close the bottom border so it renders as a complete box instead of an open frame.
|
|
212
222
|
const isPendingWithoutResult = Boolean(context?.isPartial && !context?.state?.__hasResult);
|
|
213
223
|
const mode: FrameMode = def.renderResult && !isPendingWithoutResult ? "open-bottom" : "closed";
|
|
214
|
-
const
|
|
224
|
+
const colorKey = borderColorFor(context);
|
|
225
|
+
const borderFn = (t: string) => theme.fg(colorKey, t);
|
|
215
226
|
|
|
216
227
|
let callFrame: RoundedFrame;
|
|
217
228
|
if (context?.lastComponent instanceof RoundedFrame) {
|
|
218
|
-
callFrame = context.lastComponent.update(inner, borderFn, mode);
|
|
229
|
+
callFrame = context.lastComponent.update(inner, borderFn, mode, colorKey);
|
|
219
230
|
} else {
|
|
220
|
-
callFrame =
|
|
231
|
+
callFrame = createFrame(inner, theme, mode, colorKey);
|
|
221
232
|
}
|
|
222
233
|
|
|
223
234
|
if (context?.state) {
|
|
@@ -239,91 +250,37 @@ export function wrapBuiltin(def: ToolDef): ToolDef {
|
|
|
239
250
|
const inner: Component = def.renderResult
|
|
240
251
|
? def.renderResult(result, options, theme, { ...context, lastComponent: unwrapped })
|
|
241
252
|
: EMPTY_COMPONENT;
|
|
242
|
-
const
|
|
253
|
+
const colorKey = borderColorFor(context);
|
|
254
|
+
const borderFn = (t: string) => theme.fg(colorKey, t);
|
|
243
255
|
|
|
244
256
|
if (context?.lastComponent instanceof RoundedFrame) {
|
|
245
|
-
return context.lastComponent.update(inner, borderFn, "open-top");
|
|
257
|
+
return context.lastComponent.update(inner, borderFn, "open-top", colorKey);
|
|
246
258
|
}
|
|
247
|
-
return
|
|
259
|
+
return createFrame(inner, theme, "open-top", colorKey);
|
|
248
260
|
},
|
|
249
261
|
};
|
|
250
262
|
}
|
|
251
263
|
|
|
252
|
-
|
|
253
|
-
target: ExtensionAPI | ((tool: ToolDef) => void),
|
|
254
|
-
enabled: boolean,
|
|
255
|
-
cwd: string,
|
|
256
|
-
_captured?: Map<string, ToolDef>,
|
|
257
|
-
): void {
|
|
258
|
-
const register = typeof target === "function" ? target : target.registerTool.bind(target);
|
|
259
|
-
|
|
260
|
-
let settings: any;
|
|
261
|
-
if (typeof target !== "function" && target.getSettings) {
|
|
262
|
-
try {
|
|
263
|
-
settings = target.getSettings();
|
|
264
|
-
} catch {
|
|
265
|
-
// runtime not initialized yet during early load
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
const autoResizeImages = settings?.images?.autoResize ?? true;
|
|
269
|
-
const shellCommandPrefix = settings?.shellCommandPrefix;
|
|
270
|
-
const shellPath = settings?.shellPath;
|
|
271
|
-
|
|
272
|
-
let activeToolNames: Set<string> | undefined;
|
|
273
|
-
if (typeof target !== "function" && target.getActiveTools) {
|
|
274
|
-
try {
|
|
275
|
-
const active = target.getActiveTools();
|
|
276
|
-
if (Array.isArray(active) && active.length > 0) {
|
|
277
|
-
activeToolNames = new Set(active);
|
|
278
|
-
}
|
|
279
|
-
} catch {
|
|
280
|
-
// runtime not initialized yet during early load
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
const defaultActiveNames = activeToolNames ?? new Set(["read", "bash", "edit", "write"]);
|
|
284
|
-
|
|
285
|
-
const builtins: Record<string, () => ToolDef> = {
|
|
286
|
-
read: () => createReadToolDefinition(cwd, { autoResizeImages }),
|
|
287
|
-
write: () => createWriteToolDefinition(cwd),
|
|
288
|
-
edit: () => createEditToolDefinition(cwd),
|
|
289
|
-
bash: () => createBashToolDefinition(cwd, { commandPrefix: shellCommandPrefix, shellPath }),
|
|
290
|
-
powershell: () => createPowerShellToolDefinition(cwd, { shellPath }),
|
|
291
|
-
ls: () => createLsToolDefinition(cwd),
|
|
292
|
-
grep: () => createGrepToolDefinition(cwd),
|
|
293
|
-
find: () => createFindToolDefinition(cwd),
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
for (const [name, factory] of Object.entries(builtins)) {
|
|
297
|
-
const rawDef = factory();
|
|
298
|
-
// Skip tools that already manage their own shell framing (e.g. edit)
|
|
299
|
-
if (rawDef.renderShell === "self") {
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const shouldBeActive = defaultActiveNames.has(name);
|
|
304
|
-
const baseDef: ToolDef = {
|
|
305
|
-
...rawDef,
|
|
306
|
-
defaultActive: shouldBeActive,
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
register(enabled ? wrapBuiltin(baseDef) : baseDef);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
264
|
+
// ─── Manager ─────────────────────────────────────────────────────────────
|
|
312
265
|
|
|
313
266
|
export class RoundedToolsManager {
|
|
314
267
|
private enabled = false;
|
|
315
268
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
+
});
|
|
322
281
|
}
|
|
323
282
|
|
|
324
|
-
|
|
325
|
-
apply(enabled: boolean, cwd: string): void {
|
|
283
|
+
setEnabled(enabled: boolean): void {
|
|
326
284
|
this.enabled = enabled;
|
|
327
|
-
registerRoundedTools(this.pi, enabled, cwd);
|
|
328
285
|
}
|
|
329
286
|
}
|