@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
@@ -76,8 +76,14 @@ interface DapSession {
76
76
  functionBreakpoints: DapFunctionBreakpointRecord[];
77
77
  instructionBreakpoints: DapInstructionBreakpoint[];
78
78
  dataBreakpoints: DapDataBreakpoint[];
79
- output: string;
79
+ /** Serializes breakpoint mutations — see #serializeBreakpointMutation. */
80
+ breakpointMutationQueue: Promise<void>;
81
+ /** Recent output chunks; trimmed from the front when over MAX_OUTPUT_BYTES. */
82
+ outputChunks: string[];
83
+ /** Cumulative bytes of output ever received (reported in summaries). */
80
84
  outputBytes: number;
85
+ /** Bytes currently buffered in outputChunks. */
86
+ outputBufferedBytes: number;
81
87
  outputTruncated: boolean;
82
88
  stop: DapStopLocation;
83
89
  threads: DapThread[];
@@ -175,10 +181,31 @@ function normalizePath(filePath: string): string {
175
181
 
176
182
  function truncateOutput(session: DapSession, output: string): void {
177
183
  if (!output) return;
178
- session.output += output;
179
- session.outputBytes += Buffer.byteLength(output, "utf-8");
180
- while (Buffer.byteLength(session.output, "utf-8") > MAX_OUTPUT_BYTES) {
181
- session.output = session.output.slice(Math.min(1024, session.output.length));
184
+ const bytes = Buffer.byteLength(output, "utf-8");
185
+ session.outputChunks.push(output);
186
+ session.outputBytes += bytes;
187
+ session.outputBufferedBytes += bytes;
188
+ // Trim whole chunks from the front, but only while the remainder still
189
+ // holds a full MAX_OUTPUT_BYTES tail — dropping the front chunk whenever
190
+ // the total exceeded the cap could retain far less than the cap (e.g.
191
+ // [120KB, 10KB] would keep only 10KB). Recomputing one big string's byte
192
+ // length per 1KB trim iteration was O(n^2) inside the event dispatch loop.
193
+ while (session.outputChunks.length > 1) {
194
+ const frontBytes = Buffer.byteLength(session.outputChunks[0], "utf-8");
195
+ if (session.outputBufferedBytes - frontBytes < MAX_OUTPUT_BYTES) break;
196
+ session.outputChunks.shift();
197
+ session.outputBufferedBytes -= frontBytes;
198
+ session.outputTruncated = true;
199
+ }
200
+ if (session.outputBufferedBytes > MAX_OUTPUT_BYTES) {
201
+ // Byte-slice the front chunk's head so exactly the cap remains (a torn
202
+ // code point at the cut decodes as U+FFFD, acceptable for log output).
203
+ const front = session.outputChunks[0];
204
+ const frontBytes = Buffer.byteLength(front, "utf-8");
205
+ const excess = session.outputBufferedBytes - MAX_OUTPUT_BYTES;
206
+ const kept = Buffer.from(front, "utf-8").subarray(excess).toString("utf-8");
207
+ session.outputChunks[0] = kept;
208
+ session.outputBufferedBytes += Buffer.byteLength(kept, "utf-8") - frontBytes;
182
209
  session.outputTruncated = true;
183
210
  }
184
211
  }
@@ -368,6 +395,26 @@ export class DapSessionManager {
368
395
  }
369
396
  }
370
397
 
398
+ /**
399
+ * Serialize breakpoint mutations per session: every mutator does a
400
+ * read-modify-write of session state around an await, and the adapter-side
401
+ * set*Breakpoints request replaces the whole list — concurrent mutations
402
+ * would silently drop each other's breakpoints on both sides.
403
+ */
404
+ #serializeBreakpointMutation<T>(session: DapSession, mutate: () => Promise<T>, signal?: AbortSignal): Promise<T> {
405
+ const run = session.breakpointMutationQueue.then(() => {
406
+ // A mutation can sit behind several queued 30s predecessors; honor a
407
+ // caller abort at dequeue instead of running a request nobody awaits.
408
+ if (signal?.aborted) throw signal.reason instanceof Error ? signal.reason : new Error("Aborted");
409
+ return mutate();
410
+ });
411
+ session.breakpointMutationQueue = run.then(
412
+ () => undefined,
413
+ () => undefined,
414
+ );
415
+ return run;
416
+ }
417
+
371
418
  async setBreakpoint(
372
419
  file: string,
373
420
  line: number,
@@ -376,99 +423,123 @@ export class DapSessionManager {
376
423
  timeoutMs: number = 30_000,
377
424
  ) {
378
425
  const session = this.#touchActiveSession();
379
- const sourcePath = normalizePath(file);
380
- const current = [...(session.breakpoints.get(sourcePath) ?? [])];
381
- const deduped = current.filter(entry => entry.line !== line);
382
- deduped.push({ verified: false, line, condition });
383
- deduped.sort((left, right) => left.line - right.line);
384
- const response = await this.#sendRequestWithConfig<{ breakpoints?: DapBreakpoint[] }>(
426
+ return this.#serializeBreakpointMutation(
385
427
  session,
386
- "setBreakpoints",
387
- {
388
- source: { path: sourcePath, name: path.basename(sourcePath) },
389
- breakpoints: deduped.map<DapSourceBreakpoint>(entry => ({
390
- line: entry.line,
391
- ...(entry.condition ? { condition: entry.condition } : {}),
392
- })),
428
+ async () => {
429
+ const sourcePath = normalizePath(file);
430
+ const current = [...(session.breakpoints.get(sourcePath) ?? [])];
431
+ const deduped = current.filter(entry => entry.line !== line);
432
+ deduped.push({ verified: false, line, condition });
433
+ deduped.sort((left, right) => left.line - right.line);
434
+ const response = await this.#sendRequestWithConfig<{ breakpoints?: DapBreakpoint[] }>(
435
+ session,
436
+ "setBreakpoints",
437
+ {
438
+ source: { path: sourcePath, name: path.basename(sourcePath) },
439
+ breakpoints: deduped.map<DapSourceBreakpoint>(entry => ({
440
+ line: entry.line,
441
+ ...(entry.condition ? { condition: entry.condition } : {}),
442
+ })),
443
+ },
444
+ signal,
445
+ timeoutMs,
446
+ );
447
+ session.breakpoints.set(sourcePath, this.#mapSourceBreakpoints(deduped, response?.breakpoints));
448
+ return {
449
+ snapshot: buildSummary(session),
450
+ breakpoints: session.breakpoints.get(sourcePath) ?? [],
451
+ sourcePath,
452
+ };
393
453
  },
394
454
  signal,
395
- timeoutMs,
396
455
  );
397
- session.breakpoints.set(sourcePath, this.#mapSourceBreakpoints(deduped, response?.breakpoints));
398
- return {
399
- snapshot: buildSummary(session),
400
- breakpoints: session.breakpoints.get(sourcePath) ?? [],
401
- sourcePath,
402
- };
403
456
  }
404
457
 
405
458
  async removeBreakpoint(file: string, line: number, signal?: AbortSignal, timeoutMs: number = 30_000) {
406
459
  const session = this.#touchActiveSession();
407
- const sourcePath = normalizePath(file);
408
- const current = [...(session.breakpoints.get(sourcePath) ?? [])].filter(entry => entry.line !== line);
409
- const response = await this.#sendRequestWithConfig<{ breakpoints?: DapBreakpoint[] }>(
460
+ return this.#serializeBreakpointMutation(
410
461
  session,
411
- "setBreakpoints",
412
- {
413
- source: { path: sourcePath, name: path.basename(sourcePath) },
414
- breakpoints: current.map<DapSourceBreakpoint>(entry => ({
415
- line: entry.line,
416
- ...(entry.condition ? { condition: entry.condition } : {}),
417
- })),
462
+ async () => {
463
+ const sourcePath = normalizePath(file);
464
+ const current = [...(session.breakpoints.get(sourcePath) ?? [])].filter(entry => entry.line !== line);
465
+ const response = await this.#sendRequestWithConfig<{ breakpoints?: DapBreakpoint[] }>(
466
+ session,
467
+ "setBreakpoints",
468
+ {
469
+ source: { path: sourcePath, name: path.basename(sourcePath) },
470
+ breakpoints: current.map<DapSourceBreakpoint>(entry => ({
471
+ line: entry.line,
472
+ ...(entry.condition ? { condition: entry.condition } : {}),
473
+ })),
474
+ },
475
+ signal,
476
+ timeoutMs,
477
+ );
478
+ if (current.length === 0) {
479
+ session.breakpoints.delete(sourcePath);
480
+ } else {
481
+ session.breakpoints.set(sourcePath, this.#mapSourceBreakpoints(current, response?.breakpoints));
482
+ }
483
+ return {
484
+ snapshot: buildSummary(session),
485
+ breakpoints: session.breakpoints.get(sourcePath) ?? [],
486
+ sourcePath,
487
+ };
418
488
  },
419
489
  signal,
420
- timeoutMs,
421
490
  );
422
- if (current.length === 0) {
423
- session.breakpoints.delete(sourcePath);
424
- } else {
425
- session.breakpoints.set(sourcePath, this.#mapSourceBreakpoints(current, response?.breakpoints));
426
- }
427
- return {
428
- snapshot: buildSummary(session),
429
- breakpoints: session.breakpoints.get(sourcePath) ?? [],
430
- sourcePath,
431
- };
432
491
  }
433
492
 
434
493
  async setFunctionBreakpoint(name: string, condition?: string, signal?: AbortSignal, timeoutMs: number = 30_000) {
435
494
  const session = this.#touchActiveSession();
436
- const current = session.functionBreakpoints.filter(entry => entry.name !== name);
437
- current.push({ verified: false, name, condition });
438
- current.sort((left, right) => left.name.localeCompare(right.name));
439
- const response = await this.#sendRequestWithConfig<{ breakpoints?: DapBreakpoint[] }>(
495
+ return this.#serializeBreakpointMutation(
440
496
  session,
441
- "setFunctionBreakpoints",
442
- {
443
- breakpoints: current.map<DapFunctionBreakpoint>(entry => ({
444
- name: entry.name,
445
- ...(entry.condition ? { condition: entry.condition } : {}),
446
- })),
497
+ async () => {
498
+ const current = session.functionBreakpoints.filter(entry => entry.name !== name);
499
+ current.push({ verified: false, name, condition });
500
+ current.sort((left, right) => left.name.localeCompare(right.name));
501
+ const response = await this.#sendRequestWithConfig<{ breakpoints?: DapBreakpoint[] }>(
502
+ session,
503
+ "setFunctionBreakpoints",
504
+ {
505
+ breakpoints: current.map<DapFunctionBreakpoint>(entry => ({
506
+ name: entry.name,
507
+ ...(entry.condition ? { condition: entry.condition } : {}),
508
+ })),
509
+ },
510
+ signal,
511
+ timeoutMs,
512
+ );
513
+ session.functionBreakpoints = this.#mapFunctionBreakpoints(current, response?.breakpoints);
514
+ return { snapshot: buildSummary(session), breakpoints: session.functionBreakpoints };
447
515
  },
448
516
  signal,
449
- timeoutMs,
450
517
  );
451
- session.functionBreakpoints = this.#mapFunctionBreakpoints(current, response?.breakpoints);
452
- return { snapshot: buildSummary(session), breakpoints: session.functionBreakpoints };
453
518
  }
454
519
 
455
520
  async removeFunctionBreakpoint(name: string, signal?: AbortSignal, timeoutMs: number = 30_000) {
456
521
  const session = this.#touchActiveSession();
457
- const current = session.functionBreakpoints.filter(entry => entry.name !== name);
458
- const response = await this.#sendRequestWithConfig<{ breakpoints?: DapBreakpoint[] }>(
522
+ return this.#serializeBreakpointMutation(
459
523
  session,
460
- "setFunctionBreakpoints",
461
- {
462
- breakpoints: current.map<DapFunctionBreakpoint>(entry => ({
463
- name: entry.name,
464
- ...(entry.condition ? { condition: entry.condition } : {}),
465
- })),
524
+ async () => {
525
+ const current = session.functionBreakpoints.filter(entry => entry.name !== name);
526
+ const response = await this.#sendRequestWithConfig<{ breakpoints?: DapBreakpoint[] }>(
527
+ session,
528
+ "setFunctionBreakpoints",
529
+ {
530
+ breakpoints: current.map<DapFunctionBreakpoint>(entry => ({
531
+ name: entry.name,
532
+ ...(entry.condition ? { condition: entry.condition } : {}),
533
+ })),
534
+ },
535
+ signal,
536
+ timeoutMs,
537
+ );
538
+ session.functionBreakpoints = this.#mapFunctionBreakpoints(current, response?.breakpoints);
539
+ return { snapshot: buildSummary(session), breakpoints: session.functionBreakpoints };
466
540
  },
467
541
  signal,
468
- timeoutMs,
469
542
  );
470
- session.functionBreakpoints = this.#mapFunctionBreakpoints(current, response?.breakpoints);
471
- return { snapshot: buildSummary(session), breakpoints: session.functionBreakpoints };
472
543
  }
473
544
 
474
545
  async setInstructionBreakpoint(
@@ -480,31 +551,37 @@ export class DapSessionManager {
480
551
  timeoutMs: number = 30_000,
481
552
  ) {
482
553
  const session = this.#touchActiveSession();
483
- const current = session.instructionBreakpoints.filter(
484
- entry => entry.instructionReference !== instructionReference || entry.offset !== offset,
485
- );
486
- current.push({ instructionReference, offset, condition, hitCondition });
487
- current.sort((left, right) => {
488
- const referenceOrder = left.instructionReference.localeCompare(right.instructionReference);
489
- if (referenceOrder !== 0) {
490
- return referenceOrder;
491
- }
492
- return (left.offset ?? 0) - (right.offset ?? 0);
493
- });
494
- const response = await this.#sendRequestWithConfig<{ breakpoints?: DapBreakpoint[] }>(
554
+ return this.#serializeBreakpointMutation(
495
555
  session,
496
- "setInstructionBreakpoints",
497
- {
498
- breakpoints: current,
499
- } satisfies DapSetInstructionBreakpointsArguments,
556
+ async () => {
557
+ const current = session.instructionBreakpoints.filter(
558
+ entry => entry.instructionReference !== instructionReference || entry.offset !== offset,
559
+ );
560
+ current.push({ instructionReference, offset, condition, hitCondition });
561
+ current.sort((left, right) => {
562
+ const referenceOrder = left.instructionReference.localeCompare(right.instructionReference);
563
+ if (referenceOrder !== 0) {
564
+ return referenceOrder;
565
+ }
566
+ return (left.offset ?? 0) - (right.offset ?? 0);
567
+ });
568
+ const response = await this.#sendRequestWithConfig<{ breakpoints?: DapBreakpoint[] }>(
569
+ session,
570
+ "setInstructionBreakpoints",
571
+ {
572
+ breakpoints: current,
573
+ } satisfies DapSetInstructionBreakpointsArguments,
574
+ signal,
575
+ timeoutMs,
576
+ );
577
+ session.instructionBreakpoints = current;
578
+ return {
579
+ snapshot: buildSummary(session),
580
+ breakpoints: this.#mapInstructionBreakpoints(current, response?.breakpoints),
581
+ };
582
+ },
500
583
  signal,
501
- timeoutMs,
502
584
  );
503
- session.instructionBreakpoints = current;
504
- return {
505
- snapshot: buildSummary(session),
506
- breakpoints: this.#mapInstructionBreakpoints(current, response?.breakpoints),
507
- };
508
585
  }
509
586
 
510
587
  async removeInstructionBreakpoint(
@@ -514,29 +591,35 @@ export class DapSessionManager {
514
591
  timeoutMs: number = 30_000,
515
592
  ) {
516
593
  const session = this.#touchActiveSession();
517
- const current = session.instructionBreakpoints.filter(entry => {
518
- if (entry.instructionReference !== instructionReference) {
519
- return true;
520
- }
521
- if (offset === undefined) {
522
- return false;
523
- }
524
- return entry.offset !== offset;
525
- });
526
- const response = await this.#sendRequestWithConfig<{ breakpoints?: DapBreakpoint[] }>(
594
+ return this.#serializeBreakpointMutation(
527
595
  session,
528
- "setInstructionBreakpoints",
529
- {
530
- breakpoints: current,
531
- } satisfies DapSetInstructionBreakpointsArguments,
596
+ async () => {
597
+ const current = session.instructionBreakpoints.filter(entry => {
598
+ if (entry.instructionReference !== instructionReference) {
599
+ return true;
600
+ }
601
+ if (offset === undefined) {
602
+ return false;
603
+ }
604
+ return entry.offset !== offset;
605
+ });
606
+ const response = await this.#sendRequestWithConfig<{ breakpoints?: DapBreakpoint[] }>(
607
+ session,
608
+ "setInstructionBreakpoints",
609
+ {
610
+ breakpoints: current,
611
+ } satisfies DapSetInstructionBreakpointsArguments,
612
+ signal,
613
+ timeoutMs,
614
+ );
615
+ session.instructionBreakpoints = current;
616
+ return {
617
+ snapshot: buildSummary(session),
618
+ breakpoints: this.#mapInstructionBreakpoints(current, response?.breakpoints),
619
+ };
620
+ },
532
621
  signal,
533
- timeoutMs,
534
622
  );
535
- session.instructionBreakpoints = current;
536
- return {
537
- snapshot: buildSummary(session),
538
- breakpoints: this.#mapInstructionBreakpoints(current, response?.breakpoints),
539
- };
540
623
  }
