@sayknow-cli/coding-agent 0.3.7 → 0.3.9

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 (272) hide show
  1. package/dist/types/append-only-mode.d.ts +31 -0
  2. package/dist/types/capability/rule.d.ts +8 -0
  3. package/dist/types/cli/completion-cli.d.ts +41 -0
  4. package/dist/types/cli/config-cli.d.ts +1 -0
  5. package/dist/types/cli/fixture-report.d.ts +20 -0
  6. package/dist/types/cli/fixture-report.test.d.ts +1 -0
  7. package/dist/types/cli/nofile-limit.d.ts +11 -0
  8. package/dist/types/cli/notify-cli.d.ts +7 -0
  9. package/dist/types/cli/update-cli.d.ts +22 -0
  10. package/dist/types/cli.d.ts +124 -1
  11. package/dist/types/commands/completion.d.ts +29 -0
  12. package/dist/types/commands/config.d.ts +3 -0
  13. package/dist/types/commands/deep-interview.d.ts +3 -0
  14. package/dist/types/config/file-lock.d.ts +7 -6
  15. package/dist/types/config/keybindings.d.ts +2 -2
  16. package/dist/types/config/model-profile-activation.d.ts +6 -0
  17. package/dist/types/config/model-profiles.d.ts +1 -0
  18. package/dist/types/config/settings-schema.d.ts +119 -1
  19. package/dist/types/config/settings.d.ts +8 -0
  20. package/dist/types/discovery/index.d.ts +1 -0
  21. package/dist/types/export/html/template.generated.d.ts +1 -1
  22. package/dist/types/export/ttsr.d.ts +14 -3
  23. package/dist/types/extensibility/extensions/runner.d.ts +2 -1
  24. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  25. package/dist/types/goals/runtime.d.ts +1 -0
  26. package/dist/types/hooks/native-skill-hook.d.ts +12 -0
  27. package/dist/types/main.d.ts +4 -1
  28. package/dist/types/modes/components/assistant-message.d.ts +5 -1
  29. package/dist/types/modes/components/model-selector.d.ts +4 -0
  30. package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
  31. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  32. package/dist/types/modes/components/status-line.d.ts +9 -0
  33. package/dist/types/modes/components/thinking-selector.d.ts +2 -2
  34. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  35. package/dist/types/modes/controllers/event-controller.d.ts +5 -0
  36. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  37. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  38. package/dist/types/modes/interactive-mode.d.ts +4 -0
  39. package/dist/types/modes/print-mode.d.ts +13 -1
  40. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
  41. package/dist/types/modes/rpc/rpc-mode.d.ts +1 -3
  42. package/dist/types/modes/rpc/rpc-socket-security.d.ts +3 -0
  43. package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
  44. package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
  45. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
  46. package/dist/types/modes/types.d.ts +3 -0
  47. package/dist/types/modes/utils/injected-user-submission.d.ts +52 -0
  48. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  49. package/dist/types/notifications/config-commands.d.ts +9 -0
  50. package/dist/types/notifications/config.d.ts +30 -1
  51. package/dist/types/notifications/daemon-paths.d.ts +1 -0
  52. package/dist/types/notifications/index.d.ts +8 -3
  53. package/dist/types/notifications/lifecycle-commands.d.ts +12 -3
  54. package/dist/types/notifications/lifecycle-control-runtime.d.ts +2 -1
  55. package/dist/types/notifications/reply-sent-store.d.ts +53 -0
  56. package/dist/types/notifications/rich-draft.d.ts +68 -0
  57. package/dist/types/notifications/rich-render.d.ts +90 -0
  58. package/dist/types/notifications/telegram-daemon.d.ts +45 -3
  59. package/dist/types/notifications/telegram-reference.d.ts +7 -5
  60. package/dist/types/notifications/threaded-render.d.ts +14 -0
  61. package/dist/types/runtime-api-key.d.ts +7 -0
  62. package/dist/types/sdk.d.ts +3 -0
  63. package/dist/types/session/agent-session.d.ts +61 -6
  64. package/dist/types/session/agent-storage.d.ts +1 -0
  65. package/dist/types/session/file-mention-pruning.d.ts +8 -0
  66. package/dist/types/session/messages.d.ts +4 -0
  67. package/dist/types/session/session-manager.d.ts +17 -1
  68. package/dist/types/skc-runtime/deep-interview-ambiguity.d.ts +39 -0
  69. package/dist/types/skc-runtime/deep-interview-recorder.d.ts +10 -1
  70. package/dist/types/skc-runtime/deep-interview-state.d.ts +17 -2
  71. package/dist/types/skc-runtime/session-state-sidecar.d.ts +7 -0
  72. package/dist/types/skc-runtime/state-writer.d.ts +3 -0
  73. package/dist/types/skc-runtime/tmux-common.d.ts +1 -0
  74. package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
  75. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +120 -13
  76. package/dist/types/skill-state/active-state.d.ts +18 -1
  77. package/dist/types/system-prompt.d.ts +18 -2
  78. package/dist/types/task/executor.d.ts +3 -0
  79. package/dist/types/task/fork-context-budget.d.ts +7 -0
  80. package/dist/types/task/token-log.d.ts +30 -0
  81. package/dist/types/task/token-log.test.d.ts +1 -0
  82. package/dist/types/tools/bisect.d.ts +81 -0
  83. package/dist/types/tools/computer.d.ts +2 -2
  84. package/dist/types/tools/index.d.ts +1 -0
  85. package/dist/types/tools/search.d.ts +2 -2
  86. package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
  87. package/dist/types/utils/file-mentions.d.ts +14 -0
  88. package/dist/types/utils/git.d.ts +32 -0
  89. package/dist/types/utils/pasted-image-path.d.ts +30 -0
  90. package/dist/types/web/scrapers/youtube.d.ts +8 -0
  91. package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
  92. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
  93. package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
  94. package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
  95. package/package.json +8 -8
  96. package/src/append-only-mode.ts +58 -0
  97. package/src/autoresearch/prompt.md +1 -1
  98. package/src/autoresearch/tools/log-experiment.ts +1 -1
  99. package/src/capability/rule.ts +9 -0
  100. package/src/cli/completion-cli.ts +234 -0
  101. package/src/cli/config-cli.ts +61 -9
  102. package/src/cli/fast-help.ts +1 -0
  103. package/src/cli/fixture-report.test.ts +230 -0
  104. package/src/cli/fixture-report.ts +171 -0
  105. package/src/cli/nofile-limit.ts +48 -0
  106. package/src/cli/notify-cli.ts +103 -17
  107. package/src/cli/telegram-cli.ts +1 -1
  108. package/src/cli/update-cli.ts +148 -33
  109. package/src/cli.ts +29 -1
  110. package/src/commands/completion.ts +84 -0
  111. package/src/commands/config.ts +2 -0
  112. package/src/commands/contribution-prep.ts +1 -1
  113. package/src/commands/deep-interview.ts +2 -1
  114. package/src/config/file-lock.ts +73 -34
  115. package/src/config/keybindings.ts +3 -3
  116. package/src/config/model-profile-activation.ts +65 -0
  117. package/src/config/model-profiles.ts +31 -2
  118. package/src/config/settings-schema.ts +76 -1
  119. package/src/config/settings.ts +22 -0
  120. package/src/defaults/skc/skills/team/SKILL.md +17 -14
  121. package/src/defaults/skc/skills/ultragoal/SKILL.md +59 -4
  122. package/src/defaults/skc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
  123. package/src/defaults/skc-defaults.ts +9 -0
  124. package/src/discovery/helpers.ts +10 -0
  125. package/src/discovery/index.ts +1 -0
  126. package/src/export/html/template.generated.ts +1 -1
  127. package/src/export/html/template.js +60 -2
  128. package/src/export/ttsr.ts +96 -18
  129. package/src/extensibility/extensions/runner.ts +183 -219
  130. package/src/extensibility/extensions/types.ts +9 -0
  131. package/src/extensibility/skc-plugins/runtime-adapters.ts +156 -44
  132. package/src/goals/runtime.ts +10 -2
  133. package/src/hindsight/backend.ts +1 -3
  134. package/src/hooks/native-skill-hook.ts +124 -4
  135. package/src/internal-urls/docs-index.generated.ts +17 -8
  136. package/src/lsp/client.ts +1 -0
  137. package/src/main.ts +95 -43
  138. package/src/memories/index.ts +12 -1
  139. package/src/modes/components/assistant-message.ts +241 -82
  140. package/src/modes/components/custom-editor.ts +1 -0
  141. package/src/modes/components/footer.ts +7 -2
  142. package/src/modes/components/model-selector.ts +54 -11
  143. package/src/modes/components/queued-message-selector.ts +92 -0
  144. package/src/modes/components/settings-selector.ts +4 -0
  145. package/src/modes/components/status-line.ts +167 -54
  146. package/src/modes/components/thinking-selector.ts +10 -8
  147. package/src/modes/components/tool-execution.ts +44 -19
  148. package/src/modes/components/welcome.ts +181 -44
  149. package/src/modes/controllers/command-controller.ts +40 -4
  150. package/src/modes/controllers/event-controller.ts +77 -49
  151. package/src/modes/controllers/extension-ui-controller.ts +12 -11
  152. package/src/modes/controllers/input-controller.ts +192 -35
  153. package/src/modes/controllers/selector-controller.ts +168 -24
  154. package/src/modes/interactive-mode.ts +56 -16
  155. package/src/modes/print-mode.ts +43 -4
  156. package/src/modes/prompt-action-autocomplete.ts +21 -20
  157. package/src/modes/rpc/rpc-mode.ts +32 -12
  158. package/src/modes/rpc/rpc-socket-security.ts +13 -1
  159. package/src/modes/rpc/rpc-types.ts +9 -0
  160. package/src/modes/shared/agent-wire/command-validation.ts +4 -0
  161. package/src/modes/shared/agent-wire/event-contract.ts +22 -0
  162. package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
  163. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  164. package/src/modes/types.ts +3 -0
  165. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  166. package/src/modes/utils/injected-user-submission.ts +94 -0
  167. package/src/modes/utils/ui-helpers.ts +20 -7
  168. package/src/notifications/config-commands.ts +20 -0
  169. package/src/notifications/config.ts +52 -5
  170. package/src/notifications/daemon-paths.ts +2 -0
  171. package/src/notifications/index.ts +144 -18
  172. package/src/notifications/lifecycle-commands.ts +74 -23
  173. package/src/notifications/lifecycle-control-runtime.ts +8 -3
  174. package/src/notifications/lifecycle-orchestrator.ts +19 -6
  175. package/src/notifications/reply-sent-store.ts +134 -0
  176. package/src/notifications/rich-draft.ts +107 -0
  177. package/src/notifications/rich-render.ts +142 -0
  178. package/src/notifications/telegram-daemon-control.ts +5 -3
  179. package/src/notifications/telegram-daemon.ts +606 -98
  180. package/src/notifications/telegram-reference.ts +17 -16
  181. package/src/notifications/threaded-render.ts +45 -2
  182. package/src/prompts/agents/architect.md +19 -10
  183. package/src/prompts/agents/critic.md +16 -18
  184. package/src/prompts/agents/executor.md +14 -11
  185. package/src/prompts/agents/planner.md +18 -13
  186. package/src/prompts/ci-green-request.md +3 -8
  187. package/src/prompts/goals/goal-continuation.md +0 -5
  188. package/src/prompts/goals/goal-mode-active.md +0 -5
  189. package/src/prompts/system/agent-creation-architect.md +2 -2
  190. package/src/prompts/system/plan-mode-active.md +1 -1
  191. package/src/prompts/system/plan-mode-subagent.md +3 -3
  192. package/src/prompts/system/project-prompt.md +0 -13
  193. package/src/prompts/system/subagent-system-prompt.md +3 -2
  194. package/src/prompts/system/system-prompt.md +29 -17
  195. package/src/prompts/system/volatile-project-context.md +10 -0
  196. package/src/prompts/system/web-search.md +4 -7
  197. package/src/prompts/tools/apply-patch.md +6 -0
  198. package/src/prompts/tools/ast-edit.md +1 -0
  199. package/src/prompts/tools/bash.md +1 -1
  200. package/src/prompts/tools/bisect.md +22 -0
  201. package/src/prompts/tools/browser.md +1 -1
  202. package/src/prompts/tools/computer.md +2 -2
  203. package/src/prompts/tools/cron.md +0 -2
  204. package/src/prompts/tools/eval.md +2 -2
  205. package/src/prompts/tools/find.md +1 -1
  206. package/src/prompts/tools/github.md +6 -6
  207. package/src/prompts/tools/goal.md +1 -3
  208. package/src/prompts/tools/image-gen.md +12 -5
  209. package/src/prompts/tools/irc.md +5 -4
  210. package/src/prompts/tools/job.md +2 -2
  211. package/src/prompts/tools/lsp.md +1 -1
  212. package/src/prompts/tools/monitor.md +2 -2
  213. package/src/prompts/tools/patch.md +2 -2
  214. package/src/prompts/tools/replace.md +2 -21
  215. package/src/prompts/tools/rewind.md +1 -1
  216. package/src/prompts/tools/search-tool-bm25.md +6 -11
  217. package/src/prompts/tools/search.md +1 -1
  218. package/src/prompts/tools/skill.md +3 -2
  219. package/src/prompts/tools/ssh.md +2 -1
  220. package/src/prompts/tools/subagent.md +2 -2
  221. package/src/prompts/tools/task.md +4 -5
  222. package/src/prompts/tools/todo-write.md +1 -1
  223. package/src/prompts/tools/vim.md +1 -0
  224. package/src/prompts/tools/web-search.md +2 -0
  225. package/src/prompts/tools/write.md +8 -2
  226. package/src/runtime-api-key.ts +16 -0
  227. package/src/sdk.ts +88 -36
  228. package/src/session/agent-session.ts +1013 -141
  229. package/src/session/agent-storage.ts +4 -0
  230. package/src/session/file-mention-pruning.ts +64 -0
  231. package/src/session/messages.ts +4 -0
  232. package/src/session/session-manager.ts +103 -4
  233. package/src/skc-runtime/deep-interview-ambiguity.ts +206 -0
  234. package/src/skc-runtime/deep-interview-recorder.ts +62 -8
  235. package/src/skc-runtime/deep-interview-state.ts +44 -2
  236. package/src/skc-runtime/session-state-sidecar.ts +263 -20
  237. package/src/skc-runtime/state-runtime.ts +12 -2
  238. package/src/skc-runtime/state-writer.ts +15 -0
  239. package/src/skc-runtime/tmux-common.ts +1 -0
  240. package/src/skc-runtime/ultragoal-guard.ts +55 -66
  241. package/src/skc-runtime/ultragoal-receipt-freshness.ts +315 -0
  242. package/src/skc-runtime/ultragoal-runtime.ts +1672 -159
  243. package/src/skc-runtime/workflow-manifest.generated.json +113 -2
  244. package/src/skc-runtime/workflow-manifest.ts +74 -1
  245. package/src/skill-state/active-state.ts +141 -5
  246. package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
  247. package/src/slash-commands/builtin-registry.ts +296 -72
  248. package/src/system-prompt.ts +57 -16
  249. package/src/task/executor.ts +132 -24
  250. package/src/task/fork-context-advisory.ts +2 -7
  251. package/src/task/fork-context-budget.ts +9 -0
  252. package/src/task/index.ts +13 -6
  253. package/src/task/output-manager.ts +20 -8
  254. package/src/task/receipt.ts +26 -7
  255. package/src/task/token-log.test.ts +188 -0
  256. package/src/task/token-log.ts +179 -0
  257. package/src/tools/bisect.ts +367 -0
  258. package/src/tools/computer.ts +246 -43
  259. package/src/tools/index.ts +3 -0
  260. package/src/tools/output-meta.ts +116 -4
  261. package/src/tools/search.ts +8 -2
  262. package/src/tools/skill.ts +15 -0
  263. package/src/tools/telegram-send.ts +6 -0
  264. package/src/tools/ultragoal-ask-guard.ts +39 -5
  265. package/src/utils/file-mentions.ts +39 -7
  266. package/src/utils/git.ts +73 -0
  267. package/src/utils/pasted-image-path.ts +170 -0
  268. package/src/web/scrapers/youtube.ts +24 -12
  269. package/src/workflow/workflow-intent-diff.ts +29 -6
  270. package/src/workflow/workflow-intent-report-consumer.ts +221 -0
  271. package/src/workflow/workflow-intent-report-schema.ts +129 -0
  272. package/src/workflow/workflow-intent-report.ts +135 -0
