@gajae-code/coding-agent 0.16.1 → 0.16.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 (146) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/types/cli/args.d.ts +2 -2
  3. package/dist/types/cli/root-flags.d.ts +4 -2
  4. package/dist/types/cli.d.ts +1 -1
  5. package/dist/types/commands/launch.d.ts +1 -1
  6. package/dist/types/config/autorouting-tier-map.d.ts +3 -0
  7. package/dist/types/config/model-registry.d.ts +4 -0
  8. package/dist/types/config/model-resolver.d.ts +9 -0
  9. package/dist/types/config/settings-schema.d.ts +29 -1
  10. package/dist/types/discovery/claude-plugins.d.ts +3 -1
  11. package/dist/types/discovery/helpers.d.ts +2 -0
  12. package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
  13. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
  14. package/dist/types/extensibility/runtime-skill-discovery.d.ts +1 -1
  15. package/dist/types/extensibility/shared-events.d.ts +1 -1
  16. package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
  17. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  18. package/dist/types/modes/components/settings-selector.d.ts +6 -1
  19. package/dist/types/modes/components/status-line/command.d.ts +35 -0
  20. package/dist/types/modes/components/status-line/index.d.ts +1 -0
  21. package/dist/types/modes/components/status-line/types.d.ts +5 -0
  22. package/dist/types/modes/components/tool-status-header.d.ts +14 -0
  23. package/dist/types/modes/controllers/event-controller.d.ts +6 -0
  24. package/dist/types/modes/interactive-mode.d.ts +1 -0
  25. package/dist/types/modes/types.d.ts +1 -0
  26. package/dist/types/runtime-mcp/client.d.ts +9 -1
  27. package/dist/types/runtime-mcp/manager.d.ts +7 -1
  28. package/dist/types/runtime-mcp/pool.d.ts +22 -3
  29. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  30. package/dist/types/runtime-mcp/types.d.ts +22 -0
  31. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
  32. package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
  33. package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
  34. package/dist/types/sdk/cli/rows.d.ts +7 -0
  35. package/dist/types/sdk/host/events.d.ts +1 -0
  36. package/dist/types/sdk/host/host.d.ts +2 -0
  37. package/dist/types/sdk/host/session-runtime.d.ts +23 -0
  38. package/dist/types/sdk/session-api-key.d.ts +26 -0
  39. package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
  40. package/dist/types/session/agent-session.d.ts +9 -0
  41. package/dist/types/setup/paseo/announce.d.ts +111 -0
  42. package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
  43. package/dist/types/system-prompt.d.ts +2 -0
  44. package/dist/types/task/executor.d.ts +2 -1
  45. package/dist/types/tools/fetch.d.ts +7 -0
  46. package/dist/types/tools/index.d.ts +8 -2
  47. package/dist/types/tools/path-utils.d.ts +2 -0
  48. package/dist/types/tools/search.d.ts +6 -0
  49. package/dist/types/utils/external-editor.d.ts +5 -0
  50. package/package.json +7 -7
  51. package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
  52. package/scripts/generate-sdk-operation-inventory.ts +9 -0
  53. package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
  54. package/src/cli/args.ts +20 -9
  55. package/src/cli/root-flags.ts +5 -3
  56. package/src/config/autorouting-tier-map.ts +1 -0
  57. package/src/config/file-lock.ts +181 -17
  58. package/src/config/model-preset-registry.ts +5 -3
  59. package/src/config/model-profiles.ts +40 -0
  60. package/src/config/model-registry.ts +57 -11
  61. package/src/config/model-resolver.ts +48 -0
  62. package/src/config/settings-schema.ts +33 -1
  63. package/src/config/settings.ts +1 -0
  64. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
  65. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
  66. package/src/discovery/builtin.ts +13 -4
  67. package/src/discovery/claude-plugins.ts +12 -4
  68. package/src/discovery/helpers.ts +10 -2
  69. package/src/extensibility/gjc-plugins/compiler.ts +208 -58
  70. package/src/extensibility/gjc-plugins/installer.ts +13 -12
  71. package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
  72. package/src/extensibility/gjc-plugins/paths.ts +13 -0
  73. package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
  74. package/src/extensibility/runtime-skill-discovery.ts +77 -151
  75. package/src/extensibility/shared-events.ts +1 -1
  76. package/src/extensibility/skills.ts +44 -16
  77. package/src/gjc-runtime/repository-binding.ts +1 -1
  78. package/src/gjc-runtime/session-state-lock.ts +94 -6
  79. package/src/internal-urls/docs-index.generated.ts +10 -10
  80. package/src/main.ts +72 -34
  81. package/src/modes/DESIGN.md +54 -0
  82. package/src/modes/acp/acp-agent.ts +29 -14
  83. package/src/modes/components/assistant-message.ts +22 -9
  84. package/src/modes/components/model-selector.ts +166 -33
  85. package/src/modes/components/settings-selector.ts +1075 -235
  86. package/src/modes/components/status-line/command.ts +190 -0
  87. package/src/modes/components/status-line/index.ts +1 -0
  88. package/src/modes/components/status-line/segments.ts +32 -5
  89. package/src/modes/components/status-line/types.ts +5 -0
  90. package/src/modes/components/tool-status-header.ts +393 -21
  91. package/src/modes/controllers/command-controller.ts +2 -0
  92. package/src/modes/controllers/event-controller.ts +148 -5
  93. package/src/modes/controllers/selector-controller.ts +21 -0
  94. package/src/modes/interactive-mode.ts +47 -1
  95. package/src/modes/types.ts +1 -0
  96. package/src/modes/utils/ui-helpers.ts +1 -0
  97. package/src/prompts/system/system-prompt.md +8 -0
  98. package/src/prompts/tools/bash.md +1 -1
  99. package/src/prompts/tools/job.md +1 -1
  100. package/src/prompts/tools/search.md +2 -0
  101. package/src/runtime-mcp/client.ts +60 -17
  102. package/src/runtime-mcp/manager.ts +171 -14
  103. package/src/runtime-mcp/pool.test.ts +209 -0
  104. package/src/runtime-mcp/pool.ts +224 -23
  105. package/src/runtime-mcp/transports/stdio.ts +158 -24
  106. package/src/runtime-mcp/types.ts +24 -0
  107. package/src/sdk/acp/adapter.ts +1 -1
  108. package/src/sdk/broker/broker.ts +5 -4
  109. package/src/sdk/bus/chat-daemon-control.ts +1 -1
  110. package/src/sdk/bus/chat-daemon-runtime.ts +44 -1
  111. package/src/sdk/bus/slack-daemon.ts +92 -2
  112. package/src/sdk/bus/slack-live-provider.ts +12 -0
  113. package/src/sdk/bus/slack-provider.ts +5 -0
  114. package/src/sdk/cli/rows.ts +47 -2
  115. package/src/sdk/cli/session-cli.ts +47 -23
  116. package/src/sdk/host/events.ts +25 -2
  117. package/src/sdk/host/host.ts +4 -1
  118. package/src/sdk/host/session-runtime.test.ts +36 -25
  119. package/src/sdk/host/session-runtime.ts +279 -43
  120. package/src/sdk/protocol/operation-inventory.generated.json +66 -0
  121. package/src/sdk/router/session-router.ts +60 -37
  122. package/src/sdk/session-api-key.ts +94 -0
  123. package/src/sdk/session.ts +73 -30
  124. package/src/sdk/transport/serve-cli.ts +3 -3
  125. package/src/session/agent-session.ts +123 -38
  126. package/src/session/contribution-prep.ts +214 -21
  127. package/src/setup/paseo/announce.ts +450 -0
  128. package/src/setup/paseo/setup-deps.ts +15 -3
  129. package/src/slash-commands/builtin-registry.ts +1 -0
  130. package/src/system-prompt.ts +4 -0
  131. package/src/task/executor.ts +2 -1
  132. package/src/task/index.ts +17 -4
  133. package/src/tools/bash.ts +50 -21
  134. package/src/tools/cron.ts +1 -1
  135. package/src/tools/fetch.ts +29 -1
  136. package/src/tools/index.ts +8 -2
  137. package/src/tools/job.ts +8 -2
  138. package/src/tools/monitor.ts +8 -2
  139. package/src/tools/path-utils.ts +7 -0
  140. package/src/tools/search.ts +116 -58
  141. package/src/tools/skill.ts +22 -2
  142. package/src/tools/subagent.ts +3 -1
  143. package/src/tools/tool-catalog.generated.ts +9 -2
  144. package/src/utils/external-editor.ts +16 -2
  145. package/src/web/insane/bridge.ts +113 -23
  146. package/src/web/search/providers/perplexity.ts +102 -41
