@gajae-code/coding-agent 0.11.10 → 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 (249) hide show
  1. package/CHANGELOG.md +86 -2
  2. package/dist/types/cli/read-cli.d.ts +1 -0
  3. package/dist/types/cli.d.ts +8 -1
  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-profile-activation.d.ts +1 -0
  8. package/dist/types/config/model-profile-contract.d.ts +2 -0
  9. package/dist/types/config/model-registry.d.ts +4 -0
  10. package/dist/types/config/provider-auth-health.d.ts +14 -0
  11. package/dist/types/config/provider-ranking.d.ts +59 -0
  12. package/dist/types/config/settings-schema.d.ts +86 -3
  13. package/dist/types/config/settings.d.ts +3 -0
  14. package/dist/types/extensibility/extensions/types.d.ts +43 -2
  15. package/dist/types/extensibility/shared-events.d.ts +6 -0
  16. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  17. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  18. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  19. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  20. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  21. package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
  22. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  23. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +112 -1
  24. package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
  25. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  26. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  27. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
  28. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  29. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  30. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
  31. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  32. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  33. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  34. package/dist/types/main.d.ts +1 -1
  35. package/dist/types/memories/index.d.ts +2 -0
  36. package/dist/types/modes/acp/acp-agent.d.ts +22 -9
  37. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  38. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  39. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  40. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  41. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  42. package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
  43. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  44. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  45. package/dist/types/modes/components/welcome.d.ts +1 -0
  46. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  47. package/dist/types/modes/interactive-mode.d.ts +7 -1
  48. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  49. package/dist/types/modes/types.d.ts +2 -0
  50. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  51. package/dist/types/runtime/memory-domain.d.ts +9 -0
  52. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  53. package/dist/types/runtime/memory-guard.d.ts +39 -0
  54. package/dist/types/runtime/memory-limit.d.ts +11 -0
  55. package/dist/types/runtime-mcp/manager.d.ts +8 -0
  56. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  57. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  58. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  59. package/dist/types/sdk/broker/lifecycle.d.ts +7 -0
  60. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  61. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  62. package/dist/types/sdk/bus/index.d.ts +18 -2
  63. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
  64. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  65. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  66. package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
  67. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -4
  68. package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -3
  69. package/dist/types/sdk/host/control/operations.d.ts +2 -1
  70. package/dist/types/sdk/host/host.d.ts +2 -0
  71. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  72. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  73. package/dist/types/sdk/startup-capability.d.ts +6 -0
  74. package/dist/types/session/agent-session.d.ts +51 -19
  75. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  76. package/dist/types/session/internal/managed-session-scope.d.ts +27 -0
  77. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  78. package/dist/types/session/session-manager.d.ts +59 -13
  79. package/dist/types/session/streaming-output.d.ts +60 -5
  80. package/dist/types/skill-state/active-state.d.ts +0 -4
  81. package/dist/types/skill-state/workflow-hud.d.ts +3 -0
  82. package/dist/types/task/executor.d.ts +6 -0
  83. package/dist/types/task/types.d.ts +36 -0
  84. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  85. package/dist/types/tools/browser/launch.d.ts +7 -0
  86. package/dist/types/tools/fetch.d.ts +17 -1
  87. package/dist/types/tools/output-meta.d.ts +21 -1
  88. package/dist/types/tools/read-internals.d.ts +12 -0
  89. package/dist/types/tools/read.d.ts +45 -1
  90. package/dist/types/tools/resource-gc.d.ts +23 -0
  91. package/dist/types/tools/tool-result.d.ts +7 -1
  92. package/package.json +9 -13
  93. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  94. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  95. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  96. package/scripts/run-test-manifest.ts +6 -4
  97. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  98. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  99. package/src/capability/index.ts +8 -0
  100. package/src/cli/args.ts +14 -8
  101. package/src/cli/fast-help.ts +1 -1
  102. package/src/cli/read-cli.ts +2 -1
  103. package/src/cli.ts +49 -2
  104. package/src/commands/deep-interview.ts +29 -9
  105. package/src/commands/harness.ts +35 -12
  106. package/src/commands/read.ts +11 -2
  107. package/src/commands/sdk.ts +91 -7
  108. package/src/commands/team.ts +1 -1
  109. package/src/config/model-profile-activation.ts +1 -0
  110. package/src/config/model-profile-contract.ts +13 -0
  111. package/src/config/model-profiles.ts +24 -0
  112. package/src/config/model-registry.ts +21 -2
  113. package/src/config/provider-auth-health.ts +42 -0
  114. package/src/config/provider-ranking.ts +119 -0
  115. package/src/config/settings-schema.ts +84 -3
  116. package/src/config/settings.ts +254 -108
  117. package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
  118. package/src/defaults/gjc/skills/ralplan/SKILL.md +78 -26
  119. package/src/defaults/gjc/skills/team/SKILL.md +1 -1
  120. package/src/defaults/gjc/skills/ultragoal/SKILL.md +26 -16
  121. package/src/eval/py/executor.ts +7 -2
  122. package/src/extensibility/extensions/runner.ts +4 -0
  123. package/src/extensibility/extensions/types.ts +42 -4
  124. package/src/extensibility/shared-events.ts +5 -0
  125. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -68
  126. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  127. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  128. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  129. package/src/gjc-runtime/deep-interview-state.ts +12 -921
  130. package/src/gjc-runtime/gc-render.ts +1 -0
  131. package/src/gjc-runtime/gc-runtime.ts +11 -1
  132. package/src/gjc-runtime/linux-proc.ts +70 -36
  133. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  134. package/src/gjc-runtime/ralplan-runtime.ts +979 -71
  135. package/src/gjc-runtime/state-runtime.ts +50 -12
  136. package/src/gjc-runtime/state-writer.ts +70 -222
  137. package/src/gjc-runtime/team-launch.ts +34 -5
  138. package/src/gjc-runtime/team-runtime.ts +970 -33
  139. package/src/gjc-runtime/team-store.ts +32 -0
  140. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  141. package/src/gjc-runtime/team-workers.ts +1 -0
  142. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
  143. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  144. package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
  145. package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
  146. package/src/gjc-runtime/workflow-manifest.ts +34 -179
  147. package/src/hooks/native-skill-hook.ts +17 -4
  148. package/src/hooks/skill-state.ts +1 -23
  149. package/src/internal-urls/docs-index.generated.ts +10 -11
  150. package/src/internal-urls/local-protocol.ts +23 -4
  151. package/src/internal-urls/local-root-gc.ts +127 -0
  152. package/src/main.ts +9 -5
  153. package/src/memories/index.ts +10 -0
  154. package/src/modes/DESIGN.md +47 -0
  155. package/src/modes/acp/acp-agent.ts +597 -67
  156. package/src/modes/acp/acp-event-mapper.ts +106 -12
  157. package/src/modes/acp/acp-mode.ts +39 -3
  158. package/src/modes/components/assistant-message.ts +123 -22
  159. package/src/modes/components/irc-sidebar.ts +176 -62
  160. package/src/modes/components/model-selector.ts +76 -5
  161. package/src/modes/components/notifications-settings-editor.ts +1 -1
  162. package/src/modes/components/oauth-selector.ts +42 -12
  163. package/src/modes/components/queue-pane.ts +55 -9
  164. package/src/modes/components/queued-message-selector.ts +53 -10
  165. package/src/modes/components/read-tool-group.ts +82 -14
  166. package/src/modes/components/session-observer-overlay.ts +187 -32
  167. package/src/modes/components/settings-selector.ts +60 -18
  168. package/src/modes/components/tool-execution.ts +138 -27
  169. package/src/modes/components/tool-status-header.ts +1 -4
  170. package/src/modes/components/welcome.ts +18 -3
  171. package/src/modes/controllers/command-controller.ts +4 -0
  172. package/src/modes/controllers/event-controller.ts +109 -27
  173. package/src/modes/controllers/extension-ui-controller.ts +23 -12
  174. package/src/modes/controllers/input-controller.ts +62 -6
  175. package/src/modes/controllers/selector-controller.ts +83 -37
  176. package/src/modes/interactive-mode.ts +127 -29
  177. package/src/modes/irc-observation-ledger.ts +71 -15
  178. package/src/modes/runtime-init.ts +20 -2
  179. package/src/modes/types.ts +2 -0
  180. package/src/modes/utils/ui-helpers.ts +94 -1
  181. package/src/prompts/agents/architect.md +9 -1
  182. package/src/prompts/agents/critic.md +9 -0
  183. package/src/prompts/agents/executor.md +1 -1
  184. package/src/prompts/system/system-prompt.md +0 -1
  185. package/src/prompts/tools/read.md +4 -2
  186. package/src/runtime/memory-domain.ts +66 -0
  187. package/src/runtime/memory-guard-contract.ts +73 -0
  188. package/src/runtime/memory-guard.ts +292 -0
  189. package/src/runtime/memory-limit.ts +50 -0
  190. package/src/runtime-mcp/manager.ts +26 -6
  191. package/src/runtime-mcp/smithery-auth.ts +17 -2
  192. package/src/runtime-mcp/smithery-connect.ts +11 -1
  193. package/src/sdk/acp/adapter.ts +8 -1
  194. package/src/sdk/acp/final-text.ts +40 -0
  195. package/src/sdk/acp/mcp.ts +26 -0
  196. package/src/sdk/broker/lifecycle.ts +107 -7
  197. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  198. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  199. package/src/sdk/bus/config.ts +1 -1
  200. package/src/sdk/bus/control-drain-lease.ts +104 -0
  201. package/src/sdk/bus/index.ts +503 -60
  202. package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
  203. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  204. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  205. package/src/sdk/bus/reconciliation-store.ts +224 -0
  206. package/src/sdk/bus/telegram-daemon-contract.ts +6 -4
  207. package/src/sdk/bus/telegram-daemon.ts +531 -53
  208. package/src/sdk/host/control/dispatch.ts +7 -1
  209. package/src/sdk/host/control/operations.ts +2 -1
  210. package/src/sdk/host/host.ts +25 -3
  211. package/src/sdk/host/query/handlers.ts +55 -0
  212. package/src/sdk/lifecycle-session.ts +19 -3
  213. package/src/sdk/mcp/server.ts +46 -18
  214. package/src/sdk/protocol/operation-inventory.generated.json +76 -0
  215. package/src/sdk/protocol/operation-registry.ts +8 -2
  216. package/src/sdk/session.ts +25 -2
  217. package/src/sdk/startup-capability.ts +6 -0
  218. package/src/session/agent-session.ts +720 -190
  219. package/src/session/fallback-chain-controller.ts +47 -8
  220. package/src/session/internal/managed-session-scope.ts +40 -10
  221. package/src/session/internal/native-publish-outcome.ts +8 -3
  222. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  223. package/src/session/session-manager.ts +1372 -42
  224. package/src/session/streaming-output.ts +262 -43
  225. package/src/setup/provider-onboarding.ts +19 -4
  226. package/src/skill-state/active-state.ts +0 -10
  227. package/src/skill-state/workflow-hud.ts +25 -2
  228. package/src/skill-state/workflow-mutation-guard.ts +65 -7
  229. package/src/task/executor.ts +11 -3
  230. package/src/task/index.ts +2 -0
  231. package/src/task/types.ts +22 -11
  232. package/src/task/ultragoal-redteam-activation.ts +75 -0
  233. package/src/tools/ask.ts +89 -69
  234. package/src/tools/browser/launch.ts +41 -6
  235. package/src/tools/fetch.ts +114 -23
  236. package/src/tools/output-meta.ts +176 -9
  237. package/src/tools/path-utils.ts +5 -1
  238. package/src/tools/read-internals.ts +44 -0
  239. package/src/tools/read.ts +1542 -310
  240. package/src/tools/resource-gc.ts +665 -125
  241. package/src/tools/tool-result.ts +14 -1
  242. package/src/web/search/providers/codex.ts +1 -1
  243. package/src/workflow/workflow-intent-diff.ts +4 -1
  244. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  245. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  246. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  247. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  248. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  249. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