@@ -24,6 +24,7 @@ import { HookSelectorComponent } from "../../modes/components/hook-selector";
24
24
  import { getAvailableThemesWithPaths, getThemeByName, setTheme, type Theme, theme } from "../../modes/theme/theme";
25
25
  import type { InteractiveModeContext } from "../../modes/types";
26
26
  import { setSessionTerminalTitle, setTerminalTitle } from "../../utils/title-generator";
27
+ import { applyInjectedUserSubmission } from "../utils/injected-user-submission";
27
28
  import { classifyHookSelectorBellEvent, ringTerminalBell } from "../utils/terminal-bell";
28
29
 
29
30
  const MAX_WIDGET_LINES = 10;
@@ -192,9 +193,8 @@ export class ExtensionUiController {
192
193
  this.ctx.streamingMessage = undefined;
193
194
  this.ctx.pendingTools.clear();
194
195
 
195
- this.ctx.chatContainer.addChild(new Spacer(1));
196
196
  this.ctx.chatContainer.addChild(
197
- new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
197
+ new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 0),
198
198
  );
199
199
  await this.ctx.reloadTodos();
200
200
  this.ctx.ui.requestRender();
@@ -300,15 +300,14 @@ export class ExtensionUiController {
300
300
  }
301
301
 
302
302
  #rebuildHookWidgets(): void {