541
624
 
542
625
  async dataBreakpointInfo(
@@ -570,42 +653,54 @@ export class DapSessionManager {
570
653
  timeoutMs: number = 30_000,
571
654
  ) {
572
655
  const session = this.#touchActiveSession();
573
- const current = session.dataBreakpoints.filter(entry => entry.dataId !== dataId);
574
- current.push({ dataId, accessType, condition, hitCondition });
575
- current.sort((left, right) => left.dataId.localeCompare(right.dataId));
576
- const response = await this.#sendRequestWithConfig<{ breakpoints?: DapBreakpoint[] }>(
656
+ return this.#serializeBreakpointMutation(
577
657
  session,
578
- "setDataBreakpoints",
579
- {
580
- breakpoints: current,
581
- } satisfies DapSetDataBreakpointsArguments,
658
+ async () => {
659
+ const current = session.dataBreakpoints.filter(entry => entry.dataId !== dataId);
660
+ current.push({ dataId, accessType, condition, hitCondition });
661
+ current.sort((left, right) => left.dataId.localeCompare(right.dataId));
662
+ const response = await this.#sendRequestWithConfig<{ breakpoints?: DapBreakpoint[] }>(
663
+ session,
664
+ "setDataBreakpoints",
665
+ {
666
+ breakpoints: current,
667
+ } satisfies DapSetDataBreakpointsArguments,
668
+ signal,
669
+ timeoutMs,
670
+ );
671
+ session.dataBreakpoints = current;
672
+ return {
673
+ snapshot: buildSummary(session),
674
+ breakpoints: this.#mapDataBreakpoints(current, response?.breakpoints),
675
+ };
676
+ },
582
677
  signal,
583
- timeoutMs,
584
678
  );
