pi-open-tui 0.3.11 → 0.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -49,6 +49,8 @@ The default `auto` mode checks the terminal environment, not the installed font
|
|
|
49
49
|
|
|
50
50
|
If the font is installed but `auto` still selects ASCII, choose `nerd` explicitly. In VS Code, Windows Terminal, and similar apps, configure the font in the terminal profile rather than only installing it in the operating system. If your SSH client terminal does ship a Nerd Font and you want the full icon set over SSH, set the mode to `nerd` explicitly.
|
|
51
51
|
|
|
52
|
+
If wide Nerd Font icons visually consume the gap before their labels (for example in Kitty), enable `footerSegments.nerdIconSpacing` (**Footer → Extra space after Nerd icons** in `/open-tui`). The footer then reserves an extra blank cell after Nerd Font icons, including runtime icons, and includes it in regular and inline footer width calculations. Unicode/ASCII icons and compact Git status flags are unchanged. In Kitty, `narrow_symbols U+E000-U+F8FF 1` in `kitty.conf` is an alternative.
|
|
53
|
+
|
|
52
54
|
## Configuration
|
|
53
55
|
|
|
54
56
|
Run `/open-tui` to open the settings dialog. It provides **General**, **Appearance**, **Footer**, and **Telemetry** tabs. Settings are stored in `~/.pi/agent/open-tui.json`:
|
|
@@ -74,7 +76,8 @@ Run `/open-tui` to open the settings dialog. It provides **General**, **Appearan
|
|
|
74
76
|
"tokens": true,
|
|
75
77
|
"cost": true,
|
|
76
78
|
"extensionStatuses": true,
|
|
77
|
-
"capitalizeProviderName": true
|
|
79
|
+
"capitalizeProviderName": true,
|
|
80
|
+
"nerdIconSpacing": false
|
|
78
81
|
},
|
|
79
82
|
"telemetry": {
|
|
80
83
|
"enabled": true,
|
|
@@ -101,10 +104,11 @@ Key options:
|
|
|
101
104
|
| `icons.mode` | `auto`, `nerd`, `unicode`, `ascii` | Controls footer and telemetry icons |
|
|
102
105
|
| `footerSegments` | Boolean flags | Shows or hides individual footer data |
|
|
103
106
|
| `footerSegments.capitalizeProviderName` | Boolean | Capitalizes the first character of the provider name in the footer; set it to `false` to keep the provider's original casing |
|
|
107
|
+
| `footerSegments.nerdIconSpacing` | Boolean | Adds an extra blank cell after Nerd Font icons in the footer; defaults to `false` |
|
|
104
108
|
| `telemetry` | Boolean flags | Enables telemetry and its individual measurements |
|
|
105
109
|
| `thinkingPeek.lines` | `0`, `1`, `2` | Off, one-line, or two-line hidden thinking preview |
|
|
106
110
|
|
|
107
|
-
`sessionName` appears only when the session has a name. `hostname` shows the short host name (first label of the machine's host name, e.g. `mba` from `mba.example.com`) with a server icon. `
|
|
111
|
+
`sessionName` appears only when the session has a name. `hostname` shows the short host name (first label of the machine's host name, e.g. `mba` from `mba.example.com`) with a server icon. In detached HEAD state, `gitBranch` shows `HEAD` with the short commit hash; `gitCommit` appends the tag pointing at that commit and only takes effect while `gitBranch` is on. Disabling `extensionStatuses` hides the entire extension status line, including MCP status. Each status keeps the colours its extension applied with `ctx.ui.theme.fg()`. A status without colours renders in the muted theme colour.
|
|
108
112
|
|
|
109
113
|
With `inlineFooter` enabled, the two normal Footer rows are rendered inside the editor frame to save vertical space. The top border places the Git branch on the left and CWD first in the right-hand group; the session title appears on the left too when `sessionName` is enabled. The Header and extension status rows remain separate; narrow terminals truncate lower-priority Footer data first, keeping the right-hand statistics and the border corner.
|
|
110
114
|
|
package/README.zh-CN.md
CHANGED
|
@@ -49,6 +49,8 @@ pi -e npm:pi-open-tui
|
|
|
49
49
|
|
|
50
50
|
如果已经安装字体,但 `auto` 仍选择 ASCII,请手动切换为 `nerd`。使用 VS Code、Windows Terminal 等应用时,只在操作系统中安装字体还不够,还需要在对应的终端配置中选中该字体。如果 SSH 客户端终端确实安装了 Nerd Font 并希望在 SSH 下使用完整图标,请将模式显式设为 `nerd`。
|
|
51
51
|
|
|
52
|
+
如果较宽的 Nerd Font 图标占用了图标与文字之间的间隔(例如在 Kitty 中),可开启 `footerSegments.nerdIconSpacing`(`/open-tui` 中的 **底栏 → Nerd 图标后额外留空**)。开启后底栏会在 Nerd Font 图标后额外预留一格空白,包括运行环境图标,常规底栏和内嵌底栏的宽度计算都包含这格留白;Unicode/ASCII 图标与紧凑的 Git 状态标记不受影响。Kitty 用户也可以在 `kitty.conf` 中设置 `narrow_symbols U+E000-U+F8FF 1`。
|
|
53
|
+
|
|
52
54
|
## 配置
|
|
53
55
|
|
|
54
56
|
运行 `/open-tui` 打开设置窗口,其中包含**常规**、**外观**、**底栏**和**遥测**四个页面。设置保存在 `~/.pi/agent/open-tui.json`:
|
|
@@ -74,7 +76,8 @@ pi -e npm:pi-open-tui
|
|
|
74
76
|
"tokens": true,
|
|
75
77
|
"cost": true,
|
|
76
78
|
"extensionStatuses": true,
|
|
77
|
-
"capitalizeProviderName": true
|
|
79
|
+
"capitalizeProviderName": true,
|
|
80
|
+
"nerdIconSpacing": false
|
|
78
81
|
},
|
|
79
82
|
"telemetry": {
|
|
80
83
|
"enabled": true,
|
|
@@ -101,10 +104,11 @@ pi -e npm:pi-open-tui
|
|
|
101
104
|
| `icons.mode` | `auto`、`nerd`、`unicode`、`ascii` | 控制底栏和遥测通知使用的图标 |
|
|
102
105
|
| `footerSegments` | 布尔开关 | 分别控制底栏中的各项数据 |
|
|
103
106
|
| `footerSegments.capitalizeProviderName` | 布尔开关 | 将底栏中提供商名称的首字母大写;设为 `false` 时保留原始大小写 |
|
|
107
|
+
| `footerSegments.nerdIconSpacing` | 布尔开关 | 在底栏 Nerd Font 图标后额外留一格空白;默认 `false` |
|
|
104
108
|
| `telemetry` | 布尔开关 | 控制遥测总开关和各项指标 |
|
|
105
109
|
| `thinkingPeek.lines` | `0`、`1`、`2` | 关闭、单行或双行思考预览 |
|
|
106
110
|
|
|
107
|
-
`sessionName` 仅在会话有名称时显示;`hostname` 会显示主机名的短名称(主机名的第一个标签,例如从 `mba.example.com` 显示为 `mba
|
|
111
|
+
`sessionName` 仅在会话有名称时显示;`hostname` 会显示主机名的短名称(主机名的第一个标签,例如从 `mba.example.com` 显示为 `mba`),并使用服务器图标;处于分离 HEAD(detached HEAD)状态时,`gitBranch` 会显示 `HEAD` 和短提交哈希,`gitCommit` 会在其后追加指向该提交的标签,且仅在开启 `gitBranch` 时生效;关闭 `extensionStatuses` 会隐藏整行扩展状态,其中也包括 MCP 状态。每条状态会保留其扩展通过 `ctx.ui.theme.fg()` 设置的颜色;未设置颜色的状态使用 muted 主题色显示。
|
|
108
112
|
|
|
109
113
|
开启 `inlineFooter` 后,两条常规 Footer 信息行会移入编辑器边框,从而节省垂直空间。顶部边框左侧显示 Git 分支,右侧信息组以当前目录开头;开启 `sessionName` 时,会话标题也会显示在左侧。Header 和扩展状态行保持独立显示;终端较窄时优先截断低优先级 Footer 数据,保留右侧统计信息和边框角。
|
|
110
114
|
|
|
@@ -22,6 +22,7 @@ export interface FooterSegments {
|
|
|
22
22
|
cost: boolean;
|
|
23
23
|
extensionStatuses: boolean;
|
|
24
24
|
capitalizeProviderName: boolean;
|
|
25
|
+
nerdIconSpacing: boolean;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export interface TelemetryConfig {
|
|
@@ -72,6 +73,7 @@ export const DEFAULT_CONFIG: OpenTuiConfig = {
|
|
|
72
73
|
cost: true,
|
|
73
74
|
extensionStatuses: true,
|
|
74
75
|
capitalizeProviderName: true,
|
|
76
|
+
nerdIconSpacing: false,
|
|
75
77
|
},
|
|
76
78
|
telemetry: {
|
|
77
79
|
enabled: true,
|
|
@@ -96,6 +98,18 @@ function normalizeThinkingPeekLines(value: unknown): ThinkingPeekLines {
|
|
|
96
98
|
return value === 0 || value === 1 || value === 2 ? value : DEFAULT_CONFIG.thinkingPeek.lines;
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
const ICON_MODES: IconMode[] = ["auto", "nerd", "unicode", "ascii"];
|
|
102
|
+
|
|
103
|
+
// Non-boolean flags (e.g. "false" strings) fall back to defaults instead of reading as truthy.
|
|
104
|
+
function normalizeFlags<T extends object>(value: unknown, defaults: T): T {
|
|
105
|
+
const source = typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
106
|
+
const result: Record<string, unknown> = { ...source };
|
|
107
|
+
for (const [key, fallback] of Object.entries(defaults)) {
|
|
108
|
+
if (typeof result[key] !== "boolean") result[key] = fallback;
|
|
109
|
+
}
|
|
110
|
+
return result as T;
|
|
111
|
+
}
|
|
112
|
+
|
|
99
113
|
function deepMerge<T>(base: T, override: unknown): T {
|
|
100
114
|
if (typeof base !== "object" || base === null || Array.isArray(base)) {
|
|
101
115
|
return (override as T) ?? base;
|
|
@@ -141,9 +155,17 @@ export function loadConfig(notify?: (msg: string, level: "warning" | "info") =>
|
|
|
141
155
|
const raw = readFileSync(path, "utf8");
|
|
142
156
|
const parsed: unknown = JSON.parse(raw);
|
|
143
157
|
const config = deepMerge(DEFAULT_CONFIG, parsed);
|
|
158
|
+
if (typeof config.enabled !== "boolean") {
|
|
159
|
+
config.enabled = DEFAULT_CONFIG.enabled;
|
|
160
|
+
}
|
|
144
161
|
if (typeof config.inlineFooter !== "boolean") {
|
|
145
162
|
config.inlineFooter = DEFAULT_CONFIG.inlineFooter;
|
|
146
163
|
}
|
|
164
|
+
config.footerSegments = normalizeFlags(config.footerSegments, DEFAULT_CONFIG.footerSegments);
|
|
165
|
+
config.telemetry = normalizeFlags(config.telemetry, DEFAULT_CONFIG.telemetry);
|
|
166
|
+
if (typeof config.icons !== "object" || config.icons === null || !ICON_MODES.includes(config.icons.mode)) {
|
|
167
|
+
config.icons = structuredClone(DEFAULT_CONFIG.icons);
|
|
168
|
+
}
|
|
147
169
|
if (config.settingsLanguage !== "en" && config.settingsLanguage !== "zh") {
|
|
148
170
|
config.settingsLanguage = DEFAULT_CONFIG.settingsLanguage;
|
|
149
171
|
}
|
|
@@ -31,6 +31,15 @@ export function shortHostname(hostname: string): string {
|
|
|
31
31
|
return hostname.split(".")[0] ?? "";
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// Kitty can use the blank cell after a wide Nerd icon to render the glyph.
|
|
35
|
+
// With footerSegments.nerdIconSpacing, reserve an extra cell before fitting
|
|
36
|
+
// footer segments. Keep Unicode/ASCII and Git flags compact.
|
|
37
|
+
function spaceFooterIcon(glyph: string): string {
|
|
38
|
+
return /[\uE000-\uF8FF\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]/u.test(glyph)
|
|
39
|
+
? `${glyph} `
|
|
40
|
+
: glyph;
|
|
41
|
+
}
|
|
42
|
+
|
|
34
43
|
function renderBar(theme: Theme, pct: number, barWidth: number, ascii: boolean): string {
|
|
35
44
|
const filled = Math.max(0, Math.min(barWidth, Math.round((pct / 100) * barWidth)));
|
|
36
45
|
const empty = barWidth - filled;
|
|
@@ -112,9 +121,11 @@ function renderRuntimeSegment(
|
|
|
112
121
|
theme: Theme,
|
|
113
122
|
runtime: RuntimeInfo | null,
|
|
114
123
|
iconMode: OpenTuiConfig["icons"]["mode"],
|
|
124
|
+
spaceIcon: boolean,
|
|
115
125
|
): string {
|
|
116
126
|
if (!runtime) return "";
|
|
117
|
-
const
|
|
127
|
+
const glyph = runtimeSymbol(runtime.name, iconMode);
|
|
128
|
+
const symbol = theme.fg("success", spaceIcon ? spaceFooterIcon(glyph) : glyph);
|
|
118
129
|
const version = runtime.version ? theme.fg("muted", runtime.version) : "";
|
|
119
130
|
const label = [symbol, version].filter(Boolean).join(" ");
|
|
120
131
|
return label;
|
|
@@ -286,8 +297,13 @@ function renderFooterContent(
|
|
|
286
297
|
|
|
287
298
|
const state = getState();
|
|
288
299
|
const config = getConfig();
|
|
289
|
-
const glyphs = resolveGlyphs(config.icons.mode);
|
|
290
300
|
const segments = config.footerSegments;
|
|
301
|
+
const glyphs = { ...resolveGlyphs(config.icons.mode) };
|
|
302
|
+
if (segments.nerdIconSpacing) {
|
|
303
|
+
for (const key of Object.keys(glyphs) as (keyof IconGlyphs)[]) {
|
|
304
|
+
glyphs[key] = spaceFooterIcon(glyphs[key]);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
291
307
|
const meta = getModelMeta();
|
|
292
308
|
const totals = getUsageTotals(ctx);
|
|
293
309
|
|
|
@@ -340,7 +356,7 @@ function renderFooterContent(
|
|
|
340
356
|
inlineTopLeftParts.push(leftParts.at(-1)!);
|
|
341
357
|
}
|
|
342
358
|
if (segments.runtime) {
|
|
343
|
-
const runtimeSeg = renderRuntimeSegment(theme, state.runtime, config.icons.mode);
|
|
359
|
+
const runtimeSeg = renderRuntimeSegment(theme, state.runtime, config.icons.mode, segments.nerdIconSpacing);
|
|
344
360
|
if (runtimeSeg) {
|
|
345
361
|
leftParts.push({ text: runtimeSeg, priority: 4 });
|
|
346
362
|
inlineTopRightParts.push(leftParts.at(-1)!);
|
|
@@ -178,7 +178,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
178
178
|
const scheduleGitRefresh = async (ctx: ExtensionContext) => {
|
|
179
179
|
if (!sessionLifecycle.isCurrent()) return;
|
|
180
180
|
const segs = config.footerSegments;
|
|
181
|
-
|
|
181
|
+
// gitCommit only appends a tag to the gitBranch segment, so it never needs git on its own.
|
|
182
|
+
if (!segs.gitBranch && !segs.gitStatus) {
|
|
182
183
|
state.git = emptyGitStatus();
|
|
183
184
|
requestFooterRender?.();
|
|
184
185
|
return;
|
|
@@ -186,8 +187,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
186
187
|
const generation = sessionLifecycle.currentGeneration();
|
|
187
188
|
const cwd = ctx.cwd;
|
|
188
189
|
const git = await readGitStatus(cwd, {
|
|
189
|
-
readCommit:
|
|
190
|
-
readTag: segs.gitCommit,
|
|
190
|
+
readCommit: segs.gitBranch,
|
|
191
|
+
readTag: segs.gitBranch && segs.gitCommit,
|
|
191
192
|
readCounts: segs.gitStatus,
|
|
192
193
|
});
|
|
193
194
|
if (!sessionLifecycle.isCurrent(generation)) return;
|
|
@@ -399,7 +400,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
399
400
|
if (lastCtx) {
|
|
400
401
|
pendingUiChange = getPendingUiChange(newConfig.enabled, active);
|
|
401
402
|
}
|
|
402
|
-
const gitNeeded = newConfig.footerSegments.gitBranch || newConfig.footerSegments.gitStatus
|
|
403
|
+
const gitNeeded = newConfig.footerSegments.gitBranch || newConfig.footerSegments.gitStatus;
|
|
403
404
|
if (lastCtx && gitNeeded) {
|
|
404
405
|
void scheduleGitRefresh(lastCtx);
|
|
405
406
|
} else {
|
|
@@ -28,7 +28,7 @@ const COPY = {
|
|
|
28
28
|
hint: "Tab/Shift+Tab/←/→: tabs · ↑/↓: move · Enter/Space: change · Esc/q: close",
|
|
29
29
|
labels: {
|
|
30
30
|
enabled: "Enabled",
|
|
31
|
-
inlineFooter: "
|
|
31
|
+
inlineFooter: "Footer in editor border",
|
|
32
32
|
thinkingPeek: "Thinking peek",
|
|
33
33
|
language: "Language",
|
|
34
34
|
cursorStyle: "Cursor style",
|
|
@@ -38,17 +38,18 @@ const COPY = {
|
|
|
38
38
|
sessionName: "Session name",
|
|
39
39
|
gitBranch: "Git branch",
|
|
40
40
|
gitStatus: "Git status",
|
|
41
|
-
gitCommit: "Git
|
|
42
|
-
runtime: "
|
|
43
|
-
context: "Context
|
|
44
|
-
tokens: "
|
|
45
|
-
cost: "
|
|
41
|
+
gitCommit: "Git tag (detached HEAD)",
|
|
42
|
+
runtime: "Project runtime",
|
|
43
|
+
context: "Context usage",
|
|
44
|
+
tokens: "Session tokens",
|
|
45
|
+
cost: "Session cost",
|
|
46
46
|
extensionStatuses: "Extension status line",
|
|
47
47
|
capitalizeProviderName: "Capitalize provider name",
|
|
48
|
+
nerdIconSpacing: "Extra space after Nerd icons",
|
|
48
49
|
totalDuration: "Total duration",
|
|
49
|
-
tokenCounts: "
|
|
50
|
+
tokenCounts: "Run tokens",
|
|
50
51
|
stallDetails: "Stall details",
|
|
51
|
-
costRate: "
|
|
52
|
+
costRate: "Price per 1M tokens",
|
|
52
53
|
},
|
|
53
54
|
values: {
|
|
54
55
|
on: "On",
|
|
@@ -61,11 +62,11 @@ const COPY = {
|
|
|
61
62
|
},
|
|
62
63
|
zh: {
|
|
63
64
|
title: "Open TUI 设置",
|
|
64
|
-
tabs: { features: "常规", icons: "外观", segments: "
|
|
65
|
+
tabs: { features: "常规", icons: "外观", segments: "底栏", telemetry: "遥测" },
|
|
65
66
|
hint: "Tab/Shift+Tab/←/→:切页 · ↑/↓:移动 · Enter/Space:更改 · Esc/q:关闭",
|
|
66
67
|
labels: {
|
|
67
68
|
enabled: "启用",
|
|
68
|
-
inlineFooter: "
|
|
69
|
+
inlineFooter: "底栏嵌入输入框",
|
|
69
70
|
thinkingPeek: "思考预览",
|
|
70
71
|
language: "语言",
|
|
71
72
|
cursorStyle: "光标样式",
|
|
@@ -75,17 +76,18 @@ const COPY = {
|
|
|
75
76
|
sessionName: "会话名",
|
|
76
77
|
gitBranch: "Git 分支",
|
|
77
78
|
gitStatus: "Git 状态",
|
|
78
|
-
gitCommit: "Git
|
|
79
|
-
runtime: "
|
|
80
|
-
context: "
|
|
81
|
-
tokens: "Token",
|
|
82
|
-
cost: "
|
|
79
|
+
gitCommit: "Git 标签(分离 HEAD)",
|
|
80
|
+
runtime: "项目运行环境",
|
|
81
|
+
context: "上下文用量",
|
|
82
|
+
tokens: "会话 Token",
|
|
83
|
+
cost: "会话费用",
|
|
83
84
|
extensionStatuses: "扩展状态行",
|
|
84
85
|
capitalizeProviderName: "提供商名称首字母大写",
|
|
86
|
+
nerdIconSpacing: "Nerd 图标后额外留空",
|
|
85
87
|
totalDuration: "总耗时",
|
|
86
|
-
tokenCounts: "Token
|
|
88
|
+
tokenCounts: "单次运行 Token",
|
|
87
89
|
stallDetails: "停顿详情",
|
|
88
|
-
costRate: "
|
|
90
|
+
costRate: "每百万 Token 单价",
|
|
89
91
|
},
|
|
90
92
|
values: {
|
|
91
93
|
on: "开启",
|
|
@@ -185,6 +187,7 @@ function buildSegmentsItems(config: OpenTuiConfig, copy: SettingsCopy): SettingI
|
|
|
185
187
|
{ id: "cost", label: copy.labels.cost, currentValue: flag(segs.cost) },
|
|
186
188
|
{ id: "extensionStatuses", label: copy.labels.extensionStatuses, currentValue: flag(segs.extensionStatuses) },
|
|
187
189
|
{ id: "capitalizeProviderName", label: copy.labels.capitalizeProviderName, currentValue: flag(segs.capitalizeProviderName) },
|
|
190
|
+
{ id: "nerdIconSpacing", label: copy.labels.nerdIconSpacing, currentValue: flag(segs.nerdIconSpacing) },
|
|
188
191
|
];
|
|
189
192
|
}
|
|
190
193
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-open-tui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
4
4
|
"description": "A polished TUI for Pi coding agent: animated logo header, Starship-style footer, rounded editor with model metadata, and prompt-box user messages.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|