pi-tui-footer 1.2.5 → 1.2.6

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.
@@ -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,17 +38,12 @@ 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
  roundedTools: boolean;
52
44
  inlineFooter: boolean;
53
45
  settingsLanguage: SettingsLanguage;
54
46
  cursorStyle: CursorStyle;
55
- fullscreen: FullscreenConfig;
56
47
  icons: {
57
48
  mode: IconMode;
58
49
  };
@@ -67,9 +58,6 @@ export const DEFAULT_CONFIG: OpenTuiConfig = {
67
58
  inlineFooter: false,
68
59
  settingsLanguage: "en",
69
60
  cursorStyle: "block",
70
- fullscreen: {
71
- wheelScrollLines: DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
72
- },
73
61
  icons: {
74
62
  mode: "auto",
75
63
  },
@@ -155,27 +143,23 @@ export function loadConfig(notify?: (msg: string, level: "warning" | "info") =>
155
143
  const raw = readFileSync(path, "utf8");
156
144
  const parsed: unknown = JSON.parse(raw);
157
145
  const config = deepMerge(DEFAULT_CONFIG, parsed);
146
+ if (typeof config.inlineFooter !== "boolean") {
147
+ config.inlineFooter = DEFAULT_CONFIG.inlineFooter;
148
+ }
149
+ if (typeof config.roundedTools !== "boolean") {
150
+ config.roundedTools = DEFAULT_CONFIG.roundedTools;
151
+ }
158
152
  if (config.settingsLanguage !== "en" && config.settingsLanguage !== "zh") {
159
153
  config.settingsLanguage = DEFAULT_CONFIG.settingsLanguage;
160
154
  }
161
155
  if (config.cursorStyle !== "block" && config.cursorStyle !== "bar" && config.cursorStyle !== "underline") {
162
156
  config.cursorStyle = DEFAULT_CONFIG.cursorStyle;
163
157
  }
164
- config.fullscreen.wheelScrollLines = normalizeFullscreenWheelScrollLines(
165
- config.fullscreen.wheelScrollLines,
166
- DEFAULT_CONFIG.fullscreen.wheelScrollLines,
167
- );
168
158
  if (typeof config.thinkingPeek !== "object" || config.thinkingPeek === null || Array.isArray(config.thinkingPeek)) {
169
159
  config.thinkingPeek = structuredClone(DEFAULT_CONFIG.thinkingPeek);
170
160
  } else {
171
161
  config.thinkingPeek.lines = normalizeThinkingPeekLines(config.thinkingPeek.lines);
172
162
  }
173
- if (typeof config.inlineFooter !== "boolean") {
174
- config.inlineFooter = DEFAULT_CONFIG.inlineFooter;
175
- }
176
- if (typeof config.roundedTools !== "boolean") {
177
- config.roundedTools = DEFAULT_CONFIG.roundedTools;
178
- }
179
163
  return config;
180
164
  } catch (err) {
181
165
  notify?.(`pi-tui config parse error: ${err instanceof Error ? err.message : String(err)}`, "warning");
@@ -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,
@@ -181,6 +182,18 @@ function renderStatsBlock(
181
182
  return stats.join(` ${theme.fg("dim", "|")} `);
182
183
  }
183
184
 
185
+ const ANSI_RESET = "\x1b[0m";
186
+
187
+ /**
188
+ * Keep the colours an extension applied to its own status text. Plain text stays
189
+ * muted so unstyled statuses keep the quiet footer look. The trailing reset
190
+ * stops an unclosed style from tinting the separator and the next status.
191
+ */
192
+ function renderStatusText(theme: Theme, status: string): string {
193
+ if (!hasAnsiStyles(status)) return theme.fg("muted", status);
194
+ return `${status}${ANSI_RESET}`;
195
+ }
196
+
184
197
  function renderExtensionStatusLines(
185
198
  theme: Theme,
186
199
  extensionStatuses: ReadonlyMap<string, string>,
@@ -189,12 +202,12 @@ function renderExtensionStatusLines(
189
202
  ): string[] {
190
203
  const statuses = Array.from(extensionStatuses.entries())
191
204
  .sort(([a], [b]) => a.localeCompare(b))
192
- .map(([, text]) => sanitizeStatus(text))
205
+ .map(([, text]) => sanitizeStatusPreservingStyles(text))
193
206
  .filter((text) => text.length > 0);
194
207
  if (statuses.length === 0) return [];
195
208
 
196
209
  const separator = ` ${theme.fg("dim", "|")} `;
197
- const statusText = statuses.map((status) => theme.fg("muted", status)).join(separator);
210
+ const statusText = statuses.map((status) => renderStatusText(theme, status)).join(separator);
198
211
  const line = `${theme.fg("mdLink", glyphs.extensions)} ${statusText}`;
199
212
  return wrapTextWithAnsi(line, width);
200
213
  }
@@ -4,7 +4,7 @@
4
4
  * thinking peek preview, rounded tool frames, and turn telemetry.
5
5
  *
6
6
  * Integrated from:
7
- * - pi-open-tui (v0.3.10, MIT, by OldSun, https://github.com/OldSuns/pi-open-tui)
7
+ * - pi-open-tui (v0.3.11, MIT, by OldSun, https://github.com/OldSuns/pi-open-tui)
8
8
  * - pi-rounded-tools (v0.1.3, MIT, by OrionPax, https://github.com/orionpax1997/pi-rounded-tools)
9
9
  *
10
10
  * Security audit notes:
@@ -169,7 +169,6 @@ export default function (pi: ExtensionAPI) {
169
169
  pi,
170
170
  ctx,
171
171
  config.cursorStyle,
172
- config.fullscreen.wheelScrollLines,
173
172
  {
174
173
  enabled: () => config.inlineFooter,
175
174
  render: footer.renderInline,
@@ -423,7 +422,6 @@ export default function (pi: ExtensionAPI) {
423
422
  const wasEnabled = config.enabled;
424
423
  const wasRoundedTools = config.roundedTools;
425
424
  const cursorStyleChanged = config.cursorStyle !== newConfig.cursorStyle;
426
- const wheelScrollLinesChanged = config.fullscreen.wheelScrollLines !== newConfig.fullscreen.wheelScrollLines;
427
425
  const thinkingPeekLinesChanged = config.thinkingPeek.lines !== newConfig.thinkingPeek.lines;
428
426
  saveConfig(newConfig);
429
427
  config = newConfig;
@@ -436,9 +434,6 @@ export default function (pi: ExtensionAPI) {
436
434
  if (cursorStyleChanged && active && editor) {
437
435
  editor.setCursorStyle(newConfig.cursorStyle);
438
436
  }
439
- if (wheelScrollLinesChanged && active && editor) {
440
- editor.setWheelScrollLines(newConfig.fullscreen.wheelScrollLines);
441
- }
442
437
  if (lastCtx) {
443
438
  pendingUiChange = getPendingUiChange(newConfig.enabled, active);
444
439
  }
@@ -4,7 +4,7 @@
4
4
  * Merged 2026-08 from the standalone local extension rounded-tools.ts (a
5
5
  * localized copy of npm:pi-rounded-tools@0.1.3, MIT, by OrionPax,
6
6
  * https://github.com/orionpax1997/pi-rounded-tools), integrated into pi-tui
7
- * (based on https://github.com/OldSuns/pi-open-tui v0.3.10, MIT, by
7
+ * (based on https://github.com/OldSuns/pi-open-tui v0.3.11, MIT, by
8
8
  * OldSun) and gated behind `roundedTools` in pi-tui config
9
9
  * (~/.pi/agent/pi-tui.json, default on).
10
10
  *
@@ -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,14 +25,13 @@ const COPY = {
30
25
  en: {
31
26
  title: "Pi 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
  roundedTools: "Rounded tool frames",
37
32
  inlineFooter: "Inline footer",
38
33
  thinkingPeek: "Thinking peek",
39
34
  language: "Language",
40
- wheelScrollLines: "Mouse wheel speed",
41
35
  cursorStyle: "Cursor style",
42
36
  iconMode: "Icon mode",
43
37
  cwd: "CWD",
@@ -62,8 +56,6 @@ const COPY = {
62
56
  off: "Off",
63
57
  thinkingPeek: { off: "Off", one: "1 line", two: "2 lines" },
64
58
  languages: { en: "English", zh: "简体中文" },
65
- wheelLines: (count: number) => `${count} ${count === 1 ? "line" : "lines"} / notch`,
66
- wheelPrompt: (count: number) => `Wheel scroll lines per notch, 1-10 (current: ${count}). Enter: apply · Esc: cancel`,
67
59
  cursorStyles: { block: "Block", bar: "Bar", underline: "Underline" },
68
60
  icons: { auto: "Auto", nerd: "Nerd", unicode: "Unicode", ascii: "ASCII" },
69
61
  },
@@ -71,14 +63,13 @@ const COPY = {
71
63
  zh: {
72
64
  title: "Pi TUI 设置",
73
65
  tabs: { features: "常规", icons: "外观", segments: "Footer", telemetry: "遥测" },
74
- hint: "Tab/Shift+Tab/←/→:切页 · ↑/↓:移动 · Enter/Space:更改 · 滚轮速度项 Enter 输入 1-10 · Esc/q:关闭",
66
+ hint: "Tab/Shift+Tab/←/→:切页 · ↑/↓:移动 · Enter/Space:更改 · Esc/q:关闭",
75
67
  labels: {
76
68
  enabled: "启用",
77
69
  roundedTools: "圆角工具边框",
78
70
  inlineFooter: "内联底栏",
79
71
  thinkingPeek: "思考预览",
80
72
  language: "语言",
81
- wheelScrollLines: "鼠标滚轮速度",
82
73
  cursorStyle: "光标样式",
83
74
  iconMode: "图标模式",
84
75
  cwd: "当前目录",
@@ -103,8 +94,6 @@ const COPY = {
103
94
  off: "关闭",
104
95
  thinkingPeek: { off: "关闭", one: "单行", two: "双行" },
105
96
  languages: { en: "English", zh: "简体中文" },
106
- wheelLines: (count: number) => `每格 ${count} 行`,
107
- wheelPrompt: (count: number) => `滚轮每格滚动行数(当前 ${count},范围 1-10),输入后 Enter 应用 · Esc 取消`,
108
97
  cursorStyles: { block: "块", bar: "竖线", underline: "下划线" },
109
98
  icons: { auto: "自动", nerd: "Nerd", unicode: "Unicode", ascii: "ASCII" },
110
99
  },
@@ -150,19 +139,6 @@ function cycleCursorStyle(config: OpenTuiConfig): OpenTuiConfig {
150
139
  return { ...config, cursorStyle: next };
151
140
  }
152
141
 
153
- function setWheelScrollLines(config: OpenTuiConfig, raw: string): OpenTuiConfig | undefined {
154
- if (!/^\d+$/.test(raw)) return undefined;
155
- const parsed = Number(raw);
156
- const bounded = Number.isFinite(parsed) ? parsed : Number.MAX_SAFE_INTEGER;
157
- return {
158
- ...config,
159
- fullscreen: {
160
- ...config.fullscreen,
161
- wheelScrollLines: normalizeFullscreenWheelScrollLines(bounded, DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES),
162
- },
163
- };
164
- }
165
-
166
142
  function toggleTelemetry(config: OpenTuiConfig, key: keyof OpenTuiConfig["telemetry"]): OpenTuiConfig {
167
143
  return {
168
144
  ...config,
@@ -184,11 +160,6 @@ function buildFeaturesItems(config: OpenTuiConfig, copy: SettingsCopy): SettingI
184
160
  { id: "enabled", label: copy.labels.enabled, currentValue: flag(config.enabled) },
185
161
  { id: "roundedTools", label: copy.labels.roundedTools, currentValue: flag(config.roundedTools) },
186
162
  { id: "settingsLanguage", label: copy.labels.language, currentValue: copy.values.languages[config.settingsLanguage] },
187
- {
188
- id: "wheelScrollLines",
189
- label: copy.labels.wheelScrollLines,
190
- currentValue: copy.values.wheelLines(config.fullscreen.wheelScrollLines),
191
- },
192
163
  { id: "thinkingPeek", label: copy.labels.thinkingPeek, currentValue: formatThinkingPeekLines(config.thinkingPeek.lines, copy) },
193
164
  { id: "inlineFooter", label: copy.labels.inlineFooter, currentValue: flag(config.inlineFooter) },
194
165
  ];
@@ -275,23 +246,6 @@ interface SettingsUiHandle {
275
246
  handleInput: (data: string) => void;
276
247
  }
277
248
 
278
- function insertComponentAfter(list: Component, child: Component, index: () => number): Component {
279
- // SelectList renders one line per item; splice the editor into that output.
280
- return {
281
- render(width: number): string[] {
282
- const lines = list.render(width);
283
- const insertAt = index();
284
- if (insertAt < 0 || insertAt >= lines.length) return lines;
285
- const childLines = child.render(width);
286
- return [...lines.slice(0, insertAt + 1), ...childLines, ...lines.slice(insertAt + 1)];
287
- },
288
- invalidate(): void {
289
- list.invalidate();
290
- child.invalidate();
291
- },
292
- };
293
- }
294
-
295
249
  class SettingsUi implements SettingsUiHandle {
296
250
  private tab: Tab = "features";
297
251
  private config: OpenTuiConfig;
@@ -304,7 +258,6 @@ class SettingsUi implements SettingsUiHandle {
304
258
  private cachedWidth: number | undefined;
305
259
  private cachedLines: string[] | undefined;
306
260
  private compact = false;
307
- private wheelInput: Input | undefined;
308
261
 
309
262
  constructor(
310
263
  theme: Theme,
@@ -329,35 +282,11 @@ class SettingsUi implements SettingsUiHandle {
329
282
 
330
283
  private applySetting(itemId: string): void {
331
284
  this.selectedItemByTab[this.tab] = itemId;
332
- if (this.tab === "features" && itemId === "wheelScrollLines") {
333
- this.openWheelInput();
334
- this.invalidate();
335
- return;
336
- }
337
285
  this.config = handleSettingChange(this.tab, itemId, this.config);
338
286
  this.onChange(this.config);
339
287
  this.rebuild(itemId);
340
288
  }
341
289
 
342
- private openWheelInput(): void {
343
- const input = new Input();
344
- input.onSubmit = (value) => {
345
- const next = setWheelScrollLines(this.config, value);
346
- this.wheelInput = undefined;
347
- if (next) {
348
- this.config = next;
349
- this.onChange(this.config);
350
- }
351
- this.rebuild("wheelScrollLines");
352
- };
353
- input.onEscape = () => {
354
- this.wheelInput = undefined;
355
- this.rebuild("wheelScrollLines");
356
- };
357
- this.wheelInput = input;
358
- this.rebuild("wheelScrollLines");
359
- }
360
-
361
290
  private switchTab(offset: number): void {
362
291
  const idx = TABS.indexOf(this.tab);
363
292
  this.tab = TABS[(idx + offset + TABS.length) % TABS.length]!;
@@ -377,17 +306,11 @@ class SettingsUi implements SettingsUiHandle {
377
306
  this.container.addChild(new Text(tabBar, 1, 0));
378
307
  this.container.addChild(new Text(this.theme.fg("dim", copy.hint), 1, 0));
379
308
 
380
- const editingWheel = this.tab === "features" && this.wheelInput !== undefined;
381
- const items = buildItems(this.tab, this.config).map((item) => {
382
- const editing = editingWheel && item.id === "wheelScrollLines";
383
- return {
384
- value: item.id,
385
- label: editing
386
- ? (this.compact ? `${item.label}:` : item.label)
387
- : (this.compact ? `${item.label}: ${item.currentValue}` : item.label),
388
- description: editing || this.compact ? undefined : item.currentValue,
389
- } as SelectItem;
390
- });
309
+ const items = buildItems(this.tab, this.config).map((item) => ({
310
+ value: item.id,
311
+ label: this.compact ? `${item.label}: ${item.currentValue}` : item.label,
312
+ description: this.compact ? undefined : item.currentValue,
313
+ } as SelectItem));
391
314
  this.selectList = new SelectList(items, Math.min(items.length, 12), {
392
315
  selectedPrefix: (t) => this.theme.fg("accent", t),
393
316
  selectedText: (t) => this.theme.fg("accent", t),
@@ -409,31 +332,13 @@ class SettingsUi implements SettingsUiHandle {
409
332
  this.selectList.onCancel = () => {
410
333
  this.onClose();
411
334
  };
412
- if (editingWheel) {
413
- this.wheelInput!.focused = true;
414
- const wheelInputGroup = new Box(4, 0);
415
- wheelInputGroup.addChild(new Text(
416
- this.theme.fg("muted", copy.values.wheelPrompt(this.config.fullscreen.wheelScrollLines)),
417
- 0,
418
- 0,
419
- ));
420
- wheelInputGroup.addChild(this.wheelInput!);
421
- const selectedIndex = () => items.findIndex((item) => item.value === this.selectList.getSelectedItem()?.value);
422
- this.container.addChild(insertComponentAfter(this.selectList, wheelInputGroup, selectedIndex));
423
- } else {
424
- this.container.addChild(this.selectList);
425
- }
335
+ this.container.addChild(this.selectList);
426
336
 
427
337
  this.cachedWidth = undefined;
428
338
  this.cachedLines = undefined;
429
339
  }
430
340
 
431
341
  handleInput(data: string): void {
432
- if (this.wheelInput && this.tab === "features") {
433
- this.wheelInput.handleInput(data);
434
- this.invalidate();
435
- return;
436
- }
437
342
  if (matchesKey(data, Key.tab) || matchesKey(data, Key.right)) {
438
343
  this.switchTab(1);
439
344
  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-tui-footer",
3
- "version": "1.2.5",
3
+ "version": "1.2.6",
4
4
  "description": "pi extension: TUI footer/header with git status, telemetry, rounded tools, editor and settings enhancements",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -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
- }