@@ -235,18 +235,16 @@ function getSavedUsageMode(): UsageMode {
235
235
  return segmentOptions.usage?.mode === "remaining" ? "remaining" : "used";
236
236
  }
237
237
 
238
- function setSavedUsageMode(mode: string): StatusLineSegmentOptions {
238
+ function getUsageModeSettings(mode: string): StatusLineSegmentOptions {
239
239
  const normalizedMode: UsageMode = mode === "remaining" ? "remaining" : "used";
240
240
  const segmentOptions = settings.get("statusLine.segmentOptions") as StatusLineSegmentOptions;
241
- const nextOptions: StatusLineSegmentOptions = {
241
+ return {
242
242
  ...segmentOptions,
243
243
  usage: {
244
244
  ...(segmentOptions.usage ?? {}),
245
245
  mode: normalizedMode,
246
246
  },
247
247
  };
248
- settings.set("statusLine.segmentOptions", nextOptions as Record<string, unknown>);
249
- return nextOptions;
250
248
  }
251
249
 
252
250
  function statusSegmentLabel(id: StatusLineSegmentId): string {
@@ -613,14 +611,17 @@ class StatusLineCustomEditor extends Container {
613
611
  }
614
612
 
615
613
  #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
- );
614
+ const saved = commitInteractiveSettings(this.callbacks, () => {
615
+ settings.set("statusLine.preset", "custom");
616
+ settings.set("statusLine.leftSegments", [...this.#draft.leftSegments]);
617
+ settings.set("statusLine.rightSegments", [...this.#draft.rightSegments]);
618
+ settings.set("statusLine.separator", this.#draft.separator);
619
+ settings.set(
620
+ "statusLine.segmentOptions",
621
+ cloneSegmentOptions(this.#draft.segmentOptions) as Record<string, unknown>,
622
+ );
623
+ });
624
+ if (!saved) return;
624
625
  this.callbacks.onChange("statusLine.preset", "custom");
625
626
  this.callbacks.onChange("statusLine.leftSegments", [...this.#draft.leftSegments]);
626
627
  this.callbacks.onChange("statusLine.rightSegments", [...this.#draft.rightSegments]);
@@ -707,9 +708,29 @@ export interface SettingsCallbacks {
707
708
  getStatusLinePreview?: (width?: number) => string;
708
709
  /** Called when plugins change */
709
710
  onPluginsChanged?: () => void;
711
+ /** Called when an interactive setting cannot be committed. */
712
+ onError?: (message: string) => void;
710
713
  /** Called when settings panel is closed */
711
714
  onCancel: () => void;
712
715
  }
716
+ function commitInteractiveSettings(callbacks: SettingsCallbacks, commit: () => void): boolean {
717
+ if (!settings.canWriteDurableConfig()) {
718
+ callbacks.onError?.(
719
+ "Cannot change settings while config.yml has invalid YAML syntax. Repair config.yml and reload settings.",
720
+ );
721
+ return false;
722
+ }
723
+ try {
724
+ commit();
725
+ return true;
726
+ } catch (error) {
727
+ if (!settings.canWriteDurableConfig()) {
728
+ callbacks.onError?.(error instanceof Error ? error.message : String(error));
729
+ return false;
730
+ }
731
+ throw error;
732
+ }
733
+ }
713
734
 
714
735
  /**
715
736
  * Main tabbed settings selector component.
@@ -994,11 +1015,18 @@ export class SettingsSelectorComponent extends Container {
994
1015
  // The shared pet commit policy rechecks capability immediately
995
1016
  // before mutation and persists only on acceptance; the settings
996
1017
  // surface must not persist ahead of that result.
997
- const accepted = this.callbacks.onPetCommit?.(value) ?? false;
1018
+ let accepted = false;
1019
+ if (
1020
+ !commitInteractiveSettings(this.callbacks, () => {
1021
+ accepted = this.callbacks.onPetCommit?.(value) ?? false;
1022
+ })
1023
+ ) {
1024
+ return;
1025
+ }
998
1026
  done(accepted ? value : undefined);
999
1027
  return;
1000
1028
  }
1001
- this.#setSettingValue(def.path, value);
1029
+ if (!commitInteractiveSettings(this.callbacks, () => this.#setSettingValue(def.path, value))) return;
1002
1030
  this.callbacks.onChange(def.path, value);
1003
1031
  done(value);
1004
1032
  },
@@ -1031,7 +1059,7 @@ export class SettingsSelectorComponent extends Container {
1031
1059
  value => {
1032
1060
  // Empty string clears the setting; undefined-typed string settings
1033
1061
  // store "" which the browser.ts expandPath ignores (no-op fallback).
1034
- this.#setSettingValue(def.path, value);
1062
+ if (!commitInteractiveSettings(this.callbacks, () => this.#setSettingValue(def.path, value))) return;
1035
1063
  this.callbacks.onChange(def.path, value);
1036
1064
  wrappedDone(value);
1037
1065
  },
@@ -1089,7 +1117,15 @@ export class SettingsSelectorComponent extends Container {
1089
1117
  getSettingsListTheme(),
1090
1118
  (id, newValue) => {
1091
1119
  if (id === STATUS_LINE_USAGE_MODE_ID) {
1092
- const segmentOptions = setSavedUsageMode(newValue);
1120
+ const segmentOptions = getUsageModeSettings(newValue);
1121
+ if (
1122
+ !commitInteractiveSettings(this.callbacks, () => {
1123
+ settings.set("statusLine.segmentOptions", segmentOptions as Record<string, unknown>);
1124
+ })
1125
+ ) {
1126
+ this.#refreshCurrentTabItems(defs);
1127
+ return;
1128
+ }
1093
1129
  this.callbacks.onChange("statusLine.segmentOptions", segmentOptions);
1094
1130
  if (tabId === "appearance") {
1095
1131
  this.#triggerStatusLinePreview();
@@ -1105,14 +1141,20 @@ export class SettingsSelectorComponent extends Container {
1105
1141
 
1106
1142
  if (def.type === "boolean") {
1107
1143
  const boolValue = newValue === "true";
1108
- settings.set(path, boolValue as never);
1144
+ if (!commitInteractiveSettings(this.callbacks, () => settings.set(path, boolValue as never))) {
1145
+ this.#refreshCurrentTabItems(defs);
1146
+ return;
1147
+ }
1109
1148
  this.callbacks.onChange(path, boolValue);
1110
1149
 
1111
1150
  if (tabId === "appearance") {
1112
1151
  this.#triggerStatusLinePreview();
1113
1152
  }
1114
1153
  } else if (def.type === "enum") {
1115
- settings.set(path, newValue as never);
1154
+ if (!commitInteractiveSettings(this.callbacks, () => settings.set(path, newValue as never))) {
1155
+ this.#refreshCurrentTabItems(defs);
1156
+ return;
1157
+ }
1116
1158
  this.callbacks.onChange(path, newValue);
1117
1159
  }
1118
1160
  // Submenu/text types already persisted the value inside their own
@@ -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
  }
@@ -70,16 +70,13 @@ export interface StatusLineActionHint {
70
70
 
71
71
  const ACTION_HINT_PRIORITY: readonly AppKeybinding[] = [
72
72
  "app.message.sendNow",
73
- "app.message.queue",
74
73
  "app.message.followUp",
74
+ "app.message.queue",
75
75
  "app.message.dequeue",
76
76
  "app.commandPalette.open",
77
77
  "app.plan.toggle",
78
78
  "app.mode.cycle",
79
- "app.thinking.cycle",
80
- "app.model.select",
81
79
  "app.model.cycleForward",
82
- "app.history.search",
83
80
  "app.session.togglePath",
84
81
  "app.session.toggleSort",
85
82
  "app.session.rename",
@@ -20,6 +20,7 @@ export interface WelcomeComponentOptions {
20
20
  getViewportRows?: () => number | undefined;
21
21
  getReservedBottomRows?: (termWidth: number) => number;
22
22
  changelogMarkdown?: string;
23
+ rightGutterWidth?: number;
23
24
  collapseChangelog?: boolean;
24
25
  buildLabel?: string;
25
26
  keyDisplayContext?: KeyDisplayContext;
@@ -110,7 +111,8 @@ export class WelcomeComponent implements Component {
110
111
  }
111
112
 
112
113
  render(termWidth: number): string[] {
113
- const boxWidth = Math.max(0, termWidth);
114
+ const rightGutterWidth = this.#rightGutterWidth(termWidth);
115
+ const boxWidth = Math.max(0, termWidth - rightGutterWidth);
114
116
  if (boxWidth < 4) {
115
117
  return [];
116
118
  }
@@ -245,7 +247,7 @@ export class WelcomeComponent implements Component {
245
247
  lines.push(tl + titleStyled + theme.fg("dim", hChar.repeat(afterTitle)) + tr);
246
248
  }
247
249
  if (outputRows === 1) {
248
- return lines;
250
+ return this.#withRightGutter(lines, rightGutterWidth);
249
251
  }
250
252
 
251
253
  if (showRightColumn) {
@@ -273,7 +275,7 @@ export class WelcomeComponent implements Component {
273
275
  lines.push(bl + h.repeat(leftCol) + br);
274
276
  }
275
277
 
276
- return lines;
278
+ return this.#withRightGutter(lines, rightGutterWidth);
277
279
  }
278
280
 
279
281
  /** Center text within a given width */
@@ -295,6 +297,19 @@ export class WelcomeComponent implements Component {
295
297
  }
296
298
  return str + padding(width - visLen);
297
299
  }
300
+ #rightGutterWidth(termWidth: number): number {
301
+ const configured = this.options.rightGutterWidth ?? 0;
302
+ if (!Number.isFinite(configured) || configured <= 0) return 0;
303
+ const gutterWidth = Math.floor(configured);
304
+ return Math.min(gutterWidth, Math.max(0, termWidth - 4));
305
+ }
306
+
307
+ #withRightGutter(lines: string[], rightGutterWidth: number): string[] {
308
+ if (rightGutterWidth <= 0) return lines;
309
+ const gutter = padding(rightGutterWidth);
310
+ return lines.map(line => line + gutter);
311
+ }
312
+
298
313
  #targetRows(termWidth: number): number | undefined {
299
314
  const viewportRows = this.options.getViewportRows?.();
300
315
  if (typeof viewportRows !== "number" || !Number.isFinite(viewportRows) || viewportRows <= 0) {
@@ -65,6 +65,8 @@ export function buildHelpMarkdown(keybindings: Pick<KeybindingsManager, "getAcce
65
65
  const displayKey = (action: Keybinding): string => keybindings.getAccessibleDisplayString(action) || "Disabled";
66
66
  const sessionNewKey = formatHotkeyMarkdownCode(displayKey("app.session.new"));
67
67
  const selectModelKey = formatHotkeyMarkdownCode(displayKey("app.model.select"));
68
+ const queueMessageKey = formatHotkeyMarkdownCode(displayKey("app.message.queue"));
69
+ const dequeueMessageKey = formatHotkeyMarkdownCode(displayKey("app.message.dequeue"));
68
70
  const autocompleteNavigationKeys = formatHotkeyMarkdownCode(
69
71
  `${displayKey("tui.select.up")}/${displayKey("tui.select.down")}`,
70
72
  false,
@@ -84,6 +86,8 @@ export function buildHelpMarkdown(keybindings: Pick<KeybindingsManager, "getAcce
84
86
  "| Show session details | `/session info` |",
85
87
  "| Delete current session transcript/artifacts | `/session delete` |",
86
88
  `| Select a model | \`/model\` or ${selectModelKey} |`,
89
+ `| Queue a message for the next turn | ${queueMessageKey} |`,
90
+ `| Select or edit a queued message | ${dequeueMessageKey} |`,
87
91
  "| Show all shortcuts | `?` on an empty prompt or `/hotkeys` |",
88
92
  "",
89
93
  "**Finding commands**",