@gajae-code/coding-agent 0.11.11 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/CHANGELOG.md +131 -4
  2. package/dist/types/async/job-manager.d.ts +17 -3
  3. package/dist/types/cli/read-cli.d.ts +1 -0
  4. package/dist/types/commands/deep-interview.d.ts +9 -0
  5. package/dist/types/commands/harness.d.ts +22 -0
  6. package/dist/types/commands/read.d.ts +6 -0
  7. package/dist/types/config/model-registry.d.ts +4 -0
  8. package/dist/types/config/provider-auth-health.d.ts +14 -0
  9. package/dist/types/config/provider-ranking.d.ts +59 -0
  10. package/dist/types/config/settings-schema.d.ts +83 -1
  11. package/dist/types/config/settings.d.ts +2 -0
  12. package/dist/types/exa/mcp-client.d.ts +10 -1
  13. package/dist/types/exec/bash-executor.d.ts +45 -12
  14. package/dist/types/extensibility/extensions/types.d.ts +38 -4
  15. package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
  16. package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
  17. package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
  18. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
  19. package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
  20. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
  21. package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
  22. package/dist/types/extensibility/shared-events.d.ts +6 -0
  23. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  24. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  25. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  26. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  27. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  28. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  29. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  30. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +70 -1
  31. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  32. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  33. package/dist/types/gjc-runtime/team-runtime.d.ts +27 -1
  34. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  35. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
  36. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  37. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +17 -0
  38. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  39. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
  40. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  41. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  42. package/dist/types/main.d.ts +1 -1
  43. package/dist/types/memories/index.d.ts +2 -0
  44. package/dist/types/modes/acp/acp-agent.d.ts +10 -19
  45. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  46. package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
  47. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  48. package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
  49. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  50. package/dist/types/modes/components/session-observer-overlay.d.ts +22 -0
  51. package/dist/types/modes/components/settings-selector.d.ts +11 -0
  52. package/dist/types/modes/components/status-line/gh.d.ts +5 -0
  53. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  54. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
  55. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  56. package/dist/types/modes/interactive-mode.d.ts +2 -0
  57. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  58. package/dist/types/modes/types.d.ts +2 -0
  59. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  60. package/dist/types/reminders/star-reminder.d.ts +3 -13
  61. package/dist/types/runtime-mcp/manager.d.ts +1 -0
  62. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  63. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  64. package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
  65. package/dist/types/sdk/broker/transport.d.ts +2 -0
  66. package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
  67. package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
  68. package/dist/types/sdk/bus/config.d.ts +6 -2
  69. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  70. package/dist/types/sdk/bus/index.d.ts +44 -1
  71. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +44 -0
  72. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  73. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  74. package/dist/types/sdk/bus/reconciliation-store.d.ts +69 -0
  75. package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
  76. package/dist/types/sdk/bus/rich-render.d.ts +2 -0
  77. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
  78. package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
  79. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
  80. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
  81. package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
  82. package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
  83. package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
  84. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  85. package/dist/types/sdk/host/host.d.ts +2 -0
  86. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  87. package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
  88. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  89. package/dist/types/sdk/prompt-status.d.ts +23 -3
  90. package/dist/types/sdk/session.d.ts +6 -0
  91. package/dist/types/session/agent-session.d.ts +82 -12
  92. package/dist/types/session/artifacts.d.ts +8 -0
  93. package/dist/types/session/blob-store.d.ts +57 -4
  94. package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
  95. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  96. package/dist/types/session/session-manager.d.ts +48 -19
  97. package/dist/types/session/session-storage.d.ts +4 -0
  98. package/dist/types/session/streaming-output.d.ts +96 -12
  99. package/dist/types/setup/credential-import.d.ts +12 -0
  100. package/dist/types/skill-state/active-state.d.ts +12 -4
  101. package/dist/types/skill-state/workflow-hud.d.ts +9 -0
  102. package/dist/types/task/executor.d.ts +3 -2
  103. package/dist/types/task/index.d.ts +3 -1
  104. package/dist/types/task/provider-retry-status.d.ts +8 -0
  105. package/dist/types/task/receipt.d.ts +7 -1
  106. package/dist/types/task/types.d.ts +23 -0
  107. package/dist/types/tools/ask-answer-registry.d.ts +2 -2
  108. package/dist/types/tools/bash-interactive.d.ts +4 -1
  109. package/dist/types/tools/bash.d.ts +3 -1
  110. package/dist/types/tools/browser/launch.d.ts +7 -0
  111. package/dist/types/tools/fetch.d.ts +17 -1
  112. package/dist/types/tools/image-gen.d.ts +4 -0
  113. package/dist/types/tools/index.d.ts +4 -0
  114. package/dist/types/tools/output-meta.d.ts +44 -3
  115. package/dist/types/tools/path-utils.d.ts +1 -0
  116. package/dist/types/tools/read-internals.d.ts +12 -0
  117. package/dist/types/tools/read.d.ts +45 -7
  118. package/dist/types/tools/resource-gc.d.ts +4 -0
  119. package/dist/types/tools/review.d.ts +5 -7
  120. package/dist/types/tools/subagent.d.ts +2 -0
  121. package/dist/types/tools/tool-result.d.ts +7 -1
  122. package/dist/types/utils/gh.d.ts +13 -0
  123. package/dist/types/utils/image-resize.d.ts +1 -0
  124. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  125. package/dist/types/web/search/providers/searxng.d.ts +7 -0
  126. package/dist/types/web/search/providers/xai.d.ts +2 -0
  127. package/package.json +11 -13
  128. package/scripts/acp-conformance-agent.ts +218 -0
  129. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  130. package/scripts/capture-gjc-bundle-settings.ts +306 -0
  131. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  132. package/scripts/generate-sdk-operation-inventory.ts +5 -0
  133. package/scripts/resident-memory-bench.ts +1341 -0
  134. package/scripts/run-acp-conformance.ts +285 -0
  135. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  136. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  137. package/src/async/job-manager.ts +78 -20
  138. package/src/capability/index.ts +8 -0
  139. package/src/cli/fast-help.ts +1 -1
  140. package/src/cli/plugin-cli.ts +176 -18
  141. package/src/cli/read-cli.ts +2 -1
  142. package/src/cli.ts +1 -0
  143. package/src/commands/deep-interview.ts +29 -9
  144. package/src/commands/harness.ts +35 -12
  145. package/src/commands/read.ts +11 -2
  146. package/src/commands/team.ts +20 -2
  147. package/src/config/file-lock.ts +11 -2
  148. package/src/config/model-registry.ts +21 -2
  149. package/src/config/provider-auth-health.ts +42 -0
  150. package/src/config/provider-ranking.ts +119 -0
  151. package/src/config/settings-schema.ts +74 -1
  152. package/src/config/settings.ts +246 -108
  153. package/src/coordinator-mcp/question-gate-codec.ts +1 -1
  154. package/src/coordinator-mcp/server.ts +5 -12
  155. package/src/defaults/gjc/skills/deep-interview/SKILL.md +230 -82
  156. package/src/defaults/gjc/skills/ralplan/SKILL.md +77 -41
  157. package/src/defaults/gjc/skills/team/SKILL.md +3 -1
  158. package/src/defaults/gjc/skills/ultragoal/SKILL.md +62 -39
  159. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
  160. package/src/defaults/gjc-defaults.ts +4 -4
  161. package/src/eval/py/executor.ts +7 -2
  162. package/src/exa/mcp-client.ts +12 -3
  163. package/src/exec/bash-executor.ts +144 -27
  164. package/src/extensibility/extensions/runner.ts +15 -0
  165. package/src/extensibility/extensions/types.ts +46 -5
  166. package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
  167. package/src/extensibility/gjc-plugins/index.ts +19 -2
  168. package/src/extensibility/gjc-plugins/installer.ts +189 -50
  169. package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
  170. package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
  171. package/src/extensibility/gjc-plugins/observability.ts +3 -3
  172. package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
  173. package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
  174. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
  175. package/src/extensibility/gjc-plugins/schema.ts +84 -12
  176. package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
  177. package/src/extensibility/gjc-plugins/types.ts +141 -0
  178. package/src/extensibility/gjc-plugins/validation.ts +9 -0
  179. package/src/extensibility/shared-events.ts +5 -0
  180. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
  181. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  182. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  183. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  184. package/src/gjc-runtime/deep-interview-state.ts +12 -943
  185. package/src/gjc-runtime/gc-render.ts +1 -0
  186. package/src/gjc-runtime/gc-runtime.ts +11 -1
  187. package/src/gjc-runtime/launch-tmux.ts +25 -6
  188. package/src/gjc-runtime/managed-owner-admission.ts +1 -1
  189. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  190. package/src/gjc-runtime/ralplan-runtime.ts +1149 -98
  191. package/src/gjc-runtime/state-runtime.ts +2 -1
  192. package/src/gjc-runtime/state-writer.ts +70 -222
  193. package/src/gjc-runtime/team-launch.ts +42 -5
  194. package/src/gjc-runtime/team-runtime.ts +1085 -40
  195. package/src/gjc-runtime/team-store.ts +32 -0
  196. package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
  197. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  198. package/src/gjc-runtime/team-workers.ts +1 -0
  199. package/src/gjc-runtime/tmux-owner-isolation.ts +28 -0
  200. package/src/gjc-runtime/tmux-sessions.ts +58 -13
  201. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  202. package/src/gjc-runtime/ultragoal-runtime.ts +414 -618
  203. package/src/gjc-runtime/workflow-manifest.generated.json +105 -449
  204. package/src/gjc-runtime/workflow-manifest.ts +38 -247
  205. package/src/hooks/native-skill-hook.ts +17 -4
  206. package/src/hooks/skill-state.ts +1 -23
  207. package/src/internal-urls/docs-index.generated.ts +21 -22
  208. package/src/internal-urls/local-root-gc.ts +127 -0
  209. package/src/main.ts +9 -5
  210. package/src/memories/index.ts +10 -0
  211. package/src/modes/DESIGN.md +98 -0
  212. package/src/modes/acp/acp-agent.ts +339 -191
  213. package/src/modes/acp/acp-event-mapper.ts +14 -7
  214. package/src/modes/acp/acp-mode.ts +39 -3
  215. package/src/modes/components/assistant-message.ts +123 -22
  216. package/src/modes/components/gjc-bundle-settings.ts +508 -0
  217. package/src/modes/components/irc-sidebar.ts +176 -62
  218. package/src/modes/components/model-selector.ts +61 -4
  219. package/src/modes/components/notifications-settings-editor.ts +12 -1
  220. package/src/modes/components/oauth-selector.ts +96 -28
  221. package/src/modes/components/read-tool-group.ts +82 -14
  222. package/src/modes/components/session-observer-overlay.ts +457 -83
  223. package/src/modes/components/settings-selector.ts +116 -23
  224. package/src/modes/components/status-line/gh.ts +16 -0
  225. package/src/modes/components/tool-execution.ts +138 -27
  226. package/src/modes/components/tool-status-header.ts +3 -14
  227. package/src/modes/components/transcript-viewer-overlay.ts +99 -24
  228. package/src/modes/controllers/command-controller.ts +4 -0
  229. package/src/modes/controllers/event-controller.ts +109 -27
  230. package/src/modes/controllers/extension-ui-controller.ts +26 -2
  231. package/src/modes/controllers/input-controller.ts +18 -3
  232. package/src/modes/controllers/selector-controller.ts +127 -36
  233. package/src/modes/interactive-mode.ts +62 -3
  234. package/src/modes/irc-observation-ledger.ts +71 -15
  235. package/src/modes/runtime-init.ts +22 -2
  236. package/src/modes/types.ts +2 -0
  237. package/src/modes/utils/ui-helpers.ts +94 -1
  238. package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
  239. package/src/prompts/agents/architect.md +9 -1
  240. package/src/prompts/agents/critic.md +9 -0
  241. package/src/prompts/system/eager-todo.md +8 -4
  242. package/src/prompts/system/system-prompt.md +11 -5
  243. package/src/prompts/tools/bash.md +5 -5
  244. package/src/prompts/tools/read.md +4 -2
  245. package/src/prompts/tools/search-tool-bm25.md +5 -0
  246. package/src/prompts/tools/task.md +1 -1
  247. package/src/reminders/star-reminder.ts +4 -42
  248. package/src/runtime-mcp/manager.ts +19 -4
  249. package/src/runtime-mcp/smithery-auth.ts +17 -2
  250. package/src/runtime-mcp/smithery-connect.ts +11 -1
  251. package/src/sdk/acp/adapter.ts +22 -6
  252. package/src/sdk/broker/lifecycle.ts +25 -6
  253. package/src/sdk/broker/transport.ts +35 -3
  254. package/src/sdk/bus/attachment-registry.ts +5 -1
  255. package/src/sdk/bus/chat-daemon-cli.ts +4 -39
  256. package/src/sdk/bus/chat-daemon-control.ts +6 -3
  257. package/src/sdk/bus/config.ts +18 -2
  258. package/src/sdk/bus/control-drain-lease.ts +104 -0
  259. package/src/sdk/bus/index.ts +984 -146
  260. package/src/sdk/bus/kind-aware-reconciliation.ts +317 -0
  261. package/src/sdk/bus/lifecycle-commands.ts +4 -1
  262. package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
  263. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  264. package/src/sdk/bus/reconciliation-store.ts +303 -0
  265. package/src/sdk/bus/rich-draft.ts +1 -1
  266. package/src/sdk/bus/rich-render.ts +2 -2
  267. package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
  268. package/src/sdk/bus/telegram-cli.ts +33 -8
  269. package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
  270. package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
  271. package/src/sdk/bus/telegram-daemon.ts +916 -23
  272. package/src/sdk/bus/telegram-reference.ts +40 -3
  273. package/src/sdk/bus/threaded-render.ts +48 -3
  274. package/src/sdk/bus/topic-registry.ts +20 -0
  275. package/src/sdk/host/control/dispatch.ts +5 -1
  276. package/src/sdk/host/control/operations.ts +1 -1
  277. package/src/sdk/host/host.ts +23 -1
  278. package/src/sdk/host/query/handlers.ts +55 -0
  279. package/src/sdk/host/query/revision-store.ts +36 -2
  280. package/src/sdk/host/reverse-leases.ts +44 -7
  281. package/src/sdk/mcp/server.ts +46 -18
  282. package/src/sdk/prompt-status.ts +16 -3
  283. package/src/sdk/protocol/operation-inventory.generated.json +51 -0
  284. package/src/sdk/protocol/operation-registry.ts +7 -2
  285. package/src/sdk/session.ts +101 -10
  286. package/src/session/agent-session.ts +1286 -220
  287. package/src/session/artifacts.ts +22 -0
  288. package/src/session/auth-broker-config.ts +10 -3
  289. package/src/session/blob-store.ts +915 -24
  290. package/src/session/internal/managed-session-scope.ts +44 -6
  291. package/src/session/internal/managed-session-storage.ts +48 -6
  292. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  293. package/src/session/session-manager.ts +2364 -438
  294. package/src/session/session-storage.ts +9 -1
  295. package/src/session/streaming-output.ts +500 -111
  296. package/src/setup/credential-import.ts +75 -13
  297. package/src/setup/provider-onboarding.ts +19 -4
  298. package/src/skill-state/active-state.ts +32 -10
  299. package/src/skill-state/workflow-hud.ts +42 -2
  300. package/src/skill-state/workflow-mutation-guard.ts +560 -8
  301. package/src/task/executor.ts +122 -39
  302. package/src/task/index.ts +23 -14
  303. package/src/task/provider-retry-status.ts +16 -0
  304. package/src/task/receipt.ts +23 -7
  305. package/src/task/render.ts +6 -2
  306. package/src/task/types.ts +108 -0
  307. package/src/tools/ask-answer-registry.ts +12 -6
  308. package/src/tools/ask.ts +169 -118
  309. package/src/tools/ast-edit.ts +1 -0
  310. package/src/tools/ast-grep.ts +1 -0
  311. package/src/tools/bash-interactive.ts +8 -3
  312. package/src/tools/bash.ts +503 -60
  313. package/src/tools/browser/launch.ts +41 -6
  314. package/src/tools/fetch.ts +114 -23
  315. package/src/tools/find.ts +1 -0
  316. package/src/tools/image-gen.ts +29 -5
  317. package/src/tools/index.ts +4 -0
  318. package/src/tools/output-meta.ts +281 -20
  319. package/src/tools/path-utils.ts +8 -3
  320. package/src/tools/read-internals.ts +44 -0
  321. package/src/tools/read.ts +1560 -310
  322. package/src/tools/resource-gc.ts +182 -18
  323. package/src/tools/review.ts +5 -7
  324. package/src/tools/search.ts +1 -0
  325. package/src/tools/subagent-render.ts +6 -2
  326. package/src/tools/subagent.ts +6 -0
  327. package/src/tools/telegram-send.ts +6 -3
  328. package/src/tools/tool-result.ts +14 -1
  329. package/src/utils/gh.ts +39 -0
  330. package/src/utils/image-resize.ts +3 -0
  331. package/src/web/search/providers/anthropic.ts +4 -4
  332. package/src/web/search/providers/codex.ts +1 -1
  333. package/src/web/search/providers/kimi.ts +15 -5
  334. package/src/web/search/providers/searxng.ts +47 -4
  335. package/src/web/search/providers/xai.ts +7 -2
  336. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  337. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  338. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  339. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
  340. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
  341. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  342. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  343. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
  344. package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
