pi-pretty-tui 0.2.9 → 0.3.1

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,13 +7,13 @@ 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 show a combined total of top-level and nested tool calls, plus 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. The clean-mode `Running` parent dot alternates between the prompt editor’s current border color and `dim` about every 800 ms; its label uses that same border color, and tool dots stay stable, and the pulse stops when the run settles. Expanded `Done` titles and markers use the theme's `success` color; collapsed summary dots, titles, and details use `dim`.
10
+ - **Live working duration:** in all three modes, 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. The clean-mode `Running` parent dot alternates between the prompt editor’s current border color and `dim` about every 800 ms; its label uses that same border color, and tool dots stay stable, and the pulse stops when the run settles. Expanded `Done` titles and markers use the theme's `success` color; collapsed summary dots, titles, and details use `dim`.
11
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.
12
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.
13
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.
14
- - **A cohesive visual finish:** transparent, right-aligned user chat bubbles size themselves to short messages and wrap longer ones at 75% of the terminal width, with a rounded border in the theme's link color (`mdLink`) and no redundant `User` label. Together with the rounded prompt input, theme-aware colors, clear Markdown heading hierarchy, cleaner lists, and rounded syntax-highlighted code blocks, they make the entire TUI feel intentional. In fullscreen, each fenced code block also includes a native-feeling `[Copy]` control.
14
+ - **A cohesive visual finish:** user messages use a rounded border in the theme's heading color (`mdHeading`, warm amber in the default theme), distinct from blue `mdLink` thoughts. Full and compact use background-filled, full-width `User` panels matching Assistant cards. Clean keeps transparent, right-aligned, content-sized bubbles, wrapping long messages at 75% of the terminal width without a `User` label. Together with the rounded prompt input, theme-aware colors, clear Markdown heading hierarchy, cleaner lists, and rounded syntax-highlighted code blocks, they make the entire TUI feel intentional. In fullscreen, each fenced code block also includes a native-feeling `[Copy]` control.
15
15
 
16
- Pi's built-in `read`, `bash`, `edit`, `write`, `grep`, `find`, and `ls` tools receive purpose-built compact views. Third-party tools without nested calls automatically receive a consistent compact row; opening it preserves its native renderer while removing conflicting terminal background colors. User and steering messages, compaction summaries, visible assistant text, warnings, errors, aborted/truncated responses, and displayed custom messages with a registered renderer create hard group boundaries. Pi's cursor, IME, autocomplete, and tool execution behavior remain intact; fullscreen mouse additions are described below.
16
+ Clean uses purpose-built compact views for Pi's built-in `read`, `bash`, `edit`, `write`, `grep`, `find`, and `ls` tools. Compact uses concise summaries when folded and the same native detail cards as full when expanded. Third-party tools without nested calls automatically receive a consistent compact row; opening it preserves its native renderer while removing conflicting terminal background colors. User and steering messages, compaction summaries, visible assistant text, warnings, errors, aborted/truncated responses, and displayed custom messages with a registered renderer create hard group boundaries. Pi's cursor, IME, autocomplete, and tool execution behavior remain intact; fullscreen mouse additions are described below.
17
17
 
18
18
  ## Install
19
19
 
@@ -23,19 +23,19 @@ pi install npm:pi-pretty-tui
23
23
 
24
24
  Restart Pi or run `/reload` after installation.
25
25
 
26
- Pi may warn that built-in tools are being overridden. This is expected: pi-pretty-tui re-registers Pi's built-in tools and delegates execution to their original implementations, changing only their TUI renderers.
26
+ pi-pretty-tui registers renderers without replacing tool implementations. Extensions that provide tools with built-in names, such as `bash`, keep their own execution and renderers.
27
27
 
28
28
  ## Working with tool calls
29
29
 
30
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. In clean mode, `Ctrl+O` only shows a hint to use these local disclosures; it does not expand or collapse content. Full and compact modes retain Pi's native global tool-output expansion. Third-party replacements for `grep` and `find` (such as FFF) use compact tree rows too, with their native details available when expanded. Compact tool names use bold `text`, parameters use `muted`, and running tool dots use `dim`; success/error dots keep their corresponding theme colors. Thought dots use `mdLink`, titles use bold `text`, and detail body text uses `muted`. Tool names use each definition's `label` verbatim, falling back to its raw `name`; pi-pretty-tui does not rename or title-case them.
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
+ In clean and compact, extension calls to `ctx.ui.notify(message, "info")` (or with the type omitted) use the same presentation. Clean joins them to current thought/tool work, while compact displays them independently. Warnings and errors stay standalone, use `⚠` and `✕`, and form hard boundaries. Tool failures remain owned by their tool details. Full delegates info, warning, and error delivery to Pi's original methods, retaining its message text, individual occurrences, and status behavior.
33
33
 
34
- In clean mode, Pi's native cache-miss notices join the thought/tool group of the assistant message that incurred the miss, as a yellow `⚠` child. They hide and reveal with the group and do not interrupt subsequent tool calls. If that message has no associated work (for example, a text-only final answer), the notice remains independent. Pi still decides whether a notice should appear and formats its token count, cost, model-switch and idle-gap labels; the integration hooks `addCacheMissNotice()` rather than matching text. Historical notices are re-derived on rebuild, not written as extra session entries. Full and compact modes retain Pi's native presentation.
34
+ In clean mode, Pi's native cache-miss notices join the thought/tool group of the assistant message that incurred the miss, as a yellow `⚠` child. They hide and reveal with the group and do not interrupt subsequent tool calls. If that message has no associated work (for example, a text-only final answer), the notice remains independent. Pi still decides whether a notice should appear and formats its token count, cost, model-switch and idle-gap labels; the integration hooks `addCacheMissNotice()` rather than matching text. Historical notices are re-derived on rebuild, not written as extra session entries. Full and compact show the same warning independently, without an activity-group wrapper.
35
35
 
36
- Displayed custom messages without their own renderer use one generic accent-coloured `◇` Markdown presentation: they join current thought/tool work when it exists, otherwise remain independent, and preserve their session persistence. Messages with a registered renderer remain completely native and form hard boundaries. These decisions use only Pi's notification type and renderer presence—never extension names, custom message names, or content heuristics.
36
+ In clean and compact, displayed custom messages without their own renderer use one generic accent-coloured `◇` Markdown presentation: they join current thought/tool work when it exists, otherwise remain independent, and preserve their session persistence. Messages with a registered renderer remain completely native and form hard boundaries. Full retains native custom-message rendering as well. These decisions use only Pi's notification type and renderer presence—never extension names, custom message names, or content heuristics.
37
37
 
38
- Pi's direct internal `showStatus()` feedback is transient: it appears only in the status line above the editor, a newer status replaces it, and a ten-second timer removes it. It never enters the transcript or activity timeline and is cleared on reload. In clean mode, `Ctrl+T` and `Ctrl+O` only show short hints to click the corresponding disclosure; neither changes visibility or Pi's Thinking setting. In full and compact modes, both shortcuts keep Pi's native behaviour.
38
+ In clean and compact, Pi's direct internal `showStatus()` feedback is transient: it appears only in the status line above the editor, a newer status replaces it, and a ten-second timer removes it. It never enters the transcript or activity timeline and is cleared on reload. In clean mode, `Ctrl+T` and `Ctrl+O` only show short hints to click the corresponding disclosure; neither changes visibility or Pi's Thinking setting. In full and compact modes, both shortcuts keep Pi's native behaviour. Full also preserves native `showStatus()` delivery instead of removing transcript feedback on a ten-second timer.
39
39
 
40
40
  Visible assistant text remains outside activity trees even when a later update to the same message introduces thinking. Repeated updates do not close a separate, subsequent tool group.
41
41
 
@@ -43,15 +43,15 @@ Visible assistant text remains outside activity trees even when a later update t
43
43
 
44
44
  ## Error presentation
45
45
 