303
- this.#renderHookWidgetContainer(this.ctx.hookWidgetContainerAbove, this.#hookWidgetsAbove, true, true);
304
- this.#renderHookWidgetContainer(this.ctx.hookWidgetContainerBelow, this.#hookWidgetsBelow, false, false);
303
+ this.#renderHookWidgetContainer(this.ctx.hookWidgetContainerAbove, this.#hookWidgetsAbove, true);
304
+ this.#renderHookWidgetContainer(this.ctx.hookWidgetContainerBelow, this.#hookWidgetsBelow, false);
305
305
  this.ctx.ui.requestRender();
306
306
  }
307
307
 
308
308
  #renderHookWidgetContainer(
309
309
  container: Container,
310
310
  widgets: Map<string, ExtensionUiComponent>,
311
- spacerWhenEmpty: boolean,
312
311
  leadingSpacer: boolean,
313
312
  ): void {
314
313
  // Detach (not dispose): hook widgets are persistent instances owned by the
@@ -318,9 +317,6 @@ export class ExtensionUiController {
318
317
  container.detachAll();
319
318
 
320
319
  if (widgets.size === 0) {
321
- if (spacerWhenEmpty) {
322
- container.addChild(new Spacer(1));
323
- }
324
320
  return;
325
321
  }
326
322
 
@@ -435,9 +431,8 @@ export class ExtensionUiController {
435
431
  this.ctx.streamingMessage = undefined;
436
432
  this.ctx.pendingTools.clear();
437
433
 
438
- this.ctx.chatContainer.addChild(new Spacer(1));
439
434
  this.ctx.chatContainer.addChild(
440
- new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
435
+ new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 0),
441
436
  );
442
437
  await this.ctx.reloadTodos();
443
438
  this.ctx.ui.requestRender();
@@ -960,7 +955,13 @@ export class ExtensionUiController {
960
955
  }
961
956
 
962
957
  #sendExtensionUserMessage: SendUserMessageHandler = (content, options) => {
963
- this.ctx.session.sendUserMessage(content, options).catch((err: unknown) => {
958
+ // Compute queued BEFORE send: prompt() may flip session.isStreaming synchronously.
959
+ const queued = Boolean(options?.deliverAs) || this.ctx.session.isStreaming;
960
+ // Call send first so the busy/queued path finds the session queue populated
961
+ // (queueSteer/queueFollowUp push synchronously) before refreshing pending display.
962
+ const send = this.ctx.session.sendUserMessage(content, options);
963
+ applyInjectedUserSubmission(this.ctx, { content, queued });
964
+ send.catch((err: unknown) => {
964
965
  this.ctx.showError(`Extension sendUserMessage failed: ${err instanceof Error ? err.message : String(err)}`);
965
966
  });
966
967
  };
@@ -11,22 +11,22 @@ import { createPromptActionAutocompleteProvider } from "../../modes/prompt-actio
11
11
  import { theme } from "../../modes/theme/theme";
12
12
  import { scrollTmuxToPreviousUserInput as scrollTmuxPaneToPreviousUserInput } from "../../modes/tmux-scroll";
13
13
  import type { InteractiveModeContext } from "../../modes/types";
14
- import type { AgentSessionEvent } from "../../session/agent-session";
14
+ import type { AgentSessionEvent, QueuedMessageEditEntry } from "../../session/agent-session";
15
15
  import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
16
16
  import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registry";
17
17
  import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard";
18
18
  import { getEditorCommand, openInEditor } from "../../utils/external-editor";
19
19
  import { ensureSupportedImageInput, ImageInputTooLargeError, loadImageInput } from "../../utils/image-loading";
20
20
  import { resizeImage } from "../../utils/image-resize";
21
+ import { resolvePastedImagePath } from "../../utils/pasted-image-path";
21
22
  import { generateSessionTitle, setSessionTerminalTitle } from "../../utils/title-generator";
23
+ import { type QueuedMessageMoveDirection, QueuedMessageSelectorComponent } from "../components/queued-message-selector";
22
24
 
23
25
  interface Expandable {
24
26
  setExpanded(expanded: boolean): void;
25
27
  }
26
28
 
27
29
  const INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS = 5_000;
28
- const CLIPBOARD_TEMP_IMAGE_FILE_PATTERN = /^clipboard-\d{4}-\d{2}-\d{2}-\d{6}-[A-Za-z0-9]+\.(?:png|jpe?g|gif|webp)$/i;
29
- const MACOS_CLIPBOARD_TEMP_DIR_PATTERN = /^\/var\/folders\/[^/]+\/[^/]+\/T$/;
30
30
 
31
31
  function isExpandable(obj: unknown): obj is Expandable {
32
32
  return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
@@ -313,14 +313,7 @@ export class InputController {
313
313
  this.ctx.editor.onDequeue = () => this.handleDequeue();
314
314
  this.ctx.editor.setActionKeys("app.message.queue", this.ctx.keybindings.getKeys("app.message.queue"));
315
315
  this.ctx.editor.onQueue = () => void this.handleQueueSubmit();
316
- this.ctx.editor.onTab = () => {
317
- if (!this.ctx.session.isStreaming && !this.ctx.session.isCompacting) return false;
318
- void this.handleQueueSubmit();
319
- return true;
320
- };
321
- this.ctx.editor.onTabDeclined = () => {
322
- if (this.ctx.session.isStreaming || this.ctx.session.isCompacting) void this.handleQueueSubmit();
323
- };
316
+
324
317
  this.ctx.editor.onViewportPageScroll = direction => this.ctx.ui.scrollViewportPages(direction);
325
318
  this.ctx.editor.onViewportFollowLive = () => {
326
319
  this.ctx.ui.followLiveViewport();
@@ -614,12 +607,182 @@ export class InputController {
614
607
  }
615
608
 
616
609
  handleDequeue(): void {
617
- const restored = this.restoreLatestQueuedMessageToEditor();
618
- if (restored === 0) {
610
+ const entries = this.#getEditableQueuedMessages();
611
+ if (entries.length === 0) {
612
+ this.ctx.updatePendingMessagesDisplay();
619
613
  this.ctx.showStatus("No queued messages to restore");
620
- } else {
621
- this.ctx.showStatus(`Restored ${restored} queued message${restored > 1 ? "s" : ""} to editor`);
614
+ return;
615
+ }
616
+ if (entries.length === 1) {
617
+ const restored = this.#restoreQueuedMessageToEditor(entries[0]);
618
+ this.ctx.showStatus(
619
+ restored === 0 ? "Queued message is no longer available" : "Restored queued message to editor",
620
+ );
621
+ return;
622
+ }
623
+ this.#showQueuedMessageSelector(entries, this.#newestQueuedMessageIndex(entries));
624
+ }
625
+
626
+ #compactionQueuedMessageId(index: number): string {
627
+ return `compaction:${index}`;
628
+ }
629
+
630
+ #getEditableQueuedMessages(): QueuedMessageEditEntry[] {
631
+ const compactionEntries = this.ctx.compactionQueuedMessages.map((entry, index): QueuedMessageEditEntry => {
632
+ const label = entry.mode === "steer" ? "Steer" : "Queued";
633
+ return {
634
+ id: this.#compactionQueuedMessageId(index),
635
+ text: entry.text,
636
+ mode: entry.mode,
637
+ label,
638
+ };
639
+ });
640
+ return [...compactionEntries, ...this.ctx.session.getQueuedMessageEntries()];
641
+ }
642
+
643
+ #queuedMessageStableSequence(entry: QueuedMessageEditEntry): number | undefined {
644
+ const [mode, sequenceText] = entry.id.split(":");
645
+ if ((mode !== "steer" && mode !== "followUp") || sequenceText === undefined) return undefined;
646
+ const sequence = Number(sequenceText);
647
+ return Number.isInteger(sequence) ? sequence : undefined;
648
+ }
649
+
650
+ #newestQueuedMessageIndex(entries: QueuedMessageEditEntry[]): number {
651
+ let selectedIndex = entries.length - 1;
652
+ let newestSequence = Number.NEGATIVE_INFINITY;
653
+ for (let index = 0; index < entries.length; index += 1) {
654
+ const sequence = this.#queuedMessageStableSequence(entries[index]);
655
+ if (sequence !== undefined && sequence > newestSequence) {
656
+ newestSequence = sequence;
657
+ selectedIndex = index;
658
+ }
659
+ }
660
+ return Math.max(0, selectedIndex);
661
+ }
662
+
663
+ #restoreEditorFocus(): void {
664
+ this.ctx.editorContainer.clear();
665
+ this.ctx.editorContainer.addChild(this.ctx.editor);
666
+ this.ctx.ui.setFocus(this.ctx.editor);
667
+ }
668
+
669
+ #showQueuedMessageSelector(entries: QueuedMessageEditEntry[], selectedIndex = 0): void {
670
+ const selector = new QueuedMessageSelectorComponent(
671
+ entries,
672
+ entry => {
673
+ const restored = this.#restoreQueuedMessageToEditor(entry);
674
+ this.#restoreEditorFocus();
675
+ this.ctx.showStatus(
676
+ restored === 0 ? "Queued message is no longer available" : "Restored queued message to editor",
677
+ );
678
+ this.ctx.ui.requestRender();
679
+ },
680
+ (entry, index) => {
681
+ const deleted = this.#deleteQueuedMessage(entry);
682
+ const nextEntries = this.#getEditableQueuedMessages();
683
+ if (nextEntries.length === 0) {
684
+ this.#restoreEditorFocus();
685
+ this.ctx.showStatus(deleted ? "Deleted queued message" : "Queued message is no longer available");
686
+ this.ctx.ui.requestRender();
687
+ return;
688
+ }
689
+ this.ctx.showStatus(deleted ? "Deleted queued message" : "Queued message is no longer available");
690
+ this.#showQueuedMessageSelector(nextEntries, Math.min(index, nextEntries.length - 1));
691
+ },
692
+ (entry, index, direction) => {
693
+ const moved = this.#moveQueuedMessage(entry, direction);
694
+ const nextEntries = this.#getEditableQueuedMessages();
695
+ if (nextEntries.length === 0) {
696
+ this.#restoreEditorFocus();
697
+ this.ctx.showStatus("Queued message is no longer available");
698
+ this.ctx.ui.requestRender();
699
+ return;
700
+ }
701
+ const nextIndex = direction === "up" ? index - 1 : index + 1;
702
+ const selectedNextIndex = moved ? nextIndex : index;
703
+ this.ctx.showStatus(moved ? "Moved queued message" : "Queued message cannot move further");
704
+ this.#showQueuedMessageSelector(
705
+ nextEntries,
706
+ Math.max(0, Math.min(selectedNextIndex, nextEntries.length - 1)),
707
+ );
708
+ },
709
+ () => {
710
+ this.#restoreEditorFocus();
711
+ this.ctx.ui.requestRender();
712
+ },
713
+ { selectedIndex },
714
+ );
715
+ this.ctx.editorContainer.clear();
716
+ this.ctx.editorContainer.addChild(selector);
717
+ this.ctx.ui.setFocus(selector);
718
+ this.ctx.ui.requestRender();
719
+ }
720
+
721
+ #removeQueuedMessageForEditing(id: string): string | undefined {
722
+ const compactionPrefix = "compaction:";
723
+ if (id.startsWith(compactionPrefix)) {
724
+ const index = Number(id.slice(compactionPrefix.length));
725
+ if (!Number.isInteger(index)) return undefined;
726
+ const [entry] = this.ctx.compactionQueuedMessages.splice(index, 1);
727
+ return entry?.text;
622
728
  }
