@oh-my-pi/pi-coding-agent 5.5.0 → 5.6.70

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.
Files changed (98) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/docs/python-repl.md +77 -0
  3. package/examples/hooks/snake.ts +7 -7
  4. package/package.json +5 -5
  5. package/src/bun-imports.d.ts +6 -0
  6. package/src/cli/args.ts +7 -0
  7. package/src/cli/setup-cli.ts +231 -0
  8. package/src/cli.ts +2 -0
  9. package/src/core/agent-session.ts +118 -15
  10. package/src/core/bash-executor.ts +3 -84
  11. package/src/core/compaction/compaction.ts +10 -5
  12. package/src/core/extensions/index.ts +2 -0
  13. package/src/core/extensions/loader.ts +13 -1
  14. package/src/core/extensions/runner.ts +50 -2
  15. package/src/core/extensions/types.ts +67 -2
  16. package/src/core/keybindings.ts +51 -1
  17. package/src/core/prompt-templates.ts +15 -0
  18. package/src/core/python-executor-display.test.ts +42 -0
  19. package/src/core/python-executor-lifecycle.test.ts +99 -0
  20. package/src/core/python-executor-mapping.test.ts +41 -0
  21. package/src/core/python-executor-per-call.test.ts +49 -0
  22. package/src/core/python-executor-session.test.ts +103 -0
  23. package/src/core/python-executor-streaming.test.ts +77 -0
  24. package/src/core/python-executor-timeout.test.ts +35 -0
  25. package/src/core/python-executor.lifecycle.test.ts +139 -0
  26. package/src/core/python-executor.result.test.ts +49 -0
  27. package/src/core/python-executor.test.ts +180 -0
  28. package/src/core/python-executor.ts +313 -0
  29. package/src/core/python-gateway-coordinator.ts +832 -0
  30. package/src/core/python-kernel-display.test.ts +54 -0
  31. package/src/core/python-kernel-env.test.ts +138 -0
  32. package/src/core/python-kernel-session.test.ts +87 -0
  33. package/src/core/python-kernel-ws.test.ts +104 -0
  34. package/src/core/python-kernel.lifecycle.test.ts +249 -0
  35. package/src/core/python-kernel.test.ts +461 -0
  36. package/src/core/python-kernel.ts +1182 -0
  37. package/src/core/python-modules.test.ts +102 -0
  38. package/src/core/python-modules.ts +110 -0
  39. package/src/core/python-prelude.py +889 -0
  40. package/src/core/python-prelude.test.ts +140 -0
  41. package/src/core/python-prelude.ts +3 -0
  42. package/src/core/sdk.ts +24 -6
  43. package/src/core/session-manager.ts +174 -82
  44. package/src/core/settings-manager-python.test.ts +23 -0
  45. package/src/core/settings-manager.ts +202 -0
  46. package/src/core/streaming-output.test.ts +26 -0
  47. package/src/core/streaming-output.ts +100 -0
  48. package/src/core/system-prompt.python.test.ts +17 -0
  49. package/src/core/system-prompt.ts +3 -1
  50. package/src/core/timings.ts +1 -1
  51. package/src/core/tools/bash.ts +13 -2
  52. package/src/core/tools/edit-diff.ts +9 -1
  53. package/src/core/tools/index.test.ts +50 -23
  54. package/src/core/tools/index.ts +83 -1
  55. package/src/core/tools/python-execution.test.ts +68 -0
  56. package/src/core/tools/python-fallback.test.ts +72 -0
  57. package/src/core/tools/python-renderer.test.ts +36 -0
  58. package/src/core/tools/python-tool-mode.test.ts +43 -0
  59. package/src/core/tools/python.test.ts +121 -0
  60. package/src/core/tools/python.ts +760 -0
  61. package/src/core/tools/renderers.ts +2 -0
  62. package/src/core/tools/schema-validation.test.ts +1 -0
  63. package/src/core/tools/task/executor.ts +146 -3
  64. package/src/core/tools/task/worker-protocol.ts +32 -2
  65. package/src/core/tools/task/worker.ts +182 -15
  66. package/src/index.ts +6 -0
  67. package/src/main.ts +136 -40
  68. package/src/modes/interactive/components/custom-editor.ts +16 -31
  69. package/src/modes/interactive/components/extensions/extension-dashboard.ts +5 -16
  70. package/src/modes/interactive/components/extensions/extension-list.ts +5 -13
  71. package/src/modes/interactive/components/history-search.ts +5 -8
  72. package/src/modes/interactive/components/hook-editor.ts +3 -4
  73. package/src/modes/interactive/components/hook-input.ts +3 -3
  74. package/src/modes/interactive/components/hook-selector.ts +5 -15
  75. package/src/modes/interactive/components/index.ts +1 -0
  76. package/src/modes/interactive/components/keybinding-hints.ts +66 -0
  77. package/src/modes/interactive/components/model-selector.ts +53 -66
  78. package/src/modes/interactive/components/oauth-selector.ts +5 -5
  79. package/src/modes/interactive/components/session-selector.ts +29 -23
  80. package/src/modes/interactive/components/settings-defs.ts +404 -196
  81. package/src/modes/interactive/components/settings-selector.ts +14 -10
  82. package/src/modes/interactive/components/status-line-segment-editor.ts +7 -7
  83. package/src/modes/interactive/components/tool-execution.ts +8 -0
  84. package/src/modes/interactive/components/tree-selector.ts +29 -23
  85. package/src/modes/interactive/components/user-message-selector.ts +6 -17
  86. package/src/modes/interactive/controllers/command-controller.ts +86 -37
  87. package/src/modes/interactive/controllers/event-controller.ts +8 -0
  88. package/src/modes/interactive/controllers/extension-ui-controller.ts +51 -0
  89. package/src/modes/interactive/controllers/input-controller.ts +42 -6
  90. package/src/modes/interactive/interactive-mode.ts +56 -30
  91. package/src/modes/interactive/theme/theme-schema.json +2 -2
  92. package/src/modes/interactive/types.ts +6 -1
  93. package/src/modes/interactive/utils/ui-helpers.ts +2 -1
  94. package/src/modes/print-mode.ts +23 -0
  95. package/src/modes/rpc/rpc-mode.ts +21 -0
  96. package/src/prompts/agents/reviewer.md +1 -1
  97. package/src/prompts/system/system-prompt.md +32 -1
  98. package/src/prompts/tools/python.md +91 -0