package/src/main.ts CHANGED
@@ -31,7 +31,15 @@ import { selectSession } from "./cli/session-picker";
31
31
  import { findConfigFile } from "./config";
32
32
  import { activateModelProfile, ModelProfileCredentialError } from "./config/model-profile-activation";
33
33
  import { ModelRegistry, ModelsConfigFile } from "./config/model-registry";
34
- import { resolveCliModel, resolveModelRoleValue, resolveModelScope, type ScopedModel } from "./config/model-resolver";
34
+ import {
35
+ parseModelString,
36
+ refreshMissingQualifiedModelProviders,
37
+ resolveCliModel,
38
+ resolveModelRoleValue,
39
+ resolveModelScope,
40
+ resolveStartupModelRefreshSelectors,
41
+ type ScopedModel,
42
+ } from "./config/model-resolver";
35
43
  import { selectorHead } from "./config/model-selector-value";
36
44
  import { getDefault, type SettingPath, Settings, settings } from "./config/settings";
37
45
  import { resolveMachineLocalUpdateChannel, type UpdateChannel } from "./config/update-channel";
@@ -49,7 +57,7 @@ import type { PrintModeOptions } from "./modes/print-mode";
49
57
  import { initTheme, stopThemeWatcher } from "./modes/theme/theme";
