@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,7 +22,12 @@ import { HookEditorComponent } from "../../modes/components/hook-editor";
22
22
  import { HookInputComponent } from "../../modes/components/hook-input";
23
23
  import { HookSelectorComponent } from "../../modes/components/hook-selector";
24
24
  import { getAvailableThemesWithPaths, getThemeByName, setTheme, type Theme, theme } from "../../modes/theme/theme";
25
- import type { InteractiveModeContext } from "../../modes/types";
25
+ import {
26
+ clearInteractiveActivityLoaders,
27
+ type InteractiveModeContext,
28
+ stopInteractiveActivityIndicator,
29
+ syncInteractiveActivityIndicator,
30
+ } from "../../modes/types";
26
31
  import { createReadonlySessionManager } from "../../session/session-manager";
27
32
  import { parseThinkingLevel } from "../../thinking";
28
33
  import type { TodoPhase } from "../../tools/todo-write";
@@ -36,12 +41,55 @@ const HOOK_SELECTOR_CHROME_ROWS = 7;
36
41
  const HOOK_SELECTOR_OUTLINE_ROWS = 2;
37
42
  const HOOK_SELECTOR_INLINE_INPUT_ROWS = 2;
38
43
 
44
+ const EXTENSION_ACTION_MUTATIONS: ReadonlySet<PropertyKey> = new Set([
45
+ "sendMessage",
46
+ "sendUserMessage",
47
+ "appendEntry",
48
+ "setLabel",
49
+ "setActiveTools",
50
+ "setModel",
51
+ "setThinkingLevel",
52
+ "setThinkingVisibility",
53
+ "cycleThinkingLevel",
54
+ "setThinkingLevelForControl",
55
+ "setThinkingVisibilityForControl",
56
+ "setModelTemporaryForControl",
57
+ "setSessionName",
58
+ ]);
59
+ const EXTENSION_CONTEXT_MUTATIONS: ReadonlySet<PropertyKey> = new Set([
60
+ "abort",
61
+ "abortPromptAndWait",
62
+ "shutdown",
63
+ "compact",
64
+ "clearContext",
65
+ "cycleModel",
66
+ "setModelProfile",
67
+ "cycleThinkingLevel",
68
+ "setQueueMode",
69
+ "invokeSkill",
70
+ "setPlanMode",
71
+ "operateGoal",
72
+ "setSdkPermissionProvider",
73
+ "setSdkClientBridge",
74
+ "sdkControl",
75
+ ]);
76
+ const EXTENSION_COMMAND_MUTATIONS: ReadonlySet<PropertyKey> = new Set([
77
+ "reload",
78
+ "newSession",
79
+ "branch",
80
+ "navigateTree",
81
+ "compact",
82
+ "switchSession",
83
+ ]);
84
+
39
85
  export class ExtensionUiController {
40
86
  #extensionTerminalInputUnsubscribers = new Set<() => void>();
87
+ #extensionErrorUnsubscribe?: () => void;
41
88
  #hookWidgetsAbove = new Map<string, ExtensionUiComponent>();
42
89
  #hookWidgetsBelow = new Map<string, ExtensionUiComponent>();
43
90
  #activeHookCustomComponent?: Component & { dispose?(): void };
44
91
  #activeHookCustomOverlay?: OverlayHandle;
92
+ #activeHookCustomCancel?: () => void;
45
93
 
46
94
  constructor(private ctx: InteractiveModeContext) {}
47
95
 
@@ -317,6 +365,27 @@ export class ExtensionUiController {
317
365
  this.ctx.restoreComposer();
318
366
  }
319
367
 
368
+ #isStopped(): boolean {
369
+ return this.ctx.isStopped?.() === true;
370
+ }
371
+
372
+ #assertActive(): void {
373
+ if (this.#isStopped()) throw Object.assign(new Error("Interactive mode stopped"), { code: "cancelled" });
374
+ }
375
+
376
+ #guardMutations<T extends object>(target: T, mutationNames: ReadonlySet<PropertyKey>): T {
377
+ return new Proxy(target, {
378
+ get: (current, property, receiver) => {
379
+ const member = Reflect.get(current, property, receiver) as unknown;
380
+ if (!mutationNames.has(property) || typeof member !== "function") return member;
381
+ return (...args: unknown[]) => {
382
+ this.#assertActive();
383
+ return Reflect.apply(member, current, args);
384
+ };
385
+ },
386
+ });
387
+ }
388
+
320
389
  /**
321
390
  * Initialize the hook system with TUI-based UI context.
322
391
  */
