pi-cc-extensions 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.en.md +29 -22
- package/README.md +27 -20
- package/extensions/config/config.ts +16 -0
- package/extensions/config/panel.ts +232 -48
- package/extensions/feature/shell/footer-layout.ts +229 -0
- package/extensions/feature/shell/footer.ts +100 -50
- package/extensions/feature/shell/startup-header.ts +8 -8
- package/package.json +2 -2
package/README.en.md
CHANGED
|
@@ -37,13 +37,13 @@ Run `/reload` after installation.
|
|
|
37
37
|
|
|
38
38
|
| Feature | Description | Entry point |
|
|
39
39
|
| --------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------- |
|
|
40
|
-
| Claude Code
|
|
41
|
-
| Markdown enhancements
|
|
40
|
+
| Claude Code UI | Tool summaries, expand/collapse, rich edit/write diffs, and `on` / `compact` / `off` modes | `/ccstyle` |
|
|
41
|
+
| Markdown enhancements | Mermaid diagrams, admonitions, URL linking, and more | Automatic |
|
|
42
42
|
| Fullscreen mode | Tool card/group click to expand, double-click to collapse, previews, hover highlight, and a back-to-bottom button | `TUIMODE=fullscreen` or `--tui-mode fullscreen` |
|
|
43
|
-
| Settings panel | `Style /
|
|
43
|
+
| Settings panel | `Style / Features / UI / Diff / Thinking / Footer` tabs | `/ccstyle` |
|
|
44
44
|
| Context inspection | Usage breakdown and previews for the system prompt, memory, skills, tools definition, and messages | `/context` |
|
|
45
45
|
| Session/Subagent references | Search and inject effective context from previous Sessions or existing SubAgents | `@` |
|
|
46
|
-
| Status bar |
|
|
46
|
+
| Status bar | Shows model, context, cache, cost, and git; also works with `@narumitw/pi-usage` for live quota. Git/cache Nerd Font icons can be turned off | `/ccstyle` |
|
|
47
47
|
| Theme | Bundled CC Dark and CC Light themes | `/theme` |
|
|
48
48
|
|
|
49
49
|
## Configuration
|
|
@@ -52,9 +52,26 @@ Run `/reload` after installation.
|
|
|
52
52
|
|
|
53
53
|
```js
|
|
54
54
|
{
|
|
55
|
+
// style
|
|
55
56
|
"mode": "on", // on / compact / off
|
|
56
57
|
"excludeRenderers": [], // tools keeping the native renderer; Agent always keeps its dedicated renderer
|
|
57
58
|
|
|
59
|
+
// features
|
|
60
|
+
"enableSessionReference": true, // @ session references
|
|
61
|
+
"enableSubagentAutocomplete": true, // @ subagent completion and delegation hints
|
|
62
|
+
"enableContextCommand": true, // /context usage check
|
|
63
|
+
"enableAgentSummary": true, // per-turn tool summary
|
|
64
|
+
"enableWorkingMessage": true, // Working... bottom token/elapsed
|
|
65
|
+
"enableAliases": true, // /clear, /exit aliases
|
|
66
|
+
|
|
67
|
+
// ui
|
|
68
|
+
"expandedInputMaxLines": 5, // expanded tool Input lines; overflow shows a footer hint
|
|
69
|
+
"expandedOutputMaxLines": 10, // expanded tool Output lines; overflow shows a footer hint
|
|
70
|
+
"expandedPreviewMaxLines": 40, // max lines for expanded diff/TaskList bodies
|
|
71
|
+
"inputClip": 100, // tool summary path/command clip length
|
|
72
|
+
"showStartupHeader": true, // startup header (logo + tips) toggle
|
|
73
|
+
"scrollStepLines": 3, // fullscreen wheel scroll step
|
|
74
|
+
|
|
58
75
|
// diff
|
|
59
76
|
"diffViewMode": "auto", // layout: auto / split / unified
|
|
60
77
|
"diffIndicatorMode": "bars", // change indicators: bars / classic / none
|
|
@@ -69,22 +86,13 @@ Run `/reload` after installation.
|
|
|
69
86
|
"animationIntervalMs": 90, // title animation interval (ms)
|
|
70
87
|
"dimThinkingText": false, // dim thinking body text
|
|
71
88
|
|
|
72
|
-
//
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
|
|
80
|
-
// features
|
|
81
|
-
"enableSessionReference": true, // @ session references
|
|
82
|
-
"enableSubagentAutocomplete": true, // @ subagent completion and delegation hints
|
|
83
|
-
"enableContextCommand": true, // /context usage check
|
|
84
|
-
"enableAgentSummary": true, // per-turn tool summary
|
|
85
|
-
"enableWorkingMessage": true, // Working... bottom token/elapsed
|
|
86
|
-
"enableAliases": true, // /clear, /exit aliases
|
|
87
|
-
"enableCustomFooter": true // two-line chips status bar (needs a Nerd Font)
|
|
89
|
+
// footer
|
|
90
|
+
"enableCustomFooter": true, // custom status bar
|
|
91
|
+
"footerNerdIcons": true, // Nerd Font glyphs for git/cache; false = plain text
|
|
92
|
+
"footerHiddenKeys": [], // hidden plugin chip keys
|
|
93
|
+
"footerLine1Keys": ["pi-usage"], // line1 plugin chip order; pi-usage is shown by default, data from @narumitw/pi-usage
|
|
94
|
+
"footerLine2Keys": [], // line2 plugin chip order (after cwd/git)
|
|
95
|
+
"footerLine3Keys": [] // line3 overflow slot; painted only when a chip is visible
|
|
88
96
|
}
|
|
89
97
|
```
|
|
90
98
|
|
|
@@ -102,7 +110,7 @@ npm run typecheck
|
|
|
102
110
|
|
|
103
111
|
## Compatibility
|
|
104
112
|
|
|
105
|
-
- Node.js `>=22.19.0`, Pi `^0.84.0`
|
|
113
|
+
- Node.js `>=22.19.0`, Pi `^0.84.0`
|
|
106
114
|
|
|
107
115
|
## Recommended companions
|
|
108
116
|
|
|
@@ -114,7 +122,6 @@ npm run typecheck
|
|
|
114
122
|
| `npm:@ff-labs/pi-fff` | FFF-powered fuzzy file and content search (fffind / ffgrep) |
|
|
115
123
|
| `npm:pi-web-access` | Web search, URL fetching, GitHub cloning, PDF/video parsing |
|
|
116
124
|
| `npm:@narumitw/pi-usage` | Current-account usage for Codex / Copilot / OpenRouter |
|
|
117
|
-
| `git:github.com/DietrichGebert/ponytail` | Lazy-mode coding: forces the simplest working solution |
|
|
118
125
|
|
|
119
126
|
## Credits
|
|
120
127
|
|
package/README.md
CHANGED
|
@@ -40,10 +40,10 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
40
40
|
| Claude Code UI | 工具摘要、折叠展开、rich edit/write diff,以及`on` / `compact` / `off` 三种模式 | `/ccstyle` |
|
|
41
41
|
| Markdown 增强 | Mermaid 图、提示框、URL 链接化等 | 自动生效 |
|
|
42
42
|
| Fullscreen mode | 工具卡/group 单击展开、双击收起、预览、hover 高亮、回到底部按钮 | `TUIMODE=fullscreen` 或 `--tui-mode fullscreen` |
|
|
43
|
-
| 配置面板 | `Style /
|
|
43
|
+
| 配置面板 | `Style / Features / UI / Diff / Thinking / Footer` 六页签 | `/ccstyle` |
|
|
44
44
|
| 上下文检查 | 查看上下文占用,并预览 System prompt、Memory、Skills、Tools definition 和消息内容 | `/context` |
|
|
45
45
|
| Session/Subagent 引用 | 搜索并注入历史 Session 或现有 SubAgent 的有效上下文 | `@` |
|
|
46
|
-
| 状态栏 |
|
|
46
|
+
| 状态栏 | 显示:模型、上下文、缓存、费用、git并适配 @narumitw/pi-usage 实时显示额度;git/缓存图标可关 Nerd Font | `/ccstyle` |
|
|
47
47
|
| 主题 | 随包提供内置 CC Dark、CC Light 主题 | `/theme` |
|
|
48
48
|
|
|
49
49
|
## 配置
|
|
@@ -52,9 +52,26 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
52
52
|
|
|
53
53
|
```js
|
|
54
54
|
{
|
|
55
|
+
// style
|
|
55
56
|
"mode": "on", // on / compact / off
|
|
56
57
|
"excludeRenderers": [], // 走原生渲染的工具名;Agent 始终保留专用渲染器
|
|
57
58
|
|
|
59
|
+
// features
|
|
60
|
+
"enableSessionReference": true, // @ session 引用
|
|
61
|
+
"enableSubagentAutocomplete": true, // @ subagent 补全与委派提示
|
|
62
|
+
"enableContextCommand": true, // /context 上下文检查
|
|
63
|
+
"enableAgentSummary": true, // 每回合工具摘要
|
|
64
|
+
"enableWorkingMessage": true, // Working... 底部 token/耗时
|
|
65
|
+
"enableAliases": true, // /clear、/exit 别名
|
|
66
|
+
|
|
67
|
+
// ui
|
|
68
|
+
"expandedInputMaxLines": 5, // 展开工具卡 Input 可见行数,超出在末行显示展开提示
|
|
69
|
+
"expandedOutputMaxLines": 10, // 展开工具卡 Output 可见行数,超出在末行显示展开提示
|
|
70
|
+
"expandedPreviewMaxLines": 40, // 展开 diff/TaskList 正文最大行数
|
|
71
|
+
"inputClip": 100, // 工具摘要 path/command 折叠字符数
|
|
72
|
+
"showStartupHeader": true, // 启动头(logo + tips)开关
|
|
73
|
+
"scrollStepLines": 3, // fullscreen 滚轮步进
|
|
74
|
+
|
|
58
75
|
// diff
|
|
59
76
|
"diffViewMode": "auto", // 布局:auto / split / unified
|
|
60
77
|
"diffIndicatorMode": "bars", // 变更指示:bars / classic / none
|
|
@@ -69,26 +86,17 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
69
86
|
"animationIntervalMs": 90, // 标题动画间隔(毫秒)
|
|
70
87
|
"dimThinkingText": false, // thinking 正文用 dim 色
|
|
71
88
|
|
|
72
|
-
//
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
|
|
80
|
-
// features
|
|
81
|
-
"enableSessionReference": true, // @ session 引用
|
|
82
|
-
"enableSubagentAutocomplete": true, // @ subagent 补全与委派提示
|
|
83
|
-
"enableContextCommand": true, // /context 上下文检查
|
|
84
|
-
"enableAgentSummary": true, // 每回合工具摘要
|
|
85
|
-
"enableWorkingMessage": true, // Working... 底部 token/耗时
|
|
86
|
-
"enableAliases": true, // /clear、/exit 别名
|
|
87
|
-
"enableCustomFooter": true // 两行 chips 状态栏(需 Nerd Font)
|
|
89
|
+
// footer
|
|
90
|
+
"enableCustomFooter": true, // 自定义状态栏
|
|
91
|
+
"footerNerdIcons": true, // git/缓存用 Nerd Font 图标;false 为纯文本
|
|
92
|
+
"footerHiddenKeys": [], // 隐藏的插件芯片 key
|
|
93
|
+
"footerLine1Keys": ["pi-usage"], // line1 插件芯片顺序;pi-usage 默认显示,数据来自 @narumitw/pi-usage
|
|
94
|
+
"footerLine2Keys": [], // line2 插件芯片顺序(接在 cwd/git 后)
|
|
95
|
+
"footerLine3Keys": [] // line3 备用槽,有可见芯片才占行
|
|
88
96
|
}
|
|
89
97
|
```
|
|
90
98
|
|
|
91
|
-
> **Fullscreen**:单击 `click to show more` 展开工具卡、思考、Skill 和 compact 摘要;展开后 Input/Output 超行时,末行 `… +N more lines • click to show more`
|
|
99
|
+
> **Fullscreen**:单击 `click to show more` 展开工具卡、思考、Skill 和 compact 摘要;展开后 Input/Output 超行时,末行 `… +N more lines • click to show more` 打开全量预览, 双击展开面板收起。
|
|
92
100
|
>
|
|
93
101
|
> **建议**:`markdown.mermaid` 设为 `final`(`~/.pi/agent/settings.json` 或 `/settings` 面板的 Mermaid diagrams 选项)。默认 `streaming` 逐帧重绘,`final` 渲染最终版更稳定。
|
|
94
102
|
|
|
@@ -114,7 +122,6 @@ npm run typecheck
|
|
|
114
122
|
| `npm:@ff-labs/pi-fff` | 模糊文件与内容检索(fffind / ffgrep) |
|
|
115
123
|
| `npm:pi-web-access` | 网页搜索、URL 抓取、GitHub 克隆、PDF/视频解析 |
|
|
116
124
|
| `npm:@narumitw/pi-usage` | 查看当前账号用量(Codex / Copilot / OpenRouter) |
|
|
117
|
-
| `git:github.com/DietrichGebert/ponytail` | 极简编码:强制最懒但有效的方案 |
|
|
118
125
|
|
|
119
126
|
## 致谢
|
|
120
127
|
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { CompactThinkingConfig } from "../feature/compact-thinking.ts";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_FOOTER_CHIP_LAYOUT,
|
|
4
|
+
formatFooterChipSummary,
|
|
5
|
+
normalizeFooterChipLayout,
|
|
6
|
+
} from "../feature/shell/footer-layout.ts";
|
|
2
7
|
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
8
|
import { homedir } from "node:os";
|
|
4
9
|
import { join } from "node:path";
|
|
@@ -65,6 +70,11 @@ export type Config = {
|
|
|
65
70
|
enableWorkingMessage: boolean;
|
|
66
71
|
enableAliases: boolean;
|
|
67
72
|
enableCustomFooter: boolean;
|
|
73
|
+
footerNerdIcons: boolean;
|
|
74
|
+
footerHiddenKeys: string[];
|
|
75
|
+
footerLine1Keys: string[];
|
|
76
|
+
footerLine2Keys: string[];
|
|
77
|
+
footerLine3Keys: string[];
|
|
68
78
|
};
|
|
69
79
|
|
|
70
80
|
const AGENT_DIR = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
|
@@ -128,6 +138,8 @@ export const DEFAULT_CONFIG: Config = {
|
|
|
128
138
|
enableWorkingMessage: true,
|
|
129
139
|
enableAliases: true,
|
|
130
140
|
enableCustomFooter: true,
|
|
141
|
+
footerNerdIcons: true,
|
|
142
|
+
...DEFAULT_FOOTER_CHIP_LAYOUT,
|
|
131
143
|
};
|
|
132
144
|
|
|
133
145
|
function pickEnum<T extends string>(value: unknown, allowed: readonly T[], fallback: T): T {
|
|
@@ -227,6 +239,8 @@ export function normalizeConfig(input: unknown): Config {
|
|
|
227
239
|
enableWorkingMessage: source.enableWorkingMessage !== false,
|
|
228
240
|
enableAliases: source.enableAliases !== false,
|
|
229
241
|
enableCustomFooter: source.enableCustomFooter !== false,
|
|
242
|
+
footerNerdIcons: source.footerNerdIcons !== false,
|
|
243
|
+
...normalizeFooterChipLayout(source),
|
|
230
244
|
};
|
|
231
245
|
}
|
|
232
246
|
|
|
@@ -281,6 +295,8 @@ export function formatConfigStatus(source: Config = config): string {
|
|
|
281
295
|
`workingMsg=${source.enableWorkingMessage ? "on" : "off"}`,
|
|
282
296
|
`aliases=${source.enableAliases ? "on" : "off"}`,
|
|
283
297
|
`footer=${source.enableCustomFooter ? "on" : "off"}`,
|
|
298
|
+
`footerIcons=${source.footerNerdIcons ? "nerd" : "plain"}`,
|
|
299
|
+
formatFooterChipSummary(source),
|
|
284
300
|
].join(" · ");
|
|
285
301
|
}
|
|
286
302
|
|
|
@@ -7,8 +7,23 @@
|
|
|
7
7
|
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { Input, SettingsList, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
9
9
|
import type { CompactThinkingController } from "../feature/compact-thinking.ts";
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import {
|
|
11
|
+
applyCustomFooter,
|
|
12
|
+
clearCustomFooter,
|
|
13
|
+
getFooterStatusSnapshot,
|
|
14
|
+
} from "../feature/shell/footer.ts";
|
|
15
|
+
import {
|
|
16
|
+
footerChipDescription,
|
|
17
|
+
footerLineOfKey,
|
|
18
|
+
formatFooterChipSummary,
|
|
19
|
+
orderedFooterKeys,
|
|
20
|
+
reorderFooterKey,
|
|
21
|
+
resolveFooterChipLayout,
|
|
22
|
+
shiftFooterKeyLine,
|
|
23
|
+
toggleFooterKeyHidden,
|
|
24
|
+
type FooterChipLayout,
|
|
25
|
+
} from "../feature/shell/footer-layout.ts";
|
|
26
|
+
import { applyStartupHeader, clearStartupHeader } from "../feature/shell/startup-header.ts";
|
|
12
27
|
import type { ToolGroupingHooks } from "../renderer/tool/grouping.ts";
|
|
13
28
|
import {
|
|
14
29
|
config,
|
|
@@ -59,6 +74,120 @@ function excludeRenderersDescription(names: readonly string[]): string {
|
|
|
59
74
|
: `Native renderer for: ${names.join(", ")}. Agent is always native. Enter to toggle.`;
|
|
60
75
|
}
|
|
61
76
|
|
|
77
|
+
function customFooterDescription(enabled: boolean): string {
|
|
78
|
+
if (!enabled) return "Pi native footer restored. Chip layout below still applies when turned on.";
|
|
79
|
+
return "Custom status bar with model, context, cache, cost, git, and plugin chips.";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function pluginChipsDescription(): string {
|
|
83
|
+
return `Arrange plugin chips: ${formatFooterChipSummary(config)}. Enter to edit.`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function footerLayoutFromConfig(): FooterChipLayout {
|
|
87
|
+
return {
|
|
88
|
+
footerHiddenKeys: config.footerHiddenKeys,
|
|
89
|
+
footerLine1Keys: config.footerLine1Keys,
|
|
90
|
+
footerLine2Keys: config.footerLine2Keys,
|
|
91
|
+
footerLine3Keys: config.footerLine3Keys,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function persistFooterLayout(layout: FooterChipLayout): void {
|
|
96
|
+
updateConfig({
|
|
97
|
+
footerHiddenKeys: layout.footerHiddenKeys,
|
|
98
|
+
footerLine1Keys: layout.footerLine1Keys,
|
|
99
|
+
footerLine2Keys: layout.footerLine2Keys,
|
|
100
|
+
footerLine3Keys: layout.footerLine3Keys,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function footerChipItem(
|
|
105
|
+
key: string,
|
|
106
|
+
layout: FooterChipLayout,
|
|
107
|
+
snapshot: ReadonlyMap<string, string>,
|
|
108
|
+
) {
|
|
109
|
+
const line = footerLineOfKey(layout, key) ?? 2;
|
|
110
|
+
const hidden = layout.footerHiddenKeys.includes(key);
|
|
111
|
+
const live = snapshot.get(key)?.trim() ?? "";
|
|
112
|
+
return {
|
|
113
|
+
id: key,
|
|
114
|
+
label: key,
|
|
115
|
+
description: footerChipDescription(key, live),
|
|
116
|
+
currentValue: hidden ? `line${line} · hidden` : `line${line}`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function buildFooterChipsSubmenu(
|
|
121
|
+
onClose: () => void,
|
|
122
|
+
onLiveChange: () => void,
|
|
123
|
+
): {
|
|
124
|
+
render: (width: number) => string[];
|
|
125
|
+
invalidate: () => void;
|
|
126
|
+
handleInput: (data: string) => void;
|
|
127
|
+
} {
|
|
128
|
+
const snapshot = () => getFooterStatusSnapshot();
|
|
129
|
+
const resolved = () => resolveFooterChipLayout(footerLayoutFromConfig(), [...snapshot().keys()]);
|
|
130
|
+
const items: ReturnType<typeof footerChipItem>[] = [];
|
|
131
|
+
const rebuildItems = (layout: FooterChipLayout, selectedKey?: string) => {
|
|
132
|
+
const next = orderedFooterKeys(layout).map((key) => footerChipItem(key, layout, snapshot()));
|
|
133
|
+
items.length = 0;
|
|
134
|
+
items.push(...next);
|
|
135
|
+
if (!selectedKey) return 0;
|
|
136
|
+
const index = items.findIndex((item) => item.id === selectedKey);
|
|
137
|
+
return index >= 0 ? index : 0;
|
|
138
|
+
};
|
|
139
|
+
rebuildItems(resolved());
|
|
140
|
+
const list = new SettingsList(
|
|
141
|
+
items,
|
|
142
|
+
Math.min(8, Math.max(4, items.length)),
|
|
143
|
+
getSettingsListTheme(),
|
|
144
|
+
() => {},
|
|
145
|
+
() => onClose(),
|
|
146
|
+
{ enableSearch: false },
|
|
147
|
+
);
|
|
148
|
+
const listState = list as unknown as { selectedIndex: number; items: typeof items };
|
|
149
|
+
|
|
150
|
+
const mutate = (transform: (layout: FooterChipLayout, key: string) => FooterChipLayout) => {
|
|
151
|
+
const key = listState.items[listState.selectedIndex]?.id;
|
|
152
|
+
if (!key) return;
|
|
153
|
+
const next = transform(resolved(), key);
|
|
154
|
+
persistFooterLayout(next);
|
|
155
|
+
listState.selectedIndex = rebuildItems(
|
|
156
|
+
resolveFooterChipLayout(next, [...snapshot().keys()]),
|
|
157
|
+
key,
|
|
158
|
+
);
|
|
159
|
+
onLiveChange();
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
render: (width: number) => list.render(width),
|
|
164
|
+
invalidate: () => list.invalidate(),
|
|
165
|
+
handleInput: (data: string) => {
|
|
166
|
+
if (matchesKey(data, "left")) {
|
|
167
|
+
mutate((layout, key) => shiftFooterKeyLine(layout, key, -1));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (matchesKey(data, "right")) {
|
|
171
|
+
mutate((layout, key) => shiftFooterKeyLine(layout, key, 1));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (data === "[") {
|
|
175
|
+
mutate((layout, key) => reorderFooterKey(layout, key, -1));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (data === "]") {
|
|
179
|
+
mutate((layout, key) => reorderFooterKey(layout, key, 1));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (data === " " || matchesKey(data, "space")) {
|
|
183
|
+
mutate((layout, key) => toggleFooterKeyHidden(layout, key));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
list.handleInput(data);
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
62
191
|
function diffViewModeDescription(mode: DiffViewMode): string {
|
|
63
192
|
if (mode === "split") return "Force side-by-side diff when width allows; otherwise unified.";
|
|
64
193
|
if (mode === "unified") return "Always render a single unified diff column.";
|
|
@@ -186,7 +315,7 @@ function buildNumberInputSubmenu(
|
|
|
186
315
|
|
|
187
316
|
/** Section tabs for /ccstyle — matches Zentui-style "A / B / C" headers. */
|
|
188
317
|
type CcstyleSection = {
|
|
189
|
-
id: "style" | "diff" | "thinking" | "ui" | "feature";
|
|
318
|
+
id: "style" | "diff" | "thinking" | "ui" | "feature" | "footer";
|
|
190
319
|
label: string;
|
|
191
320
|
items: any[];
|
|
192
321
|
};
|
|
@@ -242,19 +371,22 @@ export async function showCcstylePanel(
|
|
|
242
371
|
currentValue: config.mode === "compact" ? "compact (Experimental)" : config.mode,
|
|
243
372
|
values: ["on", "compact (Experimental)", "off"],
|
|
244
373
|
};
|
|
245
|
-
// Tracks whether
|
|
374
|
+
// Tracks whether a nested submenu is open so Tab switches sections
|
|
246
375
|
// only at the top level (mirrors Zentui settings: Tab = switch sections).
|
|
247
|
-
let
|
|
376
|
+
let nestedSubmenuOpen = false;
|
|
377
|
+
let nestedHint = "";
|
|
248
378
|
const excludeSetting = {
|
|
249
379
|
id: "excludeRenderers",
|
|
250
380
|
label: "Exclude tools",
|
|
251
381
|
description: excludeRenderersDescription(config.excludeRenderers),
|
|
252
382
|
currentValue: formatExcludeRenderers(config.excludeRenderers),
|
|
253
383
|
submenu: (_current: string, closeSubmenu: (selected?: string) => void) => {
|
|
254
|
-
|
|
384
|
+
nestedSubmenuOpen = true;
|
|
385
|
+
nestedHint = " Enter/Space to toggle · Esc back to Style";
|
|
255
386
|
return buildExcludeRenderersSubmenu(
|
|
256
387
|
() => {
|
|
257
|
-
|
|
388
|
+
nestedSubmenuOpen = false;
|
|
389
|
+
nestedHint = "";
|
|
258
390
|
excludeSetting.currentValue = formatExcludeRenderers(config.excludeRenderers);
|
|
259
391
|
excludeSetting.description = excludeRenderersDescription(config.excludeRenderers);
|
|
260
392
|
closeSubmenu();
|
|
@@ -456,13 +588,45 @@ export async function showCcstylePanel(
|
|
|
456
588
|
"Aliases disabled.",
|
|
457
589
|
config.enableAliases,
|
|
458
590
|
);
|
|
459
|
-
const
|
|
460
|
-
"
|
|
461
|
-
"
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
591
|
+
const footerNerdIconsSetting = {
|
|
592
|
+
id: "footerNerdIcons",
|
|
593
|
+
label: "Nerd Font icons",
|
|
594
|
+
description: config.footerNerdIcons
|
|
595
|
+
? "Git and cache chips use Nerd Font glyphs. Turn off for plain text."
|
|
596
|
+
: "Git and cache chips use plain text. No Nerd Font required.",
|
|
597
|
+
currentValue: config.footerNerdIcons ? "on" : "off",
|
|
598
|
+
values: ["on", "off"],
|
|
599
|
+
};
|
|
600
|
+
const customFooterSetting = {
|
|
601
|
+
id: "enableCustomFooter",
|
|
602
|
+
label: "Status bar",
|
|
603
|
+
description: customFooterDescription(config.enableCustomFooter),
|
|
604
|
+
currentValue: config.enableCustomFooter ? "on" : "off",
|
|
605
|
+
values: ["on", "off"],
|
|
606
|
+
};
|
|
607
|
+
const pluginChipsSetting = {
|
|
608
|
+
id: "footerPluginChips",
|
|
609
|
+
label: "Plugin chips",
|
|
610
|
+
description: pluginChipsDescription(),
|
|
611
|
+
currentValue: formatFooterChipSummary(config),
|
|
612
|
+
submenu: (_current: string, closeSubmenu: (selected?: string) => void) => {
|
|
613
|
+
nestedSubmenuOpen = true;
|
|
614
|
+
nestedHint = " ←→ line · [ ] reorder · Space hide · Esc back to Footer";
|
|
615
|
+
return buildFooterChipsSubmenu(
|
|
616
|
+
() => {
|
|
617
|
+
nestedSubmenuOpen = false;
|
|
618
|
+
nestedHint = "";
|
|
619
|
+
pluginChipsSetting.currentValue = formatFooterChipSummary(config);
|
|
620
|
+
pluginChipsSetting.description = pluginChipsDescription();
|
|
621
|
+
closeSubmenu();
|
|
622
|
+
},
|
|
623
|
+
() => {
|
|
624
|
+
pluginChipsSetting.currentValue = formatFooterChipSummary(config);
|
|
625
|
+
pluginChipsSetting.description = pluginChipsDescription();
|
|
626
|
+
},
|
|
627
|
+
);
|
|
628
|
+
},
|
|
629
|
+
};
|
|
466
630
|
const featureToggles: Record<string, { apply: (on: boolean) => void }> = {
|
|
467
631
|
enableSessionReference: sessionReferenceToggle,
|
|
468
632
|
enableSubagentAutocomplete: subagentAutocompleteToggle,
|
|
@@ -474,9 +638,22 @@ export async function showCcstylePanel(
|
|
|
474
638
|
|
|
475
639
|
const onSettingChange = (id: string, value: string) => {
|
|
476
640
|
if (id === "enableCustomFooter") {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
641
|
+
const enabled = value === "on";
|
|
642
|
+
updateConfig({ enableCustomFooter: enabled });
|
|
643
|
+
customFooterSetting.currentValue = enabled ? "on" : "off";
|
|
644
|
+
customFooterSetting.description = customFooterDescription(enabled);
|
|
645
|
+
if (enabled) applyCustomFooter(ctx);
|
|
646
|
+
else clearCustomFooter(ctx);
|
|
647
|
+
ctx.ui.notify(`Updated ${id}: ${value}`, "info");
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (id === "footerNerdIcons") {
|
|
651
|
+
const enabled = value === "on";
|
|
652
|
+
updateConfig({ footerNerdIcons: enabled });
|
|
653
|
+
footerNerdIconsSetting.currentValue = enabled ? "on" : "off";
|
|
654
|
+
footerNerdIconsSetting.description = enabled
|
|
655
|
+
? "Git and cache chips use Nerd Font glyphs. Turn off for plain text."
|
|
656
|
+
: "Git and cache chips use plain text. No Nerd Font required.";
|
|
480
657
|
ctx.ui.notify(`Updated ${id}: ${value}`, "info");
|
|
481
658
|
return;
|
|
482
659
|
}
|
|
@@ -601,14 +778,16 @@ export async function showCcstylePanel(
|
|
|
601
778
|
? "Thinking text uses the theme's dim color."
|
|
602
779
|
: "Keep the default thinking text color.";
|
|
603
780
|
break;
|
|
604
|
-
case "showStartupHeader":
|
|
605
|
-
|
|
781
|
+
case "showStartupHeader": {
|
|
782
|
+
const enabled = value === "on";
|
|
783
|
+
updateConfig({ showStartupHeader: enabled });
|
|
606
784
|
startupHeaderSetting.description = config.showStartupHeader
|
|
607
785
|
? "Show the custom startup header (logo + tips) on new sessions."
|
|
608
786
|
: "Use Pi's native startup header instead.";
|
|
609
|
-
|
|
610
|
-
|
|
787
|
+
if (enabled) applyStartupHeader(ctx);
|
|
788
|
+
else clearStartupHeader(ctx);
|
|
611
789
|
break;
|
|
790
|
+
}
|
|
612
791
|
case "scrollStepLines":
|
|
613
792
|
updateConfig({
|
|
614
793
|
scrollStepLines: pickPositiveInt(value, DEFAULT_CONFIG.scrollStepLines, 1, 50),
|
|
@@ -629,6 +808,30 @@ export async function showCcstylePanel(
|
|
|
629
808
|
label: "Style",
|
|
630
809
|
items: [modeSetting, excludeSetting],
|
|
631
810
|
},
|
|
811
|
+
{
|
|
812
|
+
id: "feature",
|
|
813
|
+
label: "Features",
|
|
814
|
+
items: [
|
|
815
|
+
sessionReferenceToggle.setting,
|
|
816
|
+
subagentAutocompleteToggle.setting,
|
|
817
|
+
contextCommandToggle.setting,
|
|
818
|
+
agentSummaryToggle.setting,
|
|
819
|
+
workingMessageToggle.setting,
|
|
820
|
+
aliasesToggle.setting,
|
|
821
|
+
],
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
id: "ui",
|
|
825
|
+
label: "UI",
|
|
826
|
+
items: [
|
|
827
|
+
expandedInputSetting,
|
|
828
|
+
expandedOutputSetting,
|
|
829
|
+
expandedMaxSetting,
|
|
830
|
+
inputClipSetting,
|
|
831
|
+
startupHeaderSetting,
|
|
832
|
+
scrollStepSetting,
|
|
833
|
+
],
|
|
834
|
+
},
|
|
632
835
|
{
|
|
633
836
|
id: "diff",
|
|
634
837
|
label: "Diff",
|
|
@@ -652,29 +855,9 @@ export async function showCcstylePanel(
|
|
|
652
855
|
],
|
|
653
856
|
},
|
|
654
857
|
{
|
|
655
|
-
id: "
|
|
656
|
-
label: "
|
|
657
|
-
items: [
|
|
658
|
-
expandedInputSetting,
|
|
659
|
-
expandedOutputSetting,
|
|
660
|
-
expandedMaxSetting,
|
|
661
|
-
inputClipSetting,
|
|
662
|
-
startupHeaderSetting,
|
|
663
|
-
scrollStepSetting,
|
|
664
|
-
],
|
|
665
|
-
},
|
|
666
|
-
{
|
|
667
|
-
id: "feature",
|
|
668
|
-
label: "Feature",
|
|
669
|
-
items: [
|
|
670
|
-
sessionReferenceToggle.setting,
|
|
671
|
-
subagentAutocompleteToggle.setting,
|
|
672
|
-
contextCommandToggle.setting,
|
|
673
|
-
agentSummaryToggle.setting,
|
|
674
|
-
workingMessageToggle.setting,
|
|
675
|
-
aliasesToggle.setting,
|
|
676
|
-
customFooterToggle.setting,
|
|
677
|
-
],
|
|
858
|
+
id: "footer",
|
|
859
|
+
label: "Footer",
|
|
860
|
+
items: [customFooterSetting, footerNerdIconsSetting, pluginChipsSetting],
|
|
678
861
|
},
|
|
679
862
|
];
|
|
680
863
|
|
|
@@ -695,7 +878,7 @@ export async function showCcstylePanel(
|
|
|
695
878
|
const activeList = () => lists[activeSection]!;
|
|
696
879
|
|
|
697
880
|
const switchSection = (delta: number) => {
|
|
698
|
-
if (
|
|
881
|
+
if (nestedSubmenuOpen) return;
|
|
699
882
|
activeSection = (activeSection + delta + sections.length) % sections.length;
|
|
700
883
|
};
|
|
701
884
|
|
|
@@ -745,7 +928,8 @@ export async function showCcstylePanel(
|
|
|
745
928
|
truncateToWidth(
|
|
746
929
|
theme.fg(
|
|
747
930
|
"dim",
|
|
748
|
-
|
|
931
|
+
nestedHint ||
|
|
932
|
+
" Enter/Space to change · Enter on numbers types a custom value · Tab/Shift+Tab to switch sections · Esc to close",
|
|
749
933
|
),
|
|
750
934
|
safeWidth,
|
|
751
935
|
),
|
|
@@ -756,12 +940,12 @@ export async function showCcstylePanel(
|
|
|
756
940
|
for (const list of lists) list.invalidate();
|
|
757
941
|
},
|
|
758
942
|
handleInput(data: string) {
|
|
759
|
-
if (!
|
|
943
|
+
if (!nestedSubmenuOpen && isForwardTabKey(data)) {
|
|
760
944
|
switchSection(1);
|
|
761
945
|
tui.requestRender();
|
|
762
946
|
return;
|
|
763
947
|
}
|
|
764
|
-
if (!
|
|
948
|
+
if (!nestedSubmenuOpen && isBackTabKey(data)) {
|
|
765
949
|
switchSection(-1);
|
|
766
950
|
tui.requestRender();
|
|
767
951
|
return;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 自定义 footer 插件芯片布局:行、顺序、隐藏。
|
|
3
|
+
* 纯函数,供 config 规范化、footer 渲染、配置面板共用。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const PI_USAGE_KEY = "pi-usage";
|
|
7
|
+
export const PI_USAGE_SOURCE = "@narumitw/pi-usage";
|
|
8
|
+
const LEGACY_PI_USAGE_KEYS = new Set(["localUsage", "piUsage"]);
|
|
9
|
+
export const SKIP_FOOTER_STATUS_KEY = "model";
|
|
10
|
+
|
|
11
|
+
export type FooterLineId = 1 | 2 | 3;
|
|
12
|
+
|
|
13
|
+
export type FooterChipLayout = {
|
|
14
|
+
footerHiddenKeys: string[];
|
|
15
|
+
footerLine1Keys: string[];
|
|
16
|
+
footerLine2Keys: string[];
|
|
17
|
+
footerLine3Keys: string[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_FOOTER_CHIP_LAYOUT: FooterChipLayout = {
|
|
21
|
+
footerHiddenKeys: [],
|
|
22
|
+
footerLine1Keys: [PI_USAGE_KEY],
|
|
23
|
+
footerLine2Keys: [],
|
|
24
|
+
footerLine3Keys: [],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function isSkippedFooterStatusKey(key: string): boolean {
|
|
28
|
+
return key === SKIP_FOOTER_STATUS_KEY;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 未单独配置时的默认行:usage 类和本包用量在 line1,其余 line2。不会默认进 line3。 */
|
|
32
|
+
export function defaultFooterLine(key: string): FooterLineId {
|
|
33
|
+
if (key === PI_USAGE_KEY) return 1;
|
|
34
|
+
if (/usage|quota|balance/i.test(key)) return 1;
|
|
35
|
+
return 2;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function normalizeFooterKeyList(value: unknown): string[] {
|
|
39
|
+
if (!Array.isArray(value)) return [];
|
|
40
|
+
const out: string[] = [];
|
|
41
|
+
const seen = new Set<string>();
|
|
42
|
+
for (const item of value) {
|
|
43
|
+
if (typeof item !== "string") continue;
|
|
44
|
+
let key = item.trim();
|
|
45
|
+
if (LEGACY_PI_USAGE_KEYS.has(key)) key = PI_USAGE_KEY;
|
|
46
|
+
if (!key || isSkippedFooterStatusKey(key) || seen.has(key)) continue;
|
|
47
|
+
seen.add(key);
|
|
48
|
+
out.push(key);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function takeUnique(keys: string[], seen: Set<string>): string[] {
|
|
54
|
+
const out: string[] = [];
|
|
55
|
+
for (const key of keys) {
|
|
56
|
+
if (seen.has(key)) continue;
|
|
57
|
+
seen.add(key);
|
|
58
|
+
out.push(key);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 跨行去重(line1 → line2 → line3);补上 pi-usage 槽位。 */
|
|
64
|
+
export function normalizeFooterChipLayout(source: Record<string, unknown>): FooterChipLayout {
|
|
65
|
+
const hasHidden = Array.isArray(source.footerHiddenKeys);
|
|
66
|
+
const hasAnyLine = [source.footerLine1Keys, source.footerLine2Keys, source.footerLine3Keys].some(
|
|
67
|
+
Array.isArray,
|
|
68
|
+
);
|
|
69
|
+
const seen = new Set<string>();
|
|
70
|
+
let footerLine1Keys = takeUnique(normalizeFooterKeyList(source.footerLine1Keys), seen);
|
|
71
|
+
const footerLine2Keys = takeUnique(normalizeFooterKeyList(source.footerLine2Keys), seen);
|
|
72
|
+
const footerLine3Keys = takeUnique(normalizeFooterKeyList(source.footerLine3Keys), seen);
|
|
73
|
+
const footerHiddenKeys = normalizeFooterKeyList(source.footerHiddenKeys);
|
|
74
|
+
|
|
75
|
+
if (!seen.has(PI_USAGE_KEY)) {
|
|
76
|
+
footerLine1Keys = [PI_USAGE_KEY, ...footerLine1Keys];
|
|
77
|
+
}
|
|
78
|
+
if (!hasHidden && !hasAnyLine) {
|
|
79
|
+
return { ...DEFAULT_FOOTER_CHIP_LAYOUT };
|
|
80
|
+
}
|
|
81
|
+
return { footerHiddenKeys, footerLine1Keys, footerLine2Keys, footerLine3Keys };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 面板描述用:pi-usage 数据来自 @narumitw/pi-usage,其它芯片用来源 setStatus key。 */
|
|
85
|
+
export function footerChipSource(key: string): string {
|
|
86
|
+
return key === PI_USAGE_KEY ? PI_USAGE_SOURCE : key;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function footerChipDescription(key: string, liveText: string): string {
|
|
90
|
+
const source = footerChipSource(key);
|
|
91
|
+
const live = liveText.trim();
|
|
92
|
+
if (live) return `${live} · from ${source}`;
|
|
93
|
+
if (key === PI_USAGE_KEY) return `From ${source}. No text right now.`;
|
|
94
|
+
return `inactive · from ${source}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function footerLineOfKey(layout: FooterChipLayout, key: string): FooterLineId | undefined {
|
|
98
|
+
if (layout.footerLine1Keys.includes(key)) return 1;
|
|
99
|
+
if (layout.footerLine2Keys.includes(key)) return 2;
|
|
100
|
+
if (layout.footerLine3Keys.includes(key)) return 3;
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function keysOnLine(layout: FooterChipLayout, line: FooterLineId): string[] {
|
|
105
|
+
if (line === 1) return layout.footerLine1Keys;
|
|
106
|
+
if (line === 2) return layout.footerLine2Keys;
|
|
107
|
+
return layout.footerLine3Keys;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function withLineKeys(
|
|
111
|
+
layout: FooterChipLayout,
|
|
112
|
+
line: FooterLineId,
|
|
113
|
+
keys: string[],
|
|
114
|
+
): FooterChipLayout {
|
|
115
|
+
if (line === 1) return { ...layout, footerLine1Keys: keys };
|
|
116
|
+
if (line === 2) return { ...layout, footerLine2Keys: keys };
|
|
117
|
+
return { ...layout, footerLine3Keys: keys };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 配置里还没有的 live key 接到默认行末尾(按 key 字母序,避免每次渲染乱序)。
|
|
122
|
+
* pi-usage 由 normalize 保证已在某一行。
|
|
123
|
+
*/
|
|
124
|
+
export function resolveFooterChipLayout(
|
|
125
|
+
layout: FooterChipLayout,
|
|
126
|
+
liveKeys: readonly string[],
|
|
127
|
+
): FooterChipLayout {
|
|
128
|
+
const known = new Set([
|
|
129
|
+
...layout.footerLine1Keys,
|
|
130
|
+
...layout.footerLine2Keys,
|
|
131
|
+
...layout.footerLine3Keys,
|
|
132
|
+
]);
|
|
133
|
+
const extras = [
|
|
134
|
+
...new Set(liveKeys.filter((key) => key && !isSkippedFooterStatusKey(key) && !known.has(key))),
|
|
135
|
+
].sort((a, b) => a.localeCompare(b));
|
|
136
|
+
const next: FooterChipLayout = {
|
|
137
|
+
footerHiddenKeys: [...layout.footerHiddenKeys],
|
|
138
|
+
footerLine1Keys: [...layout.footerLine1Keys],
|
|
139
|
+
footerLine2Keys: [...layout.footerLine2Keys],
|
|
140
|
+
footerLine3Keys: [...layout.footerLine3Keys],
|
|
141
|
+
};
|
|
142
|
+
for (const key of extras) {
|
|
143
|
+
if (defaultFooterLine(key) === 1) next.footerLine1Keys.push(key);
|
|
144
|
+
else next.footerLine2Keys.push(key);
|
|
145
|
+
}
|
|
146
|
+
return next;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function moveFooterKeyToLine(
|
|
150
|
+
layout: FooterChipLayout,
|
|
151
|
+
key: string,
|
|
152
|
+
line: FooterLineId,
|
|
153
|
+
): FooterChipLayout {
|
|
154
|
+
if (isSkippedFooterStatusKey(key)) return layout;
|
|
155
|
+
const current = footerLineOfKey(layout, key);
|
|
156
|
+
if (current === line) return layout;
|
|
157
|
+
const stripped: FooterChipLayout = {
|
|
158
|
+
footerHiddenKeys: [...layout.footerHiddenKeys],
|
|
159
|
+
footerLine1Keys: layout.footerLine1Keys.filter((item) => item !== key),
|
|
160
|
+
footerLine2Keys: layout.footerLine2Keys.filter((item) => item !== key),
|
|
161
|
+
footerLine3Keys: layout.footerLine3Keys.filter((item) => item !== key),
|
|
162
|
+
};
|
|
163
|
+
return withLineKeys(stripped, line, [...keysOnLine(stripped, line), key]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function shiftFooterKeyLine(
|
|
167
|
+
layout: FooterChipLayout,
|
|
168
|
+
key: string,
|
|
169
|
+
delta: -1 | 1,
|
|
170
|
+
): FooterChipLayout {
|
|
171
|
+
const current = footerLineOfKey(layout, key);
|
|
172
|
+
if (!current) return layout;
|
|
173
|
+
const next = (current + delta) as number;
|
|
174
|
+
if (next < 1 || next > 3) return layout;
|
|
175
|
+
return moveFooterKeyToLine(layout, key, next as FooterLineId);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function reorderFooterKey(
|
|
179
|
+
layout: FooterChipLayout,
|
|
180
|
+
key: string,
|
|
181
|
+
delta: -1 | 1,
|
|
182
|
+
): FooterChipLayout {
|
|
183
|
+
const line = footerLineOfKey(layout, key);
|
|
184
|
+
if (!line) return layout;
|
|
185
|
+
const keys = [...keysOnLine(layout, line)];
|
|
186
|
+
const index = keys.indexOf(key);
|
|
187
|
+
const nextIndex = index + delta;
|
|
188
|
+
if (index < 0 || nextIndex < 0 || nextIndex >= keys.length) return layout;
|
|
189
|
+
const swap = keys[nextIndex]!;
|
|
190
|
+
keys[nextIndex] = key;
|
|
191
|
+
keys[index] = swap;
|
|
192
|
+
return withLineKeys(layout, line, keys);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function toggleFooterKeyHidden(layout: FooterChipLayout, key: string): FooterChipLayout {
|
|
196
|
+
if (isSkippedFooterStatusKey(key)) return layout;
|
|
197
|
+
const hidden = new Set(layout.footerHiddenKeys);
|
|
198
|
+
if (hidden.has(key)) hidden.delete(key);
|
|
199
|
+
else hidden.add(key);
|
|
200
|
+
return { ...layout, footerHiddenKeys: [...hidden] };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** 按行顺序收集可见、且当前有文案的插件芯片文本。 */
|
|
204
|
+
export function visibleFooterPluginTexts(
|
|
205
|
+
lineKeys: readonly string[],
|
|
206
|
+
hiddenKeys: readonly string[],
|
|
207
|
+
texts: ReadonlyMap<string, string>,
|
|
208
|
+
): string[] {
|
|
209
|
+
const hidden = new Set(hiddenKeys);
|
|
210
|
+
const out: string[] = [];
|
|
211
|
+
for (const key of lineKeys) {
|
|
212
|
+
if (hidden.has(key)) continue;
|
|
213
|
+
const text = texts
|
|
214
|
+
.get(key)
|
|
215
|
+
?.replace(/[\r\n\t]+/g, " ")
|
|
216
|
+
.trim();
|
|
217
|
+
if (text) out.push(text);
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function formatFooterChipSummary(layout: FooterChipLayout): string {
|
|
223
|
+
const hidden = layout.footerHiddenKeys.length;
|
|
224
|
+
return `hidden=${hidden} · L1=${layout.footerLine1Keys.length} · L2=${layout.footerLine2Keys.length} · L3=${layout.footerLine3Keys.length}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function orderedFooterKeys(layout: FooterChipLayout): string[] {
|
|
228
|
+
return [...layout.footerLine1Keys, ...layout.footerLine2Keys, ...layout.footerLine3Keys];
|
|
229
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 自定义底栏:chips + zentui 图标/句子
|
|
3
3
|
*
|
|
4
|
-
* 第一行:model thinking · ██████░░░░ 45%/200k · 42% · $0.01 ·
|
|
5
|
-
* 第二行:cwd in session on branch (+16 −1) ·
|
|
4
|
+
* 第一行:model thinking · ██████░░░░ 45%/200k · 42% · $0.01 · 用户排到 line1 的插件芯片
|
|
5
|
+
* 第二行:cwd in session on branch (+16 −1) · 用户排到 line2 的插件芯片
|
|
6
|
+
* 第三行:仅当 line3 有可见插件芯片时出现
|
|
6
7
|
*
|
|
7
8
|
* - line1 短芯片,· 分隔;缓存用 zentui ,费用 success
|
|
8
9
|
* - line2 用 zentui 句式 in / on +
|
|
@@ -20,13 +21,26 @@ import { join } from "node:path";
|
|
|
20
21
|
import { pathToFileURL } from "node:url";
|
|
21
22
|
import { config } from "../../config/config.ts";
|
|
22
23
|
import { stripAnsi } from "../../utils/ansi-text.ts";
|
|
24
|
+
import {
|
|
25
|
+
PI_USAGE_KEY,
|
|
26
|
+
isSkippedFooterStatusKey,
|
|
27
|
+
resolveFooterChipLayout,
|
|
28
|
+
visibleFooterPluginTexts,
|
|
29
|
+
type FooterChipLayout,
|
|
30
|
+
} from "./footer-layout.ts";
|
|
23
31
|
|
|
24
32
|
const GIT_REFRESH_INTERVAL_MS = 10_000;
|
|
25
33
|
const USAGE_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
|
26
34
|
const USAGE_TIMEOUT_MS = 15_000;
|
|
27
|
-
// zentui NERD_DEFAULT_ICONS:git / cacheHit
|
|
28
|
-
const
|
|
29
|
-
const
|
|
35
|
+
// zentui NERD_DEFAULT_ICONS:git / cacheHit。无 Nerd Font 时留空,只保留文字。
|
|
36
|
+
export const FOOTER_NERD_ICON_GIT = "";
|
|
37
|
+
export const FOOTER_NERD_ICON_CACHE = "";
|
|
38
|
+
|
|
39
|
+
export function footerGlyphs(nerdIcons: boolean): { git: string; cache: string } {
|
|
40
|
+
return nerdIcons
|
|
41
|
+
? { git: FOOTER_NERD_ICON_GIT, cache: FOOTER_NERD_ICON_CACHE }
|
|
42
|
+
: { git: "", cache: "" };
|
|
43
|
+
}
|
|
30
44
|
|
|
31
45
|
type GitStats = { add: number; del: number };
|
|
32
46
|
|
|
@@ -54,12 +68,6 @@ export function parseGitStats(stdout: string): GitStats {
|
|
|
54
68
|
return { add, del };
|
|
55
69
|
}
|
|
56
70
|
|
|
57
|
-
export function classifyStatus(key: string): "usage" | "skip" | "other" {
|
|
58
|
-
if (key === "model") return "skip"; // line1 已有供应商/模型
|
|
59
|
-
if (/usage|quota|balance/i.test(key)) return "usage";
|
|
60
|
-
return "other";
|
|
61
|
-
}
|
|
62
|
-
|
|
63
71
|
export function formatXaiFooterChip(report: XaiFooterReport): string | undefined {
|
|
64
72
|
const included = report.buckets?.find((b) => b.id === "included-allowance");
|
|
65
73
|
if (included?.unit === "percent" && typeof included.used === "number") {
|
|
@@ -87,14 +95,44 @@ export function formatXaiFooterChip(report: XaiFooterReport): string | undefined
|
|
|
87
95
|
return undefined;
|
|
88
96
|
}
|
|
89
97
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
+
const cachedExtensionStatuses = new Map<string, string>();
|
|
99
|
+
let cachedLocalUsageText = "";
|
|
100
|
+
|
|
101
|
+
/** 面板用:当前 setStatus 文案 + 本包 pi-usage(无文案时为空字符串)。 */
|
|
102
|
+
export function getFooterStatusSnapshot(): Map<string, string> {
|
|
103
|
+
const out = new Map<string, string>();
|
|
104
|
+
for (const [key, text] of cachedExtensionStatuses) {
|
|
105
|
+
if (isSkippedFooterStatusKey(key)) continue;
|
|
106
|
+
out.set(key, text);
|
|
107
|
+
}
|
|
108
|
+
out.set(PI_USAGE_KEY, cachedLocalUsageText);
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function rememberExtensionStatuses(entries: Iterable<[string, string]>): void {
|
|
113
|
+
cachedExtensionStatuses.clear();
|
|
114
|
+
for (const [key, raw] of entries) {
|
|
115
|
+
if (isSkippedFooterStatusKey(key)) continue;
|
|
116
|
+
const text = stripAnsi(raw.replace(/[\r\n\t]+/g, " ").trim());
|
|
117
|
+
if (text) cachedExtensionStatuses.set(key, text);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function pluginTextsForRender(localUsageChip: string): Map<string, string> {
|
|
122
|
+
cachedLocalUsageText = stripAnsi(localUsageChip.replace(/[\r\n\t]+/g, " ").trim());
|
|
123
|
+
const texts = new Map<string, string>(cachedExtensionStatuses);
|
|
124
|
+
if (cachedLocalUsageText) texts.set(PI_USAGE_KEY, cachedLocalUsageText);
|
|
125
|
+
else texts.delete(PI_USAGE_KEY);
|
|
126
|
+
return texts;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function layoutFromConfig(): FooterChipLayout {
|
|
130
|
+
return {
|
|
131
|
+
footerHiddenKeys: config.footerHiddenKeys,
|
|
132
|
+
footerLine1Keys: config.footerLine1Keys,
|
|
133
|
+
footerLine2Keys: config.footerLine2Keys,
|
|
134
|
+
footerLine3Keys: config.footerLine3Keys,
|
|
135
|
+
};
|
|
98
136
|
}
|
|
99
137
|
|
|
100
138
|
let piUsageMod: any | null | undefined;
|
|
@@ -317,26 +355,33 @@ const createCustomFooterFactory =
|
|
|
317
355
|
const thinkingLevelStr = ctx.thinkingLevel || "off";
|
|
318
356
|
const thinkingColor = theme.getThinkingBorderColor(thinkingLevelStr);
|
|
319
357
|
|
|
320
|
-
|
|
321
|
-
const usageStatuses: string[] = [];
|
|
322
|
-
for (const [key, raw] of Array.from(
|
|
358
|
+
rememberExtensionStatuses(
|
|
323
359
|
footerData.getExtensionStatuses().entries() as Iterable<[string, string]>,
|
|
324
|
-
)
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
360
|
+
);
|
|
361
|
+
const pluginTexts = pluginTextsForRender(localUsageChip);
|
|
362
|
+
const layout = resolveFooterChipLayout(layoutFromConfig(), [...pluginTexts.keys()]);
|
|
363
|
+
const dimPlugin = (text: string) => colorUsageChip(theme, text);
|
|
364
|
+
const line1Plugins = visibleFooterPluginTexts(
|
|
365
|
+
layout.footerLine1Keys,
|
|
366
|
+
layout.footerHiddenKeys,
|
|
367
|
+
pluginTexts,
|
|
368
|
+
).map(dimPlugin);
|
|
369
|
+
const line2Plugins = visibleFooterPluginTexts(
|
|
370
|
+
layout.footerLine2Keys,
|
|
371
|
+
layout.footerHiddenKeys,
|
|
372
|
+
pluginTexts,
|
|
373
|
+
).map(dimPlugin);
|
|
374
|
+
const line3Plugins = visibleFooterPluginTexts(
|
|
375
|
+
layout.footerLine3Keys,
|
|
376
|
+
layout.footerHiddenKeys,
|
|
377
|
+
pluginTexts,
|
|
378
|
+
).map(dimPlugin);
|
|
337
379
|
|
|
338
380
|
const pctLabel = percent === null || percent === undefined ? "?" : `${Math.floor(percent)}%`;
|
|
339
381
|
const cachePct = getCachePct();
|
|
382
|
+
const glyphs = footerGlyphs(config.footerNerdIcons);
|
|
383
|
+
const cacheLabel =
|
|
384
|
+
cachePct > 0 ? `${glyphs.cache ? `${glyphs.cache} ` : ""}${Math.floor(cachePct)}%` : "";
|
|
340
385
|
const costChip =
|
|
341
386
|
cost || usingSubscription
|
|
342
387
|
? theme.fg("dim", `$${cost.toFixed(2)}`) +
|
|
@@ -346,9 +391,9 @@ const createCustomFooterFactory =
|
|
|
346
391
|
theme.fg("accent", modelLabel),
|
|
347
392
|
model?.reasoning ? thinkingColor(thinkingLevelStr) : "",
|
|
348
393
|
barGauge(percent ?? 0) + theme.fg("dim", ` ${pctLabel}/${fmt(contextWindow)}`),
|
|
349
|
-
|
|
394
|
+
cacheLabel ? theme.fg("dim", cacheLabel) : "",
|
|
350
395
|
costChip,
|
|
351
|
-
|
|
396
|
+
...line1Plugins,
|
|
352
397
|
]);
|
|
353
398
|
|
|
354
399
|
const gitColor = theme.getThinkingBorderColor("medium");
|
|
@@ -367,11 +412,17 @@ const createCustomFooterFactory =
|
|
|
367
412
|
theme.fg("error", `−${gitStats.del}`) +
|
|
368
413
|
theme.fg("dim", ")")
|
|
369
414
|
: "";
|
|
370
|
-
|
|
415
|
+
const gitLabel = glyphs.git ? `${glyphs.git} ${branch}` : branch;
|
|
416
|
+
place += theme.fg("muted", " on ") + gitColor(gitLabel) + stats;
|
|
371
417
|
}
|
|
372
|
-
const line2 = joinChips([place, ...
|
|
373
|
-
|
|
374
|
-
|
|
418
|
+
const line2 = joinChips([place, ...line2Plugins]);
|
|
419
|
+
const line3 = joinChips(line3Plugins);
|
|
420
|
+
|
|
421
|
+
return [
|
|
422
|
+
truncateToWidth(line1, width),
|
|
423
|
+
...(line2 ? [truncateToWidth(line2, width)] : []),
|
|
424
|
+
...(line3 ? [truncateToWidth(line3, width)] : []),
|
|
425
|
+
];
|
|
375
426
|
};
|
|
376
427
|
|
|
377
428
|
return {
|
|
@@ -395,16 +446,9 @@ const createCustomFooterFactory =
|
|
|
395
446
|
};
|
|
396
447
|
};
|
|
397
448
|
|
|
398
|
-
/**
|
|
399
|
-
* 按配置应用底栏:on → 自定义 footer;off → 恢复官方默认 footer。
|
|
400
|
-
* 导出供 /ccstyle 面板在切换开关时实时重应用。
|
|
401
|
-
*/
|
|
449
|
+
/** Apply the configured custom footer without disturbing another extension's footer when disabled. */
|
|
402
450
|
export function applyCustomFooter(ctx: ExtensionContext): void {
|
|
403
|
-
if (!ctx?.hasUI || typeof ctx.ui?.setFooter !== "function") return;
|
|
404
|
-
if (!config.enableCustomFooter) {
|
|
405
|
-
ctx.ui.setFooter(undefined);
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
451
|
+
if (!ctx?.hasUI || typeof ctx.ui?.setFooter !== "function" || !config.enableCustomFooter) return;
|
|
408
452
|
try {
|
|
409
453
|
ctx.ui.setFooter(createCustomFooterFactory(ctx));
|
|
410
454
|
} catch (err) {
|
|
@@ -412,6 +456,12 @@ export function applyCustomFooter(ctx: ExtensionContext): void {
|
|
|
412
456
|
}
|
|
413
457
|
}
|
|
414
458
|
|
|
459
|
+
/** Restore Pi's native footer when the user explicitly disables this extension's active footer. */
|
|
460
|
+
export function clearCustomFooter(ctx: ExtensionContext): void {
|
|
461
|
+
if (!ctx?.hasUI || typeof ctx.ui?.setFooter !== "function") return;
|
|
462
|
+
ctx.ui.setFooter(undefined);
|
|
463
|
+
}
|
|
464
|
+
|
|
415
465
|
export default function (pi: ExtensionAPI) {
|
|
416
466
|
// 模型/思考级别变化时强制重渲染(自定义 footer 不会被内置 invalidate() 触达)
|
|
417
467
|
pi.on("model_select", () => {
|
|
@@ -279,15 +279,9 @@ export function renderHeaderLines(
|
|
|
279
279
|
);
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
-
/**
|
|
283
|
-
* 按配置应用启动头:on → 自定义 header;off → 恢复官方默认 header。
|
|
284
|
-
* 导出供 /ccstyle 面板在切换开关时实时重应用。
|
|
285
|
-
*/
|
|
282
|
+
/** 按配置安装启动头。禁用时不碰槽位,避免清掉其他扩展的 header。 */
|
|
286
283
|
export function applyStartupHeader(ctx: any): void {
|
|
287
|
-
if (!ctx?.hasUI || typeof ctx.ui?.setHeader !== "function")
|
|
288
|
-
if (!config.showStartupHeader) {
|
|
289
|
-
// 恢复官方内置 header(logo + 快捷键提示 + onboarding)。
|
|
290
|
-
ctx.ui.setHeader(undefined);
|
|
284
|
+
if (!ctx?.hasUI || typeof ctx.ui?.setHeader !== "function" || !config.showStartupHeader) {
|
|
291
285
|
return;
|
|
292
286
|
}
|
|
293
287
|
ctx.ui.setHeader((_tui: unknown, theme: any) => ({
|
|
@@ -298,6 +292,12 @@ export function applyStartupHeader(ctx: any): void {
|
|
|
298
292
|
}));
|
|
299
293
|
}
|
|
300
294
|
|
|
295
|
+
/** 用户从 /ccstyle 关掉本扩展启动头时,恢复官方 header。 */
|
|
296
|
+
export function clearStartupHeader(ctx: any): void {
|
|
297
|
+
if (!ctx?.hasUI || typeof ctx.ui?.setHeader !== "function") return;
|
|
298
|
+
ctx.ui.setHeader(undefined);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
301
|
export default function piStartupHeader(pi: ExtensionAPI) {
|
|
302
302
|
pi.on("session_start", async (_event, ctx) => {
|
|
303
303
|
applyStartupHeader(ctx);
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cc-extensions",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Claude Code-style TUI output with some personal touches and handy utilities.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"pi-package",
|