@oh-my-pi/pi-coding-agent 5.4.2 → 5.6.7
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/CHANGELOG.md +103 -0
- package/docs/python-repl.md +77 -0
- package/examples/hooks/snake.ts +7 -7
- package/package.json +5 -5
- package/src/bun-imports.d.ts +6 -0
- package/src/cli/args.ts +7 -0
- package/src/cli/setup-cli.ts +231 -0
- package/src/cli.ts +2 -0
- package/src/core/agent-session.ts +118 -15
- package/src/core/bash-executor.ts +3 -84
- package/src/core/compaction/compaction.ts +10 -5
- package/src/core/extensions/index.ts +2 -0
- package/src/core/extensions/loader.ts +13 -1
- package/src/core/extensions/runner.ts +50 -2
- package/src/core/extensions/types.ts +67 -2
- package/src/core/keybindings.ts +51 -1
- package/src/core/prompt-templates.ts +15 -0
- package/src/core/python-executor-display.test.ts +42 -0
- package/src/core/python-executor-lifecycle.test.ts +99 -0
- package/src/core/python-executor-mapping.test.ts +41 -0
- package/src/core/python-executor-per-call.test.ts +49 -0
- package/src/core/python-executor-session.test.ts +103 -0
- package/src/core/python-executor-streaming.test.ts +77 -0
- package/src/core/python-executor-timeout.test.ts +35 -0
- package/src/core/python-executor.lifecycle.test.ts +139 -0
- package/src/core/python-executor.result.test.ts +49 -0
- package/src/core/python-executor.test.ts +180 -0
- package/src/core/python-executor.ts +313 -0
- package/src/core/python-gateway-coordinator.ts +832 -0
- package/src/core/python-kernel-display.test.ts +54 -0
- package/src/core/python-kernel-env.test.ts +138 -0
- package/src/core/python-kernel-session.test.ts +87 -0
- package/src/core/python-kernel-ws.test.ts +104 -0
- package/src/core/python-kernel.lifecycle.test.ts +249 -0
- package/src/core/python-kernel.test.ts +549 -0
- package/src/core/python-kernel.ts +1178 -0
- package/src/core/python-prelude.py +889 -0
- package/src/core/python-prelude.test.ts +140 -0
- package/src/core/python-prelude.ts +3 -0
- package/src/core/sdk.ts +24 -6
- package/src/core/session-manager.ts +174 -82
- package/src/core/settings-manager-python.test.ts +23 -0
- package/src/core/settings-manager.ts +202 -0
- package/src/core/streaming-output.test.ts +26 -0
- package/src/core/streaming-output.ts +100 -0
- package/src/core/system-prompt.python.test.ts +17 -0
- package/src/core/system-prompt.ts +3 -1
- package/src/core/timings.ts +1 -1
- package/src/core/tools/bash.ts +13 -2
- package/src/core/tools/edit-diff.ts +9 -1
- package/src/core/tools/index.test.ts +50 -23
- package/src/core/tools/index.ts +83 -1
- package/src/core/tools/python-execution.test.ts +68 -0
- package/src/core/tools/python-fallback.test.ts +72 -0
- package/src/core/tools/python-renderer.test.ts +36 -0
- package/src/core/tools/python-tool-mode.test.ts +43 -0
- package/src/core/tools/python.test.ts +121 -0
- package/src/core/tools/python.ts +760 -0
- package/src/core/tools/renderers.ts +2 -0
- package/src/core/tools/schema-validation.test.ts +1 -0
- package/src/core/tools/task/executor.ts +146 -3
- package/src/core/tools/task/worker-protocol.ts +32 -2
- package/src/core/tools/task/worker.ts +182 -15
- package/src/index.ts +6 -0
- package/src/main.ts +136 -40
- package/src/modes/interactive/components/custom-editor.ts +16 -31
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +5 -16
- package/src/modes/interactive/components/extensions/extension-list.ts +5 -13
- package/src/modes/interactive/components/history-search.ts +5 -8
- package/src/modes/interactive/components/hook-editor.ts +3 -4
- package/src/modes/interactive/components/hook-input.ts +3 -3
- package/src/modes/interactive/components/hook-selector.ts +5 -15
- package/src/modes/interactive/components/index.ts +1 -0
- package/src/modes/interactive/components/keybinding-hints.ts +66 -0
- package/src/modes/interactive/components/model-selector.ts +53 -66
- package/src/modes/interactive/components/oauth-selector.ts +5 -5
- package/src/modes/interactive/components/session-selector.ts +29 -23
- package/src/modes/interactive/components/settings-defs.ts +404 -196
- package/src/modes/interactive/components/settings-selector.ts +14 -10
- package/src/modes/interactive/components/status-line-segment-editor.ts +7 -7
- package/src/modes/interactive/components/tool-execution.ts +8 -0
- package/src/modes/interactive/components/tree-selector.ts +29 -23
- package/src/modes/interactive/components/user-message-selector.ts +6 -17
- package/src/modes/interactive/controllers/command-controller.ts +86 -37
- package/src/modes/interactive/controllers/event-controller.ts +8 -0
- package/src/modes/interactive/controllers/extension-ui-controller.ts +51 -0
- package/src/modes/interactive/controllers/input-controller.ts +42 -6
- package/src/modes/interactive/interactive-mode.ts +56 -30
- package/src/modes/interactive/theme/theme-schema.json +2 -2
- package/src/modes/interactive/types.ts +6 -1
- package/src/modes/interactive/utils/ui-helpers.ts +2 -1
- package/src/modes/print-mode.ts +23 -0
- package/src/modes/rpc/rpc-mode.ts +21 -0
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/system/system-prompt.md +32 -1
- package/src/prompts/tools/python.md +91 -0
- package/src/prompts/tools/task.md +5 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Simple text input component for hooks.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { Container, Input,
|
|
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 (
|
|
61
|
+
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
62
62
|
this.onSubmitCallback(this.input.getValue());
|
|
63
|
-
} else if (
|
|
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 (
|
|
80
|
+
if (matchesKey(keyData, "up") || keyData === "k") {
|
|
91
81
|
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
92
82
|
this.updateList();
|
|
93
|
-
} else if (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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 (
|
|
355
|
+
// Build role badges (inverted: color as background, black text)
|
|
356
356
|
const badges: string[] = [];
|
|
357
|
-
if (isDefault) badges.push(
|
|
358
|
-
if (isSmol) badges.push(
|
|
359
|
-
if (isSlow) badges.push(
|
|
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
|
|
368
|
-
line = `${prefix}${theme.fg("accent",
|
|
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",
|
|
369
|
+
line = `${prefix}${theme.fg("accent", item.id)}${badgeText}`;
|
|
371
370
|
}
|
|
372
371
|
} else {
|
|
373
372
|
const prefix = " ";
|
|
374
373
|
if (showProvider) {
|
|
375
|
-
const
|
|
376
|
-
line = `${prefix}${item.id}
|
|
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
|
|
465
|
-
if (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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,
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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(
|
|
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 (
|
|
166
|
+
if (matchesKey(keyData, "up")) {
|
|
161
167
|
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
162
168
|
}
|
|
163
169
|
// Down arrow
|
|
164
|
-
else if (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
195
|
+
else if (matchesKey(keyData, "ctrl+c")) {
|
|
190
196
|
this.onExit();
|
|
191
197
|
}
|
|
192
198
|
// Pass everything else to search input
|