pi-pretty-tui 0.2.3 → 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 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.
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]` 控件。
Binary file
@@ -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.max(1, Math.round(Math.max(0, durationMs) / 1000));
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;
@@ -663,8 +673,13 @@ export default function prettyTui(pi: ExtensionAPI) {
663
673
  collapsedActivityGroups.clear();
664
674
  expandedThinkingMembers.clear();
665
675
  renderMode = requested;
666
- if (renderMode === "clean") refreshLatestActivityWidget();
667
- else clearLatestActivityStatus();
676
+ if (renderMode === "clean") {
677
+ if (cleanRun.active && !cleanRun.settled) startWorkingElapsedTimer();
678
+ refreshLatestActivityWidget();
679
+ } else {
680
+ stopWorkingElapsedTimer();
681
+ clearLatestActivityStatus();
682
+ }
668
683
  for (const component of cleanToolComponents.values()) component.updateDisplay?.();
669
684
  for (const component of thinkingComponents.values()) component.invalidate?.();
670
685
  try {
@@ -680,6 +695,7 @@ export default function prettyTui(pi: ExtensionAPI) {
680
695
  // Keep the settings command available while disabled, but do not install
681
696
  // render patches or override built-in tools until the next enabled reload.
682
697
  if (!activeForSession) return;
698
+ pi.on("session_shutdown", stopWorkingElapsedTimer);
683
699
 
684
700
  // Give Pi's main prompt editor a complete rounded frame. Render the native
685
701
  // editor at a two-column narrower width so cursor layout, wrapping, IME, and
@@ -690,7 +706,30 @@ export default function prettyTui(pi: ExtensionAPI) {
690
706
  const hadOwnRender = Object.prototype.hasOwnProperty.call(customEditorPrototype, "render");
691
707
  const hadOwnHandleMouse = Object.prototype.hasOwnProperty.call(customEditorPrototype, "handleMouse");
692
708
  const originalEditorRender = customEditorPrototype.render;
709
+ const originalEditorTopBorder = customEditorPrototype.renderTopBorder;
693
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
+ };
694
733
  const patchedEditorRender = function (this: any, width: number): string[] {
695
734
  if (width < 3) return originalEditorRender.call(this, width);
696
735
 
@@ -724,11 +763,14 @@ export default function prettyTui(pi: ExtensionAPI) {
724
763
  hadOwnRender,
725
764
  hadOwnHandleMouse,
726
765
  originalRender: originalEditorRender,
766
+ originalTopBorder: originalEditorTopBorder,
727
767
  originalHandleMouse: originalEditorHandleMouse,
728
768
  patchedRender: patchedEditorRender,
769
+ patchedTopBorder: patchedEditorTopBorder,
729
770
  patchedHandleMouse: patchedEditorHandleMouse,
730
771
  };
731
772
  customEditorPrototype.render = patchedEditorRender;
773
+ customEditorPrototype.renderTopBorder = patchedEditorTopBorder;
732
774
  customEditorPrototype.handleMouse = patchedEditorHandleMouse;
733
775
 
734
776
  pi.on("session_shutdown", () => {
@@ -738,12 +780,16 @@ export default function prettyTui(pi: ExtensionAPI) {
738
780
  if (patch.hadOwnRender) customEditorPrototype.render = patch.originalRender;
739
781
  else delete customEditorPrototype.render;
740
782
  }
783
+ if (patch.patchedTopBorder === customEditorPrototype.renderTopBorder) {
784
+ customEditorPrototype.renderTopBorder = patch.originalTopBorder;
785
+ }
741
786
  if (patch.patchedHandleMouse === customEditorPrototype.handleMouse) {
742
787
  if (patch.hadOwnHandleMouse) customEditorPrototype.handleMouse = patch.originalHandleMouse;
743
788
  else delete customEditorPrototype.handleMouse;
744
789
  }
745
790
  if (
746
791
  customEditorPrototype.render === patch.originalRender &&
792
+ customEditorPrototype.renderTopBorder === patch.originalTopBorder &&
747
793
  customEditorPrototype.handleMouse === patch.originalHandleMouse
748
794
  ) {
749
795
  delete customEditorPrototype[editorFramePatchKey];
@@ -2798,7 +2844,7 @@ export default function prettyTui(pi: ExtensionAPI) {
2798
2844
  prefix: () => {
2799
2845
  const currentActivity = typeof activity === "function" ? activity() : activity;
2800
2846
  const color = currentActivity === "done"
2801
- ? collapsedDone ? "thinkingText" : successMarkerColor(theme)
2847
+ ? collapsedDone ? "thinkingText" : "success"
2802
2848
  : "accent";
2803
2849
  return theme.fg(color, "● ");
2804
2850
  },
@@ -2807,7 +2853,7 @@ export default function prettyTui(pi: ExtensionAPI) {
2807
2853
  const currentActivity = typeof activity === "function" ? activity() : activity;
2808
2854
  const label = currentActivity === "done" ? "Done" : "Running";
2809
2855
  const color = label === "Done"
2810
- ? collapsedDone ? "thinkingText" : successMarkerColor(theme)
2856
+ ? collapsedDone ? "thinkingText" : "success"
2811
2857
  : "accent";
2812
2858
  const detailColor = label === "Done" && collapsedDone ? "thinkingText" : "text";
2813
2859
  return theme.fg(color, theme.bold(label)) +
@@ -3095,6 +3141,7 @@ export default function prettyTui(pi: ExtensionAPI) {
3095
3141
  );
3096
3142
 
3097
3143
  const restoreCleanSession = (ctx: any) => {
3144
+ stopWorkingElapsedTimer();
3098
3145
  currentExtensionUi = ctx.ui;
3099
3146
  clearLatestActivityStatus();
3100
3147
  cleanToolsExpanded = ctx.ui.getToolsExpanded();
@@ -3519,6 +3566,7 @@ export default function prettyTui(pi: ExtensionAPI) {
3519
3566
  cleanRun.activeToolCallId = undefined;
3520
3567
  cleanRun.activeToolName = undefined;
3521
3568
  cleanRun.activity = "thinking";
3569
+ if (renderMode === "clean") startWorkingElapsedTimer();
3522
3570
  });
3523
3571
  pi.on("tool_execution_start", (event) => {
3524
3572
  activityTimeline.addTool(event.toolCallId, event.toolName);
@@ -3568,6 +3616,7 @@ export default function prettyTui(pi: ExtensionAPI) {
3568
3616
  cleanRun.activity = "thinking";
3569
3617
  });
3570
3618
  pi.on("agent_settled", (_event, ctx) => {
3619
+ stopWorkingElapsedTimer();
3571
3620
  if (cleanRun.settled) return;
3572
3621
 
3573
3622
  clearPendingToolActivities();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-pretty-tui",
3
- "version": "0.2.3",
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://unpkg.com/pi-pretty-tui@0.2.0/assets/screenshot.png"
55
+ "image": "https://raw.githubusercontent.com/ykn0309/pi-pretty-tui/main/assets/screenshot.png"
56
56
  }
57
57
  }