@oh-my-pi/pi-coding-agent 1.337.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 +1228 -0
- package/README.md +1041 -0
- package/docs/compaction.md +403 -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 +637 -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/reviewer.md +35 -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 +81 -0
- package/src/cli/args.ts +246 -0
- package/src/cli/file-processor.ts +72 -0
- package/src/cli/list-models.ts +104 -0
- package/src/cli/plugin-cli.ts +650 -0
- package/src/cli/session-picker.ts +41 -0
- package/src/cli.ts +10 -0
- package/src/commands/init.md +20 -0
- package/src/config.ts +159 -0
- package/src/core/agent-session.ts +1900 -0
- package/src/core/auth-storage.ts +236 -0
- package/src/core/bash-executor.ts +196 -0
- package/src/core/compaction/branch-summarization.ts +343 -0
- package/src/core/compaction/compaction.ts +742 -0
- package/src/core/compaction/index.ts +7 -0
- package/src/core/compaction/utils.ts +154 -0
- package/src/core/custom-tools/index.ts +21 -0
- package/src/core/custom-tools/loader.ts +248 -0
- package/src/core/custom-tools/types.ts +169 -0
- package/src/core/custom-tools/wrapper.ts +28 -0
- package/src/core/exec.ts +129 -0
- package/src/core/export-html/index.ts +211 -0
- package/src/core/export-html/template.css +781 -0
- package/src/core/export-html/template.html +54 -0
- package/src/core/export-html/template.js +1185 -0
- package/src/core/export-html/vendor/highlight.min.js +1213 -0
- package/src/core/export-html/vendor/marked.min.js +6 -0
- package/src/core/hooks/index.ts +16 -0
- package/src/core/hooks/loader.ts +312 -0
- package/src/core/hooks/runner.ts +434 -0
- package/src/core/hooks/tool-wrapper.ts +99 -0
- package/src/core/hooks/types.ts +773 -0
- package/src/core/index.ts +52 -0
- package/src/core/mcp/client.ts +158 -0
- package/src/core/mcp/config.ts +154 -0
- package/src/core/mcp/index.ts +45 -0
- package/src/core/mcp/loader.ts +68 -0
- package/src/core/mcp/manager.ts +181 -0
- package/src/core/mcp/tool-bridge.ts +148 -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 +220 -0
- package/src/core/messages.ts +189 -0
- package/src/core/model-registry.ts +317 -0
- package/src/core/model-resolver.ts +393 -0
- package/src/core/plugins/doctor.ts +59 -0
- package/src/core/plugins/index.ts +38 -0
- package/src/core/plugins/installer.ts +189 -0
- package/src/core/plugins/loader.ts +338 -0
- package/src/core/plugins/manager.ts +672 -0
- package/src/core/plugins/parser.ts +105 -0
- package/src/core/plugins/paths.ts +32 -0
- package/src/core/plugins/types.ts +190 -0
- package/src/core/sdk.ts +760 -0
- package/src/core/session-manager.ts +1128 -0
- package/src/core/settings-manager.ts +443 -0
- package/src/core/skills.ts +437 -0
- package/src/core/slash-commands.ts +248 -0
- package/src/core/system-prompt.ts +439 -0
- package/src/core/timings.ts +25 -0
- package/src/core/tools/ask.ts +211 -0
- package/src/core/tools/bash-interceptor.ts +120 -0
- package/src/core/tools/bash.ts +250 -0
- package/src/core/tools/context.ts +32 -0
- package/src/core/tools/edit-diff.ts +475 -0
- package/src/core/tools/edit.ts +208 -0
- package/src/core/tools/exa/company.ts +59 -0
- package/src/core/tools/exa/index.ts +64 -0
- package/src/core/tools/exa/linkedin.ts +59 -0
- package/src/core/tools/exa/logger.ts +56 -0
- package/src/core/tools/exa/mcp-client.ts +368 -0
- package/src/core/tools/exa/render.ts +196 -0
- package/src/core/tools/exa/researcher.ts +90 -0
- package/src/core/tools/exa/search.ts +337 -0
- package/src/core/tools/exa/types.ts +168 -0
- package/src/core/tools/exa/websets.ts +248 -0
- package/src/core/tools/find.ts +261 -0
- package/src/core/tools/grep.ts +555 -0
- package/src/core/tools/index.ts +202 -0
- package/src/core/tools/ls.ts +140 -0
- package/src/core/tools/lsp/client.ts +605 -0
- package/src/core/tools/lsp/config.ts +147 -0
- package/src/core/tools/lsp/edits.ts +101 -0
- package/src/core/tools/lsp/index.ts +804 -0
- package/src/core/tools/lsp/render.ts +447 -0
- package/src/core/tools/lsp/rust-analyzer.ts +145 -0
- package/src/core/tools/lsp/types.ts +463 -0
- package/src/core/tools/lsp/utils.ts +486 -0
- package/src/core/tools/notebook.ts +229 -0
- package/src/core/tools/path-utils.ts +61 -0
- package/src/core/tools/read.ts +240 -0
- package/src/core/tools/renderers.ts +540 -0
- package/src/core/tools/task/agents.ts +153 -0
- package/src/core/tools/task/artifacts.ts +114 -0
- package/src/core/tools/task/bundled-agents/browser.md +71 -0
- package/src/core/tools/task/bundled-agents/explore.md +82 -0
- package/src/core/tools/task/bundled-agents/plan.md +54 -0
- package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
- package/src/core/tools/task/bundled-agents/task.md +53 -0
- package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
- package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
- package/src/core/tools/task/bundled-commands/implement.md +11 -0
- package/src/core/tools/task/commands.ts +213 -0
- package/src/core/tools/task/discovery.ts +208 -0
- package/src/core/tools/task/executor.ts +367 -0
- package/src/core/tools/task/index.ts +388 -0
- package/src/core/tools/task/model-resolver.ts +115 -0
- package/src/core/tools/task/parallel.ts +38 -0
- package/src/core/tools/task/render.ts +232 -0
- package/src/core/tools/task/types.ts +99 -0
- package/src/core/tools/truncate.ts +265 -0
- package/src/core/tools/web-fetch.ts +2370 -0
- package/src/core/tools/web-search/auth.ts +193 -0
- package/src/core/tools/web-search/index.ts +537 -0
- package/src/core/tools/web-search/providers/anthropic.ts +198 -0
- package/src/core/tools/web-search/providers/exa.ts +302 -0
- package/src/core/tools/web-search/providers/perplexity.ts +195 -0
- package/src/core/tools/web-search/render.ts +182 -0
- package/src/core/tools/web-search/types.ts +180 -0
- package/src/core/tools/write.ts +99 -0
- package/src/index.ts +176 -0
- package/src/main.ts +464 -0
- package/src/migrations.ts +135 -0
- package/src/modes/index.ts +43 -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 +196 -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/footer.ts +381 -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 +247 -0
- package/src/modes/interactive/components/oauth-selector.ts +120 -0
- package/src/modes/interactive/components/plugin-settings.ts +479 -0
- package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
- package/src/modes/interactive/components/session-selector.ts +204 -0
- package/src/modes/interactive/components/settings-selector.ts +453 -0
- package/src/modes/interactive/components/show-images-selector.ts +45 -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 +675 -0
- package/src/modes/interactive/components/tree-selector.ts +866 -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 +183 -0
- package/src/modes/interactive/interactive-mode.ts +2516 -0
- package/src/modes/interactive/theme/dark.json +101 -0
- package/src/modes/interactive/theme/light.json +98 -0
- package/src/modes/interactive/theme/theme-schema.json +308 -0
- package/src/modes/interactive/theme/theme.ts +998 -0
- package/src/modes/print-mode.ts +128 -0
- package/src/modes/rpc/rpc-client.ts +527 -0
- package/src/modes/rpc/rpc-mode.ts +483 -0
- package/src/modes/rpc/rpc-types.ts +203 -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.ts +276 -0
- package/src/utils/tools-manager.ts +274 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic selector component for hooks.
|
|
3
|
+
* Displays a list of string options with keyboard navigation.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Container, isArrowDown, isArrowUp, isEnter, isEscape, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
7
|
+
import { theme } from "../theme/theme.js";
|
|
8
|
+
import { DynamicBorder } from "./dynamic-border.js";
|
|
9
|
+
|
|
10
|
+
export class HookSelectorComponent extends Container {
|
|
11
|
+
private options: string[];
|
|
12
|
+
private selectedIndex = 0;
|
|
13
|
+
private listContainer: Container;
|
|
14
|
+
private onSelectCallback: (option: string) => void;
|
|
15
|
+
private onCancelCallback: () => void;
|
|
16
|
+
|
|
17
|
+
constructor(title: string, options: string[], onSelect: (option: string) => void, onCancel: () => void) {
|
|
18
|
+
super();
|
|
19
|
+
|
|
20
|
+
this.options = options;
|
|
21
|
+
this.onSelectCallback = onSelect;
|
|
22
|
+
this.onCancelCallback = onCancel;
|
|
23
|
+
|
|
24
|
+
// Add top border
|
|
25
|
+
this.addChild(new DynamicBorder());
|
|
26
|
+
this.addChild(new Spacer(1));
|
|
27
|
+
|
|
28
|
+
// Add title
|
|
29
|
+
this.addChild(new Text(theme.fg("accent", title), 1, 0));
|
|
30
|
+
this.addChild(new Spacer(1));
|
|
31
|
+
|
|
32
|
+
// Create list container
|
|
33
|
+
this.listContainer = new Container();
|
|
34
|
+
this.addChild(this.listContainer);
|
|
35
|
+
|
|
36
|
+
this.addChild(new Spacer(1));
|
|
37
|
+
|
|
38
|
+
// Add hint
|
|
39
|
+
this.addChild(new Text(theme.fg("dim", "↑↓ navigate enter select esc cancel"), 1, 0));
|
|
40
|
+
|
|
41
|
+
this.addChild(new Spacer(1));
|
|
42
|
+
|
|
43
|
+
// Add bottom border
|
|
44
|
+
this.addChild(new DynamicBorder());
|
|
45
|
+
|
|
46
|
+
// Initial render
|
|
47
|
+
this.updateList();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private updateList(): void {
|
|
51
|
+
this.listContainer.clear();
|
|
52
|
+
|
|
53
|
+
for (let i = 0; i < this.options.length; i++) {
|
|
54
|
+
const option = this.options[i];
|
|
55
|
+
const isSelected = i === this.selectedIndex;
|
|
56
|
+
|
|
57
|
+
let text = "";
|
|
58
|
+
if (isSelected) {
|
|
59
|
+
text = theme.fg("accent", "→ ") + theme.fg("accent", option);
|
|
60
|
+
} else {
|
|
61
|
+
text = ` ${theme.fg("text", option)}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.listContainer.addChild(new Text(text, 1, 0));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
handleInput(keyData: string): void {
|
|
69
|
+
// Up arrow or k
|
|
70
|
+
if (isArrowUp(keyData) || keyData === "k") {
|
|
71
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
72
|
+
this.updateList();
|
|
73
|
+
}
|
|
74
|
+
// Down arrow or j
|
|
75
|
+
else if (isArrowDown(keyData) || keyData === "j") {
|
|
76
|
+
this.selectedIndex = Math.min(this.options.length - 1, this.selectedIndex + 1);
|
|
77
|
+
this.updateList();
|
|
78
|
+
}
|
|
79
|
+
// Enter
|
|
80
|
+
else if (isEnter(keyData) || keyData === "\n") {
|
|
81
|
+
const selected = this.options[this.selectedIndex];
|
|
82
|
+
if (selected) {
|
|
83
|
+
this.onSelectCallback(selected);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Escape
|
|
87
|
+
else if (isEscape(keyData)) {
|
|
88
|
+
this.onCancelCallback();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { Container, Input, isArrowDown, isArrowUp, isEnter, isEscape, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import type { ModelRegistry } from "../../../core/model-registry.js";
|
|
4
|
+
import type { SettingsManager } from "../../../core/settings-manager.js";
|
|
5
|
+
import { fuzzyFilter } from "../../../utils/fuzzy.js";
|
|
6
|
+
import { theme } from "../theme/theme.js";
|
|
7
|
+
import { DynamicBorder } from "./dynamic-border.js";
|
|
8
|
+
|
|
9
|
+
interface ModelItem {
|
|
10
|
+
provider: string;
|
|
11
|
+
id: string;
|
|
12
|
+
model: Model<any>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ScopedModelItem {
|
|
16
|
+
model: Model<any>;
|
|
17
|
+
thinkingLevel: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Component that renders a model selector with search
|
|
22
|
+
*/
|
|
23
|
+
export class ModelSelectorComponent extends Container {
|
|
24
|
+
private searchInput: Input;
|
|
25
|
+
private listContainer: Container;
|
|
26
|
+
private allModels: ModelItem[] = [];
|
|
27
|
+
private filteredModels: ModelItem[] = [];
|
|
28
|
+
private selectedIndex: number = 0;
|
|
29
|
+
private currentModel?: Model<any>;
|
|
30
|
+
private settingsManager: SettingsManager;
|
|
31
|
+
private modelRegistry: ModelRegistry;
|
|
32
|
+
private onSelectCallback: (model: Model<any>) => void;
|
|
33
|
+
private onCancelCallback: () => void;
|
|
34
|
+
private errorMessage?: string;
|
|
35
|
+
private tui: TUI;
|
|
36
|
+
private scopedModels: ReadonlyArray<ScopedModelItem>;
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
tui: TUI,
|
|
40
|
+
currentModel: Model<any> | undefined,
|
|
41
|
+
settingsManager: SettingsManager,
|
|
42
|
+
modelRegistry: ModelRegistry,
|
|
43
|
+
scopedModels: ReadonlyArray<ScopedModelItem>,
|
|
44
|
+
onSelect: (model: Model<any>) => void,
|
|
45
|
+
onCancel: () => void,
|
|
46
|
+
) {
|
|
47
|
+
super();
|
|
48
|
+
|
|
49
|
+
this.tui = tui;
|
|
50
|
+
this.currentModel = currentModel;
|
|
51
|
+
this.settingsManager = settingsManager;
|
|
52
|
+
this.modelRegistry = modelRegistry;
|
|
53
|
+
this.scopedModels = scopedModels;
|
|
54
|
+
this.onSelectCallback = onSelect;
|
|
55
|
+
this.onCancelCallback = onCancel;
|
|
56
|
+
|
|
57
|
+
// Add top border
|
|
58
|
+
this.addChild(new DynamicBorder());
|
|
59
|
+
this.addChild(new Spacer(1));
|
|
60
|
+
|
|
61
|
+
// Add hint about model filtering
|
|
62
|
+
const hintText =
|
|
63
|
+
scopedModels.length > 0
|
|
64
|
+
? "Showing models from --models scope"
|
|
65
|
+
: "Only showing models with configured API keys (see README for details)";
|
|
66
|
+
this.addChild(new Text(theme.fg("warning", hintText), 0, 0));
|
|
67
|
+
this.addChild(new Spacer(1));
|
|
68
|
+
|
|
69
|
+
// Create search input
|
|
70
|
+
this.searchInput = new Input();
|
|
71
|
+
this.searchInput.onSubmit = () => {
|
|
72
|
+
// Enter on search input selects the first filtered item
|
|
73
|
+
if (this.filteredModels[this.selectedIndex]) {
|
|
74
|
+
this.handleSelect(this.filteredModels[this.selectedIndex].model);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
this.addChild(this.searchInput);
|
|
78
|
+
|
|
79
|
+
this.addChild(new Spacer(1));
|
|
80
|
+
|
|
81
|
+
// Create list container
|
|
82
|
+
this.listContainer = new Container();
|
|
83
|
+
this.addChild(this.listContainer);
|
|
84
|
+
|
|
85
|
+
this.addChild(new Spacer(1));
|
|
86
|
+
|
|
87
|
+
// Add bottom border
|
|
88
|
+
this.addChild(new DynamicBorder());
|
|
89
|
+
|
|
90
|
+
// Load models and do initial render
|
|
91
|
+
this.loadModels().then(() => {
|
|
92
|
+
this.updateList();
|
|
93
|
+
// Request re-render after models are loaded
|
|
94
|
+
this.tui.requestRender();
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private async loadModels(): Promise<void> {
|
|
99
|
+
let models: ModelItem[];
|
|
100
|
+
|
|
101
|
+
// Use scoped models if provided via --models flag
|
|
102
|
+
if (this.scopedModels.length > 0) {
|
|
103
|
+
models = this.scopedModels.map((scoped) => ({
|
|
104
|
+
provider: scoped.model.provider,
|
|
105
|
+
id: scoped.model.id,
|
|
106
|
+
model: scoped.model,
|
|
107
|
+
}));
|
|
108
|
+
} else {
|
|
109
|
+
// Refresh to pick up any changes to models.json
|
|
110
|
+
this.modelRegistry.refresh();
|
|
111
|
+
|
|
112
|
+
// Check for models.json errors
|
|
113
|
+
const loadError = this.modelRegistry.getError();
|
|
114
|
+
if (loadError) {
|
|
115
|
+
this.errorMessage = loadError;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Load available models (built-in models still work even if models.json failed)
|
|
119
|
+
try {
|
|
120
|
+
const availableModels = await this.modelRegistry.getAvailable();
|
|
121
|
+
models = availableModels.map((model: Model<any>) => ({
|
|
122
|
+
provider: model.provider,
|
|
123
|
+
id: model.id,
|
|
124
|
+
model,
|
|
125
|
+
}));
|
|
126
|
+
} catch (error) {
|
|
127
|
+
this.allModels = [];
|
|
128
|
+
this.filteredModels = [];
|
|
129
|
+
this.errorMessage = error instanceof Error ? error.message : String(error);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Sort: current model first, then by provider
|
|
135
|
+
models.sort((a, b) => {
|
|
136
|
+
const aIsCurrent = modelsAreEqual(this.currentModel, a.model);
|
|
137
|
+
const bIsCurrent = modelsAreEqual(this.currentModel, b.model);
|
|
138
|
+
if (aIsCurrent && !bIsCurrent) return -1;
|
|
139
|
+
if (!aIsCurrent && bIsCurrent) return 1;
|
|
140
|
+
return a.provider.localeCompare(b.provider);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
this.allModels = models;
|
|
144
|
+
this.filteredModels = models;
|
|
145
|
+
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, models.length - 1));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private filterModels(query: string): void {
|
|
149
|
+
this.filteredModels = fuzzyFilter(this.allModels, query, ({ id, provider }) => `${id} ${provider}`);
|
|
150
|
+
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));
|
|
151
|
+
this.updateList();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private updateList(): void {
|
|
155
|
+
this.listContainer.clear();
|
|
156
|
+
|
|
157
|
+
const maxVisible = 10;
|
|
158
|
+
const startIndex = Math.max(
|
|
159
|
+
0,
|
|
160
|
+
Math.min(this.selectedIndex - Math.floor(maxVisible / 2), this.filteredModels.length - maxVisible),
|
|
161
|
+
);
|
|
162
|
+
const endIndex = Math.min(startIndex + maxVisible, this.filteredModels.length);
|
|
163
|
+
|
|
164
|
+
// Show visible slice of filtered models
|
|
165
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
166
|
+
const item = this.filteredModels[i];
|
|
167
|
+
if (!item) continue;
|
|
168
|
+
|
|
169
|
+
const isSelected = i === this.selectedIndex;
|
|
170
|
+
const isCurrent = modelsAreEqual(this.currentModel, item.model);
|
|
171
|
+
|
|
172
|
+
let line = "";
|
|
173
|
+
if (isSelected) {
|
|
174
|
+
const prefix = theme.fg("accent", "→ ");
|
|
175
|
+
const modelText = `${item.id}`;
|
|
176
|
+
const providerBadge = theme.fg("muted", `[${item.provider}]`);
|
|
177
|
+
const checkmark = isCurrent ? theme.fg("success", " ✓") : "";
|
|
178
|
+
line = `${prefix + theme.fg("accent", modelText)} ${providerBadge}${checkmark}`;
|
|
179
|
+
} else {
|
|
180
|
+
const modelText = ` ${item.id}`;
|
|
181
|
+
const providerBadge = theme.fg("muted", `[${item.provider}]`);
|
|
182
|
+
const checkmark = isCurrent ? theme.fg("success", " ✓") : "";
|
|
183
|
+
line = `${modelText} ${providerBadge}${checkmark}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
this.listContainer.addChild(new Text(line, 0, 0));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Add scroll indicator if needed
|
|
190
|
+
if (startIndex > 0 || endIndex < this.filteredModels.length) {
|
|
191
|
+
const scrollInfo = theme.fg("muted", ` (${this.selectedIndex + 1}/${this.filteredModels.length})`);
|
|
192
|
+
this.listContainer.addChild(new Text(scrollInfo, 0, 0));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Show error message or "no results" if empty
|
|
196
|
+
if (this.errorMessage) {
|
|
197
|
+
// Show error in red
|
|
198
|
+
const errorLines = this.errorMessage.split("\n");
|
|
199
|
+
for (const line of errorLines) {
|
|
200
|
+
this.listContainer.addChild(new Text(theme.fg("error", line), 0, 0));
|
|
201
|
+
}
|
|
202
|
+
} else if (this.filteredModels.length === 0) {
|
|
203
|
+
this.listContainer.addChild(new Text(theme.fg("muted", " No matching models"), 0, 0));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
handleInput(keyData: string): void {
|
|
208
|
+
// Up arrow - wrap to bottom when at top
|
|
209
|
+
if (isArrowUp(keyData)) {
|
|
210
|
+
if (this.filteredModels.length === 0) return;
|
|
211
|
+
this.selectedIndex = this.selectedIndex === 0 ? this.filteredModels.length - 1 : this.selectedIndex - 1;
|
|
212
|
+
this.updateList();
|
|
213
|
+
}
|
|
214
|
+
// Down arrow - wrap to top when at bottom
|
|
215
|
+
else if (isArrowDown(keyData)) {
|
|
216
|
+
if (this.filteredModels.length === 0) return;
|
|
217
|
+
this.selectedIndex = this.selectedIndex === this.filteredModels.length - 1 ? 0 : this.selectedIndex + 1;
|
|
218
|
+
this.updateList();
|
|
219
|
+
}
|
|
220
|
+
// Enter
|
|
221
|
+
else if (isEnter(keyData)) {
|
|
222
|
+
const selectedModel = this.filteredModels[this.selectedIndex];
|
|
223
|
+
if (selectedModel) {
|
|
224
|
+
this.handleSelect(selectedModel.model);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// Escape
|
|
228
|
+
else if (isEscape(keyData)) {
|
|
229
|
+
this.onCancelCallback();
|
|
230
|
+
}
|
|
231
|
+
// Pass everything else to search input
|
|
232
|
+
else {
|
|
233
|
+
this.searchInput.handleInput(keyData);
|
|
234
|
+
this.filterModels(this.searchInput.getValue());
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private handleSelect(model: Model<any>): void {
|
|
239
|
+
// Save as new default
|
|
240
|
+
this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
|
|
241
|
+
this.onSelectCallback(model);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
getSearchInput(): Input {
|
|
245
|
+
return this.searchInput;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
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.js";
|
|
4
|
+
import { theme } from "../theme/theme.js";
|
|
5
|
+
import { DynamicBorder } from "./dynamic-border.js";
|
|
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
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
mode: "login" | "logout",
|
|
21
|
+
authStorage: AuthStorage,
|
|
22
|
+
onSelect: (providerId: string) => void,
|
|
23
|
+
onCancel: () => void,
|
|
24
|
+
) {
|
|
25
|
+
super();
|
|
26
|
+
|
|
27
|
+
this.mode = mode;
|
|
28
|
+
this.authStorage = authStorage;
|
|
29
|
+
this.onSelectCallback = onSelect;
|
|
30
|
+
this.onCancelCallback = onCancel;
|
|
31
|
+
|
|
32
|
+
// Load all OAuth providers
|
|
33
|
+
this.loadProviders();
|
|
34
|
+
|
|
35
|
+
// Add top border
|
|
36
|
+
this.addChild(new DynamicBorder());
|
|
37
|
+
this.addChild(new Spacer(1));
|
|
38
|
+
|
|
39
|
+
// Add title
|
|
40
|
+
const title = mode === "login" ? "Select provider to login:" : "Select provider to logout:";
|
|
41
|
+
this.addChild(new TruncatedText(theme.bold(title)));
|
|
42
|
+
this.addChild(new Spacer(1));
|
|
43
|
+
|
|
44
|
+
// Create list container
|
|
45
|
+
this.listContainer = new Container();
|
|
46
|
+
this.addChild(this.listContainer);
|
|
47
|
+
|
|
48
|
+
this.addChild(new Spacer(1));
|
|
49
|
+
|
|
50
|
+
// Add bottom border
|
|
51
|
+
this.addChild(new DynamicBorder());
|
|
52
|
+
|
|
53
|
+
// Initial render
|
|
54
|
+
this.updateList();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private loadProviders(): void {
|
|
58
|
+
this.allProviders = getOAuthProviders();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private updateList(): void {
|
|
62
|
+
this.listContainer.clear();
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < this.allProviders.length; i++) {
|
|
65
|
+
const provider = this.allProviders[i];
|
|
66
|
+
if (!provider) continue;
|
|
67
|
+
|
|
68
|
+
const isSelected = i === this.selectedIndex;
|
|
69
|
+
const isAvailable = provider.available;
|
|
70
|
+
|
|
71
|
+
// Check if user is logged in for this provider
|
|
72
|
+
const credentials = this.authStorage.get(provider.id);
|
|
73
|
+
const isLoggedIn = credentials?.type === "oauth";
|
|
74
|
+
const statusIndicator = isLoggedIn ? theme.fg("success", " ✓ logged in") : "";
|
|
75
|
+
|
|
76
|
+
let line = "";
|
|
77
|
+
if (isSelected) {
|
|
78
|
+
const prefix = theme.fg("accent", "→ ");
|
|
79
|
+
const text = isAvailable ? theme.fg("accent", provider.name) : theme.fg("dim", provider.name);
|
|
80
|
+
line = prefix + text + statusIndicator;
|
|
81
|
+
} else {
|
|
82
|
+
const text = isAvailable ? ` ${provider.name}` : theme.fg("dim", ` ${provider.name}`);
|
|
83
|
+
line = text + statusIndicator;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.listContainer.addChild(new TruncatedText(line, 0, 0));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Show "no providers" if empty
|
|
90
|
+
if (this.allProviders.length === 0) {
|
|
91
|
+
const message =
|
|
92
|
+
this.mode === "login" ? "No OAuth providers available" : "No OAuth providers logged in. Use /login first.";
|
|
93
|
+
this.listContainer.addChild(new TruncatedText(theme.fg("muted", ` ${message}`), 0, 0));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
handleInput(keyData: string): void {
|
|
98
|
+
// Up arrow
|
|
99
|
+
if (isArrowUp(keyData)) {
|
|
100
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
101
|
+
this.updateList();
|
|
102
|
+
}
|
|
103
|
+
// Down arrow
|
|
104
|
+
else if (isArrowDown(keyData)) {
|
|
105
|
+
this.selectedIndex = Math.min(this.allProviders.length - 1, this.selectedIndex + 1);
|
|
106
|
+
this.updateList();
|
|
107
|
+
}
|
|
108
|
+
// Enter
|
|
109
|
+
else if (isEnter(keyData)) {
|
|
110
|
+
const selectedProvider = this.allProviders[this.selectedIndex];
|
|
111
|
+
if (selectedProvider?.available) {
|
|
112
|
+
this.onSelectCallback(selectedProvider.id);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Escape
|
|
116
|
+
else if (isEscape(keyData)) {
|
|
117
|
+
this.onCancelCallback();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|