@gajae-code/coding-agent 0.12.0 → 0.12.2

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 (247) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/dist/types/async/job-manager.d.ts +17 -3
  3. package/dist/types/config/settings-schema.d.ts +56 -2
  4. package/dist/types/exa/mcp-client.d.ts +10 -1
  5. package/dist/types/exec/bash-executor.d.ts +45 -12
  6. package/dist/types/extensibility/extensions/types.d.ts +16 -2
  7. package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
  8. package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
  9. package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
  10. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
  11. package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
  12. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
  13. package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
  14. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  15. package/dist/types/gjc-runtime/ralplan-review-conflicts.d.ts +114 -0
  16. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +23 -3
  17. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  18. package/dist/types/gjc-runtime/team-runtime.d.ts +25 -0
  19. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
  20. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +10 -0
  21. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +37 -80
  22. package/dist/types/modes/acp/acp-agent.d.ts +2 -19
  23. package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
  24. package/dist/types/modes/components/irc-sidebar.d.ts +16 -0
  25. package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
  26. package/dist/types/modes/components/session-observer-overlay.d.ts +18 -0
  27. package/dist/types/modes/components/settings-selector.d.ts +9 -0
  28. package/dist/types/modes/components/status-line/gh.d.ts +5 -0
  29. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
  30. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  31. package/dist/types/modes/controllers/event-controller.d.ts +1 -1
  32. package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
  33. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  34. package/dist/types/modes/interactive-mode.d.ts +9 -0
  35. package/dist/types/modes/theme/theme.d.ts +3 -1
  36. package/dist/types/modes/types.d.ts +32 -0
  37. package/dist/types/reminders/star-reminder.d.ts +3 -13
  38. package/dist/types/runtime-mcp/manager.d.ts +1 -0
  39. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  40. package/dist/types/sdk/broker/transport.d.ts +2 -0
  41. package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
  42. package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -1
  43. package/dist/types/sdk/bus/config.d.ts +6 -2
  44. package/dist/types/sdk/bus/index.d.ts +29 -0
  45. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +8 -1
  46. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  47. package/dist/types/sdk/bus/reconciliation-store.d.ts +7 -3
  48. package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
  49. package/dist/types/sdk/bus/rich-render.d.ts +2 -0
  50. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
  51. package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
  52. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -4
  53. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
  54. package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
  55. package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
  56. package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
  57. package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
  58. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  59. package/dist/types/sdk/prompt-status.d.ts +23 -3
  60. package/dist/types/sdk/session.d.ts +6 -0
  61. package/dist/types/session/agent-session.d.ts +40 -2
  62. package/dist/types/session/artifacts.d.ts +8 -0
  63. package/dist/types/session/blob-store.d.ts +57 -4
  64. package/dist/types/session/internal/managed-session-storage.d.ts +30 -4
  65. package/dist/types/session/session-manager.d.ts +23 -6
  66. package/dist/types/session/session-storage.d.ts +4 -0
  67. package/dist/types/session/streaming-output.d.ts +36 -7
  68. package/dist/types/setup/credential-import.d.ts +12 -0
  69. package/dist/types/skill-state/active-state.d.ts +12 -0
  70. package/dist/types/skill-state/workflow-hud.d.ts +6 -0
  71. package/dist/types/task/executor.d.ts +3 -2
  72. package/dist/types/task/index.d.ts +7 -1
  73. package/dist/types/task/output-manager.d.ts +1 -0
  74. package/dist/types/task/provider-retry-status.d.ts +8 -0
  75. package/dist/types/task/receipt.d.ts +9 -1
  76. package/dist/types/task/types.d.ts +23 -0
  77. package/dist/types/tools/ask-answer-registry.d.ts +2 -2
  78. package/dist/types/tools/bash-interactive.d.ts +4 -1
  79. package/dist/types/tools/bash.d.ts +3 -1
  80. package/dist/types/tools/image-gen.d.ts +4 -0
  81. package/dist/types/tools/index.d.ts +6 -0
  82. package/dist/types/tools/output-meta.d.ts +23 -2
  83. package/dist/types/tools/path-utils.d.ts +1 -0
  84. package/dist/types/tools/read.d.ts +0 -6
  85. package/dist/types/tools/review.d.ts +5 -7
  86. package/dist/types/tools/subagent.d.ts +2 -0
  87. package/dist/types/utils/gh.d.ts +13 -0
  88. package/dist/types/utils/image-resize.d.ts +1 -0
  89. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  90. package/dist/types/web/search/providers/searxng.d.ts +7 -0
  91. package/dist/types/web/search/providers/xai.d.ts +2 -0
  92. package/package.json +10 -8
  93. package/scripts/acp-conformance-agent.ts +218 -0
  94. package/scripts/capture-gjc-bundle-settings.ts +306 -0
  95. package/scripts/dogfood-ralplan-review-conflicts.ts +272 -0
  96. package/scripts/generate-sdk-operation-inventory.ts +5 -0
  97. package/scripts/resident-memory-bench.ts +1341 -0
  98. package/scripts/run-acp-conformance.ts +290 -0
  99. package/src/async/job-manager.ts +99 -39
  100. package/src/cli/fast-help.ts +1 -1
  101. package/src/cli/plugin-cli.ts +176 -18
  102. package/src/commands/team.ts +19 -1
  103. package/src/config/file-lock.ts +11 -2
  104. package/src/config/settings-schema.ts +53 -2
  105. package/src/coordinator-mcp/question-gate-codec.ts +1 -1
  106. package/src/coordinator-mcp/server.ts +5 -12
  107. package/src/debug/index.ts +42 -17
  108. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -0
  109. package/src/defaults/gjc/skills/ralplan/SKILL.md +25 -20
  110. package/src/defaults/gjc/skills/team/SKILL.md +2 -0
  111. package/src/defaults/gjc/skills/ultragoal/SKILL.md +39 -26
  112. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
  113. package/src/defaults/gjc-defaults.ts +4 -4
  114. package/src/exa/mcp-client.ts +12 -3
  115. package/src/exec/bash-executor.ts +144 -27
  116. package/src/extensibility/extensions/runner.ts +15 -0
  117. package/src/extensibility/extensions/types.ts +18 -1
  118. package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
  119. package/src/extensibility/gjc-plugins/index.ts +19 -2
  120. package/src/extensibility/gjc-plugins/installer.ts +189 -50
  121. package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
  122. package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
  123. package/src/extensibility/gjc-plugins/observability.ts +3 -3
  124. package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
  125. package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
  126. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
  127. package/src/extensibility/gjc-plugins/schema.ts +84 -12
  128. package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
  129. package/src/extensibility/gjc-plugins/types.ts +141 -0
  130. package/src/extensibility/gjc-plugins/validation.ts +9 -0
  131. package/src/gjc-runtime/launch-tmux.ts +25 -6
  132. package/src/gjc-runtime/launch-worktree.ts +103 -1
  133. package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
  134. package/src/gjc-runtime/managed-owner-admission.ts +1 -1
  135. package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
  136. package/src/gjc-runtime/ralplan-runtime.ts +491 -27
  137. package/src/gjc-runtime/team-launch.ts +8 -0
  138. package/src/gjc-runtime/team-runtime.ts +140 -32
  139. package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
  140. package/src/gjc-runtime/tmux-owner-isolation.ts +10 -0
  141. package/src/gjc-runtime/tmux-sessions.ts +58 -13
  142. package/src/gjc-runtime/ultragoal-evidence.ts +6 -74
  143. package/src/gjc-runtime/ultragoal-runtime.ts +635 -612
  144. package/src/gjc-runtime/workflow-manifest.generated.json +37 -99
  145. package/src/gjc-runtime/workflow-manifest.ts +21 -70
  146. package/src/internal-urls/docs-index.generated.ts +16 -16
  147. package/src/modes/DESIGN.md +104 -0
  148. package/src/modes/acp/acp-agent.ts +298 -186
  149. package/src/modes/components/gjc-bundle-settings.ts +508 -0
  150. package/src/modes/components/irc-sidebar.ts +30 -1
  151. package/src/modes/components/notifications-settings-editor.ts +12 -1
  152. package/src/modes/components/oauth-selector.ts +68 -30
  153. package/src/modes/components/session-observer-overlay.ts +305 -86
  154. package/src/modes/components/settings-selector.ts +56 -5
  155. package/src/modes/components/status-line/gh.ts +16 -0
  156. package/src/modes/components/tool-status-header.ts +3 -15
  157. package/src/modes/components/transcript-viewer-overlay.ts +99 -24
  158. package/src/modes/controllers/command-controller.ts +42 -34
  159. package/src/modes/controllers/event-controller.ts +27 -16
  160. package/src/modes/controllers/extension-ui-controller.ts +193 -34
  161. package/src/modes/controllers/selector-controller.ts +160 -22
  162. package/src/modes/interactive-mode.ts +189 -62
  163. package/src/modes/jobs-observer.ts +1 -1
  164. package/src/modes/runtime-init.ts +3 -0
  165. package/src/modes/theme/theme.ts +11 -9
  166. package/src/modes/types.ts +88 -0
  167. package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
  168. package/src/prompts/system/eager-todo.md +8 -4
  169. package/src/prompts/system/system-prompt.md +11 -4
  170. package/src/prompts/tools/bash.md +5 -5
  171. package/src/prompts/tools/search-tool-bm25.md +5 -0
  172. package/src/prompts/tools/task.md +1 -1
  173. package/src/reminders/star-reminder.ts +4 -42
  174. package/src/runtime-mcp/manager.ts +19 -4
  175. package/src/sdk/acp/adapter.ts +22 -6
  176. package/src/sdk/broker/lifecycle.ts +18 -13
  177. package/src/sdk/broker/transport.ts +35 -3
  178. package/src/sdk/bus/attachment-registry.ts +5 -1
  179. package/src/sdk/bus/chat-daemon-cli.ts +4 -39
  180. package/src/sdk/bus/chat-daemon-control.ts +7 -3
  181. package/src/sdk/bus/config.ts +18 -2
  182. package/src/sdk/bus/index.ts +661 -126
  183. package/src/sdk/bus/kind-aware-reconciliation.ts +148 -73
  184. package/src/sdk/bus/lifecycle-commands.ts +4 -1
  185. package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
  186. package/src/sdk/bus/reconciliation-store.ts +95 -16
  187. package/src/sdk/bus/rich-draft.ts +1 -1
  188. package/src/sdk/bus/rich-render.ts +2 -2
  189. package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
  190. package/src/sdk/bus/telegram-cli.ts +33 -8
  191. package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
  192. package/src/sdk/bus/telegram-daemon-contract.ts +12 -4
  193. package/src/sdk/bus/telegram-daemon.ts +916 -23
  194. package/src/sdk/bus/telegram-reference.ts +40 -3
  195. package/src/sdk/bus/threaded-render.ts +48 -3
  196. package/src/sdk/bus/topic-registry.ts +20 -0
  197. package/src/sdk/host/query/revision-store.ts +36 -2
  198. package/src/sdk/host/reverse-leases.ts +44 -7
  199. package/src/sdk/prompt-status.ts +16 -3
  200. package/src/sdk/protocol/operation-inventory.generated.json +33 -0
  201. package/src/sdk/session.ts +89 -9
  202. package/src/session/agent-session.ts +844 -173
  203. package/src/session/artifacts.ts +22 -0
  204. package/src/session/auth-broker-config.ts +10 -3
  205. package/src/session/blob-store.ts +915 -24
  206. package/src/session/internal/managed-session-scope.ts +200 -111
  207. package/src/session/internal/managed-session-storage.ts +492 -62
  208. package/src/session/session-manager.ts +1614 -435
  209. package/src/session/session-storage.ts +38 -45
  210. package/src/session/streaming-output.ts +238 -68
  211. package/src/setup/credential-import.ts +75 -13
  212. package/src/skill-state/active-state.ts +32 -0
  213. package/src/skill-state/workflow-hud.ts +17 -0
  214. package/src/skill-state/workflow-mutation-guard.ts +496 -2
  215. package/src/task/executor.ts +122 -39
  216. package/src/task/index.ts +280 -45
  217. package/src/task/output-manager.ts +24 -14
  218. package/src/task/provider-retry-status.ts +16 -0
  219. package/src/task/receipt.ts +34 -8
  220. package/src/task/render.ts +6 -2
  221. package/src/task/types.ts +108 -0
  222. package/src/tools/ask-answer-registry.ts +12 -6
  223. package/src/tools/ask.ts +86 -55
  224. package/src/tools/ast-edit.ts +1 -0
  225. package/src/tools/ast-grep.ts +1 -0
  226. package/src/tools/bash-interactive.ts +8 -3
  227. package/src/tools/bash.ts +503 -60
  228. package/src/tools/find.ts +1 -0
  229. package/src/tools/image-gen.ts +29 -5
  230. package/src/tools/index.ts +6 -0
  231. package/src/tools/output-meta.ts +108 -14
  232. package/src/tools/path-utils.ts +3 -2
  233. package/src/tools/read.ts +18 -0
  234. package/src/tools/review.ts +5 -7
  235. package/src/tools/search.ts +1 -0
  236. package/src/tools/subagent-render.ts +6 -2
  237. package/src/tools/subagent.ts +6 -0
  238. package/src/tools/telegram-send.ts +6 -3
  239. package/src/utils/gh.ts +39 -0
  240. package/src/utils/image-resize.ts +3 -0
  241. package/src/web/search/providers/anthropic.ts +4 -4
  242. package/src/web/search/providers/kimi.ts +15 -5
  243. package/src/web/search/providers/searxng.ts +47 -4
  244. package/src/web/search/providers/xai.ts +7 -2
  245. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
  246. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
  247. 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,
