@oh-my-pi/pi-coding-agent 15.10.10 → 15.10.11

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 (345) hide show
  1. package/CHANGELOG.md +95 -4
  2. package/dist/cli.js +23087 -0
  3. package/dist/tokenizers.linux-x64-gnu-xcjh3jwk.node +0 -0
  4. package/dist/types/async/job-manager.d.ts +18 -0
  5. package/dist/types/cli/args.d.ts +1 -1
  6. package/dist/types/cli/dry-balance-cli.d.ts +1 -1
  7. package/dist/types/cli/gallery-cli.d.ts +1 -1
  8. package/dist/types/cli/gallery-fixtures/types.d.ts +1 -1
  9. package/dist/types/cli/usage-cli.d.ts +72 -0
  10. package/dist/types/commands/launch.d.ts +1 -1
  11. package/dist/types/commands/read.d.ts +1 -1
  12. package/dist/types/commands/usage.d.ts +25 -0
  13. package/dist/types/config/append-only-context-mode.d.ts +2 -1
  14. package/dist/types/config/model-discovery.d.ts +55 -0
  15. package/dist/types/config/model-registry.d.ts +7 -219
  16. package/dist/types/config/model-resolver.d.ts +16 -10
  17. package/dist/types/config/model-roles.d.ts +28 -0
  18. package/dist/types/config/models-config-schema.d.ts +523 -42
  19. package/dist/types/config/models-config.d.ts +385 -0
  20. package/dist/types/config/settings-schema.d.ts +12 -7
  21. package/dist/types/config/settings.d.ts +1 -1
  22. package/dist/types/debug/log-viewer.d.ts +1 -1
  23. package/dist/types/debug/raw-sse.d.ts +1 -1
  24. package/dist/types/eval/backend.d.ts +0 -2
  25. package/dist/types/eval/idle-timeout.d.ts +0 -4
  26. package/dist/types/eval/js/shared/rewrite-imports.d.ts +6 -6
  27. package/dist/types/export/html/template.generated.d.ts +1 -1
  28. package/dist/types/extensibility/extensions/types.d.ts +3 -3
  29. package/dist/types/hindsight/mental-models.d.ts +17 -8
  30. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  31. package/dist/types/internal-urls/types.d.ts +1 -1
  32. package/dist/types/lsp/edits.d.ts +9 -0
  33. package/dist/types/lsp/index.d.ts +2 -2
  34. package/dist/types/lsp/types.d.ts +2 -0
  35. package/dist/types/lsp/utils.d.ts +3 -0
  36. package/dist/types/mcp/json-rpc.d.ts +5 -0
  37. package/dist/types/mnemopi/state.d.ts +11 -1
  38. package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
  39. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  40. package/dist/types/modes/components/bash-execution.d.ts +1 -1
  41. package/dist/types/modes/components/copy-selector.d.ts +1 -1
  42. package/dist/types/modes/components/dynamic-border.d.ts +1 -1
  43. package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -1
  44. package/dist/types/modes/components/extensions/extension-list.d.ts +1 -1
  45. package/dist/types/modes/components/extensions/inspector-panel.d.ts +1 -1
  46. package/dist/types/modes/components/footer.d.ts +1 -1
  47. package/dist/types/modes/components/hook-editor.d.ts +5 -0
  48. package/dist/types/modes/components/hook-input.d.ts +4 -0
  49. package/dist/types/modes/components/hook-selector.d.ts +1 -1
  50. package/dist/types/modes/components/model-selector.d.ts +1 -1
  51. package/dist/types/modes/components/plan-review-overlay.d.ts +1 -1
  52. package/dist/types/modes/components/session-observer-overlay.d.ts +1 -1
  53. package/dist/types/modes/components/session-selector.d.ts +1 -1
  54. package/dist/types/modes/components/status-line/component.d.ts +1 -1
  55. package/dist/types/modes/components/tiny-title-download-progress.d.ts +1 -1
  56. package/dist/types/modes/components/transcript-container.d.ts +25 -6
  57. package/dist/types/modes/components/tree-selector.d.ts +1 -1
  58. package/dist/types/modes/components/user-message-selector.d.ts +1 -1
  59. package/dist/types/modes/components/user-message.d.ts +2 -1
  60. package/dist/types/modes/components/visual-truncate.d.ts +1 -1
  61. package/dist/types/modes/components/welcome.d.ts +19 -3
  62. package/dist/types/modes/controllers/mcp-command-controller.d.ts +1 -1
  63. package/dist/types/modes/controllers/streaming-reveal.d.ts +1 -1
  64. package/dist/types/modes/interactive-mode.d.ts +1 -1
  65. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
  66. package/dist/types/modes/setup-wizard/scenes/types.d.ts +1 -1
  67. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
  68. package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +1 -1
  69. package/dist/types/modes/types.d.ts +2 -1
  70. package/dist/types/session/agent-session.d.ts +1 -1
  71. package/dist/types/session/auth-broker-config.d.ts +4 -0
  72. package/dist/types/session/session-manager.d.ts +1 -1
  73. package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +13 -0
  74. package/dist/types/ssh/connection-manager.d.ts +8 -0
  75. package/dist/types/task/parallel.d.ts +2 -2
  76. package/dist/types/task/worktree.d.ts +2 -0
  77. package/dist/types/tools/ask.d.ts +4 -0
  78. package/dist/types/tools/conflict-detect.d.ts +16 -0
  79. package/dist/types/tools/github-cache.d.ts +7 -0
  80. package/dist/types/tools/sqlite-reader.d.ts +3 -0
  81. package/dist/types/tui/output-block.d.ts +3 -3
  82. package/dist/types/utils/changelog.d.ts +8 -0
  83. package/dist/types/web/scrapers/readthedocs.d.ts +3 -0
  84. package/dist/types/web/scrapers/types.d.ts +12 -0
  85. package/dist/types/web/search/providers/codex.d.ts +1 -1
  86. package/dist/types/web/search/providers/gemini.d.ts +1 -1
  87. package/examples/extensions/tools.ts +5 -4
  88. package/package.json +14 -11
  89. package/scripts/build-binary.ts +18 -23
  90. package/scripts/bundle-dist.ts +81 -0
  91. package/scripts/{dev-launch → omp} +1 -1
  92. package/scripts/{dev-launch-preload.ts → omp.ts} +1 -1
  93. package/src/async/job-manager.ts +57 -3
  94. package/src/autoresearch/dashboard.ts +1 -1
  95. package/src/autoresearch/prompt-setup.md +6 -6
  96. package/src/autoresearch/prompt.md +6 -6
  97. package/src/capability/fs.ts +10 -0
  98. package/src/cli/args.ts +1 -1
  99. package/src/cli/auth-gateway-cli.ts +1 -3
  100. package/src/cli/dry-balance-cli.ts +1 -1
  101. package/src/cli/gallery-cli.ts +1 -1
  102. package/src/cli/gallery-fixtures/fs.ts +1 -1
  103. package/src/cli/gallery-fixtures/types.ts +5 -1
  104. package/src/cli/list-models.ts +2 -1
  105. package/src/cli/usage-cli.ts +603 -0
  106. package/src/cli-commands.ts +1 -0
  107. package/src/cli.ts +69 -5
  108. package/src/commands/complete.ts +1 -1
  109. package/src/commands/launch.ts +1 -1
  110. package/src/commands/read.ts +6 -3
  111. package/src/commands/usage.ts +35 -0
  112. package/src/commit/agentic/agent.ts +1 -1
  113. package/src/commit/model-selection.ts +1 -1
  114. package/src/config/append-only-context-mode.ts +6 -12
  115. package/src/config/model-discovery.ts +554 -0
  116. package/src/config/model-registry.ts +231 -1019
  117. package/src/config/model-resolver.ts +113 -156
  118. package/src/config/model-roles.ts +74 -0
  119. package/src/config/models-config-schema.ts +57 -8
  120. package/src/config/models-config.ts +129 -0
  121. package/src/config/settings-schema.ts +18 -4
  122. package/src/config/settings.ts +37 -1
  123. package/src/dap/client.ts +124 -37
  124. package/src/dap/session.ts +259 -158
  125. package/src/debug/log-viewer.ts +1 -1
  126. package/src/debug/raw-sse.ts +1 -1
  127. package/src/edit/diff.ts +47 -3
  128. package/src/edit/hashline/block-resolver.ts +20 -1
  129. package/src/edit/hashline/diff.ts +36 -1
  130. package/src/edit/hashline/execute.ts +8 -2
  131. package/src/edit/index.ts +16 -1
  132. package/src/edit/modes/patch.ts +52 -0
  133. package/src/edit/modes/replace.ts +56 -22
  134. package/src/edit/notebook.ts +22 -2
  135. package/src/edit/renderer.ts +36 -10
  136. package/src/eval/__tests__/completion-bridge.test.ts +1 -1
  137. package/src/eval/backend.ts +0 -2
  138. package/src/eval/completion-bridge.ts +2 -1
  139. package/src/eval/idle-timeout.ts +2 -9
  140. package/src/eval/js/context-manager.ts +6 -8
  141. package/src/eval/js/executor.ts +6 -2
  142. package/src/eval/js/index.ts +0 -2
  143. package/src/eval/js/shared/helpers.ts +5 -6
  144. package/src/eval/js/shared/local-module-loader.ts +1 -1
  145. package/src/eval/js/shared/prelude.txt +62 -1
  146. package/src/eval/js/shared/rewrite-imports.ts +40 -22
  147. package/src/eval/js/shared/runtime.ts +1 -1
  148. package/src/eval/py/index.ts +0 -2
  149. package/src/eval/py/kernel.ts +19 -0
  150. package/src/eval/py/runner.py +107 -3
  151. package/src/exec/bash-executor.ts +3 -1
  152. package/src/export/html/template.generated.ts +1 -1
  153. package/src/export/html/template.js +3 -1
  154. package/src/extensibility/extensions/types.ts +3 -2
  155. package/src/extensibility/plugins/legacy-pi-compat.ts +20 -3
  156. package/src/hindsight/mental-models.ts +59 -12
  157. package/src/hindsight/state.ts +6 -1
  158. package/src/internal-urls/artifact-protocol.ts +11 -2
  159. package/src/internal-urls/docs-index.generated.ts +8 -8
  160. package/src/internal-urls/issue-pr-protocol.ts +12 -5
  161. package/src/internal-urls/router.ts +1 -1
  162. package/src/internal-urls/types.ts +1 -1
  163. package/src/lib/xai-http.ts +1 -1
  164. package/src/lsp/client.ts +118 -38
  165. package/src/lsp/clients/biome-client.ts +101 -39
  166. package/src/lsp/edits.ts +143 -95
  167. package/src/lsp/index.ts +31 -22
  168. package/src/lsp/render.ts +1 -1
  169. package/src/lsp/types.ts +2 -0
  170. package/src/lsp/utils.ts +28 -10
  171. package/src/main.ts +165 -17
  172. package/src/mcp/json-rpc.ts +35 -5
  173. package/src/mcp/transports/stdio.ts +7 -1
  174. package/src/memories/index.ts +2 -1
  175. package/src/mnemopi/backend.ts +25 -3
  176. package/src/mnemopi/state.ts +38 -2
  177. package/src/modes/components/agent-dashboard.ts +10 -7
  178. package/src/modes/components/assistant-message.ts +19 -13
  179. package/src/modes/components/bash-execution.ts +1 -1
  180. package/src/modes/components/copy-selector.ts +1 -1
  181. package/src/modes/components/diff.ts +13 -2
  182. package/src/modes/components/dynamic-border.ts +12 -3
  183. package/src/modes/components/extensions/extension-dashboard.ts +8 -5
  184. package/src/modes/components/extensions/extension-list.ts +1 -1
  185. package/src/modes/components/extensions/inspector-panel.ts +1 -1
  186. package/src/modes/components/footer.ts +1 -1
  187. package/src/modes/components/history-search.ts +1 -1
  188. package/src/modes/components/hook-editor.ts +8 -0
  189. package/src/modes/components/hook-input.ts +8 -0
  190. package/src/modes/components/hook-selector.ts +2 -2
  191. package/src/modes/components/model-selector.ts +4 -2
  192. package/src/modes/components/plan-review-overlay.ts +1 -1
  193. package/src/modes/components/session-observer-overlay.ts +2 -2
  194. package/src/modes/components/session-selector.ts +1 -1
  195. package/src/modes/components/settings-selector.ts +5 -1
  196. package/src/modes/components/status-line/component.ts +1 -1
  197. package/src/modes/components/tiny-title-download-progress.ts +1 -1
  198. package/src/modes/components/transcript-container.ts +258 -53
  199. package/src/modes/components/tree-selector.ts +3 -3
  200. package/src/modes/components/user-message-selector.ts +1 -1
  201. package/src/modes/components/user-message.ts +17 -5
  202. package/src/modes/components/visual-truncate.ts +1 -1
  203. package/src/modes/components/welcome.ts +108 -26
  204. package/src/modes/controllers/command-controller.ts +10 -3
  205. package/src/modes/controllers/event-controller.ts +73 -4
  206. package/src/modes/controllers/input-controller.ts +1 -1
  207. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  208. package/src/modes/controllers/selector-controller.ts +1 -1
  209. package/src/modes/controllers/streaming-reveal.ts +85 -18
  210. package/src/modes/interactive-mode.ts +3 -9
  211. package/src/modes/setup-wizard/scenes/glyph.ts +1 -1
  212. package/src/modes/setup-wizard/scenes/providers.ts +1 -1
  213. package/src/modes/setup-wizard/scenes/sign-in.ts +1 -1
  214. package/src/modes/setup-wizard/scenes/theme.ts +1 -1
  215. package/src/modes/setup-wizard/scenes/types.ts +1 -1
  216. package/src/modes/setup-wizard/scenes/web-search.ts +1 -1
  217. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  218. package/src/modes/types.ts +2 -1
  219. package/src/prompts/agents/explore.md +2 -2
  220. package/src/prompts/agents/librarian.md +1 -2
  221. package/src/prompts/agents/oracle.md +1 -1
  222. package/src/prompts/agents/plan.md +5 -5
  223. package/src/prompts/agents/task.md +5 -5
  224. package/src/prompts/ci-green-request.md +5 -7
  225. package/src/prompts/goals/goal-budget-limit.md +2 -2
  226. package/src/prompts/goals/goal-continuation.md +4 -4
  227. package/src/prompts/goals/goal-mode-active.md +1 -1
  228. package/src/prompts/memories/read-path.md +1 -1
  229. package/src/prompts/memories/stage_one_system.md +2 -2
  230. package/src/prompts/review-custom-request.md +1 -1
  231. package/src/prompts/system/agent-creation-architect.md +2 -2
  232. package/src/prompts/system/auto-continue.md +1 -1
  233. package/src/prompts/system/background-tan-dispatch.md +1 -1
  234. package/src/prompts/system/btw-user.md +2 -2
  235. package/src/prompts/system/commit-message-system.md +13 -1
  236. package/src/prompts/system/custom-system-prompt.md +1 -1
  237. package/src/prompts/system/eager-todo.md +2 -2
  238. package/src/prompts/system/irc-incoming.md +1 -1
  239. package/src/prompts/system/manual-continue.md +1 -1
  240. package/src/prompts/system/omfg-user.md +3 -4
  241. package/src/prompts/system/orchestrate-notice.md +9 -9
  242. package/src/prompts/system/plan-mode-active.md +4 -4
  243. package/src/prompts/system/plan-mode-subagent.md +4 -5
  244. package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
  245. package/src/prompts/system/project-prompt.md +2 -2
  246. package/src/prompts/system/subagent-system-prompt.md +4 -4
  247. package/src/prompts/system/system-prompt.md +13 -24
  248. package/src/prompts/system/title-system.md +2 -2
  249. package/src/prompts/system/ttsr-tool-reminder.md +1 -1
  250. package/src/prompts/system/workflow-notice.md +1 -1
  251. package/src/prompts/tools/ast-edit.md +1 -1
  252. package/src/prompts/tools/ast-grep.md +2 -2
  253. package/src/prompts/tools/bash.md +5 -7
  254. package/src/prompts/tools/browser.md +7 -7
  255. package/src/prompts/tools/debug.md +1 -1
  256. package/src/prompts/tools/eval.md +3 -3
  257. package/src/prompts/tools/find.md +0 -1
  258. package/src/prompts/tools/github.md +8 -7
  259. package/src/prompts/tools/goal.md +1 -1
  260. package/src/prompts/tools/image-gen.md +1 -1
  261. package/src/prompts/tools/inspect-image-system.md +1 -1
  262. package/src/prompts/tools/irc.md +15 -15
  263. package/src/prompts/tools/lsp.md +2 -2
  264. package/src/prompts/tools/patch.md +2 -2
  265. package/src/prompts/tools/read.md +3 -4
  266. package/src/prompts/tools/recall.md +1 -1
  267. package/src/prompts/tools/reflect.md +1 -1
  268. package/src/prompts/tools/render-mermaid.md +2 -2
  269. package/src/prompts/tools/replace.md +4 -10
  270. package/src/prompts/tools/rewind.md +2 -2
  271. package/src/prompts/tools/search-tool-bm25.md +1 -9
  272. package/src/prompts/tools/search.md +0 -1
  273. package/src/prompts/tools/ssh.md +0 -4
  274. package/src/prompts/tools/task.md +2 -3
  275. package/src/prompts/tools/todo.md +1 -1
  276. package/src/sdk.ts +23 -10
  277. package/src/session/agent-session.ts +44 -10
  278. package/src/session/auth-broker-config.ts +30 -1
  279. package/src/session/session-manager.ts +2 -2
  280. package/src/session/streaming-output.ts +23 -2
  281. package/src/slash-commands/builtin-registry.ts +20 -0
  282. package/src/slash-commands/helpers/stats-dashboard.ts +85 -0
  283. package/src/ssh/connection-manager.ts +27 -0
  284. package/src/task/commands.ts +2 -1
  285. package/src/task/executor.ts +61 -53
  286. package/src/task/index.ts +137 -60
  287. package/src/task/parallel.ts +3 -3
  288. package/src/task/render.ts +2 -2
  289. package/src/task/worktree.ts +64 -56
  290. package/src/thinking.ts +2 -1
  291. package/src/tiny/title-client.ts +26 -11
  292. package/src/tools/archive-reader.ts +30 -2
  293. package/src/tools/ask.ts +104 -21
  294. package/src/tools/ast-edit.ts +25 -5
  295. package/src/tools/auto-generated-guard.ts +20 -3
  296. package/src/tools/bash-interactive.ts +27 -7
  297. package/src/tools/bash.ts +54 -13
  298. package/src/tools/browser/launch.ts +11 -2
  299. package/src/tools/browser/readable.ts +19 -2
  300. package/src/tools/browser/registry.ts +4 -1
  301. package/src/tools/browser/render.ts +2 -2
  302. package/src/tools/browser/tab-supervisor.ts +55 -16
  303. package/src/tools/conflict-detect.ts +50 -4
  304. package/src/tools/debug.ts +1 -1
  305. package/src/tools/eval-render.ts +5 -5
  306. package/src/tools/eval.ts +0 -2
  307. package/src/tools/fetch.ts +33 -10
  308. package/src/tools/gh-cache-invalidation.ts +63 -8
  309. package/src/tools/gh-renderer.ts +1 -1
  310. package/src/tools/gh.ts +172 -29
  311. package/src/tools/github-cache.ts +70 -6
  312. package/src/tools/image-gen.ts +3 -9
  313. package/src/tools/irc.ts +5 -1
  314. package/src/tools/job.ts +1 -1
  315. package/src/tools/read.ts +202 -61
  316. package/src/tools/render-utils.ts +3 -3
  317. package/src/tools/resolve.ts +1 -1
  318. package/src/tools/search.ts +92 -29
  319. package/src/tools/sqlite-reader.ts +17 -5
  320. package/src/tools/ssh.ts +8 -8
  321. package/src/tools/todo.ts +38 -8
  322. package/src/tools/write.ts +118 -18
  323. package/src/tui/output-block.ts +4 -4
  324. package/src/utils/changelog.ts +27 -1
  325. package/src/utils/file-mentions.ts +2 -1
  326. package/src/web/scrapers/arxiv.ts +1 -1
  327. package/src/web/scrapers/go-pkg.ts +1 -1
  328. package/src/web/scrapers/iacr.ts +1 -1
  329. package/src/web/scrapers/readthedocs.ts +1 -1
  330. package/src/web/scrapers/twitter.ts +2 -1
  331. package/src/web/scrapers/types.ts +87 -8
  332. package/src/web/scrapers/wikipedia.ts +1 -1
  333. package/src/web/scrapers/youtube.ts +6 -1
  334. package/src/web/search/index.ts +1 -1
  335. package/src/web/search/providers/codex.ts +2 -1
  336. package/src/web/search/providers/gemini.ts +2 -3
  337. package/src/web/search/render.ts +8 -6
  338. package/dist/types/config/model-equivalence.d.ts +0 -24
  339. package/dist/types/config/model-id-affixes.d.ts +0 -12
  340. package/dist/types/config/model-provider-priority.d.ts +0 -1
  341. package/dist/types/exec/idle-timeout-watchdog.d.ts +0 -18
  342. package/src/config/model-equivalence.ts +0 -875
  343. package/src/config/model-id-affixes.ts +0 -81
  344. package/src/config/model-provider-priority.ts +0 -56
  345. package/src/exec/idle-timeout-watchdog.ts +0 -126
