@gajae-code/coding-agent 0.12.1 → 0.12.4

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 (66) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/types/config/settings-schema.d.ts +2 -2
  3. package/dist/types/gjc-runtime/ralplan-review-conflicts.d.ts +114 -0
  4. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -3
  5. package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +5 -1
  6. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -2
  7. package/dist/types/modes/components/irc-sidebar.d.ts +16 -0
  8. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  9. package/dist/types/modes/controllers/event-controller.d.ts +1 -1
  10. package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
  11. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  12. package/dist/types/modes/interactive-mode.d.ts +9 -0
  13. package/dist/types/modes/theme/theme.d.ts +3 -1
  14. package/dist/types/modes/types.d.ts +32 -0
  15. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  16. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -2
  17. package/dist/types/session/agent-session.d.ts +2 -0
  18. package/dist/types/session/internal/managed-session-storage.d.ts +22 -3
  19. package/dist/types/task/index.d.ts +4 -0
  20. package/dist/types/task/output-manager.d.ts +1 -0
  21. package/dist/types/task/receipt.d.ts +2 -0
  22. package/dist/types/tools/index.d.ts +2 -0
  23. package/package.json +8 -8
  24. package/scripts/dogfood-ralplan-review-conflicts.ts +272 -0
  25. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  26. package/scripts/run-acp-conformance.ts +5 -0
  27. package/src/async/job-manager.ts +22 -20
  28. package/src/config/provider-ranking.ts +1 -0
  29. package/src/config/settings-schema.ts +2 -2
  30. package/src/debug/index.ts +42 -17
  31. package/src/defaults/gjc/skills/ralplan/SKILL.md +9 -7
  32. package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -5
  33. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +2 -1
  34. package/src/gjc-runtime/launch-worktree.ts +103 -1
  35. package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
  36. package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
  37. package/src/gjc-runtime/ralplan-runtime.ts +80 -10
  38. package/src/gjc-runtime/ultragoal-evidence.ts +110 -49
  39. package/src/gjc-runtime/ultragoal-runtime.ts +467 -22
  40. package/src/gjc-runtime/workflow-manifest.generated.json +29 -0
  41. package/src/gjc-runtime/workflow-manifest.ts +17 -2
  42. package/src/internal-urls/docs-index.generated.ts +2 -2
  43. package/src/modes/DESIGN.md +53 -0
  44. package/src/modes/components/irc-sidebar.ts +30 -1
  45. package/src/modes/components/tool-status-header.ts +0 -1
  46. package/src/modes/controllers/command-controller.ts +38 -34
  47. package/src/modes/controllers/event-controller.ts +27 -16
  48. package/src/modes/controllers/extension-ui-controller.ts +186 -34
  49. package/src/modes/controllers/selector-controller.ts +115 -22
  50. package/src/modes/interactive-mode.ts +189 -62
  51. package/src/modes/jobs-observer.ts +1 -1
  52. package/src/modes/theme/theme.ts +11 -9
  53. package/src/modes/types.ts +88 -0
  54. package/src/sdk/broker/lifecycle.ts +18 -13
  55. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  56. package/src/sdk/bus/telegram-daemon-contract.ts +3 -2
  57. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  58. package/src/sdk/session.ts +1 -0
  59. package/src/session/agent-session.ts +22 -1
  60. package/src/session/internal/managed-session-scope.ts +158 -107
  61. package/src/session/internal/managed-session-storage.ts +445 -57
  62. package/src/session/session-storage.ts +29 -44
  63. package/src/task/index.ts +257 -31
  64. package/src/task/output-manager.ts +24 -14
  65. package/src/task/receipt.ts +11 -1
  66. package/src/tools/index.ts +2 -0
@@ -291,6 +291,59 @@ textual state without SGR. Korean, Japanese, Chinese, and mixed CJK/Latin prose
291
291
  must wrap at semantic phrase boundaries, never through an action/status label or
292
292
  short identifier; a semantic CJK break is a visual-QA failure.
293
293
 