@@ -22,10 +22,12 @@ import type {
22
22
  StatusLineSeparatorStyle,
23
23
  } from "../../config/settings-schema";
24
24
  import { SETTING_TABS, TAB_METADATA } from "../../config/settings-schema";
25
+ import type { GjcRuntimeSnapshotProvider } from "../../extensibility/gjc-plugins/runtime-quarantine";
25
26
  import { getCurrentThemeName, getSelectListTheme, getSettingsListTheme, theme } from "../../modes/theme/theme";
26
27
  import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
27
28
  import { getTabBarTheme } from "../shared";
28
29
  import { DynamicBorder } from "./dynamic-border";
30
+ import { GjcBundleSettingsComponent } from "./gjc-bundle-settings";
29
31
  import {
30
32
  type NotificationsEditorOperations,
31
33
  NotificationsSettingsEditorComponent,
@@ -235,18 +237,16 @@ function getSavedUsageMode(): UsageMode {
235
237
  return segmentOptions.usage?.mode === "remaining" ? "remaining" : "used";
236
238
  }
237
239
 
238
- function setSavedUsageMode(mode: string): StatusLineSegmentOptions {
240
+ function getUsageModeSettings(mode: string): StatusLineSegmentOptions {
239
241
  const normalizedMode: UsageMode = mode === "remaining" ? "remaining" : "used";
240
242
  const segmentOptions = settings.get("statusLine.segmentOptions") as StatusLineSegmentOptions;
241
- const nextOptions: StatusLineSegmentOptions = {
243
+ return {
242
244
  ...segmentOptions,
243
245
  usage: {
244
246
  ...(segmentOptions.usage ?? {}),
245
247
  mode: normalizedMode,
246
248
  },
247
249
  };
248
- settings.set("statusLine.segmentOptions", nextOptions as Record<string, unknown>);
249
- return nextOptions;
250
250
  }
251
251
 
252
252
  function statusSegmentLabel(id: StatusLineSegmentId): string {
@@ -613,14 +613,17 @@ class StatusLineCustomEditor extends Container {
613
613
  }
614
614
 
615
615
  #save(): void {
616
- settings.set("statusLine.preset", "custom");
617
- settings.set("statusLine.leftSegments", [...this.#draft.leftSegments]);
618
- settings.set("statusLine.rightSegments", [...this.#draft.rightSegments]);
619
- settings.set("statusLine.separator", this.#draft.separator);
620
- settings.set(
621
- "statusLine.segmentOptions",
622
- cloneSegmentOptions(this.#draft.segmentOptions) as Record<string, unknown>,
623
- );
616
+ const saved = commitInteractiveSettings(this.callbacks, () => {
617
+ settings.set("statusLine.preset", "custom");
618
+ settings.set("statusLine.leftSegments", [...this.#draft.leftSegments]);
619
+ settings.set("statusLine.rightSegments", [...this.#draft.rightSegments]);
620
+ settings.set("statusLine.separator", this.#draft.separator);
621
+ settings.set(
622
+ "statusLine.segmentOptions",
623
+ cloneSegmentOptions(this.#draft.segmentOptions) as Record<string, unknown>,
624
+ );
625
+ });
626
+ if (!saved) return;
624
627
  this.callbacks.onChange("statusLine.preset", "custom");
625
628
  this.callbacks.onChange("statusLine.leftSegments", [...this.#draft.leftSegments]);
626
629
  this.callbacks.onChange("statusLine.rightSegments", [...this.#draft.rightSegments]);
@@ -649,6 +652,7 @@ function getSettingsTabs(): Tab[] {
649
652
  return { id, label: `${icon} ${meta.label}` };
650
653
  }),
651
654
  { id: "plugins", label: `${theme.icon.package} Plugins` },
655
+ { id: "gjc-bundles", label: `${theme.icon.package} GJC Bundles` },
652
656
  ];
653
657
  }
654
658
 
@@ -671,6 +675,14 @@ export interface SettingsRuntimeContext {
671
675
  petAvailable?: boolean;
672
676
  /** Terminal environment used to select unavailable-pet guidance. Omitted in production to use Bun.env. */
673
677
  terminalEnv?: NodeJS.ProcessEnv;
678
+ /**
679
+ * Runtime evidence published by the session for the current activation
680
+ * generation. Omitted when no session published one, in which case the GJC
681
+ * Bundles tab honestly reports runtime status as unavailable.
682
+ */
683
+ gjcRuntimeSnapshot?: GjcRuntimeSnapshotProvider;
684
+ /** Activation generation the published snapshot must match to be merged. */
685
+ gjcActivationGeneration?: number;
674
686
  }
675
687
 
676
688
  /** Status line settings subset for preview */
@@ -707,9 +719,29 @@ export interface SettingsCallbacks {
707
719
  getStatusLinePreview?: (width?: number) => string;
708
720
  /** Called when plugins change */
709
721
  onPluginsChanged?: () => void;
722
+ /** Called when an interactive setting cannot be committed. */
723
+ onError?: (message: string) => void;
710
724
  /** Called when settings panel is closed */
711
725
  onCancel: () => void;
712
726
  }
727
+ function commitInteractiveSettings(callbacks: SettingsCallbacks, commit: () => void): boolean {
728
+ if (!settings.canWriteDurableConfig()) {
729
+ callbacks.onError?.(
730
+ "Cannot change settings while config.yml has invalid YAML syntax. Repair config.yml and reload settings.",
731
+ );
732
+ return false;
733
+ }
734
+ try {
735
+ commit();
736
+ return true;
737
+ } catch (error) {
738
+ if (!settings.canWriteDurableConfig()) {
739
+ callbacks.onError?.(error instanceof Error ? error.message : String(error));
740
+ return false;
741
+ }
742
+ throw error;
743
+ }
744
+ }
713
745
 
714
746
  /**
715
747
  * Main tabbed settings selector component.
@@ -719,10 +751,11 @@ export class SettingsSelectorComponent extends Container {
719
751
  #tabBar: TabBar;
720
752
  #currentList: SettingsList | null = null;
721
753
  #pluginComponent: PluginSettingsComponent | null = null;
754
+ #gjcBundleComponent: GjcBundleSettingsComponent | null = null;
722
755
  #notificationsEditor: NotificationsSettingsEditorComponent | null = null;
723
756
  #statusPreviewContainer: Container | null = null;
724
757
  #statusPreviewText: Text | null = null;
725
- #currentTabId: SettingTab | "plugins" = "appearance";
758
+ #currentTabId: SettingTab | "plugins" | "gjc-bundles" = "appearance";
726
759
  #textInputActive = false;
727
760
 
728
761
  constructor(
@@ -738,7 +771,7 @@ export class SettingsSelectorComponent extends Container {
738
771
  // Tab bar
739
772
  this.#tabBar = new TabBar("Settings", getSettingsTabs(), getTabBarTheme());
740
773
  this.#tabBar.onTabChange = () => {
741
- this.#switchToTab(this.#tabBar.getActiveTab().id as SettingTab | "plugins");
774
+ this.#switchToTab(this.#tabBar.getActiveTab().id as SettingTab | "plugins" | "gjc-bundles");
742
775
  };
743
776
 
744
777
  this.addChild(this.#tabBar);
@@ -753,7 +786,7 @@ export class SettingsSelectorComponent extends Container {
753
786
  this.addChild(new DynamicBorder());
754
787
  }
755
788
 
756
- #switchToTab(tabId: SettingTab | "plugins"): void {
789
+ #switchToTab(tabId: SettingTab | "plugins" | "gjc-bundles"): void {
757
790
  if (this.#currentTabId === "notifications" && tabId !== "notifications" && !this.#disposeNotificationsEditor()) {
758
791
  return;
759
792
  }
@@ -768,6 +801,11 @@ export class SettingsSelectorComponent extends Container {
768
801
  this.removeChild(this.#pluginComponent);
769
802
  this.#pluginComponent = null;
770
803
  }
804
+ if (this.#gjcBundleComponent) {
805
+ this.removeChild(this.#gjcBundleComponent);
806
+ this.#gjcBundleComponent.dispose();
807
+ this.#gjcBundleComponent = null;
808
+ }
771
809
  if (this.#statusPreviewContainer) {
772
810
  this.removeChild(this.#statusPreviewContainer);
773
811
  this.#statusPreviewContainer = null;
@@ -780,6 +818,8 @@ export class SettingsSelectorComponent extends Container {
780
818
 
781
819
  if (tabId === "plugins") {
782
820
  this.#showPluginsTab();
821
+ } else if (tabId === "gjc-bundles") {
822
+ this.#showGjcBundlesTab();
783
823
  } else if (tabId === "notifications") {
784
824
  this.#showNotificationsTab();
785
825
  } else {
@@ -994,11 +1034,18 @@ export class SettingsSelectorComponent extends Container {
994
1034
  // The shared pet commit policy rechecks capability immediately
995
1035
  // before mutation and persists only on acceptance; the settings
996
1036
  // surface must not persist ahead of that result.
997
- const accepted = this.callbacks.onPetCommit?.(value) ?? false;
1037
+ let accepted = false;
1038
+ if (
1039
+ !commitInteractiveSettings(this.callbacks, () => {
1040
+ accepted = this.callbacks.onPetCommit?.(value) ?? false;
1041
+ })
1042
+ ) {
1043
+ return;
1044
+ }
998
1045
  done(accepted ? value : undefined);
999
1046
  return;
1000
1047
  }
1001
- this.#setSettingValue(def.path, value);
1048
+ if (!commitInteractiveSettings(this.callbacks, () => this.#setSettingValue(def.path, value))) return;
1002
1049
  this.callbacks.onChange(def.path, value);
1003
1050
  done(value);
1004
1051
  },
@@ -1031,7 +1078,7 @@ export class SettingsSelectorComponent extends Container {
1031
1078
  value => {
1032
1079
  // Empty string clears the setting; undefined-typed string settings
1033
1080
  // store "" which the browser.ts expandPath ignores (no-op fallback).
1034
- this.#setSettingValue(def.path, value);
1081
+ if (!commitInteractiveSettings(this.callbacks, () => this.#setSettingValue(def.path, value))) return;
1035
1082
  this.callbacks.onChange(def.path, value);
1036
1083
  wrappedDone(value);
1037
1084
  },
@@ -1089,7 +1136,15 @@ export class SettingsSelectorComponent extends Container {
1089
1136
  getSettingsListTheme(),
1090
1137
  (id, newValue) => {
1091
1138
  if (id === STATUS_LINE_USAGE_MODE_ID) {
1092
- const segmentOptions = setSavedUsageMode(newValue);
1139
+ const segmentOptions = getUsageModeSettings(newValue);
1140
+ if (
1141
+ !commitInteractiveSettings(this.callbacks, () => {
1142
+ settings.set("statusLine.segmentOptions", segmentOptions as Record<string, unknown>);
1143
+ })
1144
+ ) {
1145
+ this.#refreshCurrentTabItems(defs);
1146
+ return;
1147
+ }
1093
1148
  this.callbacks.onChange("statusLine.segmentOptions", segmentOptions);
1094
1149
  if (tabId === "appearance") {
1095
1150
  this.#triggerStatusLinePreview();
@@ -1105,14 +1160,20 @@ export class SettingsSelectorComponent extends Container {
1105
1160
 
1106
1161
  if (def.type === "boolean") {
1107
1162
  const boolValue = newValue === "true";
1108
- settings.set(path, boolValue as never);
1163
+ if (!commitInteractiveSettings(this.callbacks, () => settings.set(path, boolValue as never))) {
1164
+ this.#refreshCurrentTabItems(defs);
1165
+ return;
1166
+ }
1109
1167
  this.callbacks.onChange(path, boolValue);
1110
1168
 
1111
1169
  if (tabId === "appearance") {
1112
1170
  this.#triggerStatusLinePreview();
1113
1171
  }
1114
1172
  } else if (def.type === "enum") {
1115
- settings.set(path, newValue as never);
1173
+ if (!commitInteractiveSettings(this.callbacks, () => settings.set(path, newValue as never))) {
1174
+ this.#refreshCurrentTabItems(defs);
1175
+ return;
1176
+ }
1116
1177
  this.callbacks.onChange(path, newValue);
1117
1178
  }
1118
1179
  // Submenu/text types already persisted the value inside their own
@@ -1182,7 +1243,7 @@ export class SettingsSelectorComponent extends Container {
1182
1243
 
1183
1244
  /** Re-evaluate condition gates against the current settings and refresh the active list. */
1184
1245
  #refreshCurrentTabItems(defs: SettingDef[]): void {
1185
- if (this.#currentTabId === "plugins" || !this.#currentList) return;
1246
+ if (this.#currentTabId === "plugins" || this.#currentTabId === "gjc-bundles" || !this.#currentList) return;
1186
1247
  this.#currentList.setItems(this.#buildItemsForTab(defs, this.#currentTabId));
1187
1248
  }
1188
1249
 
@@ -1229,15 +1290,31 @@ export class SettingsSelectorComponent extends Container {
1229
1290
  });
1230
1291
  this.addChild(this.#pluginComponent);
1231
1292
  }
1293
+ #showGjcBundlesTab(): void {
1294
+ this.#gjcBundleComponent = new GjcBundleSettingsComponent(
1295
+ this.context.cwd,
1296
+ {
1297
+ onClose: () => this.callbacks.onCancel(),
1298
+ onBundlesChanged: () => this.callbacks.onPluginsChanged?.(),
1299
+ },
1300
+ {
1301
+ runtimeSnapshotProvider: this.context.gjcRuntimeSnapshot,
1302
+ activationGeneration: this.context.gjcActivationGeneration,
1303
+ },
1304
+ );
1305
+ this.addChild(this.#gjcBundleComponent);
1306
+ }
1232
1307
 
1233
1308
  getFocusComponent(): Component {
1234
- return (this.#currentList || this.#pluginComponent || this.#notificationsEditor)!;
1309
+ return (this.#currentList || this.#pluginComponent || this.#gjcBundleComponent || this.#notificationsEditor)!;
1235
1310
  }
1236
1311
 
1237
1312
  override dispose(): void {
1238
1313
  if (this.#notificationsEditor?.navigationLocked) return;
1239
1314
  this.#notificationsEditor?.dispose();
1240
1315
  this.#notificationsEditor = null;
1316
+ this.#gjcBundleComponent?.dispose();
1317
+ this.#gjcBundleComponent = null;
1241
1318
  super.dispose();
1242
1319
  }
1243
1320
 
@@ -1259,6 +1336,18 @@ export class SettingsSelectorComponent extends Container {
1259
1336
  this.#notificationsEditor.handleInput(data);
1260
1337
  return;
1261
1338
  }
1339
+ if (this.#gjcBundleComponent && this.#currentTabId === "gjc-bundles") {
1340
+ if (tabNavigation) {
1341
+ if (this.#gjcBundleComponent.navigationLocked) {
1342
+ this.#gjcBundleComponent.handleInput(data);
1343
+ return;
1344
+ }
1345
+ this.#tabBar.handleInput(data);
1346
+ return;
1347
+ }
1348
+ this.#gjcBundleComponent.handleInput(data);
1349
+ return;
1350
+ }
1262
1351
 
1263
1352
  // Handle tab switching — but NOT when a text input is active, since
1264
1353
  // arrow keys must reach the cursor and Tab must not switch tabs.
@@ -1277,6 +1366,10 @@ export class SettingsSelectorComponent extends Container {
1277
1366
  this.#pluginComponent.handleInput(data);
1278
1367
  return;
1279
1368
  }
1369
+ if (this.#gjcBundleComponent) {
1370
+ this.#gjcBundleComponent.handleInput(data);
1371
+ return;
1372
+ }
1280
1373
 
1281
1374
  // Fallback for future top-level content that does not own cancellation.
1282
1375
  if (matchesAppInterrupt(data)) {
@@ -0,0 +1,16 @@
1
+ import { type RunGh, runGhDefault } from "../../../utils/gh";
2
+
3
+ const STATUS_LINE_GH_TIMEOUT_MS = 5_000;
4
+
5
+ export async function lookupCurrentPr(runGh: RunGh = runGhDefault): Promise<{ number: number; url: string } | null> {
6
+ try {
7
+ const result = await runGh(["pr", "view", "--json", "number,url"], { timeoutMs: STATUS_LINE_GH_TIMEOUT_MS });
8
+ if (result.exitCode !== 0 || result.timedOut) return null;
9
+
10
+ const pr = JSON.parse(result.stdout) as { number?: unknown; url?: unknown };
11
+ if (typeof pr.number !== "number" || typeof pr.url !== "string") return null;
12
+ return { number: pr.number, url: pr.url };
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
@@ -129,6 +129,21 @@ function stabilizeStreamingPreviews(previews: PerFileDiffPreview[]): PerFileDiff
129
129
  return changed ? next : previews;
130
130
  }
131
131
 
132
+ function previewsAreEqual(a: PerFileDiffPreview[] | undefined, b: PerFileDiffPreview[]): boolean {
133
+ return (
134
+ a?.length === b.length &&
135
+ a.every((preview, index) => {
136
+ const next = b[index];
137
+ return (
138
+ preview.path === next?.path &&
139
+ preview.diff === next.diff &&
140
+ preview.error === next.error &&
141
+ preview.firstChangedLine === next.firstChangedLine
142
+ );
143
+ })
144
+ );
145
+ }
146
+
132
147
  function isEditLikeToolName(toolName: string): boolean {
133
148
  return toolName === "edit" || toolName === "apply_patch";
134
149
  }
@@ -144,6 +159,8 @@ export interface ToolExecutionOptions {
144
159
  editFuzzyThreshold?: number;
145
160
  editAllowFuzzy?: boolean;
146
161
  hashlineAutoDropPureInsertDuplicates?: boolean;
162
+ /** Internal observer for visible asynchronous renderer mutations. */
163
+ onVisibleTranscriptMutation?: () => void;
147
164
  }
148
165
 
149
166
  export interface ToolExecutionHandle {
@@ -168,6 +185,11 @@ export interface ToolExecutionHandle {
168
185
  * back to {@link setExpanded}.
169
186
  */
170
187
  setManuallyExpanded?(expanded: boolean): void;
188
+ /**
189
+ * Internal capability for dispatchers that need exact synchronous visible-output
190
+ * detection. Optional so existing structural handles remain source compatible.
191
+ */
192
+ consumeVisibleTranscriptChange?(): boolean;
171
193
  }
172
194
 
173
195
  /**
@@ -213,8 +235,11 @@ export class ToolExecutionComponent extends Container {
213
235
  #argsIdentityVersion = 0;
214
236
  #lastArgsReference: any;
215
237
  #shareArgsWithRenderer = false;
216
- // Cached converted images for Kitty protocol (which requires PNG), keyed by index
217
- #convertedImages: Map<number, { data: string; mimeType: string }> = new Map();
238
+ // Cached converted images for Kitty protocol (which requires PNG), keyed by index and source.
239
+ #convertedImages: Map<number, { data: string; mimeType: string; source: string }> = new Map();
240
+ #imageConversionGenerations = new Map<number, number>();
241
+ #imageConversionsInFlight = new Map<number, string>();
242
+
218
243
  // Spinner animation for partial task results
219
244
  #spinnerFrame?: number;
220
245
  #spinnerAnimation?: AnimationRegistration;
@@ -229,6 +254,10 @@ export class ToolExecutionComponent extends Container {
229
254
  expanded: false,
230
255
  isPartial: true,
231
256
  };
257
+ #onVisibleTranscriptMutation?: () => void;
258
+ #visibleTranscriptChanged = false;
259
+ #visibleProjection = "";
260
+ #disposed = false;
232
261
 
233
262
  constructor(
234
263
  toolName: string,
@@ -246,6 +275,8 @@ export class ToolExecutionComponent extends Container {
246
275
  this.#editFuzzyThreshold = options.editFuzzyThreshold;
247
276
  this.#editAllowFuzzy = options.editAllowFuzzy;
248
277
  this.#hashlineAutoDropPureInsertDuplicates = options.hashlineAutoDropPureInsertDuplicates;
278
+ this.#onVisibleTranscriptMutation = options.onVisibleTranscriptMutation;
279
+
249
280
  this.#tool = tool;
250
281
  this.#ui = ui;
251
282
  this.#cwd = cwd;
@@ -273,18 +304,23 @@ export class ToolExecutionComponent extends Container {
273
304
  this.#editMode = resolveEditModeForTool(toolName, tool);
274
305
 
275
306
  this.#updateDisplay();
307
+ this.#visibleProjection = this.#captureLogicalVisibleProjection();
308
+
276
309
  void this.#runPreviewDiff();
277
310
  }
278
311
 
279
312
  updateArgs(args: any, _toolCallId?: string): void {
313
+ const argsChanged = !Bun.deepEquals(this.#args, args);
314
+ if (!argsChanged && !this.#editMode) return;
280
315
  if (args !== this.#lastArgsReference) {
281
316
  this.#lastArgsReference = args;
282
317
  this.#argsIdentityVersion += 1;
283
318
  }
284
- this.#args = this.#shareArgsWithRenderer ? args : cloneToolArgs(args);
319
+ this.#args = argsChanged ? (this.#shareArgsWithRenderer ? args : cloneToolArgs(args)) : this.#args;
285
320
  this.#updateSpinnerAnimation();
286
321
  void this.#runPreviewDiff();
287
322
  this.#updateDisplay();
323
+ this.#markVisibleMutationIfChanged();
288
324
  }
289
325
 
290
326
  /**
@@ -295,6 +331,8 @@ export class ToolExecutionComponent extends Container {
295
331
  this.#argsComplete = true;
296
332
  this.#updateSpinnerAnimation();
297
333
  void this.#runPreviewDiff();
334
+ this.#updateDisplay();
335
+ this.#markVisibleMutationIfChanged();
298
336
  }
299
337
 
300
338
  async #runPreviewDiff(): Promise<void> {
@@ -338,14 +376,17 @@ export class ToolExecutionComponent extends Container {
338
376
  hashlineAutoDropPureInsertDuplicates: this.#hashlineAutoDropPureInsertDuplicates,
339
377
  isStreaming,
340
378
  });
341
- if (controller.signal.aborted) return;
379
+ if (this.#disposed || controller.signal.aborted) return;
342
380
  if (previews) {
343
- this.#editDiffPreview = isStreaming ? stabilizeStreamingPreviews(previews) : previews;
381
+ const nextPreview = isStreaming ? stabilizeStreamingPreviews(previews) : previews;
382
+ const changed = !previewsAreEqual(this.#editDiffPreview, nextPreview);
383
+ this.#editDiffPreview = nextPreview;
344
384
  this.#updateDisplay();
345
385
  this.#ui.requestRender();
386
+ if (changed) this.#markVisibleMutationIfChanged(true);
346
387
  }
347
388
  } catch (err) {
348
- if (controller.signal.aborted) return;
389
+ if (this.#disposed || controller.signal.aborted) return;
349
390
  logger.warn("Edit preview diff failed", { tool: this.#toolName, error: String(err) });
350
391
  }
351
392
  }
@@ -359,8 +400,10 @@ export class ToolExecutionComponent extends Container {
359
400
  isPartial = false,
360
401
  _toolCallId?: string,
361
402
  ): void {
403
+ if (this.#isPartial === isPartial && Bun.deepEquals(this.#result, result)) return;
362
404
  this.#textOutputCache = undefined;
363
405
  this.#result = result;
406
+ this.#invalidateStaleKittyConversions();
364
407
  this.#isPartial = isPartial;
365
408
  // When tool is complete, ensure args are marked complete so spinner stops
366
409
  if (!isPartial) {
@@ -370,6 +413,7 @@ export class ToolExecutionComponent extends Container {
370
413
  this.#updateDisplay();
371
414
  // Convert non-PNG images to PNG for Kitty protocol (async)
372
415
  this.#maybeConvertImagesForKitty();
416
+ this.#markVisibleMutationIfChanged();
373
417
  }
374
418
 
375
419
  /**
@@ -383,40 +427,101 @@ export class ToolExecutionComponent extends Container {
383
427
  return [...contentImages, ...detailImages];
384
428
  }
385
429
 
386
- /**
387
- * Convert non-PNG images to PNG for Kitty graphics protocol.
388
- * Kitty requires PNG format (f=100), so JPEG/GIF/WebP won't display.
389
- */
390
- #maybeConvertImagesForKitty(): void {
391
- // Only needed for Kitty protocol
392
- if (TERMINAL.imageProtocol !== ImageProtocol.Kitty) return;
393
- if (!this.#result) return;
394
-
395
- const imageBlocks = this.#getAllImageBlocks();
430
+ #imageSource(image: { data?: string; mimeType?: string }): string | undefined {
431
+ return image.data && image.mimeType ? `${image.mimeType}\u0000${image.data}` : undefined;
432
+ }
396
433
 
397
- for (let i = 0; i < imageBlocks.length; i++) {
398
- const img = imageBlocks[i];
399
- if (!img.data || !img.mimeType) continue;
400
- // Skip if already PNG or already converted
401
- if (img.mimeType === "image/png") continue;
402
- if (this.#convertedImages.has(i)) continue;
434
+ #invalidateStaleKittyConversions(): void {
435
+ const images = this.#getAllImageBlocks();
436
+ const indices = new Set<number>();
437
+ for (let index = 0; index < images.length; index++) {
438
+ const source = this.#imageSource(images[index]);
439
+ if (!source) continue;
440
+ indices.add(index);
441
+ if (this.#convertedImages.get(index)?.source !== source) this.#convertedImages.delete(index);
442
+ if (this.#imageConversionsInFlight.get(index) !== source) {
443
+ this.#imageConversionsInFlight.delete(index);
444
+ this.#imageConversionGenerations.set(index, (this.#imageConversionGenerations.get(index) ?? 0) + 1);
445
+ }
446
+ }
447
+ for (const index of this.#convertedImages.keys()) {
448
+ if (!indices.has(index)) this.#convertedImages.delete(index);
449
+ }
450
+ for (const index of this.#imageConversionsInFlight.keys()) {
451
+ if (!indices.has(index)) {
452
+ this.#imageConversionsInFlight.delete(index);
453
+ this.#imageConversionGenerations.set(index, (this.#imageConversionGenerations.get(index) ?? 0) + 1);
454
+ }
455
+ }
456
+ }
403
457
 
404
- // Convert async - catch errors from processing
405
- const index = i;
406
- new Bun.Image(Buffer.from(img.data, "base64"))
458
+ /** Convert non-PNG images to PNG for Kitty graphics protocol. */
459
+ #maybeConvertImagesForKitty(): void {
460
+ if (TERMINAL.imageProtocol !== ImageProtocol.Kitty) return;
461
+ for (const [index, image] of this.#getAllImageBlocks().entries()) {
462
+ const source = this.#imageSource(image);
463
+ if (!source || image.mimeType === "image/png") continue;
464
+ if (
465
+ this.#convertedImages.get(index)?.source === source ||
466
+ this.#imageConversionsInFlight.get(index) === source
467
+ )
468
+ continue;
469
+ const generation = this.#imageConversionGenerations.get(index) ?? 0;
470
+ this.#imageConversionsInFlight.set(index, source);
471
+ new Bun.Image(Buffer.from(image.data!, "base64"))
407
472
  .png()
408
473
  .toBase64()
409
474
  .then(data => {
410
- this.#convertedImages.set(index, { data, mimeType: "image/png" });
475
+ if (
476
+ this.#disposed ||
477
+ this.#imageConversionGenerations.get(index) !== generation ||
478
+ this.#imageConversionsInFlight.get(index) !== source ||
479
+ this.#imageSource(this.#getAllImageBlocks()[index] ?? {}) !== source
480
+ )
481
+ return;
482
+ this.#imageConversionsInFlight.delete(index);
483
+ this.#convertedImages.set(index, { data, mimeType: "image/png", source });
411
484
  this.#updateDisplay();
412
485
  this.#ui.requestRender();
486
+ this.#markVisibleMutationIfChanged(true);
413
487
  })
414
488
  .catch(() => {
415
- // Ignore conversion failures - display will use original image format
489
+ if (
490
+ this.#disposed ||
491
+ this.#imageConversionGenerations.get(index) !== generation ||
492
+ this.#imageConversionsInFlight.get(index) !== source
493
+ )
494
+ return;
495
+ this.#imageConversionsInFlight.delete(index);
416
496
  });
417
497
  }
418
498
  }
419
499
 
500
+ #captureLogicalVisibleProjection(): string {
501
+ return this.render(10_000).join("\n");
502
+ }
503
+
504
+ #markVisibleMutationIfChanged(notify = false): void {
505
+ const projection = this.#captureLogicalVisibleProjection();
506
+ if (projection === this.#visibleProjection) return;
507
+ this.#visibleProjection = projection;
508
+ this.#markVisibleMutation(notify);
509
+ }
510
+
511
+ #markVisibleMutation(notify = false): void {
512
+ if (notify) {
513
+ this.#onVisibleTranscriptMutation?.();
514
+ return;
515
+ }
516
+ this.#visibleTranscriptChanged = true;
517
+ }
518
+
519
+ consumeVisibleTranscriptChange(): boolean {
520
+ const changed = this.#visibleTranscriptChanged;
521
+ this.#visibleTranscriptChanged = false;
522
+ return changed;
523
+ }
524
+
420
525
  /**
421
526
  * Start or stop spinner animation based on whether this is a partial task result.
422
527
  */
@@ -430,6 +535,7 @@ export class ToolExecutionComponent extends Container {
430
535
  const needsSpinner = isStreamingArgs || isPartialTask;
431
536
  if (needsSpinner && !this.#spinnerAnimation) {
432
537
  this.#spinnerAnimation = registerAnimationCallback(() => {
538
+ if (this.#disposed) return;
433
539
  const frameCount = theme.spinnerFrames.length;
434
540
  if (frameCount === 0) return;
435
541
  this.#spinnerFrame = ((this.#spinnerFrame ?? -1) + 1) % frameCount;
@@ -456,6 +562,11 @@ export class ToolExecutionComponent extends Container {
456
562
  }
457
563
 
458
564
  override dispose(): void {
565
+ if (this.#disposed) return;
566
+ this.#disposed = true;
567
+ this.#imageConversionGenerations.clear();
568
+ this.#imageConversionsInFlight.clear();
569
+ this.#convertedImages.clear();
459
570
  this.stopAnimation();
460
571
  super.dispose();
461
572
  }
@@ -2,7 +2,6 @@ import * as fs from "node:fs";
2
2
 
3
3
  import { type Component, truncateToWidth, visibleWidth } from "@gajae-code/tui";
4
4
  import { formatCount, getProjectDir } from "@gajae-code/utils";
5
- import { $ } from "bun";
6
5
  import {
7
6
  type AppKeybinding,
8
7
  KEYBINDINGS,
@@ -20,6 +19,7 @@ import type { ActionRegistry, FocusDomain } from "../action-registry";
20
19
  import { EMPTY_JOBS_SNAPSHOT, type JobsSnapshot } from "../jobs-observer";
21
20
  import { sanitizeStatusText } from "../shared";
22
21
  import { renderSkillHudBar } from "./skill-hud/render";
22
+ import { lookupCurrentPr } from "./status-line/gh";
23
23
  import {
24
24
  canReuseCachedPr,
25
25
  createPrCacheContext,
@@ -416,19 +416,8 @@ export class StatusLineComponent implements Component {
416
416
  };
417
417
  try {
418
418
  // Requires `gh repo set-default` to be configured; fails gracefully if not
419
- const result = await $`gh pr view --json number,url`.quiet().nothrow();
420
- if (result.exitCode !== 0) {
421
- setCachedPr(null);
422
- return;
423
- }
424
- const pr = JSON.parse(result.stdout.toString()) as { number: number; url: string };
425
- if (typeof pr.number === "number") {
426
- setCachedPr({ number: pr.number, url: pr.url });
427
- } else {
428
- setCachedPr(null);
429
- }
430
- } catch {
431
- setCachedPr(null);
419
+ const pr = await lookupCurrentPr();
420
+ setCachedPr(pr);
432
421
  } finally {
433
422
  this.#prLookupInFlight = false;
434
423
  if (this.#onBranchChange) {