@@ -66,7 +66,7 @@ function dropTrailingBlankLines(text: string): string {
66
66
  function appendLine(component: Component, line: string | undefined): Component {
67
67
  if (!line) return component;
68
68
  const wrapped = {
69
- render: (width: number): string[] => {
69
+ render: (width: number): readonly string[] => {
70
70
  const base = component.render(width);
71
71
  return [...base, line];
72
72
  },
@@ -95,7 +95,7 @@ function renderRunCell(
95
95
 
96
96
  let cached: { key: bigint; width: number; lines: string[] } | undefined;
97
97
  return markFramedBlockComponent({
98
- render: (width: number): string[] => {
98
+ render: (width: number): readonly string[] => {
99
99
  const expanded = options.renderContext?.expanded ?? options.expanded;
100
100
  const previewLines = options.renderContext?.previewLines ?? BROWSER_DEFAULT_PREVIEW_LINES;
101
101
  const key = new Hasher()
@@ -1,4 +1,4 @@
1
- import { getPuppeteerDir, isCompiledBinary, logger, Snowflake } from "@oh-my-pi/pi-utils";
1
+ import { getPuppeteerDir, logger, Snowflake, workerHostEntry } from "@oh-my-pi/pi-utils";
2
2
  import type { Page, Target } from "puppeteer-core";
3
3
  import { callSessionTool } from "../../eval/js/tool-bridge";
4
4
  import type { ToolSession } from "../../sdk";
@@ -18,14 +18,8 @@ import type {
18
18
  WorkerOutbound,
19
19
  } from "./tab-protocol";
20
20
 
21
- // Worker entry. The literal string in `new Worker("./packages/coding-agent/src/tools/browser/tab-worker-entry.ts", …)`
22
- // below is what Bun's `--compile` static analyzer needs to bundle the worker
23
- // (registered as an additional entrypoint in `scripts/build-binary.ts`); in
24
- // dev we resolve the same source via `import.meta.url`. Replaces the older
25
- // `with { type: "file" }` pattern, which only copied the entry as a raw
26
- // asset and could not resolve the worker's relative imports inside a
27
- // compiled binary (issue #1011 was a false-positive fix — the regression
28
- // test only checked emission, not actual worker startup).
21
+ // Coding-agent binary/bundle workers route through the CLI entrypoint with a
22
+ // hidden argv mode, so compiled/npm builds only need one JavaScript entry.
29
23
 
30
24
  interface WorkerHandle {
31
25
  send(msg: WorkerInbound, transferList?: Transferable[]): void;
@@ -84,21 +78,51 @@ export interface ReleaseTabOptions {
84
78
  }
85
79
 
86
80
  const tabs = new Map<string, TabSession>();
81
+ // Per-name acquisition chain: serializes concurrent `acquireTab` calls for the
82
+ // same tab name so the existence check and `tabs.set` (separated by several
83
+ // awaits) cannot interleave and leak a worker + browser refCount.
84
+ const acquireChains = new Map<string, Promise<void>>();
87
85
  const GRACE_MS = 750;
88
86
 
89
87
  export function getTab(name: string): TabSession | undefined {
90
88
  return tabs.get(name);
91
89
  }
92
90
 
93
- export async function acquireTab(
91
+ export function acquireTab(name: string, browser: BrowserHandle, opts: AcquireTabOptions): Promise<AcquireTabResult> {
92
+ const prior = acquireChains.get(name) ?? Promise.resolve();
93
+ const result = prior.then(() => acquireTabImpl(name, browser, opts));
94
+ const tail = result.then(
95
+ () => undefined,
96
+ () => undefined,
97
+ );
98
+ acquireChains.set(name, tail);
99
+ void tail.then(() => {
100
+ if (acquireChains.get(name) === tail) acquireChains.delete(name);
101
+ });
102
+ return result;
103
+ }
104
+
105
+ async function acquireTabImpl(
94
106
  name: string,
95
107
  browser: BrowserHandle,
96
108
  opts: AcquireTabOptions,
97
109
  ): Promise<AcquireTabResult> {
110
+ // Serialized opens can sit behind a slow predecessor in the per-name
111
+ // chain; honor an abort at dequeue instead of spawning a worker and
112
+ // browser hold nobody is waiting for.
113
+ if (opts.signal?.aborted) {
114
+ throw new ToolAbortError("Browser tab open aborted");
115
+ }
116
+ // Temporary refCount hold so releasing an existing tab on the SAME browser
117
+ // below cannot drop it to refCount 0 and dispose the instance we are about
118
+ // to reuse (e.g. reopening the sole tab with a different dialogs policy).
119
+ let tempHold = false;
98
120
  const existing = tabs.get(name);
99
121
  if (existing) {
100
122
  if (existing.browser === browser && existing.state === "alive") {
101
123
  if (opts.dialogs !== undefined && opts.dialogs !== existing.dialogPolicy) {
124
+ holdBrowser(browser);
125
+ tempHold = true;
102
126
  await releaseTab(name, { kill: false });
103
127
  } else {
104
128
  const reuseSteps: string[] = [];
@@ -127,12 +151,25 @@ export async function acquireTab(
127
151
  return { tab: tabs.get(name)!, created: false };
128
152
  }
129
153
  } else {
154
+ if (existing.browser === browser) {
155
+ holdBrowser(browser);
156
+ tempHold = true;
157
+ }
130
158
  await releaseTab(name, { kill: false });
131
159
  }
132
160
  }
133
161
 
134
- const initPayload = await buildInitPayload(browser, opts);
135
- let worker = await spawnTabWorker();
162
+ let initPayload: WorkerInitPayload;
163
+ let worker: WorkerHandle;
164
+ try {
165
+ initPayload = await buildInitPayload(browser, opts);
166
+ worker = await spawnTabWorker();
167
+ } catch (error) {
168
+ // Failing before the worker took its own hold must release the
169
+ // temporary one, or the browser's refCount never reaches 0 again.
170
+ if (tempHold || browser.refCount === 0) await releaseBrowser(browser, { kill: false });
171
+ throw error;
172
+ }
136
173
  let info: ReadyInfo;
137
174
  try {
138
175
  info = await initializeTabWorker(worker, initPayload, opts.timeoutMs + GRACE_MS);
@@ -142,7 +179,7 @@ export async function acquireTab(
142
179
  // the inline worker here so module-resolution failures don't poison every tab open.
143
180
  await worker.terminate().catch(() => undefined);
144
181
  if (worker.mode === "inline") {
145
- if (browser.refCount === 0) await releaseBrowser(browser, { kill: false });
182
+ if (tempHold || browser.refCount === 0) await releaseBrowser(browser, { kill: false });
146
183
  throw error;
147
184
  }
148
185
  logger.warn("Tab worker init failed; retrying with inline tab worker (no sync-loop guard)", {
@@ -153,7 +190,7 @@ export async function acquireTab(
153
190
  info = await initializeTabWorker(worker, initPayload, opts.timeoutMs + GRACE_MS);
154
191
  } catch (inlineError) {
155
192
  await worker.terminate().catch(() => undefined);
156
- if (browser.refCount === 0) await releaseBrowser(browser, { kill: false });
193
+ if (tempHold || browser.refCount === 0) await releaseBrowser(browser, { kill: false });
157
194
  const finalError = new ToolError(
158
195
  `Failed to start browser tab worker (inline fallback also failed): ${inlineError instanceof Error ? inlineError.message : String(inlineError)}`,
159
196
  );
@@ -163,6 +200,7 @@ export async function acquireTab(
163
200
  }
164
201
 
165
202
  holdBrowser(browser);
203
+ if (tempHold) await releaseBrowser(browser, { kill: false });
166
204
  const tab: TabSession = {
167
205
  name,
168
206
  browser,
@@ -474,8 +512,9 @@ async function raceWithTimeout<T>(
474
512
 
475
513
  async function spawnTabWorker(): Promise<WorkerHandle> {
476
514
  try {
477
- const worker = isCompiledBinary()
478
- ? new Worker("./packages/coding-agent/src/tools/browser/tab-worker-entry.ts", { type: "module" })
515
+ const hostEntry = workerHostEntry();
516
+ const worker = hostEntry
517
+ ? new Worker(hostEntry, { type: "module", argv: ["__omp_tab_worker"] })
479
518
  : new Worker(new URL("./tab-worker-entry.ts", import.meta.url).href, { type: "module" });
480
519
  return wrapBunWorker(worker);
481
520
  } catch (err) {
@@ -68,7 +68,9 @@ export function scanConflictLines(lines: readonly string[], firstLineNumber: num
68
68
  } | null = null;
69
69
 
70
70
  for (let i = 0; i < lines.length; i++) {
71
- const line = lines[i];
71
+ // Strip a trailing \r so CRLF checkouts match the same markers; stored
72
+ // section lines are LF-normalized (splice re-applies \r on write).
73
+ const line = stripTrailingCr(lines[i]);
72
74
  const ln = firstLineNumber + i;
73
75
 
74
76
  const oursLabel = matchMarker(line, OURS_PREFIX);
@@ -338,13 +340,22 @@ export function spliceConflict(originalText: string, entry: ConflictEntry, repla
338
340
  }
339
341
 
340
342
  const trimmed = normalizeTrailingNewline(replacement);
341
- const replacementLines = trimmed.split("\n");
343
+ let replacementLines = trimmed.split("\n").map(stripTrailingCr);
344
+ // Round-trip fidelity for CRLF files: recorded sections are LF-normalized,
345
+ // so re-apply \r to spliced lines when the matched region used CRLF. The
346
+ // final replacement line only carries \r when another line follows it.
347
+ if (lines[match.startIdx]!.endsWith("\r")) {
348
+ const hasFollowingLine = match.endIdx + 1 < lines.length;
349
+ replacementLines = replacementLines.map((l, i) =>
350
+ i < replacementLines.length - 1 || hasFollowingLine ? `${l}\r` : l,
351
+ );
352
+ }
342
353
  const next = [...lines.slice(0, match.startIdx), ...replacementLines, ...lines.slice(match.endIdx + 1)];
343
354
  return next.join("\n");
344
355
  }
345
356
 
346
357
  /** Reconstruct the recorded marker block as it should appear in the file. */
347
- function buildRecordedRegion(entry: ConflictEntry): string[] {
358
+ function buildRecordedRegion(entry: ConflictBlock): string[] {
348
359
  const out: string[] = [];
349
360
  out.push(entry.oursLabel ? `${OURS_PREFIX} ${entry.oursLabel}` : OURS_PREFIX);
350
361
  out.push(...entry.oursLines);
@@ -358,6 +369,36 @@ function buildRecordedRegion(entry: ConflictEntry): string[] {
358
369
  return out;
359
370
  }
360
371
 
372
+ /**
373
+ * True when two registered blocks record the same marker-block content
374
+ * (labels and all sides). Out-of-band edits can shift a block's line
375
+ * numbers between reads, registering a fresh id while the stale one
376
+ * persists; callers use content identity to treat a locate-miss for the
377
+ * stale twin as "already resolved" instead of a hard failure.
378
+ */
379
+ export function conflictRegionsEqual(a: ConflictBlock, b: ConflictBlock): boolean {
380
+ const ra = buildRecordedRegion(a);
381
+ const rb = buildRecordedRegion(b);
382
+ if (ra.length !== rb.length) return false;
383
+ for (let i = 0; i < ra.length; i++) {
384
+ if (ra[i] !== rb[i]) return false;
385
+ }
386
+ return true;
387
+ }
388
+
389
+ /**
390
+ * True when the entry's recorded marker block still occurs in `content`
391
+ * (LF-normalized — recorded sections are stored LF). Distinguishes a stale
392
+ * re-registration of a just-resolved region (no longer present) from a
393
+ * DISTINCT conflict block that happens to be byte-identical (still present
394
+ * elsewhere in the file and must stay addressable).
395
+ */
396
+ export function conflictRegionPresent(content: string, entry: ConflictBlock): boolean {
397
+ const region = buildRecordedRegion(entry).join("\n");
398
+ const normalized = content.includes("\r") ? content.replace(/\r\n/g, "\n") : content;
399
+ return normalized.includes(region);
400
+ }
401
+
361
402
  /**
362
403
  * Find a contiguous match of `expected` inside `lines`, preferring the
363
404
  * occurrence closest to `preferredIdx` to disambiguate when an identical
@@ -391,11 +432,16 @@ function locateRegion(
391
432
  function matchesAt(lines: readonly string[], startIdx: number, expected: readonly string[]): boolean {
392
433
  if (startIdx < 0 || startIdx + expected.length > lines.length) return false;
393
434
  for (let i = 0; i < expected.length; i++) {
394
- if (lines[startIdx + i] !== expected[i]) return false;
435
+ // Recorded lines are LF-normalized; tolerate CRLF on-disk lines.
436
+ if (stripTrailingCr(lines[startIdx + i]!) !== expected[i]) return false;
395
437
  }
396
438
  return true;
397
439
  }
398
440
 
441
+ function stripTrailingCr(line: string): string {
442
+ return line.endsWith("\r") ? line.slice(0, -1) : line;
443
+ }
444
+
399
445
  function normalizeTrailingNewline(replacement: string): string {
400
446
  if (replacement.endsWith("\r\n")) return replacement.slice(0, -2);
401
447
  if (replacement.endsWith("\n")) return replacement.slice(0, -1);
@@ -592,7 +592,7 @@ export const debugToolRenderer = {
592
592
  ): Component {
593
593
  const outputBlock = new CachedOutputBlock();
594
594
  return markFramedBlockComponent({
595
- render(width: number): string[] {
595
+ render(width: number): readonly string[] {
596
596
  const action = (args?.action ?? result.details?.action ?? "debug").replaceAll("_", " ");
597
597
  const success = !options.isPartial && !result.isError;
598
598
  const statusIcon = success
@@ -455,7 +455,7 @@ function formatCellOutputLines(
455
455
  previewLines: number,
456
456
  theme: Theme,
457
457
  width: number,
458
- ): { lines: string[]; hiddenCount: number } {
458
+ ): { lines: readonly string[]; hiddenCount: number } {
459
459
  if (!cell.output) {
460
460
  return { lines: [], hiddenCount: 0 };
461
461
  }
@@ -492,7 +492,7 @@ export const evalToolRenderer = {
492
492
  let cached: { key: string; width: number; result: string[] } | undefined;
493
493
 
494
494
  return markFramedBlockComponent({
495
- render: (width: number): string[] => {
495
+ render: (width: number): readonly string[] => {
496
496
  const key = `${options.expanded ? 1 : 0}|${cells.map(c => `${c.language}:${c.title ?? ""}:${c.code.length}`).join("|")}`;
497
497
  if (cached && cached.key === key && cached.width === width) {
498
498
  return cached.result;
@@ -573,7 +573,7 @@ export const evalToolRenderer = {
573
573
  let cached: { key: string; width: number; result: string[] } | undefined;
574
574
 
575
575
  return markFramedBlockComponent({
576
- render: (width: number): string[] => {
576
+ render: (width: number): readonly string[] => {
577
577
  const expanded = options.renderContext?.expanded ?? options.expanded;
578
578
  const previewLines = options.renderContext?.previewLines ?? EVAL_DEFAULT_PREVIEW_LINES;
579
579
  const key = `${expanded}|${previewLines}|${options.spinnerFrame}`;
@@ -697,12 +697,12 @@ export const evalToolRenderer = {
697
697
  const textContent = `\n${styledOutput}`;
698
698
 
699
699
  let cachedWidth: number | undefined;
700
- let cachedLines: string[] | undefined;
700
+ let cachedLines: readonly string[] | undefined;
701
701
  let cachedSkipped: number | undefined;
702
702
  let cachedPreviewLines: number | undefined;
703
703
 
704
704
  return {
705
- render: (width: number): string[] => {
705
+ render: (width: number): readonly string[] => {
706
706
  const previewLines = options.renderContext?.previewLines ?? EVAL_DEFAULT_PREVIEW_LINES;
707
707
  if (cachedLines === undefined || cachedWidth !== width || cachedPreviewLines !== previewLines) {
708
708
  const result = truncateToVisualLines(textContent, previewLines, width);
package/src/tools/eval.ts CHANGED
@@ -358,8 +358,6 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
358
358
  session,
359
359
  idleTimeoutMs,
360
360
  reset: cell.reset,
361
- artifactPath,
362
- artifactId,
363
361
  onChunk: chunk => {
364
362
  outputSink!.push(chunk);
365
363
  },
@@ -7,7 +7,6 @@ import type { FetchImpl, ImageContent, TextContent } from "@oh-my-pi/pi-ai";
7
7
  import { htmlToMarkdown } from "@oh-my-pi/pi-natives";
8
8
  import { type Component, Text } from "@oh-my-pi/pi-tui";
9
9
  import { $which, ptree, truncate } from "@oh-my-pi/pi-utils";
10
- import { parseHTML } from "linkedom";
11
10
  import { LRUCache } from "lru-cache/raw";
12
11
  import type { Settings } from "../config/settings";
13
12
  import { readEditableNotebookText } from "../edit/notebook";
@@ -23,7 +22,7 @@ import { ensureTool } from "../utils/tools-manager";
23
22
  import { extractWithParallel, findParallelApiKey, getParallelExtractContent } from "../web/parallel";
24
23
  import { specialHandlers } from "../web/scrapers";
25
24
  import type { RenderResult } from "../web/scrapers/types";
26
- import { finalizeOutput, loadPage, looksLikeHtml, MAX_OUTPUT_CHARS } from "../web/scrapers/types";
25
+ import { finalizeOutput, loadPage, looksLikeHtml, MAX_BYTES, MAX_OUTPUT_CHARS } from "../web/scrapers/types";
27
26
  import { convertWithMarkit, fetchBinary } from "../web/scrapers/utils";
28
27
  import { type ArchiveFormat, listArchiveRoot, sniffArchiveFormat } from "./archive-reader";
29
28
  import { applyListLimit } from "./list-limit";
@@ -191,7 +190,7 @@ export interface ParsedReadUrlTarget {
191
190
 
192
191
  /** Recognize a single selector token (`raw` or one/many line ranges). */
193
192
  function isUrlSelectorToken(token: string): boolean {
194
- if (token === "raw") return true;
193
+ if (token.toLowerCase() === "raw") return true;
195
194
  try {
196
195
  return parseLineRanges(token) !== null;
197
196
  } catch {
@@ -213,7 +212,7 @@ export function parseReadUrlTarget(readPath: string): ParsedReadUrlTarget | null
213
212
  let raw = false;
214
213
  let ranges: readonly LineRange[] | undefined;
215
214
  for (const sel of embedded?.sels ?? []) {
216
- if (sel === "raw") {
215
+ if (sel.toLowerCase() === "raw") {
217
216
  raw = true;
218
217
  continue;
219
218
  }
@@ -549,7 +548,8 @@ function cleanFeedText(text: string): string {
549
548
  /**
550
549
  * Parse RSS/Atom feed to markdown
551
550
  */
552
- function parseFeedToMarkdown(content: string, maxItems = 10): string {
551
+ async function parseFeedToMarkdown(content: string, maxItems = 10): Promise<string> {
552
+ const { parseHTML } = await import("linkedom");
553
553
  try {
554
554
  const doc = parseHTML(content).document;
555
555
 
@@ -805,6 +805,21 @@ function isArchiveHint(mime: string, extensionHint: string): boolean {
805
805
  return ARCHIVE_MIMES.has(mime) || ARCHIVE_EXTENSIONS.has(extensionHint);
806
806
  }
807
807
 
808
+ /**
809
+ * Content types whose payload renderUrl always re-fetches via fetchBinary.
810
+ * Skipping the initial body read for them avoids downloading and
811
+ * string-decoding huge binaries (PDFs, archives, images) twice.
812
+ */
813
+ function shouldSkipBodyDownload(contentType: string): boolean {
814
+ return (
815
+ CONVERTIBLE_MIMES.has(contentType) ||
816
+ NOTEBOOK_MIMES.has(contentType) ||
817
+ SQLITE_MIMES.has(contentType) ||
818
+ ARCHIVE_MIMES.has(contentType) ||
819
+ SUPPORTED_INLINE_IMAGE_MIME_TYPES.has(contentType)
820
+ );
821
+ }
822
+
808
823
  function getArchiveFormatHint(mime: string, extensionHint: string): ArchiveFormat | undefined {
809
824
  if (extensionHint === ".zip" || mime === "application/zip" || mime === "application/x-zip-compressed") {
810
825
  return "zip";
@@ -901,6 +916,7 @@ async function tryRenderBinaryPayload(
901
916
  mime: string,
902
917
  extHint: string,
903
918
  rawContent: string,
919
+ bodySkipped: boolean,
904
920
  timeout: number,
905
921
  signal: AbortSignal | undefined,
906
922
  fetchedAt: string,
@@ -909,7 +925,7 @@ async function tryRenderBinaryPayload(
909
925
  const hasNotebookHint = isNotebookHint(mime, extHint);
910
926
  const hasSqliteHint = isSqliteHint(mime, extHint);
911
927
  const hasArchiveHint = isArchiveHint(mime, extHint);
912
- const rawLooksBinary = sampleLooksBinary(rawContent);
928
+ const rawLooksBinary = bodySkipped || sampleLooksBinary(rawContent);
913
929
  if (!hasNotebookHint && !hasSqliteHint && !hasArchiveHint && !rawLooksBinary) {
914
930
  return null;
915
931
  }
@@ -1092,7 +1108,7 @@ async function renderUrl(
1092
1108
  }
1093
1109
 
1094
1110
  // Step 2: Fetch page
1095
- const response = await loadPage(url, { timeout, signal });
1111
+ const response = await loadPage(url, { timeout, signal, skipBodyForContentType: shouldSkipBodyDownload });
1096
1112
  if (signal?.aborted) {
1097
1113
  throw new ToolAbortError();
1098
1114
  }
@@ -1105,11 +1121,17 @@ async function renderUrl(
1105
1121
  content: "",
1106
1122
  fetchedAt,
1107
1123
  truncated: false,
1108
- notes: [response.status ? `Failed to fetch URL (HTTP ${response.status})` : "Failed to fetch URL"],
1124
+ notes: [
1125
+ response.status ? `Failed to fetch URL (HTTP ${response.status})` : "Failed to fetch URL",
1126
+ ...(response.error ? [`Cause: ${response.error}`] : []),
1127
+ ],
1109
1128
  };
1110
1129
  }
1111
1130
 
1112
1131
  const { finalUrl, content: rawContent } = response;
1132
+ if (response.truncated) {
1133
+ notes.push(`Response body exceeded ${formatBytes(MAX_BYTES)} and was cut mid-stream; content is incomplete`);
1134
+ }
1113
1135
  const mime = normalizeMime(response.contentType);
1114
1136
  const extHint = getExtensionHint(finalUrl);
1115
1137
 
@@ -1276,6 +1298,7 @@ async function renderUrl(
1276
1298
  mime,
1277
1299
  extHint,
1278
1300
  rawContent,
1301
+ response.bodySkipped === true,
1279
1302
  timeout,
1280
1303
  signal,
1281
1304
  fetchedAt,
@@ -1321,7 +1344,7 @@ async function renderUrl(
1321
1344
  }
1322
1345
 
1323
1346
  if (isFeed || (isXml && (rawContent.includes("<rss") || rawContent.includes("<feed")))) {
1324
- const parsed = parseFeedToMarkdown(rawContent);
1347
+ const parsed = await parseFeedToMarkdown(rawContent);
1325
1348
  const output = finalizeOutput(parsed);
1326
1349
  return {
1327
1350
  url,
@@ -1414,7 +1437,7 @@ async function renderUrl(
1414
1437
  const altResult = await loadPage(resolved, { timeout, signal });
1415
1438
  if (altResult.ok && altResult.content.trim().length > 200) {
1416
1439
  notes.push(`Used feed alternate: ${resolved}`);
1417
- const parsed = parseFeedToMarkdown(altResult.content);
1440
+ const parsed = await parseFeedToMarkdown(altResult.content);
1418
1441
  const output = finalizeOutput(parsed);
1419
1442
  return {
1420
1443
  url,
@@ -17,7 +17,7 @@
17
17
  * number, all auth_keys) because the upside of staleness elimination
18
18
  * dwarfs the cost of one cache miss.
19
19
  */
20
- import { invalidateAllForNumber } from "./github-cache";
20
+ import { invalidateAllForNumber, invalidateAllForRepo } from "./github-cache";
21
21
 
22
22
  const PR_URL_PATTERN = /^https:\/\/github\.com\/([^/\s]+\/[^/\s]+)\/pull\/(\d+)(?:[/?#].*)?$/i;
23
23
  const ISSUE_URL_PATTERN = /^https:\/\/github\.com\/([^/\s]+\/[^/\s]+)\/issues\/(\d+)(?:[/?#].*)?$/i;
@@ -48,13 +48,60 @@ const MUTATING_PR_SUBCMDS: Record<string, true> = {
48
48
  lock: true,
49
49
  unlock: true,
50
50
  };
51
+
52
+ /**
53
+ * Flags whose value is the next argv token (`--milestone 3`). The detector
54
+ * must skip those values so `gh pr edit --milestone 3 14` invalidates #14,
55
+ * not #3. Curated for the mutating issue/PR subcommands above; a few short
56
+ * flags are booleans for *some* subcommands (e.g. `-c` is `--comment` text
57
+ * for `pr close` but a boolean for `pr review`) — we bias toward value-taking
58
+ * because over-skipping at worst falls back to repo-wide invalidation, while
59
+ * under-skipping invalidates the wrong number.
60
+ */
61
+ const VALUE_TAKING_FLAGS: ReadonlySet<string> = new Set([
62
+ "-m",
63
+ "--milestone",
64
+ "-t",
65
+ "--title",
66
+ "-b",
67
+ "--body",
68
+ "-F",
69
+ "--body-file",
70
+ "-a",
71
+ "--assignee",
72
+ "--add-assignee",
73
+ "--remove-assignee",
74
+ "-l",
75
+ "--label",
76
+ "--add-label",
77
+ "--remove-label",
78
+ "-p",
79
+ "--project",
80
+ "--add-project",
81
+ "--remove-project",
82
+ "--add-reviewer",
83
+ "--remove-reviewer",
84
+ "-B",
85
+ "--base",
86
+ "-c",
87
+ "--comment",
88
+ "-r",
89
+ "--reason",
90
+ "--branch",
91
+ "--subject",
92
+ "--match-head-commit",
93
+ "--author-email",
94
+ ]);
51
95
  /**
52
96
  * Walk a single shell command's token stream looking for a top-level
53
- * `gh (issue|pr) <subcmd> <id-or-url>` invocation and return the
54
- * invalidation key when one is found. Returns `null` for non-matching
55
- * commands so the caller can iterate cheaply.
97
+ * `gh (issue|pr) <subcmd> [<id-or-url>]` invocation and return the
98
+ * invalidation key when one is found. `number === undefined` means the
99
+ * subcommand mutates state but names no identifier (gh defaults to the
100
+ * current branch's PR), so the caller must fall back to repo-wide
101
+ * invalidation. Returns `null` for non-matching commands so the caller can
102
+ * iterate cheaply.
56
103
  */
57
- function detectGhMutation(tokens: readonly string[]): { number: number; repo?: string } | null {
104
+ function detectGhMutation(tokens: readonly string[]): { number?: number; repo?: string } | null {
58
105
  const ghIdx = tokens.indexOf("gh");
59
106
  if (ghIdx === -1) return null;
60
107
  const subject = tokens[ghIdx + 1];
@@ -82,7 +129,9 @@ function detectGhMutation(tokens: readonly string[]): { number: number; repo?: s
82
129
  }
83
130
  for (let i = ghIdx + 3; i < tokens.length; i++) {
84
131
  const token = tokens[i];
85
- if (token === "-R" || token === "--repo") {
132
+ if (token === "-R" || token === "--repo" || VALUE_TAKING_FLAGS.has(token)) {
133
+ // Skip the flag's value so it is never mistaken for the positional
134
+ // identifier (`--milestone 3 14` must invalidate #14, not #3).
86
135
  i++;
87
136
  continue;
88
137
  }
@@ -100,7 +149,9 @@ function detectGhMutation(tokens: readonly string[]): { number: number; repo?: s
100
149
  }
101
150
  }
102
151
  }
103
- return null;
152
+ // Mutating subcommand with no identifier: gh operates on the current
153
+ // branch's PR, which we cannot resolve synchronously here.
154
+ return repo !== undefined ? { repo } : {};
104
155
  }
105
156
 
106
157
  /**
@@ -195,6 +246,10 @@ export function invalidateGithubCacheForBashCommand(command: string): void {
195
246
  for (const segment of segments) {
196
247
  const hit = detectGhMutation(segment);
197
248
  if (!hit) continue;
198
- invalidateAllForNumber(hit.number, hit.repo);
249
+ if (hit.number !== undefined) {
250
+ invalidateAllForNumber(hit.number, hit.repo);
251
+ } else {
252
+ invalidateAllForRepo(hit.repo);
253
+ }
199
254
  }
200
255
  }
@@ -163,7 +163,7 @@ function getJobStateVisual(
163
163
  ): { iconRaw: string; iconColor: ToolUIColor; textColor: ThemeColor } {
164
164
  if (job.conclusion && SUCCESS_CONCLUSIONS.has(job.conclusion)) {
165
165
  return {
166
- iconRaw: theme.symbol("tool.gh"),
166
+ iconRaw: theme.status.success,
167
167
  iconColor: "accent",
168
168
  textColor: "success",
169
169
  };