46
- In clean mode, standalone errors keep their original title and complete text in a compact `✕ Error · reason` layout. Consecutive errors with exactly the same source type, title, and full text share one visible row with a dim `×2` / `×3` count. Other transcript components break the run; errors carrying thought/tool/visible-response content are not folded. This is UI-only: session records and original message components remain unchanged. Long errors wrap without an added preview truncation; full/compact modes keep native presentation.
46
+ In clean and compact modes, standalone errors keep their original title and complete text in a compact `✕ Error · reason` layout. Consecutive errors with exactly the same source type, title, and full text share one visible row with a dim `×2` / `×3` count. Other transcript components break the run; errors carrying thought/tool/visible-response content are not folded. This is UI-only: session records and original message components remain unchanged. Long errors wrap without an added preview truncation. Full mode keeps native assistant error text and individual occurrences; it does not merge errors into a repetition count.
47
47
 
48
48
  ## Codemode and nested tools
49
49
 
50
- In clean mode, tools that call other tools through Pi's nested execution API keep their own parent node. Expand the activity group to see each internal call, its status, compact arguments, duration, and result summary. Click an internal call to inspect its arguments and text result independently. Click its expanded body to collapse just that call. Script and script-output bodies also collapse their own sections when clicked. Tool calls use a `●` marker: dim while running, success-colored when successful, and error-colored when failed. Tool names use bold theme text. `[Script]` and `[Script output]` are separate disclosures; the nested parent title is informational and clicking it does not expand or collapse details. Use each section or child to expand and collapse its own details; `Ctrl+O` only shows a hint in clean mode. Extensions can still use Pi's `setToolsExpanded()` API for global control; local disclosures remain usable afterward, and another global API call resets the local overrides. Calls retain their start order even when they finish in parallel. A handled child failure remains marked as failed even if the script succeeds.
50
+ Clean and compact modes share the same nested-tool presentation. Tools that call other tools through Pi's nested execution API keep their own parent node. In clean mode, expand the activity group first; in compact mode, each parent appears independently without an activity-group wrapper. Both show each internal call, its status, compact arguments, duration, and result summary. Click an internal call to inspect its arguments and text result independently. Click its expanded body to collapse just that call. Script and script-output bodies also collapse their own sections when clicked. Tool calls use a `●` marker: dim while running, success-colored when successful, and error-colored when failed. Tool names use bold theme text. `[Script]` and `[Script output]` are separate disclosures; the nested parent title is informational and clicking it does not expand or collapse details. Use each section or child to expand and collapse its own details; `Ctrl+O` only shows a hint in clean mode. Extensions can still use Pi's `setToolsExpanded()` API for global control; local disclosures remain usable afterward, and another global API call resets the local overrides. Calls retain their start order even when they finish in parallel. A handled child failure remains marked as failed even if the script succeeds.
51
51
 
52
- The activity-group summary combines top-level and nested calls: one parent call plus three internal calls displays as `4 tool calls`. The parent call itself counts once, while its node retains the nested-call count and tree structure. Internal calls never become phantom transcript members or displace the end-of-tree collapse action. Ownership uses Pi's `parentToolCallId`, not tool names or JavaScript parsing; wrappers other than codemode benefit too. This requires a Pi version with nested execution events (0.99.0 or later); older Pi keeps its existing display. Full and compact modes retain the native tool presentation.
52
+ The activity-group summary combines top-level and nested calls: one parent call plus three internal calls displays as `4 tool calls`. The parent call itself counts once, while its node retains the nested-call count and tree structure. Internal calls never become phantom transcript members or displace the end-of-tree collapse action. Ownership uses Pi's `parentToolCallId`, not tool names or JavaScript parsing; wrappers other than codemode benefit too. This requires a Pi version with nested execution events (0.99.0 or later); older Pi keeps its existing display. Full mode uses codemode's own native renderer inside a tool card. It does not append additional child details from pretty-tui snapshots; visible information, native disclosures, and keyboard controls follow Pi. Clean and compact share nested execution state, history snapshots, status handling, and local child disclosures; global expansion resets their local overrides.
53
53
 
54
- Child text results and edit diffs are saved in hidden, UI-only session entries for reload and branch navigation. Each root retains at most 256 calls, 4,096 characters of arguments per call, 8,192 characters of text per result, and 131,072 characters of arguments/results in total; truncation is indicated. Detail previews also have a 200-line limit, and nested trees display up to eight levels below the root. Old sessions without a snapshot explicitly show that only call metadata was retained. Nested results currently use a generic text presentation, not each third-party renderer, and images are not stored in these snapshots. Root media remains available in native full/compact mode.
54
+ Child text results and edit diffs are saved in hidden, UI-only session entries for reload and branch navigation. Each root retains at most 256 calls, 4,096 characters of arguments per call, 8,192 characters of text per result, and 131,072 characters of arguments/results in total; truncation is indicated. Detail previews also have a 200-line limit, and nested trees display up to eight levels below the root. Old sessions without a snapshot explicitly show that only call metadata was retained. Nested results currently use a generic text presentation, not each third-party renderer, and images are not stored in these snapshots. Root media and the parent's original renderer remain available in full mode.
55
55
 
56
56
  These UI entries do not change the script's output or the model's tool results. They **do** save child text that a script may never have printed: treat session files as potentially sensitive. Common credential argument keys are redacted; tool output is not guaranteed to be free of secrets.
57
57
 
@@ -59,9 +59,44 @@ These UI entries do not change the script's output or the model's tool results.
59
59
 
60
60
  Run `/pretty-tui` to open the extension settings. The `Enabled` switch persists whether pi-pretty-tui installs its render patches and built-in tool renderers. Changing it automatically runs Pi's official reload flow, so no manual `/reload` is needed. You can also use `/pretty-tui enable` and `/pretty-tui disable` directly. While disabled, the settings command remains available and Pi uses its native rendering.
61
61
 
62
+ All modes retain the working timer, completion footer, and copy enhancements. Clean/compact use transient status feedback; full retains native status delivery. Clean places `● thought` entries in activity groups. Compact uses separate thought panels, collapsed by default with a first-line content preview (truncated to one terminal row) and a `▸` title indicator; click the title to reveal Markdown details (`▾`). Full preserves native thinking runs, their original order among answers, Markdown transforms, and per-run clicks; each run is decorated with a titled frame. Thoughts start expanded in full even if a saved compact/native setting was hidden. Manual folding and subsequent `Ctrl+T` visibility updates remain available.
63
+
64
+ Full presents native Pi information and behavior in one consistent, theme-backed panel style. User, Assistant, thought and tool panels derive their fill from the corresponding outline color. Dark themes use the same hue with much lower lightness and chroma: blue, purple, green and red outlines produce deep matching tints, while neutral Assistant outlines produce a deep gray. Light themes use pale matching tints to preserve native dark-text contrast. Titles, outlines and bottom-border timing keep their semantic theme colors; color conversion is cached and follows theme/color-mode changes. Foreground syntax/diff colors survive, including ANSI resets inside native content, and native image protocol rows remain verbatim.
65
+
66
+ Full and compact panels leave one terminal column of outer space on each side (none in very narrow terminals). These gutters retain the terminal background and apply consistently to User, Assistant, thought and tool panels.
67
+
68
+ Each tool is one panel spanning the remaining width with the native call above an `Output` divider and the visible result below it. There are no separate result cards, connectors or result indentation. The output section appears only when native result content exists. Exterior layout whitespace is trimmed, while internal blank lines and image reservation rows remain. Separate tools retain two blank lines between panels. User cards fill the available width with a `User` title; adjacent answer blocks share one expanded `Assistant` card; thought cards remain expanded by default with local/native visibility controls.
69
+
70
+ Built-in tools use Pi's original renderers, including previews, truncation/expansion controls and error details. Full-mode bash commands additionally use Pi's shell syntax highlighter for variables, strings, keywords and comments. Highlighting is cached by command and syntax palette; the native `$` prompt, original command text, timeout annotation and result output are preserved. Shell/codemode `Took`/`Elapsed` timing lives on the bottom border in the outline color; narrow panels keep native timing in the body when it cannot fit. Third-party renderers, media and native mouse handlers remain intact. Clean retains its activity grouping and transparent user bubbles. Compact shares the same panel surfaces, outline colors, gutter and User/Assistant cards, but defaults to concise tool parameters and result summaries. Standalone tool panel titles show `▸` when collapsed and `▾` when expanded. Click the title to toggle details, and use native `Ctrl+O` for global expansion. User/Assistant panels and informational nested-parent titles have no disclosure arrow. Expanded standalone tools use the same native renderer and panel projection as full, including shell highlighting, output boundaries and native timing, without compact tree connectors or result indentation. Nested wrappers retain independent script/output/child disclosures. Compact records `Took` on the border when a final duration is supplied by Pi; it does not invent missing timings. Extremely narrow terminals omit the outline while retaining the panel background.
71
+
62
72
  The extension supports three persistent rendering modes: `clean` (default), `compact`, and `full`. Choose one interactively, or use `/pretty-tui clean`, `/pretty-tui compact`, `/pretty-tui full`, and `/pretty-tui status` directly. Settings are stored in `~/.pi/agent/pretty-tui.json` or under the directory selected by `PI_CODING_AGENT_DIR`.
