pi-pretty-tui 0.3.1 → 0.3.2
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 +40 -103
- package/README.zh-CN.md +39 -102
- package/assets/screenshot.png +0 -0
- package/extensions/index.ts +99 -43
- package/extensions/markdown-emphasis.ts +87 -0
- package/extensions/markdown-spacing.ts +123 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -2,18 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
English | [简体中文](./README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
A
|
|
5
|
+
A cleaner, theme-aware interface for the [Pi coding agent](https://pi.dev/), with collapsible tools and thoughts, rounded message cards, and convenient copying.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
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 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
|
-
- **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
|
-
- **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
|
-
- **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:** 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
|
-
|
|
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.
|
|
7
|
+

|
|
17
8
|
|
|
18
9
|
## Install
|
|
19
10
|
|
|
@@ -21,119 +12,65 @@ Clean uses purpose-built compact views for Pi's built-in `read`, `bash`, `edit`,
|
|
|
21
12
|
pi install npm:pi-pretty-tui
|
|
22
13
|
```
|
|
23
14
|
|
|
24
|
-
Restart Pi or run `/reload
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
## Working with tool calls
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
`read` range labels accept numeric strings and omitted/null fields without concatenating line numbers; malformed range values omit the label. Expanded `edit` diffs use a fixed, right-aligned line-number gutter followed by a separate `+`/`-` column. Wrapped code aligns with the content column, preserving code indentation and theme diff colors.
|
|
15
|
+
Restart Pi or run `/reload`.
|
|
43
16
|
|
|
44
|
-
##
|
|
17
|
+
## Rendering modes
|
|
45
18
|
|
|
46
|
-
|
|
19
|
+
Run `/pretty-tui` to open settings and choose a mode:
|
|
47
20
|
|
|
48
|
-
|
|
21
|
+
| Mode | Experience |
|
|
22
|
+
|---|---|
|
|
23
|
+
| **clean** (default) | Group tools and thoughts into a collapsible `Running` / `Done` timeline. |
|
|
24
|
+
| **compact** | Show separate tool and thought cards with concise previews; expand for details. |
|
|
25
|
+
| **full** | Keep Pi's native details and interactions inside themed cards, with thoughts initially expanded. |
|
|
49
26
|
|
|
50
|
-
|
|
27
|
+
All modes include rounded messages and code blocks, syntax highlighting, a live working timer, and final-answer duration and copy controls. Clean and compact also support expandable nested-tool trees.
|
|
51
28
|
|
|
52
|
-
|
|
29
|
+
Click activity rows, tool titles or thought entries to expand and collapse details. In clean, use these local controls; in compact and full, Pi's `Ctrl+O` and `Ctrl+T` shortcuts remain available.
|
|
53
30
|
|
|
54
|
-
|
|
31
|
+
| Command | Action |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `/pretty-tui clean` | Switch to clean mode |
|
|
34
|
+
| `/pretty-tui compact` | Switch to compact mode |
|
|
35
|
+
| `/pretty-tui full` | Switch to full mode |
|
|
36
|
+
| `/pretty-tui enable` / `/pretty-tui disable` | Enable or disable the extension and reload |
|
|
37
|
+
| `/pretty-tui status` | Show current settings |
|
|
55
38
|
|
|
56
|
-
|
|
39
|
+
Settings persist across sessions. Disabling the extension restores Pi's native appearance.
|
|
57
40
|
|
|
58
|
-
##
|
|
41
|
+
## Copying
|
|
59
42
|
|
|
60
|
-
|
|
43
|
+
Start Pi in **fullscreen mode** to use mouse controls:
|
|
61
44
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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`.
|
|
73
|
-
|
|
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.
|
|
45
|
+
```sh
|
|
46
|
+
pi --tui-mode fullscreen
|
|
47
|
+
```
|
|
83
48
|
|
|
84
|
-
|
|
49
|
+
- **User message:** click its body or border to copy the original text.
|
|
50
|
+
- **Code block:** click `[Copy]` to copy the code without its frame.
|
|
51
|
+
- **Final answer:** click `[Copy]` beside the response duration to copy the answer without thoughts or tool output.
|
|
85
52
|
|
|
86
|
-
|
|
53
|
+
Dragging selects text rather than copying a whole message. Links keep their normal behavior; image attachments are not included in text copies.
|
|
87
54
|
|
|
88
|
-
|
|
55
|
+
### Optional border-free selection
|
|
89
56
|
|
|
90
|
-
|
|
57
|
+
Enable **Selection copy** in `/pretty-tui`, or run:
|
|
91
58
|
|
|
92
59
|
```text
|
|
93
60
|
/pretty-tui selection-copy on
|
|
94
61
|
/pretty-tui selection-copy off
|
|
95
62
|
```
|
|
96
63
|
|
|
97
|
-
|
|
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.
|
|
100
|
-
|
|
101
|
-
## Copy user messages
|
|
102
|
-
|
|
103
|
-
In fullscreen mode, left-click a user bubble's body or border to copy its complete original text, preserving Markdown, indentation, and line breaks without the bubble frame. Existing links and code-block `[Copy]` controls retain priority; dragging to select text does not trigger whole-message copy. Pi's native clipboard path provides copy feedback. This copies text, not image attachments. Regular mode keeps terminal-owned mouse selection and copying.
|
|
104
|
-
|
|
105
|
-
## Copy code blocks
|
|
106
|
-
|
|
107
|
-
Rounded code-block lines and corners use `borderMuted`; language labels, `[Copy]`, and code text retain their existing colors.
|
|
108
|
-
|
|
109
|
-
In fullscreen TUI mode, fenced Markdown code blocks in user and assistant transcript messages include a clickable `[Copy]` control in the top rule:
|
|
110
|
-
|
|
111
|
-
```sh
|
|
112
|
-
pi --tui-mode fullscreen
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
Clicking it copies only the code content—not the rounded border—through Pi's native fullscreen clipboard path and shows the same transient `Copied!` or `Copy failed` flash used by direct text selection. The control is hidden in regular TUI mode because the terminal keeps mouse input for text selection and scrolling there.
|
|
116
|
-
|
|
117
|
-
## Development
|
|
118
|
-
|
|
119
|
-
Run the committed regression suite before changing renderer state or transcript grouping:
|
|
120
|
-
|
|
121
|
-
```sh
|
|
122
|
-
npm install
|
|
123
|
-
npm test
|
|
124
|
-
```
|
|
125
|
-
|
|
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.
|
|
129
|
-
|
|
130
|
-
## Compatibility notice
|
|
64
|
+
This experimental option is **off by default**. In fullscreen mode, it removes UI borders, titles and layout padding from copied selections while preserving literal box-drawing characters in the content. It works in all three rendering modes; toggling it saves the setting and reloads Pi.
|
|
131
65
|
|
|
132
|
-
|
|
66
|
+
In regular mode, the terminal handles selection and copying, so this option has no border-removal effect. Fullscreen/regular is independent of clean/compact/full.
|
|
133
67
|
|
|
134
|
-
|
|
68
|
+
## Notes
|
|
135
69
|
|
|
136
|
-
|
|
70
|
+
- Tested with **Pi 1.5.0**. Future Pi updates may require compatibility fixes.
|
|
71
|
+
- Keeps Pi's native input behavior and tool execution, including third-party tool implementations.
|
|
72
|
+
- Nested-tool text details are saved with sessions for restoration and may be truncated. Session files can contain sensitive tool output.
|
|
73
|
+
- See [Releases](https://github.com/ykn0309/pi-pretty-tui/releases) for changes and contributor acknowledgments.
|
|
137
74
|
|
|
138
75
|
## Uninstall
|
|
139
76
|
|
|
@@ -141,8 +78,8 @@ No Pi source files are modified. Runtime patches are removed during session shut
|
|
|
141
78
|
pi remove npm:pi-pretty-tui
|
|
142
79
|
```
|
|
143
80
|
|
|
144
|
-
|
|
81
|
+
Restart Pi afterward.
|
|
145
82
|
|
|
146
83
|
## License
|
|
147
84
|
|
|
148
|
-
MIT
|
|
85
|
+
[MIT](./LICENSE)
|
package/README.zh-CN.md
CHANGED
|
@@ -2,18 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
[English](./README.md) | 简体中文
|
|
4
4
|
|
|
5
|
-
为 [Pi coding agent](https://pi.dev/)
|
|
5
|
+
为 [Pi coding agent](https://pi.dev/) 提供更清爽的界面:可折叠的工具与思考、跟随主题的圆角卡片,以及便捷的复制操作。
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- **可折叠的活动时间线:** 不再让大量独立调用填满对话。思考、工具和普通 `info` 通知会进入同一条按真实时间排序的活动流;工作进行时显示为 `Running(...)`,结束后变为低调的 `Done(...)`。父级合并统计顶层与嵌套工具调用总数,并单独显示思考数量;展开后可查看所有有序成员,成员摘要在展开嵌套详情后仍然保留。思考与工具地位相同:没有调用任何工具的那一轮同样会有自己的 `Running(1 thought)` / `Done(1 thought)` 父行,思考作为其子级,内容不会被丢弃。
|
|
10
|
-
- **可持久化的回答 footer:** 整轮 Agent 工作真正 settled 后(包括工具、自动重试和 compaction),最终回答底部会显示一条低调的灰色分隔线,工作时长与 fullscreen 模式下的 `[Copy]` 控件都嵌在线中。`[Copy]` 只复制最终可见回答,不包含 thought、工具、状态或 footer;reload 和 session tree 切换后仍会保留。
|
|
11
|
-
- **实时工作计时:** 三种模式下,耗时显示在输入框顶部边框的 `Working` 右侧,颜色与边框一致。它与完成 footer 共用 Agent 整轮工作的计时起点和时长格式,包含自动重试,工作真正结束后消失;窄输入框放不下时会隐藏计时。clean 模式下,`Running` 父节点圆点约每 800 ms 在输入框当前边框色和 `dim` 间切换;`Running` 文字使用相同边框色,工具圆点保持稳定,整轮结束后停止闪烁。展开后的 `Done` 标题和圆点使用主题的 `success` 色,折叠摘要的圆点、标题与内容统一使用 `dim`。
|
|
12
|
-
- **临时 UI 状态:** 编辑器上方的一行只显示 Pi 直接产生的 UI 操作反馈,例如 Thinking 或工具输出可见性变化。新状态会替换旧状态,并在十秒后自动消失,不进入 transcript 或 session。
|
|
13
|
-
- **按需查看有效信息:** 针对不同工具设计的摘要,会简洁呈现路径、命令、结果、diff、文件预览和实时 shell 输出,同时保留查看原始完整内容的能力。
|
|
14
|
-
- **统一、精致的视觉体验:** 用户消息使用主题标题色(`mdHeading`,默认暖黄/琥珀色)的圆角边框,与蓝色 `mdLink` 思考区分。full 和 compact 使用带底色、铺满宽度的 `User` 面板,与 Assistant 卡片一致;clean 保留透明、右对齐、自适应宽度的气泡,长消息在终端宽度的 75% 处换行,不显示 `User` 标注。配合圆角输入框、主题感知配色、层级清晰的 Markdown 标题、更清爽的列表,以及带语法高亮的圆角代码块,让整个 TUI 更协调。在 fullscreen 模式下,每个围栏代码块还带有符合原生体验的 `[Copy]` 控件。
|
|
15
|
-
|
|
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 鼠标交互增强见下文。
|
|
7
|
+

|
|
17
8
|
|
|
18
9
|
## 安装
|
|
19
10
|
|
|
@@ -23,117 +14,63 @@ pi install npm:pi-pretty-tui
|
|
|
23
14
|
|
|
24
15
|
安装后重启 Pi,或运行 `/reload`。
|
|
25
16
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
## 使用工具调用
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
clean 和 compact 中,扩展调用 `ctx.ui.notify(message, "info")`(或省略 type)使用相同展示:clean 加入当前 thought/tool 活动组,compact 独立显示。warning 和 error 保持独立,使用 `⚠` 和 `✕` 并形成硬边界;工具失败归属于对应工具详情。full 将 info、warning、error 的投递委托给 Pi 原始方法,保留其文案、每次独立出现及状态行为。
|
|
33
|
-
|
|
34
|
-
在 clean 模式下,Pi 原生的 cache miss 提示以黄色 `⚠` 子节点加入产生该次缓存未命中的 assistant 消息所属的 thought/tool 活动组,随组折叠与展开,并且不会打断后续工具调用。如果该消息没有关联的工作(例如只输出正文的最终回答),提示保持独立。是否显示提示,以及 token 数、成本、模型切换和空闲时长等文案仍由 Pi 决定;接管的是 `addCacheMissNotice()` 入口,不匹配文本。历史提示在重建时重新推导,不额外写入会话记录。full 和 compact 使用相同的警告展示,独立显示,不包裹活动组。
|
|
35
|
-
|
|
36
|
-
clean 和 compact 下,没有自有 renderer 的 displayed custom message 使用统一、accent 配色的 `◇` Markdown 显示:当前存在 thought/tool 工作时加入当前组,否则独立显示,并保留 session 持久化。注册了 renderer 的消息完全保留插件原生显示并形成硬边界。full 同样保留 custom message 的原生展示。所有判断只依据 Pi 的通知 type 和 renderer 是否存在,不识别扩展名称、custom message 名称,也不根据文本内容推断语义。
|
|
37
|
-
|
|
38
|
-
clean 和 compact 中,Pi 内部直接调用 `showStatus()` 产生的反馈属于临时 UI 状态:它只显示在编辑器上方,新状态替换旧状态,并由十秒 timer 自动清除;它不会进入 transcript 或活动时间线,reload 时也会清除。在 clean 模式下,`Ctrl+T` 和 `Ctrl+O` 都只显示点击对应节点的简短提示,不改变内容可见性或 Pi 的 Thinking 设置。在 full 和 compact 模式下,两者保留 Pi 的原生行为。full 也保留原生 `showStatus()` 投递,不使用十秒 timer 删除 transcript 中的状态反馈。
|
|
39
|
-
|
|
40
|
-
即使同一消息的后续更新才加入思考,可见 assistant 正文也保持在活动树之外;重复更新不会关闭后续独立的工具活动组。
|
|
41
|
-
|
|
42
|
-
`read` 行范围标签正确处理数字字符串及省略/null 参数,不再拼接出错误行号;无效范围值不显示范围标签。展开的 `edit` diff 使用固定宽度、右对齐的左侧行号栏,`+`/`-` 独占一列;长代码续行与正文列对齐,保留代码缩进及主题 diff 配色。
|
|
43
|
-
|
|
44
|
-
## 错误展示
|
|
17
|
+
## 展示模式
|
|
45
18
|
|
|
46
|
-
|
|
19
|
+
运行 `/pretty-tui` 打开设置,选择模式:
|
|
47
20
|
|
|
48
|
-
|
|
21
|
+
| 模式 | 展示方式 |
|
|
22
|
+
|---|---|
|
|
23
|
+
| **clean**(默认) | 将工具与思考归入可折叠的 `Running` / `Done` 活动时间线。 |
|
|
24
|
+
| **compact** | 工具与思考分别显示为简洁卡片,展开查看详情。 |
|
|
25
|
+
| **full** | 在主题卡片中保留 Pi 原生详情与交互,思考默认展开。 |
|
|
49
26
|
|
|
50
|
-
clean 和 compact
|
|
27
|
+
三种模式均提供圆角消息与代码块、语法高亮、实时工作计时,以及回答末尾的耗时和复制入口。clean 和 compact 还支持展开查看嵌套工具调用。
|
|
51
28
|
|
|
52
|
-
|
|
29
|
+
点击活动行、工具标题或思考条目可展开、收起详情。clean 使用这些局部入口;compact 和 full 保留 Pi 的 `Ctrl+O`、`Ctrl+T` 快捷键。
|
|
53
30
|
|
|
54
|
-
|
|
31
|
+
| 命令 | 作用 |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `/pretty-tui clean` | 切换到 clean |
|
|
34
|
+
| `/pretty-tui compact` | 切换到 compact |
|
|
35
|
+
| `/pretty-tui full` | 切换到 full |
|
|
36
|
+
| `/pretty-tui enable` / `/pretty-tui disable` | 启用或禁用插件,并自动重载 |
|
|
37
|
+
| `/pretty-tui status` | 查看当前设置 |
|
|
55
38
|
|
|
56
|
-
|
|
39
|
+
设置会持久保存。禁用插件后恢复 Pi 原生外观。
|
|
57
40
|
|
|
58
|
-
##
|
|
41
|
+
## 复制操作
|
|
59
42
|
|
|
60
|
-
|
|
43
|
+
以 **fullscreen 全屏模式**启动 Pi,即可使用鼠标复制入口:
|
|
61
44
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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` 指定目录中的对应位置。
|
|
73
|
-
|
|
74
|
-
| 模式 | 第三方工具 | codemode 等嵌套调用工具 |
|
|
75
|
-
|---|---|---|
|
|
76
|
-
| `clean` | 活动组中的紧凑摘要,展开查看原生详情 | 活动组内的嵌套树,各区块局部展开 |
|
|
77
|
-
| `compact` | 独立紧凑面板,点击标题查看原生详情 | 卡片内的独立父子树,脚本、输出及子项按需展开 |
|
|
78
|
-
| `full` | 原生内容与交互,外加标题卡片 | 标题卡片内的原生 codemode 内容与展开控件,不额外添加快照详情 |
|
|
79
|
-
|
|
80
|
-
full 和 compact 保留 Pi 原生的 `Ctrl+O` 全局展开,之后仍可局部控制共享展示中的各子项;这两种模式不生成 clean 的 `Running/Done` 活动组外壳。
|
|
81
|
-
|
|
82
|
-
fullscreen 模式下,活动树/工具交互行与复制入口允许同一行左右最多两个终端字符格的鼠标抖动;更大偏移或跨行移动仍按拖动处理。普通文本选择、链接和 Pi 的双击/三击行为保持不变。
|
|
45
|
+
```sh
|
|
46
|
+
pi --tui-mode fullscreen
|
|
47
|
+
```
|
|
83
48
|
|
|
84
|
-
|
|
49
|
+
- **用户消息:** 点击正文或边框,复制原始文本。
|
|
50
|
+
- **代码块:** 点击 `[Copy]`,复制不含边框的代码。
|
|
51
|
+
- **最终回答:** 点击回答耗时旁的 `[Copy]`,复制不含思考和工具输出的回答。
|
|
85
52
|
|
|
86
|
-
|
|
53
|
+
拖动鼠标按选区处理,不触发整条消息复制。链接保留正常行为;文本复制不包含图片附件。
|
|
87
54
|
|
|
88
|
-
|
|
55
|
+
### 可选的无边框选区复制
|
|
89
56
|
|
|
90
|
-
|
|
57
|
+
在 `/pretty-tui` 中切换 **Selection copy**,或运行:
|
|
91
58
|
|
|
92
59
|
```text
|
|
93
60
|
/pretty-tui selection-copy on
|
|
94
61
|
/pretty-tui selection-copy off
|
|
95
62
|
```
|
|
96
63
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
目前仍需实际终端验收,并依赖 Pi 内部接口,因此作为可选功能保留。长会话会额外保留标记相关内存;源码仓库中的[探索与垃圾回收报告](docs/selection-copy-exploration.md)记录了测试范围和测量结果。
|
|
100
|
-
|
|
101
|
-
## 复制用户消息
|
|
102
|
-
|
|
103
|
-
fullscreen 模式下,左键点击用户气泡的正文或边框,可复制完整原始文本,保留 Markdown、缩进和换行,不包含气泡边框。已有链接和代码块 `[Copy]` 控件保留优先级;拖动选择文本不会触发整条复制。复制使用 Pi 原生剪贴板路径及反馈,只复制文本,不复制图片附件。regular 模式保留终端自身的鼠标选择与复制行为。
|
|
104
|
-
|
|
105
|
-
## 复制代码块
|
|
106
|
-
|
|
107
|
-
代码块的圆角与线条使用 `borderMuted`,语言标签、`[Copy]` 和代码正文保持原有配色。
|
|
108
|
-
|
|
109
|
-
在 fullscreen TUI 模式下,用户消息与助手回复中的 Markdown 围栏代码块会在顶部边框显示可点击的 `[Copy]` 控件:
|
|
110
|
-
|
|
111
|
-
```sh
|
|
112
|
-
pi --tui-mode fullscreen
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
点击后只复制代码内容,不包含圆角边框;复制会通过 Pi 原生的 fullscreen 剪贴板路径完成,并显示与直接框选文本相同的临时 `Copied!` 或 `Copy failed` 提示。regular TUI 模式下,终端会保留鼠标输入用于文本选择和滚动,因此该控件会被隐藏。
|
|
116
|
-
|
|
117
|
-
## 开发
|
|
118
|
-
|
|
119
|
-
修改渲染状态或 transcript 分组前,请运行仓库内的回归测试:
|
|
120
|
-
|
|
121
|
-
```sh
|
|
122
|
-
npm install
|
|
123
|
-
npm test
|
|
124
|
-
```
|
|
125
|
-
|
|
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`。该可选集成测试会执行前台和自动转后台的命令,并检查状态和日志。
|
|
129
|
-
|
|
130
|
-
## 兼容性说明
|
|
64
|
+
这是**默认关闭的实验性选项**。在全屏模式下,复制鼠标选区时排除界面边框、标题和布局留白,保留正文里真实存在的框线字符。适用于三种展示模式,切换后保存设置并自动重载 Pi。
|
|
131
65
|
|
|
132
|
-
|
|
66
|
+
regular 非全屏模式由终端处理选择与复制,开启此选项也不会获得边框排除效果。fullscreen/regular 与 clean/compact/full 是两组独立设置。
|
|
133
67
|
|
|
134
|
-
|
|
68
|
+
## 必要说明
|
|
135
69
|
|
|
136
|
-
|
|
70
|
+
- 当前已在 **Pi 1.5.0** 上测试;未来 Pi 更新可能需要适配。
|
|
71
|
+
- 保留 Pi 原生输入行为和工具执行逻辑,包括第三方工具实现。
|
|
72
|
+
- 嵌套工具的文本详情会随会话保存以便恢复,内容可能截断;会话文件可能包含工具输出中的敏感信息。
|
|
73
|
+
- 版本变更与贡献者致谢见 [Releases](https://github.com/ykn0309/pi-pretty-tui/releases)。
|
|
137
74
|
|
|
138
75
|
## 卸载
|
|
139
76
|
|
|
@@ -141,8 +78,8 @@ npm test
|
|
|
141
78
|
pi remove npm:pi-pretty-tui
|
|
142
79
|
```
|
|
143
80
|
|
|
144
|
-
|
|
81
|
+
卸载后重启 Pi。
|
|
145
82
|
|
|
146
83
|
## 许可证
|
|
147
84
|
|
|
148
|
-
MIT
|
|
85
|
+
[MIT](./LICENSE)
|
package/assets/screenshot.png
CHANGED
|
Binary file
|
package/extensions/index.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { installSelectionCopy, selectableRow, selectionMetadataActive } from "./selection-ranges.js";
|
|
2
2
|
import { isNativeImageRow } from "./ansi-style.js";
|
|
3
3
|
import { cardMargin, cardBackground, frameNativeRows, nativeFrameWidth } from "./full-frame.js";
|
|
4
|
+
import {
|
|
5
|
+
themedInlineContext,
|
|
6
|
+
withNativeEmphasis,
|
|
7
|
+
type InlineStyleContext,
|
|
8
|
+
type InlineToken,
|
|
9
|
+
} from "./markdown-emphasis.js";
|
|
10
|
+
import { gapRowsAfterBlock, renderSpacedList } from "./markdown-spacing.js";
|
|
4
11
|
import { ThoughtPresentation } from "./thought-presentation.js";
|
|
5
12
|
import { isThirdPartyTool, renderToolProjection, THIRD_PARTY_TOOL, toolLabel, type ToolProjection } from "./tool-presentation.js";
|
|
6
13
|
import { NestedTools, NESTED_SNAPSHOT_ENTRY } from "./nested-tools.js";
|
|
@@ -65,6 +72,8 @@ type PrettyTuiConfig = {
|
|
|
65
72
|
mode?: PrettyTuiMode;
|
|
66
73
|
/** Exclude plugin decoration from fullscreen selections. Opt-in. */
|
|
67
74
|
selectionCopy?: boolean;
|
|
75
|
+
/** "native" keeps Pi's flags-only strong and emphasis. Read when the extension loads. */
|
|
76
|
+
markdownEmphasis?: "themed" | "native";
|
|
68
77
|
/** Legacy location used by the first mode implementation. */
|
|
69
78
|
bash?: {
|
|
70
79
|
mode?: "full" | "compact";
|
|
@@ -303,6 +312,17 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
303
312
|
}
|
|
304
313
|
};
|
|
305
314
|
const transcriptMarkdownActive = () => transcriptMarkdownDepth > 0;
|
|
315
|
+
// Assistant answers only. The transcript scope also covers user messages,
|
|
316
|
+
// thinking and activity updates, which keep Pi's strong and emphasis styling.
|
|
317
|
+
const assistantBodyMarkdown = new WeakSet<object>();
|
|
318
|
+
const markAssistantBodyMarkdown = (component: any) => {
|
|
319
|
+
// Pi adds answer text directly; a thought block is wrapped in a MouseRegion.
|
|
320
|
+
// A component built before this runtime is covered too: the rendered-mode
|
|
321
|
+
// symbols are per runtime, so its first render here re-runs updateContent.
|
|
322
|
+
for (const child of component.contentContainer?.children ?? []) {
|
|
323
|
+
if (child instanceof Markdown && !(child as any).defaultTextStyle) assistantBodyMarkdown.add(child);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
306
326
|
|
|
307
327
|
const setCleanGroupMembers = (lastToolCallId: string, toolCallIds: string[]) => {
|
|
308
328
|
cleanGroupToolCallIds.set(lastToolCallId, toolCallIds);
|
|
@@ -1405,6 +1425,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1405
1425
|
originalUpdateContent.call(this, renderMode === "compact" && assistantTerminalState(message)
|
|
1406
1426
|
? { ...message, errorMessage: undefined, stopReason: "stop" } : message, isStreaming);
|
|
1407
1427
|
this.lastMessage = message;
|
|
1428
|
+
markAssistantBodyMarkdown(this);
|
|
1408
1429
|
if (eligibleThinking(message)) thinkingComponents.set(assistantMessageKey(message), this);
|
|
1409
1430
|
return;
|
|
1410
1431
|
}
|
|
@@ -1432,6 +1453,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1432
1453
|
this.hideThinkingBlock = previousHideThinkingBlock;
|
|
1433
1454
|
}
|
|
1434
1455
|
this.lastMessage = message;
|
|
1456
|
+
markAssistantBodyMarkdown(this);
|
|
1435
1457
|
};
|
|
1436
1458
|
|
|
1437
1459
|
const renderAssistantTranscript = function (this: any, width: number): string[] {
|
|
@@ -2288,6 +2310,11 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2288
2310
|
const codeBlockRegionsKey = Symbol("pretty-tui.code-block-regions");
|
|
2289
2311
|
const transcriptMarkdownStateKey = Symbol("pretty-tui.transcript-markdown-state");
|
|
2290
2312
|
if (!markdownPrototype[codeBlockPatchKey]) {
|
|
2313
|
+
// Pi 1.5 reuses rendered top-level blocks while text streams. Keep copy
|
|
2314
|
+
// descriptors with each native block's highlighting-cache identity, weakly,
|
|
2315
|
+
// so reused pixels retain their hit regions without disabling that cache.
|
|
2316
|
+
const blockCopyDescriptors = new WeakMap<object, { generation: number; blocks: any[] }>();
|
|
2317
|
+
let copyRenderGeneration = 0;
|
|
2291
2318
|
const originalInvalidate = markdownPrototype.invalidate;
|
|
2292
2319
|
const originalRender = markdownPrototype.render;
|
|
2293
2320
|
const originalRenderToken = markdownPrototype.renderToken;
|
|
@@ -2332,14 +2359,22 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2332
2359
|
|
|
2333
2360
|
const needsRender =
|
|
2334
2361
|
!this.cachedLines || this.cachedText !== this.text || this.cachedWidth !== width;
|
|
2335
|
-
if (needsRender) this[codeBlockCollectionKey] = [];
|
|
2362
|
+
if (needsRender) this[codeBlockCollectionKey] = Object.assign([], { generation: ++copyRenderGeneration });
|
|
2336
2363
|
|
|
2337
2364
|
const lines = originalRender.call(this, width);
|
|
2338
2365
|
if (!needsRender) return selectableMarkdown(this, lines);
|
|
2339
2366
|
|
|
2340
2367
|
const regions: Array<{ xStart: number; xEnd: number; y: number; code: string }> = [];
|
|
2341
2368
|
let searchFrom = 0;
|
|
2342
|
-
|
|
2369
|
+
const nativeBlocks = this.blocks?.deref();
|
|
2370
|
+
const nativeTokens = this.cachedTokens?.deref()?.tokens;
|
|
2371
|
+
const copyBlocks = nativeBlocks && nativeTokens
|
|
2372
|
+
? nativeTokens.flatMap((token: any) => {
|
|
2373
|
+
const block = nativeBlocks.get(token.raw);
|
|
2374
|
+
return block ? blockCopyDescriptors.get(block.highlightedCode)?.blocks ?? [] : [];
|
|
2375
|
+
})
|
|
2376
|
+
: this[codeBlockCollectionKey] ?? [];
|
|
2377
|
+
for (const block of copyBlocks) {
|
|
2343
2378
|
let headerLine = -1;
|
|
2344
2379
|
for (let lineIndex = searchFrom; lineIndex < lines.length; lineIndex += 1) {
|
|
2345
2380
|
const renderedLine = lines[lineIndex] ?? "";
|
|
@@ -2385,36 +2420,20 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2385
2420
|
return lines;
|
|
2386
2421
|
};
|
|
2387
2422
|
const headingStyle = (text: string) => this.theme.heading(text);
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
if (!trueColor) return undefined;
|
|
2391
|
-
const [, red, green, blue] = trueColor.map(Number);
|
|
2392
|
-
return red * 0.2126 + green * 0.7152 + blue * 0.0722;
|
|
2393
|
-
};
|
|
2394
|
-
const defaultLuminance = foregroundLuminance(this.applyDefaultStyle("M"));
|
|
2395
|
-
const headingLuminance = foregroundLuminance(headingStyle("M"));
|
|
2396
|
-
const darkTheme =
|
|
2397
|
-
defaultLuminance !== undefined
|
|
2398
|
-
? defaultLuminance >= 128
|
|
2399
|
-
: (headingLuminance ?? 255) >= 160;
|
|
2400
|
-
const whiteStyle = (text: string) => `\x1b[97m${text}\x1b[39m`;
|
|
2423
|
+
// Descending emphasis: framed H1, underlined H2, bold H3,
|
|
2424
|
+
// plain H4, decorative H5, and quote-colored italic H6.
|
|
2401
2425
|
const titleStyle =
|
|
2402
|
-
level === 1 || level ===
|
|
2426
|
+
level === 1 || level === 2
|
|
2403
2427
|
? (text: string) => headingStyle(this.theme.bold(this.theme.underline(text)))
|
|
2404
|
-
: level ===
|
|
2405
|
-
? (text: string) =>
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
? (text: string) => headingStyle(this.theme.bold(text))
|
|
2411
|
-
: level === 5
|
|
2412
|
-
? headingStyle
|
|
2413
|
-
: (text: string) => this.theme.quote(this.theme.italic(text));
|
|
2414
|
-
const titleContext = {
|
|
2428
|
+
: level === 3
|
|
2429
|
+
? (text: string) => headingStyle(this.theme.bold(text))
|
|
2430
|
+
: level === 4 || level === 5
|
|
2431
|
+
? headingStyle
|
|
2432
|
+
: (text: string) => this.theme.quote(this.theme.italic(text));
|
|
2433
|
+
const titleContext = withNativeEmphasis({
|
|
2415
2434
|
applyText: titleStyle,
|
|
2416
2435
|
stylePrefix: this.getStylePrefix(titleStyle),
|
|
2417
|
-
};
|
|
2436
|
+
});
|
|
2418
2437
|
const title = this.renderInlineTokens(token.tokens || [], titleContext);
|
|
2419
2438
|
|
|
2420
2439
|
if (level === 1 && maxWidth >= 6) {
|
|
@@ -2437,17 +2456,6 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2437
2456
|
return addHeadingSpacing(lines);
|
|
2438
2457
|
}
|
|
2439
2458
|
|
|
2440
|
-
if (level === 2) {
|
|
2441
|
-
// Reverse only the rendered title cells: the theme's heading color
|
|
2442
|
-
// becomes a compact, content-width background label with no frame.
|
|
2443
|
-
const lines = wrapTextWithAnsi(title, maxWidth).map((line: string) =>
|
|
2444
|
-
darkTheme
|
|
2445
|
-
? `\x1b[48;5;94m${line}\x1b[49m`
|
|
2446
|
-
: `\x1b[107m\x1b[7m${line}\x1b[27m\x1b[49m`
|
|
2447
|
-
);
|
|
2448
|
-
return addHeadingSpacing(lines);
|
|
2449
|
-
}
|
|
2450
|
-
|
|
2451
2459
|
if (level === 5 && maxWidth >= 8) {
|
|
2452
2460
|
const prefix = "┄┄ ";
|
|
2453
2461
|
const suffix = " ┄┄";
|
|
@@ -2468,7 +2476,10 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2468
2476
|
}
|
|
2469
2477
|
|
|
2470
2478
|
if (token?.type !== "code") {
|
|
2471
|
-
return
|
|
2479
|
+
return [
|
|
2480
|
+
...originalRenderToken.call(this, token, width, nextTokenType, styleContext),
|
|
2481
|
+
...gapRowsAfterBlock(token?.type, nextTokenType),
|
|
2482
|
+
];
|
|
2472
2483
|
}
|
|
2473
2484
|
|
|
2474
2485
|
const maxWidth = Math.max(1, width);
|
|
@@ -2530,13 +2541,23 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2530
2541
|
];
|
|
2531
2542
|
|
|
2532
2543
|
if (showCopyButton && Array.isArray(this[codeBlockCollectionKey])) {
|
|
2533
|
-
|
|
2544
|
+
const descriptor = {
|
|
2534
2545
|
buttonStart,
|
|
2535
2546
|
buttonWidth: visibleWidth(copyLabel),
|
|
2536
2547
|
code,
|
|
2537
2548
|
styledTopRule,
|
|
2538
2549
|
topRule,
|
|
2539
|
-
}
|
|
2550
|
+
};
|
|
2551
|
+
const collection = this[codeBlockCollectionKey] as any[] & { generation: number };
|
|
2552
|
+
collection.push(descriptor);
|
|
2553
|
+
if (this.blockHighlightedCode) {
|
|
2554
|
+
let cached = blockCopyDescriptors.get(this.blockHighlightedCode);
|
|
2555
|
+
if (!cached || cached.generation !== collection.generation) {
|
|
2556
|
+
cached = { generation: collection.generation, blocks: [] };
|
|
2557
|
+
blockCopyDescriptors.set(this.blockHighlightedCode, cached);
|
|
2558
|
+
}
|
|
2559
|
+
cached.blocks.push(descriptor);
|
|
2560
|
+
}
|
|
2540
2561
|
}
|
|
2541
2562
|
if (nextTokenType && nextTokenType !== "space") lines.push("");
|
|
2542
2563
|
return lines;
|
|
@@ -2621,7 +2642,11 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2621
2642
|
this.theme.listBullet = (marker: string) =>
|
|
2622
2643
|
originalListBullet(marker.replace(/^- /, "• "));
|
|
2623
2644
|
try {
|
|
2624
|
-
|
|
2645
|
+
if (!transcriptMarkdownActive()) {
|
|
2646
|
+
return originalRenderList.call(this, token, depth, width, styleContext);
|
|
2647
|
+
}
|
|
2648
|
+
return renderSpacedList(token, (singleItemList) =>
|
|
2649
|
+
originalRenderList.call(this, singleItemList, depth, width, styleContext));
|
|
2625
2650
|
} finally {
|
|
2626
2651
|
this.theme.listBullet = originalListBullet;
|
|
2627
2652
|
}
|
|
@@ -2639,6 +2664,37 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2639
2664
|
});
|
|
2640
2665
|
}
|
|
2641
2666
|
|
|
2667
|
+
const inlineEmphasisPatchKey = Symbol.for("pretty-tui.inline-emphasis");
|
|
2668
|
+
if (config.markdownEmphasis !== "native" && !markdownPrototype[inlineEmphasisPatchKey]) {
|
|
2669
|
+
const originalRenderInlineTokens = markdownPrototype.renderInlineTokens;
|
|
2670
|
+
const patchedRenderInlineTokens = function (
|
|
2671
|
+
this: any,
|
|
2672
|
+
tokens: InlineToken[],
|
|
2673
|
+
styleContext?: InlineStyleContext,
|
|
2674
|
+
): string {
|
|
2675
|
+
const theme = currentExtensionUi?.theme;
|
|
2676
|
+
if (!transcriptMarkdownActive() || !assistantBodyMarkdown.has(this) || !theme?.fg) {
|
|
2677
|
+
return originalRenderInlineTokens.call(this, tokens, styleContext);
|
|
2678
|
+
}
|
|
2679
|
+
return originalRenderInlineTokens.call(
|
|
2680
|
+
this,
|
|
2681
|
+
tokens,
|
|
2682
|
+
themedInlineContext(tokens, styleContext ?? this.getDefaultInlineStyleContext(), theme),
|
|
2683
|
+
);
|
|
2684
|
+
};
|
|
2685
|
+
|
|
2686
|
+
markdownPrototype[inlineEmphasisPatchKey] = { originalRenderInlineTokens, patchedRenderInlineTokens };
|
|
2687
|
+
markdownPrototype.renderInlineTokens = patchedRenderInlineTokens;
|
|
2688
|
+
|
|
2689
|
+
pi.on("session_shutdown", () => {
|
|
2690
|
+
const patch = markdownPrototype[inlineEmphasisPatchKey];
|
|
2691
|
+
if (patch?.patchedRenderInlineTokens === markdownPrototype.renderInlineTokens) {
|
|
2692
|
+
markdownPrototype.renderInlineTokens = patch.originalRenderInlineTokens;
|
|
2693
|
+
}
|
|
2694
|
+
if (patch) delete markdownPrototype[inlineEmphasisPatchKey];
|
|
2695
|
+
});
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2642
2698
|
const textContent = (result: any): string =>
|
|
2643
2699
|
result.content
|
|
2644
2700
|
?.filter((item: any) => item.type === "text")
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Foreground colors for Markdown strong and emphasis in the assistant's answer.
|
|
3
|
+
*
|
|
4
|
+
* Pi has no strong or emphasis color token, so these reuse existing ones. Each is
|
|
5
|
+
* read from the active theme on every render, so dark, light, custom themes and
|
|
6
|
+
* `/theme` all apply. `syntaxType` looks like an option for emphasis, but it equals
|
|
7
|
+
* `mdCode` in Pi's built-in themes, which would make emphasis look like inline code.
|
|
8
|
+
*/
|
|
9
|
+
export const MARKDOWN_EMPHASIS_COLORS = {
|
|
10
|
+
strong: "syntaxNumber",
|
|
11
|
+
em: "warning",
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
type EmphasisType = keyof typeof MARKDOWN_EMPHASIS_COLORS;
|
|
15
|
+
type EmphasisColor = (typeof MARKDOWN_EMPHASIS_COLORS)[EmphasisType];
|
|
16
|
+
type ChildOwner = EmphasisType | "link";
|
|
17
|
+
|
|
18
|
+
export interface EmphasisTheme {
|
|
19
|
+
fg(color: EmphasisColor, text: string): string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Pi's inline style context: how text is styled, and what restores it after a nested span closes. */
|
|
23
|
+
export interface InlineStyleContext {
|
|
24
|
+
applyText: (text: string) => string;
|
|
25
|
+
stylePrefix: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface InlineToken {
|
|
29
|
+
type: string;
|
|
30
|
+
tokens?: InlineToken[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const SENTINEL = "\0";
|
|
34
|
+
|
|
35
|
+
const isChildOwner = (type: string): type is ChildOwner =>
|
|
36
|
+
type === "strong" || type === "em" || type === "link";
|
|
37
|
+
|
|
38
|
+
// Pi passes a token's children to renderInlineTokens with the parent's context and
|
|
39
|
+
// does not say who the parent was. Keyed by array, so Pi's cached tokens stay unmodified.
|
|
40
|
+
const childOwners = new WeakMap<readonly InlineToken[], ChildOwner>();
|
|
41
|
+
const nativeContexts = new WeakSet<InlineStyleContext>();
|
|
42
|
+
const surroundingContexts = new WeakMap<InlineStyleContext, InlineStyleContext>();
|
|
43
|
+
|
|
44
|
+
/** Marks a context whose text keeps its own foreground, such as a heading title. */
|
|
45
|
+
export function withNativeEmphasis(context: InlineStyleContext): InlineStyleContext {
|
|
46
|
+
nativeContexts.add(context);
|
|
47
|
+
return context;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function emphasisContext(
|
|
51
|
+
surrounding: InlineStyleContext,
|
|
52
|
+
color: EmphasisColor,
|
|
53
|
+
theme: EmphasisTheme,
|
|
54
|
+
): InlineStyleContext {
|
|
55
|
+
const context: InlineStyleContext = {
|
|
56
|
+
// The inner color wins, so a surrounding default style cannot hide the emphasis color.
|
|
57
|
+
applyText: (text) => surrounding.applyText(theme.fg(color, text)),
|
|
58
|
+
// A nested span's foreground close falls back to the terminal default, so
|
|
59
|
+
// Pi's stylePrefix has to reopen the emphasis color for the text after it.
|
|
60
|
+
stylePrefix: surrounding.stylePrefix + theme.fg(color, SENTINEL).split(SENTINEL)[0],
|
|
61
|
+
};
|
|
62
|
+
surroundingContexts.set(context, surrounding);
|
|
63
|
+
return context;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns the context Pi should use for these tokens. Only the context changes:
|
|
68
|
+
* Pi's own recursion still emits every flag and every nested span.
|
|
69
|
+
*
|
|
70
|
+
* The innermost strong or emphasis sets the foreground. A link label uses the
|
|
71
|
+
* context outside all emphasis, so it keeps Pi's link color, and emphasis inside
|
|
72
|
+
* it stays flags-only for the same reason. Inline code is untouched by contexts.
|
|
73
|
+
*/
|
|
74
|
+
export function themedInlineContext(
|
|
75
|
+
tokens: readonly InlineToken[],
|
|
76
|
+
context: InlineStyleContext,
|
|
77
|
+
theme: EmphasisTheme,
|
|
78
|
+
): InlineStyleContext {
|
|
79
|
+
const owner = childOwners.get(tokens);
|
|
80
|
+
for (const token of tokens) {
|
|
81
|
+
if (token.tokens && isChildOwner(token.type)) childOwners.set(token.tokens, token.type);
|
|
82
|
+
}
|
|
83
|
+
if (!owner || nativeContexts.has(context)) return context;
|
|
84
|
+
const surrounding = surroundingContexts.get(context) ?? context;
|
|
85
|
+
if (owner === "link") return withNativeEmphasis({ ...surrounding });
|
|
86
|
+
return emphasisContext(surrounding, MARKDOWN_EMPHASIS_COLORS[owner], theme);
|
|
87
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { stripTerminalSequences } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Transcript Markdown spacing policy. This is the only place that decides list
|
|
5
|
+
* spacing; change the table below to tune it. It applies to every list the same
|
|
6
|
+
* way: unordered or ordered, short or wrapped, nested, task, loose or complex.
|
|
7
|
+
*
|
|
8
|
+
* - Pi's Markdown source and parsed tokens are never changed. Items and token
|
|
9
|
+
* lists are copied, because Pi caches its parsed tokens between renders.
|
|
10
|
+
* - Only blank rows change. Text, emphasis, wrapping and markers stay Pi's.
|
|
11
|
+
* - A boundary that already has a blank row, from a source blank line or from
|
|
12
|
+
* Pi itself, does not get a second one.
|
|
13
|
+
* - Callers apply this to transcript Markdown only, so overlays stay native.
|
|
14
|
+
*/
|
|
15
|
+
export const MARKDOWN_GAP_ROWS = {
|
|
16
|
+
/** A paragraph directly followed by a list. */
|
|
17
|
+
beforeList: 1,
|
|
18
|
+
/** A list directly followed by another block, such as a heading. */
|
|
19
|
+
afterList: 1,
|
|
20
|
+
/** Sibling items, at every nesting depth. */
|
|
21
|
+
betweenItems: 1,
|
|
22
|
+
/** Blocks inside one item, such as its text and its nested list. */
|
|
23
|
+
withinItem: 1,
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
export interface MarkdownBlockToken {
|
|
27
|
+
type: string;
|
|
28
|
+
raw?: string;
|
|
29
|
+
text?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface MarkdownListItemToken {
|
|
33
|
+
tokens: MarkdownBlockToken[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface MarkdownListToken extends MarkdownBlockToken {
|
|
37
|
+
items: MarkdownListItemToken[];
|
|
38
|
+
loose: boolean;
|
|
39
|
+
start?: number | "";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const blankRows = (count: number): string[] => Array.from({ length: count }, () => "");
|
|
43
|
+
|
|
44
|
+
const isBlankRow = (row: string): boolean => stripTerminalSequences(row).trim() === "";
|
|
45
|
+
|
|
46
|
+
/** Rows to add after a top-level or blockquote block that Pi leaves unspaced. */
|
|
47
|
+
export function gapRowsAfterBlock(type: string | undefined, nextType: string | undefined): string[] {
|
|
48
|
+
if (type === "paragraph" && nextType === "list") return blankRows(MARKDOWN_GAP_ROWS.beforeList);
|
|
49
|
+
if (type === "list" && nextType !== undefined && nextType !== "space") {
|
|
50
|
+
return blankRows(MARKDOWN_GAP_ROWS.afterList);
|
|
51
|
+
}
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type ItemTokenOutput = "none" | "blank" | "content";
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* What a token inside a list item emits. Pi draws a `checkbox` as part of the
|
|
59
|
+
* item marker, so it emits nothing; treating it as content would part the marker
|
|
60
|
+
* from its text. An empty `text` still emits a row, but that row is blank.
|
|
61
|
+
*/
|
|
62
|
+
function itemTokenOutput(token: MarkdownBlockToken): ItemTokenOutput {
|
|
63
|
+
if (token.type === "checkbox" || token.type === "def") return "none";
|
|
64
|
+
if (token.type === "space") return "blank";
|
|
65
|
+
if (token.type === "text" || token.type === "paragraph") {
|
|
66
|
+
return token.text?.trim() ? "content" : "blank";
|
|
67
|
+
}
|
|
68
|
+
if (token.type === "html") return token.raw?.trim() ? "content" : "blank";
|
|
69
|
+
return "content";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Returns a copy of the item with a gap between adjacent content blocks and with
|
|
74
|
+
* no trailing blank rows. Because no item ends blank, nested lists never end
|
|
75
|
+
* blank either, so the gap after them cannot double. Pi's blocks add their own
|
|
76
|
+
* trailing gap only when told a next token type, and item blocks are not told.
|
|
77
|
+
*/
|
|
78
|
+
function withItemBlockGaps(item: MarkdownListItemToken): MarkdownListItemToken {
|
|
79
|
+
const tokens: MarkdownBlockToken[] = [];
|
|
80
|
+
let previousOutput: ItemTokenOutput = "none";
|
|
81
|
+
for (const token of item.tokens) {
|
|
82
|
+
const output = itemTokenOutput(token);
|
|
83
|
+
if (previousOutput === "content" && output === "content") {
|
|
84
|
+
for (let row = 0; row < MARKDOWN_GAP_ROWS.withinItem; row += 1) tokens.push({ type: "space" });
|
|
85
|
+
}
|
|
86
|
+
if (output !== "none") previousOutput = output;
|
|
87
|
+
tokens.push(token);
|
|
88
|
+
}
|
|
89
|
+
while (tokens.length > 0 && itemTokenOutput(tokens[tokens.length - 1]) !== "content") tokens.pop();
|
|
90
|
+
return { ...item, tokens };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Joins rendered items, skipping the gap where a blank row already separates them. */
|
|
94
|
+
function joinItems(renderedItems: readonly string[][]): string[] {
|
|
95
|
+
const rows: string[] = [];
|
|
96
|
+
for (const itemRows of renderedItems) {
|
|
97
|
+
const previousRow = rows[rows.length - 1];
|
|
98
|
+
const nextRow = itemRows[0];
|
|
99
|
+
if (previousRow !== undefined && nextRow !== undefined && !isBlankRow(previousRow) && !isBlankRow(nextRow)) {
|
|
100
|
+
rows.push(...blankRows(MARKDOWN_GAP_ROWS.betweenItems));
|
|
101
|
+
}
|
|
102
|
+
rows.push(...itemRows);
|
|
103
|
+
}
|
|
104
|
+
return rows;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Renders a list one item at a time through Pi's own list renderer, which
|
|
109
|
+
* re-enters this policy for nested lists. `loose` is cleared so that Pi's
|
|
110
|
+
* loose-list gap cannot add to the gap chosen here.
|
|
111
|
+
*/
|
|
112
|
+
export function renderSpacedList(
|
|
113
|
+
list: MarkdownListToken,
|
|
114
|
+
renderList: (singleItemList: MarkdownListToken) => string[],
|
|
115
|
+
): string[] {
|
|
116
|
+
const start = typeof list.start === "number" ? list.start : 1;
|
|
117
|
+
return joinItems(list.items.map((item, index) => renderList({
|
|
118
|
+
...list,
|
|
119
|
+
start: start + index,
|
|
120
|
+
loose: false,
|
|
121
|
+
items: [withItemBlockGaps(item)],
|
|
122
|
+
})));
|
|
123
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-pretty-tui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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>",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"typecheck": "tsc -p tsconfig.json",
|
|
18
|
-
"test": "npm run typecheck && node tests/regression.mjs && node tests/coexistence.mjs && node tests/selection-ranges.mjs && node tests/selection-options.mjs",
|
|
18
|
+
"test": "npm run typecheck && node tests/regression.mjs && node tests/coexistence.mjs && node tests/selection-ranges.mjs && node tests/selection-options.mjs && node tests/markdown-spacing.mjs && node tests/markdown-emphasis.mjs",
|
|
19
19
|
"test:selection-pty": "python3 tests/selection-pty.py",
|
|
20
20
|
"bench:selection": "node tests/selection-benchmark.mjs",
|
|
21
21
|
"test:selection-cli": "python3 tests/selection-cli.py",
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"@earendil-works/pi-tui": "*"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@earendil-works/pi-coding-agent": "1.
|
|
49
|
-
"@earendil-works/pi-tui": "1.
|
|
48
|
+
"@earendil-works/pi-coding-agent": "1.5.0",
|
|
49
|
+
"@earendil-works/pi-tui": "1.5.0",
|
|
50
50
|
"@types/node": "^24.0.0",
|
|
51
51
|
"jiti": "^2.7.0",
|
|
52
52
|
"typescript": "^5.9.0"
|