pi-open-tui 0.3.9 → 0.3.11
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 -13
- package/README.zh-CN.md +7 -13
- package/extensions/open-tui/config.ts +0 -16
- package/extensions/open-tui/editor.ts +0 -11
- package/extensions/open-tui/footer.ts +16 -3
- package/extensions/open-tui/icons.ts +54 -4
- package/extensions/open-tui/index.ts +0 -5
- package/extensions/open-tui/settings-command.ts +11 -106
- 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,13 +15,12 @@ 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
|
-
- A [Nerd Font](https://www.nerdfonts.com/font-downloads) for the full icon set (optional;
|
|
23
|
+
- A [Nerd Font](https://www.nerdfonts.com/font-downloads) for the full icon set (optional; portable Unicode icons are built in)
|
|
25
24
|
|
|
26
25
|
## Install
|
|
27
26
|
|
|
@@ -44,10 +43,11 @@ Download any patched font from the official [Nerd Fonts downloads page](https://
|
|
|
44
43
|
The default `auto` mode checks the terminal environment, not the installed font file. It uses Nerd Font icons in interactive UTF-8 TTYs, including terminals running through a runner or subshell. If icons appear as boxes or incorrect symbols, open `/open-tui` and choose one of these modes under **Appearance**:
|
|
45
44
|
|
|
46
45
|
- `nerd`: force Nerd Font icons after configuring a Nerd Font in the terminal
|
|
46
|
+
- `unicode`: portable Unicode icons (folder, branch, laptop, bulb, plug, hourglass, ...) that render without a patched font; emoji glyphs use the terminal's emoji fallback
|
|
47
47
|
- `ascii`: use plain-text icons with no patched font required
|
|
48
|
-
- `auto`: use Nerd Font icons in interactive UTF-8 TTYs; use ASCII for non-interactive output, `TERM=dumb`, or an explicitly non-UTF-8 locale
|
|
48
|
+
- `auto`: use Nerd Font icons in interactive UTF-8 TTYs; use portable Unicode icons for SSH sessions (the client terminal controls the font and usually has no Nerd Font); use ASCII for non-interactive output, `TERM=dumb`, or an explicitly non-UTF-8 locale
|
|
49
49
|
|
|
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.
|
|
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
52
|
## Configuration
|
|
53
53
|
|
|
@@ -59,9 +59,6 @@ Run `/open-tui` to open the settings dialog. It provides **General**, **Appearan
|
|
|
59
59
|
"inlineFooter": false,
|
|
60
60
|
"settingsLanguage": "en",
|
|
61
61
|
"cursorStyle": "block",
|
|
62
|
-
"fullscreen": {
|
|
63
|
-
"wheelScrollLines": 4
|
|
64
|
-
},
|
|
65
62
|
"icons": {
|
|
66
63
|
"mode": "auto"
|
|
67
64
|
},
|
|
@@ -101,19 +98,16 @@ Key options:
|
|
|
101
98
|
| `settingsLanguage` | `en`, `zh` | Changes the `/open-tui` interface language |
|
|
102
99
|
| `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 |
|
|
103
100
|
| `cursorStyle` | `block`, `bar`, `underline` | `bar` and `underline` require terminal cursor-shape support |
|
|
104
|
-
| `
|
|
105
|
-
| `icons.mode` | `auto`, `nerd`, `ascii` | Controls footer and telemetry icons |
|
|
101
|
+
| `icons.mode` | `auto`, `nerd`, `unicode`, `ascii` | Controls footer and telemetry icons |
|
|
106
102
|
| `footerSegments` | Boolean flags | Shows or hides individual footer data |
|
|
107
103
|
| `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 |
|
|
108
104
|
| `telemetry` | Boolean flags | Enables telemetry and its individual measurements |
|
|
109
105
|
| `thinkingPeek.lines` | `0`, `1`, `2` | Off, one-line, or two-line hidden thinking preview |
|
|
110
106
|
|
|
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. `gitCommit` shows the short hash and tag in detached HEAD state. Disabling `extensionStatuses` hides the entire extension status line, including MCP status.
|
|
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. `gitCommit` shows the short hash and tag in detached HEAD state. 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.
|
|
112
108
|
|
|
113
109
|
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.
|
|
114
110
|
|
|
115
|
-
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.
|
|
116
|
-
|
|
117
111
|
## Turn telemetry
|
|
118
112
|
|
|
119
113
|
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,13 +15,12 @@
|
|
|
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
|
-
- 使用完整图标集时需要 [Nerd Font](https://www.nerdfonts.com/font-downloads)
|
|
23
|
+
- 使用完整图标集时需要 [Nerd Font](https://www.nerdfonts.com/font-downloads)(可选;内置可移植 Unicode 图标)
|
|
25
24
|
|
|
26
25
|
## 安装
|
|
27
26
|
|
|
@@ -44,10 +43,11 @@ pi -e npm:pi-open-tui
|
|
|
44
43
|
默认的 `auto` 模式检查终端环境,而不是已安装的字体文件。在支持 UTF-8 的交互式 TTY 中,它会使用 Nerd Font 图标,即使 Pi 运行在 runner 或子 shell 中。如果图标显示为方框、乱码或错误符号,请打开 `/open-tui`,在**外观**页选择合适的模式:
|
|
45
44
|
|
|
46
45
|
- `nerd`:终端已配置 Nerd Font 时,强制使用 Nerd Font 图标
|
|
46
|
+
- `unicode`:可移植 Unicode 图标(文件夹、分支、笔记本电脑、灯泡、插头、沙漏等),无需修补字体;emoji 字形通过终端的 emoji 回退渲染
|
|
47
47
|
- `ascii`:使用纯文本图标,无需安装修补字体
|
|
48
|
-
- `auto`:在支持 UTF-8 的交互式 TTY 中使用 Nerd Font
|
|
48
|
+
- `auto`:在支持 UTF-8 的交互式 TTY 中使用 Nerd Font 图标;SSH 会话使用可移植 Unicode 图标(字体由客户端终端决定,通常没有 Nerd Font);非交互式输出、`TERM=dumb` 或明确配置为非 UTF-8 的 locale 使用 ASCII
|
|
49
49
|
|
|
50
|
-
如果已经安装字体,但 `auto` 仍选择 ASCII,请手动切换为 `nerd`。使用 VS Code、Windows Terminal
|
|
50
|
+
如果已经安装字体,但 `auto` 仍选择 ASCII,请手动切换为 `nerd`。使用 VS Code、Windows Terminal 等应用时,只在操作系统中安装字体还不够,还需要在对应的终端配置中选中该字体。如果 SSH 客户端终端确实安装了 Nerd Font 并希望在 SSH 下使用完整图标,请将模式显式设为 `nerd`。
|
|
51
51
|
|
|
52
52
|
## 配置
|
|
53
53
|
|
|
@@ -59,9 +59,6 @@ pi -e npm:pi-open-tui
|
|
|
59
59
|
"inlineFooter": false,
|
|
60
60
|
"settingsLanguage": "zh",
|
|
61
61
|
"cursorStyle": "block",
|
|
62
|
-
"fullscreen": {
|
|
63
|
-
"wheelScrollLines": 4
|
|
64
|
-
},
|
|
65
62
|
"icons": {
|
|
66
63
|
"mode": "auto"
|
|
67
64
|
},
|
|
@@ -101,19 +98,16 @@ pi -e npm:pi-open-tui
|
|
|
101
98
|
| `settingsLanguage` | `en`、`zh` | 切换 `/open-tui` 设置界面的语言 |
|
|
102
99
|
| `inlineFooter` | `true`、`false` | 将两条主要 Footer 信息行移入编辑器上下边框以节省垂直空间,默认关闭;扩展状态行仍显示在编辑器外 |
|
|
103
100
|
| `cursorStyle` | `block`、`bar`、`underline` | `bar` 和 `underline` 需要终端支持光标形状转义序列 |
|
|
104
|
-
| `
|
|
105
|
-
| `icons.mode` | `auto`、`nerd`、`ascii` | 控制底栏和遥测通知使用的图标 |
|
|
101
|
+
| `icons.mode` | `auto`、`nerd`、`unicode`、`ascii` | 控制底栏和遥测通知使用的图标 |
|
|
106
102
|
| `footerSegments` | 布尔开关 | 分别控制底栏中的各项数据 |
|
|
107
103
|
| `footerSegments.capitalizeProviderName` | 布尔开关 | 将底栏中提供商名称的首字母大写;设为 `false` 时保留原始大小写 |
|
|
108
104
|
| `telemetry` | 布尔开关 | 控制遥测总开关和各项指标 |
|
|
109
105
|
| `thinkingPeek.lines` | `0`、`1`、`2` | 关闭、单行或双行思考预览 |
|
|
110
106
|
|
|
111
|
-
`sessionName` 仅在会话有名称时显示;`hostname` 会显示主机名的短名称(主机名的第一个标签,例如从 `mba.example.com` 显示为 `mba`),并使用服务器图标;`gitCommit` 会在 detached HEAD 状态下显示短哈希和标签;关闭 `extensionStatuses` 会隐藏整行扩展状态,其中也包括 MCP
|
|
107
|
+
`sessionName` 仅在会话有名称时显示;`hostname` 会显示主机名的短名称(主机名的第一个标签,例如从 `mba.example.com` 显示为 `mba`),并使用服务器图标;`gitCommit` 会在 detached HEAD 状态下显示短哈希和标签;关闭 `extensionStatuses` 会隐藏整行扩展状态,其中也包括 MCP 状态。每条状态会保留其扩展通过 `ctx.ui.theme.fg()` 设置的颜色;未设置颜色的状态使用 muted 主题色显示。
|
|
112
108
|
|
|
113
109
|
开启 `inlineFooter` 后,两条常规 Footer 信息行会移入编辑器边框,从而节省垂直空间。顶部边框左侧显示 Git 分支,右侧信息组以当前目录开头;开启 `sessionName` 时,会话标题也会显示在左侧。Header 和扩展状态行保持独立显示;终端较窄时优先截断低优先级 Footer 数据,保留右侧统计信息和边框角。
|
|
114
110
|
|
|
115
|
-
全屏滚轮速度通过隔离的兼容层写入 Pi 0.84.2 的运行时字段,因为 Pi 尚未提供公开 setter。若后续 Pi 版本不再包含兼容字段,该设置会被忽略并继续使用 Pi 的默认滚动行为。
|
|
116
|
-
|
|
117
111
|
## 单轮遥测
|
|
118
112
|
|
|
119
113
|
每次 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";
|
|
@@ -42,16 +38,11 @@ export interface ThinkingPeekConfig {
|
|
|
42
38
|
lines: ThinkingPeekLines;
|
|
43
39
|
}
|
|
44
40
|
|
|
45
|
-
export interface FullscreenConfig {
|
|
46
|
-
wheelScrollLines: number;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
41
|
export interface OpenTuiConfig {
|
|
50
42
|
enabled: boolean;
|
|
51
43
|
inlineFooter: boolean;
|
|
52
44
|
settingsLanguage: SettingsLanguage;
|
|
53
45
|
cursorStyle: CursorStyle;
|
|
54
|
-
fullscreen: FullscreenConfig;
|
|
55
46
|
icons: {
|
|
56
47
|
mode: IconMode;
|
|
57
48
|
};
|
|
@@ -65,9 +56,6 @@ export const DEFAULT_CONFIG: OpenTuiConfig = {
|
|
|
65
56
|
inlineFooter: false,
|
|
66
57
|
settingsLanguage: "en",
|
|
67
58
|
cursorStyle: "block",
|
|
68
|
-
fullscreen: {
|
|
69
|
-
wheelScrollLines: DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
|
|
70
|
-
},
|
|
71
59
|
icons: {
|
|
72
60
|
mode: "auto",
|
|
73
61
|
},
|
|
@@ -162,10 +150,6 @@ export function loadConfig(notify?: (msg: string, level: "warning" | "info") =>
|
|
|
162
150
|
if (config.cursorStyle !== "block" && config.cursorStyle !== "bar" && config.cursorStyle !== "underline") {
|
|
163
151
|
config.cursorStyle = DEFAULT_CONFIG.cursorStyle;
|
|
164
152
|
}
|
|
165
|
-
config.fullscreen.wheelScrollLines = normalizeFullscreenWheelScrollLines(
|
|
166
|
-
config.fullscreen.wheelScrollLines,
|
|
167
|
-
DEFAULT_CONFIG.fullscreen.wheelScrollLines,
|
|
168
|
-
);
|
|
169
153
|
if (typeof config.thinkingPeek !== "object" || config.thinkingPeek === null || Array.isArray(config.thinkingPeek)) {
|
|
170
154
|
config.thinkingPeek = structuredClone(DEFAULT_CONFIG.thinkingPeek);
|
|
171
155
|
} 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,
|
|
@@ -179,6 +180,18 @@ function renderStatsBlock(
|
|
|
179
180
|
return stats.join(` ${theme.fg("dim", "|")} `);
|
|
180
181
|
}
|
|
181
182
|
|
|
183
|
+
const ANSI_RESET = "\x1b[0m";
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Keep the colours an extension applied to its own status text. Plain text stays
|
|
187
|
+
* muted so unstyled statuses keep the quiet footer look. The trailing reset
|
|
188
|
+
* stops an unclosed style from tinting the separator and the next status.
|
|
189
|
+
*/
|
|
190
|
+
function renderStatusText(theme: Theme, status: string): string {
|
|
191
|
+
if (!hasAnsiStyles(status)) return theme.fg("muted", status);
|
|
192
|
+
return `${status}${ANSI_RESET}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
182
195
|
function renderExtensionStatusLines(
|
|
183
196
|
theme: Theme,
|
|
184
197
|
extensionStatuses: ReadonlyMap<string, string>,
|
|
@@ -187,12 +200,12 @@ function renderExtensionStatusLines(
|
|
|
187
200
|
): string[] {
|
|
188
201
|
const statuses = Array.from(extensionStatuses.entries())
|
|
189
202
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
190
|
-
.map(([, text]) =>
|
|
203
|
+
.map(([, text]) => sanitizeStatusPreservingStyles(text))
|
|
191
204
|
.filter((text) => text.length > 0);
|
|
192
205
|
if (statuses.length === 0) return [];
|
|
193
206
|
|
|
194
207
|
const separator = ` ${theme.fg("dim", "|")} `;
|
|
195
|
-
const statusText = statuses.map((status) => theme
|
|
208
|
+
const statusText = statuses.map((status) => renderStatusText(theme, status)).join(separator);
|
|
196
209
|
const line = `${theme.fg("mdLink", glyphs.extensions)} ${statusText}`;
|
|
197
210
|
return wrapTextWithAnsi(line, width);
|
|
198
211
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type IconMode = "auto" | "nerd" | "ascii";
|
|
1
|
+
export type IconMode = "auto" | "nerd" | "unicode" | "ascii";
|
|
2
2
|
|
|
3
3
|
export interface IconGlyphs {
|
|
4
4
|
cwd: string;
|
|
@@ -94,6 +94,47 @@ const ASCII_GLYPHS: IconGlyphs = {
|
|
|
94
94
|
deleted: "x",
|
|
95
95
|
};
|
|
96
96
|
|
|
97
|
+
// Portable Unicode icon set for terminals without a Nerd Font. Keeps the
|
|
98
|
+
// footer icon-like instead of dropping to letters: emoji glyphs (folder,
|
|
99
|
+
// branch, laptop, bulb, floppy, plug, hourglass, bolt) render through the
|
|
100
|
+
// client terminal's emoji fallback at 2 columns, which matches
|
|
101
|
+
// pi-tui's visibleWidth for RGI emoji; the remaining symbols are
|
|
102
|
+
// single-width and covered by DejaVu Sans Mono, JetBrains Mono, Noto Sans
|
|
103
|
+
// Mono and Liberation Mono. Glyph choices are easy to adjust per slot.
|
|
104
|
+
const UNICODE_GLYPHS: IconGlyphs = {
|
|
105
|
+
cwd: "📁",
|
|
106
|
+
host: "⌂",
|
|
107
|
+
session: "🔖",
|
|
108
|
+
git: "🌿",
|
|
109
|
+
working: "◷",
|
|
110
|
+
done: "✓",
|
|
111
|
+
context: "≡",
|
|
112
|
+
model: "💻",
|
|
113
|
+
thinking: "💡",
|
|
114
|
+
input: "↑",
|
|
115
|
+
output: "↓",
|
|
116
|
+
cacheHit: "💾",
|
|
117
|
+
cost: "$",
|
|
118
|
+
speed: "⚡",
|
|
119
|
+
latency: "⌛",
|
|
120
|
+
stall: "⚠",
|
|
121
|
+
extensions: "🔌",
|
|
122
|
+
ahead: "↑",
|
|
123
|
+
behind: "↓",
|
|
124
|
+
diverged: "↕",
|
|
125
|
+
conflicted: "=",
|
|
126
|
+
stashed: "$",
|
|
127
|
+
modified: "!",
|
|
128
|
+
staged: "+",
|
|
129
|
+
untracked: "?",
|
|
130
|
+
renamed: "→",
|
|
131
|
+
deleted: "✗",
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
function isSshSession(): boolean {
|
|
135
|
+
return Boolean(process.env.SSH_TTY || process.env.SSH_CONNECTION);
|
|
136
|
+
}
|
|
137
|
+
|
|
97
138
|
export function detectNerdFont(): boolean {
|
|
98
139
|
// TTY and locale checks are the only reliable signals available here. The
|
|
99
140
|
// terminal emulator owns font selection, so auto mode is optimistic once
|
|
@@ -103,14 +144,23 @@ export function detectNerdFont(): boolean {
|
|
|
103
144
|
return locale === undefined || /utf-?8/i.test(locale);
|
|
104
145
|
}
|
|
105
146
|
|
|
106
|
-
export function resolveIconMode(mode: IconMode): "nerd" | "ascii" {
|
|
147
|
+
export function resolveIconMode(mode: IconMode): "nerd" | "unicode" | "ascii" {
|
|
107
148
|
if (mode === "nerd") return "nerd";
|
|
108
149
|
if (mode === "ascii") return "ascii";
|
|
109
|
-
|
|
150
|
+
if (mode === "unicode") return "unicode";
|
|
151
|
+
if (!detectNerdFont()) return "ascii";
|
|
152
|
+
// Auto cannot know whether the terminal that renders this session ships a
|
|
153
|
+
// Nerd Font (see #38). Local terminals usually do; SSH clients usually do
|
|
154
|
+
// not, because the font lives on the client device, so portable Unicode
|
|
155
|
+
// symbols are the safer default there. Terminals that do have a Nerd Font
|
|
156
|
+
// can opt back in with an explicit icons.mode of "nerd".
|
|
157
|
+
if (isSshSession()) return "unicode";
|
|
158
|
+
return "nerd";
|
|
110
159
|
}
|
|
111
160
|
|
|
112
161
|
export function resolveGlyphs(mode: IconMode): IconGlyphs {
|
|
113
162
|
const resolved = resolveIconMode(mode);
|
|
163
|
+
if (resolved === "unicode") return UNICODE_GLYPHS;
|
|
114
164
|
return resolved === "nerd" ? NERD_GLYPHS : ASCII_GLYPHS;
|
|
115
165
|
}
|
|
116
166
|
|
|
@@ -185,6 +235,6 @@ const RUNTIME_ASCII_SYMBOLS: Record<string, string> = {
|
|
|
185
235
|
};
|
|
186
236
|
|
|
187
237
|
export function runtimeSymbol(name: string, mode: IconMode): string {
|
|
188
|
-
if (resolveIconMode(mode)
|
|
238
|
+
if (resolveIconMode(mode) !== "nerd") return RUNTIME_ASCII_SYMBOLS[name] ?? name;
|
|
189
239
|
return RUNTIME_SYMBOLS[name] ?? "";
|
|
190
240
|
}
|
|
@@ -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,
|
|
@@ -385,7 +384,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
385
384
|
getConfig: () => config,
|
|
386
385
|
onConfigChanged: (newConfig) => {
|
|
387
386
|
const cursorStyleChanged = config.cursorStyle !== newConfig.cursorStyle;
|
|
388
|
-
const wheelScrollLinesChanged = config.fullscreen.wheelScrollLines !== newConfig.fullscreen.wheelScrollLines;
|
|
389
387
|
const thinkingPeekLinesChanged = config.thinkingPeek.lines !== newConfig.thinkingPeek.lines;
|
|
390
388
|
saveConfig(newConfig);
|
|
391
389
|
config = newConfig;
|
|
@@ -398,9 +396,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
398
396
|
if (cursorStyleChanged && active && editor) {
|
|
399
397
|
editor.setCursorStyle(newConfig.cursorStyle);
|
|
400
398
|
}
|
|
401
|
-
if (wheelScrollLinesChanged && active && editor) {
|
|
402
|
-
editor.setWheelScrollLines(newConfig.fullscreen.wheelScrollLines);
|
|
403
|
-
}
|
|
404
399
|
if (lastCtx) {
|
|
405
400
|
pendingUiChange = getPendingUiChange(newConfig.enabled, active);
|
|
406
401
|
}
|
|
@@ -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
31
|
inlineFooter: "Inline footer",
|
|
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",
|
|
@@ -61,22 +55,19 @@ const COPY = {
|
|
|
61
55
|
off: "Off",
|
|
62
56
|
thinkingPeek: { off: "Off", one: "1 line", two: "2 lines" },
|
|
63
57
|
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
58
|
cursorStyles: { block: "Block", bar: "Bar", underline: "Underline" },
|
|
67
|
-
icons: { auto: "Auto", nerd: "Nerd", ascii: "ASCII" },
|
|
59
|
+
icons: { auto: "Auto", nerd: "Nerd", unicode: "Unicode", ascii: "ASCII" },
|
|
68
60
|
},
|
|
69
61
|
},
|
|
70
62
|
zh: {
|
|
71
63
|
title: "Open TUI 设置",
|
|
72
64
|
tabs: { features: "常规", icons: "外观", segments: "Footer", telemetry: "遥测" },
|
|
73
|
-
hint: "Tab/Shift+Tab/←/→:切页 · ↑/↓:移动 · Enter/Space:更改 ·
|
|
65
|
+
hint: "Tab/Shift+Tab/←/→:切页 · ↑/↓:移动 · Enter/Space:更改 · Esc/q:关闭",
|
|
74
66
|
labels: {
|
|
75
67
|
enabled: "启用",
|
|
76
68
|
inlineFooter: "内联底栏",
|
|
77
69
|
thinkingPeek: "思考预览",
|
|
78
70
|
language: "语言",
|
|
79
|
-
wheelScrollLines: "鼠标滚轮速度",
|
|
80
71
|
cursorStyle: "光标样式",
|
|
81
72
|
iconMode: "图标模式",
|
|
82
73
|
cwd: "当前目录",
|
|
@@ -101,10 +92,8 @@ const COPY = {
|
|
|
101
92
|
off: "关闭",
|
|
102
93
|
thinkingPeek: { off: "关闭", one: "单行", two: "双行" },
|
|
103
94
|
languages: { en: "English", zh: "简体中文" },
|
|
104
|
-
wheelLines: (count: number) => `每格 ${count} 行`,
|
|
105
|
-
wheelPrompt: (count: number) => `滚轮每格滚动行数(当前 ${count},范围 1-10),输入后 Enter 应用 · Esc 取消`,
|
|
106
95
|
cursorStyles: { block: "块", bar: "竖线", underline: "下划线" },
|
|
107
|
-
icons: { auto: "自动", nerd: "Nerd", ascii: "ASCII" },
|
|
96
|
+
icons: { auto: "自动", nerd: "Nerd", unicode: "Unicode", ascii: "ASCII" },
|
|
108
97
|
},
|
|
109
98
|
},
|
|
110
99
|
} as const;
|
|
@@ -127,7 +116,7 @@ function toggleSetting(config: OpenTuiConfig, key: keyof OpenTuiConfig["footerSe
|
|
|
127
116
|
}
|
|
128
117
|
|
|
129
118
|
function cycleIconMode(config: OpenTuiConfig): OpenTuiConfig {
|
|
130
|
-
const order: IconMode[] = ["auto", "nerd", "ascii"];
|
|
119
|
+
const order: IconMode[] = ["auto", "nerd", "unicode", "ascii"];
|
|
131
120
|
const currentIdx = order.indexOf(config.icons.mode);
|
|
132
121
|
const next = order[(currentIdx + 1) % order.length]!;
|
|
133
122
|
return { ...config, icons: { mode: next } };
|
|
@@ -148,19 +137,6 @@ function cycleCursorStyle(config: OpenTuiConfig): OpenTuiConfig {
|
|
|
148
137
|
return { ...config, cursorStyle: next };
|
|
149
138
|
}
|
|
150
139
|
|
|
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
140
|
function toggleTelemetry(config: OpenTuiConfig, key: keyof OpenTuiConfig["telemetry"]): OpenTuiConfig {
|
|
165
141
|
return {
|
|
166
142
|
...config,
|
|
@@ -181,11 +157,6 @@ function buildFeaturesItems(config: OpenTuiConfig, copy: SettingsCopy): SettingI
|
|
|
181
157
|
return [
|
|
182
158
|
{ id: "enabled", label: copy.labels.enabled, currentValue: flag(config.enabled) },
|
|
183
159
|
{ 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
160
|
{ id: "thinkingPeek", label: copy.labels.thinkingPeek, currentValue: formatThinkingPeekLines(config.thinkingPeek.lines, copy) },
|
|
190
161
|
{ id: "inlineFooter", label: copy.labels.inlineFooter, currentValue: flag(config.inlineFooter) },
|
|
191
162
|
];
|
|
@@ -271,23 +242,6 @@ interface SettingsUiHandle {
|
|
|
271
242
|
handleInput: (data: string) => void;
|
|
272
243
|
}
|
|
273
244
|
|
|
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
245
|
class SettingsUi implements SettingsUiHandle {
|
|
292
246
|
private tab: Tab = "features";
|
|
293
247
|
private config: OpenTuiConfig;
|
|
@@ -300,7 +254,6 @@ class SettingsUi implements SettingsUiHandle {
|
|
|
300
254
|
private cachedWidth: number | undefined;
|
|
301
255
|
private cachedLines: string[] | undefined;
|
|
302
256
|
private compact = false;
|
|
303
|
-
private wheelInput: Input | undefined;
|
|
304
257
|
|
|
305
258
|
constructor(
|
|
306
259
|
theme: Theme,
|
|
@@ -325,35 +278,11 @@ class SettingsUi implements SettingsUiHandle {
|
|
|
325
278
|
|
|
326
279
|
private applySetting(itemId: string): void {
|
|
327
280
|
this.selectedItemByTab[this.tab] = itemId;
|
|
328
|
-
if (this.tab === "features" && itemId === "wheelScrollLines") {
|
|
329
|
-
this.openWheelInput();
|
|
330
|
-
this.invalidate();
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
281
|
this.config = handleSettingChange(this.tab, itemId, this.config);
|
|
334
282
|
this.onChange(this.config);
|
|
335
283
|
this.rebuild(itemId);
|
|
336
284
|
}
|
|
337
285
|
|
|
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
286
|
private switchTab(offset: number): void {
|
|
358
287
|
const idx = TABS.indexOf(this.tab);
|
|
359
288
|
this.tab = TABS[(idx + offset + TABS.length) % TABS.length]!;
|
|
@@ -373,17 +302,11 @@ class SettingsUi implements SettingsUiHandle {
|
|
|
373
302
|
this.container.addChild(new Text(tabBar, 1, 0));
|
|
374
303
|
this.container.addChild(new Text(this.theme.fg("dim", copy.hint), 1, 0));
|
|
375
304
|
|
|
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
|
-
});
|
|
305
|
+
const items = buildItems(this.tab, this.config).map((item) => ({
|
|
306
|
+
value: item.id,
|
|
307
|
+
label: this.compact ? `${item.label}: ${item.currentValue}` : item.label,
|
|
308
|
+
description: this.compact ? undefined : item.currentValue,
|
|
309
|
+
} as SelectItem));
|
|
387
310
|
this.selectList = new SelectList(items, Math.min(items.length, 12), {
|
|
388
311
|
selectedPrefix: (t) => this.theme.fg("accent", t),
|
|
389
312
|
selectedText: (t) => this.theme.fg("accent", t),
|
|
@@ -405,31 +328,13 @@ class SettingsUi implements SettingsUiHandle {
|
|
|
405
328
|
this.selectList.onCancel = () => {
|
|
406
329
|
this.onClose();
|
|
407
330
|
};
|
|
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
|
-
}
|
|
331
|
+
this.container.addChild(this.selectList);
|
|
422
332
|
|
|
423
333
|
this.cachedWidth = undefined;
|
|
424
334
|
this.cachedLines = undefined;
|
|
425
335
|
}
|
|
426
336
|
|
|
427
337
|
handleInput(data: string): void {
|
|
428
|
-
if (this.wheelInput && this.tab === "features") {
|
|
429
|
-
this.wheelInput.handleInput(data);
|
|
430
|
-
this.invalidate();
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
338
|
if (matchesKey(data, Key.tab) || matchesKey(data, Key.right)) {
|
|
434
339
|
this.switchTab(1);
|
|
435
340
|
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.11",
|
|
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
|
-
}
|