@oh-my-pi/pi-coding-agent 11.8.2 → 11.8.3
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/docs/tui.md +9 -9
- package/package.json +7 -7
- package/src/cli/file-processor.ts +8 -13
- package/src/cli/oclif-help.ts +1 -1
- package/src/cli.ts +14 -0
- package/src/commit/git/index.ts +16 -16
- package/src/config/keybindings.ts +11 -11
- package/src/config/model-registry.ts +31 -66
- package/src/config/settings.ts +88 -95
- package/src/config.ts +2 -2
- package/src/cursor.ts +4 -4
- package/src/debug/index.ts +28 -28
- package/src/discovery/codex.ts +5 -13
- package/src/discovery/cursor.ts +2 -7
- package/src/exa/mcp-client.ts +2 -2
- package/src/exa/websets.ts +2 -2
- package/src/export/html/index.ts +3 -3
- package/src/export/ttsr.ts +27 -27
- package/src/extensibility/custom-tools/loader.ts +9 -9
- package/src/extensibility/extensions/runner.ts +64 -64
- package/src/extensibility/hooks/runner.ts +46 -46
- package/src/extensibility/plugins/manager.ts +49 -49
- package/src/index.ts +0 -1
- package/src/internal-urls/router.ts +5 -5
- package/src/ipy/kernel.ts +61 -57
- package/src/lsp/client.ts +1 -1
- package/src/lsp/clients/biome-client.ts +2 -2
- package/src/lsp/clients/lsp-linter-client.ts +7 -7
- package/src/lsp/index.ts +9 -9
- package/src/mcp/manager.ts +47 -47
- package/src/mcp/tool-bridge.ts +12 -12
- package/src/mcp/transports/http.ts +34 -34
- package/src/mcp/transports/stdio.ts +47 -47
- package/src/modes/components/assistant-message.ts +25 -25
- package/src/modes/components/bash-execution.ts +51 -51
- package/src/modes/components/bordered-loader.ts +7 -7
- package/src/modes/components/branch-summary-message.ts +7 -7
- package/src/modes/components/compaction-summary-message.ts +7 -7
- package/src/modes/components/countdown-timer.ts +15 -15
- package/src/modes/components/custom-editor.ts +22 -22
- package/src/modes/components/custom-message.ts +21 -21
- package/src/modes/components/dynamic-border.ts +3 -3
- package/src/modes/components/extensions/extension-dashboard.ts +72 -72
- package/src/modes/components/extensions/extension-list.ts +99 -97
- package/src/modes/components/extensions/inspector-panel.ts +26 -26
- package/src/modes/components/footer.ts +36 -36
- package/src/modes/components/history-search.ts +52 -52
- package/src/modes/components/hook-editor.ts +20 -20
- package/src/modes/components/hook-input.ts +20 -20
- package/src/modes/components/hook-message.ts +22 -22
- package/src/modes/components/hook-selector.ts +52 -52
- package/src/modes/components/index.ts +0 -1
- package/src/modes/components/login-dialog.ts +57 -57
- package/src/modes/components/model-selector.ts +173 -173
- package/src/modes/components/oauth-selector.ts +45 -45
- package/src/modes/components/plugin-settings.ts +52 -52
- package/src/modes/components/python-execution.ts +53 -53
- package/src/modes/components/queue-mode-selector.ts +7 -7
- package/src/modes/components/read-tool-group.ts +23 -23
- package/src/modes/components/session-selector.ts +40 -37
- package/src/modes/components/settings-selector.ts +80 -80
- package/src/modes/components/show-images-selector.ts +7 -7
- package/src/modes/components/skill-message.ts +27 -27
- package/src/modes/components/status-line-segment-editor.ts +81 -81
- package/src/modes/components/status-line.ts +73 -73
- package/src/modes/components/theme-selector.ts +11 -11
- package/src/modes/components/thinking-selector.ts +7 -7
- package/src/modes/components/todo-display.ts +19 -19
- package/src/modes/components/todo-reminder.ts +9 -9
- package/src/modes/components/tool-execution.ts +204 -196
- package/src/modes/components/tree-selector.ts +144 -144
- package/src/modes/components/ttsr-notification.ts +17 -17
- package/src/modes/components/user-message-selector.ts +18 -18
- package/src/modes/components/welcome.ts +10 -10
- package/src/modes/controllers/command-controller.ts +0 -7
- package/src/modes/controllers/event-controller.ts +23 -23
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +4 -9
- package/src/modes/interactive-mode.ts +234 -241
- package/src/modes/rpc/rpc-client.ts +77 -77
- package/src/modes/rpc/rpc-mode.ts +5 -5
- package/src/modes/theme/theme.ts +113 -113
- package/src/modes/types.ts +0 -1
- package/src/patch/index.ts +45 -45
- package/src/prompts/tools/task.md +22 -2
- package/src/session/agent-session.ts +463 -476
- package/src/session/agent-storage.ts +72 -75
- package/src/session/auth-storage.ts +186 -252
- package/src/session/history-storage.ts +36 -38
- package/src/session/session-manager.ts +300 -299
- package/src/session/session-storage.ts +65 -90
- package/src/ssh/connection-manager.ts +9 -9
- package/src/task/agents.ts +1 -1
- package/src/task/executor.ts +2 -2
- package/src/task/index.ts +13 -12
- package/src/task/subprocess-tool-registry.ts +5 -5
- package/src/tools/ask.ts +7 -7
- package/src/tools/bash.ts +8 -7
- package/src/tools/browser.ts +123 -123
- package/src/tools/calculator.ts +46 -46
- package/src/tools/context.ts +9 -9
- package/src/tools/exit-plan-mode.ts +5 -5
- package/src/tools/fetch.ts +5 -5
- package/src/tools/find.ts +16 -16
- package/src/tools/grep.ts +10 -10
- package/src/tools/notebook.ts +6 -6
- package/src/tools/output-meta.ts +10 -2
- package/src/tools/python.ts +12 -11
- package/src/tools/read.ts +17 -17
- package/src/tools/ssh.ts +9 -9
- package/src/tools/submit-result.ts +13 -13
- package/src/tools/todo-write.ts +6 -6
- package/src/tools/write.ts +10 -10
- package/src/tui/output-block.ts +6 -6
- package/src/tui/utils.ts +9 -9
- package/src/utils/event-bus.ts +10 -10
- package/src/utils/frontmatter.ts +1 -1
- package/src/utils/ignore-files.ts +1 -1
- package/src/web/search/index.ts +5 -5
- package/src/web/search/providers/anthropic.ts +7 -2
- package/examples/hooks/snake.ts +0 -342
- package/src/modes/components/armin.ts +0 -379
package/docs/tui.md
CHANGED
|
@@ -20,13 +20,13 @@ interface Component {
|
|
|
20
20
|
}
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
| Member
|
|
24
|
-
|
|
|
25
|
-
| `render(width)`
|
|
26
|
-
| `handleInput?(data)`
|
|
27
|
-
| `wantsKeyRelease?`
|
|
28
|
-
| `getCursorPosition?(width)`
|
|
29
|
-
| `invalidate()`
|
|
23
|
+
| Member | Description |
|
|
24
|
+
| --------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
25
|
+
| `render(width)` | Return array of strings (one per line). Each line **must not exceed `width`**. |
|
|
26
|
+
| `handleInput?(data)` | Receive keyboard input when component has focus. |
|
|
27
|
+
| `wantsKeyRelease?` | Opt-in to key release events (Kitty protocol). Default is `false` (release events are filtered out). |
|
|
28
|
+
| `getCursorPosition?(width)` | Optional cursor position within the rendered output (0-based row/col) for hardware cursor placement. |
|
|
29
|
+
| `invalidate()` | Clear cached render state (called when themes change or the component needs a full re-render). |
|
|
30
30
|
|
|
31
31
|
## Using Components
|
|
32
32
|
|
|
@@ -333,8 +333,8 @@ class MySelector implements Component {
|
|
|
333
333
|
private cachedWidth?: number;
|
|
334
334
|
private cachedLines?: string[];
|
|
335
335
|
|
|
336
|
-
|
|
337
|
-
|
|
336
|
+
onSelect?: (item: string) => void;
|
|
337
|
+
onCancel?: () => void;
|
|
338
338
|
|
|
339
339
|
constructor(items: string[]) {
|
|
340
340
|
this.items = items;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "11.8.
|
|
3
|
+
"version": "11.8.3",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -90,12 +90,12 @@
|
|
|
90
90
|
"@mozilla/readability": "0.6.0",
|
|
91
91
|
"@oclif/core": "^4.8.0",
|
|
92
92
|
"@oclif/plugin-autocomplete": "^3.2.40",
|
|
93
|
-
"@oh-my-pi/omp-stats": "11.8.
|
|
94
|
-
"@oh-my-pi/pi-agent-core": "11.8.
|
|
95
|
-
"@oh-my-pi/pi-ai": "11.8.
|
|
96
|
-
"@oh-my-pi/pi-natives": "11.8.
|
|
97
|
-
"@oh-my-pi/pi-tui": "11.8.
|
|
98
|
-
"@oh-my-pi/pi-utils": "11.8.
|
|
93
|
+
"@oh-my-pi/omp-stats": "11.8.3",
|
|
94
|
+
"@oh-my-pi/pi-agent-core": "11.8.3",
|
|
95
|
+
"@oh-my-pi/pi-ai": "11.8.3",
|
|
96
|
+
"@oh-my-pi/pi-natives": "11.8.3",
|
|
97
|
+
"@oh-my-pi/pi-tui": "11.8.3",
|
|
98
|
+
"@oh-my-pi/pi-utils": "11.8.3",
|
|
99
99
|
"@sinclair/typebox": "^0.34.48",
|
|
100
100
|
"ajv": "^8.17.1",
|
|
101
101
|
"chalk": "^5.6.2",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Process @file CLI arguments into text content and image attachments
|
|
3
3
|
*/
|
|
4
|
-
import * as fs from "node:fs
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
7
7
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
@@ -36,15 +36,10 @@ export async function processFileArguments(fileArgs: string[], options?: Process
|
|
|
36
36
|
// Expand and resolve path (handles ~ expansion and macOS screenshot Unicode spaces)
|
|
37
37
|
const absolutePath = path.resolve(resolveReadPath(fileArg, process.cwd()));
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (isEnoent(err)) {
|
|
44
|
-
console.error(chalk.red(`Error: File not found: ${absolutePath}`));
|
|
45
|
-
process.exit(1);
|
|
46
|
-
}
|
|
47
|
-
throw err;
|
|
39
|
+
const stat = fs.statSync(absolutePath, { throwIfNoEntry: false });
|
|
40
|
+
if (!stat) {
|
|
41
|
+
console.error(chalk.red(`Error: File not found: ${absolutePath}`));
|
|
42
|
+
process.exit(1);
|
|
48
43
|
}
|
|
49
44
|
|
|
50
45
|
const mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);
|
|
@@ -58,9 +53,9 @@ export async function processFileArguments(fileArgs: string[], options?: Process
|
|
|
58
53
|
}
|
|
59
54
|
|
|
60
55
|
// Read file, handling not-found gracefully
|
|
61
|
-
let buffer:
|
|
56
|
+
let buffer: Uint8Array;
|
|
62
57
|
try {
|
|
63
|
-
buffer = await
|
|
58
|
+
buffer = await Bun.file(absolutePath).bytes();
|
|
64
59
|
} catch (err) {
|
|
65
60
|
if (isEnoent(err)) {
|
|
66
61
|
console.error(chalk.red(`Error: File not found: ${absolutePath}`));
|
|
@@ -114,7 +109,7 @@ export async function processFileArguments(fileArgs: string[], options?: Process
|
|
|
114
109
|
} else {
|
|
115
110
|
// Handle text file
|
|
116
111
|
try {
|
|
117
|
-
const content =
|
|
112
|
+
const content = new TextDecoder().decode(buffer);
|
|
118
113
|
text += `<file name="${absolutePath}">\n${content}\n</file>\n`;
|
|
119
114
|
} catch (error: unknown) {
|
|
120
115
|
const message = error instanceof Error ? error.message : String(error);
|
package/src/cli/oclif-help.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { CommandHelp, Help } from "@oclif/core";
|
|
|
5
5
|
import { getExtraHelpText } from "./args";
|
|
6
6
|
|
|
7
7
|
export default class OclifHelp extends Help {
|
|
8
|
-
|
|
8
|
+
async showRootHelp(): Promise<void> {
|
|
9
9
|
await super.showRootHelp();
|
|
10
10
|
const rootCommand = this.config.findCommand("index");
|
|
11
11
|
if (rootCommand) {
|
package/src/cli.ts
CHANGED
|
@@ -8,6 +8,20 @@
|
|
|
8
8
|
import { run } from "@oclif/core";
|
|
9
9
|
import { APP_NAME } from "./config";
|
|
10
10
|
|
|
11
|
+
// oclif's warn() doesn't unwrap AggregateError — override to surface the real messages
|
|
12
|
+
const originalWarn = console.warn;
|
|
13
|
+
console.warn = (...args: unknown[]) => {
|
|
14
|
+
for (const arg of args) {
|
|
15
|
+
if (arg instanceof AggregateError) {
|
|
16
|
+
for (const err of arg.errors) {
|
|
17
|
+
originalWarn(err instanceof Error ? (err.stack ?? err.message) : String(err));
|
|
18
|
+
}
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
originalWarn(...args);
|
|
23
|
+
};
|
|
24
|
+
|
|
11
25
|
process.title = APP_NAME;
|
|
12
26
|
const argv = process.argv.slice(2);
|
|
13
27
|
const runArgv = argv.length === 0 || argv[0]?.startsWith("-") ? ["index", ...argv] : argv;
|
package/src/commit/git/index.ts
CHANGED
|
@@ -18,21 +18,21 @@ export class ControlledGit {
|
|
|
18
18
|
async getDiff(staged: boolean): Promise<string> {
|
|
19
19
|
const args = staged ? ["diff", "--cached"] : ["diff"];
|
|
20
20
|
const result = await runGitCommand(this.cwd, args);
|
|
21
|
-
this
|
|
21
|
+
this.#ensureSuccess(result, "git diff");
|
|
22
22
|
return result.stdout;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
async getDiffForFiles(files: string[], staged = true): Promise<string> {
|
|
26
26
|
const args = staged ? ["diff", "--cached", "--", ...files] : ["diff", "--", ...files];
|
|
27
27
|
const result = await runGitCommand(this.cwd, args);
|
|
28
|
-
this
|
|
28
|
+
this.#ensureSuccess(result, "git diff (files)");
|
|
29
29
|
return result.stdout;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
async getChangedFiles(staged: boolean): Promise<string[]> {
|
|
33
33
|
const args = staged ? ["diff", "--cached", "--name-only"] : ["diff", "--name-only"];
|
|
34
34
|
const result = await runGitCommand(this.cwd, args);
|
|
35
|
-
this
|
|
35
|
+
this.#ensureSuccess(result, "git diff --name-only");
|
|
36
36
|
return result.stdout
|
|
37
37
|
.split("\n")
|
|
38
38
|
.map(line => line.trim())
|
|
@@ -42,27 +42,27 @@ export class ControlledGit {
|
|
|
42
42
|
async getStat(staged: boolean): Promise<string> {
|
|
43
43
|
const args = staged ? ["diff", "--cached", "--stat"] : ["diff", "--stat"];
|
|
44
44
|
const result = await runGitCommand(this.cwd, args);
|
|
45
|
-
this
|
|
45
|
+
this.#ensureSuccess(result, "git diff --stat");
|
|
46
46
|
return result.stdout;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
async getStatForFiles(files: string[], staged = true): Promise<string> {
|
|
50
50
|
const args = staged ? ["diff", "--cached", "--stat", "--", ...files] : ["diff", "--stat", "--", ...files];
|
|
51
51
|
const result = await runGitCommand(this.cwd, args);
|
|
52
|
-
this
|
|
52
|
+
this.#ensureSuccess(result, "git diff --stat (files)");
|
|
53
53
|
return result.stdout;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
async getNumstat(staged: boolean): Promise<NumstatEntry[]> {
|
|
57
57
|
const args = staged ? ["diff", "--cached", "--numstat"] : ["diff", "--numstat"];
|
|
58
58
|
const result = await runGitCommand(this.cwd, args);
|
|
59
|
-
this
|
|
59
|
+
this.#ensureSuccess(result, "git diff --numstat");
|
|
60
60
|
return parseNumstat(result.stdout);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
async getRecentCommits(count: number): Promise<string[]> {
|
|
64
64
|
const result = await runGitCommand(this.cwd, ["log", `-n${count}`, "--pretty=format:%s"]);
|
|
65
|
-
this
|
|
65
|
+
this.#ensureSuccess(result, "git log");
|
|
66
66
|
return result.stdout
|
|
67
67
|
.split("\n")
|
|
68
68
|
.map(line => line.trim())
|
|
@@ -71,7 +71,7 @@ export class ControlledGit {
|
|
|
71
71
|
|
|
72
72
|
async getStagedFiles(): Promise<string[]> {
|
|
73
73
|
const result = await runGitCommand(this.cwd, ["diff", "--cached", "--name-only"]);
|
|
74
|
-
this
|
|
74
|
+
this.#ensureSuccess(result, "git diff --cached --name-only");
|
|
75
75
|
return result.stdout
|
|
76
76
|
.split("\n")
|
|
77
77
|
.map(line => line.trim())
|
|
@@ -80,7 +80,7 @@ export class ControlledGit {
|
|
|
80
80
|
|
|
81
81
|
async getUntrackedFiles(): Promise<string[]> {
|
|
82
82
|
const result = await runGitCommand(this.cwd, ["ls-files", "--others", "--exclude-standard"]);
|
|
83
|
-
this
|
|
83
|
+
this.#ensureSuccess(result, "git ls-files --others --exclude-standard");
|
|
84
84
|
return result.stdout
|
|
85
85
|
.split("\n")
|
|
86
86
|
.map(line => line.trim())
|
|
@@ -89,12 +89,12 @@ export class ControlledGit {
|
|
|
89
89
|
|
|
90
90
|
async stageAll(): Promise<void> {
|
|
91
91
|
const result = await stageFiles(this.cwd, []);
|
|
92
|
-
this
|
|
92
|
+
this.#ensureSuccess(result, "git add -A");
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
async stageFiles(files: string[]): Promise<void> {
|
|
96
96
|
const result = await stageFiles(this.cwd, files);
|
|
97
|
-
this
|
|
97
|
+
this.#ensureSuccess(result, "git add");
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
async stageHunks(selections: HunkSelection[]): Promise<void> {
|
|
@@ -137,7 +137,7 @@ export class ControlledGit {
|
|
|
137
137
|
try {
|
|
138
138
|
await Bun.write(tempPath, patch);
|
|
139
139
|
const result = await runGitCommand(this.cwd, ["apply", "--cached", "--binary", tempPath]);
|
|
140
|
-
this
|
|
140
|
+
this.#ensureSuccess(result, "git apply --cached");
|
|
141
141
|
} finally {
|
|
142
142
|
await fs.rm(tempPath, { force: true });
|
|
143
143
|
}
|
|
@@ -145,17 +145,17 @@ export class ControlledGit {
|
|
|
145
145
|
|
|
146
146
|
async resetStaging(files: string[] = []): Promise<void> {
|
|
147
147
|
const result = await resetStaging(this.cwd, files);
|
|
148
|
-
this
|
|
148
|
+
this.#ensureSuccess(result, "git reset");
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
async commit(message: string): Promise<void> {
|
|
152
152
|
const result = await commit(this.cwd, message);
|
|
153
|
-
this
|
|
153
|
+
this.#ensureSuccess(result, "git commit");
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
async push(): Promise<void> {
|
|
157
157
|
const result = await push(this.cwd);
|
|
158
|
-
this
|
|
158
|
+
this.#ensureSuccess(result, "git push");
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
parseDiffFiles(diff: string): FileDiff[] {
|
|
@@ -171,7 +171,7 @@ export class ControlledGit {
|
|
|
171
171
|
return this.parseDiffHunks(diff);
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
|
|
174
|
+
#ensureSuccess(result: { exitCode: number; stderr: string }, label: string): void {
|
|
175
175
|
if (result.exitCode !== 0) {
|
|
176
176
|
logger.error("commit git command failed", { label, stderr: result.stderr });
|
|
177
177
|
throw new GitError(label, result.stderr);
|
|
@@ -166,11 +166,11 @@ export function formatKeyHints(keys: KeyId | KeyId[]): string {
|
|
|
166
166
|
* Manages all keybindings (app + editor).
|
|
167
167
|
*/
|
|
168
168
|
export class KeybindingsManager {
|
|
169
|
-
|
|
169
|
+
#appActionToKeys: Map<AppAction, KeyId[]>;
|
|
170
170
|
|
|
171
171
|
private constructor(private readonly config: KeybindingsConfig) {
|
|
172
|
-
this
|
|
173
|
-
this
|
|
172
|
+
this.#appActionToKeys = new Map();
|
|
173
|
+
this.#buildMaps();
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
/**
|
|
@@ -178,7 +178,7 @@ export class KeybindingsManager {
|
|
|
178
178
|
*/
|
|
179
179
|
static async create(agentDir: string = getAgentDir()): Promise<KeybindingsManager> {
|
|
180
180
|
const configPath = path.join(agentDir, "keybindings.json");
|
|
181
|
-
const config = await KeybindingsManager
|
|
181
|
+
const config = await KeybindingsManager.#loadFromFile(configPath);
|
|
182
182
|
const manager = new KeybindingsManager(config);
|
|
183
183
|
|
|
184
184
|
// Set up editor keybindings globally
|
|
@@ -200,7 +200,7 @@ export class KeybindingsManager {
|
|
|
200
200
|
return new KeybindingsManager(config);
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
|
|
203
|
+
static async #loadFromFile(path: string): Promise<KeybindingsConfig> {
|
|
204
204
|
try {
|
|
205
205
|
return await Bun.file(path).json();
|
|
206
206
|
} catch (error) {
|
|
@@ -209,13 +209,13 @@ export class KeybindingsManager {
|
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
|
|
213
|
-
this
|
|
212
|
+
#buildMaps(): void {
|
|
213
|
+
this.#appActionToKeys.clear();
|
|
214
214
|
|
|
215
215
|
// Set defaults for app actions
|
|
216
216
|
for (const [action, keys] of Object.entries(DEFAULT_APP_KEYBINDINGS)) {
|
|
217
217
|
const keyArray = Array.isArray(keys) ? keys : [keys];
|
|
218
|
-
this
|
|
218
|
+
this.#appActionToKeys.set(
|
|
219
219
|
action as AppAction,
|
|
220
220
|
keyArray.map(key => normalizeKeyId(key as KeyId)),
|
|
221
221
|
);
|
|
@@ -225,7 +225,7 @@ export class KeybindingsManager {
|
|
|
225
225
|
for (const [action, keys] of Object.entries(this.config)) {
|
|
226
226
|
if (keys === undefined || !isAppAction(action)) continue;
|
|
227
227
|
const keyArray = Array.isArray(keys) ? keys : [keys];
|
|
228
|
-
this
|
|
228
|
+
this.#appActionToKeys.set(
|
|
229
229
|
action,
|
|
230
230
|
keyArray.map(key => normalizeKeyId(key as KeyId)),
|
|
231
231
|
);
|
|
@@ -236,7 +236,7 @@ export class KeybindingsManager {
|
|
|
236
236
|
* Check if input matches an app action.
|
|
237
237
|
*/
|
|
238
238
|
matches(data: string, action: AppAction): boolean {
|
|
239
|
-
const keys = this
|
|
239
|
+
const keys = this.#appActionToKeys.get(action);
|
|
240
240
|
if (!keys) return false;
|
|
241
241
|
for (const key of keys) {
|
|
242
242
|
if (matchesKey(data, key)) return true;
|
|
@@ -248,7 +248,7 @@ export class KeybindingsManager {
|
|
|
248
248
|
* Get keys bound to an app action.
|
|
249
249
|
*/
|
|
250
250
|
getKeys(action: AppAction): KeyId[] {
|
|
251
|
-
return this
|
|
251
|
+
return this.#appActionToKeys.get(action) ?? [];
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
/**
|
|
@@ -253,10 +253,10 @@ function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<A
|
|
|
253
253
|
* Model registry - loads and manages models, resolves API keys via AuthStorage.
|
|
254
254
|
*/
|
|
255
255
|
export class ModelRegistry {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
256
|
+
#models: Model<Api>[] = [];
|
|
257
|
+
#customProviderApiKeys: Map<string, string> = new Map();
|
|
258
|
+
#configError: ConfigError | undefined = undefined;
|
|
259
|
+
#modelsConfigFile: ConfigFile<ModelsConfig>;
|
|
260
260
|
|
|
261
261
|
/**
|
|
262
262
|
* @param authStorage - Auth storage for API key resolution
|
|
@@ -265,83 +265,48 @@ export class ModelRegistry {
|
|
|
265
265
|
readonly authStorage: AuthStorage,
|
|
266
266
|
modelsPath?: string,
|
|
267
267
|
) {
|
|
268
|
-
this
|
|
268
|
+
this.#modelsConfigFile = ModelsConfigFile.relocate(modelsPath);
|
|
269
269
|
// Set up fallback resolver for custom provider API keys
|
|
270
270
|
this.authStorage.setFallbackResolver(provider => {
|
|
271
|
-
const keyConfig = this
|
|
271
|
+
const keyConfig = this.#customProviderApiKeys.get(provider);
|
|
272
272
|
if (keyConfig) {
|
|
273
273
|
return resolveApiKeyConfig(keyConfig);
|
|
274
274
|
}
|
|
275
275
|
return undefined;
|
|
276
276
|
});
|
|
277
277
|
// Load models synchronously in constructor
|
|
278
|
-
this
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Create an in-memory ModelRegistry instance from serialized data.
|
|
283
|
-
* Used by subagent workers to bypass discovery and use parent's models.
|
|
284
|
-
*/
|
|
285
|
-
static fromSerialized(data: SerializedModelRegistry, authStorage: AuthStorage): ModelRegistry {
|
|
286
|
-
const instance = Object.create(ModelRegistry.prototype) as ModelRegistry;
|
|
287
|
-
(instance as any).authStorage = authStorage;
|
|
288
|
-
instance.models = data.models;
|
|
289
|
-
instance.customProviderApiKeys = new Map(Object.entries(data.customProviderApiKeys ?? {}));
|
|
290
|
-
|
|
291
|
-
authStorage.setFallbackResolver(provider => {
|
|
292
|
-
const keyConfig = instance.customProviderApiKeys.get(provider);
|
|
293
|
-
if (keyConfig) {
|
|
294
|
-
return resolveApiKeyConfig(keyConfig);
|
|
295
|
-
}
|
|
296
|
-
return undefined;
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
return instance;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Serialize ModelRegistry for passing to subagent workers.
|
|
304
|
-
*/
|
|
305
|
-
serialize(): SerializedModelRegistry {
|
|
306
|
-
const customProviderApiKeys: Record<string, string> = {};
|
|
307
|
-
for (const [k, v] of this.customProviderApiKeys.entries()) {
|
|
308
|
-
customProviderApiKeys[k] = v;
|
|
309
|
-
}
|
|
310
|
-
return {
|
|
311
|
-
models: this.models,
|
|
312
|
-
customProviderApiKeys: Object.keys(customProviderApiKeys).length > 0 ? customProviderApiKeys : undefined,
|
|
313
|
-
};
|
|
278
|
+
this.#loadModels();
|
|
314
279
|
}
|
|
315
280
|
|
|
316
281
|
/**
|
|
317
282
|
* Reload models from disk (built-in + custom from models.json).
|
|
318
283
|
*/
|
|
319
284
|
refresh(): void {
|
|
320
|
-
this
|
|
321
|
-
this
|
|
322
|
-
this
|
|
323
|
-
this
|
|
285
|
+
this.#modelsConfigFile.invalidate();
|
|
286
|
+
this.#customProviderApiKeys.clear();
|
|
287
|
+
this.#configError = undefined;
|
|
288
|
+
this.#loadModels();
|
|
324
289
|
}
|
|
325
290
|
|
|
326
291
|
/**
|
|
327
292
|
* Get any error from loading models.json (undefined if no error).
|
|
328
293
|
*/
|
|
329
294
|
getError(): ConfigError | undefined {
|
|
330
|
-
return this
|
|
295
|
+
return this.#configError;
|
|
331
296
|
}
|
|
332
297
|
|
|
333
|
-
|
|
298
|
+
#loadModels() {
|
|
334
299
|
// Load custom models from models.json first (to know which providers to override)
|
|
335
300
|
const {
|
|
336
301
|
models: customModels = [],
|
|
337
302
|
overrides = new Map(),
|
|
338
303
|
modelOverrides = new Map(),
|
|
339
304
|
error: configError,
|
|
340
|
-
} = this
|
|
341
|
-
this
|
|
305
|
+
} = this.#loadCustomModels();
|
|
306
|
+
this.#configError = configError;
|
|
342
307
|
|
|
343
|
-
const builtInModels = this
|
|
344
|
-
const combined = this
|
|
308
|
+
const builtInModels = this.#loadBuiltInModels(overrides, modelOverrides);
|
|
309
|
+
const combined = this.#mergeCustomModels(builtInModels, customModels);
|
|
345
310
|
|
|
346
311
|
// Update github-copilot base URL based on OAuth credentials
|
|
347
312
|
const copilotCred = this.authStorage.getOAuthCredential("github-copilot");
|
|
@@ -350,14 +315,14 @@ export class ModelRegistry {
|
|
|
350
315
|
? (normalizeDomain(copilotCred.enterpriseUrl) ?? undefined)
|
|
351
316
|
: undefined;
|
|
352
317
|
const baseUrl = getGitHubCopilotBaseUrl(copilotCred.access, domain);
|
|
353
|
-
this
|
|
318
|
+
this.#models = combined.map(m => (m.provider === "github-copilot" ? { ...m, baseUrl } : m));
|
|
354
319
|
} else {
|
|
355
|
-
this
|
|
320
|
+
this.#models = combined;
|
|
356
321
|
}
|
|
357
322
|
}
|
|
358
323
|
|
|
359
324
|
/** Load built-in models, applying provider and per-model overrides */
|
|
360
|
-
|
|
325
|
+
#loadBuiltInModels(
|
|
361
326
|
overrides: Map<string, ProviderOverride>,
|
|
362
327
|
modelOverrides: Map<string, Map<string, ModelOverride>>,
|
|
363
328
|
): Model<Api>[] {
|
|
@@ -385,7 +350,7 @@ export class ModelRegistry {
|
|
|
385
350
|
}
|
|
386
351
|
|
|
387
352
|
/** Merge custom models with built-in, replacing by provider+id match */
|
|
388
|
-
|
|
353
|
+
#mergeCustomModels(builtInModels: Model<Api>[], customModels: Model<Api>[]): Model<Api>[] {
|
|
389
354
|
const merged = [...builtInModels];
|
|
390
355
|
for (const customModel of customModels) {
|
|
391
356
|
const existingIndex = merged.findIndex(m => m.provider === customModel.provider && m.id === customModel.id);
|
|
@@ -398,8 +363,8 @@ export class ModelRegistry {
|
|
|
398
363
|
return merged;
|
|
399
364
|
}
|
|
400
365
|
|
|
401
|
-
|
|
402
|
-
const { value, error, status } = this
|
|
366
|
+
#loadCustomModels(): CustomModelsResult {
|
|
367
|
+
const { value, error, status } = this.#modelsConfigFile.tryLoad();
|
|
403
368
|
|
|
404
369
|
if (status === "error") {
|
|
405
370
|
return { models: [], overrides: new Map(), modelOverrides: new Map(), error, found: true };
|
|
@@ -422,7 +387,7 @@ export class ModelRegistry {
|
|
|
422
387
|
|
|
423
388
|
// Always store API key for fallback resolver
|
|
424
389
|
if (providerConfig.apiKey) {
|
|
425
|
-
this
|
|
390
|
+
this.#customProviderApiKeys.set(providerName, providerConfig.apiKey);
|
|
426
391
|
}
|
|
427
392
|
|
|
428
393
|
// Parse per-model overrides
|
|
@@ -435,10 +400,10 @@ export class ModelRegistry {
|
|
|
435
400
|
}
|
|
436
401
|
}
|
|
437
402
|
|
|
438
|
-
return { models: this
|
|
403
|
+
return { models: this.#parseModels(value), overrides, modelOverrides: allModelOverrides, found: true };
|
|
439
404
|
}
|
|
440
405
|
|
|
441
|
-
|
|
406
|
+
#parseModels(config: ModelsConfig): Model<Api>[] {
|
|
442
407
|
const models: Model<Api>[] = [];
|
|
443
408
|
|
|
444
409
|
for (const [providerName, providerConfig] of Object.entries(config.providers)) {
|
|
@@ -447,7 +412,7 @@ export class ModelRegistry {
|
|
|
447
412
|
|
|
448
413
|
// Store API key config for fallback resolver
|
|
449
414
|
if (providerConfig.apiKey) {
|
|
450
|
-
this
|
|
415
|
+
this.#customProviderApiKeys.set(providerName, providerConfig.apiKey);
|
|
451
416
|
}
|
|
452
417
|
|
|
453
418
|
for (const modelDef of modelDefs) {
|
|
@@ -496,7 +461,7 @@ export class ModelRegistry {
|
|
|
496
461
|
* If models.json had errors, returns only built-in models.
|
|
497
462
|
*/
|
|
498
463
|
getAll(): Model<Api>[] {
|
|
499
|
-
return this
|
|
464
|
+
return this.#models;
|
|
500
465
|
}
|
|
501
466
|
|
|
502
467
|
/**
|
|
@@ -504,21 +469,21 @@ export class ModelRegistry {
|
|
|
504
469
|
* This is a fast check that doesn't refresh OAuth tokens.
|
|
505
470
|
*/
|
|
506
471
|
getAvailable(): Model<Api>[] {
|
|
507
|
-
return this
|
|
472
|
+
return this.#models.filter(m => this.authStorage.hasAuth(m.provider));
|
|
508
473
|
}
|
|
509
474
|
|
|
510
475
|
/**
|
|
511
476
|
* Find a model by provider and ID.
|
|
512
477
|
*/
|
|
513
478
|
find(provider: string, modelId: string): Model<Api> | undefined {
|
|
514
|
-
return this
|
|
479
|
+
return this.#models.find(m => m.provider === provider && m.id === modelId);
|
|
515
480
|
}
|
|
516
481
|
|
|
517
482
|
/**
|
|
518
483
|
* Get the base URL associated with a provider, if any model defines one.
|
|
519
484
|
*/
|
|
520
485
|
getProviderBaseUrl(provider: string): string | undefined {
|
|
521
|
-
return this
|
|
486
|
+
return this.#models.find(m => m.provider === provider && m.baseUrl)?.baseUrl;
|
|
522
487
|
}
|
|
523
488
|
|
|
524
489
|
/**
|