@yinan_chen/pi-claude-code-ui 0.9.0 → 0.9.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 +58 -56
- package/README.zh-CN.md +123 -0
- package/package.json +3 -2
- package/src/foreign-normalizer.ts +15 -4
- package/src/generic-card.ts +2 -2
- package/src/index.ts +24 -14
- package/src/settings.ts +78 -78
- package/src/skin-setting-panel.ts +19 -19
package/README.md
CHANGED
|
@@ -1,57 +1,59 @@
|
|
|
1
1
|
# @yinan_chen/pi-claude-code-ui
|
|
2
2
|
|
|
3
|
+
English | [简体中文](README.zh-CN.md)
|
|
4
|
+
|
|
3
5
|

|
|
4
6
|
|
|
5
|
-
###
|
|
7
|
+
### Demo
|
|
6
8
|
|
|
7
9
|

|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
Animated GIF preview (for platforms that don't render video, such as npm): 
|
|
10
12
|
|
|
11
|
-
>
|
|
13
|
+
> Formerly lean-tidy — a Claude Code-style UI enhancement extension for Pi: Claude-style conversation transcript views, rich diff rendering, subagent panels, and working-state hints.
|
|
12
14
|
|
|
13
|
-
##
|
|
15
|
+
## Install
|
|
14
16
|
|
|
15
17
|
```bash
|
|
16
18
|
pi install npm:@yinan_chen/pi-claude-code-ui
|
|
17
19
|
```
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
Trial run (doesn't write to your config):
|
|
20
22
|
|
|
21
23
|
```bash
|
|
22
24
|
pi -e npm:@yinan_chen/pi-claude-code-ui
|
|
23
25
|
```
|
|
24
26
|
|
|
25
|
-
##
|
|
27
|
+
## Features
|
|
26
28
|
|
|
27
|
-
- **Claude
|
|
28
|
-
-
|
|
29
|
-
- **Skill
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
29
|
+
- **Claude-style views** — conversation transcript adapter, with Pi native fallback
|
|
30
|
+
- **Sticky message bubble** — pins the user message currently being answered at the top while scrolling; click to jump back (built in since v0.2.0; formerly the sticky-first-message extension)
|
|
31
|
+
- **Skill recollapse** — expanded skill invocations automatically fold back to `/skill:name args`, covering four blind spots such as /tree and /fork backfill (built in since v0.2.0; formerly the skill-block-recollapse extension)
|
|
32
|
+
- **Generic fallback for foreign cards** — messages no renderer claims (no extension registered the customType) are taken over automatically as a unified `● card`, zero config and zero adaptation; extensions that register their own renderer keep it (dispatch-layer interception, unaffected by load order; v0.7.0)
|
|
33
|
+
- **Rich diff rendering** — syntax-highlighted diffs for edit/write tools (vendored, MIT)
|
|
34
|
+
- **Subagent panel** — background agent status at a glance
|
|
35
|
+
- **Working-state hints** — working message / running effect
|
|
36
|
+
- **Polished read-only tools** — rendering for read / bash / grep / find / ls results
|
|
37
|
+
- **Skin Settings panel** — `/skin-setting` adjusts every skin setting with save-as-you-edit; `/skin-setting default` resets to defaults in one shot (v0.9.0)
|
|
36
38
|
|
|
37
|
-
##
|
|
39
|
+
## Configuration
|
|
38
40
|
|
|
39
|
-
###
|
|
41
|
+
### Recommended: the `/skin-setting` panel
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
Type `/skin-setting` in a pi session to open the centered overlay Skin Settings panel, where changes save as you make them — every edit is validated, written to the config file immediately, and reflected in the panel row, with no save/discard burden:
|
|
42
44
|
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
- `foreignCardSkip`
|
|
47
|
-
- `/skin-setting default
|
|
48
|
-
-
|
|
45
|
+
- Built-in search filtering to quickly locate any of the 25 config keys; each row shows its description and default value
|
|
46
|
+
- Enum / boolean keys cycle through legal values on Enter, so you can't pick an invalid one; number / string keys open an input box on Enter with range validation (e.g. 0–100, 0–4)
|
|
47
|
+
- Most keys are live settings — once you change them and close the panel, a full redraw makes them visible at once; reload-required settings are fixed at load time, carry a persistent `⟳ needs /reload` annotation, and get one extra reminder when changed
|
|
48
|
+
- The `foreignCardSkip` blocklist is shown read-only — its state stays visible without risk of accidental edits
|
|
49
|
+
- `/skin-setting default`: one-shot reset to defaults after a confirmation dialog (only this skin's config keys are removed; anything else you wrote in the file is kept)
|
|
50
|
+
- In non-TUI modes (RPC / JSON / print) it prints the config file path instead — use the file-based approach below
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
**F5** forces a full-screen redraw at any time (the habitual fix for leftover artifacts).
|
|
51
53
|
|
|
52
|
-
###
|
|
54
|
+
### Editing the config file directly
|
|
53
55
|
|
|
54
|
-
|
|
56
|
+
You can also edit `~/.pi/agent/lean-tidy.json` directly (when the file doesn't exist, all defaults apply). Manual edits take effect after `/reload`; panel edits apply instantly for live settings, while reload-required settings (those marked `⟳` in the table below) need `/reload`:
|
|
55
57
|
|
|
56
58
|
```json
|
|
57
59
|
{
|
|
@@ -85,36 +87,36 @@ pi -e npm:@yinan_chen/pi-claude-code-ui
|
|
|
85
87
|
}
|
|
86
88
|
```
|
|
87
89
|
|
|
88
|
-
|
|
|
90
|
+
| Key | Default | Description |
|
|
89
91
|
|---|---|---|
|
|
90
|
-
| `profile` ⟳ | `"claude"` |
|
|
91
|
-
| `skillRendering` | `"native"` |
|
|
92
|
-
| `subagentRendering` ⟳ | `"claude"` |
|
|
93
|
-
| `detailsBackground` | `"selectedBg"` |
|
|
94
|
-
| `bash/code/errorPreviewLines` | `0`/`0`/`0` |
|
|
95
|
-
| `liveThinking` | `true` |
|
|
96
|
-
| `thinkingTailLines` | `5` |
|
|
97
|
-
| `foldAnimMs` | `240` |
|
|
98
|
-
| `stickyIncludeCustom` ⟳ | `false` |
|
|
99
|
-
| `skillResumePreview` ⟳ | `true` |
|
|
100
|
-
| `resultPrefix` | `"└"` |
|
|
101
|
-
| `resultPrefixGap` | `1` |
|
|
102
|
-
| `normalizeForeignRenderers` | `true` |
|
|
103
|
-
| `workflowRendering` ⟳ | `"claude"` |
|
|
104
|
-
| `foreignCardStyle` | `"generic"` |
|
|
105
|
-
| `foreignCardSkip` | `[]` |
|
|
106
|
-
| `diff.diffViewMode` | `"auto"` |
|
|
107
|
-
| `diff.diffIndicatorMode` | `"bars"` |
|
|
108
|
-
| `diff.diffSplitMinWidth` | `120` |
|
|
109
|
-
| `diff.edit/writeDiffCollapsedLines` | `0`/`0` | edit/write
|
|
110
|
-
| `diff.diffWordWrap` | `true` | diff
|
|
111
|
-
| `diff.expandedPreviewMaxLines` | `40` |
|
|
112
|
-
|
|
113
|
-
##
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
##
|
|
92
|
+
| `profile` ⟳ | `"claude"` | Overall look: claude = Claude Code-style views (default); native = pi native fallback. |
|
|
93
|
+
| `skillRendering` | `"native"` | Skill invocation block style: native = pi native [skill] collapsed line (default); claude = ● Skill(name) two-line style. |
|
|
94
|
+
| `subagentRendering` ⟳ | `"claude"` | Subagent completion notice style: claude = ● Subagent(desc) + branches (default); native = keep the bundled panel. |
|
|
95
|
+
| `detailsBackground` | `"selectedBg"` | Background color of the expanded tool details area; theme color key — `customMessageBg`/`toolPendingBg`/`toolSuccessBg`/`toolErrorBg` also work, `""` = none. |
|
|
96
|
+
| `bash/code/errorPreviewLines` | `0`/`0`/`0` | Preview lines for the three tool-result classes, 0–100 (0 = collapsed by default): bash output / read-write-edit-find tools / failed results. |
|
|
97
|
+
| `liveThinking` | `true` | Stream thinking blocks as they arrive. |
|
|
98
|
+
| `thinkingTailLines` | `5` | Lines kept at the tail when thinking is collapsed, ≥0. |
|
|
99
|
+
| `foldAnimMs` | `240` | Code block fold animation duration in milliseconds, ≥0 (0 disables it). |
|
|
100
|
+
| `stickyIncludeCustom` ⟳ | `false` | Whether the sticky bubble includes side-channel messages: CustomMessages injected by extensions or forwarded across sessions. |
|
|
101
|
+
| `skillResumePreview` ⟳ | `true` | Whether the /resume session list preview folds back to [skill] names. |
|
|
102
|
+
| `resultPrefix` | `"└"` | Corner prefix symbol for tool result lines (tab-drawing glyphs are the most reliable in monospace fonts; `↳` or Claude Code's official `⎿` also work, though both may widen in some fonts). |
|
|
103
|
+
| `resultPrefixGap` | `1` | Spaces between the prefix symbol and content, 0–4. |
|
|
104
|
+
| `normalizeForeignRenderers` | `true` | Rewrite the leading ⎿ of third-party extension output to resultPrefix (unifies the style of any unmatched extension's rendering; turn it off to see the third-party original). |
|
|
105
|
+
| `workflowRendering` ⟳ | `"claude"` | Workflow completion card style: claude = ● Workflow(name) (default); native = keep the bundled card. Dispatch-layer interception, unaffected by package load order. |
|
|
106
|
+
| `foreignCardStyle` | `"generic"` | Cards without their own renderer: generic = take over renderer-less customTypes automatically as a unified `● card` (zero config); symbols = unify symbols only, keep layout. |
|
|
107
|
+
| `foreignCardSkip` | `[]` | customType blocklist never taken over (skip by type when an interactive third-party card misbehaves). |
|
|
108
|
+
| `diff.diffViewMode` | `"auto"` | Diff display mode: auto / split / unified. |
|
|
109
|
+
| `diff.diffIndicatorMode` | `"bars"` | Diff change indicator: bars / classic / none. |
|
|
110
|
+
| `diff.diffSplitMinWidth` | `120` | Minimum total width for split view; falls back to unified below it. |
|
|
111
|
+
| `diff.edit/writeDiffCollapsedLines` | `0`/`0` | Context lines kept when edit / write diffs are collapsed (0 = expanded by default). |
|
|
112
|
+
| `diff.diffWordWrap` | `true` | Wrap overly wide diff lines. |
|
|
113
|
+
| `diff.expandedPreviewMaxLines` | `40` | Maximum lines for expanded diff previews. |
|
|
114
|
+
|
|
115
|
+
## Compatibility
|
|
116
|
+
|
|
117
|
+
Verified against `VERIFIED_PI_VERSION = 1.0.2`; on a version mismatch the extension shows a single notice at session startup.
|
|
118
|
+
|
|
119
|
+
## Install from source
|
|
118
120
|
|
|
119
121
|
```bash
|
|
120
122
|
pi install git:github.com/chenyn273/pi-claude-code-ui@v0.2.0
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# @yinan_chen/pi-claude-code-ui
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 简体中文
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
### 演示
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
动图预览(不渲染视频的平台,如 npm): 
|
|
12
|
+
|
|
13
|
+
> 原名 lean-tidy —— Claude Code 风格的 Pi 界面增强扩展:Claude 式对话转录视图、富文本 diff 渲染、子代理面板与工作状态提示。
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pi install npm:@yinan_chen/pi-claude-code-ui
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
试运行(不写入配置):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pi -e npm:@yinan_chen/pi-claude-code-ui
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 功能
|
|
28
|
+
|
|
29
|
+
- **Claude 式视图** — 对话转录适配器,Pi 原生回退
|
|
30
|
+
- **吸附消息泡(sticky)** — 滚动时顶部吸附显示当前正在回答的用户消息原文,点击回位(v0.2.0 内置,原 sticky-first-message 扩展)
|
|
31
|
+
- **Skill 折叠(recollapse)** — 展开的 skill 全文自动折回 `/skill:名 参数`,覆盖 /tree、/fork 回填等四处盲区(v0.2.0 内置,原 skill-block-recollapse 扩展)
|
|
32
|
+
- **第三方卡片兜底接管** — 无自带渲染方的消息(无任何扩展注册该 customType)自动接管为统一的 `● 卡片`,零配置零适配;扩展自己注册的渲染器原样保留,分发层拦截不受加载顺序影响(v0.7.0)
|
|
33
|
+
- **富 diff 渲染** — edit/write 工具的语法高亮 diff(vendored, MIT)
|
|
34
|
+
- **子代理面板** — 后台 agent 状态一目了然
|
|
35
|
+
- **工作状态提示** — working message / running effect
|
|
36
|
+
- **只读工具美化** — read/bash/grep/find/ls 结果渲染
|
|
37
|
+
- **设置面板** — `/skin-setting` 即改即存调整全部皮肤配置,`/skin-setting default` 一键恢复默认(v0.9.0)
|
|
38
|
+
|
|
39
|
+
## 配置
|
|
40
|
+
|
|
41
|
+
### 推荐方式:`/skin-setting` 设置面板
|
|
42
|
+
|
|
43
|
+
在 pi 会话里输入 `/skin-setting` 打开居中 overlay 设置面板,即改即存——每次改动校验后立即写入配置文件并刷新面板行,没有"保存/放弃"负担:
|
|
44
|
+
|
|
45
|
+
- 内置搜索过滤,25 个配置键快速定位;每行附中文说明与默认值
|
|
46
|
+
- 枚举/布尔键回车在合法值间循环切换,不会选错;数字/字符串键回车进入输入框,带范围校验(如 0–100、0–4)
|
|
47
|
+
- 大多数键是热键,改动关闭面板后整屏重绘即刻可见;加载期固化的冷键常驻 `⟳ 需 /reload 生效` 标注,改动时会再提醒一次
|
|
48
|
+
- `foreignCardSkip` 黑名单只读展示,看得见状态又不会误编辑
|
|
49
|
+
- `/skin-setting default`:经确认对话框一键恢复全部默认(只清除本皮肤的配置键,文件里手写的其他内容保留)
|
|
50
|
+
- 非 TUI 模式(RPC / JSON / print)下运行会提示配置文件路径,请改用文件方式
|
|
51
|
+
|
|
52
|
+
任何时刻可用 **F5** 整屏重绘(排残影的习惯操作)。
|
|
53
|
+
|
|
54
|
+
### 手改配置文件
|
|
55
|
+
|
|
56
|
+
也可以直接编辑 `~/.pi/agent/lean-tidy.json`(不存在时全部使用默认值)。手改后需 `/reload` 生效;设置面板的改动则是热键即时生效、冷键(下表标 `⟳` 者)需 `/reload`:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"profile": "claude",
|
|
61
|
+
"skillRendering": "native",
|
|
62
|
+
"subagentRendering": "claude",
|
|
63
|
+
"detailsBackground": "selectedBg",
|
|
64
|
+
"bashPreviewLines": 0,
|
|
65
|
+
"codePreviewLines": 0,
|
|
66
|
+
"errorPreviewLines": 0,
|
|
67
|
+
"liveThinking": true,
|
|
68
|
+
"thinkingTailLines": 5,
|
|
69
|
+
"foldAnimMs": 240,
|
|
70
|
+
"stickyIncludeCustom": false,
|
|
71
|
+
"skillResumePreview": true,
|
|
72
|
+
"resultPrefix": "└",
|
|
73
|
+
"resultPrefixGap": 1,
|
|
74
|
+
"normalizeForeignRenderers": true,
|
|
75
|
+
"workflowRendering": "claude",
|
|
76
|
+
"foreignCardStyle": "generic",
|
|
77
|
+
"foreignCardSkip": [],
|
|
78
|
+
"diff": {
|
|
79
|
+
"diffViewMode": "auto",
|
|
80
|
+
"diffIndicatorMode": "bars",
|
|
81
|
+
"diffSplitMinWidth": 120,
|
|
82
|
+
"editDiffCollapsedLines": 0,
|
|
83
|
+
"writeDiffCollapsedLines": 0,
|
|
84
|
+
"diffWordWrap": true,
|
|
85
|
+
"expandedPreviewMaxLines": 40
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
| 键 | 默认 | 说明 |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| `profile` ⟳ | `"claude"` | 整体风格:`claude` / `native`(pi 原生) |
|
|
93
|
+
| `skillRendering` | `"native"` | skill 块样式:pi 原生折叠行 / `claude` 两行风格 |
|
|
94
|
+
| `subagentRendering` ⟳ | `"claude"` | 子代理完成通知:`claude` 分支风格 / `native` 保留包内面板 |
|
|
95
|
+
| `detailsBackground` | `"selectedBg"` | 详情背景色(主题色键名,可选 `customMessageBg`/`toolPendingBg`/`toolSuccessBg`/`toolErrorBg`/`""`) |
|
|
96
|
+
| `bash/code/errorPreviewLines` | `0`/`0`/`0` | 三类工具结果的预览行数(0–100;0 = 默认折叠) |
|
|
97
|
+
| `liveThinking` | `true` | 思考过程流式展示 |
|
|
98
|
+
| `thinkingTailLines` | `5` | 思考尾行数 |
|
|
99
|
+
| `foldAnimMs` | `240` | 折叠动画时长(ms,0 关闭) |
|
|
100
|
+
| `stickyIncludeCustom` ⟳ | `false` | 吸附泡是否包含扩展注入/跨会话转发的旁路消息 |
|
|
101
|
+
| `skillResumePreview` ⟳ | `true` | `/resume` 列表预览是否折回 `[skill] 名` 单行 |
|
|
102
|
+
| `resultPrefix` | `"└"` | 工具结果行拐角前缀(制表符族,等宽字体最稳;可换 `↳` 或 Claude Code 官方的 `⎿`,后两者在部分字体下会变宽) |
|
|
103
|
+
| `resultPrefixGap` | `1` | 前缀符号与内容之间的空格数(0–4) |
|
|
104
|
+
| `normalizeForeignRenderers` | `true` | 第三方扩展卡片输出里行首 `⎿` 自动规范化为 `resultPrefix`(任何未适配扩展的渲染都能统一风格;关闭可看第三方原版) |
|
|
105
|
+
| `workflowRendering` ⟳ | `"claude"` | workflow 完成卡片样式:`● Workflow(name)` 风格 / `native` 保留包内卡片。分发层拦截实现,不受包加载顺序影响 |
|
|
106
|
+
| `foreignCardStyle` | `"generic"` | 无自带渲染方的卡片:自动接管为通用 `● 卡片`(零配置);`symbols` = 仅统一符号不接管布局 |
|
|
107
|
+
| `foreignCardSkip` | `[]` | 不接管的 customType 黑名单(交互型第三方卡片异常时按类型跳过) |
|
|
108
|
+
| `diff.diffViewMode` | `"auto"` | diff 视图:`auto`/`split`/`unified` |
|
|
109
|
+
| `diff.diffIndicatorMode` | `"bars"` | 变更指示条:`bars`/`classic`/`none` |
|
|
110
|
+
| `diff.diffSplitMinWidth` | `120` | 宽于此才用 split 视图 |
|
|
111
|
+
| `diff.edit/writeDiffCollapsedLines` | `0`/`0` | edit/write 默认折叠行数(0 = 默认展开) |
|
|
112
|
+
| `diff.diffWordWrap` | `true` | diff 自动换行 |
|
|
113
|
+
| `diff.expandedPreviewMaxLines` | `40` | 展开后最多显示行数 |
|
|
114
|
+
|
|
115
|
+
## 兼容性
|
|
116
|
+
|
|
117
|
+
按 `VERIFIED_PI_VERSION = 1.0.2` 验收;版本不一致时扩展会在 session 启动时提示一次。
|
|
118
|
+
|
|
119
|
+
## 从源码安装
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pi install git:github.com/chenyn273/pi-claude-code-ui@v0.2.0
|
|
123
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yinan_chen/pi-claude-code-ui",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "Claude Code-style UI for Pi: transcript view, rich diffs, subagent panels and working-message status.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
],
|
|
21
21
|
"files": [
|
|
22
22
|
"src",
|
|
23
|
-
"README.md"
|
|
23
|
+
"README.md",
|
|
24
|
+
"README.zh-CN.md"
|
|
24
25
|
],
|
|
25
26
|
"pi": {
|
|
26
27
|
"extensions": [
|
|
@@ -39,7 +39,13 @@ export function normalizeForeignLines(lines: readonly string[]): string[] {
|
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* patch 渲染分发器(ExtensionRunner:同时有 getMessageRenderer + getEntryRenderer 原型方法)。
|
|
42
|
-
*
|
|
42
|
+
* 两层接管语义(v0.9.2):
|
|
43
|
+
* 1. 替换层(impl.rendererOverride:skip 名单 > subagent/workflow 内置替换)先行,
|
|
44
|
+
* 无视原查表——替换型的优先级不受包加载顺序影响;
|
|
45
|
+
* 2. 未命中替换层时先走原查表拿「有主」判定:查到已注册渲染器(有主)就让路返回它;
|
|
46
|
+
* 3. 查不到(无主)才问 impl.genericFallback(generic 兜底,仅 message 侧——原生对无
|
|
47
|
+
* renderer 的 entry 完全不显示,entry 侧挖出数据型条目只是噪音),兜底不接则回落原生。
|
|
48
|
+
* trampoline:wrapper 每次调用现读 globalThis.__leanTidyImpl,/reload 后新配置立即生效。
|
|
43
49
|
*/
|
|
44
50
|
export function patchRendererDispatch(cls: any): boolean {
|
|
45
51
|
const proto = cls?.prototype;
|
|
@@ -47,15 +53,20 @@ export function patchRendererDispatch(cls: any): boolean {
|
|
|
47
53
|
proto[FLAG] = true;
|
|
48
54
|
for (const method of ["getMessageRenderer", "getEntryRenderer"]) {
|
|
49
55
|
const orig = proto[method];
|
|
50
|
-
// kind("getMessageRenderer"/"getEntryRenderer")随转发传给 impl
|
|
51
|
-
// 原生对无 renderer 的 entry 完全不显示(addCustomEntryToChat 直接 return),entry 侧挖出数据型条目只是噪音。
|
|
56
|
+
// kind("getMessageRenderer"/"getEntryRenderer")随转发传给 impl 两层。
|
|
52
57
|
proto[method] = function (customType: string) {
|
|
53
58
|
const impl = (globalThis as any).__leanTidyImpl;
|
|
54
59
|
try {
|
|
55
60
|
const override = impl?.rendererOverride?.(customType, method);
|
|
56
61
|
if (override) return override;
|
|
57
62
|
} catch {}
|
|
58
|
-
|
|
63
|
+
const native = orig.call(this, customType);
|
|
64
|
+
if (native) return native;
|
|
65
|
+
try {
|
|
66
|
+
const fallback = impl?.genericFallback?.(customType, method);
|
|
67
|
+
if (fallback) return fallback;
|
|
68
|
+
} catch {}
|
|
69
|
+
return native;
|
|
59
70
|
};
|
|
60
71
|
}
|
|
61
72
|
return true;
|
package/src/generic-card.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* generic-card —
|
|
2
|
+
* generic-card — 无主 customType(无任何渲染方认领)的通用 Claude 风格卡片(v0.7.0)。
|
|
3
3
|
*
|
|
4
|
-
* foreignCardStyle="generic"
|
|
4
|
+
* foreignCardStyle="generic"(默认)时,原查表落空的无主 custom 消息 /
|
|
5
5
|
* 会话条目都自动渲染为:
|
|
6
6
|
*
|
|
7
7
|
* ● Label
|
package/src/index.ts
CHANGED
|
@@ -93,7 +93,7 @@ interface Config {
|
|
|
93
93
|
normalizeForeignRenderers: boolean;
|
|
94
94
|
/** workflow 完成卡片(subagents:workflow entry)样式:claude = ● Workflow(name) 风格(默认);native = 保留包内卡片。 */
|
|
95
95
|
workflowRendering: "claude" | "native";
|
|
96
|
-
/**
|
|
96
|
+
/** 无主 customType(无自带渲染方)的卡片:generic = 自动接管为通用 ● 卡片(默认);symbols = 仅统一符号不接管布局。 */
|
|
97
97
|
foreignCardStyle: "generic" | "symbols";
|
|
98
98
|
/** 不接管的 customType 黑名单(保留第三方/原生渲染)。 */
|
|
99
99
|
foreignCardSkip: string[];
|
|
@@ -402,6 +402,8 @@ export default async function leanTidy(pi: ExtensionAPI): Promise<void> {
|
|
|
402
402
|
if (DEBUG) {
|
|
403
403
|
try { writeFileSync(LOG, `=== lean-tidy load #${loadCounter} ${new Date().toISOString()} ===\n`); } catch {}
|
|
404
404
|
}
|
|
405
|
+
// generic 兜底接管计数(每次加载重置,与日志头对齐):同名去重、聚合计数,仅诊断通道可见。
|
|
406
|
+
const takeoverCounts = new Map<string, number>();
|
|
405
407
|
let noticeContext: any;
|
|
406
408
|
let lastWarning: string | undefined;
|
|
407
409
|
(globalThis as any).__leanTidyClaude?.dispose?.();
|
|
@@ -432,13 +434,21 @@ export default async function leanTidy(pi: ExtensionAPI): Promise<void> {
|
|
|
432
434
|
claude && chat ? claude.renderChat(container, width) : Container.prototype.render.call(container, width),
|
|
433
435
|
normalizeForeign: (lines: readonly string[]): string[] =>
|
|
434
436
|
config.normalizeForeignRenderers ? normalizeForeignLines(lines) : (lines as string[]),
|
|
435
|
-
/**
|
|
436
|
-
rendererOverride: (customType: string,
|
|
437
|
+
/** 分发层拦截·替换层:skip 名单 > subagent/workflow 内置替换,先行于原查表;generic 兜底已拆至 genericFallback。 */
|
|
438
|
+
rendererOverride: (customType: string, _via?: "getMessageRenderer" | "getEntryRenderer"): any => {
|
|
437
439
|
if (config.foreignCardSkip.includes(customType)) return undefined;
|
|
438
440
|
if (customType === "subagent-notification") return config.subagentRendering === "claude" ? subagentRenderer : undefined;
|
|
439
441
|
if (customType === "subagents:workflow") return config.workflowRendering === "claude" ? workflowRenderer : undefined;
|
|
440
|
-
|
|
441
|
-
|
|
442
|
+
return undefined;
|
|
443
|
+
},
|
|
444
|
+
/** 分发层拦截·兜底层:仅原查表落空(无主)且 message 侧才接管为 generic 卡片——不偷任何已注册渲染器。 */
|
|
445
|
+
genericFallback: (customType: string, via?: "getMessageRenderer" | "getEntryRenderer"): any => {
|
|
446
|
+
// skip 判断在这里是必要的而非防御:替换层对名单返回 undefined 让路,wrapper 仍会继续问兜底层。
|
|
447
|
+
if (via === "getEntryRenderer" || config.foreignCardStyle !== "generic" || config.foreignCardSkip.includes(customType)) return undefined;
|
|
448
|
+
const count = (takeoverCounts.get(customType) ?? 0) + 1;
|
|
449
|
+
takeoverCounts.set(customType, count);
|
|
450
|
+
diag("generic takeover:", customType, `x${count}`);
|
|
451
|
+
return genericCardRenderer;
|
|
442
452
|
},
|
|
443
453
|
} as RuntimeImpl;
|
|
444
454
|
for (const method of IMPL_METHODS) {
|
|
@@ -571,7 +581,7 @@ export default async function leanTidy(pi: ExtensionAPI): Promise<void> {
|
|
|
571
581
|
* 冷键在加载期固化(视图与面板注册),运行时 apply 无意义,直接跳过。 */
|
|
572
582
|
function applySetting(id: string, value: unknown): void {
|
|
573
583
|
const def = SETTING_BY_ID.get(id);
|
|
574
|
-
if (!def || def.
|
|
584
|
+
if (!def || def.reloadRequired) return;
|
|
575
585
|
if (id.startsWith("diff.")) {
|
|
576
586
|
diffDisplayConfig = { ...diffDisplayConfig, [id.slice("diff.".length)]: value };
|
|
577
587
|
return;
|
|
@@ -586,18 +596,18 @@ export default async function leanTidy(pi: ExtensionAPI): Promise<void> {
|
|
|
586
596
|
let confirmed = false;
|
|
587
597
|
try {
|
|
588
598
|
confirmed = await ctx?.ui?.confirm?.(
|
|
589
|
-
"
|
|
590
|
-
"
|
|
599
|
+
"Restore default settings",
|
|
600
|
+
"This will delete all known skin config keys from lean-tidy.json (unknown keys are kept); live settings revert immediately. Continue?",
|
|
591
601
|
);
|
|
592
602
|
} catch { confirmed = false; }
|
|
593
603
|
if (!confirmed) return; // Esc / 取消:文件与运行时零改动。
|
|
594
|
-
const {
|
|
604
|
+
const { reloadRequiredReset } = resetConfigFile(CONFIG_PATH);
|
|
595
605
|
// diff 显示配置先整体回出厂拷贝(文件 diff 里的未知键不留在运行时),
|
|
596
606
|
// 再走统一的热键 apply 通道逐键重置(幂等,值相同)。
|
|
597
607
|
diffDisplayConfig = { ...DEFAULT_TOOL_DISPLAY_CONFIG };
|
|
598
608
|
applyRuntimeDefaults(applySetting);
|
|
599
609
|
forceRedraw();
|
|
600
|
-
if (
|
|
610
|
+
if (reloadRequiredReset.length > 0) ctx?.ui?.notify?.(`Defaults restored — ${reloadRequiredReset.length} setting(s) need /reload`, "info");
|
|
601
611
|
}
|
|
602
612
|
|
|
603
613
|
/** 设置面板(工单 03):当前值读取统一走 currentValueOf——顶层键读共享 config,diff.* 读 diffDisplayConfig(闭包每次读模块变量,apply 整体替换后仍取最新值)。 */
|
|
@@ -613,9 +623,9 @@ export default async function leanTidy(pi: ExtensionAPI): Promise<void> {
|
|
|
613
623
|
// /skin-setting:无参 = 打开设置面板(仅 TUI;守卫与非 TUI 提示在面板模块内),
|
|
614
624
|
// default = 确认后一键恢复默认。命令补全只提示 default。
|
|
615
625
|
pi.registerCommand("skin-setting", {
|
|
616
|
-
description: "lean-tidy
|
|
626
|
+
description: "lean-tidy: skin settings (default = reset to defaults)",
|
|
617
627
|
getArgumentCompletions(prefix: string) {
|
|
618
|
-
const items = [{ value: "default", label: "default", description: "
|
|
628
|
+
const items = [{ value: "default", label: "default", description: "Reset all settings to defaults (unknown keys are kept)" }];
|
|
619
629
|
return items.filter((item) => item.value.startsWith(prefix));
|
|
620
630
|
},
|
|
621
631
|
handler: async (args: string, ctx: any) => {
|
|
@@ -630,14 +640,14 @@ export default async function leanTidy(pi: ExtensionAPI): Promise<void> {
|
|
|
630
640
|
return;
|
|
631
641
|
}
|
|
632
642
|
ctx?.ui?.notify?.(
|
|
633
|
-
|
|
643
|
+
`Unknown argument "${arg}". Usage: /skin-setting opens the settings panel (TUI only); /skin-setting default restores defaults. Config file: ${CONFIG_PATH}`,
|
|
634
644
|
"info",
|
|
635
645
|
);
|
|
636
646
|
},
|
|
637
647
|
});
|
|
638
648
|
|
|
639
649
|
pi.registerShortcut("f5" as any, {
|
|
640
|
-
description: "lean-tidy:
|
|
650
|
+
description: "lean-tidy: force full redraw",
|
|
641
651
|
handler: () => { forceRedraw(); },
|
|
642
652
|
});
|
|
643
653
|
|
package/src/settings.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 皮肤设置纯逻辑模块(零 TUI 依赖)。
|
|
3
3
|
*
|
|
4
|
-
* 职责:25 个配置键的元数据(规范 id / label /
|
|
4
|
+
* 职责:25 个配置键的元数据(规范 id / label / 英文描述 / 类型 / 合法值或范围 /
|
|
5
5
|
* 冷热分类 / 默认值)、编辑侧校验、编辑形态派生判定(循环域 / 输入子菜单——
|
|
6
6
|
* def.type 的装配侧分派单点收敛于此,validateSetting 的 switch 是校验唯一实现点)、
|
|
7
7
|
* lean-tidy.json 的 read-modify-write 原子写、恢复默认的文件语义,以及热键默认值的
|
|
@@ -25,9 +25,9 @@ export type SettingType = "enum" | "boolean" | "number" | "string" | "readonly-a
|
|
|
25
25
|
export interface SettingDef {
|
|
26
26
|
/** 规范 id:顶层键用 camelCase,diff 段用 "diff.xxx" 点路径(与 lean-tidy.json 键名一致)。 */
|
|
27
27
|
id: string;
|
|
28
|
-
/**
|
|
28
|
+
/** 面板展示用的简短英文标签。 */
|
|
29
29
|
label: string;
|
|
30
|
-
/**
|
|
30
|
+
/** 英文描述(含默认值),面板行与文档共用。 */
|
|
31
31
|
description: string;
|
|
32
32
|
type: SettingType;
|
|
33
33
|
defaultValue: unknown;
|
|
@@ -39,128 +39,128 @@ export interface SettingDef {
|
|
|
39
39
|
/** true = 严格整数(0–100 / 0–4 类);false = 非负且向下取整(≥0 类)。 */
|
|
40
40
|
integer?: boolean;
|
|
41
41
|
/** 冷键:加载期固化,改动只落文件,需 /reload 生效。 */
|
|
42
|
-
|
|
42
|
+
reloadRequired: boolean;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/** 25 键元数据。默认值是唯一事实来源:loadConfig 经 topLevelDefaults() 消费,消除双源。 */
|
|
46
46
|
export const SETTING_DEFS: readonly SettingDef[] = [
|
|
47
47
|
// ---- 顶层 18 键 ----
|
|
48
48
|
{
|
|
49
|
-
id: "profile", label: "
|
|
50
|
-
description: "
|
|
49
|
+
id: "profile", label: "Profile", type: "enum", values: ["claude", "native"], defaultValue: "claude", reloadRequired: true,
|
|
50
|
+
description: "Overall look: claude = Claude Code-style views (default); native = pi native fallback.",
|
|
51
51
|
},
|
|
52
52
|
{
|
|
53
|
-
id: "skillRendering", label: "Skill
|
|
54
|
-
description: "Skill
|
|
53
|
+
id: "skillRendering", label: "Skill rendering", type: "enum", values: ["native", "claude"], defaultValue: "native", reloadRequired: false,
|
|
54
|
+
description: "Skill invocation block style: native = pi native [skill] collapsed line (default); claude = ● Skill(name) two-line style.",
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
|
-
id: "subagentRendering", label: "Subagent
|
|
58
|
-
description: "Subagent
|
|
57
|
+
id: "subagentRendering", label: "Subagent rendering", type: "enum", values: ["native", "claude"], defaultValue: "claude", reloadRequired: true,
|
|
58
|
+
description: "Subagent completion notice style: claude = ● Subagent(desc) + branches (default); native = keep the bundled panel.",
|
|
59
59
|
},
|
|
60
60
|
{
|
|
61
|
-
id: "detailsBackground", label: "
|
|
61
|
+
id: "detailsBackground", label: "Details background", type: "enum",
|
|
62
62
|
values: ["selectedBg", "customMessageBg", "toolPendingBg", "toolSuccessBg", "toolErrorBg", ""],
|
|
63
|
-
defaultValue: "selectedBg",
|
|
64
|
-
description: "
|
|
63
|
+
defaultValue: "selectedBg", reloadRequired: false,
|
|
64
|
+
description: "Background color of the expanded tool details area (default selectedBg; empty string = no background).",
|
|
65
65
|
},
|
|
66
66
|
{
|
|
67
|
-
id: "bashPreviewLines", label: "Bash
|
|
68
|
-
description: "bash
|
|
67
|
+
id: "bashPreviewLines", label: "Bash preview lines", type: "number", min: 0, max: 100, integer: true, defaultValue: 0, reloadRequired: false,
|
|
68
|
+
description: "Output preview lines for collapsed bash tool results, 0–100 (default 0).",
|
|
69
69
|
},
|
|
70
70
|
{
|
|
71
|
-
id: "codePreviewLines", label: "
|
|
72
|
-
description: "read / write / edit / find
|
|
71
|
+
id: "codePreviewLines", label: "Code preview lines", type: "number", min: 0, max: 100, integer: true, defaultValue: 0, reloadRequired: false,
|
|
72
|
+
description: "Preview lines for collapsed read / write / edit / find tools, 0–100 (default 0).",
|
|
73
73
|
},
|
|
74
74
|
{
|
|
75
|
-
id: "errorPreviewLines", label: "
|
|
76
|
-
description: "
|
|
75
|
+
id: "errorPreviewLines", label: "Error preview lines", type: "number", min: 0, max: 100, integer: true, defaultValue: 0, reloadRequired: false,
|
|
76
|
+
description: "Preview lines for collapsed failed tool results, 0–100 (default 0).",
|
|
77
77
|
},
|
|
78
78
|
{
|
|
79
|
-
id: "liveThinking", label: "
|
|
80
|
-
description: "
|
|
79
|
+
id: "liveThinking", label: "Live thinking stream", type: "boolean", defaultValue: true, reloadRequired: false,
|
|
80
|
+
description: "Stream thinking blocks as they arrive (default true).",
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
|
-
id: "thinkingTailLines", label: "
|
|
84
|
-
description: "
|
|
83
|
+
id: "thinkingTailLines", label: "Thinking tail lines", type: "number", min: 0, defaultValue: 5, reloadRequired: false,
|
|
84
|
+
description: "Lines kept at the tail when thinking is collapsed, ≥0 (default 5).",
|
|
85
85
|
},
|
|
86
86
|
{
|
|
87
|
-
id: "foldAnimMs", label: "
|
|
88
|
-
description: "
|
|
87
|
+
id: "foldAnimMs", label: "Fold animation duration", type: "number", min: 0, defaultValue: 240, reloadRequired: false,
|
|
88
|
+
description: "Code block fold animation duration in milliseconds, ≥0 (default 240).",
|
|
89
89
|
},
|
|
90
90
|
{
|
|
91
|
-
id: "stickyIncludeCustom", label: "
|
|
92
|
-
description: "sticky
|
|
91
|
+
id: "stickyIncludeCustom", label: "Sticky includes custom messages", type: "boolean", defaultValue: false, reloadRequired: true,
|
|
92
|
+
description: "Whether the sticky pool includes CustomMessages injected by extensions (default false).",
|
|
93
93
|
},
|
|
94
94
|
{
|
|
95
|
-
id: "skillResumePreview", label: "
|
|
96
|
-
description: "/resume
|
|
95
|
+
id: "skillResumePreview", label: "Resume preview fold-back", type: "boolean", defaultValue: true, reloadRequired: true,
|
|
96
|
+
description: "Whether the /resume session list preview folds back to [skill] names (default true).",
|
|
97
97
|
},
|
|
98
98
|
{
|
|
99
|
-
id: "resultPrefix", label: "
|
|
100
|
-
description: "
|
|
99
|
+
id: "resultPrefix", label: "Result line prefix", type: "string", defaultValue: "└", reloadRequired: false,
|
|
100
|
+
description: "Corner prefix symbol for tool result lines (default └; can be ↳ or ⎿).",
|
|
101
101
|
},
|
|
102
102
|
{
|
|
103
|
-
id: "resultPrefixGap", label: "
|
|
104
|
-
description: "
|
|
103
|
+
id: "resultPrefixGap", label: "Prefix gap", type: "number", min: 0, max: 4, integer: true, defaultValue: 1, reloadRequired: false,
|
|
104
|
+
description: "Spaces between the prefix symbol and content, 0–4 (default 1).",
|
|
105
105
|
},
|
|
106
106
|
{
|
|
107
|
-
id: "normalizeForeignRenderers", label: "
|
|
108
|
-
description: "
|
|
107
|
+
id: "normalizeForeignRenderers", label: "Normalize foreign renderers", type: "boolean", defaultValue: true, reloadRequired: false,
|
|
108
|
+
description: "Rewrite the leading ⎿ of third-party extension output to resultPrefix (default true).",
|
|
109
109
|
},
|
|
110
110
|
{
|
|
111
|
-
id: "workflowRendering", label: "Workflow
|
|
112
|
-
description: "
|
|
111
|
+
id: "workflowRendering", label: "Workflow rendering", type: "enum", values: ["claude", "native"], defaultValue: "claude", reloadRequired: true,
|
|
112
|
+
description: "Workflow completion card style: claude = ● Workflow(name) (default); native = keep the bundled card.",
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
|
-
id: "foreignCardStyle", label: "
|
|
116
|
-
description: "
|
|
115
|
+
id: "foreignCardStyle", label: "Foreign card style", type: "enum", values: ["generic", "symbols"], defaultValue: "generic", reloadRequired: false,
|
|
116
|
+
description: "Cards without their own renderer: generic = take over as a generic card (default); symbols = unify symbols only, keep layout.",
|
|
117
117
|
},
|
|
118
118
|
{
|
|
119
|
-
id: "foreignCardSkip", label: "
|
|
120
|
-
description: "
|
|
119
|
+
id: "foreignCardSkip", label: "Card takeover blocklist", type: "readonly-array", defaultValue: [], reloadRequired: false,
|
|
120
|
+
description: "customType blocklist never taken over (read-only; default []).",
|
|
121
121
|
},
|
|
122
122
|
// ---- diff.* 7 键(默认值直接引用 vendored DEFAULT_TOOL_DISPLAY_CONFIG,保持同源)----
|
|
123
123
|
{
|
|
124
|
-
id: "diff.diffViewMode", label: "
|
|
125
|
-
defaultValue: DEFAULT_TOOL_DISPLAY_CONFIG.diffViewMode,
|
|
126
|
-
description: "
|
|
124
|
+
id: "diff.diffViewMode", label: "Diff view mode", type: "enum", values: ["auto", "split", "unified"],
|
|
125
|
+
defaultValue: DEFAULT_TOOL_DISPLAY_CONFIG.diffViewMode, reloadRequired: false,
|
|
126
|
+
description: "Diff display mode: auto / split / unified (default auto).",
|
|
127
127
|
},
|
|
128
128
|
{
|
|
129
|
-
id: "diff.diffIndicatorMode", label: "
|
|
130
|
-
defaultValue: DEFAULT_TOOL_DISPLAY_CONFIG.diffIndicatorMode,
|
|
131
|
-
description: "
|
|
129
|
+
id: "diff.diffIndicatorMode", label: "Diff indicator", type: "enum", values: ["bars", "classic", "none"],
|
|
130
|
+
defaultValue: DEFAULT_TOOL_DISPLAY_CONFIG.diffIndicatorMode, reloadRequired: false,
|
|
131
|
+
description: "Diff change indicator: bars / classic / none (default bars).",
|
|
132
132
|
},
|
|
133
133
|
{
|
|
134
|
-
id: "diff.diffSplitMinWidth", label: "
|
|
135
|
-
defaultValue: DEFAULT_TOOL_DISPLAY_CONFIG.diffSplitMinWidth,
|
|
136
|
-
description: "split
|
|
134
|
+
id: "diff.diffSplitMinWidth", label: "Split min width", type: "number", min: 0,
|
|
135
|
+
defaultValue: DEFAULT_TOOL_DISPLAY_CONFIG.diffSplitMinWidth, reloadRequired: false,
|
|
136
|
+
description: "Minimum total width for split view; falls back to unified below it (default 120).",
|
|
137
137
|
},
|
|
138
138
|
{
|
|
139
|
-
id: "diff.editDiffCollapsedLines", label: "
|
|
140
|
-
defaultValue: DEFAULT_TOOL_DISPLAY_CONFIG.editDiffCollapsedLines,
|
|
141
|
-
description: "edit diff
|
|
139
|
+
id: "diff.editDiffCollapsedLines", label: "Edit collapsed lines", type: "number", min: 0,
|
|
140
|
+
defaultValue: DEFAULT_TOOL_DISPLAY_CONFIG.editDiffCollapsedLines, reloadRequired: false,
|
|
141
|
+
description: "Context lines shown when an edit diff is collapsed (default 0).",
|
|
142
142
|
},
|
|
143
143
|
{
|
|
144
|
-
id: "diff.writeDiffCollapsedLines", label: "
|
|
145
|
-
defaultValue: DEFAULT_TOOL_DISPLAY_CONFIG.writeDiffCollapsedLines,
|
|
146
|
-
description: "write diff
|
|
144
|
+
id: "diff.writeDiffCollapsedLines", label: "Write collapsed lines", type: "number", min: 0,
|
|
145
|
+
defaultValue: DEFAULT_TOOL_DISPLAY_CONFIG.writeDiffCollapsedLines, reloadRequired: false,
|
|
146
|
+
description: "Context lines shown when a write diff is collapsed (default 0).",
|
|
147
147
|
},
|
|
148
148
|
{
|
|
149
|
-
id: "diff.diffWordWrap", label: "
|
|
150
|
-
defaultValue: DEFAULT_TOOL_DISPLAY_CONFIG.diffWordWrap,
|
|
151
|
-
description: "diff
|
|
149
|
+
id: "diff.diffWordWrap", label: "Diff word wrap", type: "boolean",
|
|
150
|
+
defaultValue: DEFAULT_TOOL_DISPLAY_CONFIG.diffWordWrap, reloadRequired: false,
|
|
151
|
+
description: "Wrap overly wide diff lines (default true).",
|
|
152
152
|
},
|
|
153
153
|
{
|
|
154
|
-
id: "diff.expandedPreviewMaxLines", label: "
|
|
155
|
-
defaultValue: DEFAULT_TOOL_DISPLAY_CONFIG.expandedPreviewMaxLines,
|
|
156
|
-
description: "diff
|
|
154
|
+
id: "diff.expandedPreviewMaxLines", label: "Expanded preview max lines", type: "number", min: 0,
|
|
155
|
+
defaultValue: DEFAULT_TOOL_DISPLAY_CONFIG.expandedPreviewMaxLines, reloadRequired: false,
|
|
156
|
+
description: "Maximum lines for expanded diff previews (default 40).",
|
|
157
157
|
},
|
|
158
158
|
];
|
|
159
159
|
|
|
160
160
|
export const SETTING_BY_ID: ReadonlyMap<string, SettingDef> = new Map(SETTING_DEFS.map((def) => [def.id, def]));
|
|
161
161
|
|
|
162
162
|
/** 冷键 id 清单(派生自元数据):加载期固化,运行时不可 apply。 */
|
|
163
|
-
export const
|
|
163
|
+
export const RELOAD_REQUIRED_SETTING_IDS: readonly string[] = SETTING_DEFS.filter((def) => def.reloadRequired).map((def) => def.id);
|
|
164
164
|
|
|
165
165
|
/** 18 个顶层键的默认值对象——loadConfig 的初始值由此构造(默认值单一事实来源)。 */
|
|
166
166
|
export function topLevelDefaults(): Record<string, unknown> {
|
|
@@ -194,39 +194,39 @@ export function hasInputEditor(def: SettingDef): boolean {
|
|
|
194
194
|
|
|
195
195
|
export type ValidateResult = { ok: true; value: unknown } | { ok: false; error: string };
|
|
196
196
|
|
|
197
|
-
/**
|
|
197
|
+
/** 校验单键取值:非法值拒绝并给出英文错误;通过时返回归一化后的值(≥0 键取整)。 */
|
|
198
198
|
export function validateSetting(id: string, value: unknown): ValidateResult {
|
|
199
199
|
const def = SETTING_BY_ID.get(id);
|
|
200
|
-
if (!def) return { ok: false, error:
|
|
200
|
+
if (!def) return { ok: false, error: `Unknown setting key: ${id}` };
|
|
201
201
|
switch (def.type) {
|
|
202
202
|
case "enum": {
|
|
203
203
|
if (def.values?.includes(value)) return { ok: true, value };
|
|
204
204
|
const legal = def.values?.map((v) => JSON.stringify(v)).join(" / ") ?? "";
|
|
205
|
-
return { ok: false, error: `${def.label}
|
|
205
|
+
return { ok: false, error: `${def.label} only accepts one of ${legal}` };
|
|
206
206
|
}
|
|
207
207
|
case "boolean":
|
|
208
208
|
return typeof value === "boolean"
|
|
209
209
|
? { ok: true, value }
|
|
210
|
-
: { ok: false, error: `${def.label}
|
|
210
|
+
: { ok: false, error: `${def.label} only accepts true / false` };
|
|
211
211
|
case "number": {
|
|
212
212
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
213
|
-
return { ok: false, error: `${def.label}
|
|
213
|
+
return { ok: false, error: `${def.label} must be a number` };
|
|
214
214
|
}
|
|
215
215
|
if (def.integer && !Number.isInteger(value)) {
|
|
216
|
-
return { ok: false, error: `${def.label}
|
|
216
|
+
return { ok: false, error: `${def.label} must be an integer (${def.min}–${def.max})` };
|
|
217
217
|
}
|
|
218
218
|
if (value < (def.min ?? -Infinity) || value > (def.max ?? Infinity)) {
|
|
219
219
|
const upper = def.max === undefined ? "∞" : def.max;
|
|
220
|
-
return { ok: false, error: `${def.label}
|
|
220
|
+
return { ok: false, error: `${def.label} is out of range ${def.min}–${upper}` };
|
|
221
221
|
}
|
|
222
222
|
return { ok: true, value: def.integer ? value : Math.floor(value) };
|
|
223
223
|
}
|
|
224
224
|
case "string":
|
|
225
225
|
return typeof value === "string" && value.trim()
|
|
226
226
|
? { ok: true, value }
|
|
227
|
-
: { ok: false, error: `${def.label}
|
|
227
|
+
: { ok: false, error: `${def.label} cannot be a blank string` };
|
|
228
228
|
case "readonly-array":
|
|
229
|
-
return { ok: false, error: `${def.label}
|
|
229
|
+
return { ok: false, error: `${def.label} is a read-only blocklist; edit the config file manually` };
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
232
|
|
|
@@ -284,7 +284,7 @@ export function setSettingInFile(path: string, id: string, value: unknown): Reco
|
|
|
284
284
|
|
|
285
285
|
export interface ResetFileResult {
|
|
286
286
|
/** 文件中原本非默认、被本次删除的冷键 id(调用方据此提醒「N 项需 /reload 生效」)。 */
|
|
287
|
-
|
|
287
|
+
reloadRequiredReset: string[];
|
|
288
288
|
}
|
|
289
289
|
|
|
290
290
|
/**
|
|
@@ -293,13 +293,13 @@ export interface ResetFileResult {
|
|
|
293
293
|
* 冷键默认值均为原始类型(enum/boolean),用严格相等判断「原本非默认」。
|
|
294
294
|
*/
|
|
295
295
|
export function resetConfigFile(path: string): ResetFileResult {
|
|
296
|
-
if (!existsSync(path)) return {
|
|
296
|
+
if (!existsSync(path)) return { reloadRequiredReset: [] };
|
|
297
297
|
const obj = readConfigObject(path);
|
|
298
|
-
const
|
|
298
|
+
const reloadRequiredReset: string[] = [];
|
|
299
299
|
for (const def of SETTING_DEFS) {
|
|
300
300
|
if (def.id.includes(".")) continue;
|
|
301
301
|
if (!(def.id in obj)) continue;
|
|
302
|
-
if (def.
|
|
302
|
+
if (def.reloadRequired && obj[def.id] !== def.defaultValue) reloadRequiredReset.push(def.id);
|
|
303
303
|
delete obj[def.id];
|
|
304
304
|
}
|
|
305
305
|
const diff = obj.diff;
|
|
@@ -311,7 +311,7 @@ export function resetConfigFile(path: string): ResetFileResult {
|
|
|
311
311
|
if (Object.keys(diff).length === 0) delete obj.diff;
|
|
312
312
|
}
|
|
313
313
|
writeConfigAtomic(path, obj);
|
|
314
|
-
return {
|
|
314
|
+
return { reloadRequiredReset };
|
|
315
315
|
}
|
|
316
316
|
|
|
317
317
|
/* ---------- 运行时重置(回调注入) ---------- */
|
|
@@ -322,6 +322,6 @@ export type ApplyFn = (id: string, value: unknown) => void;
|
|
|
322
322
|
/** 热键逐键 apply 默认值;冷键加载期固化、不 apply(/reload 后生效)。 */
|
|
323
323
|
export function applyRuntimeDefaults(apply: ApplyFn): void {
|
|
324
324
|
for (const def of SETTING_DEFS) {
|
|
325
|
-
if (!def.
|
|
325
|
+
if (!def.reloadRequired) apply(def.id, def.defaultValue);
|
|
326
326
|
}
|
|
327
327
|
}
|
|
@@ -60,7 +60,7 @@ function themeFg(theme: any): (color: string, text: string) => string {
|
|
|
60
60
|
/** SettingsList 的子菜单 done 契约(settings-list.d.ts):带值提交 → 行刷新 + onChange;无值 → 仅关闭回原行。 */
|
|
61
61
|
type SubmenuDone = (selectedValue?: string, options?: { navigateTo?: string }) => void;
|
|
62
62
|
|
|
63
|
-
/**
|
|
63
|
+
/** 即改即存提交链路结果:失败时携带英文错误(由调用方决定 notify 还是子菜单原地展示)。 */
|
|
64
64
|
export type CommitResult = { ok: true; value: unknown } | { ok: false; error: string };
|
|
65
65
|
export type CommitFn = (id: string, value: unknown) => CommitResult;
|
|
66
66
|
|
|
@@ -68,9 +68,9 @@ export type CommitFn = (id: string, value: unknown) => CommitResult;
|
|
|
68
68
|
function inputConstraintHint(def: SettingDef): string {
|
|
69
69
|
if (def.type === "number") {
|
|
70
70
|
const upper = def.max === undefined ? "∞" : String(def.max);
|
|
71
|
-
return def.integer ?
|
|
71
|
+
return def.integer ? `Range ${def.min}–${upper} (integer)` : `Range ≥ ${def.min} (fractional values are floored)`;
|
|
72
72
|
}
|
|
73
|
-
return "trim
|
|
73
|
+
return "non-blank after trim";
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/** 文本 → 待校验值:数字键 Number() 解析(空输入按 NaN,交给 validateSetting 出中文错误);字符串键原样。 */
|
|
@@ -80,7 +80,7 @@ function parseInputValue(def: SettingDef, text: string): unknown {
|
|
|
80
80
|
return trimmed === "" ? Number.NaN : Number(trimmed);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
/**
|
|
83
|
+
/** 原地错误行:无错误时零行,有错误时一行红色提示(非法输入不落盘的呈现位)。 */
|
|
84
84
|
class SubmenuErrorLine implements Component {
|
|
85
85
|
error: string | null = null;
|
|
86
86
|
private readonly fg: (color: string, text: string) => string;
|
|
@@ -129,14 +129,14 @@ export class SettingInputSubmenu extends Container {
|
|
|
129
129
|
}
|
|
130
130
|
const placeholder = def.type === "number" ? String(current) : String(current ?? "");
|
|
131
131
|
this.addChild(new Text(fg("accent", def.label), 1, 0));
|
|
132
|
-
this.addChild(new Text(fg("muted", `${inputConstraintHint(def)} ·
|
|
132
|
+
this.addChild(new Text(fg("muted", `${inputConstraintHint(def)} · current ${currentValueJson}`), 1, 0));
|
|
133
133
|
this.addChild(new Spacer(1));
|
|
134
134
|
this.input = new Input({ placeholder, placeholderStyle: (text: string) => fg("dim", text) });
|
|
135
135
|
this.errorLine = new SubmenuErrorLine(fg);
|
|
136
136
|
this.input.onSubmit = (text: string) => {
|
|
137
137
|
const result = commit(def.id, parseInputValue(def, text));
|
|
138
138
|
if (!result.ok) {
|
|
139
|
-
//
|
|
139
|
+
// 原地提示不落盘:子菜单里显示错误行,输入框保留用户输入供改正,不关子菜单。
|
|
140
140
|
this.errorLine.error = result.error;
|
|
141
141
|
return;
|
|
142
142
|
}
|
|
@@ -148,7 +148,7 @@ export class SettingInputSubmenu extends Container {
|
|
|
148
148
|
};
|
|
149
149
|
this.addChild(this.input);
|
|
150
150
|
this.addChild(this.errorLine);
|
|
151
|
-
this.addChild(new Text(fg("dim", "Enter
|
|
151
|
+
this.addChild(new Text(fg("dim", "Enter to save · Esc to cancel"), 1, 0));
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
handleInput(data: string): void {
|
|
@@ -161,14 +161,14 @@ export class SettingInputSubmenu extends Container {
|
|
|
161
161
|
/**
|
|
162
162
|
* 面板行装配:25 键 → SettingItem[]。
|
|
163
163
|
* 冷键标注双保险:label 尾部 ⟳(SettingsList 的 description 只在选中行显示,
|
|
164
|
-
* 要「常驻可见」必须落在 label 上)+ description
|
|
164
|
+
* 要「常驻可见」必须落在 label 上)+ description 尾部 (⟳ needs /reload)。
|
|
165
165
|
*/
|
|
166
166
|
export function buildSettingItems(currentValueOf: (id: string) => unknown): SettingItem[] {
|
|
167
167
|
return SETTING_DEFS.map((def) => {
|
|
168
168
|
const item: SettingItem = {
|
|
169
169
|
id: def.id,
|
|
170
|
-
label: def.
|
|
171
|
-
description: def.
|
|
170
|
+
label: def.reloadRequired ? `${def.label} ⟳` : def.label,
|
|
171
|
+
description: def.reloadRequired ? `${def.description} (⟳ needs /reload)` : def.description,
|
|
172
172
|
currentValue: toJsonLiteral(currentValueOf(def.id)),
|
|
173
173
|
};
|
|
174
174
|
// 循环域(enum/boolean)由 settings.ts 的 cycleValuesOf 单点派生;number/string 的输入
|
|
@@ -209,10 +209,10 @@ class BorderLine implements Component {
|
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
/** 标题边框行:──
|
|
212
|
+
/** 标题边框行:── Skin Settings(…) ──,标题居中、随渲染宽度自适应(副句表达即改即存与输入即搜索)。 */
|
|
213
213
|
function titleBorder(fg: (color: string, text: string) => string): BorderLine {
|
|
214
214
|
return new BorderLine((width) => {
|
|
215
|
-
const title = "
|
|
215
|
+
const title = " Skin Settings (auto-save on change · type to search) ";
|
|
216
216
|
const fill = width - displayWidth(title) - 2;
|
|
217
217
|
const left = Math.max(1, Math.floor(fill / 2));
|
|
218
218
|
const right = Math.max(1, fill - left);
|
|
@@ -273,10 +273,10 @@ export async function handleSkinSettingCommand(deps: SkinSettingPanelDeps, ctx:
|
|
|
273
273
|
// 冷键 notify 去抖:同一键同一次面板会话只在首次改动时提醒。
|
|
274
274
|
// 理由:回车循环极易连按(切过头再切回),每按一次弹一条会刷屏遮挡面板;
|
|
275
275
|
// 首次提醒已传达「此键需 /reload」,同键后续改动无新信息。重开面板重置(新意图)。
|
|
276
|
-
const
|
|
276
|
+
const reloadRequiredNotified = new Set<string>();
|
|
277
277
|
let panel: SkinSettingPanelComponent;
|
|
278
278
|
// 即改即存提交链路(enum 循环与子菜单输入共用):校验 → RMW 落盘 → 热键原地
|
|
279
|
-
// 生效 → 面板行刷新 → 冷键首次 notify
|
|
279
|
+
// 生效 → 面板行刷新 → 冷键首次 notify。失败返回英文错误,由调用方决定呈现
|
|
280
280
|
// (列表循环路径 notify + 行回滚;子菜单路径原地错误行,不关子菜单)。
|
|
281
281
|
const commit: CommitFn = (id, value) => {
|
|
282
282
|
// 「未知配置键」错误文案由 validateSetting 单点产出(原先此处的预查是其重复);
|
|
@@ -287,13 +287,13 @@ export async function handleSkinSettingCommand(deps: SkinSettingPanelDeps, ctx:
|
|
|
287
287
|
try {
|
|
288
288
|
setSettingInFile(deps.configPath, id, check.value);
|
|
289
289
|
} catch (error: any) {
|
|
290
|
-
return { ok: false, error:
|
|
290
|
+
return { ok: false, error: `Failed to write ${deps.configPath}: ${error?.message ?? error}` };
|
|
291
291
|
}
|
|
292
292
|
deps.applySetting(id, check.value); // 热键原地生效;冷键 write-only(注入方 no-op)
|
|
293
293
|
panel.list.updateValue(id, toJsonLiteral(check.value));
|
|
294
|
-
if (def.
|
|
295
|
-
|
|
296
|
-
notify(`⟳ ${def.label}
|
|
294
|
+
if (def.reloadRequired && !reloadRequiredNotified.has(id)) {
|
|
295
|
+
reloadRequiredNotified.add(id);
|
|
296
|
+
notify(`⟳ ${def.label} saved — takes effect after /reload`, "info");
|
|
297
297
|
}
|
|
298
298
|
return { ok: true, value: check.value };
|
|
299
299
|
};
|
|
@@ -337,7 +337,7 @@ export async function handleSkinSettingCommand(deps: SkinSettingPanelDeps, ctx:
|
|
|
337
337
|
ctx?.ui?.notify?.(message, level);
|
|
338
338
|
};
|
|
339
339
|
if (ctx?.mode !== "tui" || typeof ctx?.ui?.custom !== "function") {
|
|
340
|
-
notify(
|
|
340
|
+
notify(`Usage: /skin-setting opens the settings panel (TUI only); /skin-setting default restores defaults. Config file: ${deps.configPath}`, "info");
|
|
341
341
|
return;
|
|
342
342
|
}
|
|
343
343
|
await ctx.ui.custom(
|