pi-open-tui 0.2.14 → 0.2.15

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
@@ -93,7 +93,7 @@ Key options:
93
93
  | --- | --- | --- |
94
94
  | `settingsLanguage` | `en`, `zh` | Changes the `/open-tui` interface language |
95
95
  | `cursorStyle` | `block`, `bar`, `underline` | `bar` and `underline` require terminal cursor-shape support |
96
- | `fullscreen.wheelScrollLines` | `1`-`10` | Lines scrolled per mouse-wheel notch in fullscreen mode; defaults to `4`. `/open-tui` cycles through `1`, `4`, `7`, and `10` for quick switching |
96
+ | `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`) |
97
97
  | `icons.mode` | `auto`, `nerd`, `ascii` | Controls footer and telemetry icons |
98
98
  | `footerSegments` | Boolean flags | Shows or hides individual footer data |
99
99
  | `telemetry` | Boolean flags | Enables telemetry and its individual measurements |
package/README.zh-CN.md CHANGED
@@ -93,7 +93,7 @@ pi -e npm:pi-open-tui
93
93
  | --- | --- | --- |
94
94
  | `settingsLanguage` | `en`、`zh` | 切换 `/open-tui` 设置界面的语言 |
95
95
  | `cursorStyle` | `block`、`bar`、`underline` | `bar` 和 `underline` 需要终端支持光标形状转义序列 |
96
- | `fullscreen.wheelScrollLines` | `1`-`10` | 全屏模式下滚轮每格滚动的行数,默认值为 `4`;`/open-tui` 使用 `1`、`4`、`7`、`10` 四档快速切换 |
96
+ | `fullscreen.wheelScrollLines` | `1`-`10` | 全屏模式下滚轮每格滚动的行数,默认值为 `4`;`/open-tui` 中在该项上按 Enter 后直接输入数字(超出范围会自动钳制到 `1`-`10`) |
97
97
  | `icons.mode` | `auto`、`nerd`、`ascii` | 控制底栏和遥测通知使用的图标 |
98
98
  | `footerSegments` | 布尔开关 | 分别控制底栏中的各项数据 |
99
99
  | `telemetry` | 布尔开关 | 控制遥测总开关和各项指标 |
@@ -1,6 +1,7 @@
1
1
  import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
3
  Box,
4
+ Input,
4
5
  Key,
5
6
  matchesKey,
6
7
  SelectList,
@@ -9,8 +10,10 @@ import {
9
10
  Text,
10
11
  } from "@earendil-works/pi-tui";
11
12
  import type { CursorStyle, IconMode, OpenTuiConfig, SettingsLanguage } from "./config.ts";
12
-
13
- const WHEEL_SCROLL_PRESETS = [1, 4, 7, 10] as const;
13
+ import {
14
+ DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
15
+ normalizeFullscreenWheelScrollLines,
16
+ } from "./fullscreen-scroll.ts";
14
17
 