63
73
 
64
- In fullscreen, interactive activity rows and copy controls tolerate same-row horizontal pointer jitter of up to two terminal columns. Larger or vertical movement remains a drag; ordinary text selection, links, and Pi's double/triple-click behavior are unchanged.
74
+ | Mode | Third-party tools | Codemode and other nested callers |
75
+ |---|---|---|
76
+ | `clean` | Compact summaries inside activity groups; native details on expansion | Nested tree inside the group, with local disclosures |
77
+ | `compact` | Concise independent panels; title toggles native details | Independent panel with parent/child tree and local script/output/child disclosures |
78
+ | `full` | Native content and interactions in a titled card | Native codemode content and disclosures in a titled card; no added snapshot-only details |
79
+
80
+ Full and compact keep Pi's native `Ctrl+O` global expansion. Shared projected rows preserve local controls afterward. These modes do not create clean-mode `Running/Done` activity wrappers.
81
+
82
+ In fullscreen, interactive activity/tool rows and copy controls tolerate same-row horizontal pointer jitter of up to two terminal columns. Larger or vertical movement remains a drag; ordinary text selection, links, and Pi's double/triple-click behavior are unchanged.
83
+
84
+ In compact and full, compaction summaries use a theme-colored `compaction ▸/▾` card. The original token count, shortcut hint and complete summary remain available; click the title/body or use native `Ctrl+O` to toggle expansion. Compaction cost notices remain separate. Clean retains Pi’s original compaction box.
85
+
86
+ ## Optional border-free selection copying
87
+
88
+ The experimental **Selection copy** option excludes plugin borders, titles and layout padding from mouse-selected text in fullscreen mode. Literal box-drawing characters in content are preserved. It is **off by default** and applies to clean, compact and full.
89
+
90
+ Open `/pretty-tui` and select `Selection copy` to toggle it, or run:
91
+
92
+ ```text
93
+ /pretty-tui selection-copy on
94
+ /pretty-tui selection-copy off
95
+ ```
96
+
97
+ The choice is stored as `selectionCopy` in `pretty-tui.json`; toggling automatically reloads Pi. `/pretty-tui status` shows the configured value. When off, no selection-copy/compositor/terminal-write hooks or private row annotations are installed. Disabling the whole extension also disables this feature, while preserving its saved preference. Regular-mode terminal-owned selection is unchanged.
98
+
99
+ This is an opt-in feature while terminal acceptance and internal-API compatibility are being evaluated. Long transcripts retain additional annotation memory; see [the exploration and GC measurements](docs/selection-copy-exploration.md) in the source repository.
65
100
 
66
101
  ## Copy user messages
67
102
 
@@ -88,11 +123,13 @@ npm install
88
123
  npm test
