pi-open-tui 0.2.14 → 0.2.16

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,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,19 @@ 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
+ // input matches /session's "uncached" total: cacheWrite is billed near full
50
+ // price (fresh content), only cacheRead is discounted repeat content.
51
+ totals.input += input + cacheWrite;
52
+ totals.output += finiteOrZero(u.output);
53
+ totals.cacheRead += cacheRead;
54
+ totals.cacheWrite += cacheWrite;
55
+ totals.cost += finiteOrZero(u.cost?.total);
56
+ const promptTokens = input + cacheRead + cacheWrite;
52
57
  if (promptTokens > 0) {
53
- totals.latestCacheHitRate = ((u.cacheRead ?? 0) / promptTokens) * 100;
58
+ totals.latestCacheHitRate = (cacheRead / promptTokens) * 100;
54
59
  }
55
60
  }
56
61
  }