@oh-my-pi/pi-coding-agent 1.340.0 → 2.0.1337
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 +115 -1
- package/README.md +1 -1
- package/examples/custom-tools/subagent/index.ts +1 -1
- package/package.json +5 -3
- package/src/cli/args.ts +13 -6
- package/src/cli/file-processor.ts +3 -3
- package/src/cli/list-models.ts +2 -2
- package/src/cli/plugin-cli.ts +1 -1
- package/src/cli/session-picker.ts +2 -2
- package/src/cli.ts +1 -1
- package/src/config.ts +3 -3
- package/src/core/agent-session.ts +189 -29
- package/src/core/bash-executor.ts +50 -10
- package/src/core/compaction/branch-summarization.ts +5 -5
- package/src/core/compaction/compaction.ts +3 -3
- package/src/core/compaction/index.ts +3 -3
- package/src/core/custom-commands/bundled/review/index.ts +156 -0
- package/src/core/custom-commands/index.ts +15 -0
- package/src/core/custom-commands/loader.ts +232 -0
- package/src/core/custom-commands/types.ts +112 -0
- package/src/core/custom-tools/index.ts +3 -3
- package/src/core/custom-tools/loader.ts +10 -8
- package/src/core/custom-tools/types.ts +11 -6
- package/src/core/custom-tools/wrapper.ts +2 -1
- package/src/core/exec.ts +22 -12
- package/src/core/export-html/index.ts +5 -5
- package/src/core/file-mentions.ts +54 -0
- package/src/core/hooks/index.ts +5 -5
- package/src/core/hooks/loader.ts +21 -16
- package/src/core/hooks/runner.ts +6 -6
- package/src/core/hooks/tool-wrapper.ts +2 -2
- package/src/core/hooks/types.ts +12 -15
- package/src/core/index.ts +6 -6
- package/src/core/logger.ts +112 -0
- package/src/core/mcp/client.ts +3 -3
- package/src/core/mcp/config.ts +1 -1
- package/src/core/mcp/index.ts +12 -12
- package/src/core/mcp/loader.ts +2 -2
- package/src/core/mcp/manager.ts +6 -6
- package/src/core/mcp/tool-bridge.ts +3 -3
- package/src/core/mcp/transports/http.ts +1 -1
- package/src/core/mcp/transports/index.ts +2 -2
- package/src/core/mcp/transports/stdio.ts +1 -1
- package/src/core/messages.ts +22 -0
- package/src/core/model-registry.ts +2 -2
- package/src/core/model-resolver.ts +103 -2
- package/src/core/plugins/doctor.ts +1 -1
- package/src/core/plugins/index.ts +6 -6
- package/src/core/plugins/installer.ts +4 -4
- package/src/core/plugins/loader.ts +4 -9
- package/src/core/plugins/manager.ts +5 -5
- package/src/core/plugins/paths.ts +3 -3
- package/src/core/sdk.ts +127 -52
- package/src/core/session-manager.ts +123 -20
- package/src/core/settings-manager.ts +106 -22
- package/src/core/skills.ts +5 -5
- package/src/core/slash-commands.ts +60 -45
- package/src/core/system-prompt.ts +6 -6
- package/src/core/title-generator.ts +94 -0
- package/src/core/tools/bash.ts +33 -157
- package/src/core/tools/context.ts +2 -2
- package/src/core/tools/edit-diff.ts +5 -5
- package/src/core/tools/edit.ts +60 -9
- package/src/core/tools/exa/company.ts +3 -3
- package/src/core/tools/exa/index.ts +16 -17
- package/src/core/tools/exa/linkedin.ts +3 -3
- package/src/core/tools/exa/mcp-client.ts +9 -9
- package/src/core/tools/exa/render.ts +5 -5
- package/src/core/tools/exa/researcher.ts +3 -3
- package/src/core/tools/exa/search.ts +6 -5
- package/src/core/tools/exa/types.ts +5 -6
- package/src/core/tools/exa/websets.ts +3 -3
- package/src/core/tools/find.ts +3 -3
- package/src/core/tools/grep.ts +6 -5
- package/src/core/tools/index.ts +114 -40
- package/src/core/tools/ls.ts +4 -4
- package/src/core/tools/lsp/client.ts +204 -108
- package/src/core/tools/lsp/config.ts +709 -35
- package/src/core/tools/lsp/edits.ts +2 -2
- package/src/core/tools/lsp/index.ts +432 -30
- package/src/core/tools/lsp/render.ts +2 -2
- package/src/core/tools/lsp/rust-analyzer.ts +3 -3
- package/src/core/tools/lsp/types.ts +5 -0
- package/src/core/tools/lsp/utils.ts +1 -1
- package/src/core/tools/notebook.ts +1 -1
- package/src/core/tools/output.ts +175 -0
- package/src/core/tools/read.ts +7 -7
- package/src/core/tools/renderers.ts +92 -13
- package/src/core/tools/review.ts +268 -0
- package/src/core/tools/task/agents.ts +1 -1
- package/src/core/tools/task/bundled-agents/explore.md +1 -1
- package/src/core/tools/task/bundled-agents/reviewer.md +53 -38
- package/src/core/tools/task/discovery.ts +2 -2
- package/src/core/tools/task/executor.ts +145 -28
- package/src/core/tools/task/index.ts +78 -30
- package/src/core/tools/task/model-resolver.ts +72 -13
- package/src/core/tools/task/parallel.ts +1 -1
- package/src/core/tools/task/render.ts +219 -30
- package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
- package/src/core/tools/task/types.ts +36 -2
- package/src/core/tools/web-fetch.ts +5 -3
- package/src/core/tools/web-search/auth.ts +1 -1
- package/src/core/tools/web-search/index.ts +17 -15
- package/src/core/tools/web-search/providers/anthropic.ts +2 -2
- package/src/core/tools/web-search/providers/exa.ts +3 -5
- package/src/core/tools/web-search/providers/perplexity.ts +1 -1
- package/src/core/tools/web-search/render.ts +3 -3
- package/src/core/tools/write.ts +70 -7
- package/src/index.ts +33 -17
- package/src/main.ts +60 -34
- package/src/migrations.ts +3 -3
- package/src/modes/index.ts +5 -5
- package/src/modes/interactive/components/armin.ts +1 -1
- package/src/modes/interactive/components/assistant-message.ts +1 -1
- package/src/modes/interactive/components/bash-execution.ts +4 -4
- package/src/modes/interactive/components/bordered-loader.ts +2 -2
- package/src/modes/interactive/components/branch-summary-message.ts +2 -2
- package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
- package/src/modes/interactive/components/diff.ts +1 -1
- package/src/modes/interactive/components/dynamic-border.ts +1 -1
- package/src/modes/interactive/components/footer.ts +5 -5
- package/src/modes/interactive/components/hook-editor.ts +2 -2
- package/src/modes/interactive/components/hook-input.ts +2 -2
- package/src/modes/interactive/components/hook-message.ts +3 -3
- package/src/modes/interactive/components/hook-selector.ts +2 -2
- package/src/modes/interactive/components/model-selector.ts +341 -41
- package/src/modes/interactive/components/oauth-selector.ts +3 -3
- package/src/modes/interactive/components/plugin-settings.ts +4 -4
- package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
- package/src/modes/interactive/components/session-selector.ts +24 -11
- package/src/modes/interactive/components/settings-defs.ts +51 -3
- package/src/modes/interactive/components/settings-selector.ts +13 -16
- package/src/modes/interactive/components/show-images-selector.ts +2 -2
- package/src/modes/interactive/components/theme-selector.ts +2 -2
- package/src/modes/interactive/components/thinking-selector.ts +2 -2
- package/src/modes/interactive/components/tool-execution.ts +44 -8
- package/src/modes/interactive/components/tree-selector.ts +5 -5
- package/src/modes/interactive/components/user-message-selector.ts +2 -2
- package/src/modes/interactive/components/user-message.ts +1 -1
- package/src/modes/interactive/components/welcome.ts +42 -5
- package/src/modes/interactive/interactive-mode.ts +169 -48
- package/src/modes/interactive/theme/theme.ts +8 -7
- package/src/modes/print-mode.ts +4 -3
- package/src/modes/rpc/rpc-client.ts +4 -4
- package/src/modes/rpc/rpc-mode.ts +21 -11
- package/src/modes/rpc/rpc-types.ts +3 -3
- package/src/utils/changelog.ts +2 -2
- package/src/utils/clipboard.ts +1 -1
- package/src/utils/shell-snapshot.ts +218 -0
- package/src/utils/shell.ts +93 -13
- package/src/utils/tools-manager.ts +1 -1
- package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
- package/src/core/tools/exa/logger.ts +0 -56
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync, type FSWatcher, readFileSync, watch } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
1
4
|
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
2
5
|
import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { dirname, join } from "path";
|
|
6
|
-
import type { AgentSession } from "../../../core/agent-session.js";
|
|
7
|
-
import { theme } from "../theme/theme.js";
|
|
6
|
+
import type { AgentSession } from "../../../core/agent-session";
|
|
7
|
+
import { theme } from "../theme/theme";
|
|
8
8
|
|
|
9
9
|
// Nerd Font icons (matching Claude/statusline-nerd.sh)
|
|
10
10
|
const ICONS = {
|
|
@@ -7,8 +7,8 @@ import * as fs from "node:fs";
|
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import { Container, Editor, isCtrlG, isEscape, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
10
|
-
import { getEditorTheme, theme } from "../theme/theme
|
|
11
|
-
import { DynamicBorder } from "./dynamic-border
|
|
10
|
+
import { getEditorTheme, theme } from "../theme/theme";
|
|
11
|
+
import { DynamicBorder } from "./dynamic-border";
|
|
12
12
|
|
|
13
13
|
export class HookEditorComponent extends Container {
|
|
14
14
|
private editor: Editor;
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Container, Input, isEnter, isEscape, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
6
|
-
import { theme } from "../theme/theme
|
|
7
|
-
import { DynamicBorder } from "./dynamic-border
|
|
6
|
+
import { theme } from "../theme/theme";
|
|
7
|
+
import { DynamicBorder } from "./dynamic-border";
|
|
8
8
|
|
|
9
9
|
export class HookInputComponent extends Container {
|
|
10
10
|
private input: Input;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { TextContent } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
3
3
|
import { Box, Container, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
4
|
-
import type { HookMessageRenderer } from "../../../core/hooks/types
|
|
5
|
-
import type { HookMessage } from "../../../core/messages
|
|
6
|
-
import { getMarkdownTheme, theme } from "../theme/theme
|
|
4
|
+
import type { HookMessageRenderer } from "../../../core/hooks/types";
|
|
5
|
+
import type { HookMessage } from "../../../core/messages";
|
|
6
|
+
import { getMarkdownTheme, theme } from "../theme/theme";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Component that renders a custom message entry from hooks.
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { Container, isArrowDown, isArrowUp, isEnter, isEscape, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
7
|
-
import { theme } from "../theme/theme
|
|
8
|
-
import { DynamicBorder } from "./dynamic-border
|
|
7
|
+
import { theme } from "../theme/theme";
|
|
8
|
+
import { DynamicBorder } from "./dynamic-border";
|
|
9
9
|
|
|
10
10
|
export class HookSelectorComponent extends Container {
|
|
11
11
|
private options: string[];
|
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import { type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import {
|
|
3
|
+
Container,
|
|
4
|
+
Input,
|
|
5
|
+
isArrowDown,
|
|
6
|
+
isArrowLeft,
|
|
7
|
+
isArrowRight,
|
|
8
|
+
isArrowUp,
|
|
9
|
+
isEnter,
|
|
10
|
+
isEscape,
|
|
11
|
+
isShiftTab,
|
|
12
|
+
isTab,
|
|
13
|
+
Spacer,
|
|
14
|
+
Text,
|
|
15
|
+
type TUI,
|
|
16
|
+
} from "@oh-my-pi/pi-tui";
|
|
17
|
+
import type { ModelRegistry } from "../../../core/model-registry";
|
|
18
|
+
import { parseModelString } from "../../../core/model-resolver";
|
|
19
|
+
import type { SettingsManager } from "../../../core/settings-manager";
|
|
20
|
+
import { fuzzyFilter } from "../../../utils/fuzzy";
|
|
21
|
+
import { theme } from "../theme/theme";
|
|
22
|
+
import { DynamicBorder } from "./dynamic-border";
|
|
8
23
|
|
|
9
24
|
interface ModelItem {
|
|
10
25
|
provider: string;
|
|
@@ -17,31 +32,63 @@ interface ScopedModelItem {
|
|
|
17
32
|
thinkingLevel: string;
|
|
18
33
|
}
|
|
19
34
|
|
|
35
|
+
type ModelRole = "default" | "smol" | "slow";
|
|
36
|
+
|
|
37
|
+
interface MenuAction {
|
|
38
|
+
label: string;
|
|
39
|
+
role: ModelRole;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const MENU_ACTIONS: MenuAction[] = [
|
|
43
|
+
{ label: "Set as Default", role: "default" },
|
|
44
|
+
{ label: "Set as Smol (Fast)", role: "smol" },
|
|
45
|
+
{ label: "Set as Slow (Thinking)", role: "slow" },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const ALL_TAB = "ALL";
|
|
49
|
+
|
|
20
50
|
/**
|
|
21
|
-
* Component that renders a model selector with
|
|
51
|
+
* Component that renders a model selector with provider tabs and context menu.
|
|
52
|
+
* - Tab/Arrow Left/Right: Switch between provider tabs
|
|
53
|
+
* - Arrow Up/Down: Navigate model list
|
|
54
|
+
* - Enter: Open context menu to select action
|
|
55
|
+
* - Escape: Close menu or selector
|
|
22
56
|
*/
|
|
23
57
|
export class ModelSelectorComponent extends Container {
|
|
24
58
|
private searchInput: Input;
|
|
59
|
+
private headerContainer: Container;
|
|
25
60
|
private listContainer: Container;
|
|
61
|
+
private menuContainer: Container;
|
|
26
62
|
private allModels: ModelItem[] = [];
|
|
27
63
|
private filteredModels: ModelItem[] = [];
|
|
28
64
|
private selectedIndex: number = 0;
|
|
29
65
|
private currentModel?: Model<any>;
|
|
66
|
+
private defaultModel?: Model<any>;
|
|
67
|
+
private smolModel?: Model<any>;
|
|
68
|
+
private slowModel?: Model<any>;
|
|
30
69
|
private settingsManager: SettingsManager;
|
|
31
70
|
private modelRegistry: ModelRegistry;
|
|
32
|
-
private onSelectCallback: (model: Model<any
|
|
71
|
+
private onSelectCallback: (model: Model<any>, role: string) => void;
|
|
33
72
|
private onCancelCallback: () => void;
|
|
34
73
|
private errorMessage?: string;
|
|
35
74
|
private tui: TUI;
|
|
36
75
|
private scopedModels: ReadonlyArray<ScopedModelItem>;
|
|
37
76
|
|
|
77
|
+
// Tab state
|
|
78
|
+
private providers: string[] = [ALL_TAB];
|
|
79
|
+
private activeTabIndex: number = 0;
|
|
80
|
+
|
|
81
|
+
// Context menu state
|
|
82
|
+
private isMenuOpen: boolean = false;
|
|
83
|
+
private menuSelectedIndex: number = 0;
|
|
84
|
+
|
|
38
85
|
constructor(
|
|
39
86
|
tui: TUI,
|
|
40
87
|
currentModel: Model<any> | undefined,
|
|
41
88
|
settingsManager: SettingsManager,
|
|
42
89
|
modelRegistry: ModelRegistry,
|
|
43
90
|
scopedModels: ReadonlyArray<ScopedModelItem>,
|
|
44
|
-
onSelect: (model: Model<any
|
|
91
|
+
onSelect: (model: Model<any>, role: string) => void,
|
|
45
92
|
onCancel: () => void,
|
|
46
93
|
) {
|
|
47
94
|
super();
|
|
@@ -54,6 +101,9 @@ export class ModelSelectorComponent extends Container {
|
|
|
54
101
|
this.onSelectCallback = onSelect;
|
|
55
102
|
this.onCancelCallback = onCancel;
|
|
56
103
|
|
|
104
|
+
// Load current role assignments from settings
|
|
105
|
+
this._loadRoleModels();
|
|
106
|
+
|
|
57
107
|
// Add top border
|
|
58
108
|
this.addChild(new DynamicBorder());
|
|
59
109
|
this.addChild(new Spacer(1));
|
|
@@ -66,12 +116,18 @@ export class ModelSelectorComponent extends Container {
|
|
|
66
116
|
this.addChild(new Text(theme.fg("warning", hintText), 0, 0));
|
|
67
117
|
this.addChild(new Spacer(1));
|
|
68
118
|
|
|
119
|
+
// Create header container for tab bar
|
|
120
|
+
this.headerContainer = new Container();
|
|
121
|
+
this.addChild(this.headerContainer);
|
|
122
|
+
|
|
123
|
+
this.addChild(new Spacer(1));
|
|
124
|
+
|
|
69
125
|
// Create search input
|
|
70
126
|
this.searchInput = new Input();
|
|
71
127
|
this.searchInput.onSubmit = () => {
|
|
72
|
-
// Enter on search input
|
|
128
|
+
// Enter on search input opens menu if we have a selection
|
|
73
129
|
if (this.filteredModels[this.selectedIndex]) {
|
|
74
|
-
this.
|
|
130
|
+
this.openMenu();
|
|
75
131
|
}
|
|
76
132
|
};
|
|
77
133
|
this.addChild(this.searchInput);
|
|
@@ -82,6 +138,10 @@ export class ModelSelectorComponent extends Container {
|
|
|
82
138
|
this.listContainer = new Container();
|
|
83
139
|
this.addChild(this.listContainer);
|
|
84
140
|
|
|
141
|
+
// Create menu container (hidden by default)
|
|
142
|
+
this.menuContainer = new Container();
|
|
143
|
+
this.addChild(this.menuContainer);
|
|
144
|
+
|
|
85
145
|
this.addChild(new Spacer(1));
|
|
86
146
|
|
|
87
147
|
// Add bottom border
|
|
@@ -89,12 +149,46 @@ export class ModelSelectorComponent extends Container {
|
|
|
89
149
|
|
|
90
150
|
// Load models and do initial render
|
|
91
151
|
this.loadModels().then(() => {
|
|
152
|
+
this.buildProviderTabs();
|
|
153
|
+
this.updateTabBar();
|
|
92
154
|
this.updateList();
|
|
93
155
|
// Request re-render after models are loaded
|
|
94
156
|
this.tui.requestRender();
|
|
95
157
|
});
|
|
96
158
|
}
|
|
97
159
|
|
|
160
|
+
private _loadRoleModels(): void {
|
|
161
|
+
const roles = this.settingsManager.getModelRoles();
|
|
162
|
+
const allModels = this.modelRegistry.getAll();
|
|
163
|
+
|
|
164
|
+
// Load default model
|
|
165
|
+
const defaultStr = roles.default;
|
|
166
|
+
if (defaultStr) {
|
|
167
|
+
const parsed = parseModelString(defaultStr);
|
|
168
|
+
if (parsed) {
|
|
169
|
+
this.defaultModel = allModels.find((m) => m.provider === parsed.provider && m.id === parsed.id);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Load smol model
|
|
174
|
+
const smolStr = roles.smol;
|
|
175
|
+
if (smolStr) {
|
|
176
|
+
const parsed = parseModelString(smolStr);
|
|
177
|
+
if (parsed) {
|
|
178
|
+
this.smolModel = allModels.find((m) => m.provider === parsed.provider && m.id === parsed.id);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Load slow model
|
|
183
|
+
const slowStr = roles.slow;
|
|
184
|
+
if (slowStr) {
|
|
185
|
+
const parsed = parseModelString(slowStr);
|
|
186
|
+
if (parsed) {
|
|
187
|
+
this.slowModel = allModels.find((m) => m.provider === parsed.provider && m.id === parsed.id);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
98
192
|
private async loadModels(): Promise<void> {
|
|
99
193
|
let models: ModelItem[];
|
|
100
194
|
|
|
@@ -131,13 +225,15 @@ export class ModelSelectorComponent extends Container {
|
|
|
131
225
|
}
|
|
132
226
|
}
|
|
133
227
|
|
|
134
|
-
// Sort: current model first, then by provider
|
|
228
|
+
// Sort: current model first, then by provider, then by id
|
|
135
229
|
models.sort((a, b) => {
|
|
136
230
|
const aIsCurrent = modelsAreEqual(this.currentModel, a.model);
|
|
137
231
|
const bIsCurrent = modelsAreEqual(this.currentModel, b.model);
|
|
138
232
|
if (aIsCurrent && !bIsCurrent) return -1;
|
|
139
233
|
if (!aIsCurrent && bIsCurrent) return 1;
|
|
140
|
-
|
|
234
|
+
const providerCmp = a.provider.localeCompare(b.provider);
|
|
235
|
+
if (providerCmp !== 0) return providerCmp;
|
|
236
|
+
return a.id.localeCompare(b.id);
|
|
141
237
|
});
|
|
142
238
|
|
|
143
239
|
this.allModels = models;
|
|
@@ -145,12 +241,81 @@ export class ModelSelectorComponent extends Container {
|
|
|
145
241
|
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, models.length - 1));
|
|
146
242
|
}
|
|
147
243
|
|
|
244
|
+
private buildProviderTabs(): void {
|
|
245
|
+
// Extract unique providers from models
|
|
246
|
+
const providerSet = new Set<string>();
|
|
247
|
+
for (const item of this.allModels) {
|
|
248
|
+
providerSet.add(item.provider.toUpperCase());
|
|
249
|
+
}
|
|
250
|
+
// Sort providers alphabetically
|
|
251
|
+
const sortedProviders = Array.from(providerSet).sort();
|
|
252
|
+
this.providers = [ALL_TAB, ...sortedProviders];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private updateTabBar(): void {
|
|
256
|
+
this.headerContainer.clear();
|
|
257
|
+
|
|
258
|
+
// Build tab bar line
|
|
259
|
+
const parts: string[] = [];
|
|
260
|
+
parts.push(theme.fg("muted", "Provider:"));
|
|
261
|
+
parts.push(" ");
|
|
262
|
+
|
|
263
|
+
for (let i = 0; i < this.providers.length; i++) {
|
|
264
|
+
const provider = this.providers[i]!;
|
|
265
|
+
const isActive = i === this.activeTabIndex;
|
|
266
|
+
|
|
267
|
+
if (isActive) {
|
|
268
|
+
parts.push(theme.fg("accent", `[ ${provider} ]`));
|
|
269
|
+
} else {
|
|
270
|
+
parts.push(theme.fg("muted", ` ${provider} `));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (i < this.providers.length - 1) {
|
|
274
|
+
parts.push(" ");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
parts.push(" ");
|
|
279
|
+
parts.push(theme.fg("dim", "(←/→ or Tab to switch)"));
|
|
280
|
+
|
|
281
|
+
this.headerContainer.addChild(new Text(parts.join(""), 0, 0));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private getActiveProvider(): string {
|
|
285
|
+
return this.providers[this.activeTabIndex] ?? ALL_TAB;
|
|
286
|
+
}
|
|
287
|
+
|
|
148
288
|
private filterModels(query: string): void {
|
|
149
|
-
|
|
289
|
+
const activeProvider = this.getActiveProvider();
|
|
290
|
+
|
|
291
|
+
// Start with all models or filter by provider
|
|
292
|
+
let baseModels = this.allModels;
|
|
293
|
+
if (activeProvider !== ALL_TAB) {
|
|
294
|
+
baseModels = this.allModels.filter((m) => m.provider.toUpperCase() === activeProvider);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Apply fuzzy filter if query is present
|
|
298
|
+
if (query.trim()) {
|
|
299
|
+
// If user is searching, auto-switch to ALL tab to show global results
|
|
300
|
+
if (activeProvider !== ALL_TAB) {
|
|
301
|
+
this.activeTabIndex = 0;
|
|
302
|
+
this.updateTabBar();
|
|
303
|
+
baseModels = this.allModels;
|
|
304
|
+
}
|
|
305
|
+
this.filteredModels = fuzzyFilter(baseModels, query, ({ id, provider }) => `${id} ${provider}`);
|
|
306
|
+
} else {
|
|
307
|
+
this.filteredModels = baseModels;
|
|
308
|
+
}
|
|
309
|
+
|
|
150
310
|
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));
|
|
151
311
|
this.updateList();
|
|
152
312
|
}
|
|
153
313
|
|
|
314
|
+
private applyTabFilter(): void {
|
|
315
|
+
const query = this.searchInput.getValue();
|
|
316
|
+
this.filterModels(query);
|
|
317
|
+
}
|
|
318
|
+
|
|
154
319
|
private updateList(): void {
|
|
155
320
|
this.listContainer.clear();
|
|
156
321
|
|
|
@@ -161,26 +326,44 @@ export class ModelSelectorComponent extends Container {
|
|
|
161
326
|
);
|
|
162
327
|
const endIndex = Math.min(startIndex + maxVisible, this.filteredModels.length);
|
|
163
328
|
|
|
329
|
+
const activeProvider = this.getActiveProvider();
|
|
330
|
+
const showProvider = activeProvider === ALL_TAB;
|
|
331
|
+
|
|
164
332
|
// Show visible slice of filtered models
|
|
165
333
|
for (let i = startIndex; i < endIndex; i++) {
|
|
166
334
|
const item = this.filteredModels[i];
|
|
167
335
|
if (!item) continue;
|
|
168
336
|
|
|
169
337
|
const isSelected = i === this.selectedIndex;
|
|
170
|
-
const
|
|
338
|
+
const isDefault = modelsAreEqual(this.defaultModel, item.model);
|
|
339
|
+
const isSmol = modelsAreEqual(this.smolModel, item.model);
|
|
340
|
+
const isSlow = modelsAreEqual(this.slowModel, item.model);
|
|
341
|
+
|
|
342
|
+
// Build role badges (right-aligned style)
|
|
343
|
+
const badges: string[] = [];
|
|
344
|
+
if (isDefault) badges.push(theme.fg("success", "[ DEFAULT ]"));
|
|
345
|
+
if (isSmol) badges.push(theme.fg("warning", "[ SMOL ]"));
|
|
346
|
+
if (isSlow) badges.push(theme.fg("accent", "[ SLOW ]"));
|
|
347
|
+
const badgeText = badges.length > 0 ? ` ${badges.join(" ")}` : "";
|
|
171
348
|
|
|
172
349
|
let line = "";
|
|
173
350
|
if (isSelected) {
|
|
174
351
|
const prefix = theme.fg("accent", "→ ");
|
|
175
|
-
const modelText =
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
352
|
+
const modelText = item.id;
|
|
353
|
+
if (showProvider) {
|
|
354
|
+
const providerBadge = theme.fg("muted", `[${item.provider}]`);
|
|
355
|
+
line = `${prefix}${theme.fg("accent", modelText)} ${providerBadge}${badgeText}`;
|
|
356
|
+
} else {
|
|
357
|
+
line = `${prefix}${theme.fg("accent", modelText)}${badgeText}`;
|
|
358
|
+
}
|
|
179
359
|
} else {
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
360
|
+
const prefix = " ";
|
|
361
|
+
if (showProvider) {
|
|
362
|
+
const providerBadge = theme.fg("muted", `[${item.provider}]`);
|
|
363
|
+
line = `${prefix}${item.id} ${providerBadge}${badgeText}`;
|
|
364
|
+
} else {
|
|
365
|
+
line = `${prefix}${item.id}${badgeText}`;
|
|
366
|
+
}
|
|
184
367
|
}
|
|
185
368
|
|
|
186
369
|
this.listContainer.addChild(new Text(line, 0, 0));
|
|
@@ -194,7 +377,6 @@ export class ModelSelectorComponent extends Container {
|
|
|
194
377
|
|
|
195
378
|
// Show error message or "no results" if empty
|
|
196
379
|
if (this.errorMessage) {
|
|
197
|
-
// Show error in red
|
|
198
380
|
const errorLines = this.errorMessage.split("\n");
|
|
199
381
|
for (const line of errorLines) {
|
|
200
382
|
this.listContainer.addChild(new Text(theme.fg("error", line), 0, 0));
|
|
@@ -204,41 +386,159 @@ export class ModelSelectorComponent extends Container {
|
|
|
204
386
|
}
|
|
205
387
|
}
|
|
206
388
|
|
|
389
|
+
private openMenu(): void {
|
|
390
|
+
if (this.filteredModels.length === 0) return;
|
|
391
|
+
|
|
392
|
+
this.isMenuOpen = true;
|
|
393
|
+
this.menuSelectedIndex = 0;
|
|
394
|
+
this.updateMenu();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private closeMenu(): void {
|
|
398
|
+
this.isMenuOpen = false;
|
|
399
|
+
this.menuContainer.clear();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private updateMenu(): void {
|
|
403
|
+
this.menuContainer.clear();
|
|
404
|
+
|
|
405
|
+
const selectedModel = this.filteredModels[this.selectedIndex];
|
|
406
|
+
if (!selectedModel) return;
|
|
407
|
+
|
|
408
|
+
// Menu header
|
|
409
|
+
this.menuContainer.addChild(new Spacer(1));
|
|
410
|
+
this.menuContainer.addChild(new Text(theme.fg("border", "─".repeat(40)), 0, 0));
|
|
411
|
+
this.menuContainer.addChild(new Text(theme.fg("text", ` Action for: ${theme.bold(selectedModel.id)}`), 0, 0));
|
|
412
|
+
this.menuContainer.addChild(new Spacer(1));
|
|
413
|
+
|
|
414
|
+
// Menu options
|
|
415
|
+
for (let i = 0; i < MENU_ACTIONS.length; i++) {
|
|
416
|
+
const action = MENU_ACTIONS[i]!;
|
|
417
|
+
const isSelected = i === this.menuSelectedIndex;
|
|
418
|
+
|
|
419
|
+
let line: string;
|
|
420
|
+
if (isSelected) {
|
|
421
|
+
line = theme.fg("accent", ` → ${action.label}`);
|
|
422
|
+
} else {
|
|
423
|
+
line = theme.fg("muted", ` ${action.label}`);
|
|
424
|
+
}
|
|
425
|
+
this.menuContainer.addChild(new Text(line, 0, 0));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
this.menuContainer.addChild(new Spacer(1));
|
|
429
|
+
this.menuContainer.addChild(new Text(theme.fg("dim", " Enter: confirm Esc: cancel"), 0, 0));
|
|
430
|
+
this.menuContainer.addChild(new Text(theme.fg("border", "─".repeat(40)), 0, 0));
|
|
431
|
+
}
|
|
432
|
+
|
|
207
433
|
handleInput(keyData: string): void {
|
|
208
|
-
|
|
434
|
+
if (this.isMenuOpen) {
|
|
435
|
+
this.handleMenuInput(keyData);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Tab bar navigation: Left/Right arrows or Tab/Shift+Tab
|
|
440
|
+
if (isArrowLeft(keyData) || isShiftTab(keyData)) {
|
|
441
|
+
this.activeTabIndex = (this.activeTabIndex - 1 + this.providers.length) % this.providers.length;
|
|
442
|
+
this.updateTabBar();
|
|
443
|
+
this.selectedIndex = 0;
|
|
444
|
+
this.applyTabFilter();
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (isArrowRight(keyData) || isTab(keyData)) {
|
|
449
|
+
this.activeTabIndex = (this.activeTabIndex + 1) % this.providers.length;
|
|
450
|
+
this.updateTabBar();
|
|
451
|
+
this.selectedIndex = 0;
|
|
452
|
+
this.applyTabFilter();
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Up arrow - navigate list (wrap to bottom when at top)
|
|
209
457
|
if (isArrowUp(keyData)) {
|
|
210
458
|
if (this.filteredModels.length === 0) return;
|
|
211
459
|
this.selectedIndex = this.selectedIndex === 0 ? this.filteredModels.length - 1 : this.selectedIndex - 1;
|
|
212
460
|
this.updateList();
|
|
461
|
+
return;
|
|
213
462
|
}
|
|
214
|
-
|
|
215
|
-
|
|
463
|
+
|
|
464
|
+
// Down arrow - navigate list (wrap to top when at bottom)
|
|
465
|
+
if (isArrowDown(keyData)) {
|
|
216
466
|
if (this.filteredModels.length === 0) return;
|
|
217
467
|
this.selectedIndex = this.selectedIndex === this.filteredModels.length - 1 ? 0 : this.selectedIndex + 1;
|
|
218
468
|
this.updateList();
|
|
469
|
+
return;
|
|
219
470
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (
|
|
224
|
-
this.
|
|
471
|
+
|
|
472
|
+
// Enter - open context menu
|
|
473
|
+
if (isEnter(keyData)) {
|
|
474
|
+
if (this.filteredModels[this.selectedIndex]) {
|
|
475
|
+
this.openMenu();
|
|
225
476
|
}
|
|
477
|
+
return;
|
|
226
478
|
}
|
|
227
|
-
|
|
228
|
-
|
|
479
|
+
|
|
480
|
+
// Escape - close selector
|
|
481
|
+
if (isEscape(keyData)) {
|
|
229
482
|
this.onCancelCallback();
|
|
483
|
+
return;
|
|
230
484
|
}
|
|
485
|
+
|
|
231
486
|
// Pass everything else to search input
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
487
|
+
this.searchInput.handleInput(keyData);
|
|
488
|
+
this.filterModels(this.searchInput.getValue());
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private handleMenuInput(keyData: string): void {
|
|
492
|
+
// Up arrow - navigate menu
|
|
493
|
+
if (isArrowUp(keyData)) {
|
|
494
|
+
this.menuSelectedIndex = (this.menuSelectedIndex - 1 + MENU_ACTIONS.length) % MENU_ACTIONS.length;
|
|
495
|
+
this.updateMenu();
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Down arrow - navigate menu
|
|
500
|
+
if (isArrowDown(keyData)) {
|
|
501
|
+
this.menuSelectedIndex = (this.menuSelectedIndex + 1) % MENU_ACTIONS.length;
|
|
502
|
+
this.updateMenu();
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Enter - confirm selection
|
|
507
|
+
if (isEnter(keyData)) {
|
|
508
|
+
const selectedModel = this.filteredModels[this.selectedIndex];
|
|
509
|
+
const action = MENU_ACTIONS[this.menuSelectedIndex];
|
|
510
|
+
if (selectedModel && action) {
|
|
511
|
+
this.handleSelect(selectedModel.model, action.role);
|
|
512
|
+
this.closeMenu();
|
|
513
|
+
}
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Escape - close menu only
|
|
518
|
+
if (isEscape(keyData)) {
|
|
519
|
+
this.closeMenu();
|
|
520
|
+
return;
|
|
235
521
|
}
|
|
236
522
|
}
|
|
237
523
|
|
|
238
|
-
private handleSelect(model: Model<any
|
|
239
|
-
// Save
|
|
240
|
-
this.settingsManager.
|
|
241
|
-
|
|
524
|
+
private handleSelect(model: Model<any>, role: ModelRole): void {
|
|
525
|
+
// Save to settings
|
|
526
|
+
this.settingsManager.setModelRole(role, `${model.provider}/${model.id}`);
|
|
527
|
+
|
|
528
|
+
// Update local state for UI
|
|
529
|
+
if (role === "default") {
|
|
530
|
+
this.defaultModel = model;
|
|
531
|
+
} else if (role === "smol") {
|
|
532
|
+
this.smolModel = model;
|
|
533
|
+
} else if (role === "slow") {
|
|
534
|
+
this.slowModel = model;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Notify caller (for updating agent state if needed)
|
|
538
|
+
this.onSelectCallback(model, role);
|
|
539
|
+
|
|
540
|
+
// Update list to show new badges
|
|
541
|
+
this.updateList();
|
|
242
542
|
}
|
|
243
543
|
|
|
244
544
|
getSearchInput(): Input {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { getOAuthProviders, type OAuthProviderInfo } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import { Container, isArrowDown, isArrowUp, isEnter, isEscape, Spacer, TruncatedText } from "@oh-my-pi/pi-tui";
|
|
3
|
-
import type { AuthStorage } from "../../../core/auth-storage
|
|
4
|
-
import { theme } from "../theme/theme
|
|
5
|
-
import { DynamicBorder } from "./dynamic-border
|
|
3
|
+
import type { AuthStorage } from "../../../core/auth-storage";
|
|
4
|
+
import { theme } from "../theme/theme";
|
|
5
|
+
import { DynamicBorder } from "./dynamic-border";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Component that renders an OAuth provider selector
|
|
@@ -18,10 +18,10 @@ import {
|
|
|
18
18
|
Spacer,
|
|
19
19
|
Text,
|
|
20
20
|
} from "@oh-my-pi/pi-tui";
|
|
21
|
-
import { PluginManager } from "../../../core/plugins/manager
|
|
22
|
-
import type { InstalledPlugin, PluginSettingSchema } from "../../../core/plugins/types
|
|
23
|
-
import { getSelectListTheme, getSettingsListTheme, theme } from "../theme/theme
|
|
24
|
-
import { DynamicBorder } from "./dynamic-border
|
|
21
|
+
import { PluginManager } from "../../../core/plugins/manager";
|
|
22
|
+
import type { InstalledPlugin, PluginSettingSchema } from "../../../core/plugins/types";
|
|
23
|
+
import { getSelectListTheme, getSettingsListTheme, theme } from "../theme/theme";
|
|
24
|
+
import { DynamicBorder } from "./dynamic-border";
|
|
25
25
|
|
|
26
26
|
// =============================================================================
|
|
27
27
|
// Plugin List Component
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Container, type SelectItem, SelectList } from "@oh-my-pi/pi-tui";
|
|
2
|
-
import { getSelectListTheme } from "../theme/theme
|
|
3
|
-
import { DynamicBorder } from "./dynamic-border
|
|
2
|
+
import { getSelectListTheme } from "../theme/theme";
|
|
3
|
+
import { DynamicBorder } from "./dynamic-border";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Component that renders a queue mode selector with borders
|