89
124
  ```
90
125
 
91
- The suite covers live and restored activity timelines, retries and terminal states, user/steering/compaction/native-renderer boundaries, ten-second UI-status replacement, stale-timer and flash cleanup on reload and shutdown, standalone severity styling, built-in and third-party tools, expandable thinking, parallel completion ownership, orphaned calls, duplicate-summary prevention, durable response timing and answer-only copy controls, adaptive user chat bubbles, complete prototype-patch restoration, and fullscreen code-block copy hit regions. GitHub Actions runs the same checks on every push and pull request.
126
+ The suite covers live and restored activity timelines, retries and terminal states, user/steering/compaction/native-renderer boundaries, ten-second UI-status replacement, stale-timer and flash cleanup on reload and shutdown, standalone severity styling, built-in and third-party tools, expandable thinking, parallel completion ownership, orphaned calls, duplicate-summary prevention, durable response timing and answer-only copy controls, adaptive user chat bubbles, complete prototype-patch restoration, and fullscreen code-block copy hit regions. It also uses Pi's real extension loader to check coexistence with a same-named replacement tool. GitHub Actions runs the same checks on every push and pull request.
127
+
128
+ To additionally exercise a local checkout of [pi-bash-bg](https://github.com/mapleluvr/pi-bash-bg), set `PI_BASH_BG_EXTENSION` to its `index.ts` before running `npm test`. This optional integration test executes foreground and automatically backgrounded commands and checks their status and logs.
92
129
 
93
130
  ## Compatibility notice
94
131
 
95
- The current development and regression-test baseline is **Pi 1.3.0**. User chat bubbles support its direct Markdown layout, keeping their transparent background and content-sized width. Nested execution requires Pi 0.99.0 or later; this is a feature requirement, not a promise that every older Pi release is tested.
132
+ The current development and regression-test baseline is **Pi 1.4.0**. User chat bubbles support its direct Markdown layout, using themed, full-width User panels in full/compact and transparent, content-sized bubbles in clean. Nested execution requires Pi 0.99.0 or later; this is a feature requirement, not a promise that every older Pi release is tested.
96
133
 
97
134
  Tool rendering uses Pi's documented extension APIs. Active-tool state follows Pi's built-in execution transitions through its exported interactive components. Clean-mode thought details use the activity hierarchy's own disclosure state; full and compact modes retain Pi's built-in Thinking visibility control. Rendering native user messages as adaptive chat bubbles, rounding the native prompt editor, changing unordered-list markers, and enhancing fenced code blocks require reload-safe runtime patches because Pi does not currently expose public renderer hooks for those presentation details. Markdown enhancements are activated only while rendering user and assistant messages in the main transcript; Markdown created by extension overlays keeps Pi's native layout. Markdown parsing, syntax highlighting, and terminal semantic zones remain handled by Pi.
98
135
 
package/README.zh-CN.md CHANGED
@@ -8,12 +8,12 @@
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 整轮工作的计时起点和时长格式,包含自动重试,工作真正结束后消失;窄输入框放不下时会隐藏计时。clean 模式下,`Running` 父节点圆点约每 800 ms 在输入框当前边框色和 `dim` 间切换;`Running` 文字使用相同边框色,工具圆点保持稳定,整轮结束后停止闪烁。展开后的 `Done` 标题和圆点使用主题的 `success` 色,折叠摘要的圆点、标题与内容统一使用 `dim`。
11
+ - **实时工作计时:** 三种模式下,耗时显示在输入框顶部边框的 `Working` 右侧,颜色与边框一致。它与完成 footer 共用 Agent 整轮工作的计时起点和时长格式,包含自动重试,工作真正结束后消失;窄输入框放不下时会隐藏计时。clean 模式下,`Running` 父节点圆点约每 800 ms 在输入框当前边框色和 `dim` 间切换;`Running` 文字使用相同边框色,工具圆点保持稳定,整轮结束后停止闪烁。展开后的 `Done` 标题和圆点使用主题的 `success` 色,折叠摘要的圆点、标题与内容统一使用 `dim`。
12
12
  - **临时 UI 状态:** 编辑器上方的一行只显示 Pi 直接产生的 UI 操作反馈,例如 Thinking 或工具输出可见性变化。新状态会替换旧状态,并在十秒后自动消失,不进入 transcript 或 session。
13
13
  - **按需查看有效信息:** 针对不同工具设计的摘要,会简洁呈现路径、命令、结果、diff、文件预览和实时 shell 输出,同时保留查看原始完整内容的能力。
14
- - **统一、精致的视觉体验:** 透明、右对齐的用户聊天气泡会根据短消息自适应宽度,长消息则在终端宽度的 75% 处换行;气泡使用主题链接色(`mdLink`)的圆角边框,并移除了多余的 `User` 标注。配合圆角输入框、主题感知配色、层级清晰的 Markdown 标题、更清爽的列表,以及带语法高亮的圆角代码块,让整个 TUI 更协调。在 fullscreen 模式下,每个围栏代码块还带有符合原生体验的 `[Copy]` 控件。
14
+ - **统一、精致的视觉体验:** 用户消息使用主题标题色(`mdHeading`,默认暖黄/琥珀色)的圆角边框,与蓝色 `mdLink` 思考区分。full 和 compact 使用带底色、铺满宽度的 `User` 面板,与 Assistant 卡片一致;clean 保留透明、右对齐、自适应宽度的气泡,长消息在终端宽度的 75% 处换行,不显示 `User` 标注。配合圆角输入框、主题感知配色、层级清晰的 Markdown 标题、更清爽的列表,以及带语法高亮的圆角代码块,让整个 TUI 更协调。在 fullscreen 模式下,每个围栏代码块还带有符合原生体验的 `[Copy]` 控件。
15
15
 
16
- Pi 内置的 `read`、`bash`、`edit`、`write`、`grep`、`find` 和 `ls` 工具使用专门设计的紧凑视图;没有嵌套调用的第三方工具会自动获得统一的紧凑行,单独展开时继续使用原生 renderer,但会移除与整体冲突的终端背景色。用户与 steer 消息、compaction 摘要、可见 assistant 正文、warning、error、aborted/length 响应,以及注册了专用 renderer 的 displayed custom message 都会形成硬分组边界。同时不会影响 Pi 原有的光标、输入法、自动补全和工具执行行为;fullscreen 鼠标交互增强见下文。
16
+ clean 中,Pi 内置的 `read`、`bash`、`edit`、`write`、`grep`、`find` 和 `ls` 工具使用专门设计的紧凑视图;compact 折叠时显示简短摘要,展开后与 full 共用原生详情卡片。没有嵌套调用的第三方工具会自动获得统一的紧凑行,单独展开时继续使用原生 renderer,但会移除与整体冲突的终端背景色。用户与 steer 消息、compaction 摘要、可见 assistant 正文、warning、error、aborted/length 响应,以及注册了专用 renderer 的 displayed custom message 都会形成硬分组边界。同时不会影响 Pi 原有的光标、输入法、自动补全和工具执行行为;fullscreen 鼠标交互增强见下文。
17
17
 
18
18
  ## 安装
19
19
 
@@ -23,19 +23,19 @@ pi install npm:pi-pretty-tui
23
23
 
24
24
  安装后重启 Pi,或运行 `/reload`。
25
25
 
26
- Pi 可能会提示内置工具被覆盖。这是正常现象:pi-pretty-tui 会重新注册 Pi 的内置工具,并将实际执行委托给原始实现,只修改它们的 TUI 渲染方式。
26
+ pi-pretty-tui 只注册渲染器,不替换工具实现。提供 `bash` 等内置同名工具的扩展仍使用自己的执行逻辑和 renderer。
27
27
 
28
28
  ## 使用工具调用
29
29
 
30
30
  点击 `Running(...)` 或 `Done(...)` 行,可以保留父级并展开其下方的工具、已完成的 `● thought` 和扩展更新。点击单个工具或思考子项可在保留摘要的同时展开嵌套详情。每棵展开的树末尾都有 `└─ [↑ Collapse]`,无需滚回父级即可只折叠当前分组;再次点击父级也可以折叠。clean 模式下,`Ctrl+O` 只提示使用这些局部展开入口,不会展开或收起内容;full 和 compact 模式保留 Pi 原生的工具输出全局展开行为。第三方替换的 `grep`、`find`(如 FFF)也使用紧凑的树节点,展开子项时仍可查看原生详情。紧凑工具名使用 `text` 加粗,参数使用 `muted`,运行中工具圆点使用 `dim`,成功/失败圆点保留对应主题色。思考圆点使用 `mdLink`,标题使用 `text` 加粗,详情正文使用 `muted`。工具名称会原样使用定义中的 `label`,没有 `label` 时回退到原始 `name`;pi-pretty-tui 不再重命名或转换大小写。
31
31
 
32
- 扩展调用 `ctx.ui.notify(message, "info")`(或省略 type)时,如果当前已有 thought/tool 活动组,通知会按真实时间顺序加入该组;如果没有现有工作,则独立显示,并且不会附着到未来的工具组。warning 和 error 保持独立,分别使用黄色 `⚠` 和红色 `✕`,并形成硬边界。工具失败继续归属于对应工具详情。上述扩展通知和错误都不会进入临时 UI 状态栏。
32
+ clean 和 compact 中,扩展调用 `ctx.ui.notify(message, "info")`(或省略 type)使用相同展示:clean 加入当前 thought/tool 活动组,compact 独立显示。warning 和 error 保持独立,使用 `⚠` 和 `✕` 并形成硬边界;工具失败归属于对应工具详情。full 将 info、warning、error 的投递委托给 Pi 原始方法,保留其文案、每次独立出现及状态行为。
33
33
 
34
- 在 clean 模式下,Pi 原生的 cache miss 提示以黄色 `⚠` 子节点加入产生该次缓存未命中的 assistant 消息所属的 thought/tool 活动组,随组折叠与展开,并且不会打断后续工具调用。如果该消息没有关联的工作(例如只输出正文的最终回答),提示保持独立。是否显示提示,以及 token 数、成本、模型切换和空闲时长等文案仍由 Pi 决定;接管的是 `addCacheMissNotice()` 入口,不匹配文本。历史提示在重建时重新推导,不额外写入会话记录。full 和 compact 模式保留 Pi 原生样式。
34
+ 在 clean 模式下,Pi 原生的 cache miss 提示以黄色 `⚠` 子节点加入产生该次缓存未命中的 assistant 消息所属的 thought/tool 活动组,随组折叠与展开,并且不会打断后续工具调用。如果该消息没有关联的工作(例如只输出正文的最终回答),提示保持独立。是否显示提示,以及 token 数、成本、模型切换和空闲时长等文案仍由 Pi 决定;接管的是 `addCacheMissNotice()` 入口,不匹配文本。历史提示在重建时重新推导,不额外写入会话记录。full 和 compact 使用相同的警告展示,独立显示,不包裹活动组。
35
35
 
36
- 没有自有 renderer 的 displayed custom message 使用统一、accent 配色的 `◇` Markdown 显示:当前存在 thought/tool 工作时加入当前组,否则独立显示,并保留 session 持久化。注册了 renderer 的消息完全保留插件原生显示并形成硬边界。所有判断只依据 Pi 的通知 type 和 renderer 是否存在,不识别扩展名称、custom message 名称,也不根据文本内容推断语义。
36
+ clean 和 compact 下,没有自有 renderer 的 displayed custom message 使用统一、accent 配色的 `◇` Markdown 显示:当前存在 thought/tool 工作时加入当前组,否则独立显示,并保留 session 持久化。注册了 renderer 的消息完全保留插件原生显示并形成硬边界。full 同样保留 custom message 的原生展示。所有判断只依据 Pi 的通知 type 和 renderer 是否存在,不识别扩展名称、custom message 名称,也不根据文本内容推断语义。
37
37
 
38
- Pi 内部直接调用 `showStatus()` 产生的反馈属于临时 UI 状态:它只显示在编辑器上方,新状态替换旧状态,并由十秒 timer 自动清除;它不会进入 transcript 或活动时间线,reload 时也会清除。在 clean 模式下,`Ctrl+T` 和 `Ctrl+O` 都只显示点击对应节点的简短提示,不改变内容可见性或 Pi 的 Thinking 设置。在 full 和 compact 模式下,两者保留 Pi 的原生行为。
38
+ clean 和 compact 中,Pi 内部直接调用 `showStatus()` 产生的反馈属于临时 UI 状态:它只显示在编辑器上方,新状态替换旧状态,并由十秒 timer 自动清除;它不会进入 transcript 或活动时间线,reload 时也会清除。在 clean 模式下,`Ctrl+T` 和 `Ctrl+O` 都只显示点击对应节点的简短提示,不改变内容可见性或 Pi 的 Thinking 设置。在 full 和 compact 模式下,两者保留 Pi 的原生行为。full 也保留原生 `showStatus()` 投递,不使用十秒 timer 删除 transcript 中的状态反馈。
39
39
 
40
40
  即使同一消息的后续更新才加入思考,可见 assistant 正文也保持在活动树之外;重复更新不会关闭后续独立的工具活动组。
41
41
 
@@ -43,15 +43,15 @@ Pi 内部直接调用 `showStatus()` 产生的反馈属于临时 UI 状态:它
43
43
 
44
44
  ## 错误展示
45
45
 
46
- clean 模式下,独立错误保留原始标题和完整内容,以紧凑的 `✕ Error · 原因` 展示。来源类型、标题及完整内容完全一致的相邻错误合为一条可见行,并使用暗色 `×2` / `×3` 标识次数。其他 transcript 组件会打断计数;包含思考、工具或可见回答的错误不参与合并。合并仅影响 UI,不删除或修改会话记录及原始消息组件。长错误完整换行,不增加预览截断;full/compact 模式保留原生展示。
46
+ clean 和 compact 模式下,独立错误保留原始标题和完整内容,以紧凑的 `✕ Error · 原因` 展示。来源类型、标题及完整内容完全一致的相邻错误合为一条可见行,并使用暗色 `×2` / `×3` 标识次数。其他 transcript 组件会打断计数;包含思考、工具或可见回答的错误不参与合并。合并仅影响 UI,不删除或修改会话记录及原始消息组件。长错误完整换行,不增加预览截断;full 保留原生 assistant 错误文案及每次独立出现,不将它们合并为重复计数。
47
47
 
48
48
  ## Codemode 与嵌套工具
49
49
 
50
- clean 模式下,通过 Pi 嵌套执行 API 调用其他工具的工具保留自己的父节点。展开活动组后,会显示各内部调用的状态、紧凑参数、耗时和结果摘要;点击某个内部调用,可以独立展开其参数和文本结果;点击展开后的正文可收起该调用,脚本和脚本输出正文也可点击收起各自区块。工具统一使用 `●` 圆点:运行中为主题暗色,成功与失败分别为主题成功色与错误色;工具名使用正文色加粗。`[Script]` 和 `[Script output]` 分别控制脚本与汇总输出;嵌套父工具标题仅用于展示,点击不会展开或折叠详情。各区块或子调用通过自己的入口独立展开/收起,`Ctrl+O` 在 clean 模式下只显示提示。扩展仍可通过 Pi 的 `setToolsExpanded()` API 全局控制,之后也可局部展开/收起;再次调用全局 API 会重置局部状态。并行调用按照启动顺序排列,不会随完成顺序跳动。即使脚本捕获错误后成功,失败的子调用仍会明确标记。
50
+ clean 和 compact 模式共用嵌套工具展示。通过 Pi 嵌套执行 API 调用其他工具的工具保留自己的父节点;clean 模式先展开活动组,compact 模式直接显示独立的父子调用树,不包裹活动组。两者都会显示各内部调用的状态、紧凑参数、耗时和结果摘要;点击某个内部调用,可以独立展开其参数和文本结果;点击展开后的正文可收起该调用,脚本和脚本输出正文也可点击收起各自区块。工具统一使用 `●` 圆点:运行中为主题暗色,成功与失败分别为主题成功色与错误色;工具名使用正文色加粗。`[Script]` 和 `[Script output]` 分别控制脚本与汇总输出;嵌套父工具标题仅用于展示,点击不会展开或折叠详情。各区块或子调用通过自己的入口独立展开/收起,`Ctrl+O` 在 clean 模式下只显示提示。扩展仍可通过 Pi 的 `setToolsExpanded()` API 全局控制,之后也可局部展开/收起;再次调用全局 API 会重置局部状态。并行调用按照启动顺序排列,不会随完成顺序跳动。即使脚本捕获错误后成功,失败的子调用仍会明确标记。
51
51
 
52
- 活动组摘要合并统计顶层与嵌套调用:一个父调用加三个内部调用显示为 `4 tool calls`,父调用本身也计一次;父工具节点仍保留子调用数量和树结构。内部调用不会变成不存在的顶层 transcript 成员,也不会挤掉树尾折叠按钮。归属依据 Pi 的 `parentToolCallId`,不依赖工具名或 JavaScript 源码解析,因此其他嵌套调用工具也适用。这需要支持嵌套执行事件的 Pi(0.99.0 及以后);旧版本保持原有显示。full 和 compact 模式保留原生工具展示。
52
+ 活动组摘要合并统计顶层与嵌套调用:一个父调用加三个内部调用显示为 `4 tool calls`,父调用本身也计一次;父工具节点仍保留子调用数量和树结构。内部调用不会变成不存在的顶层 transcript 成员,也不会挤掉树尾折叠按钮。归属依据 Pi 的 `parentToolCallId`,不依赖工具名或 JavaScript 源码解析,因此其他嵌套调用工具也适用。这需要支持嵌套执行事件的 Pi(0.99.0 及以后);旧版本保持原有显示。full 在工具卡片内使用 codemode 自己的原生 renderer,不额外追加 pretty-tui 快照中保存的子调用详情;可见信息、展开折叠及快捷键行为跟随 Pi。clean 和 compact 共用嵌套执行状态、历史快照、状态处理与子项局部展开逻辑;全局展开操作会重置它们的局部覆盖。
53
53
 
54
- 子工具的文本结果和 edit diff 会存入隐藏的、仅用于 UI 的 session 条目,支持 reload 和会话分支恢复。每个父调用最多保留 256 个子调用,每项参数最多 4,096 字符,每项文本结果最多 8,192 字符,参数和结果合计最多 131,072 字符;截断会明确提示。每项展开预览还设有 200 行上限,嵌套树最多展示父调用以下八层。没有快照的旧会话会明确显示仅保留调用记录。目前子结果使用通用文本视图,不调用各第三方工具的 renderer,也不在快照中保存图片;父工具自身的媒体结果仍可在原生 full/compact 模式查看。
54
+ 子工具的文本结果和 edit diff 会存入隐藏的、仅用于 UI 的 session 条目,支持 reload 和会话分支恢复。每个父调用最多保留 256 个子调用,每项参数最多 4,096 字符,每项文本结果最多 8,192 字符,参数和结果合计最多 131,072 字符;截断会明确提示。每项展开预览还设有 200 行上限,嵌套树最多展示父调用以下八层。没有快照的旧会话会明确显示仅保留调用记录。目前子结果使用通用文本视图,不调用各第三方工具的 renderer,也不在快照中保存图片;父工具自身的媒体结果与原生 renderer 仍可在 full 模式查看。
55
55
 
56
56
  这些 UI 条目不会修改脚本输出或模型收到的工具结果,但**会保存脚本可能未打印的子工具文本**,因此请将 session 文件视为可能含有敏感信息。常见凭证参数键会被遮盖,工具输出则不保证已清除所有敏感内容。
57
57
 
@@ -59,9 +59,44 @@ clean 模式下,通过 Pi 嵌套执行 API 调用其他工具的工具保留
59
59
 
60
60
  运行 `/pretty-tui` 可打开扩展设置。`Enabled` 开关会持久化控制 pi-pretty-tui 是否安装渲染补丁和内置工具 renderer。修改后会自动调用 Pi 官方的 reload 流程,无需手动执行 `/reload`。也可以直接使用 `/pretty-tui enable` 和 `/pretty-tui disable`。禁用期间设置命令仍然可用,Pi 会恢复原生渲染。
61
61
 
62
+ 三种模式保留输入框工作计时、回答 footer 和复制增强;clean/compact 使用临时状态反馈,full 保留原生状态投递。clean 将 `● thought` 放在活动组内。compact 使用独立思考卡片,默认折叠并预览第一行思考内容(超宽时以省略号截断为一行),标题显示 `▸`,点击标题展开 Markdown 详情后显示 `▾`。full 保留原生思考段、与正文的原始顺序、Markdown transform 和逐段点击,并为每个思考段加上左上角带名称的边框。full 中思考默认展开,不因已有 compact/原生隐藏设置而初始折叠;手动收起及后续 `Ctrl+T` 可见性切换继续生效。
63
+
64
+ full 以原生 Pi 的信息和行为为准,统一使用带主题底色的面板。User、Assistant、思考和工具面板的底色从对应边框的主题色派生。深色主题保留色相并显著降低亮度和饱和度,蓝、紫、绿、红边框对应同色系深底色,Assistant 中性边框对应深灰底色;浅色主题使用同色系浅底色,保证原生深色文字可读。标题、边框和底边耗时保留语义配色,颜色转换使用缓存并跟随主题及终端色彩模式变化。原生内容中的前景语法/diff 配色继续保留,ANSI reset 后会恢复面板底色,图片协议行保持原样。
65
+
66
+ full 和 compact 面板左右各保留一个字符的外侧留白,极窄终端取消;留白保持终端背景,统一应用于 User、Assistant、思考和工具面板。
67
+
68
+ 每个工具只有一张占满剩余宽度的面板:调用在上,原生可见结果在下,中间以 `Output` 分隔线区分。不再使用独立结果卡片、连接线或结果缩进;无可见结果时不添加输出占位。去掉边缘布局空白,保留内部空行与图片占位;不同工具之间保留两行留白。User 面板铺满宽度,左上角显示 `User`;相邻正文块合并为默认展开的 Assistant 卡片;思考默认展开,保留局部及原生可见性控制。
69
+
70
+ 内置工具使用 Pi 官方 renderer,保留预览、截断/展开入口及错误详情。full 模式的 bash 命令额外使用 Pi 的 shell 语法高亮,区分变量、字符串、关键字和注释,并按命令与语法配色缓存;原生 `$` 提示符、命令原文、超时标注和结果输出保持不变。shell/codemode 原生 `Took`/`Elapsed` 保留在底边框且与边框同色;窄面板放不下时保留原生正文计时。第三方 renderer、媒体数据和原生鼠标交互继续保留。clean 保留活动分组与透明用户气泡。compact 共用面板底色、边框配色、留白及 User/Assistant 卡片,工具默认只显示参数和结果摘要,独立工具卡片标题用 `▸` 表示折叠、`▾` 表示展开,点击标题切换详情,`Ctrl+O` 保留原生全局展开。User/Assistant 和仅用于展示的嵌套父工具标题不添加折叠箭头。独立工具展开后与 full 共用原生 renderer 和卡片投影,包括命令高亮、输出分界及原生耗时,不再带 compact 的树状连接线和结果缩进;嵌套工具保留脚本、输出和子项的局部展开。Pi 提供最终耗时时,compact 在边框显示 `Took`,缺失时不猜测。极窄终端省略边框但保留面板底色。
71
+
62
72
  扩展支持三种可持久化的渲染模式:`clean`(默认)、`compact` 和 `full`。可交互选择,也可以直接使用 `/pretty-tui clean`、`/pretty-tui compact`、`/pretty-tui full` 和 `/pretty-tui status`。设置会保存到 `~/.pi/agent/pretty-tui.json`,或 `PI_CODING_AGENT_DIR` 指定目录中的对应位置。
63
73
 
64
- fullscreen 模式下,活动树交互行与复制入口允许同一行左右最多两个终端字符格的鼠标抖动;更大偏移或跨行移动仍按拖动处理。普通文本选择、链接和 Pi 的双击/三击行为保持不变。
74
+ | 模式 | 第三方工具 | codemode 等嵌套调用工具 |
75
+ |---|---|---|
76
+ | `clean` | 活动组中的紧凑摘要,展开查看原生详情 | 活动组内的嵌套树,各区块局部展开 |
77
+ | `compact` | 独立紧凑面板,点击标题查看原生详情 | 卡片内的独立父子树,脚本、输出及子项按需展开 |
78
+ | `full` | 原生内容与交互,外加标题卡片 | 标题卡片内的原生 codemode 内容与展开控件,不额外添加快照详情 |
79
+
80
+ full 和 compact 保留 Pi 原生的 `Ctrl+O` 全局展开,之后仍可局部控制共享展示中的各子项;这两种模式不生成 clean 的 `Running/Done` 活动组外壳。
81
+
82
+ fullscreen 模式下,活动树/工具交互行与复制入口允许同一行左右最多两个终端字符格的鼠标抖动;更大偏移或跨行移动仍按拖动处理。普通文本选择、链接和 Pi 的双击/三击行为保持不变。
83
+
84
+ compact 和 full 中,压缩摘要使用主题配色的 `compaction ▸/▾` 卡片,保留原始 token 数、快捷键提示和完整摘要;点击标题/正文或使用原生 `Ctrl+O` 切换展开。压缩费用提示仍独立显示,clean 保留 Pi 原生压缩消息样式。
85
+
86
+ ## 可选的无边框选区复制
87
+
88
+ 实验性的 **Selection copy** 选项可在 fullscreen 模式下,从鼠标选区的复制结果中排除插件绘制的边框、标题和布局留白,保留正文里真实存在的框线字符。**默认关闭**,适用于 clean、compact 和 full。
89
+
90
+ 打开 `/pretty-tui`,选择 `Selection copy` 切换,或直接执行:
91
+
92
+ ```text
93
+ /pretty-tui selection-copy on
94
+ /pretty-tui selection-copy off
95
+ ```
96
+
97
+ 选择会保存为 `pretty-tui.json` 中的 `selectionCopy`,切换后自动重载 Pi;`/pretty-tui status` 可查看配置值。关闭时不安装选区复制、叠加层或终端写入补丁,也不生成私有行标记。禁用整个插件时,本功能也不会启用,但会保留开关偏好。regular 模式由终端管理的选区保持原样。
98
+
99
+ 目前仍需实际终端验收,并依赖 Pi 内部接口,因此作为可选功能保留。长会话会额外保留标记相关内存;源码仓库中的[探索与垃圾回收报告](docs/selection-copy-exploration.md)记录了测试范围和测量结果。
65
100
 
66
101
  ## 复制用户消息
67
102
 
@@ -88,11 +123,13 @@ npm install
88
123
  npm test
89
124
  ```