729
+ return this.ctx.session.removeQueuedMessageForEditing(id);
730
+ }
731
+ #parseCompactionQueuedMessageId(id: string): number | undefined {
732
+ const compactionPrefix = "compaction:";
733
+ if (!id.startsWith(compactionPrefix)) return undefined;
734
+ const index = Number(id.slice(compactionPrefix.length));
735
+ return Number.isInteger(index) ? index : undefined;
736
+ }
737
+
738
+ #moveCompactionQueuedMessage(index: number, direction: QueuedMessageMoveDirection): boolean {
739
+ const targetIndex = direction === "up" ? index - 1 : index + 1;
740
+ if (index < 0 || index >= this.ctx.compactionQueuedMessages.length) return false;
741
+ if (targetIndex < 0 || targetIndex >= this.ctx.compactionQueuedMessages.length) return false;
742
+ const current = this.ctx.compactionQueuedMessages[index];
743
+ const target = this.ctx.compactionQueuedMessages[targetIndex];
744
+ if (!current || !target) return false;
745
+ this.ctx.compactionQueuedMessages[index] = target;
746
+ this.ctx.compactionQueuedMessages[targetIndex] = current;
747
+ this.ctx.updatePendingMessagesDisplay();
748
+ return true;
749
+ }
750
+
751
+ #moveQueuedMessage(entry: QueuedMessageEditEntry, direction: QueuedMessageMoveDirection): boolean {
752
+ const compactionIndex = this.#parseCompactionQueuedMessageId(entry.id);
753
+ if (compactionIndex !== undefined) {
754
+ return this.#moveCompactionQueuedMessage(compactionIndex, direction);
755
+ }
756
+ const moved = this.ctx.session.moveQueuedMessageForEditing(entry.id, direction);
757
+ if (moved) {
758
+ this.ctx.updatePendingMessagesDisplay();
759
+ }
760
+ return moved;
761
+ }
762
+
763
+ #deleteQueuedMessage(entry: QueuedMessageEditEntry): boolean {
764
+ const queuedText = this.#removeQueuedMessageForEditing(entry.id);
765
+ this.ctx.updatePendingMessagesDisplay();
766
+ if (!queuedText) return false;
767
+ this.ctx.locallySubmittedUserSignatures.delete(`${queuedText}\u00000`);
768
+ return true;
769
+ }
770
+
771
+ #restoreQueuedMessageToEditor(entry: QueuedMessageEditEntry | undefined): number {
772
+ if (!entry) {
773
+ this.ctx.updatePendingMessagesDisplay();
774
+ return 0;
775
+ }
776
+ const queuedText = this.#removeQueuedMessageForEditing(entry.id);
777
+ if (!queuedText) {
778
+ this.ctx.updatePendingMessagesDisplay();
779
+ return 0;
780
+ }
781
+
782
+ this.ctx.locallySubmittedUserSignatures.delete(`${queuedText}\u00000`);
783
+ this.ctx.editor.setText(queuedText);
784
+ this.ctx.updatePendingMessagesDisplay();
785
+ return 1;
623
786
  }
