pi-open-tui 0.2.13 → 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.
@@ -0,0 +1,34 @@
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
+ }
@@ -87,7 +87,7 @@ export default function (pi: ExtensionAPI) {
87
87
  },
88
88
  },
89
89
  );
90
- editor = installEditor(pi, ctx, config.cursorStyle);
90
+ editor = installEditor(pi, ctx, config.cursorStyle, config.fullscreen.wheelScrollLines);
91
91
  active = true;
92
92
  }
93
93
  };
@@ -273,11 +273,15 @@ export default function (pi: ExtensionAPI) {
273
273
  getConfig: () => config,
274
274
  onConfigChanged: (newConfig) => {
275
275
  const cursorStyleChanged = config.cursorStyle !== newConfig.cursorStyle;
276
+ const wheelScrollLinesChanged = config.fullscreen.wheelScrollLines !== newConfig.fullscreen.wheelScrollLines;
276
277
  saveConfig(newConfig);
277
278
  config = newConfig;
278
279
  if (cursorStyleChanged && active && editor) {
279
280
  editor.setCursorStyle(newConfig.cursorStyle);
280
281
  }
282
+ if (wheelScrollLinesChanged && active && editor) {
283
+ editor.setWheelScrollLines(newConfig.fullscreen.wheelScrollLines);
284
+ }
281
285
  if (lastCtx) {
282
286
  pendingUiChange = getPendingUiChange(newConfig.enabled, active);
283
287
  }
@@ -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,6 +10,10 @@ import {
9
10
  Text,
10
11
  } from "@earendil-works/pi-tui";
11
12
  import type { CursorStyle, IconMode, OpenTuiConfig, SettingsLanguage } from "./config.ts";
13
+ import {
14
+ DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
15
+ normalizeFullscreenWheelScrollLines,
16
+ } from "./fullscreen-scroll.ts";
12
17
 
13
18
  interface SettingItem {
14
19
  id: string;
@@ -24,10 +29,11 @@ const COPY = {
24
29
  en: {
25
30
  title: "Open TUI Settings",
26
31
  tabs: { features: "General", icons: "Appearance", segments: "Footer", telemetry: "Telemetry" },
27
- 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",
28
33
  labels: {
29
34
  enabled: "Enabled",
30
35
  language: "Language",
36
+ wheelScrollLines: "Mouse wheel speed",
31
37
  cursorStyle: "Cursor style",
32
38
  iconMode: "Icon mode",
33
39
  cwd: "CWD",
@@ -49,6 +55,8 @@ const COPY = {
49
55
  on: "On",
50
56
  off: "Off",
51
57
  languages: { en: "English", zh: "简体中文" },
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`,
52
60
  cursorStyles: { block: "Block", bar: "Bar", underline: "Underline" },
53
61
  icons: { auto: "Auto", nerd: "Nerd", ascii: "ASCII" },
54
62
  },
@@ -56,10 +64,11 @@ const COPY = {
56
64
  zh: {
57
65
  title: "Open TUI 设置",
58
66
  tabs: { features: "常规", icons: "外观", segments: "Footer", telemetry: "遥测" },
59
- hint: "Tab/Shift+Tab/←/→:切页 · ↑/↓:移动 · Enter/Space:更改 · Esc/q:关闭",
67
+ hint: "Tab/Shift+Tab/←/→:切页 · ↑/↓:移动 · Enter/Space:更改 · 滚轮速度项 Enter 输入 1-10 · Esc/q:关闭",
60
68
  labels: {
61
69
  enabled: "启用",
62
70
  language: "语言",
71
+ wheelScrollLines: "鼠标滚轮速度",
63
72
  cursorStyle: "光标样式",
64
73
  iconMode: "图标模式",
65
74
  cwd: "当前目录",
@@ -81,6 +90,8 @@ const COPY = {
81
90
  on: "开启",
82
91
  off: "关闭",
83
92
  languages: { en: "English", zh: "简体中文" },
93
+ wheelLines: (count: number) => `每格 ${count} 行`,
94
+ wheelPrompt: (count: number) => `滚轮每格滚动行数(当前 ${count},范围 1-10),输入后 Enter 应用 · Esc 取消`,
84
95
  cursorStyles: { block: "块", bar: "竖线", underline: "下划线" },
85
96
  icons: { auto: "自动", nerd: "Nerd", ascii: "ASCII" },
86
97
  },
@@ -121,6 +132,19 @@ function cycleCursorStyle(config: OpenTuiConfig): OpenTuiConfig {
121
132
  return { ...config, cursorStyle: next };
122
133
  }
123
134
 
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
+ };
146
+ }
147
+
124
148
  function toggleTelemetry(config: OpenTuiConfig, key: keyof OpenTuiConfig["telemetry"]): OpenTuiConfig {
125
149
  return {
126
150
  ...config,
@@ -132,6 +156,11 @@ function buildFeaturesItems(config: OpenTuiConfig, copy: SettingsCopy): SettingI
132
156
  return [
133
157
  { id: "enabled", label: copy.labels.enabled, currentValue: config.enabled ? copy.values.on : copy.values.off },
134
158
  { id: "settingsLanguage", label: copy.labels.language, currentValue: copy.values.languages[config.settingsLanguage] },
159
+ {
160
+ id: "wheelScrollLines",
161
+ label: copy.labels.wheelScrollLines,
162
+ currentValue: copy.values.wheelLines(config.fullscreen.wheelScrollLines),
163
+ },
135
164
  ];
136
165
  }
137
166
 
@@ -223,6 +252,7 @@ class SettingsUi implements SettingsUiHandle {
223
252
  private cachedWidth: number | undefined;
224
253
  private cachedLines: string[] | undefined;
225
254
  private compact = false;
255
+ private wheelInput: Input | undefined;
226
256
 
227
257
  constructor(
228
258
  theme: Theme,
@@ -247,11 +277,35 @@ class SettingsUi implements SettingsUiHandle {
247
277
 
248
278
  private applySetting(itemId: string): void {
249
279
  this.selectedItemByTab[this.tab] = itemId;
280
+ if (this.tab === "features" && itemId === "wheelScrollLines") {
281
+ this.openWheelInput();
282
+ this.invalidate();
283
+ return;
284
+ }
250
285
  this.config = handleSettingChange(this.tab, itemId, this.config);
251
286
  this.onChange(this.config);
252
287
  this.rebuild(itemId);
253
288
  }
254
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
+
255
309
  private switchTab(offset: number): void {
256
310
  const idx = TABS.indexOf(this.tab);
257
311
  this.tab = TABS[(idx + offset + TABS.length) % TABS.length]!;
@@ -271,11 +325,17 @@ class SettingsUi implements SettingsUiHandle {
271
325
  this.container.addChild(new Text(tabBar, 1, 0));
272
326
  this.container.addChild(new Text(this.theme.fg("dim", copy.hint), 1, 0));
273
327
 
274
- const items = buildItems(this.tab, this.config).map((item) => ({
275
- value: item.id,
276
- label: this.compact ? `${item.label}: ${item.currentValue}` : item.label,
277
- description: this.compact ? undefined : item.currentValue,
278
- } 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
+ });
279
339
  this.selectList = new SelectList(items, Math.min(items.length, 10), {
280
340
  selectedPrefix: (t) => this.theme.fg("accent", t),
281
341
  selectedText: (t) => this.theme.fg("accent", t),
@@ -298,12 +358,28 @@ class SettingsUi implements SettingsUiHandle {
298
358
  this.onClose();
299
359
  };
300
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
+ }
301
372
 
302
373
  this.cachedWidth = undefined;
303
374
  this.cachedLines = undefined;
304
375
  }
305
376
 
306
377
  handleInput(data: string): void {
378
+ if (this.wheelInput && this.tab === "features") {
379
+ this.wheelInput.handleInput(data);
380
+ this.invalidate();
381
+ return;
382
+ }
307
383
  if (matchesKey(data, Key.tab) || matchesKey(data, Key.right)) {
308
384
  this.switchTab(1);
309
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
  }