@gajae-code/coding-agent 0.11.11 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/CHANGELOG.md +49 -3
  2. package/dist/types/cli/read-cli.d.ts +1 -0
  3. package/dist/types/commands/deep-interview.d.ts +9 -0
  4. package/dist/types/commands/harness.d.ts +22 -0
  5. package/dist/types/commands/read.d.ts +6 -0
  6. package/dist/types/config/model-registry.d.ts +4 -0
  7. package/dist/types/config/provider-auth-health.d.ts +14 -0
  8. package/dist/types/config/provider-ranking.d.ts +59 -0
  9. package/dist/types/config/settings-schema.d.ts +29 -1
  10. package/dist/types/config/settings.d.ts +2 -0
  11. package/dist/types/extensibility/extensions/types.d.ts +22 -2
  12. package/dist/types/extensibility/shared-events.d.ts +6 -0
  13. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  14. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  15. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  16. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  17. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  18. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  19. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +52 -1
  20. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  21. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  22. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
  23. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  24. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  25. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
  26. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  27. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  28. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  29. package/dist/types/main.d.ts +1 -1
  30. package/dist/types/memories/index.d.ts +2 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +8 -0
  32. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  33. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  34. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  35. package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
  36. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  37. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  38. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  39. package/dist/types/modes/interactive-mode.d.ts +2 -0
  40. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  41. package/dist/types/modes/types.d.ts +2 -0
  42. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  43. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  44. package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
  45. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  46. package/dist/types/sdk/bus/index.d.ts +15 -1
  47. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
  48. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  49. package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
  50. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  51. package/dist/types/sdk/host/host.d.ts +2 -0
  52. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  53. package/dist/types/session/agent-session.d.ts +44 -10
  54. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  55. package/dist/types/session/session-manager.d.ts +25 -13
  56. package/dist/types/session/streaming-output.d.ts +60 -5
  57. package/dist/types/skill-state/active-state.d.ts +0 -4
  58. package/dist/types/skill-state/workflow-hud.d.ts +3 -0
  59. package/dist/types/tools/browser/launch.d.ts +7 -0
  60. package/dist/types/tools/fetch.d.ts +17 -1
  61. package/dist/types/tools/output-meta.d.ts +21 -1
  62. package/dist/types/tools/read-internals.d.ts +12 -0
  63. package/dist/types/tools/read.d.ts +45 -1
  64. package/dist/types/tools/resource-gc.d.ts +4 -0
  65. package/dist/types/tools/tool-result.d.ts +7 -1
  66. package/package.json +9 -13
  67. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  68. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  69. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  70. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  71. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  72. package/src/capability/index.ts +8 -0
  73. package/src/cli/read-cli.ts +2 -1
  74. package/src/cli.ts +1 -0
  75. package/src/commands/deep-interview.ts +29 -9
  76. package/src/commands/harness.ts +35 -12
  77. package/src/commands/read.ts +11 -2
  78. package/src/commands/team.ts +1 -1
  79. package/src/config/model-registry.ts +21 -2
  80. package/src/config/provider-auth-health.ts +42 -0
  81. package/src/config/provider-ranking.ts +119 -0
  82. package/src/config/settings-schema.ts +23 -1
  83. package/src/config/settings.ts +246 -108
  84. package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
  85. package/src/defaults/gjc/skills/ralplan/SKILL.md +58 -25
  86. package/src/defaults/gjc/skills/team/SKILL.md +1 -1
  87. package/src/defaults/gjc/skills/ultragoal/SKILL.md +25 -15
  88. package/src/eval/py/executor.ts +7 -2
  89. package/src/extensibility/extensions/types.ts +28 -4
  90. package/src/extensibility/shared-events.ts +5 -0
  91. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
  92. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  93. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  94. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  95. package/src/gjc-runtime/deep-interview-state.ts +12 -943
  96. package/src/gjc-runtime/gc-render.ts +1 -0
  97. package/src/gjc-runtime/gc-runtime.ts +11 -1
  98. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  99. package/src/gjc-runtime/ralplan-runtime.ts +742 -85
  100. package/src/gjc-runtime/state-runtime.ts +2 -1
  101. package/src/gjc-runtime/state-writer.ts +70 -222
  102. package/src/gjc-runtime/team-launch.ts +34 -5
  103. package/src/gjc-runtime/team-runtime.ts +970 -33
  104. package/src/gjc-runtime/team-store.ts +32 -0
  105. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  106. package/src/gjc-runtime/team-workers.ts +1 -0
  107. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
  108. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  109. package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
  110. package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
  111. package/src/gjc-runtime/workflow-manifest.ts +34 -179
  112. package/src/hooks/native-skill-hook.ts +17 -4
  113. package/src/hooks/skill-state.ts +1 -23
  114. package/src/internal-urls/docs-index.generated.ts +8 -9
  115. package/src/internal-urls/local-root-gc.ts +127 -0
  116. package/src/main.ts +9 -5
  117. package/src/memories/index.ts +10 -0
  118. package/src/modes/DESIGN.md +47 -0
  119. package/src/modes/acp/acp-agent.ts +43 -7
  120. package/src/modes/acp/acp-event-mapper.ts +14 -7
  121. package/src/modes/acp/acp-mode.ts +39 -3
  122. package/src/modes/components/assistant-message.ts +123 -22
  123. package/src/modes/components/irc-sidebar.ts +176 -62
  124. package/src/modes/components/model-selector.ts +61 -4
  125. package/src/modes/components/oauth-selector.ts +42 -12
  126. package/src/modes/components/read-tool-group.ts +82 -14
  127. package/src/modes/components/session-observer-overlay.ts +187 -32
  128. package/src/modes/components/settings-selector.ts +60 -18
  129. package/src/modes/components/tool-execution.ts +138 -27
  130. package/src/modes/controllers/event-controller.ts +109 -27
  131. package/src/modes/controllers/extension-ui-controller.ts +19 -2
  132. package/src/modes/controllers/input-controller.ts +18 -3
  133. package/src/modes/controllers/selector-controller.ts +83 -37
  134. package/src/modes/interactive-mode.ts +62 -3
  135. package/src/modes/irc-observation-ledger.ts +71 -15
  136. package/src/modes/runtime-init.ts +19 -2
  137. package/src/modes/types.ts +2 -0
  138. package/src/modes/utils/ui-helpers.ts +94 -1
  139. package/src/prompts/agents/architect.md +9 -1
  140. package/src/prompts/agents/critic.md +9 -0
  141. package/src/prompts/system/system-prompt.md +0 -1
  142. package/src/prompts/tools/read.md +4 -2
  143. package/src/runtime-mcp/smithery-auth.ts +17 -2
  144. package/src/runtime-mcp/smithery-connect.ts +11 -1
  145. package/src/sdk/broker/lifecycle.ts +25 -6
  146. package/src/sdk/bus/control-drain-lease.ts +104 -0
  147. package/src/sdk/bus/index.ts +341 -38
  148. package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
  149. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  150. package/src/sdk/bus/reconciliation-store.ts +224 -0
  151. package/src/sdk/host/control/dispatch.ts +5 -1
  152. package/src/sdk/host/control/operations.ts +1 -1
  153. package/src/sdk/host/host.ts +23 -1
  154. package/src/sdk/host/query/handlers.ts +55 -0
  155. package/src/sdk/mcp/server.ts +46 -18
  156. package/src/sdk/protocol/operation-inventory.generated.json +29 -0
  157. package/src/sdk/protocol/operation-registry.ts +7 -2
  158. package/src/sdk/session.ts +13 -1
  159. package/src/session/agent-session.ts +466 -50
  160. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  161. package/src/session/session-manager.ts +764 -17
  162. package/src/session/streaming-output.ts +262 -43
  163. package/src/setup/provider-onboarding.ts +19 -4
  164. package/src/skill-state/active-state.ts +0 -10
  165. package/src/skill-state/workflow-hud.ts +25 -2
  166. package/src/skill-state/workflow-mutation-guard.ts +65 -7
  167. package/src/tools/ask.ts +89 -69
  168. package/src/tools/browser/launch.ts +41 -6
  169. package/src/tools/fetch.ts +114 -23
  170. package/src/tools/output-meta.ts +176 -9
  171. package/src/tools/path-utils.ts +5 -1
  172. package/src/tools/read-internals.ts +44 -0
  173. package/src/tools/read.ts +1542 -310
  174. package/src/tools/resource-gc.ts +182 -18
  175. package/src/tools/tool-result.ts +14 -1
  176. package/src/web/search/providers/codex.ts +1 -1
  177. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  178. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  179. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  180. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  181. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  182. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