624
787
 
625
788
  /**
@@ -766,7 +929,7 @@ export class InputController {
766
929
  return this.handleFollowUp();
767
930
  }
768
931
 
769
- restoreLatestQueuedMessageToEditor(options?: { currentText?: string }): number {
932
+ restoreLatestQueuedMessageToEditor(): number {
770
933
  const compactionQueued = this.ctx.compactionQueuedMessages.pop();
771
934
  const queuedText = compactionQueued?.text ?? this.ctx.session.popLastQueuedMessage();
772
935
  if (!queuedText) {
@@ -775,9 +938,7 @@ export class InputController {
775
938
  }
776
939
 
777
940
  this.ctx.locallySubmittedUserSignatures.delete(`${queuedText}\u00000`);
778
- const currentText = options?.currentText ?? this.ctx.editor.getText();
779
- const combinedText = [queuedText, currentText].filter(t => t.trim()).join("\n\n");
780
- this.ctx.editor.setText(combinedText);
941
+ this.ctx.editor.setText(queuedText);
781
942
  this.ctx.updatePendingMessagesDisplay();
782
943
  return 1;
783
944
  }
@@ -899,10 +1060,15 @@ export class InputController {
899
1060
  }
900
1061
 
901
1062
  handleTextPaste(text: string): boolean | Promise<boolean> {
902
- const imagePath = this.#getPastedImagePathCandidate(text);
1063
+ const imagePath = resolvePastedImagePath(text, { cwd: this.ctx.sessionManager.getCwd() });
903
1064
  return imagePath ? this.#attachPastedImagePath(imagePath) : false;
904
1065
  }
905
1066
 
1067
+ /**
1068
+ * Returns `false` on every failure path so the editor replays the original
1069
+ * bracketed paste — the raw path text must never be lost when attachment
1070
+ * is impossible (unsupported content, oversized image, load error).
1071
+ */
906
1072
  async #attachPastedImagePath(imagePath: string): Promise<boolean> {
