@oh-my-pi/pi-coding-agent 14.3.0 → 14.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +84 -1
  2. package/package.json +7 -7
  3. package/src/autoresearch/prompt.md +1 -1
  4. package/src/commit/agentic/prompts/analyze-file.md +1 -1
  5. package/src/config/model-registry.ts +67 -15
  6. package/src/config/prompt-templates.ts +5 -5
  7. package/src/config/settings-schema.ts +4 -4
  8. package/src/cursor.ts +3 -8
  9. package/src/discovery/helpers.ts +3 -3
  10. package/src/edit/diff.ts +50 -47
  11. package/src/edit/index.ts +86 -57
  12. package/src/edit/line-hash.ts +735 -19
  13. package/src/edit/modes/apply-patch.ts +0 -9
  14. package/src/edit/modes/atom.ts +658 -0
  15. package/src/edit/modes/chunk.ts +14 -24
  16. package/src/edit/modes/hashline.ts +188 -136
  17. package/src/edit/modes/patch.ts +5 -9
  18. package/src/edit/modes/replace.ts +6 -11
  19. package/src/edit/renderer.ts +14 -10
  20. package/src/edit/streaming.ts +50 -16
  21. package/src/exec/bash-executor.ts +2 -4
  22. package/src/export/html/template.generated.ts +1 -1
  23. package/src/export/html/template.js +4 -12
  24. package/src/extensibility/custom-tools/types.ts +2 -0
  25. package/src/extensibility/custom-tools/wrapper.ts +2 -1
  26. package/src/internal-urls/docs-index.generated.ts +2 -2
  27. package/src/lsp/index.ts +1 -1
  28. package/src/mcp/render.ts +1 -8
  29. package/src/modes/components/assistant-message.ts +4 -0
  30. package/src/modes/components/diff.ts +23 -14
  31. package/src/modes/components/footer.ts +21 -16
  32. package/src/modes/components/settings-defs.ts +6 -1
  33. package/src/modes/components/todo-reminder.ts +1 -8
  34. package/src/modes/components/tool-execution.ts +1 -4
  35. package/src/modes/controllers/selector-controller.ts +1 -1
  36. package/src/modes/print-mode.ts +8 -0
  37. package/src/prompts/agents/librarian.md +1 -1
  38. package/src/prompts/agents/reviewer.md +4 -4
  39. package/src/prompts/ci-green-request.md +1 -1
  40. package/src/prompts/review-request.md +1 -1
  41. package/src/prompts/system/subagent-system-prompt.md +3 -3
  42. package/src/prompts/system/subagent-yield-reminder.md +11 -0
  43. package/src/prompts/system/system-prompt.md +3 -0
  44. package/src/prompts/tools/ask.md +3 -2
  45. package/src/prompts/tools/ast-edit.md +15 -19
  46. package/src/prompts/tools/ast-grep.md +18 -24
  47. package/src/prompts/tools/atom.md +96 -0
  48. package/src/prompts/tools/chunk-edit.md +37 -161
  49. package/src/prompts/tools/debug.md +4 -5
  50. package/src/prompts/tools/exit-plan-mode.md +4 -5
  51. package/src/prompts/tools/find.md +4 -8
  52. package/src/prompts/tools/github.md +18 -0
  53. package/src/prompts/tools/grep.md +4 -5
  54. package/src/prompts/tools/hashline.md +22 -89
  55. package/src/prompts/tools/{gemini-image.md → image-gen.md} +1 -1
  56. package/src/prompts/tools/inspect-image.md +6 -6
  57. package/src/prompts/tools/lsp.md +1 -1
  58. package/src/prompts/tools/patch.md +12 -19
  59. package/src/prompts/tools/python.md +3 -2
  60. package/src/prompts/tools/read-chunk.md +2 -3
  61. package/src/prompts/tools/read.md +2 -2
  62. package/src/prompts/tools/ssh.md +8 -17
  63. package/src/prompts/tools/todo-write.md +54 -41
  64. package/src/sdk.ts +14 -9
  65. package/src/session/agent-session.ts +25 -2
  66. package/src/task/executor.ts +43 -48
  67. package/src/task/render.ts +11 -13
  68. package/src/tools/ask.ts +7 -7
  69. package/src/tools/ast-edit.ts +45 -41
  70. package/src/tools/ast-grep.ts +77 -85
  71. package/src/tools/bash.ts +8 -9
  72. package/src/tools/browser.ts +32 -30
  73. package/src/tools/calculator.ts +4 -4
  74. package/src/tools/cancel-job.ts +1 -1
  75. package/src/tools/checkpoint.ts +2 -2
  76. package/src/tools/debug.ts +41 -37
  77. package/src/tools/exit-plan-mode.ts +1 -1
  78. package/src/tools/find.ts +4 -4
  79. package/src/tools/gh-renderer.ts +12 -4
  80. package/src/tools/gh.ts +509 -697
  81. package/src/tools/grep.ts +115 -130
  82. package/src/tools/{gemini-image.ts → image-gen.ts} +459 -60
  83. package/src/tools/index.ts +14 -32
  84. package/src/tools/inspect-image.ts +3 -3
  85. package/src/tools/json-tree.ts +114 -114
  86. package/src/tools/match-line-format.ts +9 -8
  87. package/src/tools/notebook.ts +8 -7
  88. package/src/tools/poll-tool.ts +2 -1
  89. package/src/tools/python.ts +9 -23
  90. package/src/tools/read.ts +32 -21
  91. package/src/tools/render-mermaid.ts +1 -1
  92. package/src/tools/render-utils.ts +18 -0
  93. package/src/tools/renderers.ts +2 -2
  94. package/src/tools/report-tool-issue.ts +3 -2
  95. package/src/tools/resolve.ts +1 -1
  96. package/src/tools/review.ts +12 -10
  97. package/src/tools/search-tool-bm25.ts +2 -4
  98. package/src/tools/ssh.ts +4 -4
  99. package/src/tools/todo-write.ts +172 -147
  100. package/src/tools/vim.ts +14 -15
  101. package/src/tools/write.ts +4 -4
  102. package/src/tools/{submit-result.ts → yield.ts} +11 -13
  103. package/src/utils/edit-mode.ts +2 -1
  104. package/src/utils/file-display-mode.ts +10 -5
  105. package/src/utils/git.ts +9 -5
  106. package/src/utils/shell-snapshot.ts +2 -3
  107. package/src/vim/render.ts +4 -4
  108. package/src/prompts/system/subagent-submit-reminder.md +0 -11
  109. package/src/prompts/tools/gh-issue-view.md +0 -11
  110. package/src/prompts/tools/gh-pr-checkout.md +0 -12
  111. package/src/prompts/tools/gh-pr-diff.md +0 -12
  112. package/src/prompts/tools/gh-pr-push.md +0 -12
  113. package/src/prompts/tools/gh-pr-view.md +0 -11
  114. package/src/prompts/tools/gh-repo-view.md +0 -11
  115. package/src/prompts/tools/gh-run-watch.md +0 -12
  116. package/src/prompts/tools/gh-search-issues.md +0 -11
  117. package/src/prompts/tools/gh-search-prs.md +0 -11
