@gajae-code/coding-agent 0.16.3 → 0.16.6

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 (277) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/dist/types/capability/types.d.ts +4 -0
  3. package/dist/types/cli/args.d.ts +2 -2
  4. package/dist/types/cli/customize-doctor.d.ts +22 -0
  5. package/dist/types/cli/root-flags.d.ts +4 -2
  6. package/dist/types/cli.d.ts +1 -1
  7. package/dist/types/commands/launch.d.ts +1 -1
  8. package/dist/types/config/autorouting-tier-map.d.ts +3 -0
  9. package/dist/types/config/file-lock.d.ts +31 -3
  10. package/dist/types/config/model-registry.d.ts +4 -0
  11. package/dist/types/config/model-resolver.d.ts +9 -0
  12. package/dist/types/config/settings-schema.d.ts +29 -1
  13. package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
  14. package/dist/types/coordinator-mcp/server.d.ts +7 -0
  15. package/dist/types/discovery/claude-plugins.d.ts +3 -1
  16. package/dist/types/discovery/helpers.d.ts +35 -0
  17. package/dist/types/exec/bash-executor.d.ts +17 -1
  18. package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
  19. package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
  20. package/dist/types/exec/bash-shell-signals.d.ts +2 -0
  21. package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
  22. package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
  23. package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
  24. package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
  25. package/dist/types/exec/bash-shell-worker.d.ts +1 -0
  26. package/dist/types/exec/isolated-shell.d.ts +19 -0
  27. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
  28. package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
  29. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  30. package/dist/types/extensibility/extensions/runner.d.ts +46 -6
  31. package/dist/types/extensibility/extensions/types.d.ts +16 -0
  32. package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
  33. package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
  34. package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
  35. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
  36. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  37. package/dist/types/extensibility/hooks/types.d.ts +4 -0
  38. package/dist/types/extensibility/runtime-skill-discovery.d.ts +5 -2
  39. package/dist/types/extensibility/shared-events.d.ts +1 -1
  40. package/dist/types/extensibility/skill-management.d.ts +9 -4
  41. package/dist/types/extensibility/skills.d.ts +2 -0
  42. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
  43. package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
  44. package/dist/types/gjc-runtime/session-state-lock.d.ts +14 -0
  45. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
  46. package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
  47. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  48. package/dist/types/modes/components/bash-execution.d.ts +1 -0
  49. package/dist/types/modes/components/execution-shared.d.ts +1 -0
  50. package/dist/types/modes/components/settings-selector.d.ts +6 -1
  51. package/dist/types/modes/components/status-line/command.d.ts +35 -0
  52. package/dist/types/modes/components/status-line/index.d.ts +1 -0
  53. package/dist/types/modes/components/status-line/types.d.ts +5 -0
  54. package/dist/types/modes/components/tool-status-header.d.ts +14 -0
  55. package/dist/types/modes/controllers/event-controller.d.ts +6 -0
  56. package/dist/types/modes/interactive-mode.d.ts +3 -0
  57. package/dist/types/modes/types.d.ts +7 -0
  58. package/dist/types/runtime-mcp/client.d.ts +9 -1
  59. package/dist/types/runtime-mcp/manager.d.ts +7 -1
  60. package/dist/types/runtime-mcp/pool.d.ts +22 -3
  61. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  62. package/dist/types/runtime-mcp/types.d.ts +22 -0
  63. package/dist/types/sdk/broker/ensure.d.ts +32 -0
  64. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
  65. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  66. package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
  67. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
  68. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
  69. package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
  70. package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
  71. package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
  72. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  73. package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
  74. package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
  75. package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
  76. package/dist/types/sdk/client/client.d.ts +6 -0
  77. package/dist/types/sdk/host/host.d.ts +3 -1
  78. package/dist/types/sdk/host/session-runtime.d.ts +26 -1
  79. package/dist/types/sdk/lifecycle/service.d.ts +1 -0
  80. package/dist/types/sdk/router/session-router.d.ts +10 -2
  81. package/dist/types/sdk/session-api-key.d.ts +26 -0
  82. package/dist/types/sdk/session.d.ts +5 -1
  83. package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
  84. package/dist/types/session/agent-session.d.ts +11 -0
  85. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  86. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  87. package/dist/types/session/messages.d.ts +1 -0
  88. package/dist/types/session/session-manager.d.ts +3 -1
  89. package/dist/types/setup/paseo/announce.d.ts +112 -0
  90. package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
  91. package/dist/types/skill-state/canonical-skills.d.ts +4 -0
  92. package/dist/types/system-prompt.d.ts +10 -0
  93. package/dist/types/task/executor.d.ts +5 -1
  94. package/dist/types/tools/bash.d.ts +1 -0
  95. package/dist/types/tools/fetch.d.ts +7 -0
  96. package/dist/types/tools/github-cache.d.ts +2 -1
  97. package/dist/types/tools/index.d.ts +10 -0
  98. package/dist/types/tools/path-utils.d.ts +2 -0
  99. package/dist/types/tools/search.d.ts +6 -0
  100. package/package.json +9 -7
  101. package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
  102. package/scripts/generate-sdk-operation-inventory.ts +11 -0
  103. package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
  104. package/src/capability/index.ts +14 -2
  105. package/src/capability/types.ts +4 -0
  106. package/src/cli/args.ts +20 -9
  107. package/src/cli/customize-doctor.ts +155 -12
  108. package/src/cli/root-flags.ts +5 -3
  109. package/src/cli.ts +22 -0
  110. package/src/commands/sdk.ts +74 -31
  111. package/src/config/autorouting-tier-map.ts +1 -0
  112. package/src/config/file-lock-gc.ts +40 -2
  113. package/src/config/file-lock.ts +379 -31
  114. package/src/config/model-preset-registry.ts +120 -19
  115. package/src/config/model-profiles.ts +40 -0
  116. package/src/config/model-registry.ts +73 -23
  117. package/src/config/model-resolver.ts +48 -0
  118. package/src/config/settings-schema.ts +33 -1
  119. package/src/config/settings.ts +1 -0
  120. package/src/coordinator-mcp/question-state.ts +33 -2
  121. package/src/coordinator-mcp/server.ts +477 -101
  122. package/src/crash/sanitize.ts +1 -1
  123. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
  124. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
  125. package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
  126. package/src/discovery/builtin.ts +52 -55
  127. package/src/discovery/claude-plugins.ts +12 -4
  128. package/src/discovery/helpers.ts +307 -34
  129. package/src/exec/bash-executor.ts +330 -50
  130. package/src/exec/bash-shell-guardian.ts +388 -0
  131. package/src/exec/bash-shell-runtime-entry.ts +5 -0
  132. package/src/exec/bash-shell-signals.ts +29 -0
  133. package/src/exec/bash-shell-supervisor-entry.ts +3 -0
  134. package/src/exec/bash-shell-supervisor.ts +359 -0
  135. package/src/exec/bash-shell-worker-entry.ts +5 -0
  136. package/src/exec/bash-shell-worker-protocol.ts +36 -0
  137. package/src/exec/bash-shell-worker.ts +116 -0
  138. package/src/exec/isolated-shell.ts +393 -0
  139. package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
  140. package/src/extensibility/extensions/function-hooks.ts +959 -0
  141. package/src/extensibility/extensions/index.ts +22 -0
  142. package/src/extensibility/extensions/loader.ts +53 -2
  143. package/src/extensibility/extensions/runner.ts +895 -264
  144. package/src/extensibility/extensions/types.ts +42 -0
  145. package/src/extensibility/extensions/wrapper.ts +56 -15
  146. package/src/extensibility/gjc-plugins/compiler.ts +228 -60
  147. package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
  148. package/src/extensibility/gjc-plugins/installer.ts +13 -12
  149. package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
  150. package/src/extensibility/gjc-plugins/paths.ts +13 -0
  151. package/src/extensibility/gjc-plugins/registry.ts +113 -1
  152. package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
  153. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
  154. package/src/extensibility/gjc-plugins/schema.ts +121 -4
  155. package/src/extensibility/gjc-plugins/types.ts +25 -0
  156. package/src/extensibility/hooks/loader.ts +133 -16
  157. package/src/extensibility/hooks/types.ts +28 -0
  158. package/src/extensibility/runtime-skill-discovery.ts +149 -190
  159. package/src/extensibility/shared-events.ts +1 -1
  160. package/src/extensibility/skill-management.ts +200 -52
  161. package/src/extensibility/skills.ts +73 -25
  162. package/src/gjc-runtime/empty-delete-gc.ts +94 -4
  163. package/src/gjc-runtime/gc-runtime.ts +29 -0
  164. package/src/gjc-runtime/linux-proc.ts +14 -0
  165. package/src/gjc-runtime/repository-binding.ts +1 -1
  166. package/src/gjc-runtime/session-state-lock.ts +143 -10
  167. package/src/gjc-runtime/session-state-sidecar.ts +27 -11
  168. package/src/gjc-runtime/state-writer.ts +30 -29
  169. package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
  170. package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
  171. package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
  172. package/src/gjc-runtime/workflow-manifest.ts +16 -0
  173. package/src/harness-control-plane/session-lease.ts +2 -2
  174. package/src/internal-urls/docs-index.generated.ts +12 -12
  175. package/src/lsp/config.ts +10 -3
  176. package/src/lsp/index.ts +1 -1
  177. package/src/lsp/path-trust.ts +69 -14
  178. package/src/main.ts +78 -35
  179. package/src/modes/DESIGN.md +54 -0
  180. package/src/modes/acp/acp-agent.ts +16 -3
  181. package/src/modes/components/assistant-message.ts +22 -9
  182. package/src/modes/components/bash-execution.ts +4 -1
  183. package/src/modes/components/execution-shared.ts +4 -1
  184. package/src/modes/components/model-selector.ts +166 -33
  185. package/src/modes/components/session-observer-overlay.ts +1 -1
  186. package/src/modes/components/settings-selector.ts +1075 -235
  187. package/src/modes/components/status-line/command.ts +190 -0
  188. package/src/modes/components/status-line/index.ts +1 -0
  189. package/src/modes/components/status-line/segments.ts +32 -5
  190. package/src/modes/components/status-line/types.ts +5 -0
  191. package/src/modes/components/tool-status-header.ts +396 -22
  192. package/src/modes/controllers/command-controller.ts +3 -0
  193. package/src/modes/controllers/event-controller.ts +151 -7
  194. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  195. package/src/modes/controllers/input-controller.ts +6 -3
  196. package/src/modes/controllers/selector-controller.ts +21 -0
  197. package/src/modes/interactive-mode.ts +53 -6
  198. package/src/modes/print-mode.ts +5 -1
  199. package/src/modes/prompt-action-autocomplete.ts +54 -27
  200. package/src/modes/runtime-init.ts +2 -0
  201. package/src/modes/types.ts +17 -3
  202. package/src/modes/utils/ui-helpers.ts +2 -0
  203. package/src/prompts/system/system-prompt.md +8 -0
  204. package/src/prompts/tools/search.md +2 -0
  205. package/src/prompts/tools/subagent.md +1 -0
  206. package/src/runtime-mcp/client.ts +60 -17
  207. package/src/runtime-mcp/manager.ts +189 -14
  208. package/src/runtime-mcp/pool.test.ts +218 -0
  209. package/src/runtime-mcp/pool.ts +224 -23
  210. package/src/runtime-mcp/transports/stdio.ts +158 -24
  211. package/src/runtime-mcp/types.ts +24 -0
  212. package/src/sdk/broker/broker.ts +124 -32
  213. package/src/sdk/broker/ensure.ts +248 -41
  214. package/src/sdk/broker/lifecycle-ledger.ts +7 -17
  215. package/src/sdk/broker/lifecycle.ts +56 -6
  216. package/src/sdk/broker/startup-failure.ts +15 -6
  217. package/src/sdk/bus/chat-daemon-control.ts +3 -1
  218. package/src/sdk/bus/chat-daemon-runtime.ts +72 -6
  219. package/src/sdk/bus/chat-effect-journal.ts +132 -7
  220. package/src/sdk/bus/conversation-store.ts +29 -0
  221. package/src/sdk/bus/lifecycle-commands.ts +17 -11
  222. package/src/sdk/bus/slack-conversation.ts +1 -0
  223. package/src/sdk/bus/slack-daemon.ts +478 -49
  224. package/src/sdk/bus/slack-live-provider.ts +12 -0
  225. package/src/sdk/bus/slack-provider.ts +5 -0
  226. package/src/sdk/bus/telegram-daemon.ts +16 -2
  227. package/src/sdk/bus/topic-registry.ts +19 -0
  228. package/src/sdk/cli/master-cli.ts +1 -1
  229. package/src/sdk/cli/session-cli.ts +11 -8
  230. package/src/sdk/client/client.ts +66 -17
  231. package/src/sdk/host/host.ts +29 -9
  232. package/src/sdk/host/query/revision-store.ts +19 -13
  233. package/src/sdk/host/reverse-leases.ts +7 -4
  234. package/src/sdk/host/session-runtime.test.ts +52 -10
  235. package/src/sdk/host/session-runtime.ts +383 -44
  236. package/src/sdk/lifecycle/service.ts +4 -0
  237. package/src/sdk/protocol/operation-inventory.generated.json +77 -0
  238. package/src/sdk/router/session-router.ts +89 -38
  239. package/src/sdk/session-api-key.ts +94 -0
  240. package/src/sdk/session.ts +228 -55
  241. package/src/sdk/transport/relay.ts +35 -13
  242. package/src/sdk/transport/serve-cli.ts +3 -3
  243. package/src/session/agent-session.ts +240 -90
  244. package/src/session/contribution-prep.ts +214 -21
  245. package/src/session/fallback-chain-controller.ts +38 -0
  246. package/src/session/internal/managed-session-storage.ts +19 -5
  247. package/src/session/messages.ts +5 -0
  248. package/src/session/session-manager.ts +29 -5
  249. package/src/session-import/redact.ts +5 -1
  250. package/src/setup/paseo/announce.ts +456 -0
  251. package/src/setup/paseo/setup-deps.ts +15 -3
  252. package/src/skill-state/canonical-skills.ts +12 -0
  253. package/src/skills/skills.test.ts +41 -21
  254. package/src/slash-commands/builtin-registry.ts +1 -0
  255. package/src/system-prompt.ts +29 -3
  256. package/src/task/executor.ts +7 -1
  257. package/src/task/index.ts +21 -4
  258. package/src/tools/bash.ts +63 -19
  259. package/src/tools/cron.ts +1 -1
  260. package/src/tools/fetch.ts +29 -1
  261. package/src/tools/gh.ts +26 -3
  262. package/src/tools/github-cache.ts +68 -16
  263. package/src/tools/index.ts +10 -0
  264. package/src/tools/job.ts +21 -4
  265. package/src/tools/monitor.ts +8 -2
  266. package/src/tools/path-utils.ts +7 -0
  267. package/src/tools/read.ts +14 -2
  268. package/src/tools/render-utils.ts +1 -3
  269. package/src/tools/resource-gc.ts +28 -8
  270. package/src/tools/search.ts +116 -58
  271. package/src/tools/skill-discovery.ts +5 -0
  272. package/src/tools/skill.ts +37 -2
  273. package/src/tools/subagent.ts +6 -3
  274. package/src/tools/tool-catalog.generated.ts +10 -3
  275. package/src/tools/ultragoal-ask-guard.ts +2 -2
  276. package/src/web/insane/bridge.ts +113 -23
  277. package/src/web/search/providers/perplexity.ts +102 -41
