@oh-my-pi/pi-coding-agent 14.0.5 → 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.
Files changed (101) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/package.json +8 -8
  3. package/src/async/index.ts +1 -0
  4. package/src/async/job-manager.ts +43 -10
  5. package/src/async/support.ts +5 -0
  6. package/src/cli/list-models.ts +96 -57
  7. package/src/commit/agentic/tools/analyze-file.ts +1 -2
  8. package/src/commit/model-selection.ts +16 -13
  9. package/src/config/mcp-schema.json +1 -1
  10. package/src/config/model-equivalence.ts +675 -0
  11. package/src/config/model-registry.ts +242 -45
  12. package/src/config/model-resolver.ts +282 -65
  13. package/src/config/settings-schema.ts +27 -3
  14. package/src/config/settings.ts +1 -1
  15. package/src/cursor.ts +64 -23
  16. package/src/edit/index.ts +254 -89
  17. package/src/edit/modes/chunk.ts +336 -57
  18. package/src/edit/modes/hashline.ts +51 -26
  19. package/src/edit/modes/patch.ts +16 -10
  20. package/src/edit/modes/replace.ts +15 -7
  21. package/src/edit/renderer.ts +248 -94
  22. package/src/export/html/template.css +82 -0
  23. package/src/export/html/template.generated.ts +1 -1
  24. package/src/export/html/template.js +614 -97
  25. package/src/extensibility/custom-tools/types.ts +0 -3
  26. package/src/extensibility/extensions/loader.ts +16 -0
  27. package/src/extensibility/extensions/runner.ts +2 -7
  28. package/src/extensibility/extensions/types.ts +8 -4
  29. package/src/internal-urls/docs-index.generated.ts +4 -4
  30. package/src/internal-urls/jobs-protocol.ts +2 -1
  31. package/src/ipy/executor.ts +447 -52
  32. package/src/ipy/kernel.ts +39 -13
  33. package/src/lsp/client.ts +55 -1
  34. package/src/lsp/index.ts +8 -0
  35. package/src/lsp/types.ts +6 -0
  36. package/src/main.ts +6 -2
  37. package/src/memories/index.ts +7 -6
  38. package/src/modes/acp/acp-agent.ts +4 -1
  39. package/src/modes/components/bash-execution.ts +16 -4
  40. package/src/modes/components/model-selector.ts +221 -64
  41. package/src/modes/components/status-line/presets.ts +17 -6
  42. package/src/modes/components/status-line/segments.ts +15 -0
  43. package/src/modes/components/status-line-segment-editor.ts +1 -0
  44. package/src/modes/components/status-line.ts +7 -1
  45. package/src/modes/components/tool-execution.ts +145 -75
  46. package/src/modes/controllers/command-controller.ts +42 -1
  47. package/src/modes/controllers/event-controller.ts +4 -1
  48. package/src/modes/controllers/extension-ui-controller.ts +28 -5
  49. package/src/modes/controllers/input-controller.ts +9 -3
  50. package/src/modes/controllers/selector-controller.ts +17 -6
  51. package/src/modes/interactive-mode.ts +19 -3
  52. package/src/modes/print-mode.ts +13 -4
  53. package/src/modes/prompt-action-autocomplete.ts +3 -5
  54. package/src/modes/rpc/rpc-mode.ts +8 -2
  55. package/src/modes/shared.ts +2 -2
  56. package/src/modes/types.ts +1 -0
  57. package/src/modes/utils/ui-helpers.ts +1 -0
  58. package/src/prompts/system/system-prompt.md +5 -1
  59. package/src/prompts/tools/bash.md +16 -1
  60. package/src/prompts/tools/cancel-job.md +1 -1
  61. package/src/prompts/tools/chunk-edit.md +191 -163
  62. package/src/prompts/tools/hashline.md +11 -11
  63. package/src/prompts/tools/patch.md +10 -5
  64. package/src/prompts/tools/{await.md → poll.md} +1 -1
  65. package/src/prompts/tools/read-chunk.md +12 -3
  66. package/src/prompts/tools/read.md +9 -0
  67. package/src/prompts/tools/task.md +2 -2
  68. package/src/prompts/tools/vim.md +98 -0
  69. package/src/prompts/tools/write.md +1 -0
  70. package/src/sdk.ts +758 -725
  71. package/src/session/agent-session.ts +187 -40
  72. package/src/session/session-manager.ts +50 -4
  73. package/src/slash-commands/builtin-registry.ts +17 -0
  74. package/src/task/executor.ts +9 -5
  75. package/src/task/index.ts +3 -5
  76. package/src/task/types.ts +2 -2
  77. package/src/tools/bash.ts +240 -57
  78. package/src/tools/cancel-job.ts +2 -1
  79. package/src/tools/find.ts +5 -2
  80. package/src/tools/grep.ts +77 -8
  81. package/src/tools/index.ts +48 -19
  82. package/src/tools/inspect-image.ts +1 -1
  83. package/src/tools/{await-tool.ts → poll-tool.ts} +38 -31
  84. package/src/tools/python.ts +293 -278
  85. package/src/tools/read.ts +218 -1
  86. package/src/tools/sqlite-reader.ts +623 -0
  87. package/src/tools/submit-result.ts +5 -2
  88. package/src/tools/todo-write.ts +8 -2
  89. package/src/tools/vim.ts +966 -0
  90. package/src/tools/write.ts +187 -1
  91. package/src/utils/commit-message-generator.ts +1 -0
  92. package/src/utils/edit-mode.ts +2 -1
  93. package/src/utils/git.ts +24 -1
  94. package/src/utils/session-color.ts +55 -0
  95. package/src/utils/title-generator.ts +16 -7
  96. package/src/vim/buffer.ts +309 -0
  97. package/src/vim/commands.ts +382 -0
  98. package/src/vim/engine.ts +2426 -0
  99. package/src/vim/parser.ts +151 -0
  100. package/src/vim/render.ts +252 -0
  101. package/src/vim/types.ts +197 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,121 @@
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
+
79
+ ## [14.1.0] - 2026-04-11
80
+ ### Added
81
+
82
+ - Added richer tool rendering details in session export HTML, including metadata badges, argument formatting, and todo task tree styling for exported tool and workflow messages
83
+ - Added a persistent `js` tool backed by `node:vm`, with cross-session `highway` KV/pubsub, tool calls from inside JS cells, and `$` / `$$` interactive JavaScript execution
84
+ - Added SQLite database read support to the `read` tool for `.sqlite`, `.sqlite3`, `.db`, and `.db3` files with table listing, schema + sample output, row lookup, paginated query filtering, and read-only `q=SELECT` mode
85
+ - Added SQLite mutation support to the `write` tool so `db.sqlite:table` inserts JSON5 rows and `db.sqlite:table:key` updates or deletes rows via row key
86
+ - Added rendering of usage report entries for accounts with no usage limits, including account label and optional plan type with a `-- no limits` indicator
87
+ - Updated account label resolution to fall back to email or accountId so unlabeled unlimited-plan accounts display a meaningful name
88
+ - Added canonical model equivalence and provider coalescing across `models.yml`, `enabledModels`, `--models`, `/model`, and `--list-models`
89
+ - Added `equivalence` overrides/exclusions to `models.yml` and `modelProviderOrder` to `config.yml` for global canonical-provider preference
90
+
91
+ ### Changed
92
+
93
+ - Enabled `await` and `cancel_job` to be available when `bash.autoBackground.enabled` is set, so auto-backgrounded bash jobs can be awaited or cancelled without enabling `async.enabled`
94
+ - Updated bash auto-background behavior so short commands returned inline output when they completed before the configured threshold, while longer runs moved to background jobs automatically
95
+ - Replaced the LLM-callable Python execution path with JavaScript execution in the shared VM context, including updated renderers, prompts, session messages, and extension events
96
+ - Updated interactive and CLI model listings/selectors to work with canonical model ids while resolving them to concrete provider variants for actual execution
97
+ - Updated role assignment persistence so selected model settings now store the selector used by users, including thinking-level suffixes, while runtime continues to run against the resolved concrete provider model
98
+ - Updated model scope resolution to expand exact canonical model ids into all matching provider variants when filtering supported model sets
99
+ - Changed the agent to avoid giving time estimates or task-duration predictions in user responses, focusing on required work instead
100
+ - Changed generated code guidance to avoid speculative abstractions and extra compatibility scaffolding, favoring direct implementations that match current needs
101
+ - Changed model role resolution so roles can store either canonical model ids or explicit `provider/model` selectors while sessions continue to record the concrete model actually used
102
+ - Updated bash execution to optionally auto-background long-running commands through the existing background-job pipeline, with dedicated settings for enabling the behavior and adjusting the delay
103
+
104
+ ### Fixed
105
+
106
+ - Fixed session export rendering so JavaScript execution messages now use `jsExecution` labels and content instead of `pythonExecution`, matching current tool behavior
107
+ - Fixed JavaScript cell execution to auto-display returned values once and preserve persistent VM bindings across calls until reset
108
+ - Fixed `.db`/`.db3` reads to verify SQLite file headers and fall back to normal file reading when the extension matches but the content is not a SQLite database
109
+ - Fixed SQLite selector parsing and resolution to correctly route requests to database operations at the file-extension boundary instead of misrouting through plain file/archive handlers
110
+ - Fixed unsupported or unsafe selectors by rejecting missing tables, composite primary keys for row lookups, unknown query parameters, and row operations on non-existent tables
111
+ - Fixed model resolution for commit message generation, title generation, memory consolidation, and image inspection when role strings use canonical ids instead of raw provider/model values
112
+ - Fixed default-model updates so previously configured thinking levels were preserved when reassigning a role
113
+ - Fixed model scope and selection handling in CLI/session startup paths that previously failed to resolve aliases consistently across features
114
+ - Fixed short-lived git subprocesses to disable `core.fsmonitor` and `core.untrackedCache`, avoiding unnecessary repository watchers and cache work during agent git operations
115
+
116
+ ### Security
117
+
118
+ - Blocked destructive SQL execution in read-mode SQLite access by using read-only connections and rejecting bound-parameter raw SQL
119
+
5
120
  ## [14.0.5] - 2026-04-11