294
+ ### Direct-root anatomy, IRC lane, and pin boundary
295
+
296
+ The production root has one ordered, direct-child anatomy. Do not wrap, reorder,
297
+ or independently pin these regions:
298
+
299
+ 1. `ircSplitView` is the viewport anchor and owns the transcript (and the IRC
300
+ sidebar when effective);
301
+ 2. `pendingMessagesContainer`, `statusContainer`, `todoContainer`, and
302
+ `btwContainer` follow the anchor **before** the pin boundary;
303
+ 3. `statusLine` is the pin boundary; and
304
+ 4. `hookWidgetContainerAbove`, `editorContainer`, `petFloorContainer`, and
305
+ `hookWidgetContainerBelow` are the later direct composer children in that
306
+ order.
307
+
308
+ `statusContainer` is transient operation/status content in the pre-boundary
309
+ application flow; it is not the persistent `statusLine` telemetry rail. The
310
+ status line begins the fixed suffix. The focused editor, its cursor, and every
311
+ later direct composer child remain below that boundary during manual history,
312
+ streaming, and reflow. Pending messages, todo, and BTW therefore remain
313
+ transcript-adjacent rather than becoming accidental fixed chrome.
314
+
315
+ IRC has one shared work-lane geometry. At **64 cells or narrower** the IRC lane
316
+ is ineffective and transcript/todo use the full width. At **65 cells** it is
317
+ exactly **32 / 3 / 30** cells (left work lane / separator / IRC lane). At wider
318
+ sizes the same split calculation applies; todo uses the left work lane whenever
319
+ IRC is effective, including empty, streaming, and long IRC histories. Todo
320
+ does not create a second sidebar, a separately rounded width, or a different
321
+ collapse rule.
322
+
323
+ Todo is absent when empty. Populated todos support long text, multiple phases,
324
+ and collapsed/expanded views without crossing the separator or overflowing
325
+ their work lane. The active phase is retained in collapsed view; expanded view
326
+ keeps phase/task order. IRC empty, streaming, and long states preserve the
327
+ same root order and pin boundary.
328
+
329
+ Row reservation is content-priority based: reserve the focused composer and
330
+ status line first, then normal hooks; when height is constrained, drop the
331
+ manual-output notice first, then the decorative pet, then low-priority hooks.
332
+ A zero-row transcript capacity is valid. Neither reservation nor degradation
333
+ may hide focus/cursor geometry, move an anchor into pinned chrome, or turn a
334
+ resize into a follow/manual state change. Manual and follow paths, streaming
335
+ updates, width growth/shrink, and height growth/shrink preserve the anchor
336
+ semantics; resize must recompute the shared IRC/todo lane before rendering.
337
+
338
+ All lane measurement, clipping, and wrapping is ANSI-aware terminal-cell
339
+ measurement. Terminal graphics may be shown only where the effective layout
340
+ permits them and must degrade to the textual graphics fallback without changing
341
+ row ownership. ANSI/color output preserves visible cursor/focus state; ASCII
342
+ and no-color output retains textual selection/status affordances. CJK and mixed
343
+ CJK/Latin todo, IRC, status, and BTW text break at semantic phrase boundaries,
344
+ not inside a phase/action/status label, short identifier, or grapheme cluster.
345
+ Those defects, width overflow, overlap, hidden focus/cursor, and lost anchors
346
+ are blocking visual-QA failures.
294
347
  ### Sticky viewport deterministic visual QA
295
348
 
296
349
  `test/fixtures/tui/sticky-viewport-showcase.ts` is a fixed-clock, no-network,
@@ -82,6 +82,35 @@ export function computeIrcSplitWidths(width: number): {
82
82
  rightWidth: sidebarWidth,
83
83
  };
84
84
  }
85
+ /**
86
+ * Resolves the single shared work-lane geometry. The left lane is used by both
87
+ * transcript and todo content whenever the IRC lane is effective.
88
+ */
89
+ export function computeIrcWorkLaneWidths(
90
+ width: number,
91
+ sidebarVisible: boolean,
92
+ ): { leftWidth: number; separatorWidth: number; rightWidth: number } {
93
+ const fullWidth = Math.max(0, Math.floor(width));
94
+ return sidebarVisible
95
+ ? computeIrcSplitWidths(fullWidth)
96
+ : { leftWidth: fullWidth, separatorWidth: 0, rightWidth: 0 };
97
+ }
98
+
99
+ export class IrcLeftLaneComponent implements Component {
100
+ constructor(
101
+ private readonly content: Component,
102
+ private readonly isSidebarVisible: (width: number) => boolean,
103
+ ) {}
104
+
105
+ render(width: number): string[] {
106
+ const layout = computeIrcWorkLaneWidths(width, this.isSidebarVisible(width));
107
+ return this.content.render(layout.leftWidth);
108
+ }
109
+
110
+ invalidate(): void {
111
+ this.content.invalidate?.();
112
+ }
113
+ }
85
114
 
