@yinan_chen/pi-claude-code-ui 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -56
- package/README.zh-CN.md +123 -0
- package/package.json +3 -2
- package/src/index.ts +9 -9
- 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
|
+
- **Full third-party renderer takeover** — cards from any extension (including future unmatched ones) render automatically in a unified `● card` style, covering tools / messages / entries at all three layers, zero config and zero adaptation; dispatch-layer interception is 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 for unmatched customTypes: generic = take over 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
|
+
- **第三方渲染全接管** — 任何扩展的卡片(含未来未适配的)自动渲染为统一的 `● 卡片` 风格,工具/消息/条目三层全覆盖,零配置零适配;分发层拦截不受加载顺序影响(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.1",
|
|
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": [
|
package/src/index.ts
CHANGED
|
@@ -571,7 +571,7 @@ export default async function leanTidy(pi: ExtensionAPI): Promise<void> {
|
|
|
571
571
|
* 冷键在加载期固化(视图与面板注册),运行时 apply 无意义,直接跳过。 */
|
|
572
572
|
function applySetting(id: string, value: unknown): void {
|
|
573
573
|
const def = SETTING_BY_ID.get(id);
|
|
574
|
-
if (!def || def.
|
|
574
|
+
if (!def || def.reloadRequired) return;
|
|
575
575
|
if (id.startsWith("diff.")) {
|
|
576
576
|
diffDisplayConfig = { ...diffDisplayConfig, [id.slice("diff.".length)]: value };
|
|
577
577
|
return;
|
|
@@ -586,18 +586,18 @@ export default async function leanTidy(pi: ExtensionAPI): Promise<void> {
|
|
|
586
586
|
let confirmed = false;
|
|
587
587
|
try {
|
|
588
588
|
confirmed = await ctx?.ui?.confirm?.(
|
|
589
|
-
"
|
|
590
|
-
"
|
|
589
|
+
"Restore default settings",
|
|
590
|
+
"This will delete all known skin config keys from lean-tidy.json (unknown keys are kept); live settings revert immediately. Continue?",
|
|
591
591
|
);
|
|
592
592
|
} catch { confirmed = false; }
|
|
593
593
|
if (!confirmed) return; // Esc / 取消:文件与运行时零改动。
|
|
594
|
-
const {
|
|
594
|
+
const { reloadRequiredReset } = resetConfigFile(CONFIG_PATH);
|
|
595
595
|
// diff 显示配置先整体回出厂拷贝(文件 diff 里的未知键不留在运行时),
|
|
596
596
|
// 再走统一的热键 apply 通道逐键重置(幂等,值相同)。
|
|
597
597
|
diffDisplayConfig = { ...DEFAULT_TOOL_DISPLAY_CONFIG };
|
|
598
598
|
applyRuntimeDefaults(applySetting);
|
|
599
599
|
forceRedraw();
|
|
600
|
-
if (
|
|
600
|
+
if (reloadRequiredReset.length > 0) ctx?.ui?.notify?.(`Defaults restored — ${reloadRequiredReset.length} setting(s) need /reload`, "info");
|
|
601
601
|
}
|
|
602
602
|
|
|
603
603
|
/** 设置面板(工单 03):当前值读取统一走 currentValueOf——顶层键读共享 config,diff.* 读 diffDisplayConfig(闭包每次读模块变量,apply 整体替换后仍取最新值)。 */
|
|
@@ -613,9 +613,9 @@ export default async function leanTidy(pi: ExtensionAPI): Promise<void> {
|
|
|
613
613
|
// /skin-setting:无参 = 打开设置面板(仅 TUI;守卫与非 TUI 提示在面板模块内),
|
|
614
614
|
// default = 确认后一键恢复默认。命令补全只提示 default。
|
|
615
615
|
pi.registerCommand("skin-setting", {
|
|
616
|
-
description: "lean-tidy
|
|
616
|
+
description: "lean-tidy: skin settings (default = reset to defaults)",
|
|
617
617
|
getArgumentCompletions(prefix: string) {
|
|
618
|
-
const items = [{ value: "default", label: "default", description: "
|
|
618
|
+
const items = [{ value: "default", label: "default", description: "Reset all settings to defaults (unknown keys are kept)" }];
|
|
619
619
|
return items.filter((item) => item.value.startsWith(prefix));
|
|
620
620
|
},
|
|
621
621
|
handler: async (args: string, ctx: any) => {
|
|
@@ -630,14 +630,14 @@ export default async function leanTidy(pi: ExtensionAPI): Promise<void> {
|
|
|
630
630
|
return;
|
|
631
631
|
}
|
|
632
632
|
ctx?.ui?.notify?.(
|
|
633
|
-
|
|
633
|
+
`Unknown argument "${arg}". Usage: /skin-setting opens the settings panel (TUI only); /skin-setting default restores defaults. Config file: ${CONFIG_PATH}`,
|
|
634
634
|
"info",
|
|
635
635
|
);
|
|
636
636
|
},
|
|
637
637
|
});
|
|
638
638
|
|
|
639
639
|
pi.registerShortcut("f5" as any, {
|
|
640
|
-
description: "lean-tidy:
|
|
640
|
+
description: "lean-tidy: force full redraw",
|
|
641
641
|
handler: () => { forceRedraw(); },
|
|
642
642
|
});
|
|
643
643
|
|
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 for unmatched customTypes: 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(
|