pi-open-tui 0.3.10 → 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 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.85 or later
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
 
@@ -60,9 +59,6 @@ Run `/open-tui` to open the settings dialog. It provides **General**, **Appearan
60
59
  "inlineFooter": false,
61
60
  "settingsLanguage": "en",
62
61
  "cursorStyle": "block",
63
- "fullscreen": {
64
- "wheelScrollLines": 4
65
- },
66
62
  "icons": {
67
63
  "mode": "auto"
68
64
  },
@@ -102,19 +98,16 @@ Key options:
102
98
  | `settingsLanguage` | `en`, `zh` | Changes the `/open-tui` interface language |
103
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 |
104
100
  | `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
101
  | `icons.mode` | `auto`, `nerd`, `unicode`, `ascii` | Controls footer and telemetry icons |
107
102
  | `footerSegments` | Boolean flags | Shows or hides individual footer data |
108
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 |
109
104
  | `telemetry` | Boolean flags | Enables telemetry and its individual measurements |
110
105
  | `thinkingPeek.lines` | `0`, `1`, `2` | Off, one-line, or two-line hidden thinking preview |
111
106
 
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. `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.
113
108
 
114
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.
115
110
 
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
111
  ## Turn telemetry
119
112
 
120
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,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.85 或更高版本
21
+ - Pi 1.0 或更高版本
23
22
  - 支持 UTF-8 和彩色输出的终端
24
23
  - 使用完整图标集时需要 [Nerd Font](https://www.nerdfonts.com/font-downloads)(可选;内置可移植 Unicode 图标)
25
24
 
@@ -60,9 +59,6 @@ pi -e npm:pi-open-tui
60
59
  "inlineFooter": false,
61
60
  "settingsLanguage": "zh",
62
61
  "cursorStyle": "block",
63
- "fullscreen": {
64
- "wheelScrollLines": 4
65
- },
66
62
  "icons": {
67
63
  "mode": "auto"
68
64
  },
@@ -102,19 +98,16 @@ pi -e npm:pi-open-tui
102
98
  | `settingsLanguage` | `en`、`zh` | 切换 `/open-tui` 设置界面的语言 |
103
99
  | `inlineFooter` | `true`、`false` | 将两条主要 Footer 信息行移入编辑器上下边框以节省垂直空间,默认关闭;扩展状态行仍显示在编辑器外 |
104
100
  | `cursorStyle` | `block`、`bar`、`underline` | `bar` 和 `underline` 需要终端支持光标形状转义序列 |
105
- | `fullscreen.wheelScrollLines` | `1`-`10` | 全屏模式下滚轮每格滚动的行数,默认值为 `4`;`/open-tui` 中在该项上按 Enter 后直接输入数字(超出范围会自动钳制到 `1`-`10`) |
106
101
  | `icons.mode` | `auto`、`nerd`、`unicode`、`ascii` | 控制底栏和遥测通知使用的图标 |
107
102
  | `footerSegments` | 布尔开关 | 分别控制底栏中的各项数据 |
108
103
  | `footerSegments.capitalizeProviderName` | 布尔开关 | 将底栏中提供商名称的首字母大写;设为 `false` 时保留原始大小写 |
109
104
  | `telemetry` | 布尔开关 | 控制遥测总开关和各项指标 |
110
105
  | `thinkingPeek.lines` | `0`、`1`、`2` | 关闭、单行或双行思考预览 |
111
106
 
112
- `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 主题色显示。
113
108
 
114
109
  开启 `inlineFooter` 后,两条常规 Footer 信息行会移入编辑器边框,从而节省垂直空间。顶部边框左侧显示 Git 分支,右侧信息组以当前目录开头;开启 `sessionName` 时,会话标题也会显示在左侧。Header 和扩展状态行保持独立显示;终端较窄时优先截断低优先级 Footer 数据,保留右侧统计信息和边框角。
115
110
 
116
- 全屏滚轮速度通过隔离的兼容层写入 Pi 0.84.2 的运行时字段,因为 Pi 尚未提供公开 setter。若后续 Pi 版本不再包含兼容字段,该设置会被忽略并继续使用 Pi 的默认滚动行为。
117
-
118
111
  ## 单轮遥测
119
112
 
120
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
- sanitizeStatus,
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]) => sanitizeStatus(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.fg("muted", status)).join(separator);
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
  }
@@ -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 · Enter on wheel speed: type 1-10 · Esc/q: close",
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,8 +55,6 @@ 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
59
  icons: { auto: "Auto", nerd: "Nerd", unicode: "Unicode", ascii: "ASCII" },
68
60
  },
@@ -70,13 +62,12 @@ const COPY = {
70
62
  zh: {
71
63
  title: "Open TUI 设置",
72
64
  tabs: { features: "常规", icons: "外观", segments: "Footer", telemetry: "遥测" },
73
- hint: "Tab/Shift+Tab/←/→:切页 · ↑/↓:移动 · Enter/Space:更改 · 滚轮速度项 Enter 输入 1-10 · Esc/q:关闭",
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,8 +92,6 @@ 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
96
  icons: { auto: "自动", nerd: "Nerd", unicode: "Unicode", ascii: "ASCII" },
108
97
  },
@@ -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 editingWheel = this.tab === "features" && this.wheelInput !== undefined;
377
- const items = buildItems(this.tab, this.config).map((item) => {
378
- const editing = editingWheel && item.id === "wheelScrollLines";
379
- return {
380
- value: item.id,
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
- if (editingWheel) {
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.10",
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/fullscreen-scroll.test.ts tests/footer.test.ts tests/editor.test.ts tests/telemetry.test.ts tests/peek.test.ts",
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.85",
39
- "@earendil-works/pi-ai": ">=0.85",
40
- "@earendil-works/pi-tui": ">=0.85"
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
- }