package/CHANGELOG.md CHANGED
@@ -1,6 +1,88 @@
1
1
  # Changelog
2
2
 
3
3
  ## [Unreleased]
4
+ ### Breaking Changes
5
+
6
+ - Replaced the legacy `gh_repo_view`, `gh_issue_view`, `gh_pr_view`, `gh_pr_diff`, `gh_pr_checkout`, `gh_pr_push`, `gh_run_watch`, `gh_search_issues`, and `gh_search_prs` tool names with only `github`, which requires updating existing callers that invoked the old `gh_*` tools
7
+
8
+ ### Added
9
+
10
+ - Added the unified `github` tool with op-based dispatch for repository, issue, pull request, search, checkout, push, and Actions watch workflows
11
+ - Added `op` routing so callers can select `repo_view`, `issue_view`, `pr_view`, `pr_diff`, `pr_checkout`, `pr_push`, `search_issues`, `search_prs`, or `run_watch` through a single tool entry point
12
+
13
+ ### Changed
14
+
15
+ - Updated GitHub CLI render output to show `GitHub <op>` for tool calls dispatched through `github` operations
16
+
17
+ ## [14.4.0] - 2026-04-26
18
+
19
+ ### Breaking Changes
20
+
21
+ - Removed multi-pattern array input from `ast_grep` by changing `pat` to a single pattern string, so call sites using `pat: [...]` must be updated to send one query per invocation
22
+ - Removed `lang`, `glob`, and `sel` options from `ast_edit` and `ast_grep`, and moved those behaviors into the required `path` argument
23
+ - Required `path` for `ast_edit` and `ast_grep`, so invocations that relied on implicit repo-root searching are no longer valid
24
+ - Changed `todo_write` from multi-field verb payloads to an ordered array of flat operations, while retaining `replace` for harness bootstrap compatibility
25
+ - Renamed atom edit operations from `before` and `after` to `pre` and `post`, so existing `atom` payloads using the old operation keys must be updated
26
+ - Changed the hashline anchor format from `LINE#ID:content` to `LINEID:content` (no `#` separator, colon between anchor and content, no padding on line numbers); expanded the bigram alphabet from 40 hand-picked English bigrams to the full 647 single-token 2-letter bigrams — invalidates every previously captured `LINE#ID` reference
27
+ - Renamed the subagent completion contract from `submit_result` to `yield`, so subagent sessions must now finish with the `yield` tool and the `requireYieldTool` option; `submit_result`/`requireSubmitResultTool` and old completion calls are no longer recognized
28
+ - Changed the hashline and chunk anchor ID format from the prior hex-like tokens to two-letter BPE bigrams (for example `#th`), which invalidates previously captured `LINE#ID`/chunk selectors and requires re-reading to refresh anchors
29
+
30
+ ### Added
31
+
32
+ - Added inline file overrides in atom locators (`loc: "a.ts:160sr"`) so cross-file edits can be written without a separate per-entry `path` field
33
+ - Added `openai` to the `providers.image` options, allowing image generation to be explicitly routed through the active GPT Responses/Codex model
34
+ - Added `between` atom edit operation to replace only the lines between two surviving anchors while preserving the boundary anchors
35
+ - Added conflict detection for `between` atom edits to require non-overlapping regions and forbid edits targeting lines strictly inside those regions
36
+ - Added `atom` edit mode to `edit` with single-anchor operations (`set`, `before`, `after`, `del`, `sub`, `ins`) for hashline-anchored line edits
37
+ - Added support for request-level `path` defaults in patch, replace, and chunk edits so shared file paths no longer need to be repeated in every entry
38
+
39
+ ### Changed
40
+
41
+ - Updated `atom` and `hashline` edit anchor validation to auto-rebase a stale anchor within ±2 lines when the same hash matches a unique nearby line, continuing the edit with a warning instead of immediate failure
42
+ - Changed bash command output labels from `[full result: artifact://…]` to `[raw output: artifact://…]` for artifact references produced from large command output
43
+ - Changed `todo_write` `done`, `rm`, and `drop` operations to target all tasks when neither `task` nor `phase` is provided, and made `append` create the target phase automatically when missing
44
+ - Updated `ast_edit` and `ast_grep` to pass file-selection intent through `path` (including inline globs and comma/space-separated path lists) instead of separate `glob` filters
45
+ - Changed `ast_grep` pagination API from `offset` to `skip`
46
+ - Flattened `todo_write` operation arguments to `{ op, task?, phase?, items? }[]` and removed task details from the persisted todo shape
47
+ - Changed `grep` truncation output to report `Result limit reached; narrow path.` and label match/result caps as `first N`
48
+ - Changed JSON tree output to truncate inline argument pairs by available width and add an ellipsis when values no longer fit in the display
49
+ - Changed JSON tree rendering to hide harness-internal `intent` and `__partialJson` fields from top-level tool output
50
+ - Simplified the `grep` tool schema by requiring `path`, folding glob and type filtering into path globs, auto-detecting multiline patterns, removing model-controlled context and limit options, and renaming result skipping to `skip`.
51
+ - Changed atom edit request format to use a shared `loc` selector, including range (`"160sr-9ab"`) and boundary (`"^"`, `"$"`) forms instead of per-operation anchor fields
52
+ - Changed atom edit payload fields so `set`, `pre`, and `post` now require line-array values and `sub` now takes a `[find, replace]` tuple, with boundary deletion now expressed as `set: []`
53
+ - Changed edit diff wrapping to preserve the active line-prefix separator (`|` or `│`) while keeping continuation lines aligned by line-number width
54
+ - Changed Vim focus and viewport rendering to align cursor/selection markers and line numbers in a single gutter format
55
+ - Changed auto image provider selection for `providers.image=auto` to try active GPT image generation before Antigravity, OpenRouter, and Gemini
56
+ - Updated atom and hashline anchor validation to require the full `line+suffix` anchor format and report missing-line-number errors more clearly, including guidance when only a 2-letter suffix is provided
57
+ - Changed read, grep, and ast-edit line-prefixed output to drop fixed-width line number padding, so anchors render in natural width without leading spaces
58
+ - Updated terminal diff rendering to use a continuous `│` gutter and hide repeated line numbers on adjacent diff lines
59
+ - Updated subagent reminders, prompts, and rendered subagent output to reference `yield` completion and report missing/final results from `yield` tool data
60
+ - Updated the `edit` workflow to treat `atom` mode like hashline mode for read output, so hashline anchors are shown when `atom` is selected
61
+ - Adjusted patch/replace/chunk tooling to accept optional entry paths and to apply a top-level path default
62
+ - Updated hashline/chunk selector parsing to the new stable bigram token set used for checksums
63
+ - Renamed the image generation implementation module to `image-gen` and routed active GPT Responses/Codex models through OpenAI's hosted `image_generation` tool with WebP output
64
+
65
+ ### Removed
66
+
67
+ - Removed line-range support from `atom` mode selectors, including `loc` values like `160sr-170ab`, so edits must target a single anchor (`160sr`, `^`, or `$`) per entry
68
+ - Removed the atom `del` verb and now require anchored-line deletion to be requested with `set: []`
69
+ - Removed `todo_write` task details and the `add_notes` operation
70
+
71
+ ### Fixed
72
+
73
+ - Improved no-op edit diagnostics for `atom` and `hashline` operations so edits that leave content unchanged now fail with contextual details (edit index, locator, and reason), including guidance for `replace_range` no-op cases
74
+ - Wrapped `todo_write` operations in an `ops` object so Codex/OpenAI function schemas always use a JSON Schema object.
75
+ - Fixed JSON tree rendering for tool arguments by excluding injected internal keys from displayed root records
76
+ - Printed assistant `errorMessage` text in print mode output to stderr so message-level errors are visible during non-interactive runs
77
+ - Displayed assistant `errorMessage` text in the assistant message component for completed tool responses with non-terminal stop reasons
78
+ - Fixed atom input handling to ignore null optional verb fields so entries with `pre`, `set`, `post`, or `sub` set to `null` remain valid
79
+ - Fixed status-line Git branch rendering to degrade gracefully when the process hits `ENFILE`/`EMFILE` while reading optional Git refs
80
+ - Changed hashline mismatch failure output to show a clean numbered context block with numbered gutter and full-anchor alignment guidance when edits are rejected after the file changed
81
+ - Fixed `atom` mode to apply multiple edits on the same anchor line without index-shift artifacts, so mixed operations like `before`, `after`, `set`, `sub`, `ins`, and `del` now resolve consistently
82
+ - Fixed `atom` mode `append_file` insertion to preserve a file’s trailing newline sentinel when appending content
83
+ - Fixed `read` output for raw archive entries so hashline anchors, line numbers, and chunked formatting are not injected into raw content
84
+ - Fixed hashline parsing so lines like `# Note:` or `# TODO:` are no longer misinterpreted and stripped as hashline prefixes
85
+ - Adjusted patch and replace validation to report a clear missing-path error when neither an entry path nor a top-level path is provided
4
86
 