@@ -2,7 +2,7 @@
2
2
  * Simple text input component for hooks.
3
3
  */
4
4
 
5
- import { Container, Input, isEnter, isEscape, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
5
+ import { Container, Input, matchesKey, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
6
6
  import { theme } from "../theme/theme";
7
7
  import { CountdownTimer } from "./countdown-timer";
8
8
  import { DynamicBorder } from "./dynamic-border";
@@ -58,9 +58,9 @@ export class HookInputComponent extends Container {
58
58
  }
59
59
 
60
60
  handleInput(keyData: string): void {
61
- if (isEnter(keyData) || keyData === "\n") {
61
+ if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
62
62
  this.onSubmitCallback(this.input.getValue());
63
- } else if (isEscape(keyData)) {
63
+ } else if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
64
64
  this.onCancelCallback();
65
65
  } else {
66
66
  this.input.handleInput(keyData);
@@ -3,17 +3,7 @@
3
3
  * Displays a list of string options with keyboard navigation.
4
4
  */
5
5
 
6
- import {
7
- Container,
8
- isArrowDown,
9
- isArrowUp,
10
- isCtrlC,
11
- isEnter,
12
- isEscape,
13
- Spacer,
14
- Text,
15
- type TUI,
16
- } from "@oh-my-pi/pi-tui";
6
+ import { Container, matchesKey, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
17
7
  import { theme } from "../theme/theme";
18
8
  import { CountdownTimer } from "./countdown-timer";
19
9
  import { DynamicBorder } from "./dynamic-border";
@@ -87,16 +77,16 @@ export class HookSelectorComponent extends Container {
87
77
  }
88
78
 
89
79
  handleInput(keyData: string): void {
90
- if (isArrowUp(keyData) || keyData === "k") {
80
+ if (matchesKey(keyData, "up") || keyData === "k") {
91
81
  this.selectedIndex = Math.max(0, this.selectedIndex - 1);
92
82
  this.updateList();
93
- } else if (isArrowDown(keyData) || keyData === "j") {
83
+ } else if (matchesKey(keyData, "down") || keyData === "j") {
94
84
  this.selectedIndex = Math.min(this.options.length - 1, this.selectedIndex + 1);
95
85
  this.updateList();
96
- } else if (isEnter(keyData) || keyData === "\n") {
86
+ } else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
97
87
  const selected = this.options[this.selectedIndex];
98
88
  if (selected) this.onSelectCallback(selected);
99
- } else if (isEscape(keyData) || isCtrlC(keyData)) {
89
+ } else if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc") || matchesKey(keyData, "ctrl+c")) {
100
90
  this.onCancelCallback();
101
91
  }
102
92
  }
@@ -15,6 +15,7 @@ export { HookEditorComponent } from "./hook-editor";
15
15
  export { HookInputComponent, type HookInputOptions } from "./hook-input";
16
16
  export { HookMessageComponent } from "./hook-message";
17
17
  export { HookSelectorComponent } from "./hook-selector";
18
+ export { appKey, appKeyHint, editorKey, keyHint, rawKeyHint } from "./keybinding-hints";
18
19
  export { LoginDialogComponent } from "./login-dialog";
19
20
  export { ModelSelectorComponent } from "./model-selector";
20
21
  export { OAuthSelectorComponent } from "./oauth-selector";
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Utilities for formatting keybinding hints in the UI.
3
+ */
4
+
5
+ import { type EditorAction, getEditorKeybindings, type KeyId } from "@oh-my-pi/pi-tui";
6
+ import type { AppAction, KeybindingsManager } from "../../../core/keybindings";
7
+ import { theme } from "../theme/theme";
8
+
9
+ /**
10
+ * Format keys array as display string (e.g., ["ctrl+c", "escape"] -> "ctrl+c/escape").
11
+ */
12
+ function formatKeys(keys: KeyId[]): string {
13
+ if (keys.length === 0) return "";
14
+ if (keys.length === 1) return keys[0]!;
15
+ return keys.join("/");
16
+ }
17
+
18
+ /**
19
+ * Get display string for an editor action.
20
+ */
21
+ export function editorKey(action: EditorAction): string {
22
+ return formatKeys(getEditorKeybindings().getKeys(action));
23
+ }
24
+
25
+ /**
26
+ * Get display string for an app action.
27
+ */
28
+ export function appKey(keybindings: KeybindingsManager, action: AppAction): string {
29
+ return formatKeys(keybindings.getKeys(action));
30
+ }
31
+
32
+ /**
33
+ * Format a keybinding hint with consistent styling: dim key, muted description.
34
+ * Looks up the key from editor keybindings automatically.
35
+ *
36
+ * @param action - Editor action name (e.g., "selectConfirm", "expandTools")
37
+ * @param description - Description text (e.g., "to expand", "cancel")
38
+ * @returns Formatted string with dim key and muted description
39
+ */
40
+ export function keyHint(action: EditorAction, description: string): string {
41
+ return theme.fg("dim", editorKey(action)) + theme.fg("muted", ` ${description}`);
42
+ }
43
+
44
+ /**
45
+ * Format a keybinding hint for app-level actions.
46
+ * Requires the KeybindingsManager instance.
47
+ *
48
+ * @param keybindings - KeybindingsManager instance
49
+ * @param action - App action name (e.g., "interrupt", "externalEditor")
50
+ * @param description - Description text
51
+ * @returns Formatted string with dim key and muted description
52
+ */
53
+ export function appKeyHint(keybindings: KeybindingsManager, action: AppAction, description: string): string {
54
+ return theme.fg("dim", appKey(keybindings, action)) + theme.fg("muted", ` ${description}`);
55
+ }
56
+
57
+ /**
58
+ * Format a raw key string with description (for non-configurable keys like ↑↓).
59
+ *
60
+ * @param key - Raw key string
61
+ * @param description - Description text
62
+ * @returns Formatted string with dim key and muted description
63
+ */
64
+ export function rawKeyHint(key: string, description: string): string {
65
+ return theme.fg("dim", key) + theme.fg("muted", ` ${description}`);
66
+ }
@@ -2,16 +2,11 @@ import { type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
2
2
  import {
3
3
  Container,
4
4
  Input,
5
- isArrowDown,
6
- isArrowLeft,
7
- isArrowRight,
8
- isArrowUp,
9
- isCtrlC,
10
- isEnter,
11
- isEscape,
12
- isShiftTab,
13
- isTab,
5
+ matchesKey,
14
6
  Spacer,
7
+ type Tab,
8
+ TabBar,
9
+ type TabBarTheme,
15
10
  Text,
16
11
  type TUI,
17
12
  visibleWidth,
@@ -20,9 +15,15 @@ import type { ModelRegistry } from "../../../core/model-registry";
20
15
  import { parseModelString } from "../../../core/model-resolver";
21
16
  import type { SettingsManager } from "../../../core/settings-manager";
22
17
  import { fuzzyFilter } from "../../../utils/fuzzy";
23
- import { theme } from "../theme/theme";
18
+ import { type ThemeColor, theme } from "../theme/theme";
24
19
  import { DynamicBorder } from "./dynamic-border";
25
20
 
21
+ function makeInvertedBadge(label: string, color: ThemeColor): string {
22
+ const fgAnsi = theme.getFgAnsi(color);
23
+ const bgAnsi = fgAnsi.replace(/\x1b\[38;/g, "\x1b[48;");
24
+ return `${bgAnsi}\x1b[30m ${label} \x1b[39m\x1b[49m`;
25
+ }
26
+
26
27
  interface ModelItem {
27
28
  provider: string;
28
29
  id: string;
@@ -49,6 +50,15 @@ const MENU_ACTIONS: MenuAction[] = [
49
50
 
50
51
  const ALL_TAB = "ALL";
51
52
 
53
+ function getTabBarTheme(): TabBarTheme {
54
+ return {
55
+ label: (text: string) => theme.bold(theme.fg("accent", text)),
56
+ activeTab: (text: string) => theme.bold(theme.bg("selectedBg", theme.fg("text", text))),
57
+ inactiveTab: (text: string) => theme.fg("muted", text),
58
+ hint: (text: string) => theme.fg("dim", text),
59
+ };
60
+ }
61
+
52
62
  /**
53
63
  * Component that renders a model selector with provider tabs and context menu.
54
64
  * - Tab/Arrow Left/Right: Switch between provider tabs
@@ -59,6 +69,7 @@ const ALL_TAB = "ALL";
59
69
  export class ModelSelectorComponent extends Container {
60
70
  private searchInput: Input;
61
71
  private headerContainer: Container;
72
+ private tabBar: TabBar | null = null;
62
73
  private listContainer: Container;
63
74
  private menuContainer: Container;
64
75
  private allModels: ModelItem[] = [];
@@ -268,30 +279,15 @@ export class ModelSelectorComponent extends Container {
268
279
  private updateTabBar(): void {
269
280
  this.headerContainer.clear();
270
281
 
271
- // Build tab bar line
272
- const parts: string[] = [];
273
- parts.push(theme.fg("muted", "Provider:"));
274
- parts.push(" ");
275
-
276
- for (let i = 0; i < this.providers.length; i++) {
277
- const provider = this.providers[i]!;
278
- const isActive = i === this.activeTabIndex;
279
-
280
- if (isActive) {
281
- parts.push(theme.fg("accent", `[ ${provider} ]`));
282
- } else {
283
- parts.push(theme.fg("muted", ` ${provider} `));
284
- }
285
-
286
- if (i < this.providers.length - 1) {
287
- parts.push(" ");
288
- }
289
- }
290
-
291
- parts.push(" ");
292
- parts.push(theme.fg("dim", `(${theme.nav.back}/${theme.nav.cursor} or Tab to switch)`));
293
-
294
- this.headerContainer.addChild(new Text(parts.join(""), 0, 0));
282
+ const tabs: Tab[] = this.providers.map((provider) => ({ id: provider, label: provider }));
283
+ const tabBar = new TabBar("Models", tabs, getTabBarTheme(), this.activeTabIndex);
284
+ tabBar.onTabChange = (_tab, index) => {
285
+ this.activeTabIndex = index;
286
+ this.selectedIndex = 0;
287
+ this.applyTabFilter();
288
+ };
289
+ this.tabBar = tabBar;
290
+ this.headerContainer.addChild(tabBar);
295
291
  }
296
292
 
297
293
  private getActiveProvider(): string {
@@ -312,6 +308,10 @@ export class ModelSelectorComponent extends Container {
312
308
  // If user is searching, auto-switch to ALL tab to show global results
313
309
  if (activeProvider !== ALL_TAB) {
314
310
  this.activeTabIndex = 0;
311
+ if (this.tabBar && this.tabBar.getActiveIndex() !== 0) {
312
+ this.tabBar.setActiveIndex(0);
313
+ return;
314
+ }
315
315
  this.updateTabBar();
316
316
  baseModels = this.allModels;
317
317
  }
@@ -352,28 +352,27 @@ export class ModelSelectorComponent extends Container {
352
352
  const isSmol = modelsAreEqual(this.smolModel, item.model);
353
353
  const isSlow = modelsAreEqual(this.slowModel, item.model);
354
354
 
355
- // Build role badges (right-aligned style)
355
+ // Build role badges (inverted: color as background, black text)
356
356
  const badges: string[] = [];
357
- if (isDefault) badges.push(theme.fg("success", `${theme.sep.pipe}DEFAULT${theme.sep.pipe}`));
358
- if (isSmol) badges.push(theme.fg("warning", `${theme.sep.pipe}SMOL${theme.sep.pipe}`));
359
- if (isSlow) badges.push(theme.fg("accent", `${theme.sep.pipe}SLOW${theme.sep.pipe}`));
357
+ if (isDefault) badges.push(makeInvertedBadge("DEFAULT", "success"));
358
+ if (isSmol) badges.push(makeInvertedBadge("SMOL", "warning"));
359
+ if (isSlow) badges.push(makeInvertedBadge("SLOW", "accent"));
360
360
  const badgeText = badges.length > 0 ? ` ${badges.join(" ")}` : "";
361
361
 
362
362
  let line = "";
363
363
  if (isSelected) {
364
364
  const prefix = theme.fg("accent", `${theme.nav.cursor} `);
365
- const modelText = item.id;
366
365
  if (showProvider) {
367
- const providerBadge = theme.fg("muted", `[${item.provider}]`);
368
- line = `${prefix}${theme.fg("accent", modelText)} ${providerBadge}${badgeText}`;
366
+ const providerPrefix = theme.fg("dim", `${item.provider}/`);
367
+ line = `${prefix}${providerPrefix}${theme.fg("accent", item.id)}${badgeText}`;
369
368
  } else {
370
- line = `${prefix}${theme.fg("accent", modelText)}${badgeText}`;
369
+ line = `${prefix}${theme.fg("accent", item.id)}${badgeText}`;
371
370
  }
372
371
  } else {
373
372
  const prefix = " ";
374
373
  if (showProvider) {
375
- const providerBadge = theme.fg("muted", `[${item.provider}]`);
376
- line = `${prefix}${item.id} ${providerBadge}${badgeText}`;
374
+ const providerPrefix = theme.fg("dim", `${item.provider}/`);
375
+ line = `${prefix}${providerPrefix}${item.id}${badgeText}`;
377
376
  } else {
378
377
  line = `${prefix}${item.id}${badgeText}`;
379
378
  }
@@ -461,25 +460,13 @@ export class ModelSelectorComponent extends Container {
461
460
  return;
462
461
  }
463
462
 
464
- // Tab bar navigation: Left/Right arrows or Tab/Shift+Tab
465
- if (isArrowLeft(keyData) || isShiftTab(keyData)) {
466
- this.activeTabIndex = (this.activeTabIndex - 1 + this.providers.length) % this.providers.length;
467
- this.updateTabBar();
468
- this.selectedIndex = 0;
469
- this.applyTabFilter();
470
- return;
471
- }
472
-
473
- if (isArrowRight(keyData) || isTab(keyData)) {
474
- this.activeTabIndex = (this.activeTabIndex + 1) % this.providers.length;
475
- this.updateTabBar();
476
- this.selectedIndex = 0;
477
- this.applyTabFilter();
463
+ // Tab bar navigation
464
+ if (this.tabBar?.handleInput(keyData)) {
478
465
  return;
479
466
  }
480
467
 
481
468
  // Up arrow - navigate list (wrap to bottom when at top)
482
- if (isArrowUp(keyData)) {
469
+ if (matchesKey(keyData, "up")) {
483
470
  if (this.filteredModels.length === 0) return;
484
471
  this.selectedIndex = this.selectedIndex === 0 ? this.filteredModels.length - 1 : this.selectedIndex - 1;
485
472
  this.updateList();
@@ -487,7 +474,7 @@ export class ModelSelectorComponent extends Container {
487
474
  }
488
475
 
489
476
  // Down arrow - navigate list (wrap to top when at bottom)
490
- if (isArrowDown(keyData)) {
477
+ if (matchesKey(keyData, "down")) {
491
478
  if (this.filteredModels.length === 0) return;
492
479
  this.selectedIndex = this.selectedIndex === this.filteredModels.length - 1 ? 0 : this.selectedIndex + 1;
493
480
  this.updateList();
@@ -495,7 +482,7 @@ export class ModelSelectorComponent extends Container {
495
482
  }
496
483
 
497
484
  // Enter - open context menu or select directly in temporary mode
498
- if (isEnter(keyData)) {
485
+ if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
499
486
  const selectedModel = this.filteredModels[this.selectedIndex];
500
487
  if (selectedModel) {
501
488
  if (this.temporaryOnly) {
@@ -509,7 +496,7 @@ export class ModelSelectorComponent extends Container {
509
496
  }
510
497
 
511
498
  // Escape or Ctrl+C - close selector
512
- if (isEscape(keyData) || isCtrlC(keyData)) {
499
+ if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc") || matchesKey(keyData, "ctrl+c")) {
513
500
  this.onCancelCallback();
514
501
  return;
515
502
  }
@@ -521,21 +508,21 @@ export class ModelSelectorComponent extends Container {
521
508
 
522
509
  private handleMenuInput(keyData: string): void {
523
510
  // Up arrow - navigate menu
524
- if (isArrowUp(keyData)) {
511
+ if (matchesKey(keyData, "up")) {
525
512
  this.menuSelectedIndex = (this.menuSelectedIndex - 1 + MENU_ACTIONS.length) % MENU_ACTIONS.length;
526
513
  this.updateMenu();
527
514
  return;
528
515
  }
529
516
 
530
517
  // Down arrow - navigate menu
531
- if (isArrowDown(keyData)) {
518
+ if (matchesKey(keyData, "down")) {
532
519
  this.menuSelectedIndex = (this.menuSelectedIndex + 1) % MENU_ACTIONS.length;
533
520
  this.updateMenu();
534
521
  return;
535
522
  }
536
523
 
537
524
  // Enter - confirm selection
538
- if (isEnter(keyData)) {
525
+ if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
539
526
  const selectedModel = this.filteredModels[this.selectedIndex];
540
527
  const action = MENU_ACTIONS[this.menuSelectedIndex];
541
528
  if (selectedModel && action) {
@@ -546,7 +533,7 @@ export class ModelSelectorComponent extends Container {
546
533
  }
547
534
 
548
535
  // Escape or Ctrl+C - close menu only
549
- if (isEscape(keyData) || isCtrlC(keyData)) {
536
+ if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc") || matchesKey(keyData, "ctrl+c")) {
550
537
  this.closeMenu();
551
538
  return;
552
539
  }
@@ -1,5 +1,5 @@
1
1
  import { getOAuthProviders, type OAuthProviderInfo } from "@oh-my-pi/pi-ai";
2
- import { Container, isArrowDown, isArrowUp, isCtrlC, isEnter, isEscape, Spacer, TruncatedText } from "@oh-my-pi/pi-tui";
2
+ import { Container, matchesKey, Spacer, TruncatedText } from "@oh-my-pi/pi-tui";
3
3
  import type { AuthStorage } from "../../../core/auth-storage";
4
4
  import { theme } from "../theme/theme";
5
5
  import { DynamicBorder } from "./dynamic-border";
@@ -101,7 +101,7 @@ export class OAuthSelectorComponent extends Container {
101
101
 
102
102
  handleInput(keyData: string): void {
103
103
  // Up arrow
104
- if (isArrowUp(keyData)) {
104
+ if (matchesKey(keyData, "up")) {
105
105
  if (this.allProviders.length > 0) {
106
106
  this.selectedIndex = this.selectedIndex === 0 ? this.allProviders.length - 1 : this.selectedIndex - 1;
107
107
  }
@@ -109,7 +109,7 @@ export class OAuthSelectorComponent extends Container {
109
109
  this.updateList();
110
110
  }
111
111
  // Down arrow
112
- else if (isArrowDown(keyData)) {
112
+ else if (matchesKey(keyData, "down")) {
113
113
  if (this.allProviders.length > 0) {
114
114
  this.selectedIndex = this.selectedIndex === this.allProviders.length - 1 ? 0 : this.selectedIndex + 1;
115
115
  }
@@ -117,7 +117,7 @@ export class OAuthSelectorComponent extends Container {
117
117
  this.updateList();
118
118
  }
119
119
  // Enter
120
- else if (isEnter(keyData)) {
120
+ else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
121
121
  const selectedProvider = this.allProviders[this.selectedIndex];
122
122
  if (selectedProvider?.available) {
123
123
  this.statusMessage = undefined;
@@ -128,7 +128,7 @@ export class OAuthSelectorComponent extends Container {
128
128
  }
129
129
  }
130
130
  // Escape or Ctrl+C
131
- else if (isEscape(keyData) || isCtrlC(keyData)) {
131
+ else if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc") || matchesKey(keyData, "ctrl+c")) {
132
132
  this.onCancelCallback();
133
133
  }
134
134
  }
@@ -2,13 +2,7 @@ import {
2
2
  type Component,
3
3
  Container,
4
4
  Input,
5
- isArrowDown,
6
- isArrowUp,
7
- isCtrlC,
8
- isEnter,
9
- isEscape,
10
- isPageDown,
11
- isPageUp,
5
+ matchesKey,
12
6
  Spacer,
13
7
  Text,
14
8
  truncateToWidth,
@@ -51,11 +45,17 @@ class SessionList implements Component {
51
45
  }
52
46
 
53
47
  private filterSessions(query: string): void {
54
- this.filteredSessions = fuzzyFilter(
55
- this.allSessions,
56
- query,
57
- (session) => `${session.id} ${session.allMessagesText}`,
58
- );
48
+ this.filteredSessions = fuzzyFilter(this.allSessions, query, (session) => {
49
+ const parts = [
50
+ session.id,
51
+ session.title ?? "",
52
+ session.cwd ?? "",
53
+ session.firstMessage ?? "",
54
+ session.allMessagesText,
55
+ session.path,
56
+ ];
57
+ return parts.filter(Boolean).join(" ");
58
+ });
59
59
  this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredSessions.length - 1));
60
60
  }
61
61
 
@@ -73,10 +73,16 @@ class SessionList implements Component {
73
73
  if (this.filteredSessions.length === 0) {
74
74
  if (this.showCwd) {
75
75
  // "All" scope - no sessions anywhere that match filter
76
- lines.push(theme.fg("muted", " No sessions found"));
76
+ lines.push(truncateToWidth(theme.fg("muted", " No sessions found"), width, theme.format.ellipsis));
77
77
  } else {
78
78
  // "Current folder" scope - hint to try "all"
79
- lines.push(theme.fg("muted", " No sessions in current folder. Press Tab to view all."));
79
+ lines.push(
80
+ truncateToWidth(
81
+ theme.fg("muted", " No sessions in current folder. Press Tab to view all."),
82
+ width,
83
+ theme.format.ellipsis,
84
+ ),
85
+ );
80
86
  }
81
87
  return lines;
82
88
  }
@@ -139,7 +145,7 @@ class SessionList implements Component {
139
145
  const modified = formatDate(session.modified);
140
146
  const msgCount = `${session.messageCount} message${session.messageCount !== 1 ? "s" : ""}`;
141
147
  const metadata = ` ${modified} ${theme.sep.dot} ${msgCount}`;
142
- const metadataLine = theme.fg("dim", truncateToWidth(metadata, width, ""));
148
+ const metadataLine = theme.fg("dim", truncateToWidth(metadata, width, theme.format.ellipsis));
143
149
 
144
150
  lines.push(metadataLine);
145
151
  lines.push(""); // Blank line between sessions
@@ -148,7 +154,7 @@ class SessionList implements Component {
148
154
  // Add scroll indicator if needed
149
155
  if (startIndex > 0 || endIndex < this.filteredSessions.length) {
150
156
  const scrollText = ` (${this.selectedIndex + 1}/${this.filteredSessions.length})`;
151
- const scrollInfo = theme.fg("muted", truncateToWidth(scrollText, width, ""));
157
+ const scrollInfo = theme.fg("muted", truncateToWidth(scrollText, width, theme.format.ellipsis));
152
158
  lines.push(scrollInfo);
153
159
  }
154
160
 
@@ -157,36 +163,36 @@ class SessionList implements Component {
157
163
 
158
164
  handleInput(keyData: string): void {
159
165
  // Up arrow
160
- if (isArrowUp(keyData)) {
166
+ if (matchesKey(keyData, "up")) {
161
167
  this.selectedIndex = Math.max(0, this.selectedIndex - 1);
162
168
  }
163
169
  // Down arrow
164
- else if (isArrowDown(keyData)) {
170
+ else if (matchesKey(keyData, "down")) {
165
171
  this.selectedIndex = Math.min(this.filteredSessions.length - 1, this.selectedIndex + 1);
166
172
  }
167
173
  // Page up - jump up by maxVisible items
168
- else if (isPageUp(keyData)) {
174
+ else if (matchesKey(keyData, "pageUp")) {
169
175
  this.selectedIndex = Math.max(0, this.selectedIndex - this.maxVisible);
170
176
  }
171
177
  // Page down - jump down by maxVisible items
172
- else if (isPageDown(keyData)) {
178
+ else if (matchesKey(keyData, "pageDown")) {
173
179
  this.selectedIndex = Math.min(this.filteredSessions.length - 1, this.selectedIndex + this.maxVisible);
174
180
  }
175
181
  // Enter
176
- else if (isEnter(keyData)) {
182
+ else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
177
183
  const selected = this.filteredSessions[this.selectedIndex];
178
184
  if (selected && this.onSelect) {
179
185
  this.onSelect(selected.path);
180
186
  }
181
187
  }
182
188
  // Escape - cancel
183
- else if (isEscape(keyData)) {
189
+ else if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
184
190
  if (this.onCancel) {
185
191
  this.onCancel();
186
192
  }
187
193
  }
188
194
  // Ctrl+C - exit
189
- else if (isCtrlC(keyData)) {
195
+ else if (matchesKey(keyData, "ctrl+c")) {
190
196
  this.onExit();
191
197
  }
192
198
  // Pass everything else to search input