907
1073
  try {
908
1074
  const image = await loadImageInput({
@@ -911,8 +1077,8 @@ export class InputController {
911
1077
  autoResize: this.ctx.settings.get("images.autoResize"),
912
1078
  });
913
1079
  if (!image) {
914
- this.ctx.showStatus("Unsupported pasted clipboard image file");
915
- return true;
1080
+ this.ctx.showStatus("Unsupported pasted image file");
1081
+ return false;
916
1082
  }
917
1083
 
918
1084
  this.ctx.pendingImages.push({
@@ -927,22 +1093,13 @@ export class InputController {
927
1093
  } catch (error) {
928
1094
  if (error instanceof ImageInputTooLargeError) {
929
1095
  this.ctx.showStatus(error.message);
930
- return true;
1096
+ return false;
931
1097
  }
932
- this.ctx.showStatus("Failed to attach pasted clipboard image");
933
- return true;
1098
+ this.ctx.showStatus("Failed to attach pasted image");
1099
+ return false;
934
1100
  }
935
1101
  }
936
1102
 
937
- #getPastedImagePathCandidate(text: string): string | undefined {
938
- const resolvedPath = path.resolve(text.trim());
939
- const parentDir = path.dirname(resolvedPath);
940
- const isClipboardTempPath =
941
- (parentDir === "/tmp" || MACOS_CLIPBOARD_TEMP_DIR_PATTERN.test(parentDir)) &&
942
- CLIPBOARD_TEMP_IMAGE_FILE_PATTERN.test(path.basename(resolvedPath));
943
- return isClipboardTempPath ? resolvedPath : undefined;
944
- }
945
-
946
1103
  #nextImagePlaceholder(): string {
947
1104
  return `[image ${this.ctx.pendingImages.length}]`;
948
1105
  }
@@ -1146,7 +1303,7 @@ export class InputController {
1146
1303
  // If streaming, re-add the streaming component with updated visibility and re-render
1147
1304
  if (this.ctx.streamingComponent && this.ctx.streamingMessage) {
1148
1305
  this.ctx.streamingComponent.setHideThinkingBlock(this.ctx.hideThinkingBlock);
1149
- this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage);
1306
+ this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage, { streaming: true });
1150
1307
  this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
1151
1308
  }
1152
1309
 
@@ -4,9 +4,13 @@ import type { OAuthProvider } from "@sayknow-cli/ai/utils/oauth/types";
4
4
  import type { Component, OverlayHandle } from "@sayknow-cli/tui";
5
5
  import { Input, Loader, Spacer, Text } from "@sayknow-cli/tui";
6
6
  import { getAgentDbPath, getProjectDir } from "@sayknow-cli/utils";
7
- import { activateModelProfile, materializeActiveModelProfileAssignment } from "../../config/model-profile-activation";
8
- import { recommendModelProfileForProvider } from "../../config/model-profiles";
9
- import { SKC_MODEL_ASSIGNMENT_TARGETS } from "../../config/model-registry";
7
+ import {
8
+ activateModelProfile,
9
+ materializeActiveModelProfileAssignment,
10
+ materializeActiveModelProfileAssignments,
11
+ } from "../../config/model-profile-activation";
12
+ import { formatModelProfileDisplayLabel, recommendModelProfileForProvider } from "../../config/model-profiles";
13
+ import { SKC_MODEL_ASSIGNMENT_TARGETS, type SkcModelAssignmentTargetId } from "../../config/model-registry";
10
14
  import { formatModelSelectorValue } from "../../config/model-resolver";
