pi-open-tui 0.3.10 → 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 +7 -10
- package/README.zh-CN.md +7 -10
- package/extensions/open-tui/config.ts +22 -16
- package/extensions/open-tui/editor.ts +0 -11
- package/extensions/open-tui/footer.ts +35 -6
- package/extensions/open-tui/index.ts +5 -9
- package/extensions/open-tui/settings-command.ts +28 -120
- package/extensions/open-tui/utils.ts +22 -0
- package/package.json +8 -8
- package/extensions/open-tui/fullscreen-scroll.ts +0 -34
package/README.md
CHANGED
|
@@ -15,11 +15,10 @@ A polished terminal interface for the [Pi](https://pi.dev) coding agent. It brin
|
|
|
15
15
|
- **Turn telemetry** for TPS, time to first token (TTFT), duration, stalls, tokens, and list-price rate
|
|
16
16
|
- **Thinking peek**: an inline ticker replaces Pi's hidden `Thinking...` label with the tail of the model's reasoning while it works
|
|
17
17
|
- **Interactive settings** through `/open-tui`, available in English and Simplified Chinese
|
|
18
|
-
- **Version-guarded Pi compatibility shim**: fullscreen wheel speed falls back to Pi's default if its runtime support changes
|
|
19
18
|
|
|
20
19
|
## Requirements
|
|
21
20
|
|
|
22
|
-
- Pi 0
|
|
21
|
+
- Pi 1.0 or later
|
|
23
22
|
- A terminal with UTF-8 and color support
|
|
24
23
|
- A [Nerd Font](https://www.nerdfonts.com/font-downloads) for the full icon set (optional; portable Unicode icons are built in)
|
|
25
24
|
|
|
@@ -50,6 +49,8 @@ The default `auto` mode checks the terminal environment, not the installed font
|
|
|
50
49
|
|
|
51
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.
|
|
52
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
|
+
|
|
53
54
|
## Configuration
|
|
54
55
|
|
|
55
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`:
|
|
@@ -60,9 +61,6 @@ Run `/open-tui` to open the settings dialog. It provides **General**, **Appearan
|
|
|
60
61
|
"inlineFooter": false,
|
|
61
62
|
"settingsLanguage": "en",
|
|
62
63
|
"cursorStyle": "block",
|
|
63
|
-
"fullscreen": {
|
|
64
|
-
"wheelScrollLines": 4
|
|
65
|
-
},
|
|
66
64
|
"icons": {
|
|
67
65
|
"mode": "auto"
|
|
68
66
|
},
|
|
@@ -78,7 +76,8 @@ Run `/open-tui` to open the settings dialog. It provides **General**, **Appearan
|
|
|
78
76
|
"tokens": true,
|
|
79
77
|
"cost": true,
|
|
80
78
|
"extensionStatuses": true,
|
|
81
|
-
"capitalizeProviderName": true
|
|
79
|
+
"capitalizeProviderName": true,
|
|
80
|
+
"nerdIconSpacing": false
|
|
82
81
|
},
|
|
83
82
|
"telemetry": {
|
|
84
83
|
"enabled": true,
|
|
@@ -102,19 +101,17 @@ Key options:
|
|
|
102
101
|
| `settingsLanguage` | `en`, `zh` | Changes the `/open-tui` interface language |
|
|
103
102
|
| `inlineFooter` | `true`, `false` | Moves the two main Footer rows into the editor's top and bottom borders; defaults to `false`. Extension status rows remain below the editor |
|
|
104
103
|
| `cursorStyle` | `block`, `bar`, `underline` | `bar` and `underline` require terminal cursor-shape support |
|
|
105
|
-
| `fullscreen.wheelScrollLines` | `1`-`10` | Lines scrolled per mouse-wheel notch in fullscreen mode; defaults to `4`. In `/open-tui`, press Enter on this item and type a number (values are clamped to `1`-`10`) |
|
|
106
104
|
| `icons.mode` | `auto`, `nerd`, `unicode`, `ascii` | Controls footer and telemetry icons |
|
|
107
105
|
| `footerSegments` | Boolean flags | Shows or hides individual footer data |
|
|
108
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` |
|
|
109
108
|
| `telemetry` | Boolean flags | Enables telemetry and its individual measurements |
|
|
110
109
|
| `thinkingPeek.lines` | `0`, `1`, `2` | Off, one-line, or two-line hidden thinking preview |
|
|
111
110
|
|
|
112
|
-
`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.
|
|
113
112
|
|
|
114
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.
|
|
115
114
|
|
|
116
|
-
Fullscreen wheel speed uses an isolated compatibility shim for Pi 0.84.2's runtime field because Pi does not yet expose a public setter. On Pi versions without a compatible field, the setting is ignored and Pi's default scrolling remains active.
|
|
117
|
-
|
|
118
115
|
## Turn telemetry
|
|
119
116
|
|
|
120
117
|
After each complete agent run, pi-open-tui shows one transient result. Tool-call turns are combined into that result:
|
package/README.zh-CN.md
CHANGED
|
@@ -15,11 +15,10 @@
|
|
|
15
15
|
- **单轮遥测**:展示 TPS、首 Token 延迟(TTFT)、耗时、停顿、Token 数量和模型标价速率
|
|
16
16
|
- **思考预览**:模型工作时,在 Pi 隐藏思考块的 `Thinking...` 位置显示实时字幕,展示推理内容的末尾片段
|
|
17
17
|
- **交互式设置**:通过 `/open-tui` 配置,并支持英文和简体中文界面
|
|
18
|
-
- **带版本保护的 Pi 兼容层**:全屏滚轮速度所依赖的运行时支持发生变化时,会回退为 Pi 默认行为
|
|
19
18
|
|
|
20
19
|
## 环境要求
|
|
21
20
|
|
|
22
|
-
- Pi 0
|
|
21
|
+
- Pi 1.0 或更高版本
|
|
23
22
|
- 支持 UTF-8 和彩色输出的终端
|
|
24
23
|
- 使用完整图标集时需要 [Nerd Font](https://www.nerdfonts.com/font-downloads)(可选;内置可移植 Unicode 图标)
|
|
25
24
|
|
|
@@ -50,6 +49,8 @@ pi -e npm:pi-open-tui
|
|
|
50
49
|
|
|
51
50
|
如果已经安装字体,但 `auto` 仍选择 ASCII,请手动切换为 `nerd`。使用 VS Code、Windows Terminal 等应用时,只在操作系统中安装字体还不够,还需要在对应的终端配置中选中该字体。如果 SSH 客户端终端确实安装了 Nerd Font 并希望在 SSH 下使用完整图标,请将模式显式设为 `nerd`。
|
|
52
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
|
+
|
|
53
54
|
## 配置
|
|
54
55
|
|
|
55
56
|
运行 `/open-tui` 打开设置窗口,其中包含**常规**、**外观**、**底栏**和**遥测**四个页面。设置保存在 `~/.pi/agent/open-tui.json`:
|
|
@@ -60,9 +61,6 @@ pi -e npm:pi-open-tui
|
|
|
60
61
|
"inlineFooter": false,
|
|
61
62
|
"settingsLanguage": "zh",
|
|
62
63
|
"cursorStyle": "block",
|
|
63
|
-
"fullscreen": {
|
|
64
|
-
"wheelScrollLines": 4
|
|
65
|
-
},
|
|
66
64
|
"icons": {
|
|
67
65
|
"mode": "auto"
|
|
68
66
|
},
|
|
@@ -78,7 +76,8 @@ pi -e npm:pi-open-tui
|
|
|
78
76
|
"tokens": true,
|
|
79
77
|
"cost": true,
|
|
80
78
|
"extensionStatuses": true,
|
|
81
|
-
"capitalizeProviderName": true
|
|
79
|
+
"capitalizeProviderName": true,
|
|
80
|
+
"nerdIconSpacing": false
|
|
82
81
|
},
|
|
83
82
|
"telemetry": {
|
|
84
83
|
"enabled": true,
|
|
@@ -102,19 +101,17 @@ pi -e npm:pi-open-tui
|
|
|
102
101
|
| `settingsLanguage` | `en`、`zh` | 切换 `/open-tui` 设置界面的语言 |
|
|
103
102
|
| `inlineFooter` | `true`、`false` | 将两条主要 Footer 信息行移入编辑器上下边框以节省垂直空间,默认关闭;扩展状态行仍显示在编辑器外 |
|
|
104
103
|
| `cursorStyle` | `block`、`bar`、`underline` | `bar` 和 `underline` 需要终端支持光标形状转义序列 |
|
|
105
|
-
| `fullscreen.wheelScrollLines` | `1`-`10` | 全屏模式下滚轮每格滚动的行数,默认值为 `4`;`/open-tui` 中在该项上按 Enter 后直接输入数字(超出范围会自动钳制到 `1`-`10`) |
|
|
106
104
|
| `icons.mode` | `auto`、`nerd`、`unicode`、`ascii` | 控制底栏和遥测通知使用的图标 |
|
|
107
105
|
| `footerSegments` | 布尔开关 | 分别控制底栏中的各项数据 |
|
|
108
106
|
| `footerSegments.capitalizeProviderName` | 布尔开关 | 将底栏中提供商名称的首字母大写;设为 `false` 时保留原始大小写 |
|
|
107
|
+
| `footerSegments.nerdIconSpacing` | 布尔开关 | 在底栏 Nerd Font 图标后额外留一格空白;默认 `false` |
|
|
109
108
|
| `telemetry` | 布尔开关 | 控制遥测总开关和各项指标 |
|
|
110
109
|
| `thinkingPeek.lines` | `0`、`1`、`2` | 关闭、单行或双行思考预览 |
|
|
111
110
|
|
|
112
|
-
`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 主题色显示。
|
|
113
112
|
|
|
114
113
|
开启 `inlineFooter` 后,两条常规 Footer 信息行会移入编辑器边框,从而节省垂直空间。顶部边框左侧显示 Git 分支,右侧信息组以当前目录开头;开启 `sessionName` 时,会话标题也会显示在左侧。Header 和扩展状态行保持独立显示;终端较窄时优先截断低优先级 Footer 数据,保留右侧统计信息和边框角。
|
|
115
114
|
|
|
116
|
-
全屏滚轮速度通过隔离的兼容层写入 Pi 0.84.2 的运行时字段,因为 Pi 尚未提供公开 setter。若后续 Pi 版本不再包含兼容字段,该设置会被忽略并继续使用 Pi 的默认滚动行为。
|
|
117
|
-
|
|
118
115
|
## 单轮遥测
|
|
119
116
|
|
|
120
117
|
每次 Agent 完整运行结束后,pi-open-tui 会显示一条临时结果,并将其中的多个工具调用轮次合并统计:
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import {
|
|
5
|
-
DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
|
|
6
|
-
normalizeFullscreenWheelScrollLines,
|
|
7
|
-
} from "./fullscreen-scroll.ts";
|
|
8
4
|
import type { IconMode } from "./icons.ts";
|
|
9
5
|
|
|
10
6
|
export type SettingsLanguage = "en" | "zh";
|
|
@@ -26,6 +22,7 @@ export interface FooterSegments {
|
|
|
26
22
|
cost: boolean;
|
|
27
23
|
extensionStatuses: boolean;
|
|
28
24
|
capitalizeProviderName: boolean;
|
|
25
|
+
nerdIconSpacing: boolean;
|
|
29
26
|
}
|
|
30
27
|
|
|
31
28
|
export interface TelemetryConfig {
|
|
@@ -42,16 +39,11 @@ export interface ThinkingPeekConfig {
|
|
|
42
39
|
lines: ThinkingPeekLines;
|
|
43
40
|
}
|
|
44
41
|
|
|
45
|
-
export interface FullscreenConfig {
|
|
46
|
-
wheelScrollLines: number;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
42
|
export interface OpenTuiConfig {
|
|
50
43
|
enabled: boolean;
|
|
51
44
|
inlineFooter: boolean;
|
|
52
45
|
settingsLanguage: SettingsLanguage;
|
|
53
46
|
cursorStyle: CursorStyle;
|
|
54
|
-
fullscreen: FullscreenConfig;
|
|
55
47
|
icons: {
|
|
56
48
|
mode: IconMode;
|
|
57
49
|
};
|
|
@@ -65,9 +57,6 @@ export const DEFAULT_CONFIG: OpenTuiConfig = {
|
|
|
65
57
|
inlineFooter: false,
|
|
66
58
|
settingsLanguage: "en",
|
|
67
59
|
cursorStyle: "block",
|
|
68
|
-
fullscreen: {
|
|
69
|
-
wheelScrollLines: DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
|
|
70
|
-
},
|
|
71
60
|
icons: {
|
|
72
61
|
mode: "auto",
|
|
73
62
|
},
|
|
@@ -84,6 +73,7 @@ export const DEFAULT_CONFIG: OpenTuiConfig = {
|
|
|
84
73
|
cost: true,
|
|
85
74
|
extensionStatuses: true,
|
|
86
75
|
capitalizeProviderName: true,
|
|
76
|
+
nerdIconSpacing: false,
|
|
87
77
|
},
|
|
88
78
|
telemetry: {
|
|
89
79
|
enabled: true,
|
|
@@ -108,6 +98,18 @@ function normalizeThinkingPeekLines(value: unknown): ThinkingPeekLines {
|
|
|
108
98
|
return value === 0 || value === 1 || value === 2 ? value : DEFAULT_CONFIG.thinkingPeek.lines;
|
|
109
99
|
}
|
|
110
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
|
+
|
|
111
113
|
function deepMerge<T>(base: T, override: unknown): T {
|
|
112
114
|
if (typeof base !== "object" || base === null || Array.isArray(base)) {
|
|
113
115
|
return (override as T) ?? base;
|
|
@@ -153,19 +155,23 @@ export function loadConfig(notify?: (msg: string, level: "warning" | "info") =>
|
|
|
153
155
|
const raw = readFileSync(path, "utf8");
|
|
154
156
|
const parsed: unknown = JSON.parse(raw);
|
|
155
157
|
const config = deepMerge(DEFAULT_CONFIG, parsed);
|
|
158
|
+
if (typeof config.enabled !== "boolean") {
|
|
159
|
+
config.enabled = DEFAULT_CONFIG.enabled;
|
|
160
|
+
}
|
|
156
161
|
if (typeof config.inlineFooter !== "boolean") {
|
|
157
162
|
config.inlineFooter = DEFAULT_CONFIG.inlineFooter;
|
|
158
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
|
+
}
|
|
159
169
|
if (config.settingsLanguage !== "en" && config.settingsLanguage !== "zh") {
|
|
160
170
|
config.settingsLanguage = DEFAULT_CONFIG.settingsLanguage;
|
|
161
171
|
}
|
|
162
172
|
if (config.cursorStyle !== "block" && config.cursorStyle !== "bar" && config.cursorStyle !== "underline") {
|
|
163
173
|
config.cursorStyle = DEFAULT_CONFIG.cursorStyle;
|
|
164
174
|
}
|
|
165
|
-
config.fullscreen.wheelScrollLines = normalizeFullscreenWheelScrollLines(
|
|
166
|
-
config.fullscreen.wheelScrollLines,
|
|
167
|
-
DEFAULT_CONFIG.fullscreen.wheelScrollLines,
|
|
168
|
-
);
|
|
169
175
|
if (typeof config.thinkingPeek !== "object" || config.thinkingPeek === null || Array.isArray(config.thinkingPeek)) {
|
|
170
176
|
config.thinkingPeek = structuredClone(DEFAULT_CONFIG.thinkingPeek);
|
|
171
177
|
} else {
|
|
@@ -7,10 +7,6 @@ import {
|
|
|
7
7
|
import type { EditorTheme, TuiMouseEvent, TuiMouseEventResult, TUI } from "@earendil-works/pi-tui";
|
|
8
8
|
import { CURSOR_MARKER, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
9
9
|
import type { CursorStyle } from "./config.ts";
|
|
10
|
-
import {
|
|
11
|
-
applyFullscreenWheelScrollLines,
|
|
12
|
-
DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
|
|
13
|
-
} from "./fullscreen-scroll.ts";
|
|
14
10
|
import { findBottomBorderIndex, isEditorBorderLine, stripAnsi } from "./utils.ts";
|
|
15
11
|
|
|
16
12
|
function fillLine(content: string, width: number): string {
|
|
@@ -316,14 +312,12 @@ export function installEditor(
|
|
|
316
312
|
_pi: ExtensionAPI,
|
|
317
313
|
ctx: ExtensionContext,
|
|
318
314
|
cursorStyle: CursorStyle = "block",
|
|
319
|
-
wheelScrollLines = DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
|
|
320
315
|
inlineFooter?: InlineFooterRenderer,
|
|
321
316
|
) {
|
|
322
317
|
let activeTui: TUI | undefined;
|
|
323
318
|
let activeEditor: OpenTuiEditor | undefined;
|
|
324
319
|
let previousHardwareCursor: boolean | undefined;
|
|
325
320
|
let currentCursorStyle = cursorStyle;
|
|
326
|
-
let currentWheelScrollLines = wheelScrollLines;
|
|
327
321
|
let hiddenThinkingTarget: HiddenThinkingLabelComponent | undefined;
|
|
328
322
|
const getActiveTui = (): TUI => {
|
|
329
323
|
if (!activeTui) throw new Error("Open TUI editor is not mounted");
|
|
@@ -333,7 +327,6 @@ export function installEditor(
|
|
|
333
327
|
ctx.ui.setEditorComponent((tui, editorTheme, keybindings) => {
|
|
334
328
|
activeTui = tui;
|
|
335
329
|
hiddenThinkingTarget = undefined;
|
|
336
|
-
applyFullscreenWheelScrollLines(tui, currentWheelScrollLines);
|
|
337
330
|
previousHardwareCursor = tui.getShowHardwareCursor();
|
|
338
331
|
activeEditor = new OpenTuiEditor(tui, editorTheme, keybindings, currentCursorStyle, inlineFooter);
|
|
339
332
|
return activeEditor;
|
|
@@ -359,10 +352,6 @@ export function installEditor(
|
|
|
359
352
|
currentCursorStyle = nextCursorStyle;
|
|
360
353
|
activeEditor?.setCursorStyle(nextCursorStyle, previousHardwareCursor);
|
|
361
354
|
},
|
|
362
|
-
setWheelScrollLines(nextWheelScrollLines: number): void {
|
|
363
|
-
currentWheelScrollLines = nextWheelScrollLines;
|
|
364
|
-
if (activeTui) applyFullscreenWheelScrollLines(activeTui, currentWheelScrollLines);
|
|
365
|
-
},
|
|
366
355
|
cleanup(): void {
|
|
367
356
|
hiddenThinkingTarget = undefined;
|
|
368
357
|
ctx.ui.setEditorComponent(undefined);
|
|
@@ -16,8 +16,9 @@ import {
|
|
|
16
16
|
formatCwd,
|
|
17
17
|
formatDuration,
|
|
18
18
|
formatInputBreakdown,
|
|
19
|
+
hasAnsiStyles,
|
|
19
20
|
providerColor,
|
|
20
|
-
|
|
21
|
+
sanitizeStatusPreservingStyles,
|
|
21
22
|
stressColor,
|
|
22
23
|
truncateBranch,
|
|
23
24
|
truncatePath,
|
|
@@ -30,6 +31,15 @@ export function shortHostname(hostname: string): string {
|
|
|
30
31
|
return hostname.split(".")[0] ?? "";
|
|
31
32
|
}
|
|
32
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
|
+
|
|
33
43
|
function renderBar(theme: Theme, pct: number, barWidth: number, ascii: boolean): string {
|
|
34
44
|
const filled = Math.max(0, Math.min(barWidth, Math.round((pct / 100) * barWidth)));
|
|
35
45
|
const empty = barWidth - filled;
|
|
@@ -111,9 +121,11 @@ function renderRuntimeSegment(
|
|
|
111
121
|
theme: Theme,
|
|
112
122
|
runtime: RuntimeInfo | null,
|
|
113
123
|
iconMode: OpenTuiConfig["icons"]["mode"],
|
|
124
|
+
spaceIcon: boolean,
|
|
114
125
|
): string {
|
|
115
126
|
if (!runtime) return "";
|
|
116
|
-
const
|
|
127
|
+
const glyph = runtimeSymbol(runtime.name, iconMode);
|
|
128
|
+
const symbol = theme.fg("success", spaceIcon ? spaceFooterIcon(glyph) : glyph);
|
|
117
129
|
const version = runtime.version ? theme.fg("muted", runtime.version) : "";
|
|
118
130
|
const label = [symbol, version].filter(Boolean).join(" ");
|
|
119
131
|
return label;
|
|
@@ -179,6 +191,18 @@ function renderStatsBlock(
|
|
|
179
191
|
return stats.join(` ${theme.fg("dim", "|")} `);
|
|
180
192
|
}
|
|
181
193
|
|
|
194
|
+
const ANSI_RESET = "\x1b[0m";
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Keep the colours an extension applied to its own status text. Plain text stays
|
|
198
|
+
* muted so unstyled statuses keep the quiet footer look. The trailing reset
|
|
199
|
+
* stops an unclosed style from tinting the separator and the next status.
|
|
200
|
+
*/
|
|
201
|
+
function renderStatusText(theme: Theme, status: string): string {
|
|
202
|
+
if (!hasAnsiStyles(status)) return theme.fg("muted", status);
|
|
203
|
+
return `${status}${ANSI_RESET}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
182
206
|
function renderExtensionStatusLines(
|
|
183
207
|
theme: Theme,
|
|
184
208
|
extensionStatuses: ReadonlyMap<string, string>,
|
|
@@ -187,12 +211,12 @@ function renderExtensionStatusLines(
|
|
|
187
211
|
): string[] {
|
|
188
212
|
const statuses = Array.from(extensionStatuses.entries())
|
|
189
213
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
190
|
-
.map(([, text]) =>
|
|
214
|
+
.map(([, text]) => sanitizeStatusPreservingStyles(text))
|
|
191
215
|
.filter((text) => text.length > 0);
|
|
192
216
|
if (statuses.length === 0) return [];
|
|
193
217
|
|
|
194
218
|
const separator = ` ${theme.fg("dim", "|")} `;
|
|
195
|
-
const statusText = statuses.map((status) => theme
|
|
219
|
+
const statusText = statuses.map((status) => renderStatusText(theme, status)).join(separator);
|
|
196
220
|
const line = `${theme.fg("mdLink", glyphs.extensions)} ${statusText}`;
|
|
197
221
|
return wrapTextWithAnsi(line, width);
|
|
198
222
|
}
|
|
@@ -273,8 +297,13 @@ function renderFooterContent(
|
|
|
273
297
|
|
|
274
298
|
const state = getState();
|
|
275
299
|
const config = getConfig();
|
|
276
|
-
const glyphs = resolveGlyphs(config.icons.mode);
|
|
277
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
|
+
}
|
|
278
307
|
const meta = getModelMeta();
|
|
279
308
|
const totals = getUsageTotals(ctx);
|
|
280
309
|
|
|
@@ -327,7 +356,7 @@ function renderFooterContent(
|
|
|
327
356
|
inlineTopLeftParts.push(leftParts.at(-1)!);
|
|
328
357
|
}
|
|
329
358
|
if (segments.runtime) {
|
|
330
|
-
const runtimeSeg = renderRuntimeSegment(theme, state.runtime, config.icons.mode);
|
|
359
|
+
const runtimeSeg = renderRuntimeSegment(theme, state.runtime, config.icons.mode, segments.nerdIconSpacing);
|
|
331
360
|
if (runtimeSeg) {
|
|
332
361
|
leftParts.push({ text: runtimeSeg, priority: 4 });
|
|
333
362
|
inlineTopRightParts.push(leftParts.at(-1)!);
|
|
@@ -150,7 +150,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
150
150
|
pi,
|
|
151
151
|
ctx,
|
|
152
152
|
config.cursorStyle,
|
|
153
|
-
config.fullscreen.wheelScrollLines,
|
|
154
153
|
{
|
|
155
154
|
enabled: () => config.inlineFooter,
|
|
156
155
|
render: footer.renderInline,
|
|
@@ -179,7 +178,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
179
178
|
const scheduleGitRefresh = async (ctx: ExtensionContext) => {
|
|
180
179
|
if (!sessionLifecycle.isCurrent()) return;
|
|
181
180
|
const segs = config.footerSegments;
|
|
182
|
-
|
|
181
|
+
// gitCommit only appends a tag to the gitBranch segment, so it never needs git on its own.
|
|
182
|
+
if (!segs.gitBranch && !segs.gitStatus) {
|
|
183
183
|
state.git = emptyGitStatus();
|
|
184
184
|
requestFooterRender?.();
|
|
185
185
|
return;
|
|
@@ -187,8 +187,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
187
187
|
const generation = sessionLifecycle.currentGeneration();
|
|
188
188
|
const cwd = ctx.cwd;
|
|
189
189
|
const git = await readGitStatus(cwd, {
|
|
190
|
-
readCommit:
|
|
191
|
-
readTag: segs.gitCommit,
|
|
190
|
+
readCommit: segs.gitBranch,
|
|
191
|
+
readTag: segs.gitBranch && segs.gitCommit,
|
|
192
192
|
readCounts: segs.gitStatus,
|
|
193
193
|
});
|
|
194
194
|
if (!sessionLifecycle.isCurrent(generation)) return;
|
|
@@ -385,7 +385,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
385
385
|
getConfig: () => config,
|
|
386
386
|
onConfigChanged: (newConfig) => {
|
|
387
387
|
const cursorStyleChanged = config.cursorStyle !== newConfig.cursorStyle;
|
|
388
|
-
const wheelScrollLinesChanged = config.fullscreen.wheelScrollLines !== newConfig.fullscreen.wheelScrollLines;
|
|
389
388
|
const thinkingPeekLinesChanged = config.thinkingPeek.lines !== newConfig.thinkingPeek.lines;
|
|
390
389
|
saveConfig(newConfig);
|
|
391
390
|
config = newConfig;
|
|
@@ -398,13 +397,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
398
397
|
if (cursorStyleChanged && active && editor) {
|
|
399
398
|
editor.setCursorStyle(newConfig.cursorStyle);
|
|
400
399
|
}
|
|
401
|
-
if (wheelScrollLinesChanged && active && editor) {
|
|
402
|
-
editor.setWheelScrollLines(newConfig.fullscreen.wheelScrollLines);
|
|
403
|
-
}
|
|
404
400
|
if (lastCtx) {
|
|
405
401
|
pendingUiChange = getPendingUiChange(newConfig.enabled, active);
|
|
406
402
|
}
|
|
407
|
-
const gitNeeded = newConfig.footerSegments.gitBranch || newConfig.footerSegments.gitStatus
|
|
403
|
+
const gitNeeded = newConfig.footerSegments.gitBranch || newConfig.footerSegments.gitStatus;
|
|
408
404
|
if (lastCtx && gitNeeded) {
|
|
409
405
|
void scheduleGitRefresh(lastCtx);
|
|
410
406
|
} else {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
3
|
Box,
|
|
4
|
-
Input,
|
|
5
4
|
Key,
|
|
6
5
|
matchesKey,
|
|
7
6
|
SelectList,
|
|
@@ -11,10 +10,6 @@ import {
|
|
|
11
10
|
Text,
|
|
12
11
|
} from "@earendil-works/pi-tui";
|
|
13
12
|
import type { CursorStyle, IconMode, OpenTuiConfig, SettingsLanguage, ThinkingPeekLines } from "./config.ts";
|
|
14
|
-
import {
|
|
15
|
-
DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
|
|
16
|
-
normalizeFullscreenWheelScrollLines,
|
|
17
|
-
} from "./fullscreen-scroll.ts";
|
|
18
13
|
|
|
19
14
|
interface SettingItem {
|
|
20
15
|
id: string;
|
|
@@ -30,13 +25,12 @@ const COPY = {
|
|
|
30
25
|
en: {
|
|
31
26
|
title: "Open TUI Settings",
|
|
32
27
|
tabs: { features: "General", icons: "Appearance", segments: "Footer", telemetry: "Telemetry" },
|
|
33
|
-
hint: "Tab/Shift+Tab/←/→: tabs · ↑/↓: move · Enter/Space: change ·
|
|
28
|
+
hint: "Tab/Shift+Tab/←/→: tabs · ↑/↓: move · Enter/Space: change · Esc/q: close",
|
|
34
29
|
labels: {
|
|
35
30
|
enabled: "Enabled",
|
|
36
|
-
inlineFooter: "
|
|
31
|
+
inlineFooter: "Footer in editor border",
|
|
37
32
|
thinkingPeek: "Thinking peek",
|
|
38
33
|
language: "Language",
|
|
39
|
-
wheelScrollLines: "Mouse wheel speed",
|
|
40
34
|
cursorStyle: "Cursor style",
|
|
41
35
|
iconMode: "Icon mode",
|
|
42
36
|
cwd: "CWD",
|
|
@@ -44,39 +38,37 @@ const COPY = {
|
|
|
44
38
|
sessionName: "Session name",
|
|
45
39
|
gitBranch: "Git branch",
|
|
46
40
|
gitStatus: "Git status",
|
|
47
|
-
gitCommit: "Git
|
|
48
|
-
runtime: "
|
|
49
|
-
context: "Context
|
|
50
|
-
tokens: "
|
|
51
|
-
cost: "
|
|
41
|
+
gitCommit: "Git tag (detached HEAD)",
|
|
42
|
+
runtime: "Project runtime",
|
|
43
|
+
context: "Context usage",
|
|
44
|
+
tokens: "Session tokens",
|
|
45
|
+
cost: "Session cost",
|
|
52
46
|
extensionStatuses: "Extension status line",
|
|
53
47
|
capitalizeProviderName: "Capitalize provider name",
|
|
48
|
+
nerdIconSpacing: "Extra space after Nerd icons",
|
|
54
49
|
totalDuration: "Total duration",
|
|
55
|
-
tokenCounts: "
|
|
50
|
+
tokenCounts: "Run tokens",
|
|
56
51
|
stallDetails: "Stall details",
|
|
57
|
-
costRate: "
|
|
52
|
+
costRate: "Price per 1M tokens",
|
|
58
53
|
},
|
|
59
54
|
values: {
|
|
60
55
|
on: "On",
|
|
61
56
|
off: "Off",
|
|
62
57
|
thinkingPeek: { off: "Off", one: "1 line", two: "2 lines" },
|
|
63
58
|
languages: { en: "English", zh: "简体中文" },
|
|
64
|
-
wheelLines: (count: number) => `${count} ${count === 1 ? "line" : "lines"} / notch`,
|
|
65
|
-
wheelPrompt: (count: number) => `Wheel scroll lines per notch, 1-10 (current: ${count}). Enter: apply · Esc: cancel`,
|
|
66
59
|
cursorStyles: { block: "Block", bar: "Bar", underline: "Underline" },
|
|
67
60
|
icons: { auto: "Auto", nerd: "Nerd", unicode: "Unicode", ascii: "ASCII" },
|
|
68
61
|
},
|
|
69
62
|
},
|
|
70
63
|
zh: {
|
|
71
64
|
title: "Open TUI 设置",
|
|
72
|
-
tabs: { features: "常规", icons: "外观", segments: "
|
|
73
|
-
hint: "Tab/Shift+Tab/←/→:切页 · ↑/↓:移动 · Enter/Space:更改 ·
|
|
65
|
+
tabs: { features: "常规", icons: "外观", segments: "底栏", telemetry: "遥测" },
|
|
66
|
+
hint: "Tab/Shift+Tab/←/→:切页 · ↑/↓:移动 · Enter/Space:更改 · Esc/q:关闭",
|
|
74
67
|
labels: {
|
|
75
68
|
enabled: "启用",
|
|
76
|
-
inlineFooter: "
|
|
69
|
+
inlineFooter: "底栏嵌入输入框",
|
|
77
70
|
thinkingPeek: "思考预览",
|
|
78
71
|
language: "语言",
|
|
79
|
-
wheelScrollLines: "鼠标滚轮速度",
|
|
80
72
|
cursorStyle: "光标样式",
|
|
81
73
|
iconMode: "图标模式",
|
|
82
74
|
cwd: "当前目录",
|
|
@@ -84,25 +76,24 @@ const COPY = {
|
|
|
84
76
|
sessionName: "会话名",
|
|
85
77
|
gitBranch: "Git 分支",
|
|
86
78
|
gitStatus: "Git 状态",
|
|
87
|
-
gitCommit: "Git
|
|
88
|
-
runtime: "
|
|
89
|
-
context: "
|
|
90
|
-
tokens: "Token",
|
|
91
|
-
cost: "
|
|
79
|
+
gitCommit: "Git 标签(分离 HEAD)",
|
|
80
|
+
runtime: "项目运行环境",
|
|
81
|
+
context: "上下文用量",
|
|
82
|
+
tokens: "会话 Token",
|
|
83
|
+
cost: "会话费用",
|
|
92
84
|
extensionStatuses: "扩展状态行",
|
|
93
85
|
capitalizeProviderName: "提供商名称首字母大写",
|
|
86
|
+
nerdIconSpacing: "Nerd 图标后额外留空",
|
|
94
87
|
totalDuration: "总耗时",
|
|
95
|
-
tokenCounts: "Token
|
|
88
|
+
tokenCounts: "单次运行 Token",
|
|
96
89
|
stallDetails: "停顿详情",
|
|
97
|
-
costRate: "
|
|
90
|
+
costRate: "每百万 Token 单价",
|
|
98
91
|
},
|
|
99
92
|
values: {
|
|
100
93
|
on: "开启",
|
|
101
94
|
off: "关闭",
|
|
102
95
|
thinkingPeek: { off: "关闭", one: "单行", two: "双行" },
|
|
103
96
|
languages: { en: "English", zh: "简体中文" },
|
|
104
|
-
wheelLines: (count: number) => `每格 ${count} 行`,
|
|
105
|
-
wheelPrompt: (count: number) => `滚轮每格滚动行数(当前 ${count},范围 1-10),输入后 Enter 应用 · Esc 取消`,
|
|
106
97
|
cursorStyles: { block: "块", bar: "竖线", underline: "下划线" },
|
|
107
98
|
icons: { auto: "自动", nerd: "Nerd", unicode: "Unicode", ascii: "ASCII" },
|
|
108
99
|
},
|
|
@@ -148,19 +139,6 @@ function cycleCursorStyle(config: OpenTuiConfig): OpenTuiConfig {
|
|
|
148
139
|
return { ...config, cursorStyle: next };
|
|
149
140
|
}
|
|
150
141
|
|
|
151
|
-
function setWheelScrollLines(config: OpenTuiConfig, raw: string): OpenTuiConfig | undefined {
|
|
152
|
-
if (!/^\d+$/.test(raw)) return undefined;
|
|
153
|
-
const parsed = Number(raw);
|
|
154
|
-
const bounded = Number.isFinite(parsed) ? parsed : Number.MAX_SAFE_INTEGER;
|
|
155
|
-
return {
|
|
156
|
-
...config,
|
|
157
|
-
fullscreen: {
|
|
158
|
-
...config.fullscreen,
|
|
159
|
-
wheelScrollLines: normalizeFullscreenWheelScrollLines(bounded, DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES),
|
|
160
|
-
},
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
142
|
function toggleTelemetry(config: OpenTuiConfig, key: keyof OpenTuiConfig["telemetry"]): OpenTuiConfig {
|
|
165
143
|
return {
|
|
166
144
|
...config,
|
|
@@ -181,11 +159,6 @@ function buildFeaturesItems(config: OpenTuiConfig, copy: SettingsCopy): SettingI
|
|
|
181
159
|
return [
|
|
182
160
|
{ id: "enabled", label: copy.labels.enabled, currentValue: flag(config.enabled) },
|
|
183
161
|
{ id: "settingsLanguage", label: copy.labels.language, currentValue: copy.values.languages[config.settingsLanguage] },
|
|
184
|
-
{
|
|
185
|
-
id: "wheelScrollLines",
|
|
186
|
-
label: copy.labels.wheelScrollLines,
|
|
187
|
-
currentValue: copy.values.wheelLines(config.fullscreen.wheelScrollLines),
|
|
188
|
-
},
|
|
189
162
|
{ id: "thinkingPeek", label: copy.labels.thinkingPeek, currentValue: formatThinkingPeekLines(config.thinkingPeek.lines, copy) },
|
|
190
163
|
{ id: "inlineFooter", label: copy.labels.inlineFooter, currentValue: flag(config.inlineFooter) },
|
|
191
164
|
];
|
|
@@ -214,6 +187,7 @@ function buildSegmentsItems(config: OpenTuiConfig, copy: SettingsCopy): SettingI
|
|
|
214
187
|
{ id: "cost", label: copy.labels.cost, currentValue: flag(segs.cost) },
|
|
215
188
|
{ id: "extensionStatuses", label: copy.labels.extensionStatuses, currentValue: flag(segs.extensionStatuses) },
|
|
216
189
|
{ id: "capitalizeProviderName", label: copy.labels.capitalizeProviderName, currentValue: flag(segs.capitalizeProviderName) },
|
|
190
|
+
{ id: "nerdIconSpacing", label: copy.labels.nerdIconSpacing, currentValue: flag(segs.nerdIconSpacing) },
|
|
217
191
|
];
|
|
218
192
|
}
|
|
219
193
|
|
|
@@ -271,23 +245,6 @@ interface SettingsUiHandle {
|
|
|
271
245
|
handleInput: (data: string) => void;
|
|
272
246
|
}
|
|
273
247
|
|
|
274
|
-
function insertComponentAfter(list: Component, child: Component, index: () => number): Component {
|
|
275
|
-
// SelectList renders one line per item; splice the editor into that output.
|
|
276
|
-
return {
|
|
277
|
-
render(width: number): string[] {
|
|
278
|
-
const lines = list.render(width);
|
|
279
|
-
const insertAt = index();
|
|
280
|
-
if (insertAt < 0 || insertAt >= lines.length) return lines;
|
|
281
|
-
const childLines = child.render(width);
|
|
282
|
-
return [...lines.slice(0, insertAt + 1), ...childLines, ...lines.slice(insertAt + 1)];
|
|
283
|
-
},
|
|
284
|
-
invalidate(): void {
|
|
285
|
-
list.invalidate();
|
|
286
|
-
child.invalidate();
|
|
287
|
-
},
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
248
|
class SettingsUi implements SettingsUiHandle {
|
|
292
249
|
private tab: Tab = "features";
|
|
293
250
|
private config: OpenTuiConfig;
|
|
@@ -300,7 +257,6 @@ class SettingsUi implements SettingsUiHandle {
|
|
|
300
257
|
private cachedWidth: number | undefined;
|
|
301
258
|
private cachedLines: string[] | undefined;
|
|
302
259
|
private compact = false;
|
|
303
|
-
private wheelInput: Input | undefined;
|
|
304
260
|
|
|
305
261
|
constructor(
|
|
306
262
|
theme: Theme,
|
|
@@ -325,35 +281,11 @@ class SettingsUi implements SettingsUiHandle {
|
|
|
325
281
|
|
|
326
282
|
private applySetting(itemId: string): void {
|
|
327
283
|
this.selectedItemByTab[this.tab] = itemId;
|
|
328
|
-
if (this.tab === "features" && itemId === "wheelScrollLines") {
|
|
329
|
-
this.openWheelInput();
|
|
330
|
-
this.invalidate();
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
284
|
this.config = handleSettingChange(this.tab, itemId, this.config);
|
|
334
285
|
this.onChange(this.config);
|
|
335
286
|
this.rebuild(itemId);
|
|
336
287
|
}
|
|
337
288
|
|
|
338
|
-
private openWheelInput(): void {
|
|
339
|
-
const input = new Input();
|
|
340
|
-
input.onSubmit = (value) => {
|
|
341
|
-
const next = setWheelScrollLines(this.config, value);
|
|
342
|
-
this.wheelInput = undefined;
|
|
343
|
-
if (next) {
|
|
344
|
-
this.config = next;
|
|
345
|
-
this.onChange(this.config);
|
|
346
|
-
}
|
|
347
|
-
this.rebuild("wheelScrollLines");
|
|
348
|
-
};
|
|
349
|
-
input.onEscape = () => {
|
|
350
|
-
this.wheelInput = undefined;
|
|
351
|
-
this.rebuild("wheelScrollLines");
|
|
352
|
-
};
|
|
353
|
-
this.wheelInput = input;
|
|
354
|
-
this.rebuild("wheelScrollLines");
|
|
355
|
-
}
|
|
356
|
-
|
|
357
289
|
private switchTab(offset: number): void {
|
|
358
290
|
const idx = TABS.indexOf(this.tab);
|
|
359
291
|
this.tab = TABS[(idx + offset + TABS.length) % TABS.length]!;
|
|
@@ -373,17 +305,11 @@ class SettingsUi implements SettingsUiHandle {
|
|
|
373
305
|
this.container.addChild(new Text(tabBar, 1, 0));
|
|
374
306
|
this.container.addChild(new Text(this.theme.fg("dim", copy.hint), 1, 0));
|
|
375
307
|
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
label: editing
|
|
382
|
-
? (this.compact ? `${item.label}:` : item.label)
|
|
383
|
-
: (this.compact ? `${item.label}: ${item.currentValue}` : item.label),
|
|
384
|
-
description: editing || this.compact ? undefined : item.currentValue,
|
|
385
|
-
} as SelectItem;
|
|
386
|
-
});
|
|
308
|
+
const items = buildItems(this.tab, this.config).map((item) => ({
|
|
309
|
+
value: item.id,
|
|
310
|
+
label: this.compact ? `${item.label}: ${item.currentValue}` : item.label,
|
|
311
|
+
description: this.compact ? undefined : item.currentValue,
|
|
312
|
+
} as SelectItem));
|
|
387
313
|
this.selectList = new SelectList(items, Math.min(items.length, 12), {
|
|
388
314
|
selectedPrefix: (t) => this.theme.fg("accent", t),
|
|
389
315
|
selectedText: (t) => this.theme.fg("accent", t),
|
|
@@ -405,31 +331,13 @@ class SettingsUi implements SettingsUiHandle {
|
|
|
405
331
|
this.selectList.onCancel = () => {
|
|
406
332
|
this.onClose();
|
|
407
333
|
};
|
|
408
|
-
|
|
409
|
-
this.wheelInput!.focused = true;
|
|
410
|
-
const wheelInputGroup = new Box(4, 0);
|
|
411
|
-
wheelInputGroup.addChild(new Text(
|
|
412
|
-
this.theme.fg("muted", copy.values.wheelPrompt(this.config.fullscreen.wheelScrollLines)),
|
|
413
|
-
0,
|
|
414
|
-
0,
|
|
415
|
-
));
|
|
416
|
-
wheelInputGroup.addChild(this.wheelInput!);
|
|
417
|
-
const selectedIndex = () => items.findIndex((item) => item.value === this.selectList.getSelectedItem()?.value);
|
|
418
|
-
this.container.addChild(insertComponentAfter(this.selectList, wheelInputGroup, selectedIndex));
|
|
419
|
-
} else {
|
|
420
|
-
this.container.addChild(this.selectList);
|
|
421
|
-
}
|
|
334
|
+
this.container.addChild(this.selectList);
|
|
422
335
|
|
|
423
336
|
this.cachedWidth = undefined;
|
|
424
337
|
this.cachedLines = undefined;
|
|
425
338
|
}
|
|
426
339
|
|
|
427
340
|
handleInput(data: string): void {
|
|
428
|
-
if (this.wheelInput && this.tab === "features") {
|
|
429
|
-
this.wheelInput.handleInput(data);
|
|
430
|
-
this.invalidate();
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
341
|
if (matchesKey(data, Key.tab) || matchesKey(data, Key.right)) {
|
|
434
342
|
this.switchTab(1);
|
|
435
343
|
this.invalidate();
|
|
@@ -296,6 +296,28 @@ export function sanitizeStatus(text: string): string {
|
|
|
296
296
|
.trim();
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
+
// One status token is an SGR sequence, another escape sequence, or one control
|
|
300
|
+
// character. SGR carries colours. Everything else moves the cursor or rewrites
|
|
301
|
+
// the terminal, so it must never reach the footer row.
|
|
302
|
+
const STATUS_TOKEN = /(?:\x1b\[|\x9b)[0-?]*[ -/]*m|(?:\x1b\[|\x9b)[0-?]*[ -/]*[@-~]|(?:\x1bP|\x90|\x1bX|\x98|\x1b\^|\x9e|\x1b_|\x9f)[\s\S]*?(?:\x1b\\|\x9c|$)|(?:\x1b\]|\x9d)[\s\S]*?(?:\x07|\x1b\\|\x9c|$)|\x1b[@-Z\\_]|[\u0000-\u001f\u007f-\u009f]/g;
|
|
303
|
+
const SGR_TOKEN = /^(?:\x1b\[|\x9b)[0-9;:]*m$/;
|
|
304
|
+
const SGR_SEQUENCE = /(?:\x1b\[|\x9b)[0-9;:]*m/;
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Sanitize a status text the same way {@link sanitizeStatus} does, but keep the
|
|
308
|
+
* SGR sequences the extension set. Extensions colour their own status text with
|
|
309
|
+
* `ctx.ui.theme.fg()`. Stripping those codes renders every status grey.
|
|
310
|
+
*/
|
|
311
|
+
export function sanitizeStatusPreservingStyles(text: string): string {
|
|
312
|
+
const cleaned = text.replace(STATUS_TOKEN, (token) => (SGR_TOKEN.test(token) ? token : " "));
|
|
313
|
+
return cleaned.replace(/ {2,}/g, " ").trim();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** True when the text carries SGR colour or style sequences. */
|
|
317
|
+
export function hasAnsiStyles(text: string): boolean {
|
|
318
|
+
return SGR_SEQUENCE.test(text);
|
|
319
|
+
}
|
|
320
|
+
|
|
299
321
|
export function formatThinkingLabel(level: string): string {
|
|
300
322
|
if (level === "off") return "thinking off";
|
|
301
323
|
return `${level} effort`;
|
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": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"LICENSE"
|
|
26
26
|
],
|
|
27
27
|
"scripts": {
|
|
28
|
-
"test": "node --test tests/settings-command.test.ts tests/
|
|
28
|
+
"test": "node --test tests/settings-command.test.ts tests/footer.test.ts tests/editor.test.ts tests/telemetry.test.ts tests/peek.test.ts",
|
|
29
29
|
"typecheck": "tsc --noEmit"
|
|
30
30
|
},
|
|
31
31
|
"pi": {
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"image": "https://raw.githubusercontent.com/OldSuns/pi-open-tui/main/assets/preview_dashboard_1.png"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
39
|
-
"@earendil-works/pi-ai": ">=0.
|
|
40
|
-
"@earendil-works/pi-tui": ">=0.
|
|
38
|
+
"@earendil-works/pi-coding-agent": ">=1.0.0",
|
|
39
|
+
"@earendil-works/pi-ai": ">=1.0.0",
|
|
40
|
+
"@earendil-works/pi-tui": ">=1.0.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependenciesMeta": {
|
|
43
43
|
"@earendil-works/pi-coding-agent": {
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@earendil-works/pi-coding-agent": "
|
|
55
|
-
"@earendil-works/pi-ai": "
|
|
56
|
-
"@earendil-works/pi-tui": "
|
|
54
|
+
"@earendil-works/pi-coding-agent": "^1.0.0",
|
|
55
|
+
"@earendil-works/pi-ai": "^1.0.0",
|
|
56
|
+
"@earendil-works/pi-tui": "^1.0.0",
|
|
57
57
|
"@types/node": "^22.0.0",
|
|
58
58
|
"typescript": "^5.5.0"
|
|
59
59
|
},
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import type { TUI } from "@earendil-works/pi-tui";
|
|
2
|
-
|
|
3
|
-
export const MIN_FULLSCREEN_WHEEL_SCROLL_LINES = 1;
|
|
4
|
-
export const MAX_FULLSCREEN_WHEEL_SCROLL_LINES = 10;
|
|
5
|
-
export const DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES = 4;
|
|
6
|
-
|
|
7
|
-
export function normalizeFullscreenWheelScrollLines(
|
|
8
|
-
value: unknown,
|
|
9
|
-
fallback = DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
|
|
10
|
-
): number {
|
|
11
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
12
|
-
return Math.min(
|
|
13
|
-
MAX_FULLSCREEN_WHEEL_SCROLL_LINES,
|
|
14
|
-
Math.max(MIN_FULLSCREEN_WHEEL_SCROLL_LINES, Math.floor(value)),
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
type FullscreenWheelTui = TUI & { wheelScrollLines?: unknown };
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Pi 0.84.2 keeps this constructor option in a private runtime field. Remove
|
|
22
|
-
* this shim once Pi exposes a public setter; incompatible versions are no-ops.
|
|
23
|
-
*/
|
|
24
|
-
export function applyFullscreenWheelScrollLines(tui: TUI, value: number): boolean {
|
|
25
|
-
try {
|
|
26
|
-
const fullscreenTui = tui as FullscreenWheelTui;
|
|
27
|
-
if (fullscreenTui.mode !== "fullscreen" || typeof fullscreenTui.wheelScrollLines !== "number") {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
return Reflect.set(fullscreenTui, "wheelScrollLines", normalizeFullscreenWheelScrollLines(value));
|
|
31
|
-
} catch {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
}
|