@@ -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
  }
@@ -12,7 +12,7 @@ import {
12
12
  readArgsTargetInternalUrl,
13
13
  } from "../../modes/components/read-tool-group";
14
14
  import { TodoReminderComponent } from "../../modes/components/todo-reminder";
15
- import { ToolExecutionComponent } from "../../modes/components/tool-execution";
15
+ import { ToolExecutionComponent, type ToolExecutionHandle } from "../../modes/components/tool-execution";
16
16
  import { TtsrNotificationComponent } from "../../modes/components/ttsr-notification";
17
17
  import { getSymbolTheme, theme } from "../../modes/theme/theme";
18
18
  import type { InteractiveModeContext, TodoPhase } from "../../modes/types";
@@ -23,6 +23,7 @@ import type { AgentSessionEvent } from "../../session/agent-session";
23
23
  import { type CustomMessage, isSilentAbort, readPendingDisplayTag } from "../../session/messages";
24
24
  import { transferSessionMessageIdentity } from "../../session/session-manager";
25
25
  import type { ResolveToolDetails } from "../../tools/resolve";
26
+ import { computeIrcSplitWidths, getIrcSidebarSemanticToken } from "../components/irc-sidebar";
26
27
  import type { IrcObservationRecord } from "../irc-observation-ledger";