50
58
  import type { SubmittedUserInput } from "./modes/types";
51
59
  import { applyCliRuntimeApiKeyOverride } from "./runtime-api-key";
52
- import { parseCliCredentialSelector } from "./runtime-credential-selector";
60
+ import { type CliCredentialSelector, parseCliCredentialSelector } from "./runtime-credential-selector";
53
61
  import type { MCPManager } from "./runtime-mcp";
54
62
  import {
55
63
  type CreateAgentSessionOptions,
@@ -1645,6 +1653,66 @@ export async function runRootCommand(
1645
1653
  )
1646
1654
  : undefined;
1647
1655
 
1656
+ if (parsedArgs.apiKey && parsedArgs.credential) {
1657
+ process.stderr.write(`${chalk.red("--api-key and --credential cannot be used together")}\n`);
1658
+ process.exit(1);
1659
+ }
1660
+ if (parsedArgs.credential && parsedArgs.preferCredential) {
1661
+ process.stderr.write(`${chalk.red("--credential and --prefer-credential cannot be used together")}\n`);
1662
+ process.exit(1);
1663
+ }
1664
+ if (parsedArgs.apiKey && parsedArgs.preferCredential) {
1665
+ process.stderr.write(`${chalk.red("--api-key and --prefer-credential cannot be used together")}\n`);
1666
+ process.exit(1);
1667
+ }
1668
+
1669
+ let credentialSelector: CliCredentialSelector | undefined;
1670
+ let preferredCredentialSelector: CliCredentialSelector | undefined;
1671
+ try {
1672
+ credentialSelector = parsedArgs.credential ? parseCliCredentialSelector(parsedArgs.credential) : undefined;
1673
+ preferredCredentialSelector = parsedArgs.preferCredential
1674
+ ? parseCliCredentialSelector(parsedArgs.preferCredential)
1675
+ : undefined;
1676
+ if (preferredCredentialSelector) {
1677
+ const provider =
1678
+ preferredCredentialSelector.provider ??
1679
+ authStorage.resolveRuntimePreferredCredentialSelectorProvider(preferredCredentialSelector.selector);
1680
+ authStorage.setRuntimePreferredCredentialSelector(provider, preferredCredentialSelector.selector);
1681
+ }
1682
+ } catch (error) {
1683
+ const message = error instanceof Error ? error.message : String(error);
1684
+ process.stderr.write(`${chalk.red(message)}\n`);
1685
+ process.exit(1);
1686
+ }
1687
+
1688
+ const hasRootStartupProfile = Boolean(settingsInstance.get("modelProfile.default") || parsedArgs.mpreset);
1689
+ const startupModelSelectors = resolveStartupModelRefreshSelectors(
1690
+ {
1691
+ model: parsedArgs.model,
1692
+ provider: parsedArgs.provider,
1693
+ credential: parsedArgs.credential,
1694
+ continue: parsedArgs.continue,
1695
+ resume: parsedArgs.resume,
1696
+ hasStartupProfile: hasRootStartupProfile,
1697
+ },
1698
+ settingsInstance,
1699
+ );
1700
+ const explicitStartupModelSelector = parsedArgs.model
1701
+ ? typeof startupModelSelectors === "string"
1702
+ ? startupModelSelectors
1703
+ : parsedArgs.model
1704
+ : undefined;
1705
+ if (parsedArgs.apiKey && explicitStartupModelSelector) {
1706
+ const parsedModel = parseModelString(explicitStartupModelSelector);
1707
+ if (parsedModel) authStorage.setRuntimeApiKey(parsedModel.provider, parsedArgs.apiKey);
1708
+ }
1709
+ await logger.time(
1710
+ "refreshStartupModelProviders",
1711
+ refreshMissingQualifiedModelProviders,
1712
+ startupModelSelectors,
1713
+ modelRegistry,
1714
+ );
1715
+
1648
1716
  let scopedModels: ScopedModel[] = [];
1649
1717
  const modelPatterns = parsedArgs.models ?? settingsInstance.get("enabledModels");
1650
1718
  const modelMatchPreferences = {
@@ -1779,7 +1847,6 @@ export async function runRootCommand(
1779
1847
  if (isInteractive && sessionOptions.mcpConfigPath) {
1780
1848
  sessionOptions.deferMcpConfigStartup = true;
1781
1849
  }
1782
- const hasRootStartupProfile = Boolean(settingsInstance.get("modelProfile.default") || parsedArgs.mpreset);
1783
1850
  // ACP is not carved out: `gjc acp` is broker-backed and never builds a local
1784
1851
  // session here, and the broker-launched lifecycle child defers memory startup
1785
1852
  // unconditionally (createLifecycleAgentSession) so readiness never waits on
@@ -1790,37 +1857,8 @@ export async function runRootCommand(
1790
1857
  const acpStartupOptions = mode === "acp" ? resolveAcpStartupOptions(parsedArgs, sessionOptions) : undefined;
1791
1858
 
1792
1859
  // Handle CLI credential selection and --api-key as runtime overrides (not persisted).
1793
- if (parsedArgs.apiKey && parsedArgs.credential) {
1794
- process.stderr.write(`${chalk.red("--api-key and --credential cannot be used together")}\n`);
1795
- process.exit(1);
1796
- }
1797
- if (parsedArgs.credential && parsedArgs.preferCredential) {
1798
- process.stderr.write(`${chalk.red("--credential and --prefer-credential cannot be used together")}\n`);
1799
- process.exit(1);
1800
- }
1801
- if (parsedArgs.apiKey && parsedArgs.preferCredential) {
1802
- process.stderr.write(`${chalk.red("--api-key and --prefer-credential cannot be used together")}\n`);
1803
- process.exit(1);
1804
- }
1805
-
1806
- if (parsedArgs.credential) {
1807
- try {
1808
- sessionOptions.credentialSelector = parseCliCredentialSelector(parsedArgs.credential);
1809
- } catch (error) {
1810
- const message = error instanceof Error ? error.message : String(error);
1811
- process.stderr.write(`${chalk.red(message)}\n`);
1812
- process.exit(1);
1813
- }
1814
- }
1815
- if (parsedArgs.preferCredential) {
1816
- try {
1817
- sessionOptions.preferredCredentialSelector = parseCliCredentialSelector(parsedArgs.preferCredential);
1818
- } catch (error) {
1819
- const message = error instanceof Error ? error.message : String(error);
1820
- process.stderr.write(`${chalk.red(message)}\n`);
1821
- process.exit(1);
1822
- }
1823
- }
1860
+ if (credentialSelector) sessionOptions.credentialSelector = credentialSelector;
1861
+ if (preferredCredentialSelector) sessionOptions.preferredCredentialSelector = preferredCredentialSelector;
1824
1862
  if (parsedArgs.apiKey) {
1825
1863
  if (!sessionOptions.model && !sessionOptions.modelPattern) {
1826
1864
  process.stderr.write(
@@ -347,6 +347,60 @@ CJK/Latin todo, IRC, status, and BTW text break at semantic phrase boundaries,
347
347
  not inside a phase/action/status label, short identifier, or grapheme cluster.
348
348
  Those defects, width overflow, overlap, hidden focus/cursor, and lost anchors
349
349
  are blocking visual-QA failures.
350
+
351
+ ### Status Line Custom Editor simulation
352
+
353
+ The Appearance tab's Status Line Custom Editor is a spatial editor, not a
354
+ generic setting row list. It renders a simulated statusbar with left/right
355
+ segment areas, a hidden-segment palette, visible Separator and segment-option
356
+ controls, and Confirm/Exit actions. It must not render legacy `Segment`,
357
+ `Move left`, or `Move right` rows.
358
+
359
+ The editor owns transient state locally: focused statusbar slot, focused palette
360
+ item, focused root control, open visible-choice panel, picked segment, floating
361
+ ghost, empty origin slot, and overflow warning. Durable settings remain only the
362
+ existing `statusLine.leftSegments`, `statusLine.rightSegments`,
363
+ `statusLine.separator`, and `statusLine.segmentOptions` keys; Confirm persists
364
+ through the existing settings guard, while Exit or root-state Escape restores
365
+ the previous preview and closes.
366
+
367
+ Focus variants are `statusbar`, `palette`, `separator-control`,
368
+ `option-control(path)`, `choice(target, focusedValue, returnFocus)`, `confirm`,
369
+ and `exit`. Left/Right is owned by the editor while active and never switches
370
+ settings tabs. Enter selects/deselects, picks/drops, applies a focused choice,
371
+ or activates Confirm/Exit according to focus. Up/Down traverses palette,
372
+ visible root controls, actions, and choice candidates. Escape inside `choice`
373
+ returns to the opener without mutation; Escape everywhere else cancels and
374
+ restores. Delete only hides a focused visible statusbar segment.
375
+
376
+ Separator and every segment option are visible-choice controls, never cycle-only
377
+ rows. Descriptor data owns labels, allowed values, visible current-value
378
+ formatting, and typed draft application. Separator choices are Powerline, Thin
379
+ chevron, Slash, Pipe, Block, None, and ASCII. Boolean options show `true` and
380
+ `false`; enum options show their union values; `path.maxLength` shows `16`,
381
+ `24`, `32`, `40`, `50`, `60`, and `80`.
382
+
383
+ At constrained width the simulated statusbar renders as two rows and emits an
384
+ explicit overflow/wrap warning. ANSI-aware cell measurement remains mandatory;
385
+ ASCII/no-color output must retain textual focus, ghost/origin, selected choice,
386
+ and warning affordances.
387
+
388
+ Deterministic evidence belongs to
389
+ `test/fixtures/tui/status-line-custom-editor-showcase.ts`, captured by
390
+ `scripts/capture-status-line-custom-editor-showcase.ts --out
391
+ .gjc/qa/status-line-custom-editor-<run>` and verified by
392
+ `scripts/verify-status-line-custom-editor-showcase.ts --root
393
+ .gjc/qa/status-line-custom-editor-<run> --require-independent-review`. The
394
+ canonical states are `root-statusbar`, `picked-origin-slot`,
395
+ `palette-exact-insert`, `separator-control`, `separator-choice-focused`,
396
+ `separator-choice-applied`, `option-boolean-choice-focused`,
397
+ `option-enum-choice-focused`, `option-numeric-choice-focused`,
398
+ `option-choice-applied`, `exit-restored`, `confirm-persisted`,
399
+ `overflow-two-row-warning`, and `narrow-cjk`. Each state records
400
+ `terminal.txt`, `terminal-ansi.txt`, `terminal.html`, and `metadata.json`, plus
401
+ the root manifest. Missing state witnesses, ANSI/plain mismatch, invalid
402
+ metadata, and absent independent review are blocking visual-QA failures.
403
+
350
404
  ### Sticky viewport deterministic visual QA
351
405
 
352
406
  `test/fixtures/tui/sticky-viewport-showcase.ts` is a fixed-clock, no-network,
@@ -1699,9 +1699,13 @@ export class AcpAgent implements Agent {
1699
1699
  try {
1700
1700
  await record.adapter.ensureProviders();
1701
1701
  } catch (error) {
1702
- if (waiter.settled || record.cancelRequested || record.activePrompt !== waiter) {
1702
+ if (waiter.settled || record.activePrompt !== waiter) {
1703
1703
  this.#retiredPromptAcknowledgements.delete(params.sessionId);
1704
- if (!waiter.settled) await this.#settleCancelledPrompt(params.sessionId, record, waiter);
1704
+ return await response;
1705
+ }
1706
+ if (record.cancelRequested && waiter.cancelAcknowledged) {
1707
+ this.#retiredPromptAcknowledgements.delete(params.sessionId);
1708
+ await this.#settleCancelledPrompt(params.sessionId, record, waiter);
1705
1709
  return await response;
1706
1710
  }
1707
1711
  if (record.activePrompt === waiter) {
@@ -1712,9 +1716,13 @@ export class AcpAgent implements Agent {
1712
1716
  }
1713
1717
  throw error;
1714
1718
  }
1715
- if (waiter.settled || record.cancelRequested || record.activePrompt !== waiter) {
1719
+ if (waiter.settled || record.activePrompt !== waiter) {
1716
1720
  this.#retiredPromptAcknowledgements.delete(params.sessionId);
1717
- if (!waiter.settled) await this.#settleCancelledPrompt(params.sessionId, record, waiter);
1721
+ return await response;
1722
+ }
1723
+ if (record.cancelRequested && waiter.cancelAcknowledged) {
1724
+ this.#retiredPromptAcknowledgements.delete(params.sessionId);
1725
+ await this.#settleCancelledPrompt(params.sessionId, record, waiter);
1718
1726
  return await response;
1719
1727
  }
1720
1728
 
@@ -1739,8 +1747,11 @@ export class AcpAgent implements Agent {
1739
1747
  );
1740
1748
  }
1741
1749
  waiter.dispatched = true;
1742
- if (waiter.settled || record.cancelRequested || record.activePrompt !== waiter) {
1743
- if (!waiter.settled) await this.#settleCancelledPrompt(params.sessionId, record, waiter);
1750
+ if (waiter.settled || record.activePrompt !== waiter) {
1751
+ return await response;
1752
+ }
1753
+ if (record.cancelRequested && waiter.cancelAcknowledged) {
1754
+ await this.#settleCancelledPrompt(params.sessionId, record, waiter);
1744
1755
  return await response;
1745
1756
  }
1746
1757
 
@@ -1922,7 +1933,7 @@ export class AcpAgent implements Agent {
1922
1933
  // return the semantically meaningful `cancelled` stop reason, so that Clients
1923
1934
  // can reliably confirm the cancellation." Surfacing the transport's `busy`
1924
1935
  // rejection instead would show the user a spurious error for their own cancel.
1925
- if (record.cancelRequested) {
1936
+ if (record.cancelRequested && waiter.cancelAcknowledged) {
1926
1937
  record.cancelRequested = false;
1927
1938
  // The client's turn is settled by the return; the advisory idle publication
1928
1939
  // must not gate it and must still be attempted so the running phase is
@@ -2006,11 +2017,6 @@ export class AcpAgent implements Agent {
2006
2017
  }
2007
2018
  waiter?.cancelAttemptResolve?.(true);
2008
2019
  } catch (error) {
2009
- if (waiter && !waiter.dispatched) {
2010
- await this.#settleCancelledPrompt(params.sessionId, record, waiter);
2011
- waiter.cancelAttemptResolve?.(true);
2012
- return;
2013
- }
2014
2020
  if (!waiter) record.cancelRequested = false;
2015
2021
  // Only the LAST in-flight attempt resolves the shared promise false;
2016
2022
  // an earlier attempt may still acknowledge (review thread P2). After
@@ -2058,7 +2064,8 @@ export class AcpAgent implements Agent {
2058
2064
 
2059
2065
  async #settleCancelledPrompt(id: string, record: SessionRecord, waiter: PromptWaiter): Promise<void> {
2060
2066
  // The authoritative terminal wins whenever it arrives in time; this only runs
2061
- // when nothing settled the prompt the client already asked to cancel.
2067
+ // when nothing settled the prompt after the SDK acknowledged the cancellation.
2068
+ if (!waiter.cancelAcknowledged) return;
2062
2069
  if (this.#sessions.get(id) !== record || record.activePrompt !== waiter || waiter.settled) return;
2063
2070
  if (waiter.terminalReserved) return;
2064
2071
  record.activePrompt = undefined;
@@ -2896,6 +2903,14 @@ export class AcpAgent implements Agent {
2896
2903
  if (reconnected) {
2897
2904
  const waiter = record.activePrompt;
2898
2905
  if (waiter && !waiter.settled && !waiter.terminal) {
2906
+ // A successful abort acknowledgement is authoritative for this prompt even
2907
+ // when the SDK transport changes identity during the cancellation grace.
2908
+ // Treating the reconnect as owner loss would replace the acknowledged
2909
+ // cancellation with connection_closed and leave cancellation nondeterministic.
2910
+ if (record.cancelRequested && waiter.cancelAcknowledged) {
2911
+ await this.#settleCancelledPrompt(id, record, waiter);
2912
+ return;
2913
+ }
2899
2914
  record.activePrompt = undefined;
2900
2915
  record.busy = record.backgroundBusy;
2901
2916
  clearPromptWatchdog(waiter);
@@ -3113,7 +3128,7 @@ export class AcpAgent implements Agent {
3113
3128
  id: string,
3114
3129
  notification: SessionNotification,
3115
3130
  adapter: AcpSdkAdapter,
3116
- publicationGeneration: number,
3131
+ _publicationGeneration: number,
3117
3132
  ): void {
3118
3133
  const prior = this.#failureDiagnosticTails.get(id) ?? Promise.resolve();
3119
3134
  const task = prior.then(async () => {
@@ -106,16 +106,24 @@ export class AssistantMessageComponent extends Container {
106
106
  #toolImagesRevision = 0;
107
107
  #convertedToolImagesRevision = 0;
108
108
  #disposed = false;
109
+ // Cached Markdown children can outlive their attachment to this assistant.
110
+ // Session/live-parent validity belongs to the owner, not the paint lifetime.
111
+ readonly #requestRepaint?: () => void;
109
112
  constructor(
110
113
  message?: AssistantMessage,
111
114
  private hideThinkingBlock = false,
112
- private readonly onImageUpdate?: () => void,
115
+ onImageUpdate?: () => void,
113
116
  viewportAnchorId?: string,
114
117
  private readonly onVisibleMutation?: () => void,
115
118
  private readonly hintSession?: ProviderSafetyStopHintSession,
116
119
  ) {
117
120
  super();
118
121
  this.#viewportAnchorId = viewportAnchorId;
122
+ if (onImageUpdate) {
123
+ this.#requestRepaint = () => {
124
+ if (!this.#disposed) onImageUpdate();
125
+ };
126
+ }
119
127
 
120
128
  this.#contentContainer = new Container();
121
129
  this.addChild(this.#contentContainer);
@@ -172,6 +180,7 @@ export class AssistantMessageComponent extends Container {
172
180
  }
173
181
 
174
182
  #notifyVisibleMutation(): void {
183
+ if (this.#disposed) return;
175
184
  const after = this.#visibleContentSignature();
176
185
  if (after === this.#visibleSignature) return;
177
186
  this.#visibleSignature = after;
@@ -186,6 +195,7 @@ export class AssistantMessageComponent extends Container {
186
195
  }
187
196
 
188
197
  override invalidate(): void {
198
+ if (this.#disposed) return;
189
199
  super.invalidate();
190
200
  if (this.#lastMessage) {
191
201
  this.updateContent(this.#lastMessage, { streaming: this.#lastStreaming });
@@ -193,13 +203,14 @@ export class AssistantMessageComponent extends Container {
193
203
  }
194
204
 
195
205
  setHideThinkingBlock(hide: boolean): void {
206
+ if (this.#disposed) return;
196
207
  if (this.hideThinkingBlock === hide) return;
197
208
  this.hideThinkingBlock = hide;
198
209
  this.#notifyVisibleMutation();
199
210
  }
200
211
 
201
212
  setToolResultImages(toolCallId: string, images: ImageContent[]): void {
202
- if (!toolCallId) return;
213
+ if (this.#disposed || !toolCallId) return;
203
214
  const validImages = images.filter(img => img.type === "image" && img.data && img.mimeType);
204
215
  const previousImages = this.#toolImagesByCallId.get(toolCallId);
205
216
  if (
@@ -268,7 +279,7 @@ export class AssistantMessageComponent extends Container {
268
279
  this.#convertedKittyImages.set(key, { type: "image", data, mimeType: "image/png", source });
269
280
  this.#convertedToolImagesRevision += 1;
270
281
  if (this.#lastMessage) this.updateContent(this.#lastMessage, { streaming: this.#lastStreaming });
271
- this.onImageUpdate?.();
282
+ this.#requestRepaint?.();
272
283
  })
273
284
  .catch(() => {
274
285
  if (
@@ -283,6 +294,7 @@ export class AssistantMessageComponent extends Container {
283
294
  }
284
295
 
285
296
  setUsageInfo(usage: Usage): void {
297
+ if (this.#disposed) return;
286
298
  this.#usageInfo = usage;
287
299
  if (this.#lastMessage) {
288
300
  this.updateContent(this.#lastMessage, { streaming: this.#lastStreaming });
@@ -311,7 +323,7 @@ export class AssistantMessageComponent extends Container {
311
323
  const cached = this.#contentBlocksCache.get(content);
312
324
  if (cached?.source === content.text) {
313
325
  if (cached.component instanceof Markdown) {
314
- cached.component.setOnStaleThrottle(this.onImageUpdate);
326
+ cached.component.setOnStaleThrottle(this.#requestRepaint);
315
327
  cached.component.setStreaming(streaming);
316
328
  }
317
329
  return cached.component;
@@ -322,14 +334,14 @@ export class AssistantMessageComponent extends Container {
322
334
  // instead of constructing a new one each chunk; combined with the markdown
323
335
  // per-code-block highlight cache, appends no longer re-highlight the whole prefix.
324
336
  if (!deepInterview && cached && cached.component instanceof Markdown) {
325
- cached.component.setOnStaleThrottle(this.onImageUpdate);
337
+ cached.component.setOnStaleThrottle(this.#requestRepaint);
326
338
  cached.component.setText(trimmed, { streaming });
327
339
  cached.source = content.text;
328
340
  return cached.component;
329
341
  }
330
342
  const component = deepInterview ?? new Markdown(trimmed, 1, 0, getMarkdownTheme());
331
343
  if (component instanceof Markdown) {
332
- component.setOnStaleThrottle(this.onImageUpdate);
344
+ component.setOnStaleThrottle(this.#requestRepaint);
333
345
  component.setStreaming(streaming);
334
346
  }
335
347
  this.#contentBlocksCache.set(content, { source: content.text, component });
@@ -341,14 +353,14 @@ export class AssistantMessageComponent extends Container {
341
353
  const cached = this.#contentBlocksCache.get(content);
342
354
  if (cached?.source === content.thinking) {
343
355
  if (cached.component instanceof Markdown) {
344
- cached.component.setOnStaleThrottle(this.onImageUpdate);
356
+ cached.component.setOnStaleThrottle(this.#requestRepaint);
345
357
  cached.component.setStreaming(streaming);
346
358
  }
347
359
  return cached.component as Markdown;
348
360
  }
349
361
  const trimmed = elideRunawayThinkingRepetition(content.thinking.trim());
350
362
  if (cached?.component instanceof Markdown) {
351
- cached.component.setOnStaleThrottle(this.onImageUpdate);
363
+ cached.component.setOnStaleThrottle(this.#requestRepaint);
352
364
  cached.component.setText(trimmed, { streaming });
353
365
  cached.source = content.thinking;
354
366
  return cached.component;
@@ -357,7 +369,7 @@ export class AssistantMessageComponent extends Container {
357
369
  color: (text: string) => theme.fg("thinkingText", text),
358
370
  italic: true,
359
371
  });
360
- component.setOnStaleThrottle(this.onImageUpdate);
372
+ component.setOnStaleThrottle(this.#requestRepaint);
361
373
  component.setStreaming(streaming);
362
374
  this.#contentBlocksCache.set(content, { source: content.thinking, component });
363
375
  this.#reusableChildren.add(component);
@@ -437,6 +449,7 @@ export class AssistantMessageComponent extends Container {
437
449
  }
438
450
 
439
451
  updateContent(message: AssistantMessage, options?: AssistantMessageUpdateOptions): void {
452
+ if (this.#disposed) return;
440
453
  this.#lastMessage = message;
441
454
  this.#lastStreaming = options?.streaming ?? false;
442
455