585
- session.dataBreakpoints = current;
586
- return {
587
- snapshot: buildSummary(session),
588
- breakpoints: this.#mapDataBreakpoints(current, response?.breakpoints),
589
- };
590
679
  }
591
680
 
592
681
  async removeDataBreakpoint(dataId: string, signal?: AbortSignal, timeoutMs: number = 30_000) {
593
682
  const session = this.#touchActiveSession();
594
- const current = session.dataBreakpoints.filter(entry => entry.dataId !== dataId);
595
- const response = await this.#sendRequestWithConfig<{ breakpoints?: DapBreakpoint[] }>(
683
+ return this.#serializeBreakpointMutation(
596
684
  session,
597
- "setDataBreakpoints",
598
- {
599
- breakpoints: current,
600
- } satisfies DapSetDataBreakpointsArguments,
685
+ async () => {
686
+ const current = session.dataBreakpoints.filter(entry => entry.dataId !== dataId);
687
+ const response = await this.#sendRequestWithConfig<{ breakpoints?: DapBreakpoint[] }>(
688
+ session,
689
+ "setDataBreakpoints",
690
+ {
691
+ breakpoints: current,
692
+ } satisfies DapSetDataBreakpointsArguments,
693
+ signal,
694
+ timeoutMs,
695
+ );
696
+ session.dataBreakpoints = current;
697
+ return {
698
+ snapshot: buildSummary(session),
699
+ breakpoints: this.#mapDataBreakpoints(current, response?.breakpoints),
700
+ };
701
+ },
601
702
  signal,