27
28
  import { interruptHint } from "../shared";
28
29
  import { buildAbortDisplayMessage } from "../utils/abort-message";
@@ -36,7 +37,14 @@ type AgentSessionEventKind = AgentSessionEvent["type"];
36
37
 
37
38
  /** Test-only performance counters for advisory baseline tests. */
38
39
  export const __eventControllerPerfCounters = {
40
+ enabled: false,
39
41
  messageUpdateContentVisits: 0,
42
+ enable(): void {
43
+ this.enabled = true;
44
+ },
45
+ disable(): void {
46
+ this.enabled = false;
47
+ },
40
48
  reset(): void {
41
49
  this.messageUpdateContentVisits = 0;
42
50
  },
@@ -118,6 +126,9 @@ export class EventController {
118
126
  #toolIntentCache = new Map<string, { args: unknown; intent: string | undefined }>();
119
127
  #thinkingContentIndices = new Set<number>();
120
128
  #handlers: AgentSessionEventHandlers;
129
+ #visibleTranscriptChanged = false;
130
+ #handlingEvent = false;
131
+ #eventQueue: Promise<void> = Promise.resolve();
121
132
 
122
133
  constructor(private ctx: InteractiveModeContext) {
123
134
  this.#handlers = {
@@ -162,6 +173,26 @@ export class EventController {
162
173
  this.clearIrcExpiryTimers();
163
174
  }
164
175
 
176
+ #recordVisibleTranscriptMutation(): void {
177
+ this.#visibleTranscriptChanged = true;
178
+ }
179
+
180
+ #observeVisibleTranscriptMutation(): void {
181
+ if (this.#handlingEvent) {
182
+ this.#recordVisibleTranscriptMutation();
183
+ return;
184
+ }
185
+ this.ctx.recordVisibleTranscriptMutation?.();
186
+ }
187
+
188
+ #consumeToolVisibleChange(handle: ToolExecutionHandle): void {
189
+ // Structural third-party renderers may not implement the optional exact-consume
190
+ // capability; preserve revision correctness conservatively until they opt in.
191
+ if (typeof handle.consumeVisibleTranscriptChange !== "function" || handle.consumeVisibleTranscriptChange()) {
192
+ this.#recordVisibleTranscriptMutation();
193
+ }
194
+ }
195
+
165
196
  #resetReadGroup(): void {
166
197
  this.#lastReadGroup = undefined;
167
198
  }
@@ -228,17 +259,26 @@ export class EventController {
228
259
  });
229
260
  }
230
261
 
