@gajae-code/coding-agent 0.18.6 → 0.18.8

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 (302) hide show
  1. package/CHANGELOG.md +318 -1
  2. package/dist/types/async/job-manager.d.ts +1 -1
  3. package/dist/types/capability/mcp.d.ts +2 -0
  4. package/dist/types/capability/skill.d.ts +2 -0
  5. package/dist/types/capability/types.d.ts +6 -0
  6. package/dist/types/cli/public-command-errors.d.ts +3 -1
  7. package/dist/types/cli/setup-cli.d.ts +1 -1
  8. package/dist/types/commands/deep-interview.d.ts +3 -0
  9. package/dist/types/config/autorouting-tier-map.d.ts +348 -9
  10. package/dist/types/config/file-lock.d.ts +1 -0
  11. package/dist/types/config/model-bindings-applier.d.ts +2 -0
  12. package/dist/types/config/model-profile-activation.d.ts +11 -3
  13. package/dist/types/config/model-registry.d.ts +4 -9
  14. package/dist/types/config/settings.d.ts +4 -0
  15. package/dist/types/config/skill-settings-defaults.d.ts +2 -0
  16. package/dist/types/cursor-delete-path.d.ts +1 -0
  17. package/dist/types/cursor-mcp-name.d.ts +8 -0
  18. package/dist/types/customization/inventory.d.ts +2 -0
  19. package/dist/types/debug/report-bundle-parent.d.ts +2 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
  21. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
  22. package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
  23. package/dist/types/discovery/helpers.d.ts +8 -3
  24. package/dist/types/export/html/css-color.d.ts +2 -0
  25. package/dist/types/export/html/template.generated.d.ts +1 -1
  26. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  27. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
  28. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
  29. package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
  30. package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
  31. package/dist/types/extensibility/skill-management.d.ts +3 -1
  32. package/dist/types/extensibility/skills.d.ts +2 -0
  33. package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
  34. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
  35. package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
  36. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
  37. package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
  38. package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
  39. package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
  40. package/dist/types/gjc-runtime/state-writer.d.ts +11 -1
  41. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
  42. package/dist/types/internal-urls/skill-path.d.ts +1 -0
  43. package/dist/types/lsp/workspace-path.d.ts +1 -0
  44. package/dist/types/modes/acp/acp-agent.d.ts +7 -0
  45. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  46. package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
  47. package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
  48. package/dist/types/modes/controllers/input-controller.d.ts +9 -0
  49. package/dist/types/modes/interactive-mode.d.ts +1 -0
  50. package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
  51. package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
  52. package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
  53. package/dist/types/progress/collect-project-progress.d.ts +38 -0
  54. package/dist/types/progress/progress-contract.d.ts +154 -0
  55. package/dist/types/progress/progress-snapshot.d.ts +11 -0
  56. package/dist/types/progress/project-progress.d.ts +141 -0
  57. package/dist/types/progress/render-progress.d.ts +28 -0
  58. package/dist/types/runtime-mcp/elicitation.d.ts +12 -0
  59. package/dist/types/runtime-mcp/oauth-public-url.d.ts +1 -0
  60. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
  61. package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
  62. package/dist/types/runtime-mcp/types.d.ts +6 -0
  63. package/dist/types/sdk/acp/adapter.d.ts +1 -1
  64. package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
  65. package/dist/types/sdk/broker/broker.d.ts +18 -1
  66. package/dist/types/sdk/broker/lifecycle.d.ts +9 -0
  67. package/dist/types/sdk/broker/session-index.d.ts +12 -6
  68. package/dist/types/sdk/broker/task-artifact-owner-validation.d.ts +55 -0
  69. package/dist/types/sdk/bus/index.d.ts +12 -1
  70. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
  71. package/dist/types/sdk/bus/telegram-daemon.d.ts +3 -2
  72. package/dist/types/sdk/client/client.d.ts +2 -0
  73. package/dist/types/sdk/host/host.d.ts +6 -0
  74. package/dist/types/sdk/host/query/handlers.d.ts +3 -0
  75. package/dist/types/sdk/index.d.ts +1 -0
  76. package/dist/types/sdk/router/session-router.d.ts +9 -0
  77. package/dist/types/sdk/session-list.d.ts +1 -1
  78. package/dist/types/sdk/session.d.ts +8 -0
  79. package/dist/types/session/agent-session.d.ts +38 -9
  80. package/dist/types/session/artifacts.d.ts +2 -2
  81. package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
  82. package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
  83. package/dist/types/session/internal/managed-session-storage.d.ts +36 -1
  84. package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
  85. package/dist/types/session/internal/task-artifact-owner-access.d.ts +19 -0
  86. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +11 -0
  87. package/dist/types/session/session-manager.d.ts +31 -5
  88. package/dist/types/session/session-retirement.d.ts +71 -0
  89. package/dist/types/session/session-storage.d.ts +62 -48
  90. package/dist/types/session/session-transcript-header.d.ts +2 -0
  91. package/dist/types/session/task-artifact-owner-codec.d.ts +109 -0
  92. package/dist/types/session/task-artifact-owner-retirement.d.ts +7 -0
  93. package/dist/types/session/task-artifact-owner.d.ts +10 -0
  94. package/dist/types/setup/credential-auto-import.d.ts +1 -1
  95. package/dist/types/setup/credential-import.d.ts +6 -4
  96. package/dist/types/skill-state/active-state.d.ts +38 -0
  97. package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
  98. package/dist/types/task/executor.d.ts +3 -0
  99. package/dist/types/tools/bash-skill-urls.d.ts +1 -1
  100. package/dist/types/tools/index.d.ts +3 -0
  101. package/dist/types/tools/python.d.ts +9 -1
  102. package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
  103. package/dist/types/tools/restricted-cwd.d.ts +1 -0
  104. package/dist/types/tools/todo-write.d.ts +6 -0
  105. package/dist/types/utils/tools-checksum.d.ts +7 -0
  106. package/package.json +7 -7
  107. package/scripts/generate-sdk-operation-inventory.ts +21 -0
  108. package/scripts/resident-memory-bench.ts +174 -20
  109. package/scripts/verify-managed-empty-stop.ts +28 -0
  110. package/src/async/job-manager.ts +228 -122
  111. package/src/capability/index.ts +18 -0
  112. package/src/capability/mcp.ts +2 -0
  113. package/src/capability/skill.ts +2 -0
  114. package/src/capability/types.ts +6 -0
  115. package/src/cli/auth-broker-cli.ts +1 -1
  116. package/src/cli/config-cli.ts +8 -18
  117. package/src/cli/customize-doctor.ts +15 -2
  118. package/src/cli/public-command-errors.ts +21 -1
  119. package/src/cli/setup-cli.ts +4 -4
  120. package/src/cli/skills-cli.ts +5 -0
  121. package/src/cli-main.ts +7 -8
  122. package/src/commands/deep-interview.ts +47 -8
  123. package/src/commands/sdk.ts +140 -29
  124. package/src/config/autorouting-tier-map.ts +117 -7
  125. package/src/config/file-lock.ts +307 -68
  126. package/src/config/mcp-schema.json +4 -0
  127. package/src/config/model-bindings-applier.ts +20 -0
  128. package/src/config/model-profile-activation.ts +240 -46
  129. package/src/config/model-registry.ts +47 -153
  130. package/src/config/settings.ts +44 -4
  131. package/src/config/skill-settings-defaults.ts +7 -0
  132. package/src/coordinator-mcp/policy.ts +3 -2
  133. package/src/cursor-delete-path.ts +10 -0
  134. package/src/cursor-mcp-name.ts +15 -0
  135. package/src/cursor.ts +13 -2
  136. package/src/customization/inventory.ts +3 -0
  137. package/src/debug/report-bundle-parent.ts +22 -0
  138. package/src/debug/report-bundle.ts +3 -0
  139. package/src/debug/system-info.ts +13 -1
  140. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
  141. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
  142. package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
  143. package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
  144. package/src/defaults/gjc-defaults.test.ts +18 -1
  145. package/src/defaults/gjc-defaults.ts +3 -0
  146. package/src/defaults/gjc-skills.generated.ts +58 -36
  147. package/src/discovery/agents.ts +6 -1
  148. package/src/discovery/builtin.ts +10 -0
  149. package/src/discovery/helpers.ts +168 -23
  150. package/src/discovery/mcp-json.ts +6 -0
  151. package/src/discovery/ssh.ts +9 -4
  152. package/src/discovery/substitute-plugin-root.ts +3 -1
  153. package/src/edit/file-read-cache.ts +1 -1
  154. package/src/eval/py/executor.ts +380 -120
  155. package/src/export/html/css-color.ts +9 -0
  156. package/src/export/html/index.ts +5 -4
  157. package/src/export/html/template.generated.ts +1 -1
  158. package/src/extensibility/extensions/runner.ts +4 -0
  159. package/src/extensibility/extensions/types.ts +7 -0
  160. package/src/extensibility/gjc-plugins/runtime-adapters.ts +73 -8
  161. package/src/extensibility/plugins/manager.ts +3 -2
  162. package/src/extensibility/plugins/marketplace/cache.ts +15 -0
  163. package/src/extensibility/plugins/marketplace/manager.ts +11 -3
  164. package/src/extensibility/plugins/plugin-link-path.ts +12 -0
  165. package/src/extensibility/runtime-skill-discovery.ts +20 -2
  166. package/src/extensibility/skill-management.ts +6 -2
  167. package/src/extensibility/skills.ts +6 -1
  168. package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
  169. package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
  170. package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
  171. package/src/gjc-runtime/deep-interview-stage.ts +98 -21
  172. package/src/gjc-runtime/gc-runtime.ts +206 -8
  173. package/src/gjc-runtime/launch-worktree.ts +14 -4
  174. package/src/gjc-runtime/ralplan-runtime.ts +447 -52
  175. package/src/gjc-runtime/state-argv.ts +2 -0
  176. package/src/gjc-runtime/state-migrations.ts +91 -29
  177. package/src/gjc-runtime/state-runtime.ts +3716 -135
  178. package/src/gjc-runtime/state-writer.ts +83 -8
  179. package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
  180. package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
  181. package/src/gjc-runtime/workflow-manifest.ts +54 -21
  182. package/src/harness-control-plane/operate.ts +12 -4
  183. package/src/harness-control-plane/owner.ts +5 -1
  184. package/src/hashline/parser.ts +48 -9
  185. package/src/hashline/recovery.ts +13 -4
  186. package/src/hindsight/config.ts +9 -3
  187. package/src/internal-urls/docs-index.generated.ts +19 -18
  188. package/src/internal-urls/skill-path.ts +19 -0
  189. package/src/internal-urls/skill-protocol.ts +2 -0
  190. package/src/lsp/edits.ts +6 -0
  191. package/src/lsp/workspace-path.ts +51 -0
  192. package/src/main.ts +195 -31
  193. package/src/modes/acp/acp-agent.ts +163 -7
  194. package/src/modes/components/custom-editor.ts +14 -0
  195. package/src/modes/components/customization/customization-dashboard.ts +12 -0
  196. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  197. package/src/modes/components/extensions/state-manager.ts +15 -5
  198. package/src/modes/components/model-selector.ts +1 -4
  199. package/src/modes/components/provider-onboarding-selector.ts +1 -1
  200. package/src/modes/components/status-line/segments.ts +4 -0
  201. package/src/modes/controllers/event-controller.ts +11 -4
  202. package/src/modes/controllers/input-controller.ts +74 -29
  203. package/src/modes/controllers/runtime-mcp-command-controller.ts +8 -3
  204. package/src/modes/controllers/selector-controller.ts +28 -15
  205. package/src/modes/interactive-mode.ts +4 -0
  206. package/src/modes/runtime-init.ts +4 -0
  207. package/src/modes/utils/abort-message.ts +3 -1
  208. package/src/modes/utils/interrupt-ownership.ts +12 -0
  209. package/src/modes/utils/keybinding-matchers.ts +4 -0
  210. package/src/modes/utils/macos-option-forwarding.ts +80 -0
  211. package/src/progress/collect-project-progress.ts +153 -0
  212. package/src/progress/progress-contract.ts +172 -0
  213. package/src/progress/progress-snapshot.ts +140 -0
  214. package/src/progress/project-progress.ts +446 -0
  215. package/src/progress/render-progress.ts +209 -0
  216. package/src/prompts/system/master-mode.md +1 -0
  217. package/src/prompts/tools/skill-discovery.md +1 -1
  218. package/src/runtime-mcp/client.ts +13 -4
  219. package/src/runtime-mcp/config-writer.ts +10 -6
  220. package/src/runtime-mcp/config.ts +1 -0
  221. package/src/runtime-mcp/elicitation.ts +142 -0
  222. package/src/runtime-mcp/oauth-flow.ts +5 -0
  223. package/src/runtime-mcp/oauth-public-url.ts +8 -0
  224. package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
  225. package/src/runtime-mcp/smithery-registry.ts +22 -3
  226. package/src/runtime-mcp/transports/http.ts +2 -2
  227. package/src/runtime-mcp/types.ts +6 -0
  228. package/src/sdk/acp/adapter.ts +93 -8
  229. package/src/sdk/broker/broker-exit.ts +52 -23
  230. package/src/sdk/broker/broker.ts +83 -7
  231. package/src/sdk/broker/ensure.ts +2 -0
  232. package/src/sdk/broker/lifecycle.ts +820 -73
  233. package/src/sdk/broker/session-index.ts +191 -81
  234. package/src/sdk/broker/task-artifact-owner-validation.ts +259 -0
  235. package/src/sdk/bus/chat-daemon-cli.ts +38 -1
  236. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  237. package/src/sdk/bus/chat-daemon-runtime.ts +3 -0
  238. package/src/sdk/bus/index.ts +241 -46
  239. package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
  240. package/src/sdk/bus/telegram-daemon.ts +4 -2
  241. package/src/sdk/cli/session-cli.ts +17 -3
  242. package/src/sdk/client/client.ts +28 -8
  243. package/src/sdk/host/host.ts +6 -0
  244. package/src/sdk/host/query/handlers.ts +9 -1
  245. package/src/sdk/host/session-runtime.test.ts +2805 -411
  246. package/src/sdk/host/session-runtime.ts +428 -111
  247. package/src/sdk/host/surface-policy.ts +1 -0
  248. package/src/sdk/index.ts +8 -0
  249. package/src/sdk/lifecycle-session.ts +3 -1
  250. package/src/sdk/mcp/server.ts +15 -3
  251. package/src/sdk/prompt-deadline-manager.ts +15 -0
  252. package/src/sdk/protocol/operation-inventory.generated.json +146 -0
  253. package/src/sdk/protocol/operation-registry.ts +9 -2
  254. package/src/sdk/router/session-router.ts +94 -14
  255. package/src/sdk/session-list.ts +1 -0
  256. package/src/sdk/session.ts +81 -38
  257. package/src/session/agent-session.ts +674 -224
  258. package/src/session/artifacts.ts +174 -70
  259. package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
  260. package/src/session/internal/managed-session-scope.ts +3457 -462
  261. package/src/session/internal/managed-session-storage.ts +482 -61
  262. package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
  263. package/src/session/internal/task-artifact-owner-access.ts +245 -0
  264. package/src/session/internal/task-artifact-owner-transcript.ts +614 -0
  265. package/src/session/session-manager.ts +939 -133
  266. package/src/session/session-retirement.ts +714 -0
  267. package/src/session/session-storage.ts +418 -174
  268. package/src/session/session-transcript-header.ts +17 -0
  269. package/src/session/task-artifact-owner-codec.ts +641 -0
  270. package/src/session/task-artifact-owner-retirement.ts +477 -0
  271. package/src/session/task-artifact-owner.ts +289 -0
  272. package/src/setup/credential-auto-import.ts +12 -3
  273. package/src/setup/credential-import.ts +176 -5
  274. package/src/setup/paseo/announce.ts +6 -1
  275. package/src/skill-state/active-state.ts +132 -2
  276. package/src/slash-commands/builtin-registry.ts +39 -9
  277. package/src/slash-commands/helpers/progress-report.ts +23 -0
  278. package/src/stt/setup.ts +3 -5
  279. package/src/task/commands.ts +3 -1
  280. package/src/task/executor.ts +4 -0
  281. package/src/task/index.ts +6 -0
  282. package/src/tools/ask.ts +205 -6
  283. package/src/tools/ast-edit.ts +4 -0
  284. package/src/tools/bash-allowed-prefixes.ts +15 -0
  285. package/src/tools/bash-skill-urls.ts +56 -3
  286. package/src/tools/bash.ts +48 -79
  287. package/src/tools/descriptors.ts +5 -1
  288. package/src/tools/eval.ts +56 -36
  289. package/src/tools/image-gen.ts +1 -1
  290. package/src/tools/index.ts +3 -0
  291. package/src/tools/python.ts +187 -66
  292. package/src/tools/recipe/index.ts +9 -0
  293. package/src/tools/recipe/recipe-guard.ts +19 -0
  294. package/src/tools/restricted-cwd.ts +10 -0
  295. package/src/tools/skill-discovery.ts +5 -0
  296. package/src/tools/skill.ts +28 -1
  297. package/src/tools/todo-write.ts +10 -1
  298. package/src/tools/tool-catalog.generated.ts +1 -1
  299. package/src/tools/write.ts +1 -0
  300. package/src/utils/tools-checksum.ts +34 -0
  301. package/src/utils/tools-manager.ts +24 -6
  302. package/src/web/search/providers/codex.ts +3 -2