@@ -650,6 +652,7 @@ function getSettingsTabs(): Tab[] {
650
652
  return { id, label: `${icon} ${meta.label}` };
651
653
  }),
652
654
  { id: "plugins", label: `${theme.icon.package} Plugins` },
655
+ { id: "gjc-bundles", label: `${theme.icon.package} GJC Bundles` },
653
656
  ];
654
657
  }
655
658
 
@@ -672,6 +675,14 @@ export interface SettingsRuntimeContext {
672
675
  petAvailable?: boolean;
673
676
  /** Terminal environment used to select unavailable-pet guidance. Omitted in production to use Bun.env. */
674
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;
675
686
  }
676
687
 
677
688
  /** Status line settings subset for preview */
@@ -740,10 +751,11 @@ export class SettingsSelectorComponent extends Container {
740
751
  #tabBar: TabBar;
741
752
  #currentList: SettingsList | null = null;
742
753
  #pluginComponent: PluginSettingsComponent | null = null;
754
+ #gjcBundleComponent: GjcBundleSettingsComponent | null = null;
743
755
  #notificationsEditor: NotificationsSettingsEditorComponent | null = null;
744
756
  #statusPreviewContainer: Container | null = null;
745
757
  #statusPreviewText: Text | null = null;
746
- #currentTabId: SettingTab | "plugins" = "appearance";
758
+ #currentTabId: SettingTab | "plugins" | "gjc-bundles" = "appearance";
747
759
  #textInputActive = false;
748
760
 
749
761
  constructor(
@@ -759,7 +771,7 @@ export class SettingsSelectorComponent extends Container {
759
771
  // Tab bar
760
772
  this.#tabBar = new TabBar("Settings", getSettingsTabs(), getTabBarTheme());
761
773
  this.#tabBar.onTabChange = () => {
762
- this.#switchToTab(this.#tabBar.getActiveTab().id as SettingTab | "plugins");
774
+ this.#switchToTab(this.#tabBar.getActiveTab().id as SettingTab | "plugins" | "gjc-bundles");
763
775
  };
764
776
 
765
777
  this.addChild(this.#tabBar);
@@ -774,7 +786,7 @@ export class SettingsSelectorComponent extends Container {
774
786
  this.addChild(new DynamicBorder());
775
787
  }
776
788
 
777
- #switchToTab(tabId: SettingTab | "plugins"): void {
789
+ #switchToTab(tabId: SettingTab | "plugins" | "gjc-bundles"): void {
778
790
  if (this.#currentTabId === "notifications" && tabId !== "notifications" && !this.#disposeNotificationsEditor()) {
779
791
  return;
780
792
  }
@@ -789,6 +801,11 @@ export class SettingsSelectorComponent extends Container {
789
801
  this.removeChild(this.#pluginComponent);
790
802
  this.#pluginComponent = null;
791
803
  }
804
+ if (this.#gjcBundleComponent) {
805
+ this.removeChild(this.#gjcBundleComponent);
806
+ this.#gjcBundleComponent.dispose();
807
+ this.#gjcBundleComponent = null;
808
+ }
792
809
  if (this.#statusPreviewContainer) {
793
810
  this.removeChild(this.#statusPreviewContainer);
794
811
  this.#statusPreviewContainer = null;
@@ -801,6 +818,8 @@ export class SettingsSelectorComponent extends Container {
801
818
 
802
819
  if (tabId === "plugins") {
803
820
  this.#showPluginsTab();
821
+ } else if (tabId === "gjc-bundles") {
822
+ this.#showGjcBundlesTab();
804
823
  } else if (tabId === "notifications") {
805
824
  this.#showNotificationsTab();
806
825
  } else {
@@ -1224,7 +1243,7 @@ export class SettingsSelectorComponent extends Container {
1224
1243
 
1225
1244
  /** Re-evaluate condition gates against the current settings and refresh the active list. */
1226
1245
  #refreshCurrentTabItems(defs: SettingDef[]): void {
1227
- if (this.#currentTabId === "plugins" || !this.#currentList) return;
1246
+ if (this.#currentTabId === "plugins" || this.#currentTabId === "gjc-bundles" || !this.#currentList) return;
1228
1247
  this.#currentList.setItems(this.#buildItemsForTab(defs, this.#currentTabId));
1229
1248
  }
1230
1249
 
@@ -1271,15 +1290,31 @@ export class SettingsSelectorComponent extends Container {
1271
1290
  });
1272
1291
  this.addChild(this.#pluginComponent);
1273
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
+ }
1274
1307
 
1275
1308
  getFocusComponent(): Component {
1276
- return (this.#currentList || this.#pluginComponent || this.#notificationsEditor)!;
1309
+ return (this.#currentList || this.#pluginComponent || this.#gjcBundleComponent || this.#notificationsEditor)!;
1277
1310
  }
1278
1311
 
1279
1312
  override dispose(): void {
1280
1313
  if (this.#notificationsEditor?.navigationLocked) return;
1281
1314
  this.#notificationsEditor?.dispose();
1282
1315
  this.#notificationsEditor = null;
1316
+ this.#gjcBundleComponent?.dispose();
1317
+ this.#gjcBundleComponent = null;
1283
1318
  super.dispose();
1284
1319
  }
1285
1320
 
@@ -1301,6 +1336,18 @@ export class SettingsSelectorComponent extends Container {
1301
1336
  this.#notificationsEditor.handleInput(data);
1302
1337
  return;
1303
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
+ }
1304
1351
 
1305
1352
  // Handle tab switching — but NOT when a text input is active, since
1306
1353
  // arrow keys must reach the cursor and Tab must not switch tabs.
@@ -1319,6 +1366,10 @@ export class SettingsSelectorComponent extends Container {
1319
1366
  this.#pluginComponent.handleInput(data);
1320
1367
  return;
1321
1368
  }
1369
+ if (this.#gjcBundleComponent) {
1370
+ this.#gjcBundleComponent.handleInput(data);
1371
+ return;
1372
+ }
1322
1373
 
1323
1374
  // Fallback for future top-level content that does not own cancellation.
1324
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
+ }
@@ -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,
@@ -206,7 +206,6 @@ export class StatusLineComponent implements Component {
206
206
  separator: settings.get("statusLine.separator"),
207
207
  showHookStatus: settings.get("statusLine.showHookStatus"),
208
208
  showSkillHud: settings.get("statusLine.showSkillHud"),
209
- showActionHints: settings.get("statusLine.showActionHints"),
210
209
  segmentOptions: settings.getGroup("statusLine").segmentOptions,
211
210
  sessionAccent: settings.get("statusLine.sessionAccent"),
212
211
  maxRows: settings.get("statusLine.maxRows"),
@@ -416,19 +415,8 @@ export class StatusLineComponent implements Component {
416
415
  };
417
416
  try {
418
417
  // 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);
418
+ const pr = await lookupCurrentPr();
419
+ setCachedPr(pr);
432
420
  } finally {
433
421
  this.#prLookupInFlight = false;
434
422
  if (this.#onBranchChange) {
@@ -54,6 +54,34 @@ export type TranscriptViewerOverlayOptions = {
54
54
 
55
55
  type RenderedEntry = { lineStart: number; lineCount: number };
56
56
 
57
+ export const __transcriptViewerPerfCounters = {
58
+ enabled: false,
59
+ layoutCacheHits: 0,
60
+ layoutCacheMisses: 0,
61
+ refreshRuns: 0,
62
+ rebuildRuns: 0,
63
+ snapshot() {
64
+ return {
65
+ layoutCacheHits: this.layoutCacheHits,
66
+ layoutCacheMisses: this.layoutCacheMisses,
67
+ refreshRuns: this.refreshRuns,
68
+ rebuildRuns: this.rebuildRuns,
69
+ };
70
+ },
71
+ enable(): void {
72
+ this.enabled = true;
73
+ },
74
+ disable(): void {
75
+ this.enabled = false;
76
+ },
77
+ reset(): void {
78
+ this.layoutCacheHits = 0;
79
+ this.layoutCacheMisses = 0;
80
+ this.refreshRuns = 0;
81
+ this.rebuildRuns = 0;
82
+ },
83
+ };
84
+
57
85
  /** Message-agnostic, modal transcript browser over resolved projection entries. */
58
86
  export class TranscriptViewerOverlay extends Container {
59
87
  #options: TranscriptViewerOverlayOptions;
@@ -73,6 +101,7 @@ export class TranscriptViewerOverlay extends Container {
73
101
  #followTailPending = false;
74
102
  #skipFollowTailOnce = false;
75
103
  #followTailActive = false;
104
+ #layoutCache = new Map<string, Map<string, string[]>>();
76
105
 
77
106
  constructor(options: TranscriptViewerOverlayOptions) {
78
107
  super();
@@ -87,7 +116,11 @@ export class TranscriptViewerOverlay extends Container {
87
116
  get isFullscreen(): boolean {
88
117
  return this.#fullscreen;
89
118
  }
90
- refresh(identityMap?: ReadonlyMap<string, string>): void {
119
+ refresh(identityMap?: ReadonlyMap<string, string>, preserveLayoutCache = false): void {
120
+ if (__transcriptViewerPerfCounters.enabled) __transcriptViewerPerfCounters.refreshRuns++;
121
+ // Incremental sources explicitly invalidate same-id payload patches before requesting a
122
+ // preserving refresh. Other source changes conservatively drop all layout variants.
123
+ if (!preserveLayoutCache) this.#layoutCache.clear();
91
124
  const previous = this.selectedEntryId;
92
125
  const previousPosition = this.#selected;
93
126
  const reconciledPrevious = previous ? (identityMap?.get(previous) ?? previous) : undefined;
@@ -109,9 +142,15 @@ export class TranscriptViewerOverlay extends Container {
109
142
  this.#initialized = true;
110
143
  this.#rebuild();
111
144
  }
145
+ invalidateLayoutEntries(entryIds: readonly string[]): void {
146
+ for (const id of entryIds) this.#layoutCache.delete(id);
147
+ }
112
148
  override render(width: number): string[] {
113
- this.#width = Math.max(1, width);
114
- this.refresh();
149
+ const nextWidth = Math.max(1, width);
150
+ const widthChanged = nextWidth !== this.#width;
151
+ this.#width = nextWidth;
152
+ if (widthChanged) this.#layoutCache.clear();
153
+ if (widthChanged || getMarkdownTheme() !== this.#mdTheme) this.#rebuild();
115
154
  const defaultHeader = this.#fullscreen
116
155
  ? [theme.fg("accent", "Transcript block")]
117
156
  : [theme.fg("accent", sanitizeText(this.#options.title ?? "Transcript"))];
@@ -179,6 +218,7 @@ export class TranscriptViewerOverlay extends Container {
179
218
  if (this.#fullscreen) {
180
219
  this.#fullscreen = false;
181
220
  this.#scrollOffset = 0;
221
+ this.#rebuild();
182
222
  this.#requestRender();
183
223
  } else this.#options.onClose();
184
224
  return;
@@ -208,6 +248,7 @@ export class TranscriptViewerOverlay extends Container {
208
248
  this.#followTailActive = false;
209
249
  this.#followTailPending = false;
210
250
  this.#selected = 0;
251
+ this.#rebuild();
211
252
  this.#scrollOffset = 0;
212
253
  this.#requestRender();
213
254
  return;
@@ -216,6 +257,7 @@ export class TranscriptViewerOverlay extends Container {
216
257
  this.#followTailActive = true;
217
258
  this.#followTailPending = true;
218
259
  this.#selected = Math.max(0, count - 1);
260
+ this.#rebuild();
219
261
  this.#scrollOffset = this.#lines.length;
220
262
  this.#requestRender();
221
263
  return;
@@ -228,6 +270,7 @@ export class TranscriptViewerOverlay extends Container {
228
270
  this.#followTailActive = false;
229
271
  this.#fullscreen = true;
230
272
  this.#scrollOffset = 0;
273
+ this.#rebuild();
231
274
  this.#requestRender();
232
275
  return;
233
276
  }
@@ -243,6 +286,7 @@ export class TranscriptViewerOverlay extends Container {
243
286
  const entry = this.#entries[this.#selected];
244
287
  if (entry?.rawViewable !== false) {
245
288
  this.#raw.has(entry.id) ? this.#raw.delete(entry.id) : this.#raw.add(entry.id);
289
+ this.#rebuild();
246
290
  this.#requestRender();
247
291
  }
248
292
  }
@@ -284,6 +328,7 @@ export class TranscriptViewerOverlay extends Container {
284
328
  this.#followTailActive = false;
285
329
  this.#followTailPending = false;
286
330
  this.#selected = Math.max(0, Math.min(this.#selected + direction * 5, this.#entries.length - 1));
331
+ this.#rebuild();
287
332
  this.#scrollOffset += direction * PAGE_SIZE;
288
333
  this.#requestRender();
289
334
  }
@@ -327,32 +372,58 @@ export class TranscriptViewerOverlay extends Container {
327
372
  this.#options.requestRender?.();
328
373
  }
329
374
  #rebuild(): void {
375
+ if (__transcriptViewerPerfCounters.enabled) __transcriptViewerPerfCounters.rebuildRuns++;
376
+ // Theme is tracked by reference outside the variant key; a reference change clears
377
+ // the whole cache so plain and markdown layouts pick up new markdown colors.
378
+ const currentMdTheme = getMarkdownTheme();
379
+ if (currentMdTheme !== this.#mdTheme) {
380
+ this.#mdTheme = currentMdTheme;
381
+ this.#layoutCache.clear();
382
+ }
330
383
  const lines: string[] = [];
331
384
  this.#renderedEntries = [];
332
385
  const display = this.#fullscreen ? this.#entries.slice(this.#selected, this.#selected + 1) : this.#entries;
333
386
  const contentWidth = Math.max(1, this.#width - INDENT.length - 1);
334
387
  for (const entry of display) {
335
388
  const start = lines.length;
336
- const selected = entry.id === this.selectedEntryId;
337
- const expanded = this.#fullscreen || this.#expanded.has(entry.id);
338
- const raw = this.#raw.has(entry.id);
339
- lines.push("");
340
- lines.push(
341
- `${selected ? theme.fg("accent", "▶") : " "} ${theme.fg("muted", `[${sanitizeText(entry.label ?? entry.kind)}]`)}`,
342
- );
343
- if (
344
- !raw &&
345
- expanded &&
346
- selected &&
347
- entry.kind === "tool" &&
348
- entry.richRenderEligible === true &&
349
- entry.renderDescriptor
350
- ) {
351
- for (const line of renderToolDisplayLines(entry.renderDescriptor, contentWidth, theme))
352
- lines.push(`${INDENT}${line}`);
353
- this.#renderedEntries.push({ lineStart: start, lineCount: lines.length - start });
354
- continue;
389
+ const entryLines = this.#renderEntryLines(entry, contentWidth);
390
+ lines.push(...entryLines);
391
+ // Cumulative positions are recomputed from cached line lengths on every rebuild
392
+ // so PR2c can later separate virtual coordinates from materialized lines.
393
+ this.#renderedEntries.push({ lineStart: start, lineCount: entryLines.length });
394
+ }
395
+ this.#lines = lines.length ? lines : [theme.fg("dim", "No transcript entries yet.")];
396
+ }
397
+ #renderEntryLines(entry: TranscriptViewerEntry, contentWidth: number): string[] {
398
+ const selected = entry.id === this.selectedEntryId;
399
+ const expanded = this.#fullscreen || this.#expanded.has(entry.id);
400
+ const raw = this.#raw.has(entry.id);
401
+ const variantKey = `${expanded ? 1 : 0}|${raw ? 1 : 0}|${selected ? 1 : 0}|${contentWidth}`;
402
+ let variants = this.#layoutCache.get(entry.id);
403
+ if (variants) {
404
+ const cached = variants.get(variantKey);
405
+ if (cached) {
406
+ if (__transcriptViewerPerfCounters.enabled) __transcriptViewerPerfCounters.layoutCacheHits++;
407
+ return cached;
355
408
  }
409
+ }
410
+ if (__transcriptViewerPerfCounters.enabled) __transcriptViewerPerfCounters.layoutCacheMisses++;
411
+ const lines: string[] = [];
412
+ lines.push("");
413
+ lines.push(
414
+ `${selected ? theme.fg("accent", "▶") : " "} ${theme.fg("muted", `[${sanitizeText(entry.label ?? entry.kind)}]`)}`,
415
+ );
416
+ if (
417
+ !raw &&
418
+ expanded &&
419
+ selected &&
420
+ entry.kind === "tool" &&
421
+ entry.richRenderEligible === true &&
422
+ entry.renderDescriptor
423
+ ) {
424
+ for (const line of renderToolDisplayLines(entry.renderDescriptor, contentWidth, theme))
425
+ lines.push(`${INDENT}${line}`);
426
+ } else {
356
427
  const text = sanitizeText(
357
428
  (raw
358
429
  ? entry.payload.text
@@ -375,9 +446,13 @@ export class TranscriptViewerOverlay extends Container {
375
446
  for (const line of preview) lines.push(`${INDENT}${truncateToWidth(line, contentWidth)}`);
376
447
  if (text.split("\n").length > PREVIEW_LINES) lines.push(`${INDENT}${theme.fg("dim", "... more (Space)")}`);
377
448
  }
378
- this.#renderedEntries.push({ lineStart: start, lineCount: lines.length - start });
379
449
  }
380
- this.#lines = lines.length ? lines : [theme.fg("dim", "No transcript entries yet.")];
450
+ if (!variants) {
451
+ variants = new Map();
452
+ this.#layoutCache.set(entry.id, variants);
453
+ }
454
+ variants.set(variantKey, lines);
455
+ return lines;
381
456
  }
382
457
  #markdown(text: string, width: number): string[] {
383
458
  return new Markdown(text, 0, 0, this.#mdTheme).render(width).map(line => `${INDENT}${line.trimEnd()}`);
@@ -34,7 +34,12 @@ import { BorderedLoader } from "../../modes/components/bordered-loader";
34
34
  import { DynamicBorder } from "../../modes/components/dynamic-border";
35
35
  import { EvalExecutionComponent } from "../../modes/components/eval-execution";
36
36
  import { getMarkdownTheme, getSymbolTheme, theme } from "../../modes/theme/theme";
37
- import type { InteractiveModeContext } from "../../modes/types";
37
+ import {
38
+ clearInteractiveActivityLoaders,
39
+ type InteractiveModeContext,
40
+ stopInteractiveActivityIndicator,
41
+ suspendInteractiveActivityIndicator,
42
+ } from "../../modes/types";
38
43
  import { computeContextBreakdown, renderContextUsage } from "../../modes/utils/context-usage";
39
44
  import { buildHotkeysMarkdown, formatHotkeyMarkdownCode } from "../../modes/utils/hotkeys-markdown";
40
45
  import { buildToolsMarkdown } from "../../modes/utils/tools-markdown";
@@ -965,12 +970,10 @@ export class CommandController {
965
970
 
966
971
  async #runNewSessionFlow(options?: NewSessionOptions, label: string = "New session started"): Promise<boolean> {
967
972
  if (!(await this.ctx.session.newSession(options))) return false;
973
+ if (this.ctx.isStopped?.()) return false;
974
+ clearInteractiveActivityLoaders(this.ctx);
968
975
 
969
- if (this.ctx.loadingAnimation) {
970
- this.ctx.loadingAnimation.stop();
971
- this.ctx.loadingAnimation = undefined;
972
- }
973
- this.ctx.statusContainer.clear();
976
+ stopInteractiveActivityIndicator(this.ctx);
974
977
  this.ctx.resetIrcSidebarSession();
975
978
  this.ctx.resetObserverRegistry();
976
979
  setSessionTerminalTitle(this.ctx.sessionManager.getSessionName(), this.ctx.sessionManager.getCwd());
@@ -991,6 +994,7 @@ export class CommandController {
991
994
 
992
995
  this.ctx.chatContainer.addChild(new Text(`${theme.fg("accent", `${theme.status.success} ${label}`)}`, 1, 0));
993
996
  await this.ctx.reloadTodos();
997
+ if (this.ctx.isStopped?.()) return false;
994
998
  this.ctx.ui.requestRender();
995
999
  return true;
996
1000
  }
@@ -1000,19 +1004,18 @@ export class CommandController {
1000
1004
  }
1001
1005
 
1002
1006
  async handleContextClearCommand(): Promise<void> {
1003
- if (this.ctx.loadingAnimation) {
1004
- this.ctx.loadingAnimation.stop();
1005
- this.ctx.loadingAnimation = undefined;
1006
- }
1007
- this.ctx.statusContainer.clear();
1008
-
1007
+ if (this.ctx.isStopped?.()) return;
1008
+ clearInteractiveActivityLoaders(this.ctx);
1009
+ stopInteractiveActivityIndicator(this.ctx);
1009
1010
  if (this.ctx.session.isCompacting) {
1010
1011
  this.ctx.session.abortCompaction();
1011
1012
  while (this.ctx.session.isCompacting) {
1012
1013
  await Bun.sleep(10);
1013
1014
  }
1014
1015
  }
1016
+ if (this.ctx.isStopped?.()) return;
1015
1017
  if (!(await this.ctx.session.clearContext())) return;
1018
+ if (this.ctx.isStopped?.()) return;
1016
1019
  setSessionTerminalTitle(this.ctx.sessionManager.getSessionName(), this.ctx.sessionManager.getCwd());
1017
1020
 
1018
1021
  this.ctx.statusLine.invalidate();
@@ -1033,6 +1036,7 @@ export class CommandController {
1033
1036
  new Text(`${theme.fg("accent", `${theme.status.success} Context cleared`)}`, 1, 0),
1034
1037
  );
1035
1038
  await this.ctx.reloadTodos();
1039
+ if (this.ctx.isStopped?.()) return;
1036
1040
  this.ctx.ui.requestRender();
1037
1041
  }
1038
1042
 
@@ -1049,17 +1053,15 @@ export class CommandController {
1049
1053
  this.ctx.showWarning("Wait for the current response to finish or abort it before forking.");
1050
1054
  return;
1051
1055
  }
1052
- if (this.ctx.loadingAnimation) {
1053
- this.ctx.loadingAnimation.stop();
1054
- this.ctx.loadingAnimation = undefined;
1055
- }
1056
- this.ctx.statusContainer.clear();
1057
1056
 
1058
1057
  const success = await this.ctx.session.fork();
1058
+ if (this.ctx.isStopped?.()) return;
1059
1059
  if (!success) {
1060
1060
  this.ctx.showError("Fork failed (session not persisted or cancelled)");
1061
1061
  return;
1062
1062
  }
1063
+ clearInteractiveActivityLoaders(this.ctx);
1064
+ stopInteractiveActivityIndicator(this.ctx);
1063
1065
  this.ctx.resetIrcSidebarSession();
1064
1066
 
1065
1067
  this.ctx.statusLine.invalidate();
@@ -1175,6 +1177,10 @@ export class CommandController {
1175
1177
  }
1176
1178
  this.ctx.showError(`Bash command failed: ${error instanceof Error ? error.message : "Unknown error"}`);
1177
1179
  }
1180
+ const bashComponent = this.ctx.bashComponent;
1181
+ if (isDeferred && bashComponent && this.ctx.pendingBashComponents.includes(bashComponent)) {
1182
+ this.ctx.pendingMessagesContainer.detachChild(bashComponent);
1183
+ }
1178
1184
 
1179
1185
  this.ctx.bashComponent = undefined;
1180
1186
  this.ctx.ui.requestRender();
@@ -1252,11 +1258,7 @@ export class CommandController {
1252
1258
  customInstructionsOrOptions?: string | CompactOptions,
1253
1259
  isAuto = false,
1254
1260
  ): Promise<CompactionOutcome> {
1255
- if (this.ctx.loadingAnimation) {
1256
- this.ctx.loadingAnimation.stop();
1257
- this.ctx.loadingAnimation = undefined;
1258
- }
1259
- this.ctx.statusContainer.clear();
1261
+ const releaseActivityIndicator = suspendInteractiveActivityIndicator(this.ctx);
1260
1262
 
1261
1263
  const originalOnEscape = this.ctx.editor.onEscape;
1262
1264
  this.ctx.editor.onEscape = () => {
@@ -1283,24 +1285,28 @@ export class CommandController {
1283
1285
  ? customInstructionsOrOptions
1284
1286
  : undefined;
1285
1287
  await this.ctx.session.compact(instructions, options);
1288
+ if (this.ctx.isStopped?.()) return outcome;
1286
1289
 
1287
1290
  this.ctx.rebuildChatFromMessages("reconcile-same-transcript");
1288
1291
 
1289
1292
  this.ctx.statusLine.invalidate();
1290
1293
  this.ctx.updateEditorTopBorder();
1291
1294
  } catch (error) {
1292
- if (error instanceof CompactionCancelledError) {
1293
- outcome = "cancelled";
1295
+ outcome = error instanceof CompactionCancelledError ? "cancelled" : "failed";
1296
+ if (this.ctx.isStopped?.()) return outcome;
1297
+ if (outcome === "cancelled") {
1294
1298
  this.ctx.showError("Compaction cancelled");
1295
1299
  } else {
1296
- outcome = "failed";
1297
1300
  const message = error instanceof Error ? error.message : String(error);
1298
1301
  this.ctx.showError(`Compaction failed: ${message}`);
1299
1302
  }
1300
1303
  } finally {
1301
1304
  compactingLoader.stop();
1302
- this.ctx.statusContainer.clear();
1303
- this.ctx.editor.onEscape = originalOnEscape;
1305
+ if (!this.ctx.isStopped?.()) {
1306
+ this.ctx.statusContainer.clear();
1307
+ this.ctx.editor.onEscape = originalOnEscape;
1308
+ }
1309
+ releaseActivityIndicator();
1304
1310
  }
1305
1311
  await this.ctx.flushCompactionQueue({ willRetry: false });
1306
1312
  return outcome;
@@ -1319,11 +1325,7 @@ export class CommandController {
1319
1325
  return;
1320
1326
  }
1321
1327
 
1322
- if (this.ctx.loadingAnimation) {
1323
- this.ctx.loadingAnimation.stop();
1324
- this.ctx.loadingAnimation = undefined;
1325
- }
1326
- this.ctx.statusContainer.clear();
1328
+ const releaseActivityIndicator = suspendInteractiveActivityIndicator(this.ctx);
1327
1329
 
1328
1330
  const originalOnEscape = this.ctx.editor.onEscape;
1329
1331
  this.ctx.editor.onEscape = () => {
@@ -1343,6 +1345,7 @@ export class CommandController {
1343
1345
  try {
1344
1346
  // Handoff generation runs as a oneshot request; the new session is shown after it completes.
1345
1347
  const result = await this.ctx.session.handoff(customInstructions);
1348
+ if (this.ctx.isStopped?.()) return;
1346
1349
 
1347
1350
  if (!result) {
1348
1351
  this.ctx.showError("Handoff cancelled");
@@ -1357,6 +1360,7 @@ export class CommandController {
1357
1360
  this.ctx.updateEditorTopBorder();
1358
1361
  this.ctx.updateEditorBorderColor();
1359
1362
  await this.ctx.reloadTodos();
1363
+ if (this.ctx.isStopped?.()) return;
1360
1364
 
1361
1365
  this.ctx.chatContainer.addChild(new Spacer(1));
1362
1366
  this.ctx.chatContainer.addChild(
@@ -1366,6 +1370,7 @@ export class CommandController {
1366
1370
  this.ctx.showStatus(`Handoff document saved to: ${result.savedPath}`);
1367
1371
  }
1368
1372
  } catch (error) {
1373
+ if (this.ctx.isStopped?.()) return;
1369
1374
  const message = error instanceof Error ? error.message : String(error);
1370
1375
  if (message === "Handoff cancelled" || (error instanceof Error && error.name === "AbortError")) {
1371
1376
  this.ctx.showError("Handoff cancelled");
@@ -1385,8 +1390,11 @@ export class CommandController {
1385
1390
  }
1386
1391
  } finally {
1387
1392
  handoffLoader.stop();
1388
- this.ctx.statusContainer.clear();
1389
- this.ctx.editor.onEscape = originalOnEscape;
1393
+ if (!this.ctx.isStopped?.()) {
1394
+ this.ctx.statusContainer.clear();
1395
+ this.ctx.editor.onEscape = originalOnEscape;
1396
+ }
1397
+ releaseActivityIndicator();
1390
1398
  }
1391
1399
  }
1392
1400