@@ -329,13 +398,19 @@ export class ExtensionUiController {
329
398
  notify: (message, type) => this.showHookNotify(message, type),
330
399
  onTerminalInput: handler => this.addExtensionTerminalInputListener(handler),
331
400
  setStatus: (key, text) => this.setHookStatus(key, text),
332
- setWorkingMessage: message => this.ctx.setWorkingMessage(message),
401
+ setWorkingMessage: message => {
402
+ if (!this.#isStopped()) this.ctx.setWorkingMessage(message);
403
+ },
333
404
  setWidget: (key, content, options) => this.setHookWidget(key, content, options),
334
- setTitle: title => setTerminalTitle(title),
405
+ setTitle: title => {
406
+ if (!this.#isStopped()) setTerminalTitle(title);
407
+ },
335
408
  custom: (factory, options) => this.showHookCustom(factory, options),
336
- setEditorText: text => this.ctx.editor.setText(text),
409
+ setEditorText: text => {
410
+ if (!this.#isStopped()) this.ctx.editor.setText(text);
411
+ },
337
412
  pasteToEditor: text => {
338
- this.ctx.editor.handleInput(`\x1b[200~${text}\x1b[201~`);
413
+ if (!this.#isStopped()) this.ctx.editor.handleInput(`\x1b[200~${text}\x1b[201~`);
339
414
  },
340
415
  getEditorText: () => this.ctx.editor.getText(),
341
416
  editor: (title, prefill, dialogOptions, editorOptions) =>
@@ -346,17 +421,22 @@ export class ExtensionUiController {
346
421
  getAllThemes: async () => (await getAvailableThemesWithPaths()).map(t => ({ name: t.name, path: t.path })),
347
422
  getTheme: name => getThemeByName(name),
348
423
  setTheme: async themeArg => {
424
+ if (this.#isStopped()) return { success: false, error: "Interactive mode stopped" };
349
425
  if (typeof themeArg === "string") {
350
- return await setTheme(themeArg, true);
426
+ return await setTheme(themeArg, true, { shouldApply: () => !this.#isStopped() });
351
427
  }
352
428
  // Theme object passed directly - not supported in current implementation
353
429
  return Promise.resolve({ success: false, error: "Direct theme object not supported" });
354
430
  },
355
431
  setFooter: () => {},
356
432
  setHeader: () => {},
357
- setEditorComponent: factory => this.ctx.setEditorComponent(factory),
433
+ setEditorComponent: factory => {
434
+ if (!this.#isStopped()) this.ctx.setEditorComponent(factory);
435
+ },
358
436
  getToolsExpanded: () => this.ctx.toolOutputExpanded,
359
- setToolsExpanded: expanded => this.ctx.setToolsExpanded(expanded),
437
+ setToolsExpanded: expanded => {
438
+ if (!this.#isStopped()) this.ctx.setToolsExpanded(expanded);
439
+ },
360
440
  };
361
441
  this.ctx.setToolUIContext(uiContext, true);
362
442
 
@@ -370,8 +450,11 @@ export class ExtensionUiController {
370
450
  const wasStreaming = this.ctx.session.isStreaming;
371
451
  this.ctx.session
372
452
  .sendCustomMessage(message, options)
373
- .then(() => this.#applyCustomMessageDisplay(wasStreaming, message.display))
453
+ .then(() => {
454
+ if (!this.#isStopped()) this.#applyCustomMessageDisplay(wasStreaming, message.display);
455
+ })
374
456
  .catch((err: unknown) => {
457
+ if (this.#isStopped()) return;
375
458
  this.ctx.showError(
376
459
  `Extension sendMessage failed: ${err instanceof Error ? err.message : String(err)}`,
377
460
  );
@@ -416,7 +499,9 @@ export class ExtensionUiController {
416
499
  const contextActions: ExtensionContextActions = {
417
500
  getModel: () => this.ctx.session.model,
418
501
  isIdle: () => !this.ctx.session.isStreaming,
502
+ getActivePromptHandle: () => this.ctx.session.activePromptHandle,
419
503
  abort: () => this.ctx.session.abort(),
504
+ abortPromptAndWait: (handle, options) => this.ctx.session.abortPromptAndWait(handle, options),
420
505
  hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
421
506
  getPendingMessageCounts: () => this.ctx.session.pendingMessageCounts,
422
507
  getTranscript: () => this.ctx.session.getTranscript(),
@@ -468,6 +553,7 @@ export class ExtensionUiController {
468
553
  getBranchCandidates: () => this.ctx.sessionManager.getTree(),
469
554
  getExtensions: () => this.ctx.session.extensionRunner?.getExtensionPaths() ?? [],
470
555
  setSdkPermissionProvider: provider => this.ctx.session.setSdkPermissionProvider(provider),
556
+ setSdkClientBridge: bridge => this.ctx.session.setClientBridge(bridge),
471
557
  sdkControl: this.#sdkControl,
472
558
  };
473
559
  const commandActions: ExtensionCommandContextActions = {
@@ -476,10 +562,12 @@ export class ExtensionUiController {
476
562
  reload: async () => {
477
563
  const previousSessionId = this.ctx.sessionManager.getSessionId();
478
564
  await this.ctx.session.reload();
565
+ if (this.#isStopped()) return;
479
566
  const sessionIdentityChanged = previousSessionId !== this.ctx.sessionManager.getSessionId();
480
567
  if (sessionIdentityChanged) this.ctx.resetIrcSidebarSession();
481
568
  this.ctx.rebuildInitialMessages(sessionIdentityChanged ? "replace-identity" : "reconcile-same-transcript");
482
569
  await this.ctx.reloadTodos();
570
+ if (this.#isStopped()) return;
483
571
  this.ctx.showStatus("Reloaded session");
484
572
  },
485
573
  newSession: async options => {
@@ -488,18 +576,17 @@ export class ExtensionUiController {
488
576
  if (!success) {
489
577
  return { cancelled: true };
490
578
  }
579
+ if (this.#isStopped()) return { cancelled: true };
580
+ clearInteractiveActivityLoaders(this.ctx);
491
581
  cleanupPreviousSessionUi();
492
582
 
493
- if (this.ctx.loadingAnimation) {
494
- this.ctx.loadingAnimation.stop();
495
- this.ctx.loadingAnimation = undefined;
496
- }
497
- this.ctx.statusContainer.clear();
583
+ stopInteractiveActivityIndicator(this.ctx);
498
584
  this.ctx.resetIrcSidebarSession();
499
585
  setSessionTerminalTitle(this.ctx.sessionManager.getSessionName(), this.ctx.sessionManager.getCwd());
500
586
 
501
587
  if (options?.setup) {
502
588
  await options.setup(this.ctx.sessionManager);
589
+ if (this.#isStopped()) return { cancelled: true };
503
590
  }
504
591
 
505
592
  this.ctx.statusLine.invalidate();
@@ -519,12 +606,14 @@ export class ExtensionUiController {
519
606
  new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 0),
520
607
  );
521
608
  await this.ctx.reloadTodos();
609
+ if (this.#isStopped()) return { cancelled: true };
522
610
  this.ctx.ui.requestRender();
523
611
 
524
612
  return { cancelled: false };
525
613
  },
526
614
  branch: async entryId => {
527
615
  const result = await this.ctx.session.branch(entryId);
616
+ if (this.#isStopped()) return { cancelled: true };
528
617
  if (result.cancelled) {
529
618
  return { cancelled: true };
530
619
  }
@@ -533,6 +622,7 @@ export class ExtensionUiController {
533
622
  // Update UI
534
623
  this.ctx.rebuildInitialMessages("replace-identity");
535
624
  await this.ctx.reloadTodos();
625
+ if (this.#isStopped()) return { cancelled: true };
536
626
  this.ctx.editor.setText(result.selectedText);
537
627
  this.ctx.showStatus("Branched to new session");
538
628
 
@@ -540,6 +630,7 @@ export class ExtensionUiController {
540
630
  },
541
631
  navigateTree: async (targetId, options) => {
542
632
  const result = await this.ctx.session.navigateTree(targetId, { summarize: options?.summarize });
633
+ if (this.#isStopped()) return { cancelled: true };
543
634
  if (result.cancelled) {
544
635
  return { cancelled: true };
545
636
  }
@@ -547,6 +638,7 @@ export class ExtensionUiController {
547
638
  // Update UI
548
639
  this.ctx.rebuildInitialMessages("reconcile-same-transcript");
549
640
  await this.ctx.reloadTodos();
641
+ if (this.#isStopped()) return { cancelled: true };
550
642
  if (result.editorText && !this.ctx.editor.getText().trim()) {
551
643
  this.ctx.editor.setText(result.editorText);
552
644
  }
@@ -560,9 +652,11 @@ export class ExtensionUiController {
560
652
 
561
653
  this.clearHookWidgets();
562
654
  const result = await this.ctx.session.switchSession(sessionPath);
655
+ if (this.#isStopped()) return { cancelled: true };
563
656
  if (!result) {
564
657
  return { cancelled: true };
565
658
  }
659
+ clearInteractiveActivityLoaders(this.ctx);
566
660
  const switchingToDifferentSession = previousSessionId !== this.ctx.sessionManager.getSessionId();
567
661
  if (switchingToDifferentSession) this.ctx.resetIrcSidebarSession();
568
662
 
@@ -571,15 +665,23 @@ export class ExtensionUiController {
571
665
  switchingToDifferentSession ? "replace-identity" : "reconcile-same-transcript",
572
666
  );
573
667
  await this.ctx.reloadTodos();
668
+ if (this.#isStopped()) return { cancelled: true };
669
+ syncInteractiveActivityIndicator(this.ctx);
574
670
  return { cancelled: false };
575
671
  },
576
672
  };
577
673
 
578
- extensionRunner.initialize(actions, contextActions, commandActions, uiContext);
674
+ extensionRunner.initialize(
675
+ this.#guardMutations(actions, EXTENSION_ACTION_MUTATIONS),
676
+ this.#guardMutations(contextActions, EXTENSION_CONTEXT_MUTATIONS),
677
+ this.#guardMutations(commandActions, EXTENSION_COMMAND_MUTATIONS),
678
+ uiContext,
679
+ );
579
680
 
580
681
  // Subscribe to extension errors
581
- extensionRunner.onError((error: ExtensionError) => {
582
- this.showExtensionError(error.extensionPath, error.error);
682
+ this.#extensionErrorUnsubscribe?.();
683
+ this.#extensionErrorUnsubscribe = extensionRunner.onError((error: ExtensionError) => {
684
+ if (!this.#isStopped()) this.showExtensionError(error.extensionPath, error.error);
583
685
  });
584
686
 
585
687
  // Emit session_start event
@@ -589,6 +691,7 @@ export class ExtensionUiController {
589
691
  }
590
692
 
591
693
  setHookWidget(key: string, content: ExtensionWidgetContent, options?: ExtensionWidgetOptions): void {
694
+ if (this.#isStopped()) return;
592
695
  const placement = options?.placement ?? "aboveEditor";
593
696
  this.#removeHookWidget(this.#hookWidgetsAbove, key);
594
697
  this.#removeHookWidget(this.#hookWidgetsBelow, key);
@@ -715,7 +818,9 @@ export class ExtensionUiController {
715
818
  const contextActions: ExtensionContextActions = {
716
819
  getModel: () => this.ctx.session.model,
717
820
  isIdle: () => !this.ctx.session.isStreaming,
821
+ getActivePromptHandle: () => this.ctx.session.activePromptHandle,
718
822
  abort: () => this.ctx.session.abort(),
823
+ abortPromptAndWait: (handle, options) => this.ctx.session.abortPromptAndWait(handle, options),
719
824
  hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
720
825
  getPendingMessageCounts: () => this.ctx.session.pendingMessageCounts,
721
826
  getTranscript: () => this.ctx.session.getTranscript(),
@@ -764,6 +869,7 @@ export class ExtensionUiController {
764
869
  getBranchCandidates: () => this.ctx.sessionManager.getTree(),
765
870
  getExtensions: () => this.ctx.session.extensionRunner?.getExtensionPaths() ?? [],
766
871
  setSdkPermissionProvider: provider => this.ctx.session.setSdkPermissionProvider(provider),
872
+ setSdkClientBridge: bridge => this.ctx.session.setClientBridge(bridge),
767
873
  sdkControl: this.#sdkControl,
768
874
  };
769
875
  const commandActions: ExtensionCommandContextActions = {
@@ -775,10 +881,12 @@ export class ExtensionUiController {
775
881
  }
776
882
  const previousSessionId = this.ctx.sessionManager.getSessionId();
777
883
  await this.ctx.session.reload();
884
+ if (this.#isStopped()) return;
778
885
  const sessionIdentityChanged = previousSessionId !== this.ctx.sessionManager.getSessionId();
779
886
  if (sessionIdentityChanged) this.ctx.resetIrcSidebarSession();
780
887
  this.ctx.rebuildInitialMessages(sessionIdentityChanged ? "replace-identity" : "reconcile-same-transcript");
781
888
  await this.ctx.reloadTodos();
889
+ if (this.#isStopped()) return;
782
890
  this.ctx.showStatus("Reloaded session");
783
891
  },
784
892
  newSession: async options => {
@@ -790,17 +898,16 @@ export class ExtensionUiController {
790
898
  if (!success) {
791
899
  return { cancelled: true };
792
900
  }
901
+ if (this.#isStopped()) return { cancelled: true };
902
+ clearInteractiveActivityLoaders(this.ctx);
793
903
  cleanupPreviousSessionUi();
794
904
 
795
- if (this.ctx.loadingAnimation) {
796
- this.ctx.loadingAnimation.stop();
797
- this.ctx.loadingAnimation = undefined;
798
- }
799
- this.ctx.statusContainer.clear();
905
+ stopInteractiveActivityIndicator(this.ctx);
800
906
  this.ctx.resetIrcSidebarSession();
801
907
 
802
908
  if (options?.setup) {
803
909
  await options.setup(this.ctx.sessionManager);
910
+ if (this.#isStopped()) return { cancelled: true };
804
911
  }
805
912
 
806
913
  prepareTranscriptRebuild(this.ctx.ui, "replace-identity");
@@ -815,6 +922,7 @@ export class ExtensionUiController {
815
922
  new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 0),
816
923
  );
817
924
  await this.ctx.reloadTodos();
925
+ if (this.#isStopped()) return { cancelled: true };
818
926
  this.ctx.ui.requestRender();
819
927
 
820
928
  return { cancelled: false };
@@ -824,6 +932,7 @@ export class ExtensionUiController {
824
932
  return { cancelled: true };
825
933
  }
826
934
  const result = await this.ctx.session.branch(entryId);
935
+ if (this.#isStopped()) return { cancelled: true };
827
936
  if (result.cancelled) {
828
937
  return { cancelled: true };
829
938
  }
@@ -832,6 +941,7 @@ export class ExtensionUiController {
832
941
  // Update UI
833
942
  this.ctx.rebuildInitialMessages("replace-identity");
834
943
  await this.ctx.reloadTodos();
944
+ if (this.#isStopped()) return { cancelled: true };
835
945
  this.ctx.editor.setText(result.selectedText);
836
946
  this.ctx.showStatus("Branched to new session");
837
947
 
@@ -842,6 +952,7 @@ export class ExtensionUiController {
842
952
  return { cancelled: true };
843
953
  }
844
954
  const result = await this.ctx.session.navigateTree(targetId, { summarize: options?.summarize });
955
+ if (this.#isStopped()) return { cancelled: true };
845
956
  if (result.cancelled) {
846
957
  return { cancelled: true };
847
958
  }
@@ -849,6 +960,7 @@ export class ExtensionUiController {
849
960
  // Update UI
850
961
  this.ctx.rebuildInitialMessages("reconcile-same-transcript");
851
962
  await this.ctx.reloadTodos();
963
+ if (this.#isStopped()) return { cancelled: true };
852
964
  if (result.editorText && !this.ctx.editor.getText().trim()) {
853
965
  this.ctx.editor.setText(result.editorText);
854
966
  }
@@ -865,20 +977,29 @@ export class ExtensionUiController {
865
977
 
866
978
  this.clearHookWidgets();
867
979
  const result = await this.ctx.session.switchSession(sessionPath);
980
+ if (this.#isStopped()) return { cancelled: true };
868
981
  if (!result) {
869
982
  return { cancelled: true };
870
983
  }
984
+ clearInteractiveActivityLoaders(this.ctx);
871
985
  const switchingToDifferentSession = previousSessionId !== this.ctx.sessionManager.getSessionId();
872
986
  if (switchingToDifferentSession) this.ctx.resetIrcSidebarSession();
873
987
  this.ctx.rebuildInitialMessages(
874
988
  switchingToDifferentSession ? "replace-identity" : "reconcile-same-transcript",
875
989
  );
876
990
  await this.ctx.reloadTodos();
991
+ if (this.#isStopped()) return { cancelled: true };
992
+ syncInteractiveActivityIndicator(this.ctx);
877
993
  return { cancelled: false };
878
994
  },
879
995
  };
880
996
 
881
- extensionRunner.initialize(actions, contextActions, commandActions, uiContext);
997
+ extensionRunner.initialize(
998
+ this.#guardMutations(actions, EXTENSION_ACTION_MUTATIONS),
999
+ this.#guardMutations(contextActions, EXTENSION_CONTEXT_MUTATIONS),
1000
+ this.#guardMutations(commandActions, EXTENSION_COMMAND_MUTATIONS),
1001
+ uiContext,
1002
+ );
882
1003
  }
883
1004
 
884
1005
  createBackgroundUiContext(): ExtensionUIContext {
@@ -935,6 +1056,7 @@ export class ExtensionUiController {
935
1056
  sessionManager: createReadonlySessionManager(this.ctx.session.sessionManager),
936
1057
  modelRegistry: this.ctx.session.modelRegistry,
937
1058
  model: this.ctx.session.model,
1059
+ getActivePromptHandle: () => this.ctx.session.activePromptHandle,
938
1060
  isIdle: () => !this.ctx.session.isStreaming,
939
1061
  hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
940
1062
  getPendingMessageCounts: () => this.ctx.session.pendingMessageCounts,
@@ -1022,7 +1144,7 @@ export class ExtensionUiController {
1022
1144
  * Set hook status text in the footer.
1023
1145
  */
1024
1146
  setHookStatus(key: string, text: string | undefined): void {
1025
- if (this.ctx.isBackgrounded) {
1147
+ if (this.#isStopped() || this.ctx.isBackgrounded) {
1026
1148
  return;
1027
1149
  }
1028
1150
  this.ctx.statusLine.setHookStatus(key, text);
@@ -1037,6 +1159,7 @@ export class ExtensionUiController {
1037
1159
  options: string[],
1038
1160
  dialogOptions?: ExtensionUIDialogOptions,
1039
1161
  ): Promise<string | undefined> {
1162
+ if (this.#isStopped()) return Promise.resolve(undefined);
1040
1163
  const { promise, finish, attachAbort } = this.#createHookDialogState(
1041
1164
  () => this.hideHookSelector(),
1042
1165
  dialogOptions?.signal,
@@ -1141,8 +1264,9 @@ export class ExtensionUiController {
1141
1264
  */
1142
1265
  hideHookSelector(): void {
1143
1266
  this.ctx.hookSelector?.dispose();
1144
- this.#restoreComposerEditor();
1145
1267
  this.ctx.hookSelector = undefined;
1268
+ if (this.#isStopped()) return;
1269
+ this.#restoreComposerEditor();
1146
1270
  this.ctx.ui.setFocus(this.ctx.editor);
1147
1271
  this.ctx.ui.requestRender();
1148
1272
  }
@@ -1164,6 +1288,7 @@ export class ExtensionUiController {
1164
1288
  dialogOptions?: ExtensionUIDialogOptions,
1165
1289
  inputOptions?: { readonly initialValue?: string },
1166
1290
  ): Promise<string | undefined> {
1291
+ if (this.#isStopped()) return Promise.resolve(undefined);
1167
1292
  const { promise, finish, attachAbort } = this.#createHookDialogState(
1168
1293
  () => this.hideHookInput(),
1169
1294
  dialogOptions?.signal,
@@ -1202,8 +1327,9 @@ export class ExtensionUiController {
1202
1327
  */
1203
1328
  hideHookInput(): void {
1204
1329
  this.ctx.hookInput?.dispose();
1205
- this.#restoreComposerEditor();
1206
1330
  this.ctx.hookInput = undefined;
1331
+ if (this.#isStopped()) return;
1332
+ this.#restoreComposerEditor();
1207
1333
  this.ctx.ui.setFocus(this.ctx.editor);
1208
1334
  this.ctx.ui.requestRender();
1209
1335
  }
@@ -1217,6 +1343,7 @@ export class ExtensionUiController {
1217
1343
  dialogOptions?: ExtensionUIDialogOptions,
1218
1344
  editorOptions?: { promptStyle?: boolean },
1219
1345
  ): Promise<string | undefined> {
1346
+ if (this.#isStopped()) return Promise.resolve(undefined);
1220
1347
  const { promise, finish, attachAbort } = this.#createHookDialogState(
1221
1348
  () => this.hideHookEditor(),
1222
1349
  dialogOptions?.signal,
@@ -1251,6 +1378,11 @@ export class ExtensionUiController {
1251
1378
  * Hide the hook editor.
1252
1379
  */
1253
1380
  hideHookEditor(): void {
1381
+ if (this.#isStopped()) {
1382
+ this.ctx.hookEditor?.dispose();
1383
+ this.ctx.hookEditor = undefined;
1384
+ return;
1385
+ }
1254
1386
  this.#restoreComposerEditor();
1255
1387
  this.ctx.hookEditor = undefined;
1256
1388
  this.ctx.ui.setFocus(this.ctx.editor);
@@ -1261,6 +1393,7 @@ export class ExtensionUiController {
1261
1393
  * Show a notification for hooks.
1262
1394
  */
1263
1395
  showHookNotify(message: string, type?: "info" | "warning" | "error"): void {
1396
+ if (this.#isStopped()) return;
1264
1397
  if (type === "error") {
1265
1398
  this.ctx.showError(message);
1266
1399
  } else if (type === "warning") {
@@ -1282,6 +1415,7 @@ export class ExtensionUiController {
1282
1415
  ) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,
1283
1416
  options?: { overlay?: boolean },
1284
1417
  ): Promise<T> {
1418
+ if (this.#isStopped()) return undefined as T;
1285
1419
  const savedText = this.ctx.editor.getText();
1286
1420
  const keybindings = KeybindingsManager.inMemory();
1287
1421
 
@@ -1292,7 +1426,12 @@ export class ExtensionUiController {
1292
1426
  const close = (result: T) => {
1293
1427
  if (closed) return;
1294
1428
  closed = true;
1429
+ this.#activeHookCustomCancel = undefined;
1295
1430
  this.#clearActiveHookCustom();
1431
+ if (this.#isStopped()) {
1432
+ resolve(result);
1433
+ return;
1434
+ }
1296
1435
  if (!options?.overlay) {
1297
1436
  this.#restoreComposerEditor();
1298
1437
  this.ctx.editor.setText(savedText);
@@ -1302,10 +1441,16 @@ export class ExtensionUiController {
1302
1441
  resolve(result);
1303
1442
  };
1304
1443
 
1444
+ this.#activeHookCustomCancel?.();
1305
1445
  this.#clearActiveHookCustom();
1446
+ this.#activeHookCustomCancel = () => close(undefined as T);
1306
1447
  Promise.try(() => factory(this.ctx.ui, theme, keybindings, close)).then(c => {
1307
- if (closed) {
1448
+ if (closed || this.#isStopped()) {
1308
1449
  c.dispose?.();
1450
+ if (!closed) {
1451
+ closed = true;
1452
+ resolve(undefined as T);
1453
+ }
1309
1454
  return;
1310
1455
  }
1311
1456
  component = c;
@@ -1334,6 +1479,7 @@ export class ExtensionUiController {
1334
1479
  * Show an extension error in the UI.
1335
1480
  */
1336
1481
  addExtensionTerminalInputListener(handler: TerminalInputHandler): () => void {
1482
+ if (this.#isStopped()) return () => {};
1337
1483
  const unsubscribe = this.ctx.ui.addInputListener(handler);
1338
1484
  this.#extensionTerminalInputUnsubscribers.add(unsubscribe);
1339
1485
  return () => {
@@ -1362,7 +1508,17 @@ export class ExtensionUiController {
1362
1508
  this.#extensionTerminalInputUnsubscribers.clear();
1363
1509
  }
1364
1510
 
1511
+ dispose(): void {
1512
+ this.#extensionErrorUnsubscribe?.();
1513
+ this.#extensionErrorUnsubscribe = undefined;
1514
+ this.#activeHookCustomCancel?.();
1515
+ this.#activeHookCustomCancel = undefined;
1516
+ this.clearExtensionTerminalInputListeners();
1517
+ this.clearHookWidgets();
1518
+ }
1519
+
1365
1520
  showExtensionError(extensionPath: string, error: string): void {
1521
+ if (this.#isStopped()) return;
1366
1522
  const errorText = new Text(theme.fg("error", `Extension "${extensionPath}" error: ${error}`), 1, 0);
1367
1523
  this.ctx.chatContainer.addChild(errorText);
1368
1524
  this.ctx.ui.requestRender();
@@ -1388,6 +1544,7 @@ export class ExtensionUiController {
1388
1544
  }
1389
1545
 
1390
1546
  #sendExtensionUserMessage: SendUserMessageHandler = (content, options) => {
1547
+ if (this.#isStopped()) return Promise.resolve();
1391
1548
  // Compute queued BEFORE send: prompt() may flip session.isStreaming synchronously.
1392
1549
  const queued = Boolean(options?.deliverAs) || this.ctx.session.isStreaming;
1393
1550
  // Call send first so the busy/queued path finds the session queue populated
@@ -1395,6 +1552,7 @@ export class ExtensionUiController {
1395
1552
  const send = this.ctx.session.sendUserMessage(content, options);
1396
1553
  applyInjectedUserSubmission(this.ctx, { content, queued });
1397
1554
  void send.catch((err: unknown) => {
1555
+ if (this.#isStopped()) return;
1398
1556
  this.ctx.showError(`Extension sendUserMessage failed: ${err instanceof Error ? err.message : String(err)}`);
1399
1557
  });
1400
1558
  return send;
@@ -1418,20 +1576,21 @@ export class ExtensionUiController {
1418
1576
  } {
1419
1577
  const { promise, resolve } = Promise.withResolvers<string | undefined>();
1420
1578
  let settled = false;
1421
- const onAbort = () => {
1422
- hide();
1423
- if (!settled) {
1424
- settled = true;
1425
- resolve(undefined);
1426
- }
1427
- };
1579
+ let unregisterStop: (() => void) | undefined;
1428
1580
  const finish = (value: string | undefined) => {
1429
1581
  if (settled) return;
1430
1582
  settled = true;
1431
1583
  signal?.removeEventListener("abort", onAbort);
1584
+ unregisterStop?.();
1432
1585
  resolve(value);
1433
1586
  };
1587
+ const onAbort = () => {
1588
+ hide();
1589
+ finish(undefined);
1590
+ };
1434
1591
  const attachAbort = () => {
1592
+ unregisterStop = this.ctx.onStop?.(onAbort);
1593
+ if (settled) unregisterStop?.();
1435
1594
  if (!signal) return;
1436
1595
  if (signal.aborted) {
1437
1596
  onAbort();