86
115
  type SemanticLine = Readonly<{
87
116
  kind: "header" | "body" | "elision" | "blank";
@@ -288,7 +317,7 @@ export class IrcSplitViewComponent implements ViewportAnchorProvider {
288
317
  renderWithViewportAnchors(width: number): ViewportAnchorRender {
289
318
  if (!this.#visible) return renderComponentWithViewportAnchors(this.leftPane, width);
290
319
  const componentTheme = typeof this.componentTheme === "function" ? this.componentTheme() : this.componentTheme;
291
- const { leftWidth, separatorWidth, rightWidth } = computeIrcSplitWidths(width);
320
+ const { leftWidth, separatorWidth, rightWidth } = computeIrcWorkLaneWidths(width, this.#visible);
292
321
  if (rightWidth === 0) return renderComponentWithViewportAnchors(this.leftPane, width);
293
322
  const separator = separatorWidth > 0 ? componentTheme.fg("dim", ` ${componentTheme.boxSharp.vertical} `) : "";
294
323
  const leftRender = withTerminalGraphicsFallback(
@@ -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"),
@@ -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();
@@ -1256,11 +1258,7 @@ export class CommandController {
1256
1258
  customInstructionsOrOptions?: string | CompactOptions,
1257
1259
  isAuto = false,
1258
1260
  ): Promise<CompactionOutcome> {
1259
- if (this.ctx.loadingAnimation) {
1260
- this.ctx.loadingAnimation.stop();
1261
- this.ctx.loadingAnimation = undefined;
1262
- }
1263
- this.ctx.statusContainer.clear();
1261
+ const releaseActivityIndicator = suspendInteractiveActivityIndicator(this.ctx);
1264
1262
 
1265
1263
  const originalOnEscape = this.ctx.editor.onEscape;
1266
1264
  this.ctx.editor.onEscape = () => {
@@ -1287,24 +1285,28 @@ export class CommandController {
1287
1285
  ? customInstructionsOrOptions
1288
1286
  : undefined;
1289
1287
  await this.ctx.session.compact(instructions, options);
1288
+ if (this.ctx.isStopped?.()) return outcome;
1290
1289
 
1291
1290
  this.ctx.rebuildChatFromMessages("reconcile-same-transcript");
1292
1291
 
1293
1292
  this.ctx.statusLine.invalidate();
1294
1293
  this.ctx.updateEditorTopBorder();
1295
1294
  } catch (error) {
1296
- if (error instanceof CompactionCancelledError) {
1297
- outcome = "cancelled";
1295
+ outcome = error instanceof CompactionCancelledError ? "cancelled" : "failed";
1296
+ if (this.ctx.isStopped?.()) return outcome;
1297
+ if (outcome === "cancelled") {
1298
1298
  this.ctx.showError("Compaction cancelled");
1299
1299
  } else {
1300
- outcome = "failed";
1301
1300
  const message = error instanceof Error ? error.message : String(error);
1302
1301
  this.ctx.showError(`Compaction failed: ${message}`);
1303
1302
  }
1304
1303
  } finally {
1305
1304
  compactingLoader.stop();
1306
- this.ctx.statusContainer.clear();
1307
- this.ctx.editor.onEscape = originalOnEscape;
1305
+ if (!this.ctx.isStopped?.()) {
1306
+ this.ctx.statusContainer.clear();
1307
+ this.ctx.editor.onEscape = originalOnEscape;
1308
+ }
1309
+ releaseActivityIndicator();
1308
1310
  }
1309
1311
  await this.ctx.flushCompactionQueue({ willRetry: false });
1310
1312
  return outcome;
@@ -1323,11 +1325,7 @@ export class CommandController {
1323
1325
  return;
1324
1326
  }
1325
1327
 
1326
- if (this.ctx.loadingAnimation) {
1327
- this.ctx.loadingAnimation.stop();
1328
- this.ctx.loadingAnimation = undefined;
1329
- }
1330
- this.ctx.statusContainer.clear();
1328
+ const releaseActivityIndicator = suspendInteractiveActivityIndicator(this.ctx);
1331
1329
 
1332
1330
  const originalOnEscape = this.ctx.editor.onEscape;
1333
1331
  this.ctx.editor.onEscape = () => {
@@ -1347,6 +1345,7 @@ export class CommandController {
1347
1345
  try {
1348
1346
  // Handoff generation runs as a oneshot request; the new session is shown after it completes.
1349
1347
  const result = await this.ctx.session.handoff(customInstructions);
1348
+ if (this.ctx.isStopped?.()) return;
1350
1349
 
1351
1350
  if (!result) {
1352
1351
  this.ctx.showError("Handoff cancelled");
@@ -1361,6 +1360,7 @@ export class CommandController {
1361
1360
  this.ctx.updateEditorTopBorder();
1362
1361
  this.ctx.updateEditorBorderColor();
1363
1362
  await this.ctx.reloadTodos();
1363
+ if (this.ctx.isStopped?.()) return;
1364
1364
 
1365
1365
  this.ctx.chatContainer.addChild(new Spacer(1));
1366
1366
  this.ctx.chatContainer.addChild(
@@ -1370,6 +1370,7 @@ export class CommandController {
1370
1370
  this.ctx.showStatus(`Handoff document saved to: ${result.savedPath}`);
1371
1371
  }
1372
1372
  } catch (error) {
1373
+ if (this.ctx.isStopped?.()) return;
1373
1374
  const message = error instanceof Error ? error.message : String(error);
1374
1375
  if (message === "Handoff cancelled" || (error instanceof Error && error.name === "AbortError")) {
1375
1376
  this.ctx.showError("Handoff cancelled");
@@ -1389,8 +1390,11 @@ export class CommandController {
1389
1390
  }
1390
1391
  } finally {
1391
1392
  handoffLoader.stop();
1392
- this.ctx.statusContainer.clear();
1393
- this.ctx.editor.onEscape = originalOnEscape;
1393
+ if (!this.ctx.isStopped?.()) {
1394
+ this.ctx.statusContainer.clear();
1395
+ this.ctx.editor.onEscape = originalOnEscape;
1396
+ }
1397
+ releaseActivityIndicator();
1394
1398
  }
1395
1399
  }
1396
1400
 
@@ -15,7 +15,12 @@ import { TodoReminderComponent } from "../../modes/components/todo-reminder";
15
15
  import { ToolExecutionComponent, type ToolExecutionHandle } from "../../modes/components/tool-execution";
16
16
  import { TtsrNotificationComponent } from "../../modes/components/ttsr-notification";
17
17
  import { getSymbolTheme, theme } from "../../modes/theme/theme";
18
- import type { InteractiveModeContext, TodoPhase } from "../../modes/types";
18
+ import {
19
+ type InteractiveModeContext,
20
+ stopInteractiveActivityIndicator,
21
+ syncInteractiveActivityIndicator,
22
+ type TodoPhase,
23
+ } from "../../modes/types";
19
24
  import type { PlanApprovalDetails } from "../../plan-mode/approved-plan";
20
25
  import { completionNotifyDisabledByEnv } from "../../sdk/bus/config";
21
26
  import { summaryFromMessage } from "../../sdk/bus/helpers";
@@ -166,6 +171,14 @@ export class EventController {
166
171
  this.ctx.retryEscapeHandler = undefined;
167
172
  }
168
173
  this.ctx.retryEscapePrimed = false;
174
+ if (this.ctx.autoCompactionEscapeHandler) {
175
+ this.ctx.editor.onEscape = this.ctx.autoCompactionEscapeHandler;
176
+ this.ctx.autoCompactionEscapeHandler = undefined;
177
+ }
178
+ if (this.ctx.autoCompactionLoader) {
179
+ this.ctx.autoCompactionLoader.stop();
180
+ this.ctx.autoCompactionLoader = undefined;
181
+ }
169
182
  if (this.ctx.retryLoader) {
170
183
  this.ctx.retryLoader.stop();
171
184
  this.ctx.retryLoader = undefined;
@@ -266,7 +279,9 @@ export class EventController {
266
279
  }
267
280
 
268
281
  async #dispatchEvent(event: AgentSessionEvent): Promise<void> {
282
+ if (this.ctx.isStopped?.()) return;
269
283
  if (!this.ctx.isInitialized) await this.ctx.init();
284
+ if (this.ctx.isStopped?.()) return;
270
285
  this.#visibleTranscriptChanged = false;
271
286
  this.#handlingEvent = true;
272
287
  try {
@@ -274,6 +289,7 @@ export class EventController {
274
289
  this.ctx.updateEditorTopBorder();
275
290
  const run = this.#handlers[event.type] as (e: AgentSessionEvent) => Promise<void>;
276
291
  await run(event);
292
+ if (this.ctx.isStopped?.()) return;
277
293
  if (this.#visibleTranscriptChanged) this.ctx.recordVisibleTranscriptMutation?.();
278
294
  if (this.ctx.isTranscriptViewerOpen?.()) this.ctx.refreshTranscriptViewer?.();
279
295
  } finally {
@@ -878,23 +894,22 @@ export class EventController {
878
894
  const planDetails = details.sourceResultDetails as PlanApprovalDetails | undefined;
879
895
  if (planDetails) {
880
896
  await this.ctx.planModeController.handleApproval(planDetails);
897
+ if (this.ctx.isStopped?.()) return;
881
898
  }
882
899
  }
883
900
  }
884
901
  }
885
902
 
886
903
  async #handleAgentEnd(_event: Extract<AgentSessionEvent, { type: "agent_end" }>): Promise<void> {
887
- if (this.ctx.loadingAnimation) {
888
- this.ctx.loadingAnimation.stop();
889
- this.ctx.loadingAnimation = undefined;
890
- this.ctx.statusContainer.clear();
891
- }
904
+ this.ctx.setWorkingMessage(undefined);
905
+ stopInteractiveActivityIndicator(this.ctx);
892
906
  if (this.ctx.streamingComponent) {
893
907
  this.ctx.chatContainer.removeChild(this.ctx.streamingComponent);
894
908
  this.ctx.streamingComponent = undefined;
895
909
  this.ctx.streamingMessage = undefined;
896
910
  }
897
911
  await this.ctx.planModeController.flushPendingModelSwitch();
912
+ if (this.ctx.isStopped?.()) return;
898
913
  for (const toolCallId of Array.from(this.ctx.pendingTools.keys())) {
899
914
  if (!this.#backgroundToolCallIds.has(toolCallId)) {
900
915
  this.ctx.pendingTools.delete(toolCallId);
@@ -922,11 +937,7 @@ export class EventController {
922
937
  this.ctx.editor.onEscape = () => {
923
938
  this.ctx.session.abortCompaction();
924
939
  };
925
- if (this.ctx.loadingAnimation) {
926
- this.ctx.loadingAnimation.stop();
927
- this.ctx.loadingAnimation = undefined;
928
- }
929
- this.ctx.statusContainer.clear();
940
+ stopInteractiveActivityIndicator(this.ctx, { restoreBackground: false });
930
941
  const reasonText =
931
942
  event.reason === "overflow" ? "Context overflow detected, " : event.reason === "idle" ? "Idle " : "";
932
943
  const actionLabel = event.action === "handoff" ? "Auto-handoff" : "Auto context-full maintenance";
@@ -982,6 +993,7 @@ export class EventController {
982
993
  this.ctx.statusLine.invalidate();
983
994
  this.ctx.updateEditorTopBorder();
984
995
  await this.ctx.reloadTodos();
996
+ if (this.ctx.isStopped?.()) return;
985
997
  this.ctx.showStatus("Auto-handoff completed");
986
998
  } else if (event.skipped) {
987
999
  // Benign skip: no model selected, no candidate models available, or nothing
@@ -995,6 +1007,8 @@ export class EventController {
995
1007
  this.ctx.showWarning("Auto context-full maintenance failed; continuing without maintenance");
996
1008
  }
997
1009
  await this.ctx.flushCompactionQueue({ willRetry: event.willRetry });
1010
+ if (this.ctx.isStopped?.()) return;
1011
+ syncInteractiveActivityIndicator(this.ctx);
998
1012
  this.ctx.ui.requestRender();
999
1013
  }
1000
1014
 
@@ -1024,11 +1038,7 @@ export class EventController {
1024
1038
  this.ctx.session.abortRetry();
1025
1039
  }
1026
1040
  };
1027
- if (this.ctx.loadingAnimation) {
1028
- this.ctx.loadingAnimation.stop();
1029
- this.ctx.loadingAnimation = undefined;
1030
- }
1031
- this.ctx.statusContainer.clear();
1041
+ stopInteractiveActivityIndicator(this.ctx, { restoreBackground: false });
1032
1042
  // Stop any prior retry loader/timer before installing a new one.
1033
1043
  this.ctx.retryLoader?.stop();
1034
1044
  this.#clearRetryCountdown();
@@ -1071,6 +1081,7 @@ export class EventController {
1071
1081
  if (!event.success) {
1072
1082
  this.ctx.showError(`Retry failed after ${event.attempt} attempts: ${event.finalError || "Unknown error"}`);
1073
1083
  }
1084
+ syncInteractiveActivityIndicator(this.ctx);
1074
1085
  this.ctx.ui.requestRender();
1075
1086
  }
1076
1087