11
15
  import type { ModelProfileConfig } from "../../config/models-config-schema";
12
16
  import { type Settings, settings } from "../../config/settings";
@@ -77,6 +81,7 @@ import { SessionSelectorComponent } from "../components/session-selector";
77
81
  import { SettingsSelectorComponent } from "../components/settings-selector";
78
82
  import type { StatusLineSettings } from "../components/status-line";
79
83
  import { ThemeSelectorComponent } from "../components/theme-selector";
84
+ import { ThinkingSelectorComponent } from "../components/thinking-selector";
80
85
  import { ToolExecutionComponent } from "../components/tool-execution";
81
86
  import { TreeSelectorComponent } from "../components/tree-selector";
82
87
  import { UserMessageSelectorComponent } from "../components/user-message-selector";
@@ -324,6 +329,46 @@ export class SelectorController {
324
329
  });
325
330
  }
326
331
 
332
+ showEffortSelector(): void {
333
+ const availableLevels = [
334
+ ThinkingLevel.Inherit,
335
+ ThinkingLevel.Off,
336
+ ...this.ctx.session.getAvailableThinkingLevels(),
337
+ ];
338
+
339
+ this.showSelector(done => {
340
+ const selector = new ThinkingSelectorComponent(
341
+ this.ctx.session.thinkingLevel,
342
+ availableLevels,
343
+ level => {
344
+ done();
345
+
346
+ const configuredDefault = this.ctx.settings.get("defaultThinkingLevel");
347
+ const levelToApply = level === ThinkingLevel.Inherit ? configuredDefault : level;
348
+ this.ctx.session.setThinkingLevel(levelToApply, false);
349
+ const effectiveLevel = this.ctx.session.thinkingLevel ?? ThinkingLevel.Off;
350
+ const requestedLabel =
351
+ level === ThinkingLevel.Inherit ? `${level} (configured default: ${configuredDefault})` : level;
352
+ const clampedSuffix =
353
+ effectiveLevel === levelToApply ? "" : ` Requested ${levelToApply}; effective ${effectiveLevel}.`;
354
+
355
+ this.ctx.statusLine.invalidate();
356
+ this.ctx.updateEditorBorderColor();
357
+ this.ctx.updateEditorTopBorder();
358
+ this.ctx.ui.requestRender();
359
+ this.ctx.showStatus(
360
+ `Reasoning effort set to ${requestedLabel}. Effective effort: ${effectiveLevel}.${clampedSuffix}`,
361
+ );
362
+ },
363
+ () => {
364
+ done();
365
+ this.ctx.ui.requestRender();
366
+ },
367
+ );
368
+ return { component: selector, focus: selector.getSelectList() };
369
+ });
370
+ }
371
+
327
372
  showSettingsSelector(): void {
328
373
  getAvailableThemes().then(availableThemes => {
329
374
  this.showSelector(done => {
@@ -332,6 +377,7 @@ export class SelectorController {
332
377
  availableThinkingLevels: [...this.ctx.session.getAvailableThinkingLevels()],
333
378
  thinkingLevel: this.ctx.session.thinkingLevel,
334
379
  availableThemes,
380
+ availableModelProfiles: [...this.ctx.session.modelRegistry.getModelProfiles().keys()],
335
381
  cwd: getProjectDir(),
336
382
  },
337
383
  {
@@ -528,6 +574,18 @@ export class SelectorController {
528
574
  this.ctx.updateEditorBorderColor();
529
575
  break;
530
576
 
577
+ case "modelProfile.default": {
578
+ // Applying the default profile live mirrors the /model preset flow so the
579
+ // running session switches immediately, not only on next startup.
580
+ const profileName = typeof value === "string" ? value : "";
581
+ if (!profileName) break;
582
+ this.#applyModelProfile(profileName, true)
583
+ .then(() => this.ctx.ui.requestRender())
584
+ .catch(error => {
585
+ this.ctx.showError(error instanceof Error ? error.message : String(error));
586
+ });
587
+ break;
588
+ }
531
589
  case "clearOnShrink":
532
590
  this.ctx.ui.setClearOnShrink(value as boolean);
533
591
  break;
@@ -678,6 +736,29 @@ export class SelectorController {
678
736
  }
679
737
  }
680
738
 
739
+ /**
740
+ * Activate a model profile through the shared /model + /settings path: swap the
741
+ * live session model (and, when persistDefault, persist it as the startup
742
+ * default) then refresh the status surfaces. Rethrows so callers surface errors.
743
+ */
744
+ async #applyModelProfile(profileName: string, persistDefault: boolean): Promise<void> {
745
+ const profileLabel = formatModelProfileDisplayLabel(
746
+ this.ctx.session.modelRegistry.getModelProfile(profileName) ?? { name: profileName },
747
+ );
748
+ await activateModelProfile(
749
+ {
750
+ session: this.ctx.session,
751
+ modelRegistry: this.ctx.session.modelRegistry,
752
+ settings: this.ctx.settings,
753
+ profileName,
754
+ },
755
+ { persistDefault },
756
+ );
757
+ this.ctx.statusLine.invalidate();
758
+ this.ctx.updateEditorBorderColor();
759
+ this.ctx.showStatus(persistDefault ? `Default model profile: ${profileLabel}` : `Model profile: ${profileLabel}`);
760
+ }
761
+
681
762
  showModelSelector(options?: { temporaryOnly?: boolean }): void {
682
763
  this.showSelector(done => {
683
764
  let modelSelector: ModelSelectorComponent;
@@ -708,22 +789,7 @@ export class SelectorController {
708
789
  return;
709
790
  }
710
791
  if (selection.kind === "profile") {
711
- await activateModelProfile(
712
- {
713
- session: this.ctx.session,
714
- modelRegistry: this.ctx.session.modelRegistry,
715
- settings: this.ctx.settings,
716
- profileName: selection.profileName,
717
- },
718
- { persistDefault: selection.setDefault },
719
- );
720
- this.ctx.statusLine.invalidate();
721
- this.ctx.updateEditorBorderColor();
722
- this.ctx.showStatus(
723
- selection.setDefault
724
- ? `Default model profile: ${selection.profileName}`
725
- : `Model profile: ${selection.profileName}`,
726
- );
792
+ await this.#applyModelProfile(selection.profileName, selection.setDefault);
727
793
  done();
728
794
  this.ctx.ui.requestRender();
729
795
  return;
@@ -737,6 +803,79 @@ export class SelectorController {
737
803
  this.ctx.showStatus(`Temporary model: ${selectedSelector ?? model.id}`);
738
804
  done();
739
805
  this.ctx.ui.requestRender();
806
+ } else if (selection.roles) {
807
+ const targetRoles: readonly SkcModelAssignmentTargetId[] = selection.roles;
808
+ const includesDefault = targetRoles.includes("default");
809
+ const includesRoleAgent = targetRoles.some(targetRole => targetRole !== "default");
810
+ if (includesRoleAgent) {
811
+ const apiKey = await this.ctx.session.modelRegistry.getApiKey(
812
+ model,
813
+ this.ctx.session.sessionId,
814
+ );
815
+ if (!apiKey) {
816
+ throw new Error(`No API key for ${model.provider}/${model.id}`);
817
+ }
818
+ }
819
+ const value =
820
+ selectedSelector ?? formatModelSelectorValue(`${model.provider}/${model.id}`, thinkingLevel);
821
+ const assignments = new Map<SkcModelAssignmentTargetId, string>();
822
+ for (const targetRole of targetRoles) assignments.set(targetRole, value);
823
+ const defaultSelector =
824
+ selectedSelector && thinkingLevel && selectedSelector.endsWith(`:${thinkingLevel}`)
825
+ ? selectedSelector.slice(0, -thinkingLevel.length - 1)
826
+ : selectedSelector;
827
+
828
+ if (includesDefault) {
829
+ await this.ctx.session.setModel(model, "default", {
830
+ selector: defaultSelector,
831
+ thinkingLevel,
832
+ });
833
+ if (thinkingLevel && thinkingLevel !== ThinkingLevel.Inherit) {
834
+ this.ctx.session.setThinkingLevel(thinkingLevel);
835
+ }
836
+ }
837
+ const materializedProfile = materializeActiveModelProfileAssignments({
838
+ session: this.ctx.session,
839
+ settings: this.ctx.settings,
840
+ assignments,
841
+ });
842
+ if (!materializedProfile) {
843
+ const overrides = this.ctx.settings.get("task.agentModelOverrides");
844
+ const nextOverrides = { ...overrides };
845
+ let writesOverrides = false;
846
+ for (const targetRole of targetRoles) {
847
+ const target = SKC_MODEL_ASSIGNMENT_TARGETS[targetRole];
848
+ if (target.settingsPath === "modelRoles") {
849
+ this.ctx.settings.setModelRole(targetRole, value);
850
+ } else {
851
+ nextOverrides[targetRole] = value;
852
+ writesOverrides = true;
853
+ }
854
+ }
855
+ if (writesOverrides) {
856
+ this.ctx.settings.set("task.agentModelOverrides", nextOverrides);
857
+ }
858
+ }
859
+ modelSelector.refreshRoleAssignments({
860
+ currentModel: this.ctx.session.model,
861
+ currentThinkingLevel: this.ctx.session.thinkingLevel,
862
+ activeModelProfile:
863
+ this.ctx.session.getActiveModelProfile?.() ?? this.ctx.settings.get("modelProfile.default"),
864
+ });
865
+ this.ctx.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
866
+ this.ctx.statusLine.invalidate();
867
+ this.ctx.updateEditorBorderColor();
868
+ await this.ctx.notifyConfigChanged?.();
869
+ const labels = targetRoles.map(
870
+ targetRole => SKC_MODEL_ASSIGNMENT_TARGETS[targetRole].tag ?? targetRole.toUpperCase(),
871
+ );
872
+ this.ctx.showStatus(
873
+ includesDefault
874
+ ? `All model targets set to ${value} for ${labels.join(", ")}.`
875
+ : `Role-agent models set to ${value} for ${labels.join(", ")}.`,
876
+ );
877
+ done();
878
+ this.ctx.ui.requestRender();
740
879
  } else if (role === "default") {
741
880
  // Default: update agent state and persist as the active default model.
742
881
  await this.ctx.session.setModel(model, role, {
@@ -774,19 +913,21 @@ export class SelectorController {
774
913
  }
775
914
  const value =
776
915
  selectedSelector ?? formatModelSelectorValue(`${model.provider}/${model.id}`, thinkingLevel);
777
- const materializedProfile = materializeActiveModelProfileAssignment({
916
+ const assignments = new Map<SkcModelAssignmentTargetId, string>([[role, value]]);
917
+ const materializedProfile = materializeActiveModelProfileAssignments({
778
918
  session: this.ctx.session,
779
919
  settings: this.ctx.settings,
780
- role,
781
- selector: value,
920
+ assignments,
782
921
  });
783
922
  if (!materializedProfile) {
784
923
  const target = SKC_MODEL_ASSIGNMENT_TARGETS[role];
785
924
  if (target.settingsPath === "modelRoles") {
786
925
  this.ctx.settings.setModelRole(role, value);
787
926
  } else {
788
- const overrides = this.ctx.settings.get("task.agentModelOverrides");
789
- this.ctx.settings.set("task.agentModelOverrides", { ...overrides, [role]: value });
927
+ this.ctx.settings.set("task.agentModelOverrides", {
928
+ ...this.ctx.settings.get("task.agentModelOverrides"),
929
+ [role]: value,
930
+ });
790
931
  }
791
932
  }
792
933
  modelSelector.refreshRoleAssignments({
@@ -796,6 +937,9 @@ export class SelectorController {
796
937
  this.ctx.session.getActiveModelProfile?.() ?? this.ctx.settings.get("modelProfile.default"),
797
938
  });
798
939
  this.ctx.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
940
+ this.ctx.statusLine.invalidate();
941
+ this.ctx.updateEditorBorderColor();
942
+ await this.ctx.notifyConfigChanged?.();
799
943
  this.ctx.showStatus(`${role} agent model: ${value}`);
800
944
  done();
801
945
  this.ctx.ui.requestRender();