5
87
  ## [14.3.0] - 2026-04-25
6
88
 
@@ -60,6 +142,7 @@
60
142
  - Fixed Mermaid fenced markdown rendering in assistant messages on terminals without image protocol support ([#650](https://github.com/can1357/oh-my-pi/issues/650))
61
143
  - Fixed chunk edit path parsing so plan-mode edits to section-addressed `local://PLAN.md:<selector>` paths are classified as writes to the plan file
62
144
  - Fixed SQLite `read` helper queries to reject `where=` clauses with SQL control syntax that could override the structured selector's pagination; raw SQL remains available through `q=SELECT ...`
145
+ - Fixed `models` provider transport overrides so `headers`-only entries apply without requiring `baseUrl`, including runtime `registerProvider()` overrides that now persist across `refresh()` / `refreshProvider()`, preserve existing `baseUrl` on subsequent headers-only updates, clear stale transport overrides when a provider is re-registered under a different extension source, and keep runtime transport headers authoritative when `modelOverrides` set overlapping header keys
63
146
 
64
147
  ## [14.2.0] - 2026-04-23
65
148
 
@@ -7186,4 +7269,4 @@ Initial public release.
7186
7269
  - Git branch display in footer
7187
7270
  - Message queueing during streaming responses
7188
7271
  - OAuth integration for Gmail and Google Calendar access
7189
- - HTML export with syntax highlighting and collapsible sections
7272
+ - HTML export with syntax highlighting and collapsible sections
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.3.0",
4
+ "version": "14.4.0",
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,12 +46,12 @@
46
46
  "dependencies": {
47
47
  "@agentclientprotocol/sdk": "0.20.0",
48
48
  "@mozilla/readability": "^0.6.0",
49
- "@oh-my-pi/omp-stats": "14.3.0",
50
- "@oh-my-pi/pi-agent-core": "14.3.0",
51
- "@oh-my-pi/pi-ai": "14.3.0",
52
- "@oh-my-pi/pi-natives": "14.3.0",
53
- "@oh-my-pi/pi-tui": "14.3.0",
54
- "@oh-my-pi/pi-utils": "14.3.0",
49
+ "@oh-my-pi/omp-stats": "14.4.0",
50
+ "@oh-my-pi/pi-agent-core": "14.4.0",
51
+ "@oh-my-pi/pi-ai": "14.4.0",
52
+ "@oh-my-pi/pi-natives": "14.4.0",
53
+ "@oh-my-pi/pi-tui": "14.4.0",
54
+ "@oh-my-pi/pi-utils": "14.4.0",
55
55
  "@sinclair/typebox": "^0.34.49",
56
56
  "@xterm/headless": "^6.0.0",
57
57
  "ajv": "^8.20.0",
@@ -1,4 +1,4 @@
1
- {{{base_system_prompt}}}
1
+ {{base_system_prompt}}
2
2
 
3
3
  ## Autoresearch Mode
4
4
 
@@ -19,4 +19,4 @@ Return concise JSON object with:
19
19
  Consider how file's changes relate to above files.
20
20
  {{/if}}
21
21
 
22
- Call submit_result tool with JSON payload.
22
+ Call yield tool with JSON payload.
@@ -300,6 +300,7 @@ interface ProviderValidationModel {
300
300
 
301
301
  interface ProviderValidationConfig {
302
302
  baseUrl?: string;
303
+ headers?: Record<string, string>;
303
304
  apiKey?: string;
304
305
  api?: Api;
305
306
  auth?: ProviderAuthMode;
@@ -321,9 +322,9 @@ function validateProviderConfiguration(
321
322
  if (models.length === 0) {
322
323
  if (mode === "models-config") {
323
324
  const hasModelOverrides = config.modelOverrides && Object.keys(config.modelOverrides).length > 0;
324
- if (!config.baseUrl && !config.compat && !hasModelOverrides && !config.discovery) {
325
+ if (!config.baseUrl && !config.headers && !config.compat && !hasModelOverrides && !config.discovery) {
325
326
  throw new Error(
326
- `Provider ${providerName}: must specify "baseUrl", "compat", "modelOverrides", "discovery", or "models"`,
327
+ `Provider ${providerName}: must specify "baseUrl", "headers", "compat", "modelOverrides", "discovery", or "models"`,
327
328
  );
328
329
  }
329
330
  }
@@ -378,6 +379,7 @@ export const ModelsConfigFile = new ConfigFile<ModelsConfig>("models", ModelsCon
378
379
  providerName,
379
380
  {
380
381
  baseUrl: providerConfig.baseUrl,
382
+ headers: providerConfig.headers,
381
383
  apiKey: providerConfig.apiKey,
382
384
  api: providerConfig.api as Api | undefined,
383
385
  auth: (providerConfig.auth ?? "apiKey") as ProviderAuthMode,
@@ -770,6 +772,7 @@ export class ModelRegistry {
770
772
  // models registered by extensions survive the model selector's offline reload.
771
773
  #runtimeModelOverlays: CustomModelOverlay[] = [];
772
774
  #runtimeProviderApiKeys: Map<string, string> = new Map();
775
+ #runtimeProviderOverrides: Map<string, ProviderOverride> = new Map();
773
776
  #runtimeProvidersBySource: Map<string, Set<string>> = new Map();
774
777
  #runtimeProviderSourceByName: Map<string, string> = new Map();
775
778
 
@@ -883,8 +886,8 @@ export class ModelRegistry {
883
886
  const withConfigModels = this.#mergeCustomModels(resolvedDefaults, this.#customModelOverlays);
884
887
  // Merge runtime extension models so they survive refresh() cycles
885
888
  const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
886
-
887
- this.#models = this.#applyModelOverrides(combined, this.#modelOverrides);
889
+ const withModelOverrides = this.#applyModelOverrides(combined, this.#modelOverrides);
890
+ this.#models = this.#applyRuntimeProviderOverrides(withModelOverrides);
888
891
  this.#rebuildCanonicalIndex();
889
892
  }
890
893
 
@@ -1175,7 +1178,8 @@ export class ModelRegistry {
1175
1178
  const withConfigModels = this.#mergeCustomModels(resolved, this.#customModelOverlays);
1176
1179
  // Merge runtime extension models so they survive online discovery completion
1177
1180
  const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
1178
- this.#models = this.#applyModelOverrides(combined, this.#modelOverrides);
1181
+ const withModelOverrides = this.#applyModelOverrides(combined, this.#modelOverrides);
1182
+ this.#models = this.#applyRuntimeProviderOverrides(withModelOverrides);
1179
1183
  this.#rebuildCanonicalIndex();
1180
1184
  }
1181
1185
 
@@ -1657,6 +1661,32 @@ export class ModelRegistry {
1657
1661
  });
1658
1662
  }
1659
1663
 
1664
+ #mergeProviderOverride(baseOverride: ProviderOverride | undefined, override: ProviderOverride): ProviderOverride {
1665
+ return {
1666
+ baseUrl: override.baseUrl ?? baseOverride?.baseUrl,
1667
+ apiKey: override.apiKey ?? baseOverride?.apiKey,
1668
+ headers: override.headers ? { ...(baseOverride?.headers ?? {}), ...override.headers } : baseOverride?.headers,
1669
+ compat: override.compat ? mergeCompat(baseOverride?.compat, override.compat) : baseOverride?.compat,
1670
+ };
1671
+ }
1672
+ #applyProviderTransportOverride<T extends { baseUrl?: string; headers?: Record<string, string> }>(
1673
+ entry: T,
1674
+ override: Pick<ProviderOverride, "baseUrl" | "headers">,
1675
+ ): T {
1676
+ return {
1677
+ ...entry,
1678
+ baseUrl: override.baseUrl ?? entry.baseUrl,
1679
+ headers: override.headers ? { ...entry.headers, ...override.headers } : entry.headers,
1680
+ };
1681
+ }
1682
+ #applyRuntimeProviderOverrides(models: Model<Api>[]): Model<Api>[] {
1683
+ if (this.#runtimeProviderOverrides.size === 0) return models;
1684
+ return models.map(model => {
1685
+ const override = this.#runtimeProviderOverrides.get(model.provider);
1686
+ if (!override) return model;
1687
+ return this.#applyProviderTransportOverride(model, override);
1688
+ });
1689
+ }
1660
1690
  #applyModelOverrides(models: Model<Api>[], overrides: Map<string, Map<string, ModelOverride>>): Model<Api>[] {
1661
1691
  if (overrides.size === 0) return models;
1662
1692
  return models.map(model => {
@@ -1916,6 +1946,12 @@ export class ModelRegistry {
1916
1946
  return this.authStorage.hasOAuth(model.provider);
1917
1947
  }
1918
1948
 
1949
+ #clearRuntimeProviderState(providerName: string): void {
1950
+ this.#runtimeProviderApiKeys.delete(providerName);
1951
+ this.#runtimeProviderOverrides.delete(providerName);
1952
+ this.#runtimeModelOverlays = this.#runtimeModelOverlays.filter(overlay => overlay.provider !== providerName);
1953
+ }
1954
+
1919
1955
  /**
1920
1956
  * Remove custom API/OAuth registrations for a specific extension source.
1921
1957
  */
@@ -1932,8 +1968,7 @@ export class ModelRegistry {
1932
1968
  continue;
1933
1969
  }
1934
1970
  this.#runtimeProviderSourceByName.delete(providerName);
1935
- this.#runtimeProviderApiKeys.delete(providerName);
1936
- this.#runtimeModelOverlays = this.#runtimeModelOverlays.filter(overlay => overlay.provider !== providerName);
1971
+ this.#clearRuntimeProviderState(providerName);
1937
1972
  }
1938
1973
  this.#reloadStaticModels();
1939
1974
  this.#rebuildCanonicalIndex();
@@ -1970,6 +2005,7 @@ export class ModelRegistry {
1970
2005
  providerName,
1971
2006
  {
1972
2007
  baseUrl: config.baseUrl,
2008
+ headers: config.headers,
1973
2009
  apiKey: config.apiKey,
1974
2010
  api: config.api,
1975
2011
  oauthConfigured: Boolean(config.oauth),
@@ -1993,6 +2029,7 @@ export class ModelRegistry {
1993
2029
  });
1994
2030
  }
1995
2031
 
2032
+ let sourceHandoff = false;
1996
2033
  if (sourceId) {
1997
2034
  this.#registeredProviderSources.add(sourceId);
1998
2035
  const previousSourceId = this.#runtimeProviderSourceByName.get(providerName);
@@ -2002,12 +2039,18 @@ export class ModelRegistry {
2002
2039
  if (previousProviders && previousProviders.size === 0) {
2003
2040
  this.#runtimeProvidersBySource.delete(previousSourceId);
2004
2041
  }
2042
+ this.#clearRuntimeProviderState(providerName);
2043
+ sourceHandoff = true;
2005
2044
  }
2006
2045
  const sourceProviders = this.#runtimeProvidersBySource.get(sourceId) ?? new Set<string>();
2007
2046
  sourceProviders.add(providerName);
2008
2047
  this.#runtimeProvidersBySource.set(sourceId, sourceProviders);
2009
2048
  this.#runtimeProviderSourceByName.set(providerName, sourceId);
2010
2049
  }
2050
+ if (sourceHandoff) {
2051
+ this.#reloadStaticModels();
2052
+ }
2053
+
2011
2054
  if (config.apiKey) {
2012
2055
  this.#customProviderApiKeys.set(providerName, config.apiKey);
2013
2056
  // Persist runtime API keys so they survive #reloadStaticModels() cycles
@@ -2042,29 +2085,38 @@ export class ModelRegistry {
2042
2085
  for (const overlay of newOverlays) {
2043
2086
  nextModels.push(finalizeCustomModel(overlay, { useDefaults: true }));
2044
2087
  }
2088
+ const runtimeTransportOverride = this.#runtimeProviderOverrides.get(providerName);
2089
+ const withRuntimeTransportOverride = runtimeTransportOverride
2090
+ ? nextModels.map(model => {
2091
+ if (model.provider !== providerName) return model;
2092
+ return this.#applyProviderTransportOverride(model, runtimeTransportOverride);
2093
+ })
2094
+ : nextModels;
2045
2095
 
2046
2096
  if (config.oauth?.modifyModels) {
2047
2097
  const credential = this.authStorage.getOAuthCredential(providerName);
2048
2098
  if (credential) {
2049
- this.#models = config.oauth.modifyModels(nextModels, credential);
2099
+ this.#models = config.oauth.modifyModels(withRuntimeTransportOverride, credential);
2050
2100
  this.#rebuildCanonicalIndex();
2051
2101
  return;
2052
2102
  }
2053
2103
  }
2054
2104
 
2055
- this.#models = nextModels;
2105
+ this.#models = withRuntimeTransportOverride;
2056
2106
  this.#rebuildCanonicalIndex();
2057
2107
  return;
2058
2108
  }
2059
2109
 
2060
- if (config.baseUrl) {
2110
+ if (config.baseUrl || config.headers) {
2111
+ const transportOverride = { baseUrl: config.baseUrl, headers: config.headers };
2112
+ const nextRuntimeOverride = this.#mergeProviderOverride(
2113
+ this.#runtimeProviderOverrides.get(providerName),
2114
+ transportOverride,
2115
+ );
2116
+ this.#runtimeProviderOverrides.set(providerName, nextRuntimeOverride);
2061
2117
  this.#models = this.#models.map(m => {
2062
2118
  if (m.provider !== providerName) return m;
2063
- return {
2064
- ...m,
2065
- baseUrl: config.baseUrl ?? m.baseUrl,
2066
- headers: config.headers ? { ...m.headers, ...config.headers } : m.headers,
2067
- };
2119
+ return this.#applyProviderTransportOverride(m, transportOverride);
2068
2120
  });
2069
2121
  this.#rebuildCanonicalIndex();
2070
2122
  }
@@ -9,7 +9,7 @@ import {
9
9
  parseFrontmatter,
10
10
  prompt,
11
11
  } from "@oh-my-pi/pi-utils";
12
- import { computeLineHash } from "../edit/line-hash";
12
+ import { computeLineHash, HASHLINE_CONTENT_SEPARATOR } from "../edit/line-hash";
13
13
  import { jtdToTypeScript } from "../tools/jtd-to-typescript";
14
14
  import { parseCommandArgs, substituteArgs } from "../utils/command-args";
15
15
 
@@ -40,13 +40,13 @@ function formatHashlineRef(lineNum: unknown, content: unknown): { num: number; t
40
40
  const num = typeof lineNum === "number" ? lineNum : Number.parseInt(String(lineNum), 10);
41
41
  const raw = typeof content === "string" ? content : String(content ?? "");
42
42
  const text = raw.replace(/\\t/g, "\t").replace(/\\n/g, "\n").replace(/\\r/g, "\r");
43
- const ref = `${num}#${computeLineHash(num, text)}`;
43
+ const ref = `${num}${computeLineHash(num, text)}`;
44
44
  return { num, text, ref };
45
45
  }
46
46
 
47
47
  /**
48
48
  * {{href lineNum "content"}} — compute a real hashline ref for prompt examples.
49
- * Returns `"lineNum#hash"` using the actual hash algorithm.
49
+ * Returns `"lineNumBIGRAM"` (e.g., `"42nd"`) using the actual hash algorithm.
50
50
  */
51
51
  prompt.registerHelper("href", (lineNum: unknown, content: unknown): string => {
52
52
  const { ref } = formatHashlineRef(lineNum, content);
@@ -55,11 +55,11 @@ prompt.registerHelper("href", (lineNum: unknown, content: unknown): string => {
55
55
 
56
56
  /**
57
57
  * {{hline lineNum "content"}} — format a full read-style line with prefix.
58
- * Returns `"lineNum#hash:content"`.
58
+ * Returns `"lineNumBIGRAM:content"` (colon between anchor and content).
59
59
  */
60
60
  prompt.registerHelper("hline", (lineNum: unknown, content: unknown): string => {
61
61
  const { ref, text } = formatHashlineRef(lineNum, content);
62
- return `${ref}:${text}`;
62
+ return `${ref}${HASHLINE_CONTENT_SEPARATOR}${text}`;
63
63
  });
64
64
 
65
65
  /**
@@ -955,7 +955,7 @@ export const SETTINGS_SCHEMA = {
955
955
  // Edit tool
956
956
  "edit.mode": {
957
957
  type: "enum",
958
- values: ["replace", "patch", "hashline", "chunk", "vim", "apply_patch"] as const,
958
+ values: ["replace", "patch", "hashline", "chunk", "vim", "apply_patch", "atom"] as const,
959
959
  default: "hashline",
960
960
  ui: {
961
961
  tab: "editing",
@@ -1021,7 +1021,7 @@ export const SETTINGS_SCHEMA = {
1021
1021
  ui: {
1022
1022
  tab: "editing",
1023
1023
  label: "Hash Lines",
1024
- description: "Include line hashes in read output for hashline edit mode (LINE#ID:content)",
1024
+ description: "Include line hashes in read output for hashline edit mode (LINE+ID\\tcontent)",
1025
1025
  },
1026
1026
  },
1027
1027
 
@@ -1349,7 +1349,7 @@ export const SETTINGS_SCHEMA = {
1349
1349
  tab: "tools",
1350
1350
  label: "GitHub CLI",
1351
1351
  description:
1352
- "Enable gh_* tools for GitHub repository, issue, pull request, diff, search, checkout, and PR push workflows",
1352
+ "Enable the github tool (op-based dispatch for repository, issue, pull request, diff, search, checkout, push, and Actions watch workflows)",
1353
1353
  },
1354
1354
  },
1355
1355
 
@@ -1702,7 +1702,7 @@ export const SETTINGS_SCHEMA = {
1702
1702
  },
1703
1703
  "providers.image": {
1704
1704
  type: "enum",
1705
- values: ["auto", "gemini", "openrouter"] as const,
1705
+ values: ["auto", "openai", "gemini", "openrouter"] as const,
1706
1706
  default: "auto",
1707
1707
  ui: {
1708
1708
  tab: "providers",
package/src/cursor.ts CHANGED
@@ -177,16 +177,11 @@ export class CursorExecHandlers implements ICursorExecHandlers {
177
177
 
178
178
  async grep(args: Parameters<NonNullable<ICursorExecHandlers["grep"]>>[0]) {
179
179
  const toolCallId = decodeToolCallId(args.toolCallId);
180
+ const grepPath = args.glob ? `${args.path || "."}/${args.glob}` : args.path || ".";
180
181
  const toolResultMessage = await executeTool(this.options, "grep", toolCallId, {
181
182
  pattern: args.pattern,
182
- path: args.path || undefined,
183
- glob: args.glob || undefined,
184
- mode: args.outputMode || undefined,
185
- context: args.context ?? args.contextBefore ?? args.contextAfter ?? undefined,
186
- ignore_case: args.caseInsensitive || undefined,
187
- type: args.type || undefined,
188
- limit: args.headLimit ?? undefined,
189
- multiline: args.multiline || undefined,
183
+ path: grepPath,
184
+ i: args.caseInsensitive || undefined,
190
185
  });
191
186
  return toolResultMessage;
192
187
  }
@@ -221,9 +221,9 @@ export function parseAgentFields(frontmatter: Record<string, unknown>): ParsedAg
221
221
 
222
222
  let tools = parseArrayOrCSV(frontmatter.tools)?.map(tool => tool.toLowerCase());
223
223
 
224
- // Subagents with explicit tool lists always need submit_result
225
- if (tools && !tools.includes("submit_result")) {
226
- tools = [...tools, "submit_result"];
224
+ // Subagents with explicit tool lists always need yield
225
+ if (tools && !tools.includes("yield")) {
226
+ tools = [...tools, "yield"];
227
227
  }
228
228
 
229
229
  // Parse spawns field (array, "*", or CSV)
package/src/edit/diff.ts CHANGED
@@ -50,17 +50,8 @@ export class ApplyPatchError extends Error {
50
50
  // Diff String Generation
51
51
  // ═══════════════════════════════════════════════════════════════════════════
52
52
 
53
- function countContentLines(content: string): number {
54
- const lines = content.split("\n");
55
- if (lines.length > 1 && lines[lines.length - 1] === "") {
56
- lines.pop();
57
- }
58
- return Math.max(1, lines.length);
59
- }
60
-
61
- function formatNumberedDiffLine(prefix: "+" | "-" | " ", lineNum: number, width: number, content: string): string {
62
- const padded = String(lineNum).padStart(width, " ");
63
- return `${prefix}${padded}|${content}`;
53
+ function formatNumberedDiffLine(prefix: "+" | "-" | " ", lineNum: number, content: string): string {
54
+ return `${prefix}${lineNum}|${content}`;
64
55
  }
65
56
 
66
57
  /**
@@ -71,9 +62,6 @@ export function generateDiffString(oldContent: string, newContent: string, conte
71
62
  const parts = Diff.diffLines(oldContent, newContent);
72
63
  const output: string[] = [];
73
64
 
74
- const maxLineNum = Math.max(countContentLines(oldContent), countContentLines(newContent));
75
- const lineNumWidth = String(maxLineNum).length;
76
-
77
65
  let oldLineNum = 1;
78
66
  let newLineNum = 1;
79
67
  let lastWasChange = false;
@@ -95,10 +83,10 @@ export function generateDiffString(oldContent: string, newContent: string, conte
95
83
  // Show the change
96
84
  for (const line of raw) {
97
85
  if (part.added) {
98
- output.push(formatNumberedDiffLine("+", newLineNum, lineNumWidth, line));
86
+ output.push(formatNumberedDiffLine("+", newLineNum, line));
99
87
  newLineNum++;
100
88
  } else {
101
- output.push(formatNumberedDiffLine("-", oldLineNum, lineNumWidth, line));
89
+ output.push(formatNumberedDiffLine("-", oldLineNum, line));
102
90
  oldLineNum++;
103
91
  }
104
92
  }
@@ -108,40 +96,57 @@ export function generateDiffString(oldContent: string, newContent: string, conte
108
96
  const nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);
109
97
 
110
98
  if (lastWasChange || nextPartIsChange) {
111
- let linesToShow = raw;
112
- let skipStart = 0;
113
- let skipEnd = 0;
114
-
115
- if (!lastWasChange) {
116
- // Show only last N lines as leading context
117
- skipStart = Math.max(0, raw.length - contextLines);
118
- linesToShow = raw.slice(skipStart);
119
- }
120
-
121
- if (!nextPartIsChange && linesToShow.length > contextLines) {
122
- // Show only first N lines as trailing context
123
- skipEnd = linesToShow.length - contextLines;
124
- linesToShow = linesToShow.slice(0, contextLines);
99
+ const contextLimit = Math.max(0, contextLines);
100
+ let leadingSkip = 0;
101
+ let middleSkip = 0;
102
+ let trailingSkip = 0;
103
+ let linesToShow: string[];
104
+
105
+ if (lastWasChange && nextPartIsChange) {
106
+ if (raw.length > contextLimit * 2) {
107
+ const leadingContext = raw.slice(0, contextLimit);
108
+ const trailingContext = raw.slice(raw.length - contextLimit);
109
+ middleSkip = raw.length - leadingContext.length - trailingContext.length;
110
+ linesToShow = [...leadingContext, ...trailingContext];
111
+ } else {
112
+ linesToShow = raw;
113
+ }
114
+ } else if (nextPartIsChange) {
115
+ leadingSkip = Math.max(0, raw.length - contextLimit);
116
+ linesToShow = raw.slice(leadingSkip);
117
+ } else {
118
+ trailingSkip = Math.max(0, raw.length - contextLimit);
119
+ linesToShow = raw.slice(0, contextLimit);
125
120
  }
126
121
 
127
- // Add ellipsis if we skipped lines at start
128
- if (skipStart > 0) {
129
- output.push(formatNumberedDiffLine(" ", oldLineNum, lineNumWidth, "..."));
130
- oldLineNum += skipStart;
131
- newLineNum += skipStart;
122
+ if (leadingSkip > 0) {
123
+ output.push(formatNumberedDiffLine(" ", oldLineNum, "..."));
124
+ oldLineNum += leadingSkip;
125
+ newLineNum += leadingSkip;
132
126
  }
133
127
 
134
- for (const line of linesToShow) {
135
- output.push(formatNumberedDiffLine(" ", oldLineNum, lineNumWidth, line));
128
+ const firstChunkLength = middleSkip > 0 ? contextLimit : linesToShow.length;
129
+ for (const line of linesToShow.slice(0, firstChunkLength)) {
130
+ output.push(formatNumberedDiffLine(" ", oldLineNum, line));
136
131
  oldLineNum++;
137
132
  newLineNum++;
138
133
  }
139
134
 
140
- // Add ellipsis if we skipped lines at end
141
- if (skipEnd > 0) {
142
- output.push(formatNumberedDiffLine(" ", oldLineNum, lineNumWidth, "..."));
143
- oldLineNum += skipEnd;
144
- newLineNum += skipEnd;
135
+ if (middleSkip > 0) {
136
+ output.push(formatNumberedDiffLine(" ", oldLineNum, "..."));
137
+ oldLineNum += middleSkip;
138
+ newLineNum += middleSkip;
139
+ for (const line of linesToShow.slice(firstChunkLength)) {
140
+ output.push(formatNumberedDiffLine(" ", oldLineNum, line));
141
+ oldLineNum++;
142
+ newLineNum++;
143
+ }
144
+ }
145
+
146
+ if (trailingSkip > 0) {
147
+ output.push(formatNumberedDiffLine(" ", oldLineNum, "..."));
148
+ oldLineNum += trailingSkip;
149
+ newLineNum += trailingSkip;
145
150
  }
146
151
  } else {
147
152
  // Skip these context lines entirely
@@ -184,8 +189,6 @@ export function generateUnifiedDiffString(oldContent: string, newContent: string
184
189
  const patch = Diff.structuredPatch("", "", oldContent, newContent, "", "", { context: contextLines });
185
190
  const output: string[] = [];
186
191
  let firstChangedLine: number | undefined;
187
- const maxLineNum = Math.max(countContentLines(oldContent), countContentLines(newContent));
188
- const lineNumWidth = String(maxLineNum).length;
189
192
  for (const hunk of patch.hunks) {
190
193
  output.push(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`);
191
194
  let oldLine = hunk.oldStart;
@@ -193,18 +196,18 @@ export function generateUnifiedDiffString(oldContent: string, newContent: string
193
196
  for (const line of hunk.lines) {
194
197
  if (line.startsWith("-")) {
195
198
  if (firstChangedLine === undefined) firstChangedLine = newLine;
196
- output.push(formatNumberedDiffLine("-", oldLine, lineNumWidth, line.slice(1)));
199
+ output.push(formatNumberedDiffLine("-", oldLine, line.slice(1)));
197
200
  oldLine++;
198
201
  continue;
199
202
  }
200
203
  if (line.startsWith("+")) {
201
204
  if (firstChangedLine === undefined) firstChangedLine = newLine;
202
- output.push(formatNumberedDiffLine("+", newLine, lineNumWidth, line.slice(1)));
205
+ output.push(formatNumberedDiffLine("+", newLine, line.slice(1)));
203
206
  newLine++;
204
207
  continue;
205
208
  }
206
209
  if (line.startsWith(" ")) {
207
- output.push(formatNumberedDiffLine(" ", oldLine, lineNumWidth, line.slice(1)));
210
+ output.push(formatNumberedDiffLine(" ", oldLine, line.slice(1)));
208
211
  oldLine++;
209
212
  newLine++;
210
213
  continue;