@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
@@ -13,7 +13,7 @@ import {
13
13
  Text,
14
14
  TUI,
15
15
  } from "@gajae-code/tui";
16
- import { APP_NAME, adjustHsv, getProjectDir, logger, postmortem } from "@gajae-code/utils";
16
+ import { APP_NAME, adjustHsv, getProjectDir, logger, postmortem, sanitizeText } from "@gajae-code/utils";
17
17
  import chalk from "chalk";
18
18
  import { AsyncJobManager } from "../async";
19
19
  import {
@@ -61,7 +61,12 @@ import { GajaePetWidget, type PetMode } from "./components/gajae-pet-widget";
61
61
  import type { HookEditorComponent } from "./components/hook-editor";
62
62
  import type { HookInputComponent } from "./components/hook-input";
63
63
  import type { HookSelectorComponent } from "./components/hook-selector";
64
- import { computeIrcSplitWidths, getIrcSidebarSemanticToken, IrcSplitViewComponent } from "./components/irc-sidebar";
64
+ import {
65
+ computeIrcSplitWidths,
66
+ getIrcSidebarSemanticToken,
67
+ IrcLeftLaneComponent,
68
+ IrcSplitViewComponent,
69
+ } from "./components/irc-sidebar";
65
70
  import {
66
71
  getPetUnavailableWarning,
67
72
  isPetAvailable,
@@ -118,8 +123,9 @@ import { addChatChild, prepareTranscriptRebuild, UiHelpers } from "./utils/ui-he
118
123
  function buildComposerPlaceholder(
119
124
  keybindings: Pick<KeybindingsManager, "getDisplayString">,
120
125
  context: KeyDisplayContext,
121
- options: { readonly busy: boolean; readonly busyPromptMode: "steer" | "queue" },
126
+ options: { readonly busy: boolean; readonly busyPromptMode: "steer" | "queue"; readonly showActionHints: boolean },
122
127
  ): string {
128
+ if (!options.showActionHints) return "Type your message...";
123
129
  const parts: string[] = [];
124
130
  const submitKey = options.busy ? keybindings.getDisplayString("tui.input.submit", context) : "";
125
131
  if (submitKey) {
@@ -155,7 +161,11 @@ export function getDefaultComposerPlaceholder(
155
161
  KeybindingsManager.inMemory({
156
162
  "app.message.queue": defaultMessageQueueKeysForPlatform(context.platform),
157
163
  });
158
- return buildComposerPlaceholder(effectiveKeybindings, context, { busy: false, busyPromptMode: "steer" });
164
+ return buildComposerPlaceholder(effectiveKeybindings, context, {
165
+ busy: false,
166
+ busyPromptMode: "steer",
167
+ showActionHints: true,
168
+ });
159
169
  }
160
170
 
161
171
  export const DEFAULT_COMPOSER_PLACEHOLDER = getDefaultComposerPlaceholder();
@@ -163,10 +173,24 @@ export const DEFAULT_COMPOSER_PLACEHOLDER = getDefaultComposerPlaceholder();
163
173
  export function getComposerPlaceholder(
164
174
  keybindings: Pick<KeybindingsManager, "getDisplayString">,
165
175
  context: KeyDisplayContext,
166
- options: { readonly busy: boolean; readonly busyPromptMode: "steer" | "queue" },
176
+ options: { readonly busy: boolean; readonly busyPromptMode: "steer" | "queue"; readonly showActionHints: boolean },
167
177
  ): string {
168
178
  return buildComposerPlaceholder(keybindings, context, options);
169
179
  }
180
+
181
+ export function resolveActivityIndicatorMessage(
182
+ foregroundActive: boolean,
183
+ activeBackgroundTasks: number,
184
+ foregroundMessage: string,
185
+ ): string | undefined {
186
+ const backgroundCount = Math.max(0, Math.trunc(activeBackgroundTasks));
187
+ if (foregroundActive) {
188
+ if (backgroundCount === 0) return foregroundMessage;
189
+ return `${foregroundMessage} · ${backgroundCount} background task${backgroundCount === 1 ? "" : "s"}`;
190
+ }
191
+ if (backgroundCount === 0) return undefined;
192
+ return `Background: ${backgroundCount} task${backgroundCount === 1 ? "" : "s"}…`;
193
+ }
170
194
  const WELCOME_RESERVED_CONTAINER_CHILD_LIMIT = 8;
171
195
  const COMPOSER_RIGHT_GUTTER_WIDTH = 1;
172
196
 
@@ -432,6 +456,12 @@ export class InteractiveMode implements InteractiveModeContext {
432
456
 
433
457
  #jobsObserver?: JobsObserver;
434
458
  #tasksAggregator?: TasksAggregator;
459
+ #foregroundActivity = false;
460
+ #activityIndicatorSuspensions = 0;
461
+ #suspendedActivityIndicator?: Loader;
462
+ #stopped = false;
463
+ #initPromise?: Promise<void>;
464
+ #stopListeners = new Set<() => void>();
435
465
  #eventBus?: EventBus;
436
466
  #eventBusUnsubscribers: Array<() => void> = [];
437
467
  #welcomeComponent?: WelcomeComponent;
@@ -646,7 +676,7 @@ export class InteractiveMode implements InteractiveModeContext {
646
676
  this.#todoCommandController = new TodoCommandController(this);
647
677
  this.#selectorController = new SelectorController(this);
648
678
  this.#inputController = new InputController(this);
649
- this.statusLine.setActionRegistry(this.#inputController.actionRegistry, () => this.keybindings);
679
+ // Composer shortcut discovery owns contextual action hints; retain only status telemetry in the rail.
650
680
  this.promptSuggestion = new PromptSuggestionController(this);
651
681
  this.#observerRegistry = new SessionObserverRegistry();
652
682
  }
@@ -664,8 +694,16 @@ export class InteractiveMode implements InteractiveModeContext {
664
694
  );
665
695
  }
666
696
 
667
- async init(): Promise<void> {
668
- if (this.isInitialized) return;
697
+ init(): Promise<void> {
698
+ if (this.#stopped || this.isInitialized) return Promise.resolve();
699
+ this.#initPromise ??= this.#initialize().finally(() => {
700
+ this.#initPromise = undefined;
701
+ });
702
+ return this.#initPromise;
703
+ }
704
+
705
+ async #initialize(): Promise<void> {
706
+ if (this.#stopped || this.isInitialized) return;
669
707
 
670
708
  this.keybindings = logger.time("InteractiveMode.init:keybindings", () => KeybindingsManager.create());
671
709
  this.keybindings.setDisplayContext(this.#keyDisplayContext);
@@ -686,6 +724,7 @@ export class InteractiveMode implements InteractiveModeContext {
686
724
  this.refreshSlashCommandState.bind(this),
687
725
  getProjectDir(),
688
726
  );
727
+ if (this.#stopped) return;
689
728
 
690
729
  // Get current model info for welcome screen
691
730
  const modelName = this.session.model?.name ?? "Unknown";
@@ -814,10 +853,15 @@ export class InteractiveMode implements InteractiveModeContext {
814
853
  this.#observerRegistry,
815
854
  this.session.getAgentId(),
816
855
  );
856
+ this.#tasksAggregator.onChange(() => {
857
+ this.syncActivityIndicator();
858
+ this.ui.requestRender();
859
+ });
817
860
  }
818
861
 
819
862
  // Load initial todos
820
863
  await this.#loadTodoList();
864
+ if (this.#stopped) return;
821
865
 
822
866
  // Start the UI
823
867
  this.ui.start();
@@ -826,6 +870,7 @@ export class InteractiveMode implements InteractiveModeContext {
826
870
  this.updateEditorChrome();
827
871
  this.#syncEditorMaxHeight();
828
872
  this.isInitialized = true;
873
+ this.syncActivityIndicator();
829
874
  if (this.settings.get("tasksPane.defaultVisible")) this.showTasksPane();
830
875
  this.#syncIrcSidebarAvailabilityFromSettings();
831
876
  this.ui.requestRender(true);
@@ -837,6 +882,7 @@ export class InteractiveMode implements InteractiveModeContext {
837
882
  getRecentSessions(this.sessionManager.getSessionDir()),
838
883
  )
839
884
  .then(sessions => {
885
+ if (this.#stopped) return;
840
886
  if (this.#welcomeComponent !== welcomeComponent) return;
841
887
  welcomeComponent.setRecentSessions(
842
888
  sessions.map(session => ({
@@ -871,22 +917,26 @@ export class InteractiveMode implements InteractiveModeContext {
871
917
  }
872
918
  if (starReminderGate.schedule) {
873
919
  scheduleLaunchStarReminderAfterFirstRender({
874
- confirm: (title, message) => this.showHookConfirm(title, message),
920
+ confirm: (title, message) =>
921
+ this.#stopped ? Promise.resolve(false) : this.showHookConfirm(title, message),
875
922
  isIdle: () => !this.session.isStreaming && !this.isBackgrounded && !this.hookSelector,
876
923
  });
877
924
  }
878
925
 
879
926
  // Initialize hooks with TUI-based UI context
880
927
  await this.initHooksAndCustomTools();
928
+ if (this.#stopped) return;
881
929
 
882
930
  // Restore mode from session (e.g. plan mode on resume)
883
931
  await this.#restoreModeFromSession();
932
+ if (this.#stopped) return;
884
933
 
885
934
  // Restore unsent editor draft from previous session shutdown (Ctrl+D).
886
935
  // One-shot: consumeDraft removes the sidecar after read so the next
887
936
  // resume does not re-restore the same text.
888
937
  try {
889
938
  const draft = await this.sessionManager.consumeDraft();
939
+ if (this.#stopped) return;
890
940
  if (draft && !this.editor.getText()) {
891
941
  this.editor.setText(draft);
892
942
  this.updateEditorChrome();
@@ -895,6 +945,7 @@ export class InteractiveMode implements InteractiveModeContext {
895
945
  } catch (err) {
896
946
  logger.warn("Failed to restore session draft", { error: String(err) });
897
947
  }
948
+ if (this.#stopped) return;
898
949
 
899
950
  // Subscribe to agent events
900
951
  this.#subscribeToAgent();
@@ -906,6 +957,7 @@ export class InteractiveMode implements InteractiveModeContext {
906
957
  );
907
958
  // Set up theme file watcher
908
959
  onThemeChange(() => {
960
+ if (this.#stopped) return;
909
961
  clearRenderCache();
910
962
  this.#ircSplitView.invalidateTheme();
911
963
  configureDefaultComposerChrome(this.editor);
@@ -938,8 +990,10 @@ export class InteractiveMode implements InteractiveModeContext {
938
990
 
939
991
  /** Reload slash commands and autocomplete for the provided working directory. */
940
992
  async refreshSlashCommandState(cwd?: string): Promise<void> {
993
+ if (this.#stopped) return;
941
994
  const basePath = cwd ?? this.sessionManager.getCwd();
942
995
  const fileCommands = await loadSlashCommands({ cwd: basePath });
996
+ if (this.#stopped) return;
943
997
  const fileCommandNames = new Set(fileCommands.map(cmd => cmd.name));
944
998
  this.fileSlashCommands = fileCommandNames;
945
999
  const fileSlashCommands: SlashCommand[] = fileCommands.map(cmd => ({
@@ -1069,11 +1123,7 @@ export class InteractiveMode implements InteractiveModeContext {
1069
1123
  this.#pendingSubmissionDispose = undefined;
1070
1124
  this.#pendingWorkingMessage = undefined;
1071
1125
  this.#goalModeController.onPendingSubmissionFinished(submission.customType);
1072
- if (this.loadingAnimation) {
1073
- this.loadingAnimation.stop();
1074
- this.loadingAnimation = undefined;
1075
- this.statusContainer.clear();
1076
- }
1126
+ this.stopLoadingAnimation();
1077
1127
  if (!submission.customType) {
1078
1128
  this.pendingImages = submission.images ? [...submission.images] : [];
1079
1129
  this.rebuildChatFromMessages("reconcile-same-transcript");
@@ -1105,11 +1155,7 @@ export class InteractiveMode implements InteractiveModeContext {
1105
1155
  this.optimisticUserMessageSignature = undefined;
1106
1156
  pendingSubmissionDispose?.();
1107
1157
  this.#pendingWorkingMessage = undefined;
1108
- if (this.loadingAnimation) {
1109
- this.loadingAnimation.stop();
1110
- this.loadingAnimation = undefined;
1111
- this.statusContainer.clear();
1112
- }
1158
+ this.stopLoadingAnimation();
1113
1159
  }
1114
1160
  }
1115
1161
 
@@ -1132,6 +1178,7 @@ export class InteractiveMode implements InteractiveModeContext {
1132
1178
  return getComposerPlaceholder(this.keybindings, this.#keyDisplayContext, {
1133
1179
  busy: this.#isPromptDeliveryBusy(),
1134
1180
  busyPromptMode: this.settings.get("busyPromptMode"),
1181
+ showActionHints: this.settings.get("statusLine.showActionHints"),
1135
1182
  });
1136
1183
  }
1137
1184
 
@@ -1280,18 +1327,23 @@ export class InteractiveMode implements InteractiveModeContext {
1280
1327
  this.renderSessionContext(context);
1281
1328
  }
1282
1329
 
1330
+ #sanitizeTodoText(text: string): string {
1331
+ return sanitizeText(text).replaceAll("\t", " ");
1332
+ }
1333
+
1283
1334
  #formatTodoLine(todo: TodoItem, prefix: string): string {
1284
1335
  const checkbox = theme.checkbox;
1285
1336
  const marker = formatHudNoteMarker(todo.notes?.length ?? 0);
1337
+ const content = this.#sanitizeTodoText(todo.content);
1286
1338
  switch (todo.status) {
1287
1339
  case "completed":
1288
- return theme.fg("success", `${prefix}${checkbox.checked} ${chalk.strikethrough(todo.content)}`) + marker;
1340
+ return theme.fg("success", `${prefix}${checkbox.checked} ${chalk.strikethrough(content)}`) + marker;
1289
1341
  case "in_progress":
1290
- return theme.fg("accent", `${prefix}${checkbox.unchecked} ${todo.content}`) + marker;
1342
+ return theme.fg("accent", `${prefix}${checkbox.unchecked} ${content}`) + marker;
1291
1343
  case "abandoned":
1292
- return theme.fg("error", `${prefix}${checkbox.unchecked} ${chalk.strikethrough(todo.content)}`) + marker;
1344
+ return theme.fg("error", `${prefix}${checkbox.unchecked} ${chalk.strikethrough(content)}`) + marker;
1293
1345
  default:
1294
- return theme.fg("dim", `${prefix}${checkbox.unchecked} ${todo.content}`) + marker;
1346
+ return theme.fg("dim", `${prefix}${checkbox.unchecked} ${content}`) + marker;
1295
1347
  }
1296
1348
  }
1297
1349
 
@@ -1303,6 +1355,13 @@ export class InteractiveMode implements InteractiveModeContext {
1303
1355
  return active ?? nonEmpty[nonEmpty.length - 1];
1304
1356
  }
1305
1357
 
1358
+ #addTodoContent(lines: string[]): void {
1359
+ const content = new Text(lines.join("\n"), 1, 0);
1360
+ this.todoContainer.addChild(
1361
+ new IrcLeftLaneComponent(content, width => this.#ircSplitView.effectiveSidebarVisible(width)),
1362
+ );
1363
+ }
1364
+
1306
1365
  #renderTodoList(): void {
1307
1366
  this.todoContainer.clear();
1308
1367
  const phases = this.todoPhases.filter(phase => phase.tasks.length > 0);
@@ -1319,7 +1378,7 @@ export class InteractiveMode implements InteractiveModeContext {
1319
1378
  const activePhase = phases[activeIdx];
1320
1379
  if (!activePhase) return;
1321
1380
  lines.push(
1322
- `${indent}${theme.fg("accent", `${hook} ${formatPhaseDisplayName(activePhase.name, activeIdx + 1)}`)}`,
1381
+ `${indent}${theme.fg("accent", `${hook} ${formatPhaseDisplayName(this.#sanitizeTodoText(activePhase.name), activeIdx + 1)}`)}`,
1323
1382
  );
1324
1383
  const visibleTasks = activePhase.tasks.slice(0, 5);
1325
1384
  visibleTasks.forEach((todo, index) => {
@@ -1330,19 +1389,21 @@ export class InteractiveMode implements InteractiveModeContext {
1330
1389
  const remaining = activePhase.tasks.length - visibleTasks.length;
1331
1390
  lines.push(theme.fg("muted", `${indent} ${hook} +${remaining} more`));
1332
1391
  }
1333
- this.todoContainer.addChild(new Text(lines.join("\n"), 1, 0));
1392
+ this.#addTodoContent(lines);
1334
1393
  return;
1335
1394
  }
1336
1395
 
1337
1396
  phases.forEach((phase, phaseIndex) => {
1338
- lines.push(`${indent}${theme.fg("accent", `${hook} ${formatPhaseDisplayName(phase.name, phaseIndex + 1)}`)}`);
1397
+ lines.push(
1398
+ `${indent}${theme.fg("accent", `${hook} ${formatPhaseDisplayName(this.#sanitizeTodoText(phase.name), phaseIndex + 1)}`)}`,
1399
+ );
1339
1400
  phase.tasks.forEach((todo, index) => {
1340
1401
  const prefix = `${indent}${index === 0 ? hook : " "} `;
1341
1402
  lines.push(this.#formatTodoLine(todo, prefix));
1342
1403
  });
1343
1404
  });
1344
1405
 
1345
- this.todoContainer.addChild(new Text(lines.join("\n"), 1, 0));
1406
+ this.#addTodoContent(lines);
1346
1407
  }
1347
1408
 
1348
1409
  async #loadTodoList(): Promise<void> {
@@ -1377,17 +1438,38 @@ export class InteractiveMode implements InteractiveModeContext {
1377
1438
  await this.#planModeController.restoreFromSession(sessionContext);
1378
1439
  }
1379
1440
 
1441
+ isStopped(): boolean {
1442
+ return this.#stopped;
1443
+ }
1444
+
1445
+ onStop(callback: () => void): () => void {
1446
+ if (this.#stopped) {
1447
+ callback();
1448
+ return () => {};
1449
+ }
1450
+ this.#stopListeners.add(callback);
1451
+ return () => this.#stopListeners.delete(callback);
1452
+ }
1453
+
1380
1454
  stop(): void {
1455
+ const wasInitialized = this.isInitialized;
1456
+ this.#stopped = true;
1457
+ for (const listener of this.#stopListeners) {
1458
+ try {
1459
+ listener();
1460
+ } catch (error) {
1461
+ logger.warn("Interactive stop listener failed", { error: String(error) });
1462
+ }
1463
+ }
1464
+ this.#stopListeners.clear();
1465
+ this.#stopLoadingAnimation();
1466
+ this.#suspendedActivityIndicator = undefined;
1381
1467
  this.#petProtocolUnsubscribe?.();
1382
1468
  this.#petProtocolUnsubscribe = undefined;
1383
1469
  this.#petUnavailableWarningDisposer?.();
1384
1470
  this.#petUnavailableWarningDisposer = undefined;
1385
1471
  this.petWidget?.dispose();
1386
1472
  this.petWidget = undefined;
1387
- if (this.loadingAnimation) {
1388
- this.loadingAnimation.stop();
1389
- this.loadingAnimation = undefined;
1390
- }
1391
1473
  this.#welcomeComponent?.dispose();
1392
1474
  this.#welcomeComponent = undefined;
1393
1475
  if (this.#sttController) {
@@ -1395,8 +1477,7 @@ export class InteractiveMode implements InteractiveModeContext {
1395
1477
  this.#sttController = undefined;
1396
1478
  }
1397
1479
  this.#goalModeController.cancelContinuation();
1398
- this.#extensionUiController.clearExtensionTerminalInputListeners();
1399
- this.#extensionUiController.clearHookWidgets();
1480
+ this.#extensionUiController.dispose();
1400
1481
  for (const unsubscribe of this.#eventBusUnsubscribers) {
1401
1482
  unsubscribe();
1402
1483
  }
@@ -1420,10 +1501,8 @@ export class InteractiveMode implements InteractiveModeContext {
1420
1501
  if (this.#subprocessTeardownUnsubscribe) {
1421
1502
  this.#subprocessTeardownUnsubscribe();
1422
1503
  }
1423
- if (this.isInitialized) {
1424
- this.ui.stop();
1425
- this.isInitialized = false;
1426
- }
1504
+ if (wasInitialized) this.ui.stop();
1505
+ this.isInitialized = false;
1427
1506
  }
1428
1507
 
1429
1508
  async shutdown(): Promise<void> {
@@ -1539,6 +1618,7 @@ export class InteractiveMode implements InteractiveModeContext {
1539
1618
  };
1540
1619
 
1541
1620
  void this.refreshSlashCommandState().catch(error => {
1621
+ if (this.#stopped) return;
1542
1622
  logger.warn("Failed to refresh slash command state for custom editor", { error: String(error) });
1543
1623
  });
1544
1624
 
@@ -1562,11 +1642,7 @@ export class InteractiveMode implements InteractiveModeContext {
1562
1642
  this.#pendingSubmissionDispose?.();
1563
1643
  this.#pendingSubmissionDispose = undefined;
1564
1644
  this.#pendingWorkingMessage = undefined;
1565
- if (this.loadingAnimation) {
1566
- this.loadingAnimation.stop();
1567
- this.loadingAnimation = undefined;
1568
- this.statusContainer.clear();
1569
- }
1645
+ this.stopLoadingAnimation();
1570
1646
  this.#uiHelpers.showError(message);
1571
1647
  }
1572
1648
 
@@ -1612,7 +1688,33 @@ export class InteractiveMode implements InteractiveModeContext {
1612
1688
  return main && dim ? { main, dim } : undefined;
1613
1689
  }
1614
1690
 
1615
- ensureLoadingAnimation(): void {
1691
+ #activeBackgroundTaskCount(): number {
1692
+ return this.session.getAsyncJobSnapshot()?.running.length ?? 0;
1693
+ }
1694
+
1695
+ #stopLoadingAnimation(): void {
1696
+ this.loadingAnimation?.stop();
1697
+ this.loadingAnimation = undefined;
1698
+ this.statusContainer.clear();
1699
+ }
1700
+
1701
+ syncActivityIndicator(): void {
1702
+ if (this.#stopped || this.#activityIndicatorSuspensions > 0 || this.autoCompactionLoader || this.retryLoader)
1703
+ return;
1704
+ const foregroundActive = this.#foregroundActivity || this.session.isStreaming;
1705
+ if (!this.isInitialized && !foregroundActive) {
1706
+ this.#stopLoadingAnimation();
1707
+ return;
1708
+ }
1709
+ const message = resolveActivityIndicatorMessage(
1710
+ this.#foregroundActivity || this.session.isStreaming,
1711
+ this.#activeBackgroundTaskCount(),
1712
+ this.#pendingWorkingMessage ?? this.#defaultWorkingMessage,
1713
+ );
1714
+ if (!message) {
1715
+ this.#stopLoadingAnimation();
1716
+ return;
1717
+ }
1616
1718
  if (!this.loadingAnimation) {
1617
1719
  this.statusContainer.clear();
1618
1720
  this.loadingAnimation = new Loader(
@@ -1621,42 +1723,66 @@ export class InteractiveMode implements InteractiveModeContext {
1621
1723
  const accent = this.#getWorkingMessageAccent();
1622
1724
  return accent ? `${accent.main}${spinner}\x1b[39m` : theme.fg("accent", spinner);
1623
1725
  },
1624
- message => renderWorkingMessage(message, this.#getWorkingMessageAccent()),
1625
- this.#defaultWorkingMessage,
1726
+ workingMessage => renderWorkingMessage(workingMessage, this.#getWorkingMessageAccent()),
1727
+ message,
1626
1728
  getSymbolTheme().spinnerFrames,
1627
1729
  { timeDependentColor: true },
1628
1730
  );
1629
1731
  this.statusContainer.addChild(this.loadingAnimation);
1630
1732
  }
1733
+ this.loadingAnimation.setMessage(message);
1734
+ }
1631
1735
 
1632
- this.applyPendingWorkingMessage();
1736
+ ensureLoadingAnimation(): void {
1737
+ this.#foregroundActivity = true;
1738
+ this.syncActivityIndicator();
1633
1739
  }
1634
1740
 
1635
- setWorkingMessage(message?: string): void {
1636
- if (message === undefined) {
1637
- this.#pendingWorkingMessage = undefined;
1638
- if (this.loadingAnimation) {
1639
- this.loadingAnimation.setMessage(this.#defaultWorkingMessage);
1640
- }
1741
+ stopLoadingAnimation(options?: { restoreBackground?: boolean }): void {
1742
+ this.#foregroundActivity = false;
1743
+ if (options?.restoreBackground === false) {
1744
+ this.#stopLoadingAnimation();
1641
1745
  return;
1642
1746
  }
1747
+ this.syncActivityIndicator();
1748
+ }
1643
1749
 
1644
- if (this.loadingAnimation) {
1645
- this.loadingAnimation.setMessage(message);
1646
- return;
1750
+ suspendActivityIndicator(): () => void {
1751
+ const isFirstSuspension = this.#activityIndicatorSuspensions++ === 0;
1752
+ if (isFirstSuspension && !this.autoCompactionLoader && !this.retryLoader) {
1753
+ const loadingAnimation = this.loadingAnimation;
1754
+ if (loadingAnimation && this.statusContainer.children.includes(loadingAnimation)) {
1755
+ this.statusContainer.detachChild(loadingAnimation);
1756
+ this.#suspendedActivityIndicator = loadingAnimation;
1757
+ }
1647
1758
  }
1759
+ let released = false;
1760
+ return () => {
1761
+ if (released) return;
1762
+ released = true;
1763
+ this.#activityIndicatorSuspensions = Math.max(0, this.#activityIndicatorSuspensions - 1);
1764
+ if (this.#activityIndicatorSuspensions > 0) return;
1765
+ const suspended = this.#suspendedActivityIndicator;
1766
+ this.#suspendedActivityIndicator = undefined;
1767
+ if (
1768
+ !this.#stopped &&
1769
+ suspended &&
1770
+ this.loadingAnimation === suspended &&
1771
+ !this.statusContainer.children.includes(suspended)
1772
+ ) {
1773
+ this.statusContainer.addChild(suspended);
1774
+ }
1775
+ this.syncActivityIndicator();
1776
+ };
1777
+ }
1648
1778
 
1779
+ setWorkingMessage(message?: string): void {
1649
1780
  this.#pendingWorkingMessage = message;
1781
+ if (this.#foregroundActivity) this.syncActivityIndicator();
1650
1782
  }
1651
1783
 
1652
1784
  applyPendingWorkingMessage(): void {
1653
- if (this.#pendingWorkingMessage === undefined) {
1654
- return;
1655
- }
1656
-
1657
- const message = this.#pendingWorkingMessage;
1658
- this.#pendingWorkingMessage = undefined;
1659
- this.setWorkingMessage(message);
1785
+ this.syncActivityIndicator();
1660
1786
  }
1661
1787
 
1662
1788
  showNewVersionNotification(newVersion: string): void {
@@ -2275,6 +2401,7 @@ export class InteractiveMode implements InteractiveModeContext {
2275
2401
 
2276
2402
  async reloadTodos(): Promise<void> {
2277
2403
  await this.#loadTodoList();
2404
+ if (this.#stopped) return;
2278
2405
  this.ui.requestRender();
2279
2406
  }
2280
2407
 
@@ -178,7 +178,7 @@ export class JobsObserver {
178
178
 
179
179
  /** Cancel a running monitor job. Returns true when the job was cancelled. */
180
180
  cancelMonitor(id: string): boolean {
181
- return this.#manager.cancel(id);
181
+ return this.#manager.cancel(id, this.#ownerId ? { ownerId: this.#ownerId } : undefined);
182
182
  }
183
183
 
184
184
  /** Delete a visible scheduled cron job. Returns true when removed. */
@@ -94,7 +94,9 @@ export async function initializeExtensions(session: AgentSession, options: Initi
94
94
  {
95
95
  getModel: () => session.model,
96
96
  isIdle: () => !session.isStreaming,
97
+ getActivePromptHandle: () => session.activePromptHandle,
97
98
  abort: () => session.abort(),
99
+ abortPromptAndWait: (handle, abortOptions) => session.abortPromptAndWait(handle, abortOptions),
98
100
  hasPendingMessages: () => session.queuedMessageCount > 0,
99
101
  getPendingMessageCounts: () => session.pendingMessageCounts,
100
102
  getTranscript: () => session.getTranscript(),
@@ -168,6 +170,7 @@ export async function initializeExtensions(session: AgentSession, options: Initi
168
170
  },
169
171
  getJobs: () => session.getAsyncJobSnapshot(),
170
172
  setSdkPermissionProvider: provider => session.setSdkPermissionProvider(provider),
173
+ setSdkClientBridge: bridge => session.setClientBridge(bridge),
171
174
  sdkControl: async (operation, input) => {
172
175
  switch (operation) {
173
176
  case "model.set": {
@@ -1826,32 +1826,34 @@ export async function initTheme(
1826
1826
  export async function setTheme(
1827
1827
  name: string,
1828
1828
  enableWatcher: boolean = false,
1829
+ options?: { shouldApply?: () => boolean },
1829
1830
  ): Promise<{ success: boolean; error?: string }> {
1830
- autoDetectedTheme = false;
1831
- previewThemeActive = false;
1832
- currentThemeName = name;
1833
1831
  const requestId = ++themeLoadRequestId;
1832
+ const shouldApply = options?.shouldApply ?? (() => true);
1834
1833
  try {
1835
1834
  const loadedTheme = await loadTheme(name, getCurrentThemeOptions());
1836
1835
  if (requestId !== themeLoadRequestId) {
1837
1836
  return { success: false, error: "Theme change superseded by a newer request" };
1838
1837
  }
1838
+ if (!shouldApply()) return { success: false, error: "Theme change cancelled" };
1839
+ autoDetectedTheme = false;
1840
+ previewThemeActive = false;
1841
+ currentThemeName = name;
1839
1842
  theme = loadedTheme;
1840
1843
  if (enableWatcher) {
1841
1844
  await startThemeWatcher();
1842
1845
  }
1843
- if (onThemeChangeCallback) {
1844
- onThemeChangeCallback();
1845
- }
1846
+ if (!shouldApply()) return { success: false, error: "Theme change cancelled" };
1847
+ onThemeChangeCallback?.();
1846
1848
  return { success: true };
1847
1849
  } catch (error) {
1848
1850
  if (requestId !== themeLoadRequestId) {
1849
1851
  return { success: false, error: "Theme change superseded by a newer request" };
1850
1852
  }
1851
- // Theme is invalid - fall back to red-claw theme
1853
+ const fallbackTheme = await loadTheme("red-claw", getCurrentThemeOptions());
1854
+ if (!shouldApply()) return { success: false, error: "Theme change cancelled" };
1852
1855
  currentThemeName = "red-claw";
1853
- theme = await loadTheme("red-claw", getCurrentThemeOptions());
1854
- // Don't start watcher for fallback theme
1856
+ theme = fallbackTheme;
1855
1857
  return {
1856
1858
  success: false,
1857
1859
  error: error instanceof Error ? error.message : String(error),