231
- async handleEvent(event: AgentSessionEvent): Promise<void> {
232
- if (!this.ctx.isInitialized) {
233
- await this.ctx.init();
234
- }
235
-
236
- this.ctx.statusLine.invalidate();
237
- this.ctx.updateEditorTopBorder();
262
+ handleEvent(event: AgentSessionEvent): Promise<void> {
263
+ const dispatch = this.#eventQueue.then(() => this.#dispatchEvent(event));
264
+ this.#eventQueue = dispatch.catch(() => {});
265
+ return dispatch;
266
+ }
238
267
 
239
- const run = this.#handlers[event.type] as (e: AgentSessionEvent) => Promise<void>;
240
- await run(event);
241
- if (this.ctx.isTranscriptViewerOpen?.()) this.ctx.refreshTranscriptViewer?.();
268
+ async #dispatchEvent(event: AgentSessionEvent): Promise<void> {
269
+ if (!this.ctx.isInitialized) await this.ctx.init();
270
+ this.#visibleTranscriptChanged = false;
271
+ this.#handlingEvent = true;
272
+ try {
273
+ this.ctx.statusLine.invalidate();
274
+ this.ctx.updateEditorTopBorder();
275
+ const run = this.#handlers[event.type] as (e: AgentSessionEvent) => Promise<void>;
276
+ await run(event);
277
+ if (this.#visibleTranscriptChanged) this.ctx.recordVisibleTranscriptMutation?.();
278
+ if (this.ctx.isTranscriptViewerOpen?.()) this.ctx.refreshTranscriptViewer?.();
279
+ } finally {
280
+ this.#handlingEvent = false;
281
+ }
242
282
  }
243
283
 
244
284
  async #handleAgentStart(_event: Extract<AgentSessionEvent, { type: "agent_start" }>): Promise<void> {
@@ -268,8 +308,12 @@ export class EventController {
268
308
  const parsed = parseIrcMessage(message);
269
309
  if (!parsed) return;
270
310
  const arrival = this.ctx.captureIrcArrivalSnapshot();
311
+ const rightWidth = arrival.panelVisible
312
+ ? computeIrcSplitWidths(this.ctx.ui.terminal?.columns ?? 0).rightWidth
313
+ : 0;
314
+ const beforeToken = rightWidth > 0 ? getIrcSidebarSemanticToken(this.ctx.ircLedger, rightWidth) : "";
271
315
  const record = this.ctx.ircLedger.observe(parsed, arrival.panelVisible);
272
- this.#cleanupEvictedIrcObservations();
316
+ const evictedInlineRemoved = this.#cleanupIrcObservationIds(this.ctx.ircLedger.drainEvictedObservationIds());
273
317
  if (!record) return;
274
318
  const signature = `irc:${record.observationId}`;
275
319
  if (this.#renderedCustomMessages.has(signature)) return;
@@ -279,9 +323,13 @@ export class EventController {
279
323
  this.#renderedIrcComponents.set(record.observationId, components);
280
324
  this.#scheduleIrcExpiry(record, components);
281
325
  this.ctx.ui.requestRender();
326
+ const afterToken = rightWidth > 0 ? getIrcSidebarSemanticToken(this.ctx.ircLedger, rightWidth) : "";
327
+ if (components.length > 0 || evictedInlineRemoved || (rightWidth > 0 && beforeToken !== afterToken)) {
328
+ this.#recordVisibleTranscriptMutation();
329
+ }
282
330
  }
283
331
 
284
- #cleanupIrcObservationIds(observationIds: readonly string[]): void {
332
+ #cleanupIrcObservationIds(observationIds: readonly string[]): boolean {
285
333
  const removedComponents = new Set<Component>();
286
334
  for (const observationId of observationIds) {
287
335
  const timer = this.#ircExpiryTimers.get(observationId);
@@ -294,23 +342,26 @@ export class EventController {
294
342
  for (const component of this.ctx.removeRenderedIrcInlineComponents(observationId) ?? []) {
295
343
  removedComponents.add(component);
296
344
  }
297
-
298
345
  this.#renderedCustomMessages.delete(`irc:${observationId}`);
299
346
  }
300
347
  for (const component of removedComponents) this.ctx.chatContainer.removeChild(component);
348
+ return removedComponents.size > 0;
301
349
  }
302
350
 
303
351
  #cleanupEvictedIrcObservations(): void {
304
- this.#cleanupIrcObservationIds(this.ctx.ircLedger.drainEvictedObservationIds());
352
+ if (this.#cleanupIrcObservationIds(this.ctx.ircLedger.drainEvictedObservationIds())) {
353
+ this.#recordVisibleTranscriptMutation();
354
+ }
305
355
  }
306
356
 
307
- #cleanupExpiredIrcInlineComponents(observationId: string): void {
357
+ #cleanupExpiredIrcInlineComponents(observationId: string): boolean {
308
358
  const removedComponents = new Set<Component>(this.#renderedIrcComponents.get(observationId));
309
359
  this.#renderedIrcComponents.delete(observationId);
310
360
  for (const component of this.ctx.removeRenderedIrcInlineComponents(observationId) ?? []) {
311
361
  removedComponents.add(component);
312
362
  }
313
363
  for (const component of removedComponents) this.ctx.chatContainer.removeChild(component);
364
+ return removedComponents.size > 0;
314
365
  }
315
366
 
316
367
  async #handleMessageStart(event: Extract<AgentSessionEvent, { type: "message_start" }>): Promise<void> {
@@ -326,6 +377,7 @@ export class EventController {
326
377
  this.#renderedCustomMessages.add(signature);
327
378
  this.#resetReadGroup();
328
379
  this.ctx.addMessageToChat(event.message);
380
+ if (event.message.display) this.#recordVisibleTranscriptMutation();
329
381
  // Tag-keyed pending-bar refresh: when AgentSession.#handleAgentEvent
330
382
  // spliced this dequeued custom message out of #steeringMessages /
331
383
  // #followUpMessages (it ran before this emit), the array state is
@@ -373,10 +425,12 @@ export class EventController {
373
425
  this.ctx.updatePendingMessagesDisplay();
374
426
  }
375
427
  this.ctx.ui.requestRender();
428
+ if (!wasOptimistic) this.#recordVisibleTranscriptMutation();
376
429
  } else if (event.message.role === "fileMention") {
377
430
  this.#resetReadGroup();
378
431
  this.ctx.addMessageToChat(event.message);
379
432
  this.ctx.ui.requestRender();
433
+ this.#recordVisibleTranscriptMutation();
380
434
  } else if (event.message.role === "assistant") {
381
435
  this.#lastThinkingCount = 0;
382
436
  this.#resetReadGroup();
@@ -387,6 +441,7 @@ export class EventController {
387
441
  this.ctx.hideThinkingBlock,
388
442
  () => this.ctx.ui.requestRender(),
389
443
  this.ctx.getAssistantViewportAnchorId?.(event.message),
444
+ () => this.#observeVisibleTranscriptMutation(),
390
445
  );
391
446
  this.ctx.streamingMessage = event.message;
392
447
  addChatChild(this.ctx, this.ctx.streamingComponent);
@@ -429,15 +484,10 @@ export class EventController {
429
484
  this.#cleanupEvictedIrcObservations();
430
485
  const projectedObservationIds = new Set(inlineProjection.map(record => record.observationId));
431
486
  for (const [observationId, components] of componentsByObservationId) {
432
- const record = this.ctx.ircLedger.getRecord(observationId);
433
- if (
434
- !projectedObservationIds.has(observationId) ||
435
- (record?.mode === "ephemeral" && record.expiresAt! - now <= 0)
436
- ) {
487
+ if (!projectedObservationIds.has(observationId)) {
437
488
  for (const component of components) {
438
489
  this.ctx.chatContainer.removeChild(component);
439
490
  }
440
- this.#renderedIrcComponents.delete(observationId);
441
491
  componentsByObservationId.delete(observationId);
442
492
  }
443
493
  }
@@ -468,12 +518,12 @@ export class EventController {
468
518
  }
469
519
  const remainingMs = record.expiresAt! - Date.now();
470
520
  if (remainingMs <= 0) {
471
- this.#cleanupExpiredIrcInlineComponents(record.observationId);
521
+ if (this.#cleanupExpiredIrcInlineComponents(record.observationId)) this.#observeVisibleTranscriptMutation();
472
522
  return false;
473
523
  }
474
524
  const timer = setTimeout(() => {
475
525
  this.#ircExpiryTimers.delete(record.observationId);
476
- this.#cleanupExpiredIrcInlineComponents(record.observationId);
526
+ if (this.#cleanupExpiredIrcInlineComponents(record.observationId)) this.#observeVisibleTranscriptMutation();
477
527
  this.ctx.ui.requestRender();
478
528
  }, remainingMs);
479
529
  timer.unref?.();
@@ -496,6 +546,7 @@ export class EventController {
496
546
  this.#resetReadGroup();
497
547
  this.ctx.addMessageToChat(event.message);
498
548
  this.ctx.ui.requestRender();
549
+ this.#recordVisibleTranscriptMutation();
499
550
  }
500
551
 
501
552
  async #handleModelFallbackSwitched(
@@ -549,19 +600,24 @@ export class EventController {
549
600
  }
550
601
 
551
602
  for (const content of contentsToProcess) {
552
- __eventControllerPerfCounters.messageUpdateContentVisits += 1;
603
+ if (__eventControllerPerfCounters.enabled) __eventControllerPerfCounters.messageUpdateContentVisits += 1;
553
604
  if (content.type !== "toolCall") continue;
554
605
  if (content.name === "read") {
555
606
  if (!readArgsHaveTarget(content.arguments)) continue;
556
607
  if (!readArgsTargetInternalUrl(content.arguments)) {
557
608
  this.#trackReadToolCall(content.id, content.arguments);
558
609
  const component = this.ctx.pendingTools.get(content.id);
559
- if (component) component.updateArgs(content.arguments, content.id);
560
- else {
610
+ if (component) {
611
+ component.updateArgs(content.arguments, content.id);
612
+ this.#consumeToolVisibleChange(component);
613
+ } else {
561
614
  const group = this.#getReadGroup();
562
615
  group.updateArgs(content.arguments, content.id);
563
616
  this.ctx.pendingTools.set(content.id, group);
617
+ this.#recordVisibleTranscriptMutation();
618
+ group.consumeVisibleTranscriptChange();
564
619
  }
620
+
565
621
  continue;
566
622
  }
567
623
  }
@@ -582,6 +638,7 @@ export class EventController {
582
638
  editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
583
639
  editAllowFuzzy: settings.get("edit.fuzzyMatch"),
584
640
  hashlineAutoDropPureInsertDuplicates: settings.get("edit.hashlineAutoDropPureInsertDuplicates"),
641
+ onVisibleTranscriptMutation: () => this.#observeVisibleTranscriptMutation(),
585
642
  },
586
643
  tool,
587
644
  this.ctx.ui,
@@ -591,8 +648,10 @@ export class EventController {
591
648
  component.setExpanded(this.ctx.toolOutputExpanded);
592
649
  this.ctx.pendingTools.set(content.id, component);
593
650
  addChatChild(this.ctx, component);
651
+ this.#recordVisibleTranscriptMutation();
594
652
  } else {
595
653
  this.ctx.pendingTools.get(content.id)?.updateArgs(renderArgs, content.id);
654
+ this.#consumeToolVisibleChange(this.ctx.pendingTools.get(content.id)!);
596
655
  }
597
656
 
598
657
  const args = content.arguments;
@@ -659,6 +718,7 @@ export class EventController {
659
718
  if (this.ctx.streamingMessage.stopReason !== "aborted" && this.ctx.streamingMessage.stopReason !== "error") {
660
719
  for (const [toolCallId, component] of this.ctx.pendingTools.entries()) {
661
720
  component.setArgsComplete(toolCallId);
721
+ this.#consumeToolVisibleChange(component);
662
722
  }
663
723
  }
664
724
  this.#lastAssistantComponent = this.ctx.streamingComponent;
@@ -685,6 +745,8 @@ export class EventController {
685
745
  this.ctx.pendingTools.set(event.toolCallId, group);
686
746
  }
687
747
  this.ctx.ui.requestRender();
748
+ this.#consumeToolVisibleChange(this.ctx.pendingTools.get(event.toolCallId)!);
749
+
688
750
  return;
689
751
  }
690
752
 
@@ -698,6 +760,7 @@ export class EventController {
698
760
  editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
699
761
  editAllowFuzzy: settings.get("edit.fuzzyMatch"),
700
762
  hashlineAutoDropPureInsertDuplicates: settings.get("edit.hashlineAutoDropPureInsertDuplicates"),
763
+ onVisibleTranscriptMutation: () => this.#observeVisibleTranscriptMutation(),
701
764
  },
702
765
  tool,
703
766
  this.ctx.ui,
@@ -707,6 +770,8 @@ export class EventController {
707
770
  component.setExpanded(this.ctx.toolOutputExpanded);
708
771
  this.ctx.pendingTools.set(event.toolCallId, component);
709
772
  addChatChild(this.ctx, component);
773
+ this.#recordVisibleTranscriptMutation();
774
+
710
775
  this.ctx.ui.requestRender();
711
776
  }
712
777
  }
@@ -723,6 +788,8 @@ export class EventController {
723
788
  !isFinalAsyncState,
724
789
  event.toolCallId,
725
790
  );
791
+ this.#consumeToolVisibleChange(component);
792
+
726
793
  if (isFinalAsyncState) {
727
794
  this.ctx.pendingTools.delete(event.toolCallId);
728
795
  this.#backgroundToolCallIds.delete(event.toolCallId);
@@ -737,6 +804,8 @@ export class EventController {
737
804
  const component = this.ctx.pendingTools.get(event.toolCallId);
738
805
  if (component) {
739
806
  component.updateResult({ ...event.result, isError: event.isError }, false, event.toolCallId);
807
+ this.#consumeToolVisibleChange(component);
808
+
740
809
  this.ctx.pendingTools.delete(event.toolCallId);
741
810
  }
742
811
  const asyncState = (event.result.details as { async?: { state?: string } } | undefined)?.async?.state;
@@ -761,6 +830,8 @@ export class EventController {
761
830
  const asyncState = (event.result.details as { async?: { state?: string } } | undefined)?.async?.state;
762
831
  const isBackgroundRunning = asyncState === "running";
763
832
  component.updateResult({ ...event.result, isError: event.isError }, isBackgroundRunning, event.toolCallId);
833
+ this.#consumeToolVisibleChange(component);
834
+
764
835
  if (isBackgroundRunning) {
765
836
  this.#backgroundToolCallIds.add(event.toolCallId);
766
837
  } else {
@@ -776,6 +847,8 @@ export class EventController {
776
847
  const asyncState = (event.result.details as { async?: { state?: string } } | undefined)?.async?.state;
777
848
  const isBackgroundRunning = asyncState === "running";
778
849
  component.updateResult({ ...event.result, isError: event.isError }, isBackgroundRunning, event.toolCallId);
850
+ this.#consumeToolVisibleChange(component);
851
+
779
852
  if (isBackgroundRunning) {
780
853
  this.#backgroundToolCallIds.add(event.toolCallId);
781
854
  } else {
@@ -893,6 +966,12 @@ export class EventController {
893
966
  } else if (event.willRetry && !isHandoffAction) {
894
967
  this.ctx.showStatus("Context overflow maintenance completed");
895
968
  }
969
+ } else if (
970
+ event.skipped &&
971
+ event.errorMessage?.startsWith("Context overflow recovery skipped: nothing eligible to compact.") &&
972
+ !isHandoffAction
973
+ ) {
974
+ this.ctx.showStatus(event.errorMessage);
896
975
  } else if (event.errorMessage) {
897
976
  this.ctx.showWarning(event.errorMessage);
898
977
  } else if (isHandoffAction) {
@@ -999,12 +1078,15 @@ export class EventController {
999
1078
  const component = new TtsrNotificationComponent(event.rules);
1000
1079
  component.setExpanded(this.ctx.toolOutputExpanded);
1001
1080
  addChatChild(this.ctx, component);
1081
+ this.#recordVisibleTranscriptMutation();
1082
+
1002
1083
  this.ctx.ui.requestRender();
1003
1084
  }
1004
1085
 
1005
1086
  async #handleTodoReminder(event: Extract<AgentSessionEvent, { type: "todo_reminder" }>): Promise<void> {
1006
1087
  const component = new TodoReminderComponent(event.todos, event.attempt, event.maxAttempts);
1007
1088
  addChatChild(this.ctx, component);
1089
+ this.#recordVisibleTranscriptMutation();
1008
1090
  this.ctx.ui.requestRender();
1009
1091
  }
1010
1092