90
125
 
91
- 测试覆盖实时与历史恢复的活动时间线、自动重试与终止状态、用户/steer/compaction/原生 renderer 边界、十秒 UI 状态替换、reload 与 shutdown 时的旧 timer 与状态清理、独立严重级别样式、内置与第三方工具、可展开思考、并行工具完成归属、孤立 tool call、重复摘要防护、可持久化的工作时长与仅回答复制控件、自适应用户聊天气泡、原型补丁的完整恢复,以及 fullscreen 代码块复制的点击区域。GitHub Actions 会在每次 push 和 pull request 时运行相同检查。
126
+ 测试覆盖实时与历史恢复的活动时间线、自动重试与终止状态、用户/steer/compaction/原生 renderer 边界、十秒 UI 状态替换、reload 与 shutdown 时的旧 timer 与状态清理、独立严重级别样式、内置与第三方工具、可展开思考、并行工具完成归属、孤立 tool call、重复摘要防护、可持久化的工作时长与仅回答复制控件、自适应用户聊天气泡、原型补丁的完整恢复,以及 fullscreen 代码块复制的点击区域。测试还使用 Pi 的真实扩展加载器检查同名替换工具的共存。GitHub Actions 会在每次 push 和 pull request 时运行相同检查。
127
+
128
+ 如需额外测试本地 [pi-bash-bg](https://github.com/mapleluvr/pi-bash-bg) 仓库,运行 `npm test` 前将 `PI_BASH_BG_EXTENSION` 指向其 `index.ts`。该可选集成测试会执行前台和自动转后台的命令,并检查状态和日志。
92
129
 
93
130
  ## 兼容性说明
94
131
 
95
- 当前开发与回归测试基线为 **Pi 1.3.0**。用户气泡已适配该版本直接使用 Markdown 的布局,保留透明背景与按内容自适应的宽度。嵌套执行需要 Pi 0.99.0 及以后;这是功能要求,不代表每个旧版都经过测试。
132
+ 当前开发与回归测试基线为 **Pi 1.4.0**。用户气泡已适配该版本直接使用 Markdown 的布局,full/compact 使用带底色、铺满宽度的 User 面板,clean 使用透明、按内容自适应的气泡。嵌套执行需要 Pi 0.99.0 及以后;这是功能要求,不代表每个旧版都经过测试。
96
133
 
97
134
  工具渲染使用 Pi 的公开扩展 API。活动工具状态通过 Pi 导出的交互组件跟随其内置执行状态变化;clean 模式的 thought 详情使用活动时间线自身的展开状态,full 和 compact 模式则保留 Pi 内置的 Thinking 可见性控制。由于 Pi 目前没有为这些展示细节提供公开渲染钩子,自适应的原生用户消息气泡、原生输入编辑器圆角、无序列表标记和围栏代码块增强需要使用可安全重载的运行时补丁。Markdown 增强仅在渲染主 transcript 中的用户消息和助手回复时启用;扩展 overlay 创建的 Markdown 保持 Pi 原生布局。Markdown 解析、语法高亮和终端语义区域仍由 Pi 处理。
98
135
 
@@ -0,0 +1,29 @@
1
+ /** Remove SGR backgrounds while retaining foreground colors and text styles. */
2
+ export const stripAnsiBackgrounds = (value: string): string =>
3
+ value.replace(/\x1b\[([0-9:;]*)m/g, (sequence, raw: string) => {
4
+ if (raw === "") return sequence; // SGR reset also restores default background.
5
+ const fields = raw.split(";");
6
+ const kept: string[] = [];
7
+ for (let i = 0; i < fields.length; i++) {
8
+ const field = fields[i];
9
+ const code = Number(field.split(":")[0] || 0);
10
+ if (field.includes(":")) {
11
+ if (code !== 48 && !(code >= 40 && code <= 49) && !(code >= 100 && code <= 107)) kept.push(field);
12
+ continue;
13
+ }
14
+ if (code === 38 || code === 48 || code === 58) {
15
+ const mode = fields[i + 1];
16
+ const count = mode === "2" ? 4 : mode === "5" ? 2 : 0;
17
+ const group = fields.slice(i, i + count + 1);
18
+ if (code !== 48) kept.push(...group);
19
+ i += count;
20
+ continue;
21
+ }
22
+ if ((code >= 40 && code <= 49) || (code >= 100 && code <= 107)) continue;
23
+ kept.push(field);
24
+ }
25
+ return kept.length ? `\x1b[${kept.join(";")}m` : "";
26
+ });
27
+
28
+ export const isNativeImageRow = (row: string): boolean =>
29
+ row.includes("\x1b_G") || row.includes("\x1b]1337;File=");
@@ -0,0 +1,38 @@
1
+ import { colorToOklch, oklchColor, type Color } from "@earendil-works/pi-tui";
2
+
3
+ type Surface = { prefix: string; signature: string; paint: (text: string) => string };
4
+ const surfaces = new WeakMap<object, Map<string, {
5
+ source: Color; appearance: string; mode: unknown; style: unknown; surface: Surface;
6
+ }>>();
7
+
8
+ const brush = (sample: string): Surface => {
9
+ const at = sample.indexOf("\u0000");
10
+ const prefix = at < 0 ? "" : sample.slice(0, at);
11
+ const suffix = at < 0 ? "" : sample.slice(at + 1);
12
+ return { prefix, signature: prefix + suffix, paint: (text) => prefix + text + suffix };
13
+ };
14
+
15
+ /** A subdued surface with the outline's hue, resolved through Pi's color mode. */
16
+ export function cardSurface(theme: any, borderRole?: string, fallback = "toolPendingBg"): Surface {
17
+ const source: Color | undefined = borderRole ? theme.colors?.[borderRole] : undefined;
18
+ if (!source || typeof theme.style !== "function") {
19
+ return brush(typeof theme.bg === "function" ? theme.bg(fallback, "\u0000") : "\u0000");
20
+ }
21
+ const appearance = theme.appearance === "light" ? "light" : "dark";
22
+ const mode = theme.getColorMode?.();
23
+ const entries = surfaces.get(theme) ?? new Map();
24
+ surfaces.set(theme, entries);
25
+ const cached = entries.get(borderRole!);
26
+ if (cached && cached.source === source && cached.appearance === appearance && cached.mode === mode && cached.style === theme.style) {
27
+ return cached.surface;
28
+ }
29
+ const { l, c, h } = colorToOklch(source);
30
+ // Dark themes get deep tinted panels; light themes keep a pale tint so
31
+ // their native dark foreground remains readable. No fixed RGB palette.
32
+ const color = appearance === "dark"
33
+ ? oklchColor(Math.min(0.24, l * 0.28), Math.min(0.06, c * 0.35), h)
34
+ : oklchColor(1 - (1 - l) * 0.12, Math.min(0.035, c * 0.25), h);
35
+ const surface = brush(theme.style("\u0000", { bg: color }));
36
+ entries.set(borderRole!, { source, appearance, mode, style: theme.style, surface });
37
+ return surface;
38
+ }
@@ -0,0 +1,145 @@
1
+ import { selectableRow, selectionMetadataActive } from "./selection-ranges.js";
2
+ import { cardSurface } from "./card-surface.js";
3
+ import { isNativeImageRow, stripAnsiBackgrounds } from "./ansi-style.js";
4
+ import { stripTerminalSequences, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
5
+
6
+ export type NativeFrame = {
7
+ lines: string[]; sourceRows: Map<number, number>; contentWidth: number; inset: number;
8
+ };
9
+ const frameCache = new WeakMap<object, {
10
+ title: string; source: string[]; width: number; palette: string; sections: string; frame: NativeFrame;
11
+ }>();
12
+ export const cardMargin = (width: number) => width >= 20 ? 1 : 0;
13
+ export const nativeFrameWidth = (width: number) => {
14
+ const panelWidth = width - cardMargin(width) * 2;
15
+ return panelWidth >= 8 ? panelWidth - 4 : Math.max(1, panelWidth);
16
+ };
17
+
18
+ /** Fill one panel row, preserving foreground styles and reopening after resets. */
19
+ export const cardBackground = (line: string, width: number, theme: any, role = "toolPendingBg", borderRole?: string): string => {
20
+ if (isNativeImageRow(line)) return line;
21
+ const content = stripAnsiBackgrounds(line);
22
+ const padded = content + " ".repeat(Math.max(0, width - visibleWidth(content)));
23
+ const surface = cardSurface(theme, borderRole, role);
24
+ const prefix = surface.prefix;
25
+ const reopened = padded.replace(/\x1b\[([0-9:;]*)m/g, (sequence, raw: string) => {
26
+ const fields = raw.split(";");
27
+ let reset = raw === "";
28
+ for (let i = 0; i < fields.length; i++) {
29
+ const code = Number(fields[i].split(":")[0] || 0);
30
+ if (code === 0) reset = true;
31
+ if (!fields[i].includes(":") && (code === 38 || code === 58)) {
32
+ i += fields[i + 1] === "2" ? 4 : fields[i + 1] === "5" ? 2 : 0;
33
+ }
34
+ }
35
+ return reset ? sequence + prefix : sequence;
36
+ });
37
+ return surface.paint(reopened);
38
+ };
39
+
40
+ /** One shared panel for native text, media, thinking, replies and user messages. */
41
+ function renderPanel(
42
+ title: string, source: string[], width: number, theme: any,
43
+ sections = new Map<number, { label: string; color: string }>(), cacheOwner?: object,
44
+ titleColor = "text", footer = "", borderColor = "borderMuted", background = "toolPendingBg",
45
+ ): NativeFrame {
46
+ const palette = ["borderMuted", "text", "accent", "success", "error", titleColor, borderColor]
47
+ .map((role) => theme.fg(role, "x")).join("|") + (theme.bold?.("x") ?? "x") +
48
+ cardSurface(theme, borderColor, background).signature + selectionMetadataActive();
49
+ const sectionKey = titleColor + borderColor + footer + background + JSON.stringify([...sections]);
50
+ const cached = cacheOwner ? frameCache.get(cacheOwner) : undefined;
51
+ if (cached && cached.title === title && cached.width === width && cached.palette === palette && cached.sections === sectionKey &&
52
+ cached.source.length === source.length && cached.source.every((line, i) => line === source[i])) return cached.frame;
53
+ const sourceRows = new Map<number, number>();
54
+ if (width < 8) {
55
+ source.forEach((_, y) => sourceRows.set(y, y));
56
+ return { lines: source.map((row) => cardBackground(row, width, theme, background, borderColor)), sourceRows, contentWidth: Math.max(1, width), inset: 0 };
57
+ }
58
+ const contentWidth = width - 4;
59
+ const border = (s: string) => theme.fg(borderColor, s);
60
+ const plainCaption = truncateToWidth(title, width - 6, "…");
61
+ const caption = theme.fg(titleColor, theme.bold?.(plainCaption) ?? plainCaption);
62
+ const copyRanges: Array<Array<[number, number]>> | undefined = selectionMetadataActive() ? [[]] : undefined;
63
+ const lines = [border("╭─ ") + caption + border(" " + "─".repeat(Math.max(0, width - visibleWidth(plainCaption) - 5)) + "╮")];
64
+ source.forEach((row, sourceY) => {
65
+ const section = sections.get(sourceY);
66
+ if (section) {
67
+ const plainLabel = truncateToWidth(section.label, width - 5, "…");
68
+ const label = theme.fg(section.color, plainLabel);
69
+ sourceRows.set(lines.length, sourceY);
70
+ lines.push(border("├─ ") + label + border(" " + "─".repeat(Math.max(0, width - visibleWidth(plainLabel) - 5)) + "┤"));
71
+ copyRanges?.push([]);
72
+ }
73
+ sourceRows.set(lines.length, sourceY);
74
+ if (isNativeImageRow(row)) { lines.push(row); copyRanges?.push([]); }
75
+ else {
76
+ const content = stripAnsiBackgrounds(row);
77
+ const length = visibleWidth(content);
78
+ lines.push(border("│ ") + content + " ".repeat(Math.max(0, contentWidth - length)) + border(" │"));
79
+ copyRanges?.push([[2, 2 + Math.max(1, Math.min(contentWidth, length))]]);
80
+ }
81
+ });
82
+ const footerFits = footer && visibleWidth(footer) + 6 <= width;
83
+ lines.push(footerFits
84
+ ? border("╰" + "─".repeat(width - visibleWidth(footer) - 5) + " " + footer + " ─╯")
85
+ : border("╰" + "─".repeat(width - 2) + "╯"));
86
+ copyRanges?.push([]);
87
+ // Apply backgrounds before adding this frame's annotations so the width
88
+ // cache doesn't retain an extra marked variant of every newly painted row.
89
+ const frame = { lines: lines.map((line, i) => {
90
+ const painted = cardBackground(line, width, theme, background, borderColor);
91
+ return copyRanges ? selectableRow(painted, width, copyRanges[i]) : painted;
92
+ }), sourceRows, contentWidth, inset: 2 };
93
+ if (cacheOwner) frameCache.set(cacheOwner, { title, source: source.slice(), width, palette, sections: sectionKey, frame });
94
+ return frame;
95
+ }
96
+
97
+ const gutterFrames = new WeakMap<NativeFrame, NativeFrame>();
98
+
99
+ /** Outer gutters stay on the terminal background, outside the filled panel. */
100
+ export function frameNativeRows(
101
+ title: string, source: string[], width: number, theme: any,
102
+ sections = new Map<number, { label: string; color: string }>(), cacheOwner?: object,
103
+ titleColor = "text", footer = "", borderColor = "borderMuted", background = "toolPendingBg",
104
+ ): NativeFrame {
105
+ const margin = cardMargin(width);
106
+ const frame = renderPanel(title, source, width - margin * 2, theme, sections, cacheOwner,
107
+ titleColor, footer, borderColor, background);
108
+ if (!margin) return frame;
109
+ const cached = gutterFrames.get(frame);
110
+ if (cached) return cached;
111
+ const padded = { ...frame, inset: frame.inset + margin,
112
+ lines: frame.lines.map((line) => isNativeImageRow(line) ? line : selectableRow(" ".repeat(margin) + line + " ".repeat(margin), width, [[margin, width - margin]])),
113
+ };
114
+ gutterFrames.set(frame, padded);
115
+ return padded;
116
+ }
117
+
118
+ const trimmedIndices = (source: string[], start: number, end: number): number[] => {
119
+ // Preserve image reservation rows and internal text blank lines.
120
+ if (!source.slice(start, end).some(isNativeImageRow)) {
121
+ while (start < end && !stripTerminalSequences(source[start]).trim()) start++;
122
+ while (end > start && !stripTerminalSequences(source[end - 1]).trim()) end--;
123
+ }
124
+ return Array.from({ length: end - start }, (_, i) => start + i);
125
+ };
126
+
127
+ /** Keep native call/output content in one panel, divided at its actual boundary. */
128
+ export function frameNativeToolRows(
129
+ title: string, source: string[], width: number, theme: any,
130
+ resultStart: number | undefined, resultColor: string, owner: object, footer = "",
131
+ ): NativeFrame {
132
+ const hasOutput = resultStart !== undefined && resultStart > 0 && resultStart < source.length;
133
+ const indices = width < 8 ? source.map((_, i) => i) : hasOutput
134
+ ? [...trimmedIndices(source, 0, resultStart!), ...trimmedIndices(source, resultStart!, source.length)]
135
+ : trimmedIndices(source, 0, source.length);
136
+ const sections = new Map<number, { label: string; color: string }>();
137
+ if (hasOutput) {
138
+ const output = indices.findIndex((i) => i >= resultStart!);
139
+ if (output >= 0) sections.set(output, { label: "Output", color: resultColor });
140
+ }
141
+ const color = resultColor === "error" || resultColor === "success" ? resultColor : "accent";
142
+ const frame = frameNativeRows(title, indices.map((i) => source[i]), width, theme, sections, owner,
143
+ color, footer, color, resultColor === "error" ? "toolErrorBg" : "toolPendingBg");
144
+ return { ...frame, sourceRows: new Map([...frame.sourceRows].map(([y, i]) => [y, indices[i]])) };
145
+ }