15
18
  interface SettingItem {
16
19
  id: string;
@@ -26,7 +29,7 @@ const COPY = {
26
29
  en: {
27
30
  title: "Open TUI Settings",
28
31
  tabs: { features: "General", icons: "Appearance", segments: "Footer", telemetry: "Telemetry" },
29
- hint: "Tab/Shift+Tab/←/→: tabs · ↑/↓: move · Enter/Space: change · Esc/q: close",
32
+ hint: "Tab/Shift+Tab/←/→: tabs · ↑/↓: move · Enter/Space: change · Enter on wheel speed: type 1-10 · Esc/q: close",
30
33
  labels: {
31
34
  enabled: "Enabled",
32
35
  language: "Language",
@@ -53,6 +56,7 @@ const COPY = {
53
56
  off: "Off",
54
57
  languages: { en: "English", zh: "简体中文" },
55
58
  wheelLines: (count: number) => `${count} ${count === 1 ? "line" : "lines"} / notch`,
59
+ wheelPrompt: (count: number) => `Wheel scroll lines per notch, 1-10 (current: ${count}). Enter: apply · Esc: cancel`,
56
60
  cursorStyles: { block: "Block", bar: "Bar", underline: "Underline" },
57
61
  icons: { auto: "Auto", nerd: "Nerd", ascii: "ASCII" },
58
62
  },
@@ -60,7 +64,7 @@ const COPY = {
60
64
  zh: {
61
65
  title: "Open TUI 设置",
62
66
  tabs: { features: "常规", icons: "外观", segments: "Footer", telemetry: "遥测" },
63
- hint: "Tab/Shift+Tab/←/→:切页 · ↑/↓:移动 · Enter/Space:更改 · Esc/q:关闭",
67
+ hint: "Tab/Shift+Tab/←/→:切页 · ↑/↓:移动 · Enter/Space:更改 · 滚轮速度项 Enter 输入 1-10 · Esc/q:关闭",
64
68
  labels: {
65
69
  enabled: "启用",
66
70
  language: "语言",
@@ -87,6 +91,7 @@ const COPY = {
87
91
  off: "关闭",
88
92
  languages: { en: "English", zh: "简体中文" },
89
93
  wheelLines: (count: number) => `每格 ${count} 行`,
94
+ wheelPrompt: (count: number) => `滚轮每格滚动行数(当前 ${count},范围 1-10),输入后 Enter 应用 · Esc 取消`,
90
95
  cursorStyles: { block: "块", bar: "竖线", underline: "下划线" },
91
96
  icons: { auto: "自动", nerd: "Nerd", ascii: "ASCII" },
92
97
  },
@@ -127,10 +132,17 @@ function cycleCursorStyle(config: OpenTuiConfig): OpenTuiConfig {
127
132
  return { ...config, cursorStyle: next };
128
133
  }
129
134
 
130
- function cycleWheelScrollLines(config: OpenTuiConfig): OpenTuiConfig {
131
- const current = config.fullscreen.wheelScrollLines;
132
- const wheelScrollLines = WHEEL_SCROLL_PRESETS.find((value) => value > current) ?? WHEEL_SCROLL_PRESETS[0];
133
- return { ...config, fullscreen: { ...config.fullscreen, wheelScrollLines } };
135
+ function setWheelScrollLines(config: OpenTuiConfig, raw: string): OpenTuiConfig | undefined {
136
+ if (!/^\d+$/.test(raw)) return undefined;
137
+ const parsed = Number(raw);
138
+ const bounded = Number.isFinite(parsed) ? parsed : Number.MAX_SAFE_INTEGER;
139
+ return {
140
+ ...config,
141
+ fullscreen: {
142
+ ...config.fullscreen,
143
+ wheelScrollLines: normalizeFullscreenWheelScrollLines(bounded, DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES),
144
+ },
145
+ };
134
146
  }
135
147
 
136
148
  function toggleTelemetry(config: OpenTuiConfig, key: keyof OpenTuiConfig["telemetry"]): OpenTuiConfig {
@@ -208,7 +220,6 @@ function handleSettingChange(
208
220
  if (tab === "features") {
209
221
  if (itemId === "enabled") return toggleEnabled(config);
210
222
  if (itemId === "settingsLanguage") return toggleLanguage(config);
211
- if (itemId === "wheelScrollLines") return cycleWheelScrollLines(config);
212
223
  }
213
224
  if (tab === "icons") {
214
225
  if (itemId === "mode") return cycleIconMode(config);
@@ -241,6 +252,7 @@ class SettingsUi implements SettingsUiHandle {
241
252
  private cachedWidth: number | undefined;
242
253
  private cachedLines: string[] | undefined;
243
254
  private compact = false;
255
+ private wheelInput: Input | undefined;
244
256
 
245
257
  constructor(
246
258
  theme: Theme,
@@ -265,11 +277,35 @@ class SettingsUi implements SettingsUiHandle {
265
277
 
266
278
  private applySetting(itemId: string): void {
267
279
  this.selectedItemByTab[this.tab] = itemId;
280
+ if (this.tab === "features" && itemId === "wheelScrollLines") {
281
+ this.openWheelInput();
282
+ this.invalidate();
283
+ return;
284
+ }
268
285
  this.config = handleSettingChange(this.tab, itemId, this.config);
269
286
  this.onChange(this.config);
270
287
  this.rebuild(itemId);
271
288
  }
272
289
 
290
+ private openWheelInput(): void {
291
+ const input = new Input();
292
+ input.onSubmit = (value) => {
293
+ const next = setWheelScrollLines(this.config, value);
294
+ this.wheelInput = undefined;
295
+ if (next) {
296
+ this.config = next;
297
+ this.onChange(this.config);
298
+ }
299
+ this.rebuild("wheelScrollLines");
300
+ };
301
+ input.onEscape = () => {
302
+ this.wheelInput = undefined;
303
+ this.rebuild("wheelScrollLines");
304
+ };
305
+ this.wheelInput = input;
306
+ this.rebuild("wheelScrollLines");
307
+ }
308
+
273
309
  private switchTab(offset: number): void {
274
310
  const idx = TABS.indexOf(this.tab);
275
311
  this.tab = TABS[(idx + offset + TABS.length) % TABS.length]!;
@@ -289,11 +325,17 @@ class SettingsUi implements SettingsUiHandle {
289
325
  this.container.addChild(new Text(tabBar, 1, 0));
290
326
  this.container.addChild(new Text(this.theme.fg("dim", copy.hint), 1, 0));
291
327
 
292
- const items = buildItems(this.tab, this.config).map((item) => ({
293
- value: item.id,
294
- label: this.compact ? `${item.label}: ${item.currentValue}` : item.label,
295
- description: this.compact ? undefined : item.currentValue,
296
- } as SelectItem));
328
+ const editingWheel = this.tab === "features" && this.wheelInput !== undefined;
329
+ const items = buildItems(this.tab, this.config).map((item) => {
330
+ const editing = editingWheel && item.id === "wheelScrollLines";
331
+ return {
332
+ value: item.id,
333
+ label: editing
334
+ ? (this.compact ? `${item.label}:` : item.label)
335
+ : (this.compact ? `${item.label}: ${item.currentValue}` : item.label),
336
+ description: editing || this.compact ? undefined : item.currentValue,
337
+ } as SelectItem;
338
+ });
297
339
  this.selectList = new SelectList(items, Math.min(items.length, 10), {
298
340
  selectedPrefix: (t) => this.theme.fg("accent", t),
299
341
  selectedText: (t) => this.theme.fg("accent", t),
@@ -316,12 +358,28 @@ class SettingsUi implements SettingsUiHandle {
316
358
  this.onClose();
317
359
  };
318
360
  this.container.addChild(this.selectList);
361
+ if (editingWheel) {
362
+ this.wheelInput!.focused = true;
363
+ const wheelInputGroup = new Box(4, 0);
364
+ wheelInputGroup.addChild(new Text(
365
+ this.theme.fg("muted", copy.values.wheelPrompt(this.config.fullscreen.wheelScrollLines)),
366
+ 0,
367
+ 0,
368
+ ));
369
+ wheelInputGroup.addChild(this.wheelInput!);
370
+ this.container.addChild(wheelInputGroup);
371
+ }
319
372
 
320
373
  this.cachedWidth = undefined;
321
374
  this.cachedLines = undefined;
322
375
  }
323
376
 
324
377
  handleInput(data: string): void {
378
+ if (this.wheelInput && this.tab === "features") {
379
+ this.wheelInput.handleInput(data);
380
+ this.invalidate();
381
+ return;
382
+ }
325
383
  if (matchesKey(data, Key.tab) || matchesKey(data, Key.right)) {
326
384
  this.switchTab(1);
327
385
  this.invalidate();
@@ -3,7 +3,7 @@ import type { AssistantMessage } from "@earendil-works/pi-ai";
3
3
  import type { GitStatus } from "./git.ts";
4
4
  import { emptyGitStatus } from "./git.ts";
5
5
  import type { RuntimeInfo } from "./runtime.ts";
6
- import { fmtTokens, formatProviderLabel } from "./utils.ts";
6
+ import { finiteOrZero, fmtTokens, formatProviderLabel } from "./utils.ts";
7
7
 
8
8
  export interface FooterState {
9
9
  git: GitStatus;
@@ -43,14 +43,17 @@ export function getUsageTotals(ctx: ExtensionContext): UsageTotals {
43
43
  const m = entry.message as AssistantMessage;
44
44
  const u = m.usage;
45
45
  if (!u) continue;
46
- totals.input += u.input ?? 0;
47
- totals.output += u.output ?? 0;
48
- totals.cacheRead += u.cacheRead ?? 0;
49
- totals.cacheWrite += u.cacheWrite ?? 0;
50
- totals.cost += u.cost?.total ?? 0;
51
- const promptTokens = (u.input ?? 0) + (u.cacheRead ?? 0) + (u.cacheWrite ?? 0);
46
+ const input = finiteOrZero(u.input);
47
+ const cacheRead = finiteOrZero(u.cacheRead);
48
+ const cacheWrite = finiteOrZero(u.cacheWrite);
49
+ totals.input += input;
50
+ totals.output += finiteOrZero(u.output);
51
+ totals.cacheRead += cacheRead;
52
+ totals.cacheWrite += cacheWrite;
53
+ totals.cost += finiteOrZero(u.cost?.total);
54
+ const promptTokens = input + cacheRead + cacheWrite;
52
55
  if (promptTokens > 0) {
53
- totals.latestCacheHitRate = ((u.cacheRead ?? 0) / promptTokens) * 100;
56
+ totals.latestCacheHitRate = (cacheRead / promptTokens) * 100;
54
57
  }
55
58
  }
56
59
  }