pi-pretty-tui 0.2.2 → 0.2.4
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 +2 -1
- package/README.zh-CN.md +2 -1
- package/assets/screenshot.png +0 -0
- package/extensions/index.ts +173 -91
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ A beautiful, polished TUI for the [Pi coding agent](https://pi.dev/).
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- **Collapsible activity timeline:** instead of filling the conversation with individual calls, thoughts, tools, and ordinary `info` notifications share one chronological activity stream. Work appears beneath a single `Running(...)` row and settles into a quiet `Done(...)` row. Parent rows count only tool calls and thoughts; expanding one reveals every ordered member with summaries retained above nested details. Thoughts and tools are peers: a turn that calls no tool still gets its own `Running(1 thought)` / `Done(1 thought)` parent row with the Thought as its child, so thinking is never dropped.
|
|
10
|
+
- **Live working duration:** in clean mode, elapsed time appears beside `Working` in the prompt editor's top border, using the border's own color. It shares the agent-run clock and duration format with the completion footer, includes automatic retries, and disappears when the run settles. Narrow editors omit the timer when it cannot fit. Expanded `Done` titles and markers use the theme's `success` color; collapsed summaries remain quiet gray.
|
|
10
11
|
- **Transient UI status:** one line above the editor shows direct Pi UI feedback such as Thinking/tool-output visibility changes. A newer UI status replaces it, and it disappears automatically after ten seconds without entering the transcript or session.
|
|
11
12
|
- **Durable response footers:** once the complete agent run settles—including tools, retries, and compaction—the final answer ends with one quiet gray divider containing the elapsed-work time and, in fullscreen, a `[Copy]` control. It copies only the final visible answer, excluding thoughts, tools, statuses, and the footer itself; the footer survives reloads and session-tree navigation.
|
|
12
13
|
- **Useful details on demand:** tool-aware summaries keep paths, commands, results, diffs, file previews, and live shell output concise without removing access to the original content.
|
|
@@ -26,7 +27,7 @@ Pi may warn that built-in tools are being overridden. This is expected: pi-prett
|
|
|
26
27
|
|
|
27
28
|
## Working with tool calls
|
|
28
29
|
|
|
29
|
-
Click a `Running(...)` or `Done(...)` row to keep it as a parent and reveal tools, completed `● thought` entries, and extension updates beneath it. Click an individual tool or thought to reveal nested details without replacing its summary,
|
|
30
|
+
Click a `Running(...)` or `Done(...)` row to keep it as a parent and reveal tools, completed `● thought` entries, and extension updates beneath it. Click an individual tool or thought to reveal nested details without replacing its summary. The last item in each expanded tree, `└─ [↑ Collapse]`, collapses just that group without scrolling back to its parent; clicking the parent also works. Press `Ctrl+O` at any time to use Pi's global expansion control for all tool output and thinking content. The end-of-tree action still collapses only its own group after global expansion. Third-party replacements for `grep` and `find` (such as FFF) use compact tree rows too, with their native details available when expanded. Tool names use each definition's `label` verbatim, falling back to its raw `name`; pi-pretty-tui does not rename or title-case them.
|
|
30
31
|
|
|
31
32
|
Extension calls to `ctx.ui.notify(message, "info")` (or with the type omitted) join an existing thought/tool group in chronological order; without existing work they remain independent and never attach to a future group. Warnings and errors stay standalone, use yellow `⚠` and red `✕`, and are hard boundaries. Tool failures remain owned by their tool details. None of these extension notifications or errors appear in the transient UI-status line.
|
|
32
33
|
|
package/README.zh-CN.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
- **可折叠的活动时间线:** 不再让大量独立调用填满对话。思考、工具和普通 `info` 通知会进入同一条按真实时间排序的活动流;工作进行时显示为 `Running(...)`,结束后变为低调的 `Done(...)`。父级只统计工具调用和思考数量;展开后可查看所有有序成员,成员摘要在展开嵌套详情后仍然保留。思考与工具地位相同:没有调用任何工具的那一轮同样会有自己的 `Running(1 thought)` / `Done(1 thought)` 父行,思考作为其子级,内容不会被丢弃。
|
|
10
10
|
- **可持久化的回答 footer:** 整轮 Agent 工作真正 settled 后(包括工具、自动重试和 compaction),最终回答底部会显示一条低调的灰色分隔线,工作时长与 fullscreen 模式下的 `[Copy]` 控件都嵌在线中。`[Copy]` 只复制最终可见回答,不包含 thought、工具、状态或 footer;reload 和 session tree 切换后仍会保留。
|
|
11
|
+
- **实时工作计时:** clean 模式下,耗时显示在输入框顶部边框的 `Working` 右侧,颜色与边框一致。它与完成 footer 共用 Agent 整轮工作的计时起点和时长格式,包含自动重试,工作真正结束后消失;窄输入框放不下时会隐藏计时。展开后的 `Done` 标题和圆点使用主题的 `success` 色,折叠摘要仍保持低调的灰色。
|
|
11
12
|
- **临时 UI 状态:** 编辑器上方的一行只显示 Pi 直接产生的 UI 操作反馈,例如 Thinking 或工具输出可见性变化。新状态会替换旧状态,并在十秒后自动消失,不进入 transcript 或 session。
|
|
12
13
|
- **按需查看有效信息:** 针对不同工具设计的摘要,会简洁呈现路径、命令、结果、diff、文件预览和实时 shell 输出,同时保留查看原始完整内容的能力。
|
|
13
14
|
- **统一、精致的视觉体验:** 透明、右对齐的用户聊天气泡会根据短消息自适应宽度,长消息则在终端宽度的 75% 处换行;气泡使用 accent 浅蓝色圆角边框,并移除了多余的 `User` 标注。配合圆角输入框、主题感知配色、层级清晰的 Markdown 标题、更清爽的列表,以及带语法高亮的圆角代码块,让整个 TUI 更协调。在 fullscreen 模式下,每个围栏代码块还带有符合原生体验的 `[Copy]` 控件。
|
|
@@ -26,7 +27,7 @@ Pi 可能会提示内置工具被覆盖。这是正常现象:pi-pretty-tui 会
|
|
|
26
27
|
|
|
27
28
|
## 使用工具调用
|
|
28
29
|
|
|
29
|
-
点击 `Running(...)` 或 `Done(...)` 行,可以保留父级并展开其下方的工具、已完成的 `● thought`
|
|
30
|
+
点击 `Running(...)` 或 `Done(...)` 行,可以保留父级并展开其下方的工具、已完成的 `● thought` 和扩展更新。点击单个工具或思考子项可在保留摘要的同时展开嵌套详情。每棵展开的树末尾都有 `└─ [↑ Collapse]`,无需滚回父级即可只折叠当前分组;再次点击父级也可以折叠。任何时候都可以按 `Ctrl+O`,使用 Pi 的全局展开控制来显示或隐藏全部工具输出和思考内容;全局展开后,树尾操作仍只折叠本组。第三方替换的 `grep`、`find`(如 FFF)也使用紧凑的树节点,展开子项时仍可查看原生详情。工具名称会原样使用定义中的 `label`,没有 `label` 时回退到原始 `name`;pi-pretty-tui 不再重命名或转换大小写。
|
|
30
31
|
|
|
31
32
|
扩展调用 `ctx.ui.notify(message, "info")`(或省略 type)时,如果当前已有 thought/tool 活动组,通知会按真实时间顺序加入该组;如果没有现有工作,则独立显示,并且不会附着到未来的工具组。warning 和 error 保持独立,分别使用黄色 `⚠` 和红色 `✕`,并形成硬边界。工具失败继续归属于对应工具详情。上述扩展通知和错误都不会进入临时 UI 状态栏。
|
|
32
33
|
|
package/assets/screenshot.png
CHANGED
|
Binary file
|
package/extensions/index.ts
CHANGED
|
@@ -192,6 +192,16 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
192
192
|
};
|
|
193
193
|
const responseAnswerTexts = new Map<string, string>();
|
|
194
194
|
let responseRunStartedAt: number | undefined;
|
|
195
|
+
let workingElapsedTimer: ReturnType<typeof setInterval> | undefined;
|
|
196
|
+
const stopWorkingElapsedTimer = () => {
|
|
197
|
+
if (workingElapsedTimer) clearInterval(workingElapsedTimer);
|
|
198
|
+
workingElapsedTimer = undefined;
|
|
199
|
+
};
|
|
200
|
+
const startWorkingElapsedTimer = () => {
|
|
201
|
+
if (workingElapsedTimer) return;
|
|
202
|
+
workingElapsedTimer = setInterval(() => currentTui?.requestRender?.(), 1000);
|
|
203
|
+
workingElapsedTimer.unref?.();
|
|
204
|
+
};
|
|
195
205
|
let responseHasAssistantMessage = false;
|
|
196
206
|
const assistantAnswerText = (message: any): string => {
|
|
197
207
|
if (typeof message?.content === "string") return message.content.trim();
|
|
@@ -203,7 +213,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
203
213
|
.trim();
|
|
204
214
|
};
|
|
205
215
|
const formatResponseDuration = (durationMs: number): string => {
|
|
206
|
-
const totalSeconds = Math.
|
|
216
|
+
const totalSeconds = Math.floor(Math.max(0, durationMs) / 1000);
|
|
207
217
|
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
208
218
|
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
209
219
|
const seconds = totalSeconds % 60;
|
|
@@ -214,6 +224,8 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
214
224
|
};
|
|
215
225
|
const activityTimeline = new ActivityTimeline();
|
|
216
226
|
const revealedActivityGroups = new Set<string>();
|
|
227
|
+
// A local collapse must still work when Ctrl+O has expanded every group.
|
|
228
|
+
const collapsedActivityGroups = new Set<string>();
|
|
217
229
|
const expandedThinkingMembers = new Set<string>();
|
|
218
230
|
const thinkingComponents = new Map<string, any>();
|
|
219
231
|
const activityFallbackThemes = new Map<string, any>();
|
|
@@ -342,7 +354,8 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
342
354
|
};
|
|
343
355
|
|
|
344
356
|
const activityGroupRevealed = (group: ActivityGroup): boolean =>
|
|
345
|
-
|
|
357
|
+
!collapsedActivityGroups.has(group.id) &&
|
|
358
|
+
(cleanToolsExpanded || revealedActivityGroups.has(group.id));
|
|
346
359
|
|
|
347
360
|
// A group is revealable once it owns anything renderable beneath its parent
|
|
348
361
|
// row. Thinking counts on its own: a tool-free turn still gets a parent row
|
|
@@ -355,6 +368,67 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
355
368
|
last: group.members[group.members.length - 1]?.id === member.id,
|
|
356
369
|
});
|
|
357
370
|
|
|
371
|
+
const collapseLabel = "[↑ Collapse]";
|
|
372
|
+
const collapsePrefix = " └─ ";
|
|
373
|
+
const renderActivityCollapse = (group: ActivityGroup, width: number): string => {
|
|
374
|
+
const theme = activityGroupTheme(group);
|
|
375
|
+
return truncateToWidth(
|
|
376
|
+
theme.fg("dim", collapsePrefix) + theme.fg("muted", collapseLabel),
|
|
377
|
+
Math.max(1, width),
|
|
378
|
+
"",
|
|
379
|
+
);
|
|
380
|
+
};
|
|
381
|
+
const withActivityCollapse = (
|
|
382
|
+
group: ActivityGroup,
|
|
383
|
+
member: ActivityMember,
|
|
384
|
+
width: number,
|
|
385
|
+
lines: string[],
|
|
386
|
+
): string[] => activityMemberPosition(group, member).last && activityGroupRevealed(group)
|
|
387
|
+
? [...lines, renderActivityCollapse(group, width)]
|
|
388
|
+
: lines;
|
|
389
|
+
const activityCollapseClicked = (
|
|
390
|
+
group: ActivityGroup,
|
|
391
|
+
member: ActivityMember,
|
|
392
|
+
event: any,
|
|
393
|
+
row: number,
|
|
394
|
+
): boolean => activityGroupRevealed(group) &&
|
|
395
|
+
activityMemberPosition(group, member).last &&
|
|
396
|
+
event.type === "click" && event.button === "left" && event.y === row &&
|
|
397
|
+
event.x >= visibleWidth(collapsePrefix) &&
|
|
398
|
+
event.x < Math.min(event.width, visibleWidth(collapsePrefix + collapseLabel));
|
|
399
|
+
|
|
400
|
+
const refreshActivityGroup = (group: ActivityGroup): void => {
|
|
401
|
+
for (const child of group.members) {
|
|
402
|
+
if (child.kind === "tool" && child.toolCallId) {
|
|
403
|
+
cleanToolComponents.get(child.toolCallId)?.updateDisplay?.();
|
|
404
|
+
} else if (child.kind === "thinking" && child.messageKey) {
|
|
405
|
+
thinkingComponents.get(child.messageKey)?.invalidate?.();
|
|
406
|
+
} else if (child.kind === "update") {
|
|
407
|
+
activityUpdateComponents.get(child.id)?.invalidate?.();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
const revealActivityGroup = (group: ActivityGroup, ui?: any): void => {
|
|
412
|
+
collapsedActivityGroups.delete(group.id);
|
|
413
|
+
revealedActivityGroups.add(group.id);
|
|
414
|
+
for (const toolCallId of group.toolCallIds) cleanCompactToolCallIds.add(toolCallId);
|
|
415
|
+
refreshActivityGroup(group);
|
|
416
|
+
(ui ?? currentTui)?.requestRender?.();
|
|
417
|
+
};
|
|
418
|
+
const collapseActivityGroup = (group: ActivityGroup, ui?: any): void => {
|
|
419
|
+
revealedActivityGroups.delete(group.id);
|
|
420
|
+
if (cleanToolsExpanded) collapsedActivityGroups.add(group.id);
|
|
421
|
+
else collapsedActivityGroups.delete(group.id);
|
|
422
|
+
for (const toolCallId of group.toolCallIds) {
|
|
423
|
+
cleanCompactToolCallIds.delete(toolCallId);
|
|
424
|
+
const component = cleanToolComponents.get(toolCallId);
|
|
425
|
+
if (component?.expanded) component.setExpanded(false);
|
|
426
|
+
}
|
|
427
|
+
for (const child of group.members) expandedThinkingMembers.delete(child.id);
|
|
428
|
+
refreshActivityGroup(group);
|
|
429
|
+
(ui ?? currentTui)?.requestRender?.();
|
|
430
|
+
};
|
|
431
|
+
|
|
358
432
|
const activityTreeStyle = (
|
|
359
433
|
group: ActivityGroup,
|
|
360
434
|
member: ActivityMember,
|
|
@@ -362,8 +436,10 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
362
436
|
memberTheme?: any,
|
|
363
437
|
) => {
|
|
364
438
|
const { last } = activityMemberPosition(group, member);
|
|
365
|
-
|
|
366
|
-
const
|
|
439
|
+
// While expanded, the collapse action is the final sibling in the tree.
|
|
440
|
+
const hasFollowingCollapse = last && activityGroupRevealed(group);
|
|
441
|
+
const rawPrefix = last && !hasFollowingCollapse ? " └─ " : " ├─ ";
|
|
442
|
+
const rawContinuation = last && !hasFollowingCollapse ? " " : " │ ";
|
|
367
443
|
const prefixWidth = visibleWidth(rawPrefix);
|
|
368
444
|
if (width <= prefixWidth) {
|
|
369
445
|
return { prefix: "", continuation: "", childWidth: Math.max(1, width), prefixWidth: 0 };
|
|
@@ -450,9 +526,10 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
450
526
|
truncateToWidth((index === 0 ? prefix : continuation) + line, Math.max(1, width), "")
|
|
451
527
|
);
|
|
452
528
|
const position = activityMemberPosition(group, member);
|
|
453
|
-
return
|
|
454
|
-
|
|
455
|
-
: decorated
|
|
529
|
+
return withActivityCollapse(
|
|
530
|
+
group, member, width,
|
|
531
|
+
position.first ? ["", ...renderActivityGroupSummary(group, width), ...decorated] : decorated,
|
|
532
|
+
);
|
|
456
533
|
};
|
|
457
534
|
|
|
458
535
|
const renderPendingActivityUpdate = (member: ActivityMember, width: number): string[] => {
|
|
@@ -495,29 +572,16 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
495
572
|
const position = activityMemberPosition(group, member);
|
|
496
573
|
if (!activityGroupRevealed(group)) {
|
|
497
574
|
if (!position.first) return undefined;
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
} else {
|
|
501
|
-
const summaryHeight = position.first ? renderActivityGroupSummary(group, event.width).length : 0;
|
|
502
|
-
if (!position.first || event.y <= 0 || event.y > summaryHeight) return undefined;
|
|
503
|
-
revealedActivityGroups.delete(group.id);
|
|
504
|
-
for (const toolCallId of group.toolCallIds) {
|
|
505
|
-
cleanCompactToolCallIds.delete(toolCallId);
|
|
506
|
-
const component = cleanToolComponents.get(toolCallId);
|
|
507
|
-
if (component?.expanded) component.setExpanded(false);
|
|
508
|
-
}
|
|
509
|
-
for (const child of group.members) expandedThinkingMembers.delete(child.id);
|
|
510
|
-
}
|
|
511
|
-
for (const child of group.members) {
|
|
512
|
-
if (child.kind === "tool" && child.toolCallId) {
|
|
513
|
-
cleanToolComponents.get(child.toolCallId)?.updateDisplay?.();
|
|
514
|
-
} else if (child.kind === "thinking" && child.messageKey) {
|
|
515
|
-
thinkingComponents.get(child.messageKey)?.invalidate?.();
|
|
516
|
-
} else if (child.kind === "update") {
|
|
517
|
-
activityUpdateComponents.get(child.id)?.invalidate?.();
|
|
518
|
-
}
|
|
575
|
+
revealActivityGroup(group);
|
|
576
|
+
return { handled: true };
|
|
519
577
|
}
|
|
520
|
-
|
|
578
|
+
const summaryHeight = position.first ? renderActivityGroupSummary(group, event.width).length : 0;
|
|
579
|
+
const onSummary = position.first && event.y > 0 && event.y <= summaryHeight;
|
|
580
|
+
const onCollapse = position.last && activityCollapseClicked(
|
|
581
|
+
group, member, event, renderActivityUpdate(group, member, event.width).length - 1,
|
|
582
|
+
);
|
|
583
|
+
if (!onSummary && !onCollapse) return undefined;
|
|
584
|
+
collapseActivityGroup(group);
|
|
521
585
|
return { handled: true };
|
|
522
586
|
};
|
|
523
587
|
|
|
@@ -606,10 +670,16 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
606
670
|
|
|
607
671
|
cleanCompactToolCallIds.clear();
|
|
608
672
|
revealedActivityGroups.clear();
|
|
673
|
+
collapsedActivityGroups.clear();
|
|
609
674
|
expandedThinkingMembers.clear();
|
|
610
675
|
renderMode = requested;
|
|
611
|
-
if (renderMode === "clean")
|
|
612
|
-
|
|
676
|
+
if (renderMode === "clean") {
|
|
677
|
+
if (cleanRun.active && !cleanRun.settled) startWorkingElapsedTimer();
|
|
678
|
+
refreshLatestActivityWidget();
|
|
679
|
+
} else {
|
|
680
|
+
stopWorkingElapsedTimer();
|
|
681
|
+
clearLatestActivityStatus();
|
|
682
|
+
}
|
|
613
683
|
for (const component of cleanToolComponents.values()) component.updateDisplay?.();
|
|
614
684
|
for (const component of thinkingComponents.values()) component.invalidate?.();
|
|
615
685
|
try {
|
|
@@ -625,6 +695,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
625
695
|
// Keep the settings command available while disabled, but do not install
|
|
626
696
|
// render patches or override built-in tools until the next enabled reload.
|
|
627
697
|
if (!activeForSession) return;
|
|
698
|
+
pi.on("session_shutdown", stopWorkingElapsedTimer);
|
|
628
699
|
|
|
629
700
|
// Give Pi's main prompt editor a complete rounded frame. Render the native
|
|
630
701
|
// editor at a two-column narrower width so cursor layout, wrapping, IME, and
|
|
@@ -635,7 +706,30 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
635
706
|
const hadOwnRender = Object.prototype.hasOwnProperty.call(customEditorPrototype, "render");
|
|
636
707
|
const hadOwnHandleMouse = Object.prototype.hasOwnProperty.call(customEditorPrototype, "handleMouse");
|
|
637
708
|
const originalEditorRender = customEditorPrototype.render;
|
|
709
|
+
const originalEditorTopBorder = customEditorPrototype.renderTopBorder;
|
|
638
710
|
const originalEditorHandleMouse = customEditorPrototype.handleMouse;
|
|
711
|
+
const patchedEditorTopBorder = function (this: any, width: number, hiddenLineCount: number): string {
|
|
712
|
+
if (this.tui) currentTui = this.tui;
|
|
713
|
+
const indicator = this.workingStatusIndicator;
|
|
714
|
+
if (renderMode !== "clean" || indicator?.kind !== "working" || responseRunStartedAt === undefined) {
|
|
715
|
+
return originalEditorTopBorder.call(this, width, hiddenLineCount);
|
|
716
|
+
}
|
|
717
|
+
const elapsed = formatResponseDuration(Date.now() - responseRunStartedAt);
|
|
718
|
+
const indicatorColor = (text: string) => this.borderColor(text);
|
|
719
|
+
const originalRenderInBorder = indicator.renderInBorder;
|
|
720
|
+
indicator.renderInBorder = function (availableWidth: number): string {
|
|
721
|
+
const status = originalRenderInBorder.call(this, availableWidth);
|
|
722
|
+
const suffix = ` ${elapsed}`;
|
|
723
|
+
return visibleWidth(status) + visibleWidth(suffix) <= availableWidth
|
|
724
|
+
? status + indicatorColor(suffix)
|
|
725
|
+
: status;
|
|
726
|
+
};
|
|
727
|
+
try {
|
|
728
|
+
return originalEditorTopBorder.call(this, width, hiddenLineCount);
|
|
729
|
+
} finally {
|
|
730
|
+
indicator.renderInBorder = originalRenderInBorder;
|
|
731
|
+
}
|
|
732
|
+
};
|
|
639
733
|
const patchedEditorRender = function (this: any, width: number): string[] {
|
|
640
734
|
if (width < 3) return originalEditorRender.call(this, width);
|
|
641
735
|
|
|
@@ -669,11 +763,14 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
669
763
|
hadOwnRender,
|
|
670
764
|
hadOwnHandleMouse,
|
|
671
765
|
originalRender: originalEditorRender,
|
|
766
|
+
originalTopBorder: originalEditorTopBorder,
|
|
672
767
|
originalHandleMouse: originalEditorHandleMouse,
|
|
673
768
|
patchedRender: patchedEditorRender,
|
|
769
|
+
patchedTopBorder: patchedEditorTopBorder,
|
|
674
770
|
patchedHandleMouse: patchedEditorHandleMouse,
|
|
675
771
|
};
|
|
676
772
|
customEditorPrototype.render = patchedEditorRender;
|
|
773
|
+
customEditorPrototype.renderTopBorder = patchedEditorTopBorder;
|
|
677
774
|
customEditorPrototype.handleMouse = patchedEditorHandleMouse;
|
|
678
775
|
|
|
679
776
|
pi.on("session_shutdown", () => {
|
|
@@ -683,12 +780,16 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
683
780
|
if (patch.hadOwnRender) customEditorPrototype.render = patch.originalRender;
|
|
684
781
|
else delete customEditorPrototype.render;
|
|
685
782
|
}
|
|
783
|
+
if (patch.patchedTopBorder === customEditorPrototype.renderTopBorder) {
|
|
784
|
+
customEditorPrototype.renderTopBorder = patch.originalTopBorder;
|
|
785
|
+
}
|
|
686
786
|
if (patch.patchedHandleMouse === customEditorPrototype.handleMouse) {
|
|
687
787
|
if (patch.hadOwnHandleMouse) customEditorPrototype.handleMouse = patch.originalHandleMouse;
|
|
688
788
|
else delete customEditorPrototype.handleMouse;
|
|
689
789
|
}
|
|
690
790
|
if (
|
|
691
791
|
customEditorPrototype.render === patch.originalRender &&
|
|
792
|
+
customEditorPrototype.renderTopBorder === patch.originalTopBorder &&
|
|
692
793
|
customEditorPrototype.handleMouse === patch.originalHandleMouse
|
|
693
794
|
) {
|
|
694
795
|
delete customEditorPrototype[editorFramePatchKey];
|
|
@@ -1015,15 +1116,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1015
1116
|
);
|
|
1016
1117
|
|
|
1017
1118
|
const refreshGroup = (group: ActivityGroup) => {
|
|
1018
|
-
|
|
1019
|
-
if (member.kind === "tool" && member.toolCallId) {
|
|
1020
|
-
cleanToolComponents.get(member.toolCallId)?.updateDisplay?.();
|
|
1021
|
-
} else if (member.kind === "thinking" && member.messageKey) {
|
|
1022
|
-
thinkingComponents.get(member.messageKey)?.invalidate?.();
|
|
1023
|
-
} else if (member.kind === "update") {
|
|
1024
|
-
activityUpdateComponents.get(member.id)?.invalidate?.();
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1119
|
+
refreshActivityGroup(group);
|
|
1027
1120
|
currentTui?.requestRender?.();
|
|
1028
1121
|
};
|
|
1029
1122
|
|
|
@@ -1189,9 +1282,10 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1189
1282
|
const decorated = contentLines.map((line, index) =>
|
|
1190
1283
|
truncateToWidth((index === 0 ? prefix : continuation) + line, Math.max(1, width), "")
|
|
1191
1284
|
);
|
|
1192
|
-
const output =
|
|
1193
|
-
|
|
1194
|
-
: decorated
|
|
1285
|
+
const output = withActivityCollapse(
|
|
1286
|
+
group, member, width,
|
|
1287
|
+
position.first ? ["", ...renderActivityGroupSummary(group, width), ...decorated] : decorated,
|
|
1288
|
+
);
|
|
1195
1289
|
if (cacheable) {
|
|
1196
1290
|
this[cleanThinkingRenderCacheKey] = {
|
|
1197
1291
|
width,
|
|
@@ -1232,10 +1326,11 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1232
1326
|
// visible Markdown. The Markdown is rendered after the activity rows;
|
|
1233
1327
|
// route clicks in that suffix back to Pi's native component so links,
|
|
1234
1328
|
// text selection, and code-block Copy controls keep working.
|
|
1329
|
+
let activityHeight: number | undefined;
|
|
1235
1330
|
if (visibleAssistantText(message)) {
|
|
1236
1331
|
const visibleLines = originalRender.call(this, event.width);
|
|
1237
1332
|
const renderedLines = patchedRender.call(this, event.width);
|
|
1238
|
-
|
|
1333
|
+
activityHeight = Math.max(0, renderedLines.length - visibleLines.length);
|
|
1239
1334
|
if (event.y >= activityHeight) {
|
|
1240
1335
|
return originalHandleMouse.call(this, {
|
|
1241
1336
|
...event,
|
|
@@ -1247,25 +1342,21 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1247
1342
|
|
|
1248
1343
|
if (!activityGroupRevealed(group)) {
|
|
1249
1344
|
if (!isLeftClick) return undefined;
|
|
1250
|
-
if (position.first)
|
|
1251
|
-
revealedActivityGroups.add(group.id);
|
|
1252
|
-
for (const toolCallId of group.toolCallIds) cleanCompactToolCallIds.add(toolCallId);
|
|
1253
|
-
refreshGroup(group);
|
|
1254
|
-
}
|
|
1345
|
+
if (position.first) revealActivityGroup(group);
|
|
1255
1346
|
return { handled: true };
|
|
1256
1347
|
}
|
|
1257
1348
|
const summaryHeight = position.first ? renderActivityGroupSummary(group, event.width).length : 0;
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
refreshGroup(group);
|
|
1349
|
+
const collapseRow = position.last
|
|
1350
|
+
? (activityHeight ?? patchedRender.call(this, event.width).length) - 1
|
|
1351
|
+
: -1;
|
|
1352
|
+
if (
|
|
1353
|
+
(position.first && event.y > 0 && event.y <= summaryHeight && isLeftClick) ||
|
|
1354
|
+
activityCollapseClicked(group, member, event, collapseRow)
|
|
1355
|
+
) {
|
|
1356
|
+
collapseActivityGroup(group);
|
|
1267
1357
|
return { handled: true };
|
|
1268
1358
|
}
|
|
1359
|
+
if (position.last && event.y === collapseRow) return undefined;
|
|
1269
1360
|
if (isLeftClick) {
|
|
1270
1361
|
if (expandedThinkingMembers.has(member.id)) expandedThinkingMembers.delete(member.id);
|
|
1271
1362
|
else expandedThinkingMembers.add(member.id);
|
|
@@ -1390,6 +1481,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1390
1481
|
const originalShowError = interactiveModePrototype.showError;
|
|
1391
1482
|
const patchedSetToolsExpanded = function (this: any, expanded: boolean) {
|
|
1392
1483
|
cleanToolsExpanded = expanded;
|
|
1484
|
+
collapsedActivityGroups.clear();
|
|
1393
1485
|
changingAllToolsExpansion = true;
|
|
1394
1486
|
if (!expanded) {
|
|
1395
1487
|
cleanCompactToolCallIds.clear();
|
|
@@ -2426,32 +2518,8 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2426
2518
|
const originalToolHandleMouse = toolExecutionPrototype.handleMouse;
|
|
2427
2519
|
const renderedModeKey = Symbol("pretty-tui.tool-rendered-mode");
|
|
2428
2520
|
const cleanChildRenderCacheKey = Symbol("pretty-tui.clean-child-render-cache");
|
|
2429
|
-
const
|
|
2430
|
-
|
|
2431
|
-
if (member.kind === "tool" && member.toolCallId) {
|
|
2432
|
-
cleanToolComponents.get(member.toolCallId)?.updateDisplay();
|
|
2433
|
-
} else if (member.kind === "thinking" && member.messageKey) {
|
|
2434
|
-
thinkingComponents.get(member.messageKey)?.invalidate?.();
|
|
2435
|
-
}
|
|
2436
|
-
}
|
|
2437
|
-
};
|
|
2438
|
-
const revealCleanGroup = (group: ActivityGroup, ui?: any) => {
|
|
2439
|
-
revealedActivityGroups.add(group.id);
|
|
2440
|
-
for (const toolCallId of group.toolCallIds) cleanCompactToolCallIds.add(toolCallId);
|
|
2441
|
-
refreshActivityGroup(group);
|
|
2442
|
-
ui?.requestRender?.();
|
|
2443
|
-
};
|
|
2444
|
-
const collapseCleanGroup = (group: ActivityGroup, ui?: any) => {
|
|
2445
|
-
revealedActivityGroups.delete(group.id);
|
|
2446
|
-
for (const toolCallId of group.toolCallIds) {
|
|
2447
|
-
const component = cleanToolComponents.get(toolCallId);
|
|
2448
|
-
if (component?.expanded) originalSetExpanded.call(component, false);
|
|
2449
|
-
cleanCompactToolCallIds.delete(toolCallId);
|
|
2450
|
-
}
|
|
2451
|
-
for (const member of group.members) expandedThinkingMembers.delete(member.id);
|
|
2452
|
-
refreshActivityGroup(group);
|
|
2453
|
-
ui?.requestRender?.();
|
|
2454
|
-
};
|
|
2521
|
+
const revealCleanGroup = revealActivityGroup;
|
|
2522
|
+
const collapseCleanGroup = collapseActivityGroup;
|
|
2455
2523
|
const patchedMarkExecutionStarted = function (this: any) {
|
|
2456
2524
|
activityTimeline.addTool(this.toolCallId, this.toolName);
|
|
2457
2525
|
if (!cleanRun.currentToolCallIds.includes(this.toolCallId)) {
|
|
@@ -2512,7 +2580,11 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2512
2580
|
continuation,
|
|
2513
2581
|
childWidth,
|
|
2514
2582
|
} = activityTreeStyle(group, member, width, childTheme);
|
|
2515
|
-
|
|
2583
|
+
// Other extensions (notably FFF in override mode) can register their own
|
|
2584
|
+
// grep/find renderers under built-in names. Their default boxed shell is
|
|
2585
|
+
// not one of our self-rendering specialized tools.
|
|
2586
|
+
const thirdParty = !SPECIALIZED_TOOL_NAMES.has(this.toolName) ||
|
|
2587
|
+
this.toolDefinition?.renderShell !== "self";
|
|
2516
2588
|
const cacheable = !this.expanded && Boolean(this.result) && !this.isPartial;
|
|
2517
2589
|
const cached = cacheable ? this[cleanChildRenderCacheKey] : undefined;
|
|
2518
2590
|
let decoratedContent: string[];
|
|
@@ -2578,8 +2650,10 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2578
2650
|
};
|
|
2579
2651
|
}
|
|
2580
2652
|
}
|
|
2581
|
-
|
|
2582
|
-
|
|
2653
|
+
return withActivityCollapse(
|
|
2654
|
+
group, member, width,
|
|
2655
|
+
position.first ? ["", ...renderActivityGroupSummary(group, width), ...decoratedContent] : decoratedContent,
|
|
2656
|
+
);
|
|
2583
2657
|
};
|
|
2584
2658
|
const patchedToolHandleMouse = function (this: any, event: any) {
|
|
2585
2659
|
const member = activityTimeline.memberForTool(this.toolCallId);
|
|
@@ -2598,7 +2672,11 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2598
2672
|
if (!revealed) return isLeftClick ? { handled: true } : undefined;
|
|
2599
2673
|
|
|
2600
2674
|
const summaryHeight = position.first ? renderActivityGroupSummary(group, event.width).length : 0;
|
|
2601
|
-
|
|
2675
|
+
const onSummary = position.first && event.y > 0 && event.y <= summaryHeight && isLeftClick;
|
|
2676
|
+
const onCollapse = position.last && activityCollapseClicked(
|
|
2677
|
+
group, member, event, patchedToolRender.call(this, event.width).length - 1,
|
|
2678
|
+
);
|
|
2679
|
+
if (onSummary || onCollapse) {
|
|
2602
2680
|
changingAllToolsExpansion = true;
|
|
2603
2681
|
try {
|
|
2604
2682
|
collapseCleanGroup(group, this.ui);
|
|
@@ -2612,7 +2690,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2612
2690
|
// replaces that spacer with the compact header for non-first members.
|
|
2613
2691
|
const thirdPartyHeaderY = position.first ? summaryHeight + 1 : 0;
|
|
2614
2692
|
if (
|
|
2615
|
-
!SPECIALIZED_TOOL_NAMES.has(this.toolName) &&
|
|
2693
|
+
(!SPECIALIZED_TOOL_NAMES.has(this.toolName) || this.toolDefinition?.renderShell !== "self") &&
|
|
2616
2694
|
isLeftClick &&
|
|
2617
2695
|
event.y === thirdPartyHeaderY
|
|
2618
2696
|
) {
|
|
@@ -2766,7 +2844,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2766
2844
|
prefix: () => {
|
|
2767
2845
|
const currentActivity = typeof activity === "function" ? activity() : activity;
|
|
2768
2846
|
const color = currentActivity === "done"
|
|
2769
|
-
? collapsedDone ? "thinkingText" :
|
|
2847
|
+
? collapsedDone ? "thinkingText" : "success"
|
|
2770
2848
|
: "accent";
|
|
2771
2849
|
return theme.fg(color, "● ");
|
|
2772
2850
|
},
|
|
@@ -2775,7 +2853,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2775
2853
|
const currentActivity = typeof activity === "function" ? activity() : activity;
|
|
2776
2854
|
const label = currentActivity === "done" ? "Done" : "Running";
|
|
2777
2855
|
const color = label === "Done"
|
|
2778
|
-
? collapsedDone ? "thinkingText" :
|
|
2856
|
+
? collapsedDone ? "thinkingText" : "success"
|
|
2779
2857
|
: "accent";
|
|
2780
2858
|
const detailColor = label === "Done" && collapsedDone ? "thinkingText" : "text";
|
|
2781
2859
|
return theme.fg(color, theme.bold(label)) +
|
|
@@ -3063,6 +3141,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3063
3141
|
);
|
|
3064
3142
|
|
|
3065
3143
|
const restoreCleanSession = (ctx: any) => {
|
|
3144
|
+
stopWorkingElapsedTimer();
|
|
3066
3145
|
currentExtensionUi = ctx.ui;
|
|
3067
3146
|
clearLatestActivityStatus();
|
|
3068
3147
|
cleanToolsExpanded = ctx.ui.getToolsExpanded();
|
|
@@ -3074,6 +3153,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3074
3153
|
responseHasAssistantMessage = false;
|
|
3075
3154
|
nativeCustomBoundaryKeys.clear();
|
|
3076
3155
|
revealedActivityGroups.clear();
|
|
3156
|
+
collapsedActivityGroups.clear();
|
|
3077
3157
|
expandedThinkingMembers.clear();
|
|
3078
3158
|
thinkingComponents.clear();
|
|
3079
3159
|
activityFallbackThemes.clear();
|
|
@@ -3486,6 +3566,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3486
3566
|
cleanRun.activeToolCallId = undefined;
|
|
3487
3567
|
cleanRun.activeToolName = undefined;
|
|
3488
3568
|
cleanRun.activity = "thinking";
|
|
3569
|
+
if (renderMode === "clean") startWorkingElapsedTimer();
|
|
3489
3570
|
});
|
|
3490
3571
|
pi.on("tool_execution_start", (event) => {
|
|
3491
3572
|
activityTimeline.addTool(event.toolCallId, event.toolName);
|
|
@@ -3535,6 +3616,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3535
3616
|
cleanRun.activity = "thinking";
|
|
3536
3617
|
});
|
|
3537
3618
|
pi.on("agent_settled", (_event, ctx) => {
|
|
3619
|
+
stopWorkingElapsedTimer();
|
|
3538
3620
|
if (cleanRun.settled) return;
|
|
3539
3621
|
|
|
3540
3622
|
clearPendingToolActivities();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-pretty-tui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "A beautiful, polished TUI for the Pi coding agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Kainan Yang <ykn0309@whu.edu.cn>",
|
|
@@ -52,6 +52,6 @@
|
|
|
52
52
|
"extensions": [
|
|
53
53
|
"./extensions/index.ts"
|
|
54
54
|
],
|
|
55
|
-
"image": "https://
|
|
55
|
+
"image": "https://raw.githubusercontent.com/ykn0309/pi-pretty-tui/main/assets/screenshot.png"
|
|
56
56
|
}
|
|
57
57
|
}
|