602
- timeoutMs,
603
703
  );
604
- session.dataBreakpoints = current;
605
- return {
606
- snapshot: buildSummary(session),
607
- breakpoints: this.#mapDataBreakpoints(current, response?.breakpoints),
608
- };
609
704
  }
610
705
 
611
706
  async disassemble(
@@ -756,21 +851,25 @@ export class DapSessionManager {
756
851
 
757
852
  async pause(signal?: AbortSignal, timeoutMs: number = 30_000): Promise<DapSessionSummary> {
758
853
  const session = this.#touchActiveSession();
759
- if (session.status === "stopped") {
854
+ // status is mutated by the event reader between awaits; check through a
855
+ // closure so TS does not carry stale narrowing from the early return.
856
+ const isStopped = () => session.status === "stopped";
857
+ if (isStopped()) {
760
858
  return buildSummary(session);
761
859
  }
762
860
  const threadId = await this.#resolveThreadId(session, signal, timeoutMs);
861
+ // Subscribe BEFORE sending pause: the stopped event can arrive in the
862
+ // same chunk as the response and would otherwise be dispatched before
863
+ // the waiter subscribes, burning the whole timeout.
864
+ const stoppedPromise = session.client.waitForEvent<DapStoppedEventBody>("stopped", undefined, signal, timeoutMs);
865
+ stoppedPromise.catch(() => {});
763
866
  await this.#sendRequestWithConfig(session, "pause", { threadId } satisfies DapPauseArguments, signal, timeoutMs);
764
- // The stopped event may already have been processed by #handleStoppedEvent
765
- // between the request and here. Wait for it, but tolerate timeout if the
766
- // session already transitioned.
767
- try {
768
- await untilAborted(
769
- signal,
770
- session.client.waitForEvent<DapStoppedEventBody>("stopped", undefined, signal, timeoutMs),
771
- );
772
- } catch {
773
- // Timeout or abort — report current state regardless
867
+ if (!isStopped()) {
868
+ try {
869
+ await untilAborted(signal, stoppedPromise);
870
+ } catch {
871
+ // Timeout or abort — report current state regardless
872
+ }
774
873
  }
775
874
  return buildSummary(session);
776
875
  }
@@ -884,16 +983,16 @@ export class DapSessionManager {
884
983
 
885
984
  getOutput(limitBytes?: number): DapOutputSnapshot {
886
985
  const session = this.#touchActiveSession();
887
- if (!limitBytes || limitBytes <= 0 || Buffer.byteLength(session.output, "utf-8") <= limitBytes) {
888
- return { snapshot: buildSummary(session), output: session.output };
986
+ const output = session.outputChunks.join("");
987
+ if (!limitBytes || limitBytes <= 0 || session.outputBufferedBytes <= limitBytes) {
988
+ return { snapshot: buildSummary(session), output };
889
989
  }
890
- let sliceStart = session.output.length;
891
- let remaining = limitBytes;
892
- while (sliceStart > 0 && remaining > 0) {
893
- sliceStart -= 1;
894
- remaining -= Buffer.byteLength(session.output[sliceStart] ?? "", "utf-8");
990
+ // Byte-slice the tail once; a torn code point at the cut decodes as U+FFFD.
991
+ const buffer = Buffer.from(output, "utf-8");
992
+ if (buffer.length <= limitBytes) {
993
+ return { snapshot: buildSummary(session), output };
895
994
  }
896
- return { snapshot: buildSummary(session), output: session.output.slice(sliceStart) };
995
+ return { snapshot: buildSummary(session), output: buffer.subarray(buffer.length - limitBytes).toString("utf-8") };
897
996
  }
898
997
 
899
998
  async terminate(signal?: AbortSignal, timeoutMs: number = 30_000): Promise<DapSessionSummary | null> {
@@ -973,8 +1072,10 @@ export class DapSessionManager {
973
1072
  functionBreakpoints: [],
974
1073
  instructionBreakpoints: [],
975
1074
  dataBreakpoints: [],
976
- output: "",
1075
+ breakpointMutationQueue: Promise.resolve(),
1076
+ outputChunks: [],
977
1077
  outputBytes: 0,
1078
+ outputBufferedBytes: 0,
978
1079
  outputTruncated: false,
979
1080
  stop: {},
980
1081
  threads: [],
@@ -602,7 +602,7 @@ export class DebugLogViewerComponent implements Component {
602
602
  // no cached child state
603
603
  }
604
604
 
605
- render(width: number): string[] {
605
+ render(width: number): readonly string[] {
606
606
  this.#lastRenderWidth = Math.max(20, width);
607
607
  this.#ensureCursorVisible();
608
608
 
@@ -147,7 +147,7 @@ export class RawSseViewerComponent implements Component {
147
147
 
148
148
  invalidate(): void {}
149
149
 
150
- render(width: number): string[] {
150
+ render(width: number): readonly string[] {
151
151
  this.#lastRenderWidth = Math.max(MIN_VIEWER_WIDTH, width);
152
152
  this.#followIfNeeded();
153
153
 
package/src/edit/diff.ts CHANGED
@@ -74,6 +74,49 @@ function isDiffChangeRow(row: string | undefined): boolean {
74
74
  return row !== undefined && (row.startsWith("+") || row.startsWith("-"));
75
75
  }
76
76
 
77
+ /** Blank row separating non-contiguous regions of a numbered diff. */
78
+ const DIFF_GAP_ROW = "";
79
+
80
+ /** Old-file line number of a source-visible row (`-` or context); `+`/gap/other rows yield undefined. */
81
+ function parseSourceRowLineNumber(row: string): number | undefined {
82
+ const parsed = parseNumberedDiffRow(row);
83
+ return parsed === undefined || parsed.prefix === "+" ? undefined : parsed.lineNumber;
84
+ }
85
+
86
+ /**
87
+ * Drop gap rows that no longer separate anything. Context rows are inserted
88
+ * one at a time, each adding its own gap rows from a snapshot of the diff, so
89
+ * the raw result can contain adjacent gap rows, gap rows whose neighbors
90
+ * became contiguous after a later insert filled the hole, and gap rows at the
91
+ * diff edges. The sweep keeps a gap row only when it sits between two
92
+ * source-numbered rows (old-file coordinates — the same numbering the
93
+ * insertion gap test uses) that are actually non-contiguous, and never keeps
94
+ * two in a row.
95
+ */
96
+ function normalizeDiffGapRows(rows: string[]): void {
97
+ const kept: string[] = [];
98
+ for (let i = 0; i < rows.length; i++) {
99
+ const row = rows[i];
100
+ if (row !== DIFF_GAP_ROW) {
101
+ kept.push(row);
102
+ continue;
103
+ }
104
+ if (kept.length === 0 || kept[kept.length - 1] === DIFF_GAP_ROW) continue;
105
+ let before: number | undefined;
106
+ for (let j = kept.length - 1; j >= 0 && before === undefined; j--) {
107
+ before = parseSourceRowLineNumber(kept[j]);
108
+ }
109
+ let after: number | undefined;
110
+ for (let j = i + 1; j < rows.length && after === undefined; j++) {
111
+ if (rows[j] === DIFF_GAP_ROW) continue;
112
+ after = parseSourceRowLineNumber(rows[j]);
113
+ }
114
+ if (before === undefined || after === undefined || after <= before + 1) continue;
115
+ kept.push(row);
116
+ }
117
+ if (kept.length !== rows.length) rows.splice(0, rows.length, ...kept);
118
+ }
119
+
77
120
  function adjustedContextInsertIndex(rows: readonly string[], index: number): number {
78
121
  let start = index;
79
122
  while (start > 0 && isDiffChangeRow(rows[start - 1])) start--;
@@ -108,13 +151,13 @@ function insertBracketContextRows(
108
151
  }
109
152
 
110
153
  const chunk: string[] = [];
111
- if (previousSourceLine !== undefined && lineNumber > previousSourceLine + 1) chunk.push("...");
154
+ if (previousSourceLine !== undefined && lineNumber > previousSourceLine + 1) chunk.push(DIFF_GAP_ROW);
112
155
  chunk.push(row);
113
- if (nextSourceLine !== undefined && nextSourceLine > lineNumber + 1) chunk.push("...");
156
+ if (nextSourceLine !== undefined && nextSourceLine > lineNumber + 1) chunk.push(DIFF_GAP_ROW);
114
157
 
115
158
  const adjustedIndex = adjustedContextInsertIndex(rows, insertIndex);
116
159
  rows.splice(adjustedIndex, 0, ...chunk);
117
- for (const inserted of chunk) seenRows.add(inserted);
160
+ seenRows.add(row);
118
161
  }
119
162
  }
120
163
 
@@ -179,6 +222,7 @@ function addMatchingBracketContextRows(
179
222
  if (!contextRows.has(oldLineNumber)) contextRows.set(oldLineNumber, text);
180
223
  }
181
224
  insertBracketContextRows(rows, contextRows, seenRows);
225
+ normalizeDiffGapRows(rows);
182
226
  }
183
227
 
184
228
  /**
@@ -8,7 +8,26 @@
8
8
  import type { BlockResolver } from "@oh-my-pi/hashline";
9
9
  import { blockRangeAt } from "@oh-my-pi/pi-natives";
10
10
 
11
+ /**
12
+ * `blockRangeAt` runs a full synchronous tree-sitter parse of `text` per
13
+ * call, and streaming previews re-resolve the same (text, line) every
14
+ * streamed chunk. Memoize by content: identical text + line always yields the
15
+ * same span. FIFO-bounded; hashing the text is orders of magnitude cheaper
16
+ * than re-parsing it.
17
+ */
18
+ const resolutionCache = new Map<string, { start: number; end: number } | null>();
19
+ const RESOLUTION_CACHE_MAX = 512;
20
+
11
21
  export const nativeBlockResolver: BlockResolver = ({ path, text, line }) => {
22
+ const key = `${Bun.hash(text).toString(36)}:${text.length}:${line}:${path}`;
23
+ const cached = resolutionCache.get(key);
24
+ if (cached !== undefined) return cached;
12
25
  const range = blockRangeAt({ code: text, path, line });
13
- return range ? { start: range.startLine, end: range.endLine } : null;
26
+ const result = range ? { start: range.startLine, end: range.endLine } : null;
27
+ if (resolutionCache.size >= RESOLUTION_CACHE_MAX) {
28
+ const oldest = resolutionCache.keys().next().value;
29
+ if (oldest !== undefined) resolutionCache.delete(oldest);
30
+ }
31
+ resolutionCache.set(key, result);
32
+ return result;
14
33
  };