6
121
  ### Added
7
122
 
@@ -44,6 +159,10 @@
44
159
  - Fixed stale diagnostics being reused after unrelated file publishes by clearing cached diagnostics before refreshing file state
45
160
  - Fixed Codex search to use streamed answer text when final answer is an image placeholder or empty
46
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
+
47
166
  ## [14.0.4] - 2026-04-10
48
167
  ### Added
49
168
 
@@ -64,6 +183,7 @@
64
183
 
65
184
  - Fixed typo in system prompt: 'backwards compatibiltity' → 'backwards compatibility'
66
185
 
186
+
67
187
  ## [14.0.3] - 2026-04-09
68
188
 
69
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.0.5",
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": "14.0.5",
50
- "@oh-my-pi/pi-agent-core": "14.0.5",
51
- "@oh-my-pi/pi-ai": "14.0.5",
52
- "@oh-my-pi/pi-natives": "14.0.5",
53
- "@oh-my-pi/pi-tui": "14.0.5",
54
- "@oh-my-pi/pi-utils": "14.0.5",
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",
@@ -1 +1,2 @@
1
1
  export * from "./job-manager";
2
+ export * from "./support";
@@ -1,4 +1,4 @@
1
- import { logger, Snowflake } from "@oh-my-pi/pi-utils";
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.#suppressedDeliveries.has(delivery.jobId)),
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
- if (!preferredId || preferredId.trim().length === 0) {
262
- return `bg_${Snowflake.next()}`;
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
- #isDeliverySuppressed(jobId: string): boolean {
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 (this.#isDeliverySuppressed(jobId)) {
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.#isDeliverySuppressed(delivery.jobId)) {
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
- if (this.#isDeliverySuppressed(delivery.jobId)) {
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.#isDeliverySuppressed(delivery.jobId)) {
397
+ if (!this.isDeliverySuppressed(delivery.jobId)) {
365
398
  this.#deliveries.push(delivery);
366
399
  }
367
400
  logger.warn("Async job completion delivery failed", {
@@ -0,0 +1,5 @@
1
+ import type { Settings } from "../config/settings";
2
+
3
+ export function isBackgroundJobSupportEnabled(settings: Pick<Settings, "get">): boolean {
4
+ return settings.get("async.enabled") || settings.get("bash.autoBackground.enabled");
5
+ }
@@ -6,6 +6,45 @@ import { formatNumber } from "@oh-my-pi/pi-utils";
6
6
  import type { ModelRegistry } from "../config/model-registry";
7
7
  import { fuzzyFilter } from "../utils/fuzzy";
8
8
 
9
+ interface ProviderRow {
10
+ provider: string;
11
+ model: string;
12
+ context: string;
13
+ maxOut: string;
14
+ thinking: string;
15
+ images: string;
16
+ }
17
+
18
+ interface CanonicalRow {
19
+ canonical: string;
20
+ selected: string;
21
+ variants: string;
22
+ context: string;
23
+ maxOut: string;
24
+ }
25
+
26
+ function writeLine(line = ""): void {
27
+ process.stdout.write(`${line}\n`);
28
+ }
29
+
30
+ function renderTable<T extends Record<string, string>>(rows: T[], headers: T): void {
31
+ const widths = Object.fromEntries(
32
+ Object.keys(headers).map(key => [key, Math.max(headers[key]!.length, ...rows.map(row => row[key]!.length))]),
33
+ ) as Record<keyof T, number>;
34
+
35
+ const headerLine = Object.keys(headers)
36
+ .map(key => headers[key as keyof T]!.padEnd(widths[key as keyof T]))
37
+ .join(" ");
38
+ writeLine(headerLine);
39
+
40
+ for (const row of rows) {
41
+ const line = Object.keys(headers)
42
+ .map(key => row[key as keyof T]!.padEnd(widths[key as keyof T]))
43
+ .join(" ");
44
+ writeLine(line);
45
+ }
46
+ }
47
+
9
48
  /**
10
49
  * List available models, optionally filtered by search pattern
11
50
  */
@@ -13,77 +52,77 @@ export async function listModels(modelRegistry: ModelRegistry, searchPattern?: s
13
52
  const models = modelRegistry.getAvailable();
14
53
 
15
54
  if (models.length === 0) {
16
- console.log("No models available. Set API keys in environment variables.");
55
+ writeLine("No models available. Set API keys in environment variables.");
17
56
  return;
18
57
  }
19
58
 
20
- // Apply fuzzy filter if search pattern provided
21
59
  let filteredModels: Model<Api>[] = models;
22
60
  if (searchPattern) {
23
- filteredModels = fuzzyFilter(models, searchPattern, m => `${m.provider} ${m.id}`);
61
+ filteredModels = fuzzyFilter(models, searchPattern, model => `${model.provider} ${model.id}`);
24
62
  }
25
63
 
26
- if (filteredModels.length === 0) {
27
- console.log(`No models matching "${searchPattern}"`);
64
+ const filteredCanonical = modelRegistry
65
+ .getCanonicalModels({ availableOnly: true, candidates: filteredModels })
66
+ .map(record => {
67
+ const selected = modelRegistry.resolveCanonicalModel(record.id, {
68
+ availableOnly: true,
69
+ candidates: filteredModels,
70
+ });
71
+ if (!selected) return undefined;
72
+ return {
73
+ canonical: record.id,
74
+ selected: `${selected.provider}/${selected.id}`,
75
+ variants: String(record.variants.length),
76
+ context: formatNumber(selected.contextWindow),
77
+ maxOut: formatNumber(selected.maxTokens),
78
+ } satisfies CanonicalRow;
79
+ })
80
+ .filter((row): row is CanonicalRow => row !== undefined)
81
+ .sort((left, right) => left.canonical.localeCompare(right.canonical));
82
+
83
+ if (filteredModels.length === 0 && filteredCanonical.length === 0) {
84
+ writeLine(`No models matching "${searchPattern}"`);
28
85
  return;
29
86
  }
30
87
 
31
- // Sort by provider, then by model id
32
- filteredModels.sort((a, b) => {
33
- const providerCmp = a.provider.localeCompare(b.provider);
88
+ filteredModels.sort((left, right) => {
89
+ const providerCmp = left.provider.localeCompare(right.provider);
34
90
  if (providerCmp !== 0) return providerCmp;
35
- return a.id.localeCompare(b.id);
91
+ return left.id.localeCompare(right.id);
36
92
  });
37
93
 
38
- // Calculate column widths
39
- const rows = filteredModels.map(m => ({
40
- provider: m.provider,
41
- model: m.id,
42
- context: formatNumber(m.contextWindow),
43
- maxOut: formatNumber(m.maxTokens),
44
- thinking: m.thinking ? getSupportedEfforts(m).join(",") : m.reasoning ? "yes" : "-",
45
- images: m.input.includes("image") ? "yes" : "no",
46
- }));
94
+ const providerRows = filteredModels.map(model => ({
95
+ provider: model.provider,
96
+ model: model.id,
97
+ context: formatNumber(model.contextWindow),
98
+ maxOut: formatNumber(model.maxTokens),
99
+ thinking: model.thinking ? getSupportedEfforts(model).join(",") : model.reasoning ? "yes" : "-",
100
+ images: model.input.includes("image") ? "yes" : "no",
101
+ })) satisfies ProviderRow[];
47
102
 
48
- const headers = {
49
- provider: "provider",
50
- model: "model",
51
- context: "context",
52
- maxOut: "max-out",
53
- thinking: "thinking",
54
- images: "images",
55
- };
56
-
57
- const widths = {
58
- provider: Math.max(headers.provider.length, ...rows.map(r => r.provider.length)),
59
- model: Math.max(headers.model.length, ...rows.map(r => r.model.length)),
60
- context: Math.max(headers.context.length, ...rows.map(r => r.context.length)),
61
- maxOut: Math.max(headers.maxOut.length, ...rows.map(r => r.maxOut.length)),
62
- thinking: Math.max(headers.thinking.length, ...rows.map(r => r.thinking.length)),
63
- images: Math.max(headers.images.length, ...rows.map(r => r.images.length)),
64
- };
65
-
66
- // Print header
67
- const headerLine = [
68
- headers.provider.padEnd(widths.provider),
69
- headers.model.padEnd(widths.model),
70
- headers.context.padEnd(widths.context),
71
- headers.maxOut.padEnd(widths.maxOut),
72
- headers.thinking.padEnd(widths.thinking),
73
- headers.images.padEnd(widths.images),
74
- ].join(" ");
75
- console.log(headerLine);
103
+ if (filteredCanonical.length > 0) {
104
+ writeLine("Canonical models");
105
+ renderTable(filteredCanonical, {
106
+ canonical: "canonical",
107
+ selected: "selected",
108
+ variants: "variants",
109
+ context: "context",
110
+ maxOut: "max-out",
111
+ });
112
+ if (providerRows.length > 0) {
113
+ writeLine();
114
+ }
115
+ }
76
116
 
77
- // Print rows
78
- for (const row of rows) {
79
- const line = [
80
- row.provider.padEnd(widths.provider),
81
- row.model.padEnd(widths.model),
82
- row.context.padEnd(widths.context),
83
- row.maxOut.padEnd(widths.maxOut),
84
- row.thinking.padEnd(widths.thinking),
85
- row.images.padEnd(widths.images),
86
- ].join(" ");
87
- console.log(line);
117
+ if (providerRows.length > 0) {
118
+ writeLine("Provider models");
119
+ renderTable(providerRows, {
120
+ provider: "provider",
121
+ model: "model",
122
+ context: "context",
123
+ maxOut: "max-out",
124
+ thinking: "thinking",
125
+ images: "images",
126
+ });
88
127
  }
89
128
  }
@@ -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);
@@ -1,7 +1,12 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Api, Model } from "@oh-my-pi/pi-ai";
3
3
  import { MODEL_ROLE_IDS } from "../config/model-registry";
4
- import { parseModelPattern, resolveModelRoleValue, resolveRoleSelection } from "../config/model-resolver";
4
+ import {
5
+ type ModelLookupRegistry,
6
+ parseModelPattern,
7
+ resolveModelRoleValue,
8
+ resolveRoleSelection,
9
+ } from "../config/model-resolver";
5
10
  import type { Settings } from "../config/settings";
6
11
  import MODEL_PRIO from "../priority.json" with { type: "json" };
7
12
 
@@ -11,19 +16,20 @@ export interface ResolvedCommitModel {
11
16
  thinkingLevel?: ThinkingLevel;
12
17
  }
13
18
 
19
+ type CommitModelRegistry = ModelLookupRegistry & {
20
+ getApiKey: (model: Model<Api>) => Promise<string | undefined>;
21
+ };
22
+
14
23
  export async function resolvePrimaryModel(
15
24
  override: string | undefined,
16
25
  settings: Settings,
17
- modelRegistry: {
18
- getAvailable: () => Model<Api>[];
19
- getApiKey: (model: Model<Api>) => Promise<string | undefined>;
20
- },
26
+ modelRegistry: CommitModelRegistry,
21
27
  ): Promise<ResolvedCommitModel> {
22
28
  const available = modelRegistry.getAvailable();
23
29
  const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
24
30
  const resolved = override
25
- ? resolveModelRoleValue(override, available, { settings, matchPreferences })
26
- : resolveRoleSelection(["commit", "smol", ...MODEL_ROLE_IDS], settings, available);
31
+ ? resolveModelRoleValue(override, available, { settings, matchPreferences, modelRegistry })
32
+ : resolveRoleSelection(["commit", "smol", ...MODEL_ROLE_IDS], settings, available, modelRegistry);
27
33
  const model = resolved?.model;
28
34
  if (!model) {
29
35
  throw new Error("No model available for commit generation");
@@ -37,15 +43,12 @@ export async function resolvePrimaryModel(
37
43
 
38
44
  export async function resolveSmolModel(
39
45
  settings: Settings,
40
- modelRegistry: {
41
- getAvailable: () => Model<Api>[];
42
- getApiKey: (model: Model<Api>) => Promise<string | undefined>;
43
- },
46
+ modelRegistry: CommitModelRegistry,
44
47
  fallbackModel: Model<Api>,
45
48
  fallbackApiKey: string,
46
49
  ): Promise<ResolvedCommitModel> {
47
50
  const available = modelRegistry.getAvailable();
48
- const resolvedSmol = resolveRoleSelection(["smol"], settings, available);
51
+ const resolvedSmol = resolveRoleSelection(["smol"], settings, available, modelRegistry);
49
52
  if (resolvedSmol?.model) {
50
53
  const apiKey = await modelRegistry.getApiKey(resolvedSmol.model);
51
54
  if (apiKey) return { model: resolvedSmol.model, apiKey, thinkingLevel: resolvedSmol.thinkingLevel };
@@ -53,7 +56,7 @@ export async function resolveSmolModel(
53
56
 
54
57
  const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
55
58
  for (const pattern of MODEL_PRIO.smol) {
56
- const candidate = parseModelPattern(pattern, available, matchPreferences).model;
59
+ const candidate = parseModelPattern(pattern, available, matchPreferences, { modelRegistry }).model;
57
60
  if (!candidate) continue;
58
61
  const apiKey = await modelRegistry.getApiKey(candidate);
59
62
  if (apiKey) return { model: candidate, apiKey };
@@ -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": {