@@ -38,6 +38,8 @@ import {
38
38
  } from "@agentclientprotocol/sdk";
39
39
  import { getAgentDir, logger, resolveEquivalentPath } from "@gajae-code/utils";
40
40
  import packageJson from "../../../package.json" with { type: "json" };
41
+ import { PROJECT_PROGRESS_SNAPSHOT_SCHEMA, type ProjectProgressSnapshot } from "../../progress/progress-contract";
42
+ import { PROGRESS_COMMAND_ACP_DESCRIPTION, renderProgressSnapshot } from "../../progress/render-progress";
41
43
  import {
42
44
  ACP_SESSION_RECONNECT,
43
45
  type AcpProviderRegistration,
@@ -258,6 +260,8 @@ type PendingAttachment = { epoch: number; task: Promise<void> };
258
260
 
259
261
  type PromptAdmissionSettlement = { kind: "cancelled" } | { kind: "rejected"; error: AcpSdkAdapterError };
260
262
  type PromptAdmissionReservation = {
263
+ /** Held by a model-free builtin (`/progress`) answered locally; no host turn exists to abort. */
264
+ localCommand?: true;
261
265
  cancelled?: boolean;
262
266
  settlement?: PromptAdmissionSettlement;
263
267
  wake: () => void;
@@ -526,9 +530,12 @@ function pageItems(value: unknown): unknown[] {
526
530
  return Array.isArray(page?.items) ? page.items : [];
527
531
  }
528
532
 
533
+ /** Builtin `/progress`: answered locally from the read-only `session.progress` SDK query, never a model turn. */
534
+ const ACP_PROGRESS_COMMAND: AvailableCommand = { name: "progress", description: PROGRESS_COMMAND_ACP_DESCRIPTION };
535
+
529
536
  /** Build the ACP command palette from the shared builtins and live SDK skill state. */
530
537
  export function acpAvailableCommandsFromSkills(query: unknown): AvailableCommand[] {
531
- const commands = new Map<string, AvailableCommand>();
538
+ const commands = new Map<string, AvailableCommand>([[ACP_PROGRESS_COMMAND.name, ACP_PROGRESS_COMMAND]]);
532
539
  for (const item of pageItems(query)) {
533
540
  const skill = object(item);
534
541
  if (typeof skill?.name !== "string" || !skill.name) continue;
@@ -1426,6 +1433,21 @@ const THINKING_CONFIG_OPTIONS = ["off", "minimal", "low", "medium", "high", "xhi
1426
1433
  name: value,
1427
1434
  }));
1428
1435
 
1436
+ function thinkingConfigOptions(query: unknown, currentModel: string | undefined): { value: string; name: string }[] {
1437
+ if (!currentModel) return THINKING_CONFIG_OPTIONS;
1438
+ const separator = currentModel.indexOf("/");
1439
+ if (separator <= 0 || separator === currentModel.length - 1) return THINKING_CONFIG_OPTIONS;
1440
+ const provider = currentModel.slice(0, separator);
1441
+ const id = currentModel.slice(separator + 1);
1442
+ const model = pageItems(query)
1443
+ .map(item => object(item))
1444
+ .find(item => item?.provider === provider && item.id === id);
1445
+ const validLevels = object(model?.thinking)?.validLevels;
1446
+ if (!Array.isArray(validLevels) || validLevels.length === 0) return THINKING_CONFIG_OPTIONS;
1447
+ const levels = validLevels.filter((value): value is string => typeof value === "string");
1448
+ return levels.length === 0 ? THINKING_CONFIG_OPTIONS : levels.map(value => ({ value, name: value }));
1449
+ }
1450
+
1429
1451
  /** Maps live canonical SDK config and the selected model catalog into the ACP 1.2.1 session state surface. */
1430
1452
  export function acpSessionStateFromConfig(
1431
1453
  query: unknown,
@@ -1436,6 +1458,7 @@ export function acpSessionStateFromConfig(
1436
1458
  const values = configValues(query);
1437
1459
  const useModelPresets = modelPreset !== undefined;
1438
1460
  const currentModeId = values.get(MODE_CONFIG_ID) === ACP_PLAN_MODE_ID ? ACP_PLAN_MODE_ID : ACP_DEFAULT_MODE_ID;
1461
+ const thinkingOptions = thinkingConfigOptions(modelCatalogQuery, values.get(MODEL_CONFIG_ID));
1439
1462
  return {
1440
1463
  configOptions: [
1441
1464
  {
@@ -1461,7 +1484,7 @@ export function acpSessionStateFromConfig(
1461
1484
  ? modelPresetConfigOptions(modelCatalogQuery, value)
1462
1485
  : modelConfigOptions(modelCatalogQuery, value, activeProviders)
1463
1486
  : option.id === THINKING_CONFIG_ID
1464
- ? THINKING_CONFIG_OPTIONS
1487
+ ? thinkingOptions
1465
1488
  : [...option.options];
1466
1489
  return [
1467
1490
  {
@@ -1492,6 +1515,22 @@ export function acpSkillInvocation(blocks: PromptRequest["prompt"]): { name: str
1492
1515
  return { name: match[1], args: match[2]?.trim() ?? "" };
1493
1516
  }
1494
1517
 
1518
+ /**
1519
+ * Recognize the builtin `/progress` command only when it is the complete, single text
1520
+ * prompt. Like the TUI (`allowArgs: false`), `/progress <args>` is not the command.
1521
+ */
1522
+ export function isAcpProgressCommand(blocks: PromptRequest["prompt"]): boolean {
1523
+ return blocks.length === 1 && blocks[0]?.type === "text" && blocks[0].text.trim() === "/progress";
1524
+ }
1525
+
1526
+ /** Render the `session.progress` query result as the plain-text `/progress` view. */
1527
+ export function acpProgressReportText(query: unknown): string {
1528
+ const snapshot = object(pageItems(query)[0]);
1529
+ if (snapshot?.schema !== PROJECT_PROGRESS_SNAPSHOT_SCHEMA)
1530
+ throw new AcpSdkAdapterError("unavailable", "SDK session.progress response omitted the progress snapshot.");
1531
+ return renderProgressSnapshot(snapshot as unknown as ProjectProgressSnapshot).join("\n");
1532
+ }
1533
+
1495
1534
  /** Convert every ACP prompt block the agent advertises without silently discarding context. */
1496
1535
  export function acpPromptPayload(blocks: PromptRequest["prompt"]): {
1497
1536
  text: string;
@@ -1798,11 +1837,13 @@ export class AcpAgent implements Agent {
1798
1837
  if (adapter) adapter.acceptFrame(acpFrame);
1799
1838
  else this.#pendingRouterFrames.get(attachment.sessionId)?.push(acpFrame);
1800
1839
  },
1801
- onSessionRemoved: attachment => {
1840
+ onSessionRemoved: (attachment, removalReason) => {
1802
1841
  const adapter =
1803
1842
  this.#sessions.get(attachment.sessionId)?.adapter ??
1804
1843
  this.#pendingRouterAdapters.get(attachment.sessionId);
1805
1844
  adapter?.revokeAttachment(attachment);
1845
+ if (adapter && removalReason !== "replaced_same_generation")
1846
+ this.#settlePromptAfterHostClose(attachment.sessionId, removalReason ?? "host_exit");
1806
1847
  },
1807
1848
  },
1808
1849
  });
@@ -2158,7 +2199,22 @@ export class AcpAgent implements Agent {
2158
2199
  }
2159
2200
  break;
2160
2201
  case THINKING_CONFIG_ID:
2161
- await this.#adapter(params.sessionId).control("thinking.set", { level: params.value });
2202
+ {
2203
+ const before = await this.#sessionState(params.sessionId);
2204
+ const thinkingOption = before.configOptions?.find(option => option.id === THINKING_CONFIG_ID);
2205
+ if (
2206
+ thinkingOption?.type !== "select" ||
2207
+ !thinkingOption.options.some(option => "value" in option && option.value === params.value)
2208
+ )
2209
+ throw new Error(`Unsupported thinking level: ${params.value}`);
2210
+ await this.#adapter(params.sessionId).control("thinking.set", { level: params.value });
2211
+ const after = await this.#sessionState(params.sessionId);
2212
+ const currentValue = after.configOptions?.find(option => option.id === THINKING_CONFIG_ID)?.currentValue;
2213
+ if (currentValue !== params.value)
2214
+ throw new Error(
2215
+ `Thinking level was not applied: requested ${params.value}, current ${currentValue ?? "unknown"}`,
2216
+ );
2217
+ }
2162
2218
  break;
2163
2219
  default: {
2164
2220
  const operation = ACP_CONFIG_CONTROL_OPERATIONS[params.configId];
@@ -2365,6 +2421,38 @@ export class AcpAgent implements Agent {
2365
2421
  }
2366
2422
  }
2367
2423
 
2424
+ /**
2425
+ * Read-only builtin `/progress`: answered from the session.progress query instead of a
2426
+ * model turn. It owns prompt admission (`pendingPromptAdmission`) from the query until its
2427
+ * response is published, so a concurrent prompt is refused as `conflict` exactly as it is
2428
+ * while a model turn is active. `cancel` and session teardown settle that reservation
2429
+ * through the existing admission paths; this command observes it and stops publishing.
2430
+ */
2431
+ async #runProgressCommand(sessionId: string, record: SessionRecord): Promise<PromptResponse> {
2432
+ const { promise: woken, resolve: wake } = Promise.withResolvers<void>();
2433
+ const reservation: PromptAdmissionReservation = { localCommand: true, wake, woken };
2434
+ // Replaces a follow-up reservation this prompt already consumed; `#submitPrompt` only
2435
+ // clears the reservation it installed, so ownership stays with this command.
2436
+ record.pendingPromptAdmission = reservation;
2437
+ try {
2438
+ const query = record.adapter.query("session.progress");
2439
+ void query.catch(() => undefined);
2440
+ const result = await Promise.race([query.then(value => ({ value })), woken.then(() => undefined)]);
2441
+ const settlement = reservation.settlement;
2442
+ if (settlement?.kind === "rejected") throw settlement.error;
2443
+ if (settlement?.kind === "cancelled" || reservation.cancelled || !result) return { stopReason: "cancelled" };
2444
+ const text = acpProgressReportText(result.value);
2445
+ await this.#publishSessionUpdate(
2446
+ sessionId,
2447
+ { sessionId, update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text } } },
2448
+ record.adapter,
2449
+ );
2450
+ return { stopReason: "end_turn" };
2451
+ } finally {
2452
+ if (record.pendingPromptAdmission === reservation) record.pendingPromptAdmission = undefined;
2453
+ }
2454
+ }
2455
+
2368
2456
  async #waitForPromptSettlement(
2369
2457
  waiter: PromptWaiter,
2370
2458
  admissionReservation: PromptAdmissionReservation,
@@ -2449,6 +2537,7 @@ export class AcpAgent implements Agent {
2449
2537
  );
2450
2538
  return { stopReason: "end_turn" };
2451
2539
  }
2540
+ if (isAcpProgressCommand(params.prompt)) return await this.#runProgressCommand(params.sessionId, record);
2452
2541
  // The SDK transport hard-caps a single request frame at 256 KiB and answers an
2453
2542
  // oversize frame by closing the socket (CloseCode::Size, crates/gjc-sdk/src/server.rs),
2454
2543
  // which surfaces to the client as an opaque `connection_closed` mid-turn. Reject
@@ -2936,6 +3025,7 @@ export class AcpAgent implements Agent {
2936
3025
  // Record the client's intent before awaiting the SDK so a prompt that rejects
2937
3026
  // mid-cancel (e.g. preflight `busy`) can still settle as `cancelled`.
2938
3027
  record.cancelRequested = true;
3028
+ const localCommand = !record.activePrompt && record.pendingPromptAdmission?.localCommand === true;
2939
3029
  this.#settlePendingPromptAdmission(record, { kind: "cancelled" });
2940
3030
  // C04 terminal abort: an external client cancel stops the current turn
2941
3031
  // (`scope:"turn"`, the default, matching the SDK `turn.abort` default and
@@ -2943,6 +3033,12 @@ export class AcpAgent implements Agent {
2943
3033
  // subagents and background tasks stopped opts in with
2944
3034
  // `_meta.gjc.abortScope: "owned"` (or `GJC_ACP_ABORT_SCOPE=owned`).
2945
3035
  const scope = resolveAcpAbortScope(params._meta, process.env);
3036
+ // The current turn is a model-free builtin answered locally: settling its reservation
3037
+ // above cancels it, and the host has no turn to abort.
3038
+ if (localCommand && scope === "turn") {
3039
+ record.cancelRequested = false;
3040
+ return;
3041
+ }
2946
3042
  const waiter = record.activePrompt;
2947
3043
  const waiterWasUnacknowledged = waiter !== undefined && !waiter.acknowledged;
2948
3044
  const waiterWasBeforeActivity = waiter !== undefined && !waiter.observedTurnActivity;
@@ -3018,9 +3114,11 @@ export class AcpAgent implements Agent {
3018
3114
  // publication is advisory and must never gate the settlement above.
3019
3115
  void this.#publishPromptPhaseIdle(params.sessionId, record.adapter);
3020
3116
  } else {
3117
+ if (waiter && record.activePrompt === waiter && waiter.terminal && !waiter.settled)
3118
+ this.#settlePrompt(params.sessionId, record, waiter);
3021
3119
  // The acknowledgement proves the run was aborted, not that its terminal was
3022
3120
  // published. Arm the bounded settlement so the turn cannot outlive the cancel.
3023
- this.#scheduleCancelSettlement(params.sessionId, record);
3121
+ else this.#scheduleCancelSettlement(params.sessionId, record);
3024
3122
  }
3025
3123
  waiter?.cancelAttemptResolve?.(true);
3026
3124
  } catch (error) {
@@ -3036,6 +3134,9 @@ export class AcpAgent implements Agent {
3036
3134
  !waiter.settled
3037
3135
  ) {
3038
3136
  if (waiter.terminalReserved) {
3137
+ await record.frameTail;
3138
+ if (record.activePrompt === waiter && waiter.terminal && !waiter.settled)
3139
+ this.#settlePrompt(params.sessionId, record, waiter);
3039
3140
  waiter.cancelAttemptResolve?.(true);
3040
3141
  return;
3041
3142
  }
@@ -3091,6 +3192,9 @@ export class AcpAgent implements Agent {
3091
3192
  waiter.cancelAttempt = undefined;
3092
3193
  waiter.cancelAttemptResolve = undefined;
3093
3194
  }
3195
+ const cancelWaveSettled = !waiter || waiter.cancelAcknowledged || (waiter.pendingCancelAttempts ?? 0) <= 1;
3196
+ if (cancelWaveSettled && waiter && record.activePrompt === waiter && waiter.terminal && !waiter.settled)
3197
+ this.#settlePrompt(params.sessionId, record, waiter);
3094
3198
  throw cancellationError;
3095
3199
  } finally {
3096
3200
  if (waiter) {
@@ -4459,6 +4563,37 @@ export class AcpAgent implements Agent {
4459
4563
  return undefined;
4460
4564
  }
4461
4565
 
4566
+ /** Settles the active prompt when the host explicitly reports that its session closed. */
4567
+ #settlePromptAfterHostClose(id: string, reason: string): void {
4568
+ const record = this.#sessions.get(id);
4569
+ if (!record) return;
4570
+ const waiter = record.activePrompt;
4571
+ if (!waiter || waiter.settled || waiter.terminalReserved) return;
4572
+ if (record.cancelRequested && waiter.cancelAcknowledged) {
4573
+ void this.#settleCancelledPrompt(id, record, waiter);
4574
+ return;
4575
+ }
4576
+ // An accepted mutation without its acknowledgement remains uncertain. Keep the
4577
+ // existing reconciliation path so a host exit never turns an ambiguous prompt
4578
+ // into a retryable failure.
4579
+ if (waiter.dispatched && !waiter.acknowledged) {
4580
+ this.#startUncertainPromptRecovery(id, record, waiter);
4581
+ return;
4582
+ }
4583
+ record.busy = record.backgroundBusy;
4584
+ void this.#rejectPrompt(
4585
+ record,
4586
+ id,
4587
+ waiter,
4588
+ new AcpPromptAbandonedError(
4589
+ "prompt_abandoned",
4590
+ `ACP prompt was abandoned because the SDK session host closed (${reason}). The turn was settled so the ` +
4591
+ `client stops waiting; a new prompt requires a valid or re-established session attachment.`,
4592
+ waiter.planSnapshot,
4593
+ ),
4594
+ );
4595
+ }
4596
+
4462
4597
  /**
4463
4598
  * Settles the ACP prompt only. The agent's own work is left alone: this reports that the
4464
4599
  * turn can no longer be observed, it does not cancel or tear down the session.
@@ -4532,6 +4667,14 @@ export class AcpAgent implements Agent {
4532
4667
  else logger.warn(`ACP session ${id} dropped an event from a foreign session identity.`);
4533
4668
  return;
4534
4669
  }
4670
+ if (ingressEvent?.type === "session_closed" || ingressEvent?.type === "session_terminated") {
4671
+ const reason =
4672
+ typeof ingressEvent.reason === "string" && ingressEvent.reason.length > 0
4673
+ ? ingressEvent.reason
4674
+ : ingressEvent.type;
4675
+ this.#settlePromptAfterHostClose(id, reason);
4676
+ return;
4677
+ }
4535
4678
  if (frame.type !== "hello" && frame.type !== "server_hello" && typeof frame.connectionId === "string")
4536
4679
  record.connectionId = frame.connectionId;
4537
4680
  // Ingress ordering is recorded before queued work begins.
@@ -4813,7 +4956,18 @@ export class AcpAgent implements Agent {
4813
4956
  // Failure diagnostics are useful but advisory. Settle before any mapped
4814
4957
  // session update can await a backpressured client transport; otherwise an
4815
4958
  // already-decided failure can still lose to the inactivity watchdog.
4816
- this.#settlePrompt(id, record, activePrompt);
4959
+ // A terminal that races an in-flight cancel must not settle the public prompt before
4960
+ // the cancel request proves its outcome. Otherwise a follow-up prompt can be admitted
4961
+ // between the stale terminal settlement and the abort acknowledgement, leaving the
4962
+ // cancelled background request reported as `end_turn`.
4963
+ const cancelSettlementPending =
4964
+ activePrompt.cancelAttempt !== undefined &&
4965
+ !activePrompt.cancelAcknowledged &&
4966
+ record.statusRecovery !== activePrompt &&
4967
+ !this.#ownsUncertainAbort(record, activePrompt);
4968
+ if (!cancelSettlementPending) {
4969
+ this.#settlePrompt(id, record, activePrompt);
4970
+ }
4817
4971
  }
4818
4972
  if (!isTerminal && derivedCorrelation === undefined) return;
4819
4973
  if (!isTerminal && hasCorrelation(correlation)) {
@@ -5202,7 +5356,9 @@ export class AcpAgent implements Agent {
5202
5356
  const { outcome } = waiter.terminal;
5203
5357
  this.#rememberSettledPromptCorrelation(id, record, waiter.correlation);
5204
5358
  if (outcome.kind === "stopped") {
5205
- waiter.resolve({ stopReason: outcome.reason });
5359
+ // Once the SDK has acknowledged a client cancel, the cancellation owns the
5360
+ // turn even if a stale terminal frame races in with another stopped reason.
5361
+ waiter.resolve({ stopReason: waiter.cancelAcknowledged ? "cancelled" : outcome.reason });
5206
5362
  return;
5207
5363
  }
5208
5364
  // `phase` is the host's own claim, and a host that omits it leaves `terminalOutcome`
@@ -1,6 +1,7 @@
1
1
  import { Editor, type KeyId, matchesKey, parseKittySequence } from "@gajae-code/tui";
2
2
  import { BracketedPasteHandler } from "@gajae-code/tui/bracketed-paste";
3
3
  import { type AppKeybinding, KEYBINDINGS } from "../../config/keybindings";
4
+ import { macosOptionChordForComposedText } from "../utils/macos-option-forwarding";
4
5
 
5
6
  type ConfigurableEditorAction = Extract<
6
7
  AppKeybinding,
@@ -116,6 +117,12 @@ export class CustomEditor extends Editor {
116
117
  onQueue?: () => void;
117
118
  /** Called when Caps Lock is pressed. */
118
119
  onCapsLock?: () => void;
120
+ /**
121
+ * Called when typed text is what macOS composes for a bound Option chord
122
+ * (for example `œ` for a bound `alt+q`), meaning the terminal sent Option as
123
+ * text. The text is still inserted; the callback only explains the miss.
124
+ */
125
+ onUnforwardedOptionChord?: (key: KeyId, text: string) => void;
119
126
  /** Called when PageUp/PageDown should scroll the transcript viewport instead of prompt history. */
120
127
  onViewportPageScroll?: (direction: -1 | 1) => void;
121
128
  /** Called before regular composer input should return the transcript viewport to the live bottom. */
@@ -473,6 +480,13 @@ export class CustomEditor extends Editor {
473
480
  }
474
481
  }
475
482
 
483
+ if (this.onUnforwardedOptionChord) {
484
+ const optionKey = macosOptionChordForComposedText(data);
485
+ if (optionKey && (this.hasActionKey(optionKey) || this.#customKeyHandlers.has(optionKey))) {
486
+ this.onUnforwardedOptionChord(optionKey, data);
487
+ }
488
+ }
489
+
476
490
  // Pass to parent for normal handling.
477
491
  super.handleInput(data);
478
492
  }
@@ -20,6 +20,7 @@
20
20
  */
21
21
  import { Container, matchesKey, type SelectItem, SelectList, Text } from "@gajae-code/tui";
22
22
  import { sanitizeText } from "@gajae-code/utils";
23
+ import { resolveGlobalUserSkillLinkTrust } from "../../../config/skill-settings-defaults";
23
24
  import { type CustomizationInventory, loadCustomizationInventory } from "../../../customization/inventory";
24
25
  import {
25
26
  removeHookFile,
@@ -47,6 +48,7 @@ import { ImportWizard } from "./import-wizard";
47
48
  /** Minimal settings slice the dashboard reads and writes. */
48
49
  export interface CustomizationSettingsSlice {
49
50
  get(key: string): unknown;
51
+ getGlobal?(key: string): unknown;
50
52
  set?(key: string, value: unknown): void;
51
53
  }
52
54
 
@@ -150,6 +152,16 @@ export class CustomizationDashboard extends Container {
150
152
  cwd: this.#cwd,
151
153
  home: this.#homeDir,
152
154
  policy: this.#skillPolicy(),
155
+ allowExternalUserSkillSymlinks: resolveGlobalUserSkillLinkTrust({
156
+ trustUserSkills:
157
+ typeof this.#settings?.getGlobal === "function"
158
+ ? (this.#settings.getGlobal("skills.trustUserSkills") as boolean | undefined)
159
+ : undefined,
160
+ enablePiUser:
161
+ typeof this.#settings?.getGlobal === "function"
162
+ ? (this.#settings.getGlobal("skills.enablePiUser") as boolean | undefined)
163
+ : undefined,
164
+ }),
153
165
  disabledExtensions: this.#getStringArray("disabledExtensions"),
154
166
  });
155
167
  }
@@ -59,7 +59,7 @@ export class ExtensionDashboard extends Container {
59
59
  async #init(): Promise<void> {
60
60
  const sm = this.settings ?? (await Settings.init());
61
61
  const disabledIds = sm ? ((sm.get("disabledExtensions") as string[]) ?? []) : [];
62
- this.#state = await createInitialState(this.cwd, disabledIds);
62
+ this.#state = await createInitialState(this.cwd, disabledIds, sm);
63
63
 
64
64
  // Calculate max visible items based on terminal height
65
65
  // Reserve ~10 lines for header, tabs, help text, borders
@@ -193,7 +193,7 @@ export class ExtensionDashboard extends Container {
193
193
 
194
194
  const sm = this.settings ?? Settings.instance;
195
195
  const disabledIds = sm ? ((sm.get("disabledExtensions") as string[]) ?? []) : [];
196
- const nextState = await refreshState(this.#state, this.cwd, disabledIds);
196
+ const nextState = await refreshState(this.#state, this.cwd, disabledIds, sm);
197
197
  if (refreshToken !== this.#refreshToken) return;
198
198
  this.#state = nextState;
199
199
 
@@ -14,6 +14,7 @@ import type { Skill } from "../../../capability/skill";
14
14
  import type { SlashCommand } from "../../../capability/slash-command";
15
15
  import type { CustomTool } from "../../../capability/tool";
16
16
  import type { SourceMeta } from "../../../capability/types";
17
+ import type { Settings } from "../../../config/settings";
17
18
  import {
18
19
  disableProvider,
19
20
  enableProvider,
@@ -43,7 +44,11 @@ export interface ExtensionSettingsManager {
43
44
  /**
44
45
  * Load all extensions from all capabilities.
45
46
  */
46
- export async function loadAllExtensions(cwd?: string, disabledIds?: string[]): Promise<Extension[]> {
47
+ export async function loadAllExtensions(
48
+ cwd?: string,
49
+ disabledIds?: string[],
50
+ settings?: Settings,
51
+ ): Promise<Extension[]> {
47
52
  const extensions: Extension[] = [];
48
53
  const disabledExtensions = new Set<string>(disabledIds ?? []);
49
54
 
@@ -97,7 +102,7 @@ export async function loadAllExtensions(cwd?: string, disabledIds?: string[]): P
97
102
  }
98
103
  }
99
104
 
100
- const loadOpts = cwd ? { cwd, includeDisabled: true } : { includeDisabled: true };
105
+ const loadOpts = cwd ? { cwd, includeDisabled: true, settings } : { includeDisabled: true, settings };
101
106
 
102
107
  // Load skills
103
108
  try {
@@ -553,8 +558,12 @@ export function applyDisabledExtensionsToState(state: DashboardState, disabledId
553
558
  /**
554
559
  * Create initial dashboard state.
555
560
  */
556
- export async function createInitialState(cwd?: string, disabledIds?: string[]): Promise<DashboardState> {
557
- const extensions = await loadAllExtensions(cwd, disabledIds);
561
+ export async function createInitialState(
562
+ cwd?: string,
563
+ disabledIds?: string[],
564
+ settings?: Settings,
565
+ ): Promise<DashboardState> {
566
+ const extensions = await loadAllExtensions(cwd, disabledIds, settings);
558
567
  const tabs = buildProviderTabs(extensions);
559
568
  const tabFiltered = extensions; // "all" tab by default
560
569
  const searchFiltered = tabFiltered;
@@ -592,8 +601,9 @@ export async function refreshState(
592
601
  state: DashboardState,
593
602
  cwd?: string,
594
603
  disabledIds?: string[],
604
+ settings?: Settings,
595
605
  ): Promise<DashboardState> {
596
- const extensions = await loadAllExtensions(cwd, disabledIds);
606
+ const extensions = await loadAllExtensions(cwd, disabledIds, settings);
597
607
  const tabs = buildProviderTabs(extensions);
598
608
 
599
609
  // Get current provider from tabs
@@ -1369,10 +1369,7 @@ export class ModelSelectorComponent extends Container {
1369
1369
  }
1370
1370
 
1371
1371
  #getProfileAvailableModels(): Model[] {
1372
- const getAvailableForProfileActivation = this.#modelRegistry.getAvailableForProfileActivation;
1373
- return typeof getAvailableForProfileActivation === "function"
1374
- ? getAvailableForProfileActivation.call(this.#modelRegistry)
1375
- : this.#modelRegistry.getAvailable();
1372
+ return this.#modelRegistry.getAvailable();
1376
1373
  }
1377
1374
 
1378
1375
  #createProfileResolutionRegistry(availableModels: readonly Model[]) {
@@ -30,7 +30,7 @@ const PROVIDER_ONBOARDING_OPTIONS: ProviderOnboardingOption[] = [
30
30
  },
31
31
  {
32
32
  label: "Import existing credentials",
33
- description: "Detect and import Claude Code / Codex CLI logins already on this machine.",
33
+ description: "Detect and import Claude Code / Codex CLI / Kiro CLI logins already on this machine.",
34
34
  action: "import-credentials",
35
35
  },
36
36
  ];
@@ -109,6 +109,10 @@ const modelSegment: StatusLineSegment = {
109
109
  const modelName = shortenModelId(state.model?.id ?? state.model?.name);
110
110
 
111
111
  let content = withIcon(theme.icon.model, modelName);
112
+ const unavailableProfile = ctx.session.getUnavailableModelProfile?.();
113
+ if (unavailableProfile) {
114
+ content = `${theme.fg("warning", `profile unavailable: ${sanitizeStatusText(unavailableProfile)}`)} ${content}`;
115
+ }
112
116
 
113
117
  if (ctx.session.isFastModeActive() && theme.icon.fast) {
114
118
  content += ` ${theme.icon.fast}`;
@@ -60,6 +60,8 @@ export const __eventControllerPerfCounters = {
60
60
  const COMPLETION_NOTIFY_COMMAND_TIMEOUT_MS = 10_000;
61
61
  const TEXTLESS_LENGTH_WARNING =
62
62
  'Response ended with stopReason "length" before producing visible text or a tool call. Check the model\'s output and context token limits (maxTokens in models.yml) or lower the reasoning level (/reasoning), then retry.';
63
+ const TEXTLESS_INCOMPLETE_WARNING =
64
+ "Response ended before producing visible text or a tool call. See the provider diagnostic for the reason.";
63
65
 
64
66
  function isTextlessLengthStop(message: AssistantMessage | undefined): boolean {
65
67
  return (
@@ -1090,7 +1092,7 @@ export class EventController {
1090
1092
  event.stopReason !== "maintenance" &&
1091
1093
  isTextlessLengthStop(lastAssistant)
1092
1094
  ) {
1093
- this.ctx.showWarning(TEXTLESS_LENGTH_WARNING);
1095
+ this.ctx.showWarning(lastAssistant?.errorMessage ? TEXTLESS_INCOMPLETE_WARNING : TEXTLESS_LENGTH_WARNING);
1094
1096
  }
1095
1097
  this.sendCompletionNotification();
1096
1098
  this.ctx.promptSuggestion?.onAgentEnd();
@@ -1372,15 +1374,20 @@ export class EventController {
1372
1374
 
1373
1375
  const sessionName = this.ctx.sessionManager.getSessionName();
1374
1376
  const textlessLengthStop = isTextlessLengthStop(last);
1377
+ const incompleteTitle = last?.errorMessage ? "Response incomplete" : "Output limit reached";
1375
1378
  const title = textlessLengthStop
1376
1379
  ? sessionName
1377
- ? `${sessionName}: Output limit reached`
1378
- : "Output limit reached"
1380
+ ? `${sessionName}: ${incompleteTitle}`
1381
+ : incompleteTitle
1379
1382
  : sessionName
1380
1383
  ? `${sessionName}: Complete`
1381
1384
  : "Complete";
1382
1385
  const summary = summaryFromMessage(last, 1000);
1383
- const body = textlessLengthStop ? TEXTLESS_LENGTH_WARNING : (summary ?? "Complete");
1386
+ const body = textlessLengthStop
1387
+ ? last?.errorMessage
1388
+ ? TEXTLESS_INCOMPLETE_WARNING
1389
+ : TEXTLESS_LENGTH_WARNING
1390
+ : (summary ?? "Complete");
1384
1391
  const sessionManager = this.ctx.sessionManager as { getCwd?: () => string; getSessionId?: () => string };
1385
1392
  const payload: CompletionNotifyPayload = {
1386
1393
  type: "agent-turn-complete",