package/src/lsp/config.ts CHANGED
@@ -336,14 +336,21 @@ export function resolveCommand(command: string, cwd: string): string | null {
336
336
  return piUtils.$which(command);
337
337
  }
338
338
 
339
- /** Resolve an LSP executable without consulting project-controlled bin directories. */
339
+ /**
340
+ * Resolve an LSP executable without consulting project-controlled bin directories.
341
+ *
342
+ * Trust is evaluated on both the discovered and canonical paths, but the returned
343
+ * path is the discovered one: multiplexed launchers such as rustup pick the proxied
344
+ * tool from argv[0], so `~/.cargo/bin/rust-analyzer -> rustup` must be spawned as
345
+ * `rust-analyzer`, not as the resolved `rustup` binary.
346
+ */
340
347
  function resolveTrustedLspCommand(command: string, cwd: string): string | null {
341
348
  if (!path.isAbsolute(command) && (command.includes("/") || command.includes("\\"))) return null;
342
349
  const discovered = path.isAbsolute(command) ? command : piUtils.$which(command);
343
350
  if (!discovered) return null;
344
351
  if (isProjectControlledPath(discovered, cwd)) return null;
345
- const canonical = canonicalExistingPath(discovered);
346
- return canonical;
352
+ if (!canonicalExistingPath(discovered)) return null;
353
+ return discovered;
347
354
  }
