@oh-my-pi/pi-coding-agent 14.1.0 → 14.1.1
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 +79 -0
- package/package.json +8 -8
- package/src/async/job-manager.ts +43 -10
- package/src/commit/agentic/tools/analyze-file.ts +1 -2
- package/src/config/mcp-schema.json +1 -1
- package/src/config/model-equivalence.ts +1 -0
- package/src/config/model-registry.ts +63 -34
- package/src/config/model-resolver.ts +111 -15
- package/src/config/settings-schema.ts +4 -3
- package/src/config/settings.ts +1 -1
- package/src/cursor.ts +64 -23
- package/src/edit/index.ts +254 -89
- package/src/edit/modes/chunk.ts +336 -57
- package/src/edit/modes/hashline.ts +51 -26
- package/src/edit/modes/patch.ts +16 -10
- package/src/edit/modes/replace.ts +15 -7
- package/src/edit/renderer.ts +248 -94
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +6 -4
- package/src/extensibility/custom-tools/types.ts +0 -3
- package/src/extensibility/extensions/loader.ts +16 -0
- package/src/extensibility/extensions/runner.ts +2 -7
- package/src/extensibility/extensions/types.ts +8 -4
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/ipy/executor.ts +447 -52
- package/src/ipy/kernel.ts +39 -13
- package/src/lsp/client.ts +54 -0
- package/src/lsp/index.ts +8 -0
- package/src/lsp/types.ts +6 -0
- package/src/main.ts +0 -1
- package/src/modes/acp/acp-agent.ts +4 -1
- package/src/modes/components/bash-execution.ts +16 -4
- package/src/modes/components/status-line/presets.ts +17 -6
- package/src/modes/components/status-line/segments.ts +15 -0
- package/src/modes/components/status-line-segment-editor.ts +1 -0
- package/src/modes/components/status-line.ts +7 -1
- package/src/modes/components/tool-execution.ts +145 -75
- package/src/modes/controllers/command-controller.ts +24 -1
- package/src/modes/controllers/event-controller.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +28 -5
- package/src/modes/controllers/input-controller.ts +9 -3
- package/src/modes/controllers/selector-controller.ts +4 -1
- package/src/modes/interactive-mode.ts +19 -3
- package/src/modes/print-mode.ts +13 -4
- package/src/modes/prompt-action-autocomplete.ts +3 -5
- package/src/modes/rpc/rpc-mode.ts +8 -2
- package/src/modes/shared.ts +2 -2
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/chunk-edit.md +191 -163
- package/src/prompts/tools/hashline.md +11 -11
- package/src/prompts/tools/patch.md +10 -5
- package/src/prompts/tools/{await.md → poll.md} +1 -1
- package/src/prompts/tools/read-chunk.md +3 -3
- package/src/prompts/tools/task.md +2 -2
- package/src/prompts/tools/vim.md +98 -0
- package/src/sdk.ts +754 -724
- package/src/session/agent-session.ts +164 -34
- package/src/session/session-manager.ts +50 -4
- package/src/slash-commands/builtin-registry.ts +17 -0
- package/src/task/executor.ts +4 -4
- package/src/task/index.ts +3 -5
- package/src/task/types.ts +2 -2
- package/src/tools/bash.ts +26 -8
- package/src/tools/find.ts +5 -2
- package/src/tools/grep.ts +77 -8
- package/src/tools/index.ts +48 -19
- package/src/tools/{await-tool.ts → poll-tool.ts} +36 -30
- package/src/tools/python.ts +293 -278
- package/src/tools/submit-result.ts +5 -2
- package/src/tools/todo-write.ts +8 -2
- package/src/tools/vim.ts +966 -0
- package/src/utils/edit-mode.ts +2 -1
- package/src/utils/session-color.ts +55 -0
- package/src/utils/title-generator.ts +15 -6
- package/src/vim/buffer.ts +309 -0
- package/src/vim/commands.ts +382 -0
- package/src/vim/engine.ts +2426 -0
- package/src/vim/parser.ts +151 -0
- package/src/vim/render.ts +252 -0
- package/src/vim/types.ts +197 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,80 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [14.1.1] - 2026-04-14
|
|
6
|
+
|
|
7
|
+
### Breaking Changes
|
|
8
|
+
|
|
9
|
+
- Removed the standalone `vim` tool from built-in tool lists, so vim-style editing is now invoked through `edit` in `vim` mode
|
|
10
|
+
- Removed the `searchDb` field from session and extension tool contexts, so custom tools and extensions no longer receive a shared native search DB handle from `ToolSession`, `CustomToolContext`, `ExtensionContext`, and `CreateAgentSessionOptions`
|
|
11
|
+
- Changed the `vim` tool API to require either `open: "path"` or `kbd: [...]` per call and removed direct `line`/`col` cursor parameters from `open`, so callers must position the cursor via key sequences after opening
|
|
12
|
+
- Changed the `edit` schemas for patch, replace, hashline, and chunk modes from top-level request fields to `edits` array entries, requiring path/mode details on each edit and breaking callers that send legacy top-level `path`, `old_text`, `new_text`, `op`, `move`, or `delete` payloads
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Added Vim ex aliases `:del`, `:ya`, `:co`, and `:mo` as shorthand for existing delete, yank, copy, and move commands
|
|
17
|
+
- Added support for additional Vim ex command aliases `:write`/`write!`, `:edit`/`edit!`, and `:update`/`:up` in command parsing
|
|
18
|
+
- Added support for vim `:global` and `:vglobal`/`/` variants as `:g/pattern/d` and `:v/pattern/d` parsing and execution
|
|
19
|
+
- Added support for extra Vim operations by treating `x`, `X`, `s`, `S`, `C`, and `D` as delete/change operator aliases
|
|
20
|
+
- Added support for new Vim motions `gE`/`ge`, `g_`, `g*`, `g#`, and `|`
|
|
21
|
+
- Added support for `C-f` and `C-b` page motions in vim mode
|
|
22
|
+
- Added `C-u` and `C-o` in vim insert mode to clear to line start and execute a one-off normal-mode command before returning to insert
|
|
23
|
+
- Added insert-mode visual operators `J`, `u`, `U`, `p`, and `P` to join lines, convert case, and replace the selected region with register content
|
|
24
|
+
- Added normal-mode line motions `+`, `-`, and `_` to move to line offsets at the first non-blank character
|
|
25
|
+
- Added `*` and `#` normal-mode commands to search forward or backward for the word under the cursor
|
|
26
|
+
- Added `gJ` to join a line range, `gv` to restore the last visual selection, and `ZZ`/`ZQ` shortcuts for save-and-exit or exit-without-save in vim mode
|
|
27
|
+
- Added paragraph text object `p` for `ip`/`ap`-style paragraph selection
|
|
28
|
+
- Added support for Vim ex line-address forms like `.`, `$`, `+N`/`-N`, destination addresses such as `:t$`, and ranged `:global` commands
|
|
29
|
+
- Added Vim ex `:join`/`:j` and `:join!`/`:j!` support to join addressed lines with or without whitespace normalization
|
|
30
|
+
- Added a warning when chunk edits write to the `~` selector with body lines that appear over-indented, instructing users to start top-level body text at column 0
|
|
31
|
+
- Added validation feedback for suspect indentation in chunk-mode `~` body writes so users can align content with the tool's automatic base indentation
|
|
32
|
+
- Added support for multi-file `edit` calls across replace, patch, hashline, and chunk modes by grouping `edits` entries by file path and returning combined per-file results
|
|
33
|
+
- Added per-edit `path` support in chunk entries so each operation can target explicit files when submitting mixed edits in a single request
|
|
34
|
+
- Added support for `computeHashlineDiff` to accept hashline edits with `loc` and `content` payloads without requiring pre-resolved `op` fields
|
|
35
|
+
- Added `/rename <title>` slash command to set an explicit session name, updating the session header and terminal tab title ([#658](https://github.com/can1357/oh-my-pi/issues/658))
|
|
36
|
+
- Added `session_name` status line segment: displays the session name in the status bar right side with a stable hash-derived accent color unique to each name; shown in all presets when a name is set
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- Changed vim path normalization to accept colon-prefixed `path` values instead of rejecting them as Vim commands
|
|
41
|
+
- Changed default `providers.openaiWebsockets` setting to `off` when unset, so OpenAI websocket transport is now disabled unless explicitly enabled
|
|
42
|
+
- Changed Vim ex `:update`/`:up` execution to skip writing unchanged buffers and report buffer unchanged status
|
|
43
|
+
- Changed Vim page-scroll commands `C-f`, `C-b`, `C-u`, and `C-d` to move in viewport-height based increments instead of fixed constants
|
|
44
|
+
- Changed `z` command behavior so `zt`, `zb`, and `z.` now align cursor movement to first non-blank in the line
|
|
45
|
+
- Changed `:g`/`:v` global command handling to process matching lines safely by working in reverse order and preserving file structure
|
|
46
|
+
- Changed vim tab breadcrumb rendering from ` → ` to `→` in the editor view
|
|
47
|
+
- Changed custom tool and task execution contexts to no longer expose a shared `searchDb` accessor, removing direct access to native grep/glob/fuzzyFind search backends from extension callbacks
|
|
48
|
+
- Changed the `task` tool `schema` field to require JSON-encoded JTD schema text instead of a schema object, matching prompt guidance and task-subagent invocation
|
|
49
|
+
- Changed chunk edit payloads to encode selectors as `path: "file:selector"` and updated chunk tool guidance and examples to match
|
|
50
|
+
- Updated `edit` call/result rendering to show per-file diff sections and append a `(+N more)` hint when edits target multiple files
|
|
51
|
+
- Grouped chunk-mode `grep` results by directory, file, and chunk so directory searches now render as hierarchical sections (`#`/`##`) with per-chunk anchor lines
|
|
52
|
+
- Updated chunk-mode `grep` output to include match lines under their containing chunk entries with consistent line-number alignment based on file length
|
|
53
|
+
- Changed eager todo enforcement to only apply on the first user message of a conversation, skipping subsequent user turns that may correct, clarify, or redirect the prior task
|
|
54
|
+
|
|
55
|
+
### Removed
|
|
56
|
+
|
|
57
|
+
- Removed live in-progress Vim tool previews during streaming call execution, so the TUI now shows only the last completed file viewport until the call finishes
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
- Fixed vim-mode multi-step line edits by auto-reordering ascending line-positioned commands to descending order before execution
|
|
62
|
+
- Fixed Vim viewport rendering to display the inline highlighted cursor character and keep long cursor lines centered around the cursor in tool previews
|
|
63
|
+
- Fixed Vim `:global` command defaults to handle only supported subcommands and report unsupported ones explicitly
|
|
64
|
+
- Fixed Vim ex execution so parsed `:update`, `:yank`, and `:put` commands now run instead of falling through
|
|
65
|
+
- Fixed vim tool rendering so streamed calls preview the live target viewport and large insert payloads update incrementally instead of popping in all at once
|
|
66
|
+
- Fixed session event delivery so streaming `message_update`/tool-call previews reach the TUI immediately instead of waiting for extension handlers to finish
|
|
67
|
+
- Fixed HTML session export rendering so background-job wait calls render as `poll` instead of stale `await`, while still recognizing legacy exported sessions
|
|
68
|
+
- Fixed OpenRouter model resolution to accept dated routed selectors such as `openrouter/z-ai/glm-4.7-20251222:nitro`, inheriting metadata from the base catalog model when the exact variant is not listed yet
|
|
69
|
+
- Fixed pre-execution edit preview routing so replace/patch/hashline mode diffs are computed from the new structured edit entries
|
|
70
|
+
- Adjusted chunk/hashline/prompt guidance and validation to align with the refactored per-entry schema
|
|
71
|
+
- Fixed chunk streaming output detection to verify chunk edits with `chunkToolEditSchema`, preventing non-chunk edit payloads from being rendered as chunk diffs
|
|
72
|
+
- Fixed tool execution output to return the original `toolResult` text content from tools instead of sanitizing it before sending completion messages
|
|
73
|
+
- Fixed session accent rendering in the status line and editor to reset only foreground color (`\x1b[39m`) so applying a session color no longer clears other ANSI styles
|
|
74
|
+
- Session name sanitization: strip C0/C1 control characters (including ANSI ESC) from session names at storage time and in status line rendering, preventing escape sequence injection into TUI output
|
|
75
|
+
- Auto-generated session titles no longer overwrite a name set via `/rename`: `setSessionName` now tracks whether the name was set by the user or auto-generated and silently ignores auto titles once a user name is in place; terminal title follows the same guard
|
|
76
|
+
- Session accent border color now applied on session resume and after auto-title generation, not only after an explicit `/rename`
|
|
77
|
+
- Fixed retained Python kernel ownership so `AgentSession.dispose()` only shuts down kernels owned by that session, including warmup-created kernels
|
|
78
|
+
|
|
5
79
|
## [14.1.0] - 2026-04-11
|
|
6
80
|
### Added
|
|
7
81
|
|
|
@@ -85,6 +159,10 @@
|
|
|
85
159
|
- Fixed stale diagnostics being reused after unrelated file publishes by clearing cached diagnostics before refreshing file state
|
|
86
160
|
- Fixed Codex search to use streamed answer text when final answer is an image placeholder or empty
|
|
87
161
|
|
|
162
|
+
### Fixed
|
|
163
|
+
|
|
164
|
+
- Fixed MCP config docs and schema to use `~/.omp/agent/mcp.json` for user-scoped OMP-native MCP config while keeping project config at `<cwd>/.omp/mcp.json`
|
|
165
|
+
|
|
88
166
|
## [14.0.4] - 2026-04-10
|
|
89
167
|
### Added
|
|
90
168
|
|
|
@@ -105,6 +183,7 @@
|
|
|
105
183
|
|
|
106
184
|
- Fixed typo in system prompt: 'backwards compatibiltity' → 'backwards compatibility'
|
|
107
185
|
|
|
186
|
+
|
|
108
187
|
## [14.0.3] - 2026-04-09
|
|
109
188
|
|
|
110
189
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "14.1.
|
|
4
|
+
"version": "14.1.1",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -46,19 +46,19 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
48
48
|
"@mozilla/readability": "^0.6",
|
|
49
|
-
"@oh-my-pi/omp-stats": "
|
|
50
|
-
"@oh-my-pi/pi-agent-core": "
|
|
51
|
-
"@oh-my-pi/pi-ai": "
|
|
52
|
-
"@oh-my-pi/pi-natives": "
|
|
53
|
-
"@oh-my-pi/pi-tui": "
|
|
54
|
-
"@oh-my-pi/pi-utils": "
|
|
49
|
+
"@oh-my-pi/omp-stats": "workspace:*",
|
|
50
|
+
"@oh-my-pi/pi-agent-core": "workspace:*",
|
|
51
|
+
"@oh-my-pi/pi-ai": "workspace:*",
|
|
52
|
+
"@oh-my-pi/pi-natives": "workspace:*",
|
|
53
|
+
"@oh-my-pi/pi-tui": "workspace:*",
|
|
54
|
+
"@oh-my-pi/pi-utils": "workspace:*",
|
|
55
55
|
"@sinclair/typebox": "^0.34",
|
|
56
56
|
"@xterm/headless": "^6.0",
|
|
57
57
|
"ajv": "^8.18",
|
|
58
58
|
"chalk": "^5.6",
|
|
59
59
|
"diff": "^8.0",
|
|
60
60
|
"fflate": "0.8.2",
|
|
61
|
-
"handlebars": "^4.7",
|
|
61
|
+
"handlebars": "^4.7.9",
|
|
62
62
|
"linkedom": "^0.18",
|
|
63
63
|
"lru-cache": "11.3.1",
|
|
64
64
|
"markit-ai": "0.5.0",
|
package/src/async/job-manager.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { logger
|
|
1
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
2
2
|
|
|
3
3
|
const DELIVERY_RETRY_BASE_MS = 500;
|
|
4
4
|
const DELIVERY_RETRY_MAX_MS = 30_000;
|
|
@@ -48,6 +48,7 @@ export class AsyncJobManager {
|
|
|
48
48
|
readonly #jobs = new Map<string, AsyncJob>();
|
|
49
49
|
readonly #deliveries: AsyncJobDelivery[] = [];
|
|
50
50
|
readonly #suppressedDeliveries = new Set<string>();
|
|
51
|
+
readonly #watchedJobs = new Set<string>();
|
|
51
52
|
readonly #evictionTimers = new Map<string, NodeJS.Timeout>();
|
|
52
53
|
readonly #onJobComplete: AsyncJobManagerOptions["onJobComplete"];
|
|
53
54
|
readonly #maxRunningJobs: number;
|
|
@@ -184,6 +185,25 @@ export class AsyncJobManager {
|
|
|
184
185
|
return this.#deliveries.length > 0;
|
|
185
186
|
}
|
|
186
187
|
|
|
188
|
+
watchJobs(jobIds: string[]): number {
|
|
189
|
+
const uniqueJobIds = Array.from(new Set(jobIds.map(id => id.trim()).filter(id => id.length > 0)));
|
|
190
|
+
for (const jobId of uniqueJobIds) {
|
|
191
|
+
this.#watchedJobs.add(jobId);
|
|
192
|
+
}
|
|
193
|
+
return uniqueJobIds.length;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
unwatchJobs(jobIds: string[]): number {
|
|
197
|
+
const uniqueJobIds = Array.from(new Set(jobIds.map(id => id.trim()).filter(id => id.length > 0)));
|
|
198
|
+
let removed = 0;
|
|
199
|
+
for (const jobId of uniqueJobIds) {
|
|
200
|
+
if (this.#watchedJobs.delete(jobId)) {
|
|
201
|
+
removed += 1;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return removed;
|
|
205
|
+
}
|
|
206
|
+
|
|
187
207
|
acknowledgeDeliveries(jobIds: string[]): number {
|
|
188
208
|
const uniqueJobIds = Array.from(new Set(jobIds.map(id => id.trim()).filter(id => id.length > 0)));
|
|
189
209
|
if (uniqueJobIds.length === 0) return 0;
|
|
@@ -196,7 +216,7 @@ export class AsyncJobManager {
|
|
|
196
216
|
this.#deliveries.splice(
|
|
197
217
|
0,
|
|
198
218
|
this.#deliveries.length,
|
|
199
|
-
...this.#deliveries.filter(delivery => !this
|
|
219
|
+
...this.#deliveries.filter(delivery => !this.isDeliverySuppressed(delivery.jobId)),
|
|
200
220
|
);
|
|
201
221
|
return before - this.#deliveries.length;
|
|
202
222
|
}
|
|
@@ -254,12 +274,21 @@ export class AsyncJobManager {
|
|
|
254
274
|
this.#jobs.clear();
|
|
255
275
|
this.#deliveries.length = 0;
|
|
256
276
|
this.#suppressedDeliveries.clear();
|
|
277
|
+
this.#watchedJobs.clear();
|
|
257
278
|
return drained;
|
|
258
279
|
}
|
|
259
280
|
|
|
260
281
|
#resolveJobId(preferredId?: string): string {
|
|
261
|
-
|
|
262
|
-
|
|
282
|
+
preferredId = preferredId?.trim();
|
|
283
|
+
if (!preferredId) {
|
|
284
|
+
let candidate = 1;
|
|
285
|
+
while (true) {
|
|
286
|
+
const id = `bg_${candidate}`;
|
|
287
|
+
if (!this.#jobs.has(id)) {
|
|
288
|
+
return id;
|
|
289
|
+
}
|
|
290
|
+
candidate += 1;
|
|
291
|
+
}
|
|
263
292
|
}
|
|
264
293
|
|
|
265
294
|
const base = preferredId.trim();
|
|
@@ -278,6 +307,7 @@ export class AsyncJobManager {
|
|
|
278
307
|
if (this.#retentionMs <= 0) {
|
|
279
308
|
this.#jobs.delete(jobId);
|
|
280
309
|
this.#suppressedDeliveries.delete(jobId);
|
|
310
|
+
this.#watchedJobs.delete(jobId);
|
|
281
311
|
return;
|
|
282
312
|
}
|
|
283
313
|
const existing = this.#evictionTimers.get(jobId);
|
|
@@ -288,6 +318,7 @@ export class AsyncJobManager {
|
|
|
288
318
|
this.#evictionTimers.delete(jobId);
|
|
289
319
|
this.#jobs.delete(jobId);
|
|
290
320
|
this.#suppressedDeliveries.delete(jobId);
|
|
321
|
+
this.#watchedJobs.delete(jobId);
|
|
291
322
|
}, this.#retentionMs);
|
|
292
323
|
timer.unref();
|
|
293
324
|
this.#evictionTimers.set(jobId, timer);
|
|
@@ -300,12 +331,13 @@ export class AsyncJobManager {
|
|
|
300
331
|
this.#evictionTimers.clear();
|
|
301
332
|
}
|
|
302
333
|
|
|
303
|
-
|
|
304
|
-
return this.#suppressedDeliveries.has(jobId);
|
|
334
|
+
isDeliverySuppressed(jobId: string): boolean {
|
|
335
|
+
return this.#suppressedDeliveries.has(jobId) || this.#watchedJobs.has(jobId);
|
|
305
336
|
}
|
|
306
337
|
|
|
307
338
|
#enqueueDelivery(jobId: string, text: string): void {
|
|
308
|
-
if
|
|
339
|
+
// Skip delivery if already acknowledged
|
|
340
|
+
if (this.isDeliverySuppressed(jobId)) {
|
|
309
341
|
return;
|
|
310
342
|
}
|
|
311
343
|
this.#deliveries.push({
|
|
@@ -337,7 +369,7 @@ export class AsyncJobManager {
|
|
|
337
369
|
async #runDeliveryLoop(): Promise<void> {
|
|
338
370
|
while (this.#deliveries.length > 0) {
|
|
339
371
|
const delivery = this.#deliveries[0];
|
|
340
|
-
if (this
|
|
372
|
+
if (this.isDeliverySuppressed(delivery.jobId)) {
|
|
341
373
|
this.#deliveries.shift();
|
|
342
374
|
continue;
|
|
343
375
|
}
|
|
@@ -348,7 +380,8 @@ export class AsyncJobManager {
|
|
|
348
380
|
if (this.#deliveries[0] !== delivery) {
|
|
349
381
|
continue;
|
|
350
382
|
}
|
|
351
|
-
|
|
383
|
+
// Check again after sleep
|
|
384
|
+
if (this.isDeliverySuppressed(delivery.jobId)) {
|
|
352
385
|
this.#deliveries.shift();
|
|
353
386
|
continue;
|
|
354
387
|
}
|
|
@@ -361,7 +394,7 @@ export class AsyncJobManager {
|
|
|
361
394
|
delivery.lastError = error instanceof Error ? error.message : String(error);
|
|
362
395
|
delivery.nextAttemptAt = Date.now() + this.#getRetryDelay(delivery.attempt);
|
|
363
396
|
this.#deliveries.shift();
|
|
364
|
-
if (!this
|
|
397
|
+
if (!this.isDeliverySuppressed(delivery.jobId)) {
|
|
365
398
|
this.#deliveries.push(delivery);
|
|
366
399
|
}
|
|
367
400
|
logger.warn("Async job completion delivery failed", {
|
|
@@ -43,7 +43,6 @@ function buildToolSession(
|
|
|
43
43
|
settings: options.settings,
|
|
44
44
|
authStorage: options.authStorage,
|
|
45
45
|
modelRegistry: options.modelRegistry,
|
|
46
|
-
searchDb: ctx.searchDb,
|
|
47
46
|
};
|
|
48
47
|
}
|
|
49
48
|
|
|
@@ -79,7 +78,7 @@ export function createAnalyzeFileTool(options: {
|
|
|
79
78
|
});
|
|
80
79
|
const taskParams: TaskParams = {
|
|
81
80
|
agent: "quick_task",
|
|
82
|
-
schema: analyzeFileOutputSchema,
|
|
81
|
+
schema: JSON.stringify(analyzeFileOutputSchema),
|
|
83
82
|
tasks,
|
|
84
83
|
};
|
|
85
84
|
return taskTool.execute(toolCallId, taskParams, signal, onUpdate);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
3
|
"$id": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json",
|
|
4
4
|
"title": "OMP MCP configuration",
|
|
5
|
-
"description": "Schema for mcp.json, .mcp.json, .omp/mcp.json, and ~/.omp/mcp.json used by the OMP coding agent.",
|
|
5
|
+
"description": "Schema for mcp.json, .mcp.json, .omp/mcp.json, and ~/.omp/agent/mcp.json used by the OMP coding agent.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"properties": {
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
import { isRecord, logger } from "@oh-my-pi/pi-utils";
|
|
29
29
|
import { type Static, Type } from "@sinclair/typebox";
|
|
30
30
|
import { type ConfigError, ConfigFile } from "../config";
|
|
31
|
-
import { parseModelString } from "../config/model-resolver";
|
|
31
|
+
import { parseModelString, resolveProviderModelReference } from "../config/model-resolver";
|
|
32
32
|
import { isValidThemeColor, type ThemeColor } from "../modes/theme/theme";
|
|
33
33
|
import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
|
|
34
34
|
import {
|
|
@@ -160,6 +160,7 @@ const OpenAICompatSchema = Type.Object({
|
|
|
160
160
|
vercelGatewayRouting: Type.Optional(VercelGatewayRoutingSchema),
|
|
161
161
|
extraBody: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
162
162
|
supportsStrictMode: Type.Optional(Type.Boolean()),
|
|
163
|
+
toolStrictMode: Type.Optional(Type.Union([Type.Literal("all_strict"), Type.Literal("none")])),
|
|
163
164
|
});
|
|
164
165
|
|
|
165
166
|
const EffortSchema = Type.Union([
|
|
@@ -720,31 +721,6 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
|
|
|
720
721
|
} as Model<Api>);
|
|
721
722
|
}
|
|
722
723
|
|
|
723
|
-
function buildCustomModel(
|
|
724
|
-
providerName: string,
|
|
725
|
-
providerBaseUrl: string,
|
|
726
|
-
providerApi: Api | undefined,
|
|
727
|
-
providerHeaders: Record<string, string> | undefined,
|
|
728
|
-
providerApiKey: string | undefined,
|
|
729
|
-
authHeader: boolean | undefined,
|
|
730
|
-
providerCompat: Model<Api>["compat"] | undefined,
|
|
731
|
-
modelDef: CustomModelDefinitionLike,
|
|
732
|
-
options: CustomModelBuildOptions,
|
|
733
|
-
): Model<Api> | undefined {
|
|
734
|
-
const model = buildCustomModelOverlay(
|
|
735
|
-
providerName,
|
|
736
|
-
providerBaseUrl,
|
|
737
|
-
providerApi,
|
|
738
|
-
providerHeaders,
|
|
739
|
-
providerApiKey,
|
|
740
|
-
authHeader,
|
|
741
|
-
providerCompat,
|
|
742
|
-
modelDef,
|
|
743
|
-
);
|
|
744
|
-
if (!model) return undefined;
|
|
745
|
-
return finalizeCustomModel(model, options);
|
|
746
|
-
}
|
|
747
|
-
|
|
748
724
|
function normalizeSuppressedSelector(selector: string): string {
|
|
749
725
|
const trimmed = selector.trim();
|
|
750
726
|
if (!trimmed) return trimmed;
|
|
@@ -790,6 +766,12 @@ export class ModelRegistry {
|
|
|
790
766
|
#suppressedSelectors: Map<string, number> = new Map();
|
|
791
767
|
#backgroundRefresh?: Promise<void>;
|
|
792
768
|
#lastDiscoveryWarnings: Map<string, string> = new Map();
|
|
769
|
+
// Runtime extension model overlays — persist across refresh() cycles so that
|
|
770
|
+
// models registered by extensions survive the model selector's offline reload.
|
|
771
|
+
#runtimeModelOverlays: CustomModelOverlay[] = [];
|
|
772
|
+
#runtimeProviderApiKeys: Map<string, string> = new Map();
|
|
773
|
+
#runtimeProvidersBySource: Map<string, Set<string>> = new Map();
|
|
774
|
+
#runtimeProviderSourceByName: Map<string, string> = new Map();
|
|
793
775
|
|
|
794
776
|
/**
|
|
795
777
|
* @param authStorage - Auth storage for API key resolution
|
|
@@ -854,6 +836,11 @@ export class ModelRegistry {
|
|
|
854
836
|
this.#customProviderApiKeys.clear();
|
|
855
837
|
this.#keylessProviders.clear();
|
|
856
838
|
this.#discoverableProviders = [];
|
|
839
|
+
// Restore runtime API keys before #loadModels — survives because
|
|
840
|
+
// #loadModels only calls .set() on #customProviderApiKeys, never reassigns it.
|
|
841
|
+
for (const [k, v] of this.#runtimeProviderApiKeys) {
|
|
842
|
+
this.#customProviderApiKeys.set(k, v);
|
|
843
|
+
}
|
|
857
844
|
this.#providerOverrides.clear();
|
|
858
845
|
this.#modelOverrides.clear();
|
|
859
846
|
this.#equivalenceConfig = undefined;
|
|
@@ -893,7 +880,9 @@ export class ModelRegistry {
|
|
|
893
880
|
const builtInModels = this.#applyHardcodedModelPolicies(this.#loadBuiltInModels(overrides));
|
|
894
881
|
const cachedDiscoveries = this.#applyHardcodedModelPolicies(this.#loadCachedDiscoverableModels());
|
|
895
882
|
const resolvedDefaults = this.#mergeResolvedModels(builtInModels, cachedDiscoveries);
|
|
896
|
-
const
|
|
883
|
+
const withConfigModels = this.#mergeCustomModels(resolvedDefaults, this.#customModelOverlays);
|
|
884
|
+
// Merge runtime extension models so they survive refresh() cycles
|
|
885
|
+
const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
|
|
897
886
|
|
|
898
887
|
this.#models = this.#applyModelOverrides(combined, this.#modelOverrides);
|
|
899
888
|
this.#rebuildCanonicalIndex();
|
|
@@ -1183,7 +1172,9 @@ export class ModelRegistry {
|
|
|
1183
1172
|
}),
|
|
1184
1173
|
);
|
|
1185
1174
|
const resolved = this.#mergeResolvedModels(this.#models, discoveredModels);
|
|
1186
|
-
const
|
|
1175
|
+
const withConfigModels = this.#mergeCustomModels(resolved, this.#customModelOverlays);
|
|
1176
|
+
// Merge runtime extension models so they survive online discovery completion
|
|
1177
|
+
const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
|
|
1187
1178
|
this.#models = this.#applyModelOverrides(combined, this.#modelOverrides);
|
|
1188
1179
|
this.#rebuildCanonicalIndex();
|
|
1189
1180
|
}
|
|
@@ -1881,7 +1872,7 @@ export class ModelRegistry {
|
|
|
1881
1872
|
* Find a model by provider and ID.
|
|
1882
1873
|
*/
|
|
1883
1874
|
find(provider: string, modelId: string): Model<Api> | undefined {
|
|
1884
|
-
return
|
|
1875
|
+
return resolveProviderModelReference(provider, modelId, this.#models);
|
|
1885
1876
|
}
|
|
1886
1877
|
|
|
1887
1878
|
/**
|
|
@@ -1931,6 +1922,21 @@ export class ModelRegistry {
|
|
|
1931
1922
|
clearSourceRegistrations(sourceId: string): void {
|
|
1932
1923
|
unregisterCustomApis(sourceId);
|
|
1933
1924
|
unregisterOAuthProviders(sourceId);
|
|
1925
|
+
const sourceProviders = this.#runtimeProvidersBySource.get(sourceId);
|
|
1926
|
+
if (!sourceProviders || sourceProviders.size === 0) {
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
this.#runtimeProvidersBySource.delete(sourceId);
|
|
1930
|
+
for (const providerName of sourceProviders) {
|
|
1931
|
+
if (this.#runtimeProviderSourceByName.get(providerName) !== sourceId) {
|
|
1932
|
+
continue;
|
|
1933
|
+
}
|
|
1934
|
+
this.#runtimeProviderSourceByName.delete(providerName);
|
|
1935
|
+
this.#runtimeProviderApiKeys.delete(providerName);
|
|
1936
|
+
this.#runtimeModelOverlays = this.#runtimeModelOverlays.filter(overlay => overlay.provider !== providerName);
|
|
1937
|
+
}
|
|
1938
|
+
this.#reloadStaticModels();
|
|
1939
|
+
this.#rebuildCanonicalIndex();
|
|
1934
1940
|
}
|
|
1935
1941
|
|
|
1936
1942
|
/**
|
|
@@ -1989,15 +1995,30 @@ export class ModelRegistry {
|
|
|
1989
1995
|
|
|
1990
1996
|
if (sourceId) {
|
|
1991
1997
|
this.#registeredProviderSources.add(sourceId);
|
|
1998
|
+
const previousSourceId = this.#runtimeProviderSourceByName.get(providerName);
|
|
1999
|
+
if (previousSourceId && previousSourceId !== sourceId) {
|
|
2000
|
+
const previousProviders = this.#runtimeProvidersBySource.get(previousSourceId);
|
|
2001
|
+
previousProviders?.delete(providerName);
|
|
2002
|
+
if (previousProviders && previousProviders.size === 0) {
|
|
2003
|
+
this.#runtimeProvidersBySource.delete(previousSourceId);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
const sourceProviders = this.#runtimeProvidersBySource.get(sourceId) ?? new Set<string>();
|
|
2007
|
+
sourceProviders.add(providerName);
|
|
2008
|
+
this.#runtimeProvidersBySource.set(sourceId, sourceProviders);
|
|
2009
|
+
this.#runtimeProviderSourceByName.set(providerName, sourceId);
|
|
1992
2010
|
}
|
|
1993
2011
|
if (config.apiKey) {
|
|
1994
2012
|
this.#customProviderApiKeys.set(providerName, config.apiKey);
|
|
2013
|
+
// Persist runtime API keys so they survive #reloadStaticModels() cycles
|
|
2014
|
+
this.#runtimeProviderApiKeys.set(providerName, config.apiKey);
|
|
1995
2015
|
}
|
|
1996
2016
|
|
|
1997
2017
|
if (config.models && config.models.length > 0) {
|
|
1998
|
-
|
|
2018
|
+
// Build model overlays that persist across refresh() cycles
|
|
2019
|
+
const newOverlays: CustomModelOverlay[] = [];
|
|
1999
2020
|
for (const modelDef of config.models) {
|
|
2000
|
-
const
|
|
2021
|
+
const overlay = buildCustomModelOverlay(
|
|
2001
2022
|
providerName,
|
|
2002
2023
|
config.baseUrl!,
|
|
2003
2024
|
config.api,
|
|
@@ -2006,12 +2027,20 @@ export class ModelRegistry {
|
|
|
2006
2027
|
config.authHeader,
|
|
2007
2028
|
config.compat,
|
|
2008
2029
|
modelDef as CustomModelDefinitionLike,
|
|
2009
|
-
{ useDefaults: true },
|
|
2010
2030
|
);
|
|
2011
|
-
if (!
|
|
2031
|
+
if (!overlay) {
|
|
2012
2032
|
throw new Error(`Provider ${providerName}, model ${modelDef.id}: no "api" specified.`);
|
|
2013
2033
|
}
|
|
2014
|
-
|
|
2034
|
+
newOverlays.push(overlay);
|
|
2035
|
+
}
|
|
2036
|
+
// Store as runtime overlays so they survive #reloadStaticModels()
|
|
2037
|
+
this.#runtimeModelOverlays = this.#runtimeModelOverlays.filter(m => m.provider !== providerName);
|
|
2038
|
+
this.#runtimeModelOverlays.push(...newOverlays);
|
|
2039
|
+
|
|
2040
|
+
// Also update #models immediately for the current cycle
|
|
2041
|
+
const nextModels = this.#models.filter(m => m.provider !== providerName);
|
|
2042
|
+
for (const overlay of newOverlays) {
|
|
2043
|
+
nextModels.push(finalizeCustomModel(overlay, { useDefaults: true }));
|
|
2015
2044
|
}
|
|
2016
2045
|
|
|
2017
2046
|
if (config.oauth?.modifyModels) {
|
|
@@ -62,6 +62,101 @@ export function formatModelSelectorValue(selector: string, thinkingLevel: Thinki
|
|
|
62
62
|
return thinkingLevel && thinkingLevel !== ThinkingLevel.Inherit ? `${selector}:${thinkingLevel}` : selector;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
function getOpenRouterRouteSuffix(modelId: string): { baseId: string; suffix: string } | undefined {
|
|
66
|
+
const colonIdx = modelId.lastIndexOf(":");
|
|
67
|
+
if (colonIdx === -1) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const suffix = modelId.slice(colonIdx + 1).trim();
|
|
72
|
+
if (!suffix || parseThinkingLevel(suffix)) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { baseId: modelId.slice(0, colonIdx), suffix };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function stripOpenRouterDateSuffix(modelId: string): string | undefined {
|
|
80
|
+
const stripped = modelId.replace(/-\d{8}(?=$|:)/i, "");
|
|
81
|
+
return stripped !== modelId ? stripped : undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getOpenRouterFallbackModelIds(modelId: string): string[] {
|
|
85
|
+
const orderedCandidates: string[] = [];
|
|
86
|
+
const queue = [modelId];
|
|
87
|
+
const seen = new Set<string>();
|
|
88
|
+
|
|
89
|
+
while (queue.length > 0) {
|
|
90
|
+
const candidate = queue.shift();
|
|
91
|
+
if (!candidate || seen.has(candidate)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
seen.add(candidate);
|
|
95
|
+
orderedCandidates.push(candidate);
|
|
96
|
+
|
|
97
|
+
const routedSuffix = getOpenRouterRouteSuffix(candidate);
|
|
98
|
+
if (routedSuffix) {
|
|
99
|
+
queue.push(routedSuffix.baseId);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const strippedDate = stripOpenRouterDateSuffix(candidate);
|
|
103
|
+
if (strippedDate) {
|
|
104
|
+
queue.push(strippedDate);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return orderedCandidates;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function cloneModelWithRequestedId(model: Model<Api>, requestedId: string): Model<Api> {
|
|
112
|
+
return {
|
|
113
|
+
...model,
|
|
114
|
+
id: requestedId,
|
|
115
|
+
...(model.name === model.id ? { name: requestedId } : {}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function resolveProviderModelReference(
|
|
120
|
+
provider: string,
|
|
121
|
+
modelId: string,
|
|
122
|
+
availableModels: readonly Model<Api>[],
|
|
123
|
+
): Model<Api> | undefined {
|
|
124
|
+
const normalizedProvider = provider.trim().toLowerCase();
|
|
125
|
+
const normalizedModelId = modelId.trim().toLowerCase();
|
|
126
|
+
if (!normalizedProvider || !normalizedModelId) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const exactMatches = availableModels.filter(
|
|
131
|
+
model => model.provider.toLowerCase() === normalizedProvider && model.id.toLowerCase() === normalizedModelId,
|
|
132
|
+
);
|
|
133
|
+
if (exactMatches.length === 1) {
|
|
134
|
+
return exactMatches[0];
|
|
135
|
+
}
|
|
136
|
+
if (exactMatches.length > 1) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (normalizedProvider !== "openrouter") {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const fallbackId of getOpenRouterFallbackModelIds(modelId).slice(1)) {
|
|
145
|
+
const baseMatches = availableModels.filter(
|
|
146
|
+
model =>
|
|
147
|
+
model.provider.toLowerCase() === normalizedProvider && model.id.toLowerCase() === fallbackId.toLowerCase(),
|
|
148
|
+
);
|
|
149
|
+
if (baseMatches.length === 1) {
|
|
150
|
+
return cloneModelWithRequestedId(baseMatches[0], modelId);
|
|
151
|
+
}
|
|
152
|
+
if (baseMatches.length > 1) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
65
160
|
export interface ModelMatchPreferences {
|
|
66
161
|
/** Most-recently-used model keys (provider/modelId) to prefer when ambiguous. */
|
|
67
162
|
usageOrder?: string[];
|
|
@@ -171,17 +266,7 @@ export function findExactModelReferenceMatch(
|
|
|
171
266
|
const provider = trimmedReference.substring(0, slashIndex).trim();
|
|
172
267
|
const modelId = trimmedReference.substring(slashIndex + 1).trim();
|
|
173
268
|
if (provider && modelId) {
|
|
174
|
-
|
|
175
|
-
model =>
|
|
176
|
-
model.provider.toLowerCase() === provider.toLowerCase() &&
|
|
177
|
-
model.id.toLowerCase() === modelId.toLowerCase(),
|
|
178
|
-
);
|
|
179
|
-
if (providerMatches.length === 1) {
|
|
180
|
-
return providerMatches[0];
|
|
181
|
-
}
|
|
182
|
-
if (providerMatches.length > 1) {
|
|
183
|
-
return undefined;
|
|
184
|
-
}
|
|
269
|
+
return resolveProviderModelReference(provider, modelId, availableModels);
|
|
185
270
|
}
|
|
186
271
|
}
|
|
187
272
|
return undefined;
|
|
@@ -853,10 +938,8 @@ export function resolveCliModel(options: {
|
|
|
853
938
|
let exact: (typeof availableModels)[number] | undefined;
|
|
854
939
|
if (slashIdx !== -1) {
|
|
855
940
|
const prefix = lower.substring(0, slashIdx);
|
|
856
|
-
const suffix =
|
|
857
|
-
exact = availableModels
|
|
858
|
-
model => model.provider.toLowerCase() === prefix && model.id.toLowerCase() === suffix,
|
|
859
|
-
);
|
|
941
|
+
const suffix = trimmedModel.substring(slashIdx + 1);
|
|
942
|
+
exact = resolveProviderModelReference(prefix, suffix, availableModels);
|
|
860
943
|
}
|
|
861
944
|
if (!exact && !trimmedModel.includes(":")) {
|
|
862
945
|
const canonicalMatch = modelRegistry.resolveCanonicalModel?.(trimmedModel, { availableOnly: false });
|
|
@@ -905,6 +988,19 @@ export function resolveCliModel(options: {
|
|
|
905
988
|
}
|
|
906
989
|
}
|
|
907
990
|
|
|
991
|
+
if (provider) {
|
|
992
|
+
const exactProviderMatch = resolveProviderModelReference(provider, pattern, availableModels);
|
|
993
|
+
if (exactProviderMatch) {
|
|
994
|
+
return {
|
|
995
|
+
model: exactProviderMatch,
|
|
996
|
+
selector: formatModelString(exactProviderMatch),
|
|
997
|
+
warning: undefined,
|
|
998
|
+
thinkingLevel: undefined,
|
|
999
|
+
error: undefined,
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
908
1004
|
const candidates = provider ? availableModels.filter(model => model.provider === provider) : availableModels;
|
|
909
1005
|
const { model, thinkingLevel, warning } = parseModelPattern(pattern, candidates, preferences, {
|
|
910
1006
|
allowInvalidThinkingSelectorFallback: false,
|
|
@@ -74,7 +74,8 @@ export type StatusLineSegmentId =
|
|
|
74
74
|
| "session"
|
|
75
75
|
| "hostname"
|
|
76
76
|
| "cache_read"
|
|
77
|
-
| "cache_write"
|
|
77
|
+
| "cache_write"
|
|
78
|
+
| "session_name";
|
|
78
79
|
|
|
79
80
|
interface UiMetadata {
|
|
80
81
|
tab: SettingTab;
|
|
@@ -951,12 +952,12 @@ export const SETTINGS_SCHEMA = {
|
|
|
951
952
|
// Edit tool
|
|
952
953
|
"edit.mode": {
|
|
953
954
|
type: "enum",
|
|
954
|
-
values: ["replace", "patch", "hashline", "chunk"] as const,
|
|
955
|
+
values: ["replace", "patch", "hashline", "chunk", "vim"] as const,
|
|
955
956
|
default: "hashline",
|
|
956
957
|
ui: {
|
|
957
958
|
tab: "editing",
|
|
958
959
|
label: "Edit Mode",
|
|
959
|
-
description: "Select the edit tool variant (replace, patch, hashline, or
|
|
960
|
+
description: "Select the edit tool variant (replace, patch, hashline, chunk, or vim)",
|
|
960
961
|
},
|
|
961
962
|
},
|
|
962
963
|
|
package/src/config/settings.ts
CHANGED
|
@@ -326,7 +326,7 @@ export class Settings {
|
|
|
326
326
|
|
|
327
327
|
/**
|
|
328
328
|
* Get the edit variant for a specific model.
|
|
329
|
-
* Returns "patch", "replace", "hashline", "chunk", or null (use global default).
|
|
329
|
+
* Returns "patch", "replace", "hashline", "chunk", "vim", or null (use global default).
|
|
330
330
|
*/
|
|
331
331
|
getEditVariantForModel(model: string | undefined): EditMode | null {
|
|
332
332
|
if (!model) return null;
|