@oh-my-pi/pi-coding-agent 0.1.0
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 +1629 -0
- package/README.md +1041 -0
- package/docs/compaction.md +403 -0
- package/docs/config-usage.md +113 -0
- package/docs/custom-tools.md +541 -0
- package/docs/extension-loading.md +1004 -0
- package/docs/hooks.md +867 -0
- package/docs/rpc.md +1040 -0
- package/docs/sdk.md +994 -0
- package/docs/session-tree-plan.md +441 -0
- package/docs/session.md +240 -0
- package/docs/skills.md +290 -0
- package/docs/theme.md +670 -0
- package/docs/tree.md +197 -0
- package/docs/tui.md +341 -0
- package/examples/README.md +21 -0
- package/examples/custom-tools/README.md +124 -0
- package/examples/custom-tools/hello/index.ts +20 -0
- package/examples/custom-tools/question/index.ts +84 -0
- package/examples/custom-tools/subagent/README.md +172 -0
- package/examples/custom-tools/subagent/agents/planner.md +37 -0
- package/examples/custom-tools/subagent/agents/scout.md +50 -0
- package/examples/custom-tools/subagent/agents/worker.md +24 -0
- package/examples/custom-tools/subagent/agents.ts +156 -0
- package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
- package/examples/custom-tools/subagent/commands/implement.md +10 -0
- package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
- package/examples/custom-tools/subagent/index.ts +1002 -0
- package/examples/custom-tools/todo/index.ts +212 -0
- package/examples/hooks/README.md +56 -0
- package/examples/hooks/auto-commit-on-exit.ts +49 -0
- package/examples/hooks/confirm-destructive.ts +59 -0
- package/examples/hooks/custom-compaction.ts +116 -0
- package/examples/hooks/dirty-repo-guard.ts +52 -0
- package/examples/hooks/file-trigger.ts +41 -0
- package/examples/hooks/git-checkpoint.ts +53 -0
- package/examples/hooks/handoff.ts +150 -0
- package/examples/hooks/permission-gate.ts +34 -0
- package/examples/hooks/protected-paths.ts +30 -0
- package/examples/hooks/qna.ts +119 -0
- package/examples/hooks/snake.ts +343 -0
- package/examples/hooks/status-line.ts +40 -0
- package/examples/sdk/01-minimal.ts +22 -0
- package/examples/sdk/02-custom-model.ts +49 -0
- package/examples/sdk/03-custom-prompt.ts +44 -0
- package/examples/sdk/04-skills.ts +44 -0
- package/examples/sdk/05-tools.ts +90 -0
- package/examples/sdk/06-hooks.ts +61 -0
- package/examples/sdk/07-context-files.ts +36 -0
- package/examples/sdk/08-slash-commands.ts +42 -0
- package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
- package/examples/sdk/10-settings.ts +38 -0
- package/examples/sdk/11-sessions.ts +48 -0
- package/examples/sdk/12-full-control.ts +95 -0
- package/examples/sdk/README.md +154 -0
- package/package.json +89 -0
- package/src/bun-imports.d.ts +16 -0
- package/src/capability/context-file.ts +40 -0
- package/src/capability/extension.ts +48 -0
- package/src/capability/hook.ts +40 -0
- package/src/capability/index.ts +616 -0
- package/src/capability/instruction.ts +37 -0
- package/src/capability/mcp.ts +52 -0
- package/src/capability/prompt.ts +35 -0
- package/src/capability/rule.ts +56 -0
- package/src/capability/settings.ts +35 -0
- package/src/capability/skill.ts +49 -0
- package/src/capability/slash-command.ts +40 -0
- package/src/capability/system-prompt.ts +35 -0
- package/src/capability/tool.ts +38 -0
- package/src/capability/types.ts +166 -0
- package/src/cli/args.ts +259 -0
- package/src/cli/file-processor.ts +121 -0
- package/src/cli/list-models.ts +104 -0
- package/src/cli/plugin-cli.ts +661 -0
- package/src/cli/session-picker.ts +41 -0
- package/src/cli/update-cli.ts +274 -0
- package/src/cli.ts +10 -0
- package/src/config.ts +391 -0
- package/src/core/agent-session.ts +2178 -0
- package/src/core/auth-storage.ts +258 -0
- package/src/core/bash-executor.ts +197 -0
- package/src/core/compaction/branch-summarization.ts +315 -0
- package/src/core/compaction/compaction.ts +664 -0
- package/src/core/compaction/index.ts +7 -0
- package/src/core/compaction/utils.ts +153 -0
- 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 +226 -0
- package/src/core/custom-commands/types.ts +112 -0
- package/src/core/custom-tools/index.ts +22 -0
- package/src/core/custom-tools/loader.ts +248 -0
- package/src/core/custom-tools/types.ts +185 -0
- package/src/core/custom-tools/wrapper.ts +29 -0
- package/src/core/exec.ts +139 -0
- package/src/core/export-html/index.ts +159 -0
- package/src/core/export-html/template.css +774 -0
- package/src/core/export-html/template.generated.ts +2 -0
- package/src/core/export-html/template.html +45 -0
- package/src/core/export-html/template.js +1185 -0
- package/src/core/export-html/template.macro.ts +24 -0
- package/src/core/file-mentions.ts +54 -0
- package/src/core/hooks/index.ts +16 -0
- package/src/core/hooks/loader.ts +288 -0
- package/src/core/hooks/runner.ts +434 -0
- package/src/core/hooks/tool-wrapper.ts +98 -0
- package/src/core/hooks/types.ts +770 -0
- package/src/core/index.ts +53 -0
- package/src/core/logger.ts +112 -0
- package/src/core/mcp/client.ts +185 -0
- package/src/core/mcp/config.ts +248 -0
- package/src/core/mcp/index.ts +45 -0
- package/src/core/mcp/loader.ts +99 -0
- package/src/core/mcp/manager.ts +235 -0
- package/src/core/mcp/tool-bridge.ts +156 -0
- package/src/core/mcp/transports/http.ts +316 -0
- package/src/core/mcp/transports/index.ts +6 -0
- package/src/core/mcp/transports/stdio.ts +252 -0
- package/src/core/mcp/types.ts +228 -0
- package/src/core/messages.ts +211 -0
- package/src/core/model-registry.ts +334 -0
- package/src/core/model-resolver.ts +494 -0
- package/src/core/plugins/doctor.ts +67 -0
- package/src/core/plugins/index.ts +38 -0
- package/src/core/plugins/installer.ts +189 -0
- package/src/core/plugins/loader.ts +339 -0
- package/src/core/plugins/manager.ts +672 -0
- package/src/core/plugins/parser.ts +105 -0
- package/src/core/plugins/paths.ts +37 -0
- package/src/core/plugins/types.ts +190 -0
- package/src/core/sdk.ts +900 -0
- package/src/core/session-manager.ts +1837 -0
- package/src/core/settings-manager.ts +860 -0
- package/src/core/skills.ts +352 -0
- package/src/core/slash-commands.ts +132 -0
- package/src/core/system-prompt.ts +442 -0
- package/src/core/timings.ts +25 -0
- package/src/core/title-generator.ts +110 -0
- package/src/core/tools/ask.ts +193 -0
- package/src/core/tools/bash-interceptor.ts +120 -0
- package/src/core/tools/bash.ts +91 -0
- package/src/core/tools/context.ts +32 -0
- package/src/core/tools/edit-diff.ts +487 -0
- package/src/core/tools/edit.ts +140 -0
- package/src/core/tools/exa/company.ts +59 -0
- package/src/core/tools/exa/index.ts +63 -0
- package/src/core/tools/exa/linkedin.ts +59 -0
- package/src/core/tools/exa/mcp-client.ts +368 -0
- package/src/core/tools/exa/render.ts +200 -0
- package/src/core/tools/exa/researcher.ts +90 -0
- package/src/core/tools/exa/search.ts +338 -0
- package/src/core/tools/exa/types.ts +167 -0
- package/src/core/tools/exa/websets.ts +248 -0
- package/src/core/tools/find.ts +244 -0
- package/src/core/tools/grep.ts +584 -0
- package/src/core/tools/index.ts +283 -0
- package/src/core/tools/ls.ts +142 -0
- package/src/core/tools/lsp/client.ts +767 -0
- package/src/core/tools/lsp/clients/biome-client.ts +207 -0
- package/src/core/tools/lsp/clients/index.ts +49 -0
- package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
- package/src/core/tools/lsp/config.ts +845 -0
- package/src/core/tools/lsp/edits.ts +110 -0
- package/src/core/tools/lsp/index.ts +1364 -0
- package/src/core/tools/lsp/render.ts +560 -0
- package/src/core/tools/lsp/rust-analyzer.ts +145 -0
- package/src/core/tools/lsp/types.ts +495 -0
- package/src/core/tools/lsp/utils.ts +526 -0
- package/src/core/tools/notebook.ts +182 -0
- package/src/core/tools/output.ts +198 -0
- package/src/core/tools/path-utils.ts +61 -0
- package/src/core/tools/read.ts +507 -0
- package/src/core/tools/renderers.ts +820 -0
- package/src/core/tools/review.ts +275 -0
- package/src/core/tools/rulebook.ts +124 -0
- package/src/core/tools/task/agents.ts +158 -0
- package/src/core/tools/task/artifacts.ts +114 -0
- package/src/core/tools/task/commands.ts +157 -0
- package/src/core/tools/task/discovery.ts +217 -0
- package/src/core/tools/task/executor.ts +531 -0
- package/src/core/tools/task/index.ts +548 -0
- package/src/core/tools/task/model-resolver.ts +176 -0
- package/src/core/tools/task/parallel.ts +38 -0
- package/src/core/tools/task/render.ts +502 -0
- package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
- package/src/core/tools/task/types.ts +142 -0
- package/src/core/tools/truncate.ts +265 -0
- package/src/core/tools/web-fetch.ts +2511 -0
- package/src/core/tools/web-search/auth.ts +199 -0
- package/src/core/tools/web-search/index.ts +583 -0
- package/src/core/tools/web-search/providers/anthropic.ts +198 -0
- package/src/core/tools/web-search/providers/exa.ts +196 -0
- package/src/core/tools/web-search/providers/perplexity.ts +195 -0
- package/src/core/tools/web-search/render.ts +372 -0
- package/src/core/tools/web-search/types.ts +180 -0
- package/src/core/tools/write.ts +63 -0
- package/src/core/ttsr.ts +211 -0
- package/src/core/utils.ts +187 -0
- package/src/discovery/agents-md.ts +75 -0
- package/src/discovery/builtin.ts +647 -0
- package/src/discovery/claude.ts +623 -0
- package/src/discovery/cline.ts +104 -0
- package/src/discovery/codex.ts +571 -0
- package/src/discovery/cursor.ts +266 -0
- package/src/discovery/gemini.ts +368 -0
- package/src/discovery/github.ts +120 -0
- package/src/discovery/helpers.test.ts +127 -0
- package/src/discovery/helpers.ts +249 -0
- package/src/discovery/index.ts +84 -0
- package/src/discovery/mcp-json.ts +127 -0
- package/src/discovery/vscode.ts +99 -0
- package/src/discovery/windsurf.ts +219 -0
- package/src/index.ts +192 -0
- package/src/main.ts +507 -0
- package/src/migrations.ts +156 -0
- package/src/modes/cleanup.ts +23 -0
- package/src/modes/index.ts +48 -0
- package/src/modes/interactive/components/armin.ts +382 -0
- package/src/modes/interactive/components/assistant-message.ts +86 -0
- package/src/modes/interactive/components/bash-execution.ts +199 -0
- package/src/modes/interactive/components/bordered-loader.ts +41 -0
- package/src/modes/interactive/components/branch-summary-message.ts +42 -0
- package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
- package/src/modes/interactive/components/custom-editor.ts +122 -0
- package/src/modes/interactive/components/diff.ts +147 -0
- package/src/modes/interactive/components/dynamic-border.ts +25 -0
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +296 -0
- package/src/modes/interactive/components/extensions/extension-list.ts +479 -0
- package/src/modes/interactive/components/extensions/index.ts +9 -0
- package/src/modes/interactive/components/extensions/inspector-panel.ts +313 -0
- package/src/modes/interactive/components/extensions/state-manager.ts +558 -0
- package/src/modes/interactive/components/extensions/types.ts +191 -0
- package/src/modes/interactive/components/hook-editor.ts +117 -0
- package/src/modes/interactive/components/hook-input.ts +64 -0
- package/src/modes/interactive/components/hook-message.ts +96 -0
- package/src/modes/interactive/components/hook-selector.ts +91 -0
- package/src/modes/interactive/components/model-selector.ts +560 -0
- package/src/modes/interactive/components/oauth-selector.ts +136 -0
- package/src/modes/interactive/components/plugin-settings.ts +481 -0
- package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
- package/src/modes/interactive/components/session-selector.ts +220 -0
- package/src/modes/interactive/components/settings-defs.ts +597 -0
- package/src/modes/interactive/components/settings-selector.ts +545 -0
- package/src/modes/interactive/components/show-images-selector.ts +45 -0
- package/src/modes/interactive/components/status-line/index.ts +4 -0
- package/src/modes/interactive/components/status-line/presets.ts +94 -0
- package/src/modes/interactive/components/status-line/segments.ts +350 -0
- package/src/modes/interactive/components/status-line/separators.ts +55 -0
- package/src/modes/interactive/components/status-line/types.ts +81 -0
- package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
- package/src/modes/interactive/components/status-line.ts +384 -0
- package/src/modes/interactive/components/theme-selector.ts +62 -0
- package/src/modes/interactive/components/thinking-selector.ts +64 -0
- package/src/modes/interactive/components/tool-execution.ts +946 -0
- package/src/modes/interactive/components/tree-selector.ts +877 -0
- package/src/modes/interactive/components/ttsr-notification.ts +82 -0
- package/src/modes/interactive/components/user-message-selector.ts +159 -0
- package/src/modes/interactive/components/user-message.ts +18 -0
- package/src/modes/interactive/components/visual-truncate.ts +50 -0
- package/src/modes/interactive/components/welcome.ts +228 -0
- package/src/modes/interactive/interactive-mode.ts +2669 -0
- package/src/modes/interactive/theme/dark.json +102 -0
- package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
- package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
- package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
- package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
- package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
- package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
- package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
- package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
- package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
- package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
- package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
- package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
- package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
- package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
- package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
- package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
- package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
- package/src/modes/interactive/theme/defaults/index.ts +67 -0
- package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
- package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
- package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
- package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
- package/src/modes/interactive/theme/defaults/light-github.json +114 -0
- package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
- package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
- package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
- package/src/modes/interactive/theme/defaults/light-one.json +105 -0
- package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
- package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
- package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
- package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
- package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
- package/src/modes/interactive/theme/light.json +99 -0
- package/src/modes/interactive/theme/theme-schema.json +424 -0
- package/src/modes/interactive/theme/theme.ts +2211 -0
- package/src/modes/print-mode.ts +163 -0
- package/src/modes/rpc/rpc-client.ts +527 -0
- package/src/modes/rpc/rpc-mode.ts +494 -0
- package/src/modes/rpc/rpc-types.ts +203 -0
- package/src/prompts/architect-plan.md +10 -0
- package/src/prompts/branch-summary-preamble.md +3 -0
- package/src/prompts/branch-summary.md +28 -0
- package/src/prompts/browser.md +71 -0
- package/src/prompts/compaction-summary.md +34 -0
- package/src/prompts/compaction-turn-prefix.md +16 -0
- package/src/prompts/compaction-update-summary.md +41 -0
- package/src/prompts/explore.md +82 -0
- package/src/prompts/implement-with-critic.md +11 -0
- package/src/prompts/implement.md +11 -0
- package/src/prompts/init.md +30 -0
- package/src/prompts/plan.md +54 -0
- package/src/prompts/reviewer.md +81 -0
- package/src/prompts/summarization-system.md +3 -0
- package/src/prompts/system-prompt.md +27 -0
- package/src/prompts/task.md +56 -0
- package/src/prompts/title-system.md +8 -0
- package/src/prompts/tools/ask.md +24 -0
- package/src/prompts/tools/bash.md +23 -0
- package/src/prompts/tools/edit.md +9 -0
- package/src/prompts/tools/find.md +6 -0
- package/src/prompts/tools/grep.md +12 -0
- package/src/prompts/tools/lsp.md +14 -0
- package/src/prompts/tools/output.md +23 -0
- package/src/prompts/tools/read.md +25 -0
- package/src/prompts/tools/web-fetch.md +8 -0
- package/src/prompts/tools/web-search.md +10 -0
- package/src/prompts/tools/write.md +10 -0
- package/src/utils/changelog.ts +99 -0
- package/src/utils/clipboard.ts +265 -0
- package/src/utils/fuzzy.ts +108 -0
- package/src/utils/mime.ts +30 -0
- package/src/utils/shell-snapshot.ts +218 -0
- package/src/utils/shell.ts +364 -0
- package/src/utils/tools-manager.ts +265 -0
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
import { type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
|
|
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
|
+
visibleWidth,
|
|
17
|
+
} from "@oh-my-pi/pi-tui";
|
|
18
|
+
import type { ModelRegistry } from "../../../core/model-registry";
|
|
19
|
+
import { parseModelString } from "../../../core/model-resolver";
|
|
20
|
+
import type { SettingsManager } from "../../../core/settings-manager";
|
|
21
|
+
import { fuzzyFilter } from "../../../utils/fuzzy";
|
|
22
|
+
import { theme } from "../theme/theme";
|
|
23
|
+
import { DynamicBorder } from "./dynamic-border";
|
|
24
|
+
|
|
25
|
+
interface ModelItem {
|
|
26
|
+
provider: string;
|
|
27
|
+
id: string;
|
|
28
|
+
model: Model<any>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ScopedModelItem {
|
|
32
|
+
model: Model<any>;
|
|
33
|
+
thinkingLevel: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type ModelRole = "default" | "smol" | "slow";
|
|
37
|
+
|
|
38
|
+
interface MenuAction {
|
|
39
|
+
label: string;
|
|
40
|
+
role: ModelRole;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const MENU_ACTIONS: MenuAction[] = [
|
|
44
|
+
{ label: "Set as Default", role: "default" },
|
|
45
|
+
{ label: "Set as Smol (Fast)", role: "smol" },
|
|
46
|
+
{ label: "Set as Slow (Thinking)", role: "slow" },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const ALL_TAB = "ALL";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Component that renders a model selector with provider tabs and context menu.
|
|
53
|
+
* - Tab/Arrow Left/Right: Switch between provider tabs
|
|
54
|
+
* - Arrow Up/Down: Navigate model list
|
|
55
|
+
* - Enter: Open context menu to select action
|
|
56
|
+
* - Escape: Close menu or selector
|
|
57
|
+
*/
|
|
58
|
+
export class ModelSelectorComponent extends Container {
|
|
59
|
+
private searchInput: Input;
|
|
60
|
+
private headerContainer: Container;
|
|
61
|
+
private listContainer: Container;
|
|
62
|
+
private menuContainer: Container;
|
|
63
|
+
private allModels: ModelItem[] = [];
|
|
64
|
+
private filteredModels: ModelItem[] = [];
|
|
65
|
+
private selectedIndex: number = 0;
|
|
66
|
+
private currentModel?: Model<any>;
|
|
67
|
+
private defaultModel?: Model<any>;
|
|
68
|
+
private smolModel?: Model<any>;
|
|
69
|
+
private slowModel?: Model<any>;
|
|
70
|
+
private settingsManager: SettingsManager;
|
|
71
|
+
private modelRegistry: ModelRegistry;
|
|
72
|
+
private onSelectCallback: (model: Model<any>, role: string) => void;
|
|
73
|
+
private onCancelCallback: () => void;
|
|
74
|
+
private errorMessage?: string;
|
|
75
|
+
private tui: TUI;
|
|
76
|
+
private scopedModels: ReadonlyArray<ScopedModelItem>;
|
|
77
|
+
|
|
78
|
+
// Tab state
|
|
79
|
+
private providers: string[] = [ALL_TAB];
|
|
80
|
+
private activeTabIndex: number = 0;
|
|
81
|
+
|
|
82
|
+
// Context menu state
|
|
83
|
+
private isMenuOpen: boolean = false;
|
|
84
|
+
private menuSelectedIndex: number = 0;
|
|
85
|
+
|
|
86
|
+
constructor(
|
|
87
|
+
tui: TUI,
|
|
88
|
+
currentModel: Model<any> | undefined,
|
|
89
|
+
settingsManager: SettingsManager,
|
|
90
|
+
modelRegistry: ModelRegistry,
|
|
91
|
+
scopedModels: ReadonlyArray<ScopedModelItem>,
|
|
92
|
+
onSelect: (model: Model<any>, role: string) => void,
|
|
93
|
+
onCancel: () => void,
|
|
94
|
+
) {
|
|
95
|
+
super();
|
|
96
|
+
|
|
97
|
+
this.tui = tui;
|
|
98
|
+
this.currentModel = currentModel;
|
|
99
|
+
this.settingsManager = settingsManager;
|
|
100
|
+
this.modelRegistry = modelRegistry;
|
|
101
|
+
this.scopedModels = scopedModels;
|
|
102
|
+
this.onSelectCallback = onSelect;
|
|
103
|
+
this.onCancelCallback = onCancel;
|
|
104
|
+
|
|
105
|
+
// Load current role assignments from settings
|
|
106
|
+
this._loadRoleModels();
|
|
107
|
+
|
|
108
|
+
// Add top border
|
|
109
|
+
this.addChild(new DynamicBorder());
|
|
110
|
+
this.addChild(new Spacer(1));
|
|
111
|
+
|
|
112
|
+
// Add hint about model filtering
|
|
113
|
+
const hintText =
|
|
114
|
+
scopedModels.length > 0
|
|
115
|
+
? "Showing models from --models scope"
|
|
116
|
+
: "Only showing models with configured API keys (see README for details)";
|
|
117
|
+
this.addChild(new Text(theme.fg("warning", hintText), 0, 0));
|
|
118
|
+
this.addChild(new Spacer(1));
|
|
119
|
+
|
|
120
|
+
// Create header container for tab bar
|
|
121
|
+
this.headerContainer = new Container();
|
|
122
|
+
this.addChild(this.headerContainer);
|
|
123
|
+
|
|
124
|
+
this.addChild(new Spacer(1));
|
|
125
|
+
|
|
126
|
+
// Create search input
|
|
127
|
+
this.searchInput = new Input();
|
|
128
|
+
this.searchInput.onSubmit = () => {
|
|
129
|
+
// Enter on search input opens menu if we have a selection
|
|
130
|
+
if (this.filteredModels[this.selectedIndex]) {
|
|
131
|
+
this.openMenu();
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
this.addChild(this.searchInput);
|
|
135
|
+
|
|
136
|
+
this.addChild(new Spacer(1));
|
|
137
|
+
|
|
138
|
+
// Create list container
|
|
139
|
+
this.listContainer = new Container();
|
|
140
|
+
this.addChild(this.listContainer);
|
|
141
|
+
|
|
142
|
+
// Create menu container (hidden by default)
|
|
143
|
+
this.menuContainer = new Container();
|
|
144
|
+
this.addChild(this.menuContainer);
|
|
145
|
+
|
|
146
|
+
this.addChild(new Spacer(1));
|
|
147
|
+
|
|
148
|
+
// Add bottom border
|
|
149
|
+
this.addChild(new DynamicBorder());
|
|
150
|
+
|
|
151
|
+
// Load models and do initial render
|
|
152
|
+
this.loadModels().then(() => {
|
|
153
|
+
this.buildProviderTabs();
|
|
154
|
+
this.updateTabBar();
|
|
155
|
+
this.updateList();
|
|
156
|
+
// Request re-render after models are loaded
|
|
157
|
+
this.tui.requestRender();
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private _loadRoleModels(): void {
|
|
162
|
+
const roles = this.settingsManager.getModelRoles();
|
|
163
|
+
const allModels = this.modelRegistry.getAll();
|
|
164
|
+
|
|
165
|
+
// Load default model
|
|
166
|
+
const defaultStr = roles.default;
|
|
167
|
+
if (defaultStr) {
|
|
168
|
+
const parsed = parseModelString(defaultStr);
|
|
169
|
+
if (parsed) {
|
|
170
|
+
this.defaultModel = allModels.find((m) => m.provider === parsed.provider && m.id === parsed.id);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Load smol model
|
|
175
|
+
const smolStr = roles.smol;
|
|
176
|
+
if (smolStr) {
|
|
177
|
+
const parsed = parseModelString(smolStr);
|
|
178
|
+
if (parsed) {
|
|
179
|
+
this.smolModel = allModels.find((m) => m.provider === parsed.provider && m.id === parsed.id);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Load slow model
|
|
184
|
+
const slowStr = roles.slow;
|
|
185
|
+
if (slowStr) {
|
|
186
|
+
const parsed = parseModelString(slowStr);
|
|
187
|
+
if (parsed) {
|
|
188
|
+
this.slowModel = allModels.find((m) => m.provider === parsed.provider && m.id === parsed.id);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private async loadModels(): Promise<void> {
|
|
194
|
+
let models: ModelItem[];
|
|
195
|
+
|
|
196
|
+
// Use scoped models if provided via --models flag
|
|
197
|
+
if (this.scopedModels.length > 0) {
|
|
198
|
+
models = this.scopedModels.map((scoped) => ({
|
|
199
|
+
provider: scoped.model.provider,
|
|
200
|
+
id: scoped.model.id,
|
|
201
|
+
model: scoped.model,
|
|
202
|
+
}));
|
|
203
|
+
} else {
|
|
204
|
+
// Refresh to pick up any changes to models.json
|
|
205
|
+
this.modelRegistry.refresh();
|
|
206
|
+
|
|
207
|
+
// Check for models.json errors
|
|
208
|
+
const loadError = this.modelRegistry.getError();
|
|
209
|
+
if (loadError) {
|
|
210
|
+
this.errorMessage = loadError;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Load available models (built-in models still work even if models.json failed)
|
|
214
|
+
try {
|
|
215
|
+
const availableModels = await this.modelRegistry.getAvailable();
|
|
216
|
+
models = availableModels.map((model: Model<any>) => ({
|
|
217
|
+
provider: model.provider,
|
|
218
|
+
id: model.id,
|
|
219
|
+
model,
|
|
220
|
+
}));
|
|
221
|
+
} catch (error) {
|
|
222
|
+
this.allModels = [];
|
|
223
|
+
this.filteredModels = [];
|
|
224
|
+
this.errorMessage = error instanceof Error ? error.message : String(error);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Sort: current model first, then by provider, then by id
|
|
230
|
+
models.sort((a, b) => {
|
|
231
|
+
const aIsCurrent = modelsAreEqual(this.currentModel, a.model);
|
|
232
|
+
const bIsCurrent = modelsAreEqual(this.currentModel, b.model);
|
|
233
|
+
if (aIsCurrent && !bIsCurrent) return -1;
|
|
234
|
+
if (!aIsCurrent && bIsCurrent) return 1;
|
|
235
|
+
const providerCmp = a.provider.localeCompare(b.provider);
|
|
236
|
+
if (providerCmp !== 0) return providerCmp;
|
|
237
|
+
return a.id.localeCompare(b.id);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
this.allModels = models;
|
|
241
|
+
this.filteredModels = models;
|
|
242
|
+
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, models.length - 1));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private buildProviderTabs(): void {
|
|
246
|
+
// Extract unique providers from models
|
|
247
|
+
const providerSet = new Set<string>();
|
|
248
|
+
for (const item of this.allModels) {
|
|
249
|
+
providerSet.add(item.provider.toUpperCase());
|
|
250
|
+
}
|
|
251
|
+
// Sort providers alphabetically
|
|
252
|
+
const sortedProviders = Array.from(providerSet).sort();
|
|
253
|
+
this.providers = [ALL_TAB, ...sortedProviders];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private updateTabBar(): void {
|
|
257
|
+
this.headerContainer.clear();
|
|
258
|
+
|
|
259
|
+
// Build tab bar line
|
|
260
|
+
const parts: string[] = [];
|
|
261
|
+
parts.push(theme.fg("muted", "Provider:"));
|
|
262
|
+
parts.push(" ");
|
|
263
|
+
|
|
264
|
+
for (let i = 0; i < this.providers.length; i++) {
|
|
265
|
+
const provider = this.providers[i]!;
|
|
266
|
+
const isActive = i === this.activeTabIndex;
|
|
267
|
+
|
|
268
|
+
if (isActive) {
|
|
269
|
+
parts.push(theme.fg("accent", `[ ${provider} ]`));
|
|
270
|
+
} else {
|
|
271
|
+
parts.push(theme.fg("muted", ` ${provider} `));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (i < this.providers.length - 1) {
|
|
275
|
+
parts.push(" ");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
parts.push(" ");
|
|
280
|
+
parts.push(theme.fg("dim", `(${theme.nav.back}/${theme.nav.cursor} or Tab to switch)`));
|
|
281
|
+
|
|
282
|
+
this.headerContainer.addChild(new Text(parts.join(""), 0, 0));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private getActiveProvider(): string {
|
|
286
|
+
return this.providers[this.activeTabIndex] ?? ALL_TAB;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private filterModels(query: string): void {
|
|
290
|
+
const activeProvider = this.getActiveProvider();
|
|
291
|
+
|
|
292
|
+
// Start with all models or filter by provider
|
|
293
|
+
let baseModels = this.allModels;
|
|
294
|
+
if (activeProvider !== ALL_TAB) {
|
|
295
|
+
baseModels = this.allModels.filter((m) => m.provider.toUpperCase() === activeProvider);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Apply fuzzy filter if query is present
|
|
299
|
+
if (query.trim()) {
|
|
300
|
+
// If user is searching, auto-switch to ALL tab to show global results
|
|
301
|
+
if (activeProvider !== ALL_TAB) {
|
|
302
|
+
this.activeTabIndex = 0;
|
|
303
|
+
this.updateTabBar();
|
|
304
|
+
baseModels = this.allModels;
|
|
305
|
+
}
|
|
306
|
+
this.filteredModels = fuzzyFilter(baseModels, query, ({ id, provider }) => `${id} ${provider}`);
|
|
307
|
+
} else {
|
|
308
|
+
this.filteredModels = baseModels;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));
|
|
312
|
+
this.updateList();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private applyTabFilter(): void {
|
|
316
|
+
const query = this.searchInput.getValue();
|
|
317
|
+
this.filterModels(query);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private updateList(): void {
|
|
321
|
+
this.listContainer.clear();
|
|
322
|
+
|
|
323
|
+
const maxVisible = 10;
|
|
324
|
+
const startIndex = Math.max(
|
|
325
|
+
0,
|
|
326
|
+
Math.min(this.selectedIndex - Math.floor(maxVisible / 2), this.filteredModels.length - maxVisible),
|
|
327
|
+
);
|
|
328
|
+
const endIndex = Math.min(startIndex + maxVisible, this.filteredModels.length);
|
|
329
|
+
|
|
330
|
+
const activeProvider = this.getActiveProvider();
|
|
331
|
+
const showProvider = activeProvider === ALL_TAB;
|
|
332
|
+
|
|
333
|
+
// Show visible slice of filtered models
|
|
334
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
335
|
+
const item = this.filteredModels[i];
|
|
336
|
+
if (!item) continue;
|
|
337
|
+
|
|
338
|
+
const isSelected = i === this.selectedIndex;
|
|
339
|
+
const isDefault = modelsAreEqual(this.defaultModel, item.model);
|
|
340
|
+
const isSmol = modelsAreEqual(this.smolModel, item.model);
|
|
341
|
+
const isSlow = modelsAreEqual(this.slowModel, item.model);
|
|
342
|
+
|
|
343
|
+
// Build role badges (right-aligned style)
|
|
344
|
+
const badges: string[] = [];
|
|
345
|
+
if (isDefault) badges.push(theme.fg("success", `${theme.sep.pipe}DEFAULT${theme.sep.pipe}`));
|
|
346
|
+
if (isSmol) badges.push(theme.fg("warning", `${theme.sep.pipe}SMOL${theme.sep.pipe}`));
|
|
347
|
+
if (isSlow) badges.push(theme.fg("accent", `${theme.sep.pipe}SLOW${theme.sep.pipe}`));
|
|
348
|
+
const badgeText = badges.length > 0 ? ` ${badges.join(" ")}` : "";
|
|
349
|
+
|
|
350
|
+
let line = "";
|
|
351
|
+
if (isSelected) {
|
|
352
|
+
const prefix = theme.fg("accent", `${theme.nav.cursor} `);
|
|
353
|
+
const modelText = item.id;
|
|
354
|
+
if (showProvider) {
|
|
355
|
+
const providerBadge = theme.fg("muted", `[${item.provider}]`);
|
|
356
|
+
line = `${prefix}${theme.fg("accent", modelText)} ${providerBadge}${badgeText}`;
|
|
357
|
+
} else {
|
|
358
|
+
line = `${prefix}${theme.fg("accent", modelText)}${badgeText}`;
|
|
359
|
+
}
|
|
360
|
+
} else {
|
|
361
|
+
const prefix = " ";
|
|
362
|
+
if (showProvider) {
|
|
363
|
+
const providerBadge = theme.fg("muted", `[${item.provider}]`);
|
|
364
|
+
line = `${prefix}${item.id} ${providerBadge}${badgeText}`;
|
|
365
|
+
} else {
|
|
366
|
+
line = `${prefix}${item.id}${badgeText}`;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
this.listContainer.addChild(new Text(line, 0, 0));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Add scroll indicator if needed
|
|
374
|
+
if (startIndex > 0 || endIndex < this.filteredModels.length) {
|
|
375
|
+
const scrollInfo = theme.fg("muted", ` (${this.selectedIndex + 1}/${this.filteredModels.length})`);
|
|
376
|
+
this.listContainer.addChild(new Text(scrollInfo, 0, 0));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Show error message or "no results" if empty
|
|
380
|
+
if (this.errorMessage) {
|
|
381
|
+
const errorLines = this.errorMessage.split("\n");
|
|
382
|
+
for (const line of errorLines) {
|
|
383
|
+
this.listContainer.addChild(new Text(theme.fg("error", line), 0, 0));
|
|
384
|
+
}
|
|
385
|
+
} else if (this.filteredModels.length === 0) {
|
|
386
|
+
this.listContainer.addChild(new Text(theme.fg("muted", " No matching models"), 0, 0));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private openMenu(): void {
|
|
391
|
+
if (this.filteredModels.length === 0) return;
|
|
392
|
+
|
|
393
|
+
this.isMenuOpen = true;
|
|
394
|
+
this.menuSelectedIndex = 0;
|
|
395
|
+
this.updateMenu();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private closeMenu(): void {
|
|
399
|
+
this.isMenuOpen = false;
|
|
400
|
+
this.menuContainer.clear();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private updateMenu(): void {
|
|
404
|
+
this.menuContainer.clear();
|
|
405
|
+
|
|
406
|
+
const selectedModel = this.filteredModels[this.selectedIndex];
|
|
407
|
+
if (!selectedModel) return;
|
|
408
|
+
|
|
409
|
+
const headerText = ` Action for: ${selectedModel.id}`;
|
|
410
|
+
const hintText = " Enter: confirm Esc: cancel";
|
|
411
|
+
const actionLines = MENU_ACTIONS.map((action, index) => {
|
|
412
|
+
const prefix = index === this.menuSelectedIndex ? ` ${theme.nav.cursor} ` : " ";
|
|
413
|
+
return `${prefix}${action.label}`;
|
|
414
|
+
});
|
|
415
|
+
const menuWidth = Math.max(
|
|
416
|
+
visibleWidth(headerText),
|
|
417
|
+
visibleWidth(hintText),
|
|
418
|
+
...actionLines.map((line) => visibleWidth(line)),
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
// Menu header
|
|
422
|
+
this.menuContainer.addChild(new Spacer(1));
|
|
423
|
+
this.menuContainer.addChild(new Text(theme.fg("border", theme.boxSharp.horizontal.repeat(menuWidth)), 0, 0));
|
|
424
|
+
this.menuContainer.addChild(new Text(theme.fg("text", ` Action for: ${theme.bold(selectedModel.id)}`), 0, 0));
|
|
425
|
+
this.menuContainer.addChild(new Spacer(1));
|
|
426
|
+
|
|
427
|
+
// Menu options
|
|
428
|
+
for (let i = 0; i < MENU_ACTIONS.length; i++) {
|
|
429
|
+
const action = MENU_ACTIONS[i]!;
|
|
430
|
+
const isSelected = i === this.menuSelectedIndex;
|
|
431
|
+
|
|
432
|
+
let line: string;
|
|
433
|
+
if (isSelected) {
|
|
434
|
+
line = theme.fg("accent", ` ${theme.nav.cursor} ${action.label}`);
|
|
435
|
+
} else {
|
|
436
|
+
line = theme.fg("muted", ` ${action.label}`);
|
|
437
|
+
}
|
|
438
|
+
this.menuContainer.addChild(new Text(line, 0, 0));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
this.menuContainer.addChild(new Spacer(1));
|
|
442
|
+
this.menuContainer.addChild(new Text(theme.fg("dim", hintText), 0, 0));
|
|
443
|
+
this.menuContainer.addChild(new Text(theme.fg("border", theme.boxSharp.horizontal.repeat(menuWidth)), 0, 0));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
handleInput(keyData: string): void {
|
|
447
|
+
if (this.isMenuOpen) {
|
|
448
|
+
this.handleMenuInput(keyData);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Tab bar navigation: Left/Right arrows or Tab/Shift+Tab
|
|
453
|
+
if (isArrowLeft(keyData) || isShiftTab(keyData)) {
|
|
454
|
+
this.activeTabIndex = (this.activeTabIndex - 1 + this.providers.length) % this.providers.length;
|
|
455
|
+
this.updateTabBar();
|
|
456
|
+
this.selectedIndex = 0;
|
|
457
|
+
this.applyTabFilter();
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (isArrowRight(keyData) || isTab(keyData)) {
|
|
462
|
+
this.activeTabIndex = (this.activeTabIndex + 1) % this.providers.length;
|
|
463
|
+
this.updateTabBar();
|
|
464
|
+
this.selectedIndex = 0;
|
|
465
|
+
this.applyTabFilter();
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Up arrow - navigate list (wrap to bottom when at top)
|
|
470
|
+
if (isArrowUp(keyData)) {
|
|
471
|
+
if (this.filteredModels.length === 0) return;
|
|
472
|
+
this.selectedIndex = this.selectedIndex === 0 ? this.filteredModels.length - 1 : this.selectedIndex - 1;
|
|
473
|
+
this.updateList();
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Down arrow - navigate list (wrap to top when at bottom)
|
|
478
|
+
if (isArrowDown(keyData)) {
|
|
479
|
+
if (this.filteredModels.length === 0) return;
|
|
480
|
+
this.selectedIndex = this.selectedIndex === this.filteredModels.length - 1 ? 0 : this.selectedIndex + 1;
|
|
481
|
+
this.updateList();
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Enter - open context menu
|
|
486
|
+
if (isEnter(keyData)) {
|
|
487
|
+
if (this.filteredModels[this.selectedIndex]) {
|
|
488
|
+
this.openMenu();
|
|
489
|
+
}
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Escape - close selector
|
|
494
|
+
if (isEscape(keyData)) {
|
|
495
|
+
this.onCancelCallback();
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Pass everything else to search input
|
|
500
|
+
this.searchInput.handleInput(keyData);
|
|
501
|
+
this.filterModels(this.searchInput.getValue());
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private handleMenuInput(keyData: string): void {
|
|
505
|
+
// Up arrow - navigate menu
|
|
506
|
+
if (isArrowUp(keyData)) {
|
|
507
|
+
this.menuSelectedIndex = (this.menuSelectedIndex - 1 + MENU_ACTIONS.length) % MENU_ACTIONS.length;
|
|
508
|
+
this.updateMenu();
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Down arrow - navigate menu
|
|
513
|
+
if (isArrowDown(keyData)) {
|
|
514
|
+
this.menuSelectedIndex = (this.menuSelectedIndex + 1) % MENU_ACTIONS.length;
|
|
515
|
+
this.updateMenu();
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Enter - confirm selection
|
|
520
|
+
if (isEnter(keyData)) {
|
|
521
|
+
const selectedModel = this.filteredModels[this.selectedIndex];
|
|
522
|
+
const action = MENU_ACTIONS[this.menuSelectedIndex];
|
|
523
|
+
if (selectedModel && action) {
|
|
524
|
+
this.handleSelect(selectedModel.model, action.role);
|
|
525
|
+
this.closeMenu();
|
|
526
|
+
}
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Escape - close menu only
|
|
531
|
+
if (isEscape(keyData)) {
|
|
532
|
+
this.closeMenu();
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
private handleSelect(model: Model<any>, role: ModelRole): void {
|
|
538
|
+
// Save to settings
|
|
539
|
+
this.settingsManager.setModelRole(role, `${model.provider}/${model.id}`);
|
|
540
|
+
|
|
541
|
+
// Update local state for UI
|
|
542
|
+
if (role === "default") {
|
|
543
|
+
this.defaultModel = model;
|
|
544
|
+
} else if (role === "smol") {
|
|
545
|
+
this.smolModel = model;
|
|
546
|
+
} else if (role === "slow") {
|
|
547
|
+
this.slowModel = model;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Notify caller (for updating agent state if needed)
|
|
551
|
+
this.onSelectCallback(model, role);
|
|
552
|
+
|
|
553
|
+
// Update list to show new badges
|
|
554
|
+
this.updateList();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
getSearchInput(): Input {
|
|
558
|
+
return this.searchInput;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { getOAuthProviders, type OAuthProviderInfo } from "@oh-my-pi/pi-ai";
|
|
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";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Component that renders an OAuth provider selector
|
|
9
|
+
*/
|
|
10
|
+
export class OAuthSelectorComponent extends Container {
|
|
11
|
+
private listContainer: Container;
|
|
12
|
+
private allProviders: OAuthProviderInfo[] = [];
|
|
13
|
+
private selectedIndex: number = 0;
|
|
14
|
+
private mode: "login" | "logout";
|
|
15
|
+
private authStorage: AuthStorage;
|
|
16
|
+
private onSelectCallback: (providerId: string) => void;
|
|
17
|
+
private onCancelCallback: () => void;
|
|
18
|
+
private statusMessage: string | undefined;
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
mode: "login" | "logout",
|
|
22
|
+
authStorage: AuthStorage,
|
|
23
|
+
onSelect: (providerId: string) => void,
|
|
24
|
+
onCancel: () => void,
|
|
25
|
+
) {
|
|
26
|
+
super();
|
|
27
|
+
|
|
28
|
+
this.mode = mode;
|
|
29
|
+
this.authStorage = authStorage;
|
|
30
|
+
this.onSelectCallback = onSelect;
|
|
31
|
+
this.onCancelCallback = onCancel;
|
|
32
|
+
|
|
33
|
+
// Load all OAuth providers
|
|
34
|
+
this.loadProviders();
|
|
35
|
+
|
|
36
|
+
// Add top border
|
|
37
|
+
this.addChild(new DynamicBorder());
|
|
38
|
+
this.addChild(new Spacer(1));
|
|
39
|
+
|
|
40
|
+
// Add title
|
|
41
|
+
const title = mode === "login" ? "Select provider to login:" : "Select provider to logout:";
|
|
42
|
+
this.addChild(new TruncatedText(theme.bold(title)));
|
|
43
|
+
this.addChild(new Spacer(1));
|
|
44
|
+
|
|
45
|
+
// Create list container
|
|
46
|
+
this.listContainer = new Container();
|
|
47
|
+
this.addChild(this.listContainer);
|
|
48
|
+
|
|
49
|
+
this.addChild(new Spacer(1));
|
|
50
|
+
|
|
51
|
+
// Add bottom border
|
|
52
|
+
this.addChild(new DynamicBorder());
|
|
53
|
+
|
|
54
|
+
// Initial render
|
|
55
|
+
this.updateList();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private loadProviders(): void {
|
|
59
|
+
this.allProviders = getOAuthProviders();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private updateList(): void {
|
|
63
|
+
this.listContainer.clear();
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < this.allProviders.length; i++) {
|
|
66
|
+
const provider = this.allProviders[i];
|
|
67
|
+
if (!provider) continue;
|
|
68
|
+
|
|
69
|
+
const isSelected = i === this.selectedIndex;
|
|
70
|
+
const isAvailable = provider.available;
|
|
71
|
+
|
|
72
|
+
// Check if user is logged in for this provider
|
|
73
|
+
const credentials = this.authStorage.get(provider.id);
|
|
74
|
+
const isLoggedIn = credentials?.type === "oauth";
|
|
75
|
+
const statusIndicator = isLoggedIn ? theme.fg("success", ` ${theme.status.success} logged in`) : "";
|
|
76
|
+
|
|
77
|
+
let line = "";
|
|
78
|
+
if (isSelected) {
|
|
79
|
+
const prefix = theme.fg("accent", `${theme.nav.cursor} `);
|
|
80
|
+
const text = isAvailable ? theme.fg("accent", provider.name) : theme.fg("dim", provider.name);
|
|
81
|
+
line = prefix + text + statusIndicator;
|
|
82
|
+
} else {
|
|
83
|
+
const text = isAvailable ? ` ${provider.name}` : theme.fg("dim", ` ${provider.name}`);
|
|
84
|
+
line = text + statusIndicator;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.listContainer.addChild(new TruncatedText(line, 0, 0));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Show "no providers" if empty
|
|
91
|
+
if (this.allProviders.length === 0) {
|
|
92
|
+
const message =
|
|
93
|
+
this.mode === "login" ? "No OAuth providers available" : "No OAuth providers logged in. Use /login first.";
|
|
94
|
+
this.listContainer.addChild(new TruncatedText(theme.fg("muted", ` ${message}`), 0, 0));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (this.statusMessage) {
|
|
98
|
+
this.listContainer.addChild(new Spacer(1));
|
|
99
|
+
this.listContainer.addChild(new TruncatedText(theme.fg("warning", ` ${this.statusMessage}`), 0, 0));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
handleInput(keyData: string): void {
|
|
104
|
+
// Up arrow
|
|
105
|
+
if (isArrowUp(keyData)) {
|
|
106
|
+
if (this.allProviders.length > 0) {
|
|
107
|
+
this.selectedIndex = this.selectedIndex === 0 ? this.allProviders.length - 1 : this.selectedIndex - 1;
|
|
108
|
+
}
|
|
109
|
+
this.statusMessage = undefined;
|
|
110
|
+
this.updateList();
|
|
111
|
+
}
|
|
112
|
+
// Down arrow
|
|
113
|
+
else if (isArrowDown(keyData)) {
|
|
114
|
+
if (this.allProviders.length > 0) {
|
|
115
|
+
this.selectedIndex = this.selectedIndex === this.allProviders.length - 1 ? 0 : this.selectedIndex + 1;
|
|
116
|
+
}
|
|
117
|
+
this.statusMessage = undefined;
|
|
118
|
+
this.updateList();
|
|
119
|
+
}
|
|
120
|
+
// Enter
|
|
121
|
+
else if (isEnter(keyData)) {
|
|
122
|
+
const selectedProvider = this.allProviders[this.selectedIndex];
|
|
123
|
+
if (selectedProvider?.available) {
|
|
124
|
+
this.statusMessage = undefined;
|
|
125
|
+
this.onSelectCallback(selectedProvider.id);
|
|
126
|
+
} else if (selectedProvider) {
|
|
127
|
+
this.statusMessage = "Provider unavailable in this environment.";
|
|
128
|
+
this.updateList();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Escape
|
|
132
|
+
else if (isEscape(keyData)) {
|
|
133
|
+
this.onCancelCallback();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|