pi-open-tui 0.4.2 → 0.4.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/CHANGELOG.md +25 -0
- package/README.md +52 -120
- package/README.zh-CN.md +52 -120
- package/docs/configuration.md +245 -0
- package/docs/footer-segments.md +121 -0
- package/docs/implementation.md +185 -0
- package/extensions/open-tui/config.ts +25 -7
- package/extensions/open-tui/copy-buttons.ts +141 -63
- package/extensions/open-tui/footer-segments.ts +119 -0
- package/extensions/open-tui/footer.ts +121 -13
- package/extensions/open-tui/index.ts +8 -1
- package/extensions/open-tui/settings-command.ts +312 -63
- package/extensions/open-tui/tool-preview.ts +8 -2
- package/extensions/open-tui/tool-view.ts +184 -31
- package/extensions/open-tui/utils.ts +18 -6
- package/package.json +9 -8
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { isAbsolute, relative, sep } from "node:path";
|
|
3
|
-
import type { ExtensionAPI, Theme, ThemeColor, ToolRenderers } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { Box, type Component, Spacer, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
5
|
-
import type {
|
|
3
|
+
import type { ExtensionAPI, SourceInfo, Theme, ThemeColor, ToolRenderers } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { Box, type Component, getCapabilities, getImageDimensions, imageFallback, Spacer, Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
5
|
+
import type { ToolViewConfig } from "./config.ts";
|
|
6
6
|
import type { IconGlyphs } from "./icons.ts";
|
|
7
7
|
import { codeLines, prepareToolPreview } from "./tool-preview.ts";
|
|
8
8
|
import { sanitizeStatus, stripAnsi } from "./utils.ts";
|
|
@@ -24,6 +24,15 @@ const TOOL_LABELS: Record<string, string> = {
|
|
|
24
24
|
ls: "LS",
|
|
25
25
|
};
|
|
26
26
|
const LIVE_TAIL_LINES = 3;
|
|
27
|
+
/** Arguments that best identify a call to a tool this view knows nothing else about. */
|
|
28
|
+
const GENERIC_DETAIL_KEYS = ["command", "query", "sql", "pattern", "prompt", "task", "url", "path", "file_path"];
|
|
29
|
+
const GENERIC_DETAIL_ARGS = 3;
|
|
30
|
+
const GENERIC_DETAIL_VALUE_CHARS = 80;
|
|
31
|
+
const FFF_TOOLS = new Map<string, "grep" | "find">([
|
|
32
|
+
["ffgrep", "grep"], ["grep", "grep"],
|
|
33
|
+
["fffind", "find"], ["find", "find"],
|
|
34
|
+
["fff-multi-grep", "grep"], ["multi_grep", "grep"],
|
|
35
|
+
]);
|
|
27
36
|
|
|
28
37
|
/** Per-row slots: Pi's native renderers keep their own state and components here. */
|
|
29
38
|
interface RowState {
|
|
@@ -33,6 +42,7 @@ interface RowState {
|
|
|
33
42
|
callCard?: Box;
|
|
34
43
|
callCardBottom?: Spacer;
|
|
35
44
|
resultCard?: Box;
|
|
45
|
+
hasResult?: boolean;
|
|
36
46
|
startedAt?: number;
|
|
37
47
|
pulse?: ReturnType<typeof setInterval>;
|
|
38
48
|
pulseBright?: boolean;
|
|
@@ -165,6 +175,23 @@ export function callDetail(toolName: string, args: Args, cwd: string): string {
|
|
|
165
175
|
}
|
|
166
176
|
}
|
|
167
177
|
|
|
178
|
+
/** `Tool(...)` for tools with unknown arguments: the most identifying one, else the first few as `key=value`. */
|
|
179
|
+
export function genericDetail(args: Args, cwd: string): string {
|
|
180
|
+
for (const key of GENERIC_DETAIL_KEYS) {
|
|
181
|
+
const value = args[key];
|
|
182
|
+
if (typeof value !== "string" || !value.trim()) continue;
|
|
183
|
+
return key === "path" || key === "file_path" ? displayPath(value, cwd) : firstLine(value);
|
|
184
|
+
}
|
|
185
|
+
return Object.entries(args)
|
|
186
|
+
.filter(([, value]) => value !== undefined && value !== null)
|
|
187
|
+
.slice(0, GENERIC_DETAIL_ARGS)
|
|
188
|
+
.map(([key, value]) => {
|
|
189
|
+
const shown = typeof value === "string" ? firstLine(value) : JSON.stringify(value) ?? String(value);
|
|
190
|
+
return `${key}=${shown.slice(0, GENERIC_DETAIL_VALUE_CHARS)}`;
|
|
191
|
+
})
|
|
192
|
+
.join(", ");
|
|
193
|
+
}
|
|
194
|
+
|
|
168
195
|
function details(result: ToolResult): Record<string, unknown> {
|
|
169
196
|
const value: unknown = result.details;
|
|
170
197
|
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : {};
|
|
@@ -235,6 +262,77 @@ export function summarizeResult(toolName: string, result: ToolResult, args: Args
|
|
|
235
262
|
}
|
|
236
263
|
}
|
|
237
264
|
|
|
265
|
+
/**
|
|
266
|
+
* One-line summary for a tool whose output format is unknown, so nothing is parsed out of it.
|
|
267
|
+
* The first line goes last so narrow rows cut it before the counts and duration.
|
|
268
|
+
*/
|
|
269
|
+
export function summarizeGeneric(result: ToolResult, duration = ""): string {
|
|
270
|
+
const body = textOutput(result).split("\n").filter((line) => line.trim());
|
|
271
|
+
const images = result.content.filter((block) => block.type === "image").length;
|
|
272
|
+
const head = sanitizeStatus((body[0] ?? "").replace(/^#+\s+/, ""));
|
|
273
|
+
const info = truncation(result);
|
|
274
|
+
const totalLines = info.truncated && typeof info.totalLines === "number"
|
|
275
|
+
&& Number.isSafeInteger(info.totalLines) && info.totalLines >= body.length
|
|
276
|
+
? info.totalLines : body.length;
|
|
277
|
+
const counts = [
|
|
278
|
+
...(totalLines ? [plural(totalLines, "line")] : []),
|
|
279
|
+
...(images ? [plural(images, "image")] : []),
|
|
280
|
+
].join(" · ");
|
|
281
|
+
return (counts || "Done") + (info.truncated ? " · truncated" : "") + duration + (head ? ` · ${head}` : "");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** FFF's grep count belongs to this page; find's count covers all matching files. */
|
|
285
|
+
function summarizeFff(kind: "grep" | "find", result: ToolResult, duration = ""): string {
|
|
286
|
+
const info = details(result);
|
|
287
|
+
const matched = info.totalMatched;
|
|
288
|
+
if (typeof matched !== "number" || !Number.isSafeInteger(matched) || matched < 0
|
|
289
|
+
|| result.content.some((block) => block.type === "image")) return summarizeGeneric(result, duration);
|
|
290
|
+
const output = textOutput(result).trim();
|
|
291
|
+
const footer = /\n\n\[(Invalid regex: [\s\S]+, used literal match(?:\. Continue with cursor="fff_c\d+")?|[^\n]+)\]$/.exec(output);
|
|
292
|
+
const body = footer ? output.slice(0, footer.index) : output;
|
|
293
|
+
// FFF also returns zero counts for its wildcard guard; keep that explanation first.
|
|
294
|
+
if (matched === 0 && body !== (kind === "grep" ? "No matches found" : "No files found matching pattern")) {
|
|
295
|
+
return summarizeGeneric(result, duration);
|
|
296
|
+
}
|
|
297
|
+
const notice = footer?.[1] ?? "";
|
|
298
|
+
const fuzzy = kind === "grep" && output.startsWith("[0 exact matches. Maybe you meant this?]\n");
|
|
299
|
+
const literal = kind === "grep" && /^Invalid regex: [\s\S]+, used literal match(?:\. Continue with cursor="fff_c\d+")?$/.test(notice);
|
|
300
|
+
let more: boolean;
|
|
301
|
+
let weak: RegExpExecArray | null = null;
|
|
302
|
+
if (kind === "find") {
|
|
303
|
+
if (typeof info.hasMore !== "boolean" || typeof info.pageIndex !== "number"
|
|
304
|
+
|| !Number.isSafeInteger(info.pageIndex) || info.pageIndex < 0) return summarizeGeneric(result, duration);
|
|
305
|
+
// Weak fuzzy results can have hasMore=true while FFF intentionally provides no cursor.
|
|
306
|
+
weak = /^Query "[^\n]*" produced only weak scattered fuzzy matches\. Output capped at (\d+)\/(\d+)\.$/.exec(notice);
|
|
307
|
+
if (weak && (Number(weak[2]) !== matched || Number(weak[1]) > matched)) return summarizeGeneric(result, duration);
|
|
308
|
+
more = info.hasMore && /^\d+ more match(?:es)? available\. cursor="\d+" to continue$/.test(notice);
|
|
309
|
+
if (info.hasMore && !more && !weak) return summarizeGeneric(result, duration);
|
|
310
|
+
} else {
|
|
311
|
+
more = /(?:^|\. )(?:Continue with cursor="fff_c\d+"|More available\. cursor="fff_c\d+" to continue)$/.test(notice);
|
|
312
|
+
}
|
|
313
|
+
const matchType = fuzzy ? "fuzzy " : literal ? "literal " : "";
|
|
314
|
+
const count = kind === "find" ? `${plural(matched, "file")} matched`
|
|
315
|
+
: `${plural(matched, `${matchType}match`, `${matchType}matches`)} on this page`;
|
|
316
|
+
const head = sanitizeStatus(output.split("\n")[fuzzy ? 1 : 0] ?? "");
|
|
317
|
+
return count + (more ? " · more available" : "")
|
|
318
|
+
+ (literal ? " · regex fallback" : "")
|
|
319
|
+
+ (weak ? " · weak matches" + (Number(weak[1]) < matched ? " · capped" : "") : "")
|
|
320
|
+
+ (truncation(result).truncated ? " · truncated" : "")
|
|
321
|
+
+ duration + (head ? ` · ${head}` : "");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Match npm provenance, its installed entry point, or the official git source and entry point. */
|
|
325
|
+
export function fffKind(toolName: string, sourceInfo: SourceInfo | undefined): "grep" | "find" | undefined {
|
|
326
|
+
const kind = FFF_TOOLS.get(toolName);
|
|
327
|
+
if (!kind || !sourceInfo) return undefined;
|
|
328
|
+
const source = sourceInfo.source;
|
|
329
|
+
const path = sourceInfo.path.replace(/\\/g, "/");
|
|
330
|
+
const git = /^(?:git:)?(?:https?:\/\/github\.com\/|ssh:\/\/git@github\.com\/|git@github\.com:|github\.com\/)dmtrkovalenko\/fff(?:\.git)?(?:[@#][^\s]+)?$/i.test(source)
|
|
331
|
+
&& /(?:^|\/)packages\/pi-fff\/src\/index\.ts$/.test(path);
|
|
332
|
+
return source === "npm:@ff-labs/pi-fff" || source.startsWith("npm:@ff-labs/pi-fff@")
|
|
333
|
+
|| /(?:^|\/)node_modules\/@ff-labs\/pi-fff\/src\/index\.ts$/.test(path) || git ? kind : undefined;
|
|
334
|
+
}
|
|
335
|
+
|
|
238
336
|
/**
|
|
239
337
|
* One-line summary for a failed call: the shell's closing status plus its last output line,
|
|
240
338
|
* otherwise the error's first line.
|
|
@@ -300,24 +398,58 @@ function stopPulse(row: RowState): void {
|
|
|
300
398
|
row.pulse = undefined;
|
|
301
399
|
}
|
|
302
400
|
|
|
401
|
+
/** Pi's expanded card for a tool without its own call renderer. */
|
|
402
|
+
function genericCall(toolName: string, args: unknown, theme: Theme): Component {
|
|
403
|
+
const entries = typeof args === "object" && args !== null ? Object.entries(args) : [];
|
|
404
|
+
const rows = entries.map(([key, value]) => {
|
|
405
|
+
const shown = typeof value === "string" ? value : JSON.stringify(value, null, 2) ?? String(value);
|
|
406
|
+
return ` ${sanitizeStatus(key)}: ${cleanOutput(shown).replace(/\t/g, " ").split("\n").join("\n ")}`;
|
|
407
|
+
});
|
|
408
|
+
const title = theme.fg("toolTitle", theme.bold(sanitizeStatus(toolName)));
|
|
409
|
+
return new Text(rows.length ? `${title}\n${theme.fg("muted", rows.join("\n"))}` : title, 0, 0);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Pi's expanded card for a tool without its own result renderer. Pi still draws images it can show. */
|
|
413
|
+
function genericResult(result: ToolResult, theme: Theme, context: RenderContext): Component {
|
|
414
|
+
const inline = getCapabilities().images && context.showImages;
|
|
415
|
+
const missing = inline ? [] : result.content.flatMap((block) => (block.type === "image" ? [imageFallback(block.mimeType, getImageDimensions(block.data, block.mimeType) ?? undefined)] : []));
|
|
416
|
+
const output = [textOutput(result).replace(/\t/g, " "), ...missing].filter(Boolean).join("\n");
|
|
417
|
+
return new Text(output ? theme.fg("toolOutput", output) : "", 0, 0);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* `generic` is for tools other than Pi's own built-ins: their output format and renderers are
|
|
422
|
+
* unknown. A known extension can supply its own summary. Native renderers start on expansion;
|
|
423
|
+
* after collapse they still receive completion and error updates, but skip hidden progress.
|
|
424
|
+
*/
|
|
303
425
|
export function createCompactRenderers(
|
|
304
426
|
toolName: string,
|
|
305
|
-
native: ToolRenderers,
|
|
427
|
+
native: ToolRenderers | undefined,
|
|
306
428
|
glyphs: () => IconGlyphs,
|
|
307
429
|
running?: Map<string, RowState | undefined>,
|
|
430
|
+
generic = false,
|
|
431
|
+
summarizeExtension = summarizeGeneric,
|
|
308
432
|
): ToolRenderers {
|
|
309
|
-
const label = TOOL_LABELS[toolName]
|
|
310
|
-
const isShell = toolName === "bash" || toolName === "powershell";
|
|
433
|
+
const label = Object.hasOwn(TOOL_LABELS, toolName) ? TOOL_LABELS[toolName]! : sanitizeStatus(toolName);
|
|
434
|
+
const isShell = !generic && (toolName === "bash" || toolName === "powershell");
|
|
435
|
+
const nativeShell = native?.renderShell;
|
|
436
|
+
const renderNativeCall: RenderCall | undefined = native?.renderCall
|
|
437
|
+
?? (generic ? (args, theme) => genericCall(toolName, args, theme) : undefined);
|
|
438
|
+
const renderNativeResult: RenderResult | undefined = native?.renderResult
|
|
439
|
+
?? (generic ? (result, _options, theme, context) => genericResult(result, theme, context) : undefined);
|
|
311
440
|
// Expanded rows hand off to Pi's own card so nothing is hidden.
|
|
312
441
|
const showNative = (context: RenderContext) => context.expanded;
|
|
442
|
+
// A generic row that was expanded skips hidden progress but still sees the call finish, for a renderer's own timers.
|
|
443
|
+
const feedCollapsedNative = (row: RowState, isPartial: boolean, isError: boolean) =>
|
|
444
|
+
row.nativeCall !== undefined && (!isPartial || isError);
|
|
313
445
|
|
|
314
446
|
return {
|
|
315
447
|
renderShell: "self",
|
|
316
448
|
renderCall(args, theme, context) {
|
|
317
449
|
const row = rowState(context);
|
|
318
450
|
// Keep native timers/edit state current; write rebuilds its full highlight cache only on expansion.
|
|
319
|
-
if (toolName !== "write"
|
|
320
|
-
row.nativeCall =
|
|
451
|
+
if (context.expanded || (generic ? feedCollapsedNative(row, context.isPartial, context.isError) : toolName !== "write")) {
|
|
452
|
+
row.nativeCall = renderNativeCall?.(args, theme, nativeContext(context, row, row.nativeCall));
|
|
321
453
|
}
|
|
322
454
|
if (context.executionStarted) row.startedAt ??= Date.now();
|
|
323
455
|
const live = context.executionStarted && context.isPartial && !context.isError;
|
|
@@ -336,7 +468,7 @@ export function createCompactRenderers(
|
|
|
336
468
|
stopPulse(row);
|
|
337
469
|
}
|
|
338
470
|
if (showNative(context) && row.nativeCall) {
|
|
339
|
-
if (
|
|
471
|
+
if (nativeShell === "self") return row.nativeCall;
|
|
340
472
|
// Recreate Pi's default card: top padding, call, then result (or bottom padding while pending).
|
|
341
473
|
row.callCard ??= new Box(context.outputPad, 0);
|
|
342
474
|
row.callCardBottom ??= new Spacer(1);
|
|
@@ -352,20 +484,33 @@ export function createCompactRenderers(
|
|
|
352
484
|
? live && row.pulseBright !== false ? "accent" : "dim"
|
|
353
485
|
: "success";
|
|
354
486
|
const titleColor = context.isPartial || context.isError ? "toolTitle" : "dim";
|
|
355
|
-
const
|
|
487
|
+
const callArgs = (args ?? {}) as Args;
|
|
488
|
+
const detail = sanitizeStatus(generic ? genericDetail(callArgs, context.cwd) : callDetail(toolName, callArgs, context.cwd));
|
|
356
489
|
const pad = " ".repeat(context.outputPad);
|
|
357
|
-
return lines((_width, icons) =>
|
|
358
|
-
pad + theme.fg(color, icons.toolCall) + " " + theme.fg(titleColor, titleColor === "dim" ? label : theme.bold(label))
|
|
359
|
-
+ (detail ? theme.fg(context.isPartial || context.isError ? "muted" : "dim", `(${detail})`) : "")
|
|
360
|
-
|
|
490
|
+
return lines((_width, icons) => {
|
|
491
|
+
const call = pad + theme.fg(color, icons.toolCall) + " " + theme.fg(titleColor, titleColor === "dim" ? label : theme.bold(label))
|
|
492
|
+
+ (detail ? theme.fg(context.isPartial || context.isError ? "muted" : "dim", `(${detail})`) : "");
|
|
493
|
+
// Pi does not call renderResult until the tool provides its first result.
|
|
494
|
+
// Read the shared flag during rendering, after both slots have been prepared.
|
|
495
|
+
if (generic && live && !row.hasResult) {
|
|
496
|
+
const elapsed = row.startedAt === undefined ? "" : ` ${formatElapsed(Date.now() - row.startedAt)}`;
|
|
497
|
+
const gutter = `${pad} ${theme.fg("dim", icons.toolResult)} `;
|
|
498
|
+
return [call, gutter + theme.fg("warning", `Running…${elapsed}`)];
|
|
499
|
+
}
|
|
500
|
+
return [call];
|
|
501
|
+
}, theme, glyphs, !context.isPartial);
|
|
361
502
|
},
|
|
362
503
|
|
|
363
504
|
renderResult(result, options, theme, context) {
|
|
364
505
|
const row = rowState(context);
|
|
506
|
+
row.hasResult = true;
|
|
507
|
+
const partial = options.isPartial && !context.isError;
|
|
365
508
|
if (!options.isPartial || context.isError) stopPulse(row);
|
|
366
|
-
|
|
509
|
+
if (!generic || context.expanded || feedCollapsedNative(row, options.isPartial, context.isError)) {
|
|
510
|
+
row.nativeResult = renderNativeResult?.(result, options, theme, nativeContext(context, row, row.nativeResult));
|
|
511
|
+
}
|
|
367
512
|
if (showNative(context) && row.nativeResult) {
|
|
368
|
-
if (
|
|
513
|
+
if (nativeShell === "self") return row.nativeResult;
|
|
369
514
|
// The result now closes the card, so the call card drops its pending bottom padding.
|
|
370
515
|
if (row.callCard && row.callCardBottom) row.callCard.removeChild(row.callCardBottom);
|
|
371
516
|
row.resultCard ??= new Box(context.outputPad, 0);
|
|
@@ -378,30 +523,31 @@ export function createCompactRenderers(
|
|
|
378
523
|
}
|
|
379
524
|
const pad = " ".repeat(context.outputPad);
|
|
380
525
|
const args = (context.args ?? {}) as Args;
|
|
381
|
-
if (!options.isPartial && !context.isError && (toolName === "edit" || toolName === "write")) {
|
|
526
|
+
if (!generic && !options.isPartial && !context.isError && (toolName === "edit" || toolName === "write")) {
|
|
382
527
|
const source = toolName === "edit" ? str(details(result).diff) : str(args.content);
|
|
383
528
|
const path = str(args.file_path ?? args.path);
|
|
384
529
|
if (!row.preview || row.preview.source !== source || row.preview.path !== path || row.preview.colors !== theme.colors) {
|
|
385
530
|
row.preview = { source, path, colors: theme.colors, render: prepareToolPreview(toolName, source, path, theme) };
|
|
386
531
|
}
|
|
387
532
|
}
|
|
388
|
-
const
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
:
|
|
392
|
-
|
|
533
|
+
const timed = isShell || generic;
|
|
534
|
+
const duration = timed && context.durationMs !== undefined ? ` · ${formatElapsed(context.durationMs)}` : "";
|
|
535
|
+
const summary = partial ? "" : context.isError
|
|
536
|
+
? summarizeError(generic ? "" : toolName, result, duration)
|
|
537
|
+
: generic ? summarizeExtension(result, duration) : summarizeResult(toolName, result, args) + duration;
|
|
538
|
+
const tail = partial && isShell ? tailLines(textOutput(result), LIVE_TAIL_LINES) : [];
|
|
393
539
|
return lines((width, { toolResult }) => {
|
|
394
540
|
const gutter = `${pad} ${theme.fg("dim", toolResult)} `;
|
|
395
541
|
const indent = `${pad} ${" ".repeat(visibleWidth(toolResult))} `;
|
|
396
|
-
if (
|
|
397
|
-
if (!
|
|
542
|
+
if (partial) {
|
|
543
|
+
if (!timed) return [gutter + theme.fg("warning", "Running…")];
|
|
398
544
|
const elapsed = row.startedAt === undefined ? "" : ` ${formatElapsed(Date.now() - row.startedAt)}`;
|
|
399
545
|
return [...tail.map((line) => theme.fg("toolOutput", line)), theme.fg("warning", `Running…${elapsed}`)]
|
|
400
546
|
.map((line, index) => (index === 0 ? gutter : indent) + line);
|
|
401
547
|
}
|
|
402
548
|
if (context.isError) return [gutter + theme.fg("error", summary)];
|
|
403
549
|
return [gutter + theme.fg("dim", summary), ...(row.preview?.render(width, indent) ?? [])];
|
|
404
|
-
}, theme, glyphs, !
|
|
550
|
+
}, theme, glyphs, !partial);
|
|
405
551
|
},
|
|
406
552
|
};
|
|
407
553
|
}
|
|
@@ -409,10 +555,10 @@ export function createCompactRenderers(
|
|
|
409
555
|
const OWNED_TOOLS = new Set(Object.keys(TOOL_LABELS));
|
|
410
556
|
|
|
411
557
|
/**
|
|
412
|
-
* Display-only: Pi's tools, execution, arguments and results are untouched. The
|
|
413
|
-
* when Pi builds a row, so
|
|
558
|
+
* Display-only: Pi's tools, execution, arguments and results are untouched. The configuration is
|
|
559
|
+
* read when Pi builds a row, so changing it affects new rows only.
|
|
414
560
|
*/
|
|
415
|
-
export function registerToolView(pi: ExtensionAPI,
|
|
561
|
+
export function registerToolView(pi: ExtensionAPI, getView: () => ToolViewConfig, glyphs: () => IconGlyphs): void {
|
|
416
562
|
const running = new Map<string, RowState | undefined>();
|
|
417
563
|
pi.on("tool_execution_start", (event) => { running.set(event.toolCallId, undefined); });
|
|
418
564
|
pi.on("tool_execution_end", (event) => {
|
|
@@ -426,8 +572,15 @@ export function registerToolView(pi: ExtensionAPI, getMode: () => ToolViewMode,
|
|
|
426
572
|
});
|
|
427
573
|
pi.registerToolRenderer((toolName, next) => {
|
|
428
574
|
const native = next();
|
|
429
|
-
|
|
430
|
-
if (
|
|
431
|
-
|
|
575
|
+
const view = getView();
|
|
576
|
+
if (view.mode !== "compact" || view.exclude.includes(toolName)) return native;
|
|
577
|
+
const sourceInfo = OWNED_TOOLS.has(toolName) || view.extensionTools && FFF_TOOLS.has(toolName)
|
|
578
|
+
? pi.getAllTools().find((tool) => tool.name === toolName)?.sourceInfo : undefined;
|
|
579
|
+
const builtin = OWNED_TOOLS.has(toolName) && sourceInfo?.source === "builtin";
|
|
580
|
+
if (builtin) return native && createCompactRenderers(toolName, native, glyphs, running);
|
|
581
|
+
if (!view.extensionTools) return native;
|
|
582
|
+
const fff = fffKind(toolName, sourceInfo);
|
|
583
|
+
return createCompactRenderers(toolName, native, glyphs, running, true,
|
|
584
|
+
fff ? (result, duration) => summarizeFff(fff, result, duration) : summarizeGeneric);
|
|
432
585
|
});
|
|
433
586
|
}
|
|
@@ -133,6 +133,13 @@ export type PrioritizedSegment = {
|
|
|
133
133
|
truncate?: (text: string, maxWidth: number, ellipsis: string) => string;
|
|
134
134
|
};
|
|
135
135
|
|
|
136
|
+
export interface SegmentFitOptions {
|
|
137
|
+
/** Gap between adjacent surviving segments, using their original indices. */
|
|
138
|
+
gapWidth?: (previousIndex: number, nextIndex: number) => number;
|
|
139
|
+
/** Keep existing callers' compact order unless the row opts into priorities. */
|
|
140
|
+
compactOrder?: "input" | "priority";
|
|
141
|
+
}
|
|
142
|
+
|
|
136
143
|
/**
|
|
137
144
|
* Pack segments into maxWidth: compact segments first, then shrink/drop the
|
|
138
145
|
* lowest-priority segments (higher priority = survives longer). Returns one
|
|
@@ -142,8 +149,10 @@ export function fitSegmentsByPriority(
|
|
|
142
149
|
segs: readonly PrioritizedSegment[],
|
|
143
150
|
maxW: number,
|
|
144
151
|
ellipsis = "...",
|
|
152
|
+
options: SegmentFitOptions = {},
|
|
145
153
|
): string[] {
|
|
146
|
-
const items = segs.map((s) => ({
|
|
154
|
+
const items = segs.map((s, index) => ({
|
|
155
|
+
index,
|
|
147
156
|
text: s.text,
|
|
148
157
|
compactText: s.compactText,
|
|
149
158
|
priority: s.priority,
|
|
@@ -152,11 +161,16 @@ export function fitSegmentsByPriority(
|
|
|
152
161
|
}));
|
|
153
162
|
const totalW = () => {
|
|
154
163
|
const active = items.filter((it) => it.text !== "");
|
|
155
|
-
return active.reduce((
|
|
164
|
+
return active.reduce((width, item, index) => width + item.w + (index === 0
|
|
165
|
+
? 0
|
|
166
|
+
: options.gapWidth?.(active[index - 1]!.index, item.index) ?? 1), 0);
|
|
156
167
|
};
|
|
157
168
|
// Compact (e.g. cwd -> basename) before sacrificing any segment content.
|
|
158
169
|
if (totalW() > maxW) {
|
|
159
|
-
|
|
170
|
+
const compactItems = options.compactOrder === "priority"
|
|
171
|
+
? [...items].sort((a, b) => a.priority - b.priority || a.index - b.index)
|
|
172
|
+
: items;
|
|
173
|
+
for (const item of compactItems) {
|
|
160
174
|
if (!item.compactText || visibleWidth(item.compactText) >= item.w) continue;
|
|
161
175
|
item.text = item.compactText;
|
|
162
176
|
item.w = visibleWidth(item.text);
|
|
@@ -189,9 +203,7 @@ export function fitSegmentsByPriority(
|
|
|
189
203
|
}
|
|
190
204
|
}
|
|
191
205
|
if (target === -1) break;
|
|
192
|
-
const
|
|
193
|
-
const otherW = others.reduce((a, it) => a + it.w, 0) + Math.max(0, others.length - 1);
|
|
194
|
-
const avail = maxW - otherW - (others.length > 0 ? 1 : 0);
|
|
206
|
+
const avail = maxW - (nowTotal - items[target].w);
|
|
195
207
|
if (avail <= visibleWidth(ellipsis)) {
|
|
196
208
|
items[target].text = "";
|
|
197
209
|
items[target].w = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-open-tui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "A polished TUI for Pi coding agent: branded header, responsive footer, framed editor, thinking preview, and compact tool views.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"files": [
|
|
21
21
|
"extensions/",
|
|
22
|
+
"docs/",
|
|
22
23
|
"assets/",
|
|
23
24
|
"CHANGELOG.md",
|
|
24
25
|
"README.md",
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
"LICENSE"
|
|
27
28
|
],
|
|
28
29
|
"scripts": {
|
|
29
|
-
"test": "node --test tests/settings-command.test.ts tests/footer.test.ts tests/editor.test.ts tests/telemetry.test.ts tests/peek.test.ts tests/tool-view.test.ts tests/copy-buttons.test.ts",
|
|
30
|
+
"test": "node --test tests/settings-command.test.ts tests/footer.test.ts tests/footer-protocol.test.ts tests/footer-segments.test.ts tests/editor.test.ts tests/telemetry.test.ts tests/peek.test.ts tests/tool-view.test.ts tests/copy-buttons.test.ts tests/copy-markdown.test.ts",
|
|
30
31
|
"typecheck": "tsc --noEmit"
|
|
31
32
|
},
|
|
32
33
|
"pi": {
|
|
@@ -36,9 +37,9 @@
|
|
|
36
37
|
"image": "https://raw.githubusercontent.com/OldSuns/pi-open-tui/main/assets/preview_dashboard_1.png"
|
|
37
38
|
},
|
|
38
39
|
"peerDependencies": {
|
|
39
|
-
"@earendil-works/pi-ai": ">=1.
|
|
40
|
-
"@earendil-works/pi-coding-agent": ">=1.
|
|
41
|
-
"@earendil-works/pi-tui": ">=1.
|
|
40
|
+
"@earendil-works/pi-ai": ">=1.5.0",
|
|
41
|
+
"@earendil-works/pi-coding-agent": ">=1.5.0",
|
|
42
|
+
"@earendil-works/pi-tui": ">=1.5.0"
|
|
42
43
|
},
|
|
43
44
|
"peerDependenciesMeta": {
|
|
44
45
|
"@earendil-works/pi-coding-agent": {
|
|
@@ -52,9 +53,9 @@
|
|
|
52
53
|
}
|
|
53
54
|
},
|
|
54
55
|
"devDependencies": {
|
|
55
|
-
"@earendil-works/pi-ai": "^1.
|
|
56
|
-
"@earendil-works/pi-coding-agent": "^1.
|
|
57
|
-
"@earendil-works/pi-tui": "^1.
|
|
56
|
+
"@earendil-works/pi-ai": "^1.5.0",
|
|
57
|
+
"@earendil-works/pi-coding-agent": "^1.5.0",
|
|
58
|
+
"@earendil-works/pi-tui": "^1.5.0",
|
|
58
59
|
"@types/node": "^22.0.0",
|
|
59
60
|
"typescript": "^5.5.0"
|
|
60
61
|
},
|