348
355
 
349
356
  interface ConfigSource {
package/src/lsp/index.ts CHANGED
@@ -1160,7 +1160,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1160
1160
 
1161
1161
  const serverStatus =
1162
1162
  servers.length > 0
1163
- ? `Active language servers: ${servers.join(", ")}`
1163
+ ? `Configured language servers: ${servers.join(", ")}`
1164
1164
  : "No language servers configured for this project";
1165
1165
 
1166
1166
  const output = lspmuxStatus ? `${serverStatus}\n${lspmuxStatus}` : serverStatus;
@@ -59,29 +59,84 @@ function findProjectTrustRoot(start: string, stopPaths: ReadonlySet<string>): st
59
59
  }
60
60
  }
61
61
 
62
+ function isProjectMarkerRoot(root: string): boolean {
63
+ return fs.existsSync(path.join(root, ".git")) || isDirectory(path.join(root, CONFIG_DIR_NAME));
64
+ }
65
+
66
+ /**
67
+ * Spellings of `candidate` that differ only in how HOME is named: the path as
68
+ * given, plus the same path with any ancestor that is HOME under another name
69
+ * replaced by canonical HOME. The suffix beneath HOME is kept as spelled, and an
70
+ * alias that lives inside a project (`repo/home-link -> HOME`) is project
71
+ * content, not a HOME spelling, so it is never expanded.
72
+ */
73
+ function candidateSpellings(candidate: string, canonicalHome: string, projectRoot: string | undefined): string[] {
74
+ const resolved = path.resolve(candidate);
75
+ const spellings = [resolved];
76
+ const normalizedHome = normalizePathForComparison(canonicalHome);
77
+ let current = path.dirname(resolved);
78
+ for (;;) {
79
+ if (
80
+ normalizePathForComparison(current) !== normalizedHome &&
81
+ (projectRoot === undefined || !pathIsLexicallyWithin(projectRoot, current)) &&
82
+ normalizePathForComparison(canonicalPath(current)) === normalizedHome
83
+ ) {
84
+ spellings.push(path.join(canonicalHome, path.relative(current, resolved)));
85
+ }
86
+ const parent = path.dirname(current);
87
+ if (parent === current) return spellings;
88
+ current = parent;
89
+ }
90
+ }
91
+
62
92
  export function isProjectControlledPath(candidate: string, cwd: string): boolean {
63
93
  const home = os.homedir();
64
- const stopPaths = new Set([path.resolve(home), canonicalPath(home)].map(normalizePathForComparison));
94
+ const canonicalHome = canonicalPath(home);
95
+ const stopPaths = new Set([path.resolve(home), canonicalHome].map(normalizePathForComparison));
65
96
  const lexicalTrustRoot = findProjectTrustRoot(cwd, stopPaths);
66
- const candidateIsHomeScoped = pathIsLexicallyWithin(home, candidate) || pathIsWithin(home, candidate);
67
- if (
68
- lexicalTrustRoot !== undefined &&
69
- pathIsLexicallyWithin(lexicalTrustRoot, candidate) &&
70
- (!candidateIsHomeScoped || pathIsLexicallyWithin(home, lexicalTrustRoot))
71
- )
72
- return true;
97
+ // A trust root outside HOME must not claim user executables inside HOME
98
+ // (e.g. ~/.gjc/bin); a root under HOME still owns what it contains. Each check
99
+ // judges home scope on the same view of the path it inspects, without
100
+ // dereferencing the candidate's final component: mixing views let a project
101
+ // link into HOME, or a HOME link into the project, escape rejection. HOME may
102
+ // be spelled lexically or canonically on either side, so the lexical check
103
+ // runs over every HOME-alias spelling of both the candidate and the root.
104
+ if (lexicalTrustRoot !== undefined) {
105
+ const trustRootIsHomeScoped = pathIsWithin(canonicalHome, lexicalTrustRoot);
106
+ // A root under HOME may legitimately be reached through a HOME alias, and a
107
+ // bare fallback root (no project marker) owns nothing on its own; a real
108
+ // project root outside HOME owns every alias directory it contains.
109
+ const aliasBoundary =
110
+ !trustRootIsHomeScoped && isProjectMarkerRoot(lexicalTrustRoot) ? lexicalTrustRoot : undefined;
111
+ const rootSpellings = candidateSpellings(lexicalTrustRoot, canonicalHome, undefined);
112
+ const spellings = candidateSpellings(candidate, canonicalHome, aliasBoundary);
113
+ // Every spelling names the same location, so home scope is a property of
114
+ // the location: one spelling under HOME makes all of them HOME-scoped.
115
+ const candidateIsHomeScoped = spellings.some(
116
+ spelling => pathIsLexicallyWithin(home, spelling) || pathIsLexicallyWithin(canonicalHome, spelling),
117
+ );
118
+ if (
119
+ (!candidateIsHomeScoped || trustRootIsHomeScoped) &&
120
+ spellings.some(spelling => rootSpellings.some(root => pathIsLexicallyWithin(root, spelling)))
121
+ ) {
122
+ return true;
123
+ }
124
+ }
73
125
 
126
+ const canonicalCandidate = canonicalPath(candidate);
127
+ const canonicalCandidateParent = canonicalParentPath(candidate);
128
+ const parentIsHomeScoped = pathIsLexicallyWithin(canonicalHome, canonicalCandidateParent);
129
+ const targetIsHomeScoped = pathIsWithin(canonicalHome, canonicalCandidate);
74
130
  const canonicalTrustRoots = new Set<string>();
75
131
  if (lexicalTrustRoot !== undefined) canonicalTrustRoots.add(canonicalPath(lexicalTrustRoot));
76
132
  const canonicalTrustRoot = findProjectTrustRoot(canonicalPath(cwd), stopPaths);
77
133
  if (canonicalTrustRoot !== undefined) canonicalTrustRoots.add(canonicalPath(canonicalTrustRoot));
78
134
  for (const trustRoot of canonicalTrustRoots) {
79
- if (
80
- (pathIsLexicallyWithin(trustRoot, canonicalParentPath(candidate)) ||
81
- pathIsWithin(trustRoot, canonicalPath(candidate))) &&
82
- (!candidateIsHomeScoped || pathIsLexicallyWithin(home, trustRoot) || pathIsWithin(home, trustRoot))
83
- )
84
- return true;
135
+ const trustRootIsHomeScoped = pathIsLexicallyWithin(canonicalHome, trustRoot);
136
+ const parentOwned =
137
+ pathIsLexicallyWithin(trustRoot, canonicalCandidateParent) && (!parentIsHomeScoped || trustRootIsHomeScoped);
138
+ const targetOwned = pathIsWithin(trustRoot, canonicalCandidate) && (!targetIsHomeScoped || trustRootIsHomeScoped);
139
+ if (parentOwned || targetOwned) return true;
85
140
  }
86
141
  return false;
87
142
  }
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,
@@ -60,6 +68,7 @@ import {
60
68
  import { processIncarnation } from "./sdk/broker/process-incarnation";
61
69
  import { newMasterAttestationEpoch, resolveSessionLocator, SessionIndex } from "./sdk/broker/session-index";
62
70
  import type { AgentSession } from "./session/agent-session";
71
+ import { ManagedAppendIdentityMismatchError } from "./session/internal/managed-session-storage";
63
72
  import { SessionMigrationBusyError } from "./session/internal/session-open-errors";
64
73
  import {
65
74
  type ResumeSessionIdentity,
@@ -543,7 +552,11 @@ export async function applyStartupModelProfiles(args: StartupModelProfileArgs):
543
552
 
544
553
  async function exitForStartupModelProfileError(args: StartupModelProfileArgs, error: unknown): Promise<never> {
545
554
  const message = error instanceof Error ? error.message : String(error);
546
- process.stderr.write(`${chalk.red(`Error: ${message}`)}\n`);
555
+ process.stderr.write(
556
+ error instanceof ManagedAppendIdentityMismatchError
557
+ ? `${error.operatorMessage}\n`
558
+ : `${chalk.red(`Error: ${message}`)}\n`,
559
+ );
547
560
  await args.session.dispose();
548
561
  process.exit(1);
549
562
  }
@@ -1645,6 +1658,66 @@ export async function runRootCommand(
1645
1658
  )
1646
1659
  : undefined;
1647
1660
 
1661
+ if (parsedArgs.apiKey && parsedArgs.credential) {
1662
+ process.stderr.write(`${chalk.red("--api-key and --credential cannot be used together")}\n`);
1663
+ process.exit(1);
1664
+ }
1665
+ if (parsedArgs.credential && parsedArgs.preferCredential) {
1666
+ process.stderr.write(`${chalk.red("--credential and --prefer-credential cannot be used together")}\n`);
1667
+ process.exit(1);
1668
+ }
1669
+ if (parsedArgs.apiKey && parsedArgs.preferCredential) {
1670
+ process.stderr.write(`${chalk.red("--api-key and --prefer-credential cannot be used together")}\n`);
1671
+ process.exit(1);
1672
+ }
1673
+
1674
+ let credentialSelector: CliCredentialSelector | undefined;
1675
+ let preferredCredentialSelector: CliCredentialSelector | undefined;
1676
+ try {
1677
+ credentialSelector = parsedArgs.credential ? parseCliCredentialSelector(parsedArgs.credential) : undefined;
1678
+ preferredCredentialSelector = parsedArgs.preferCredential
1679
+ ? parseCliCredentialSelector(parsedArgs.preferCredential)
1680
+ : undefined;
1681
+ if (preferredCredentialSelector) {
1682
+ const provider =
1683
+ preferredCredentialSelector.provider ??
1684
+ authStorage.resolveRuntimePreferredCredentialSelectorProvider(preferredCredentialSelector.selector);
1685
+ authStorage.setRuntimePreferredCredentialSelector(provider, preferredCredentialSelector.selector);
1686
+ }
1687
+ } catch (error) {
1688
+ const message = error instanceof Error ? error.message : String(error);
1689
+ process.stderr.write(`${chalk.red(message)}\n`);
1690
+ process.exit(1);
1691
+ }
1692
+
1693
+ const hasRootStartupProfile = Boolean(settingsInstance.get("modelProfile.default") || parsedArgs.mpreset);
1694
+ const startupModelSelectors = resolveStartupModelRefreshSelectors(
1695
+ {
1696
+ model: parsedArgs.model,
1697
+ provider: parsedArgs.provider,
1698
+ credential: parsedArgs.credential,
1699
+ continue: parsedArgs.continue,
1700
+ resume: parsedArgs.resume,
1701
+ hasStartupProfile: hasRootStartupProfile,
1702
+ },
1703
+ settingsInstance,
1704
+ );
1705
+ const explicitStartupModelSelector = parsedArgs.model
1706
+ ? typeof startupModelSelectors === "string"
1707
+ ? startupModelSelectors
1708
+ : parsedArgs.model
1709
+ : undefined;
1710
+ if (parsedArgs.apiKey && explicitStartupModelSelector) {
1711
+ const parsedModel = parseModelString(explicitStartupModelSelector);
1712
+ if (parsedModel) authStorage.setRuntimeApiKey(parsedModel.provider, parsedArgs.apiKey);
1713
+ }
1714
+ await logger.time(
1715
+ "refreshStartupModelProviders",
1716
+ refreshMissingQualifiedModelProviders,
1717
+ startupModelSelectors,
1718
+ modelRegistry,
1719
+ );
1720
+
1648
1721
  let scopedModels: ScopedModel[] = [];
1649
1722
  const modelPatterns = parsedArgs.models ?? settingsInstance.get("enabledModels");
1650
1723
  const modelMatchPreferences = {
@@ -1779,7 +1852,6 @@ export async function runRootCommand(
1779
1852
  if (isInteractive && sessionOptions.mcpConfigPath) {
1780
1853
  sessionOptions.deferMcpConfigStartup = true;
1781
1854
  }
1782
- const hasRootStartupProfile = Boolean(settingsInstance.get("modelProfile.default") || parsedArgs.mpreset);
1783
1855
  // ACP is not carved out: `gjc acp` is broker-backed and never builds a local
1784
1856
  // session here, and the broker-launched lifecycle child defers memory startup
1785
1857
  // unconditionally (createLifecycleAgentSession) so readiness never waits on
@@ -1790,37 +1862,8 @@ export async function runRootCommand(
1790
1862
  const acpStartupOptions = mode === "acp" ? resolveAcpStartupOptions(parsedArgs, sessionOptions) : undefined;
1791
1863
 
1792
1864
  // 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
- }
1865
+ if (credentialSelector) sessionOptions.credentialSelector = credentialSelector;
1866
+ if (preferredCredentialSelector) sessionOptions.preferredCredentialSelector = preferredCredentialSelector;
1824
1867
  if (parsedArgs.apiKey) {
1825
1868
  if (!sessionOptions.model && !sessionOptions.modelPattern) {
1826
1869
  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,
@@ -101,6 +101,16 @@ const MAX_PROMPT_FRAME_BYTES = 256 * 1024;
101
101
  * envelope. A canonical-length UUID keeps the measurement equal to the real frame.
102
102
  */
103
103
  const PROMPT_FRAME_ID_PLACEHOLDER = "00000000-0000-4000-8000-000000000000";
104
+ /**
105
+ * Readiness budget every ACP session launch requests, independent of MCP.
106
+ *
107
+ * Host cold start costs the same whether or not the client declared MCP servers,
108
+ * so leaving the MCP-less path on the broker's 10s default made it the only ACP
109
+ * launch that fails under ordinary concurrency: a second host starting beside the
110
+ * first measurably crosses that deadline, and the broker then reports the launch
111
+ * as `terminal_uncertain`, which an external client surfaces as a dead provider.
112
+ */
113
+ const ACP_SESSION_READINESS_TIMEOUT_MS = ACP_MCP_LIFECYCLE_TIMEOUT_MS;
104
114
 
105
115
  type JsonObject = Record<string, unknown>;
106
116
  interface PromptWaiter {
@@ -1362,7 +1372,8 @@ export class AcpAgent implements Agent {
1362
1372
  cwd: params.cwd,
1363
1373
  target: { path: params.cwd },
1364
1374
  ...(this.#startupOptions?.modelPreset ? { modelPreset: this.#startupOptions.modelPreset } : {}),
1365
- ...(mcpServers.length > 0 ? { mcpServers, readinessTimeoutMs: ACP_MCP_LIFECYCLE_TIMEOUT_MS } : {}),
1375
+ readinessTimeoutMs: ACP_SESSION_READINESS_TIMEOUT_MS,
1376
+ ...(mcpServers.length > 0 ? { mcpServers } : {}),
1366
1377
  },
1367
1378
  randomUUID(),
1368
1379
  mcpServers,
@@ -1418,7 +1429,8 @@ export class AcpAgent implements Agent {
1418
1429
  sourceSessionId: params.sessionId,
1419
1430
  sourceSessionPath: source,
1420
1431
  target: { path: params.cwd },
1421
- ...(mcpServers.length > 0 ? { mcpServers, readinessTimeoutMs: ACP_MCP_LIFECYCLE_TIMEOUT_MS } : {}),
1432
+ readinessTimeoutMs: ACP_SESSION_READINESS_TIMEOUT_MS,
1433
+ ...(mcpServers.length > 0 ? { mcpServers } : {}),
1422
1434
  },
1423
1435
  randomUUID(),
1424
1436
  mcpServers,
@@ -2268,7 +2280,8 @@ export class AcpAgent implements Agent {
2268
2280
  sessionId: id,
2269
2281
  sessionPath: saved,
2270
2282
  target: { path: cwd },
2271
- ...(mcpServers.length > 0 ? { mcpServers, readinessTimeoutMs: ACP_MCP_LIFECYCLE_TIMEOUT_MS } : {}),
2283
+ readinessTimeoutMs: ACP_SESSION_READINESS_TIMEOUT_MS,
2284
+ ...(mcpServers.length > 0 ? { mcpServers } : {}),
2272
2285
  },
2273
2286
  randomUUID(),
2274
2287
  mcpServers,
@@ -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
 
@@ -43,6 +43,7 @@ export class BashExecutionComponent extends Container {
43
43
  #outputLines: string[] = [];
44
44
  #status: ExecutionStatus = "running";
45
45
  #exitCode: number | undefined = undefined;
46
+ #signal: string | undefined = undefined;
46
47
  #loader: Loader;
47
48
  #truncation?: TruncationMeta;
48
49
  #expanded = false;
@@ -127,9 +128,10 @@ export class BashExecutionComponent extends Container {
127
128
  setComplete(
128
129
  exitCode: number | undefined,
129
130
  cancelled: boolean,
130
- options?: { output?: string; truncation?: TruncationMeta },
131
+ options?: { output?: string; signal?: string; truncation?: TruncationMeta },
131
132
  ): void {
132
133
  this.#exitCode = exitCode;
134
+ this.#signal = options?.signal;
133
135
  this.#status = resolveExecutionStatus(exitCode, cancelled);
134
136
  this.#truncation = options?.truncation;
135
137
  if (options?.output !== undefined) {
@@ -215,6 +217,7 @@ export class BashExecutionComponent extends Container {
215
217
  const footer = buildStatusFooter({
216
218
  status: this.#status,
217
219
  exitCode: this.#exitCode,
220
+ signal: this.#signal,
218
221
  truncation: this.#truncation,
219
222
  hiddenLineCount,
220
223
  suppressHiddenCount: hasSixelOutput && !fallbackActive,
@@ -68,6 +68,7 @@ export function createCollapsedPreview(previewText: string, previewLines: number
68
68
  export function buildStatusFooter(opts: {
69
69
  status: ExecutionStatus;
70
70
  exitCode: number | undefined;
71
+ signal?: string;
71
72
  truncation: TruncationMeta | undefined;
72
73
  hiddenLineCount: number;
73
74
  /** Suppress the "… N more lines" hint (used when sixel passthrough renders the full output). */
@@ -81,7 +82,9 @@ export function buildStatusFooter(opts: {
81
82
  if (opts.status === "cancelled") {
82
83
  parts.push(theme.fg("warning", "(cancelled)"));
83
84
  } else if (opts.status === "error") {
84
- parts.push(theme.fg("error", `(exit ${opts.exitCode})`));
85
+ parts.push(
86
+ theme.fg("error", opts.signal ? `(${opts.signal}, exit ${opts.exitCode})` : `(exit ${opts.exitCode})`),
87
+ );
85
88
  }
86
89
  if (opts.truncation) {
87
90
  parts.push(theme.fg("warning", formatTruncationMetaNotice(opts.truncation)));