@gajae-code/coding-agent 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/types/cli/completion-cli.d.ts +41 -0
  3. package/dist/types/cli/config-cli.d.ts +1 -0
  4. package/dist/types/cli/notify-cli.d.ts +7 -0
  5. package/dist/types/cli.d.ts +124 -1
  6. package/dist/types/commands/completion.d.ts +29 -0
  7. package/dist/types/commands/config.d.ts +3 -0
  8. package/dist/types/commands/deep-interview.d.ts +3 -0
  9. package/dist/types/config/file-lock.d.ts +7 -6
  10. package/dist/types/config/keybindings.d.ts +2 -2
  11. package/dist/types/config/model-profile-activation.d.ts +6 -0
  12. package/dist/types/config/settings.d.ts +8 -0
  13. package/dist/types/extensibility/extensions/runner.d.ts +2 -1
  14. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  15. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +39 -0
  16. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
  17. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -1
  18. package/dist/types/gjc-runtime/deep-interview-state.d.ts +12 -0
  19. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
  20. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  21. package/dist/types/main.d.ts +4 -1
  22. package/dist/types/modes/components/model-selector.d.ts +4 -0
  23. package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
  24. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  25. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  26. package/dist/types/modes/print-mode.d.ts +13 -1
  27. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
  28. package/dist/types/notifications/config.d.ts +8 -1
  29. package/dist/types/notifications/daemon-paths.d.ts +1 -0
  30. package/dist/types/notifications/index.d.ts +6 -3
  31. package/dist/types/notifications/lifecycle-commands.d.ts +11 -3
  32. package/dist/types/notifications/telegram-daemon.d.ts +18 -3
  33. package/dist/types/notifications/telegram-reference.d.ts +0 -5
  34. package/dist/types/runtime-api-key.d.ts +7 -0
  35. package/dist/types/session/agent-session.d.ts +10 -0
  36. package/dist/types/session/agent-storage.d.ts +1 -0
  37. package/dist/types/task/executor.d.ts +2 -0
  38. package/dist/types/tools/computer.d.ts +2 -2
  39. package/dist/types/tools/skill.d.ts +1 -1
  40. package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
  41. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
  42. package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
  43. package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
  44. package/package.json +7 -7
  45. package/src/cli/completion-cli.ts +234 -0
  46. package/src/cli/config-cli.ts +37 -9
  47. package/src/cli/fast-help.ts +1 -0
  48. package/src/cli/notify-cli.ts +91 -7
  49. package/src/cli.ts +3 -1
  50. package/src/commands/completion.ts +84 -0
  51. package/src/commands/config.ts +2 -0
  52. package/src/commands/deep-interview.ts +2 -1
  53. package/src/config/file-lock.ts +73 -34
  54. package/src/config/keybindings.ts +2 -2
  55. package/src/config/model-profile-activation.ts +65 -0
  56. package/src/config/model-registry.ts +14 -2
  57. package/src/config/settings.ts +22 -0
  58. package/src/coordinator-mcp/server.ts +67 -12
  59. package/src/defaults/gjc/skills/deep-interview/SKILL.md +38 -5
  60. package/src/defaults/gjc/skills/ralplan/SKILL.md +15 -11
  61. package/src/defaults/gjc/skills/team/SKILL.md +1 -1
  62. package/src/extensibility/extensions/runner.ts +2 -0
  63. package/src/extensibility/extensions/types.ts +9 -0
  64. package/src/extensibility/skills.ts +30 -2
  65. package/src/gjc-runtime/deep-interview-ambiguity.ts +206 -0
  66. package/src/gjc-runtime/deep-interview-recorder.ts +62 -8
  67. package/src/gjc-runtime/deep-interview-runtime.ts +210 -2
  68. package/src/gjc-runtime/deep-interview-state.ts +13 -0
  69. package/src/gjc-runtime/session-state-sidecar.ts +59 -11
  70. package/src/gjc-runtime/state-runtime.ts +12 -2
  71. package/src/gjc-runtime/team-runtime.ts +23 -0
  72. package/src/gjc-runtime/ultragoal-runtime.ts +73 -19
  73. package/src/hooks/native-skill-hook.ts +29 -0
  74. package/src/internal-urls/docs-index.generated.ts +2 -2
  75. package/src/main.ts +52 -41
  76. package/src/modes/acp/acp-agent.ts +7 -6
  77. package/src/modes/components/model-selector.ts +54 -11
  78. package/src/modes/components/queued-message-selector.ts +92 -0
  79. package/src/modes/components/settings-selector.ts +4 -0
  80. package/src/modes/components/welcome.ts +160 -44
  81. package/src/modes/controllers/command-controller.ts +4 -4
  82. package/src/modes/controllers/extension-ui-controller.ts +4 -10
  83. package/src/modes/controllers/input-controller.ts +186 -25
  84. package/src/modes/controllers/selector-controller.ts +122 -27
  85. package/src/modes/interactive-mode.ts +43 -16
  86. package/src/modes/print-mode.ts +43 -4
  87. package/src/modes/prompt-action-autocomplete.ts +24 -13
  88. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  89. package/src/modes/utils/ui-helpers.ts +2 -2
  90. package/src/notifications/config.ts +9 -1
  91. package/src/notifications/daemon-paths.ts +2 -0
  92. package/src/notifications/index.ts +39 -9
  93. package/src/notifications/lifecycle-commands.ts +37 -7
  94. package/src/notifications/lifecycle-orchestrator.ts +19 -6
  95. package/src/notifications/telegram-daemon-control.ts +3 -1
  96. package/src/notifications/telegram-daemon.ts +231 -43
  97. package/src/notifications/telegram-reference.ts +0 -16
  98. package/src/prompts/system/system-prompt.md +3 -0
  99. package/src/prompts/tools/computer.md +2 -2
  100. package/src/runtime-api-key.ts +16 -0
  101. package/src/sdk.ts +20 -6
  102. package/src/session/agent-session.ts +225 -31
  103. package/src/session/agent-storage.ts +4 -0
  104. package/src/session/session-manager.ts +44 -2
  105. package/src/slash-commands/builtin-registry.ts +153 -71
  106. package/src/task/executor.ts +7 -4
  107. package/src/task/index.ts +9 -3
  108. package/src/task/output-manager.ts +16 -6
  109. package/src/task/receipt.ts +26 -7
  110. package/src/tools/computer.ts +246 -43
  111. package/src/tools/skill.ts +1 -1
  112. package/src/tools/telegram-send.ts +6 -0
  113. package/src/workflow/workflow-intent-diff.ts +29 -6
  114. package/src/workflow/workflow-intent-report-consumer.ts +221 -0
  115. package/src/workflow/workflow-intent-report-schema.ts +129 -0
  116. package/src/workflow/workflow-intent-report.ts +135 -0
package/src/main.ts CHANGED
@@ -37,6 +37,7 @@ import type { ExtensionUIContext } from "./extensibility/extensions/types";
37
37
  import type { InteractiveMode } from "./modes/interactive-mode";
38
38
  import { initTheme, stopThemeWatcher } from "./modes/theme/theme";
39
39
  import type { SubmittedUserInput } from "./modes/types";
40
+ import { applyCliRuntimeApiKeyOverride } from "./runtime-api-key";
40
41
  import type { MCPManager } from "./runtime-mcp";
41
42
  import {
42
43
  type CreateAgentSessionOptions,
@@ -189,6 +190,11 @@ function applyExtensionFlagValues(session: AgentSession, rawArgs: string[]): Map
189
190
  return extensionRunner.getFlagValues();
190
191
  }
191
192
 
193
+ type CreateSessionForMain = (
194
+ options: CreateAgentSessionOptions,
195
+ context?: { skipPostCreateModelRefresh?: boolean },
196
+ ) => Promise<CreateAgentSessionResult>;
197
+
192
198
  type AcpSessionFactory = (cwd: string) => Promise<AgentSession>;
193
199
 
194
200
  export interface AcpSessionFactoryOptions {
@@ -199,7 +205,7 @@ export interface AcpSessionFactoryOptions {
199
205
  modelRegistry: ModelRegistry;
200
206
  parsedArgs: Pick<Args, "apiKey" | "default" | "model" | "mpreset" | "thinking">;
201
207
  rawArgs: string[];
202
- createSession: (options: CreateAgentSessionOptions) => Promise<CreateAgentSessionResult>;
208
+ createSession: CreateSessionForMain;
203
209
  }
204
210
 
205
211
  export async function applyStartupModelProfiles(args: {
@@ -221,8 +227,11 @@ export async function applyStartupModelProfiles(args: {
221
227
  // override it. startupModel covers the eager path; session.model covers the
222
228
  // deferred `--model <pattern>` path resolved inside createAgentSession.
223
229
  const explicitModel = args.parsedArgs.model ? (args.startupModel ?? args.session.model) : undefined;
224
-
225
230
  const defaultProfile = args.settings.get("modelProfile.default");
231
+ if (defaultProfile || args.parsedArgs.mpreset) {
232
+ await args.modelRegistry.refresh("online-if-uncached");
233
+ }
234
+
226
235
  if (defaultProfile) {
227
236
  await applyProfile(defaultProfile, false);
228
237
  }
@@ -263,19 +272,23 @@ export async function applyStartupModelProfilesOrExit(
263
272
  export function createAcpSessionFactory(args: AcpSessionFactoryOptions): AcpSessionFactory {
264
273
  return async cwd => {
265
274
  const nextSettings = await args.settings.cloneForCwd(cwd);
275
+ const hasStartupProfile = Boolean(nextSettings.get("modelProfile.default") || args.parsedArgs.mpreset);
266
276
  const nextSessionManager = SessionManager.create(cwd, args.sessionDir);
267
277
  const agentId = `acp:${nextSessionManager.getSessionId()}`;
268
- const { session: nextSession } = await args.createSession({
269
- ...args.baseOptions,
270
- cwd,
271
- sessionManager: nextSessionManager,
272
- settings: nextSettings,
273
- authStorage: args.authStorage,
274
- modelRegistry: args.modelRegistry,
275
- agentId,
276
- hasUI: false,
277
- enableMCP: false,
278
- });
278
+ const { session: nextSession } = await args.createSession(
279
+ {
280
+ ...args.baseOptions,
281
+ cwd,
282
+ sessionManager: nextSessionManager,
283
+ settings: nextSettings,
284
+ authStorage: args.authStorage,
285
+ modelRegistry: args.modelRegistry,
286
+ agentId,
287
+ hasUI: false,
288
+ enableMCP: false,
289
+ },
290
+ { skipPostCreateModelRefresh: hasStartupProfile },
291
+ );
279
292
  await applyStartupModelProfilesOrExit({
280
293
  session: nextSession,
281
294
  settings: nextSettings,
@@ -284,9 +297,7 @@ export function createAcpSessionFactory(args: AcpSessionFactoryOptions): AcpSess
284
297
  startupModel: args.baseOptions.model,
285
298
  startupThinkingLevel: args.baseOptions.thinkingLevel,
286
299
  });
287
- if (args.parsedArgs.apiKey && !args.baseOptions.model && nextSession.model) {
288
- args.authStorage.setRuntimeApiKey(nextSession.model.provider, args.parsedArgs.apiKey);
289
- }
300
+ applyCliRuntimeApiKeyOverride(args.authStorage, args.parsedArgs.apiKey, nextSession.model);
290
301
  applyExtensionFlagValues(nextSession, args.rawArgs);
291
302
  return nextSession;
292
303
  };
@@ -395,30 +406,28 @@ async function getChangelogForDisplay(parsed: Args): Promise<string | undefined>
395
406
  return undefined;
396
407
  }
397
408
 
398
- const lastVersion = settings.get("lastChangelogVersion");
399
- if (lastVersion === VERSION) {
400
- // Steady state: user already saw the current version's changelog. Skip the file read + parse.
409
+ const entries = getDisplayChangelogEntries();
410
+ if (entries.length === 0) {
401
411
  return undefined;
402
412
  }
403
413
 
404
- const entries = getDisplayChangelogEntries();
405
-
414
+ const lastVersion = settings.get("lastChangelogVersion");
406
415
  if (!lastVersion) {
407
- if (entries.length > 0) {
408
- settings.set("lastChangelogVersion", VERSION);
409
- await flushChangelogVersion();
410
- return getInstalledVersionChangelogEntry(entries, VERSION)?.content;
411
- }
412
- } else {
416
+ settings.set("lastChangelogVersion", VERSION);
417
+ await flushChangelogVersion();
418
+ return getInstalledVersionChangelogEntry(entries, VERSION)?.content;
419
+ }
420
+
421
+ if (lastVersion !== VERSION) {
413
422
  const newEntries = getNewEntries(entries, lastVersion);
423
+ settings.set("lastChangelogVersion", VERSION);
424
+ await flushChangelogVersion();
414
425
  if (newEntries.length > 0) {
415
- settings.set("lastChangelogVersion", VERSION);
416
- await flushChangelogVersion();
417
426
  return newEntries.map(e => e.content).join("\n\n");
418
427
  }
419
428
  }
420
429
 
421
- return undefined;
430
+ return getInstalledVersionChangelogEntry(entries, VERSION)?.content;
422
431
  }
423
432
 
424
433
  async function flushChangelogVersion(): Promise<void> {
@@ -946,6 +955,7 @@ export async function runRootCommand(
946
955
  sessionOptions.modelRegistry = modelRegistry;
947
956
  sessionOptions.hasUI = isInteractive || mode === "rpc-ui";
948
957
  sessionOptions.settings = settingsInstance;
958
+ const hasRootStartupProfile = Boolean(settingsInstance.get("modelProfile.default") || parsedArgs.mpreset);
949
959
 
950
960
  // Research-mode (RLM) preset: augment session options before session creation.
951
961
  deps.rlmPreset?.applyOptions(sessionOptions, settingsInstance);
@@ -958,18 +968,19 @@ export async function runRootCommand(
958
968
  );
959
969
  process.exit(1);
960
970
  }
961
- if (sessionOptions.model) {
962
- authStorage.setRuntimeApiKey(sessionOptions.model.provider, parsedArgs.apiKey);
963
- }
971
+ applyCliRuntimeApiKeyOverride(authStorage, parsedArgs.apiKey, sessionOptions.model);
964
972
  }
965
973
 
966
974
  const createAgentSessionImpl = deps.createAgentSession ?? createAgentSession;
967
- const createSession = async (options: CreateAgentSessionOptions): Promise<CreateAgentSessionResult> => {
975
+ const createSession: CreateSessionForMain = async (options, context): Promise<CreateAgentSessionResult> => {
968
976
  const result = await logger.time("createAgentSession", createAgentSessionImpl, options);
969
977
  // Kick off background model discovery only after createAgentSession finishes its parallel
970
978
  // discovery arms; running these concurrently contends for the event loop and stretches
971
- // every parallel arm by ~30ms.
972
- modelRegistry.refreshInBackground();
979
+ // every parallel arm by ~30ms. Startup model profiles do their own foreground refresh
980
+ // before activation so project-scoped defaults can resolve freshly discovered models.
981
+ if (!context?.skipPostCreateModelRefresh) {
982
+ modelRegistry.refreshInBackground();
983
+ }
973
984
  return result;
974
985
  };
975
986
 
@@ -986,11 +997,11 @@ export async function runRootCommand(
986
997
  });
987
998
  await (deps.runAcpMode ?? (await import("./modes/acp")).runAcpMode)(createAcpSession);
988
999
  } else {
989
- const { session, setToolUIContext, modelFallbackMessage, lspServers, mcpManager, eventBus } =
990
- await createSession(sessionOptions);
991
- if (parsedArgs.apiKey && !sessionOptions.model && session.model) {
992
- authStorage.setRuntimeApiKey(session.model.provider, parsedArgs.apiKey);
993
- }
1000
+ const { session, setToolUIContext, modelFallbackMessage, lspServers, mcpManager, eventBus } = await createSession(
1001
+ sessionOptions,
1002
+ { skipPostCreateModelRefresh: hasRootStartupProfile },
1003
+ );
1004
+ applyCliRuntimeApiKeyOverride(authStorage, parsedArgs.apiKey, session.model);
994
1005
 
995
1006
  // Research-mode (RLM) preset: hard tool-boundary assertion after the registry is assembled.
996
1007
  if (deps.rlmPreset?.onSessionCreated) {
@@ -710,9 +710,6 @@ export class AcpAgent implements Agent {
710
710
  text: string,
711
711
  options: { directAliasMayCollide?: boolean } = {},
712
712
  ): Promise<boolean> {
713
- if (!text.startsWith("/")) {
714
- return false;
715
- }
716
713
  if (!record.session.skillsSettings?.enableSkillCommands) {
717
714
  return false;
718
715
  }
@@ -758,9 +755,13 @@ export class AcpAgent implements Agent {
758
755
  }
759
756
  return true;
760
757
  }
761
- const spaceIndex = text.indexOf(" ");
762
- const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
763
- const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
758
+ const slashText = text.trimStart();
759
+ if (!slashText.startsWith("/")) {
760
+ return false;
761
+ }
762
+ const spaceIndex = slashText.indexOf(" ");
763
+ const commandName = spaceIndex === -1 ? slashText.slice(1) : slashText.slice(1, spaceIndex);
764
+ const args = spaceIndex === -1 ? "" : slashText.slice(spaceIndex + 1).trim();
764
765
  if (
765
766
  !isNamespacedSkillSlashCommandName(commandName) &&
766
767
  options.directAliasMayCollide !== true &&
@@ -96,6 +96,7 @@ export type ModelSelectorSelection =
96
96
  kind: "assignment";
97
97
  model: Model;
98
98
  role: GjcModelAssignmentTargetId | null;
99
+ roles?: readonly GjcModelAssignmentTargetId[];
99
100
  thinkingLevel?: ThinkingLevel;
100
101
  selector?: string;
101
102
  }
@@ -1360,7 +1361,11 @@ export class ModelSelectorComponent extends Container {
1360
1361
  for (let i = 0; i < actionCount; i++) {
1361
1362
  const prefix = i === this.#selectedActionIndex ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
1362
1363
  const role = GJC_MODEL_ASSIGNMENT_TARGET_IDS[i];
1363
- const label = `Set as ${GJC_MODEL_ASSIGNMENT_TARGETS[role].tag ?? role.toUpperCase()} (${GJC_MODEL_ASSIGNMENT_TARGETS[role].name})`;
1364
+ const label = role
1365
+ ? `Set as ${GJC_MODEL_ASSIGNMENT_TARGETS[role].tag ?? role.toUpperCase()} (${GJC_MODEL_ASSIGNMENT_TARGETS[role].name})`
1366
+ : i === GJC_MODEL_ASSIGNMENT_TARGET_IDS.length
1367
+ ? "Set for all role agents"
1368
+ : "Set for all targets";
1364
1369
  this.#listContainer.addChild(
1365
1370
  new Text(`${prefix}${i === this.#selectedActionIndex ? theme.fg("accent", label) : label}`, 0, 0),
1366
1371
  );
@@ -1389,7 +1394,7 @@ export class ModelSelectorComponent extends Container {
1389
1394
  return this.#roles[role]?.thinkingLevel ?? ThinkingLevel.Inherit;
1390
1395
  }
1391
1396
  #getActionCount(_model: Model): number {
1392
- return GJC_MODEL_ASSIGNMENT_TARGET_IDS.length;
1397
+ return GJC_MODEL_ASSIGNMENT_TARGET_IDS.length + 2;
1393
1398
  }
1394
1399
 
1395
1400
  #getSelectedItem(): ModelItem | CanonicalModelItem | undefined {
@@ -1660,7 +1665,15 @@ export class ModelSelectorComponent extends Container {
1660
1665
  if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
1661
1666
  this.#pendingActionItem = undefined;
1662
1667
  const role = GJC_MODEL_ASSIGNMENT_TARGET_IDS[this.#selectedActionIndex];
1663
- if (role) this.#handleSelect(item, role);
1668
+ if (role) {
1669
+ this.#handleSelect(item, role);
1670
+ return;
1671
+ }
1672
+ const roles =
1673
+ this.#selectedActionIndex === GJC_MODEL_ASSIGNMENT_TARGET_IDS.length
1674
+ ? (["executor", "architect", "planner", "critic"] as const)
1675
+ : GJC_MODEL_ASSIGNMENT_TARGET_IDS;
1676
+ this.#handleSelect(item, "default", undefined, roles);
1664
1677
  return;
1665
1678
  }
1666
1679
  if (getKeybindings().matches(keyData, "tui.select.cancel")) {
@@ -1699,17 +1712,34 @@ export class ModelSelectorComponent extends Container {
1699
1712
  this.#updateList();
1700
1713
  }
1701
1714
  }
1715
+ #getInitialThinkingChoiceIndex(item: ModelItem | CanonicalModelItem, levels: ThinkingLevel[]): number {
1716
+ const preferred = this.#getPreferredThinkingLevel(item);
1717
+ if (preferred && preferred !== ThinkingLevel.Inherit) {
1718
+ const index = levels.indexOf(preferred);
1719
+ if (index !== -1) return index;
1720
+ }
1721
+ return 0;
1722
+ }
1723
+
1724
+ #getPreferredThinkingLevel(item: ModelItem | CanonicalModelItem): ThinkingLevel | undefined {
1725
+ if (item.thinkingLevel && item.thinkingLevel !== ThinkingLevel.Inherit) {
1726
+ return item.thinkingLevel;
1727
+ }
1728
+ return undefined;
1729
+ }
1702
1730
 
1703
1731
  #handleSelect(
1704
1732
  item: ModelItem | CanonicalModelItem,
1705
1733
  role: GjcModelAssignmentTargetId | null,
1706
1734
  thinkingLevel?: ThinkingLevel,
1735
+ roles?: readonly GjcModelAssignmentTargetId[],
1707
1736
  ): void {
1708
1737
  const itemThinkingLevel = thinkingLevel ?? item.thinkingLevel;
1709
1738
  const hasExplicitThinkingChoice = thinkingLevel !== undefined || item.explicitThinkingLevel === true;
1710
- if (!hasExplicitThinkingChoice && requiresExplicitThinkingChoice(item.model)) {
1711
- this.#pendingThinkingChoice = { item, role, levels: getSelectableThinkingLevels(item.model) };
1712
- this.#selectedThinkingIndex = 0;
1739
+ if (!hasExplicitThinkingChoice && requiresExplicitThinkingChoice(item.model, role)) {
1740
+ const levels = getSelectableThinkingLevels(item.model);
1741
+ this.#pendingThinkingChoice = { item, role, levels };
1742
+ this.#selectedThinkingIndex = this.#getInitialThinkingChoiceIndex(item, levels);
1713
1743
  this.#updateList();
1714
1744
  return;
1715
1745
  }
@@ -1727,17 +1757,23 @@ export class ModelSelectorComponent extends Container {
1727
1757
  }
1728
1758
 
1729
1759
  const selectedThinkingLevel = itemThinkingLevel ?? this.#getCurrentRoleThinkingLevel(role);
1730
- const selectorValue =
1731
- role === "default" ? item.selector : formatModelSelectorValue(item.selector, selectedThinkingLevel);
1760
+ const selectorValue = roles
1761
+ ? formatModelSelectorValue(item.selector, selectedThinkingLevel)
1762
+ : role === "default"
1763
+ ? item.selector
1764
+ : formatModelSelectorValue(item.selector, selectedThinkingLevel);
1732
1765
 
1733
1766
  // Update local state for UI
1734
- this.#roles[role] = { model: item.model, thinkingLevel: selectedThinkingLevel };
1767
+ for (const targetRole of roles ?? [role]) {
1768
+ this.#roles[targetRole] = { model: item.model, thinkingLevel: selectedThinkingLevel };
1769
+ }
1735
1770
 
1736
1771
  // Notify caller (for updating agent state if needed)
1737
1772
  this.#onSelectCallback({
1738
1773
  kind: "assignment",
1739
1774
  model: item.model,
1740
1775
  role,
1776
+ roles,
1741
1777
  thinkingLevel: selectedThinkingLevel,
1742
1778
  selector: selectorValue,
1743
1779
  });
@@ -1752,6 +1788,11 @@ export class ModelSelectorComponent extends Container {
1752
1788
  async __testSelectProfile(profileName: string, setDefault: boolean): Promise<void> {
1753
1789
  await this.#onSelectCallback({ kind: "profile", profileName, setDefault });
1754
1790
  }
1791
+ async __testSelectAssignment(
1792
+ selection: Omit<Extract<ModelSelectorSelection, { kind: "assignment" }>, "kind">,
1793
+ ): Promise<void> {
1794
+ await this.#onSelectCallback({ kind: "assignment", ...selection });
1795
+ }
1755
1796
  async __testSelectPresetAction(profileName: string, action: "rename" | "delete"): Promise<void> {
1756
1797
  await this.#onSelectCallback({
1757
1798
  kind: action === "rename" ? "renameProfile" : "deleteProfile",
@@ -1764,8 +1805,10 @@ export class ModelSelectorComponent extends Container {
1764
1805
  }
1765
1806
  }
1766
1807
 
1767
- function requiresExplicitThinkingChoice(model: Model): boolean {
1768
- return model.reasoning === true && (model.provider === "openai" || model.provider === "openai-codex");
1808
+ function requiresExplicitThinkingChoice(model: Model, role: GjcModelAssignmentTargetId | null): boolean {
1809
+ if (model.reasoning !== true) return false;
1810
+ if (model.provider === "openai" || model.provider === "openai-codex") return true;
1811
+ return role !== null && GJC_MODEL_ASSIGNMENT_TARGETS[role].settingsPath === "task.agentModelOverrides";
1769
1812
  }
1770
1813
 
1771
1814
  function getSelectableThinkingLevels(model: Model): ThinkingLevel[] {
@@ -0,0 +1,92 @@
1
+ import { Container, matchesKey, type SelectItem, SelectList, Spacer, Text } from "@gajae-code/tui";
2
+ import type { QueuedMessageEditEntry } from "../../session/agent-session";
3
+ import { getSelectListTheme, theme } from "../theme/theme";
4
+ import { DynamicBorder } from "./dynamic-border";
5
+
6
+ const MAX_VISIBLE_QUEUED_MESSAGES = 8;
7
+ const RAW_UP = "\x1b[A";
8
+ const RAW_DOWN = "\x1b[B";
9
+ type QueuedMessageMoveDirection = "up" | "down";
10
+
11
+ export type { QueuedMessageMoveDirection };
12
+
13
+ export class QueuedMessageSelectorComponent extends Container {
14
+ #selectList: SelectList;
15
+ #selectedEntry: QueuedMessageEditEntry | undefined;
16
+ #selectedIndex = 0;
17
+ #onDelete: (entry: QueuedMessageEditEntry, selectedIndex: number) => void;
18
+ #onMove: (entry: QueuedMessageEditEntry, selectedIndex: number, direction: QueuedMessageMoveDirection) => void;
19
+
20
+ constructor(
21
+ entries: QueuedMessageEditEntry[],
22
+ onSelect: (entry: QueuedMessageEditEntry) => void,
23
+ onDelete: (entry: QueuedMessageEditEntry, selectedIndex: number) => void,
24
+ onMove: (entry: QueuedMessageEditEntry, selectedIndex: number, direction: QueuedMessageMoveDirection) => void,
25
+ onCancel: () => void,
26
+ options?: { selectedIndex?: number },
27
+ ) {
28
+ super();
29
+
30
+ this.#onDelete = onDelete;
31
+ this.#onMove = onMove;
32
+ const byId = new Map(entries.map(entry => [entry.id, entry]));
33
+ this.#selectedIndex = Math.max(0, Math.min(options?.selectedIndex ?? 0, entries.length - 1));
34
+ this.#selectedEntry = entries[this.#selectedIndex];
35
+ const items: SelectItem[] = entries.map((entry, index) => ({
36
+ value: entry.id,
37
+ label: `${entry.label} ${index + 1}`,
38
+ description: entry.text,
39
+ hint: "Enter edit · Del remove · Ctrl+↑/↓ move",
40
+ }));
41
+
42
+ this.addChild(new Spacer(1));
43
+ this.addChild(new Text(theme.bold("Queued messages"), 1, 0));
44
+ this.addChild(new Text(theme.fg("muted", "Enter edit · Del remove · Ctrl+↑/↓ move · Esc cancel"), 1, 0));
45
+ this.addChild(new Spacer(1));
46
+ this.addChild(new DynamicBorder());
47
+
48
+ this.#selectList = new SelectList(items, MAX_VISIBLE_QUEUED_MESSAGES, getSelectListTheme());
49
+ this.#selectList.onSelect = item => {
50
+ const entry = byId.get(item.value);
51
+ if (entry) onSelect(entry);
52
+ };
53
+ this.#selectList.onSelectionChange = item => {
54
+ const index = entries.findIndex(entry => entry.id === item.value);
55
+ this.#selectedIndex = index === -1 ? this.#selectedIndex : index;
56
+ this.#selectedEntry = byId.get(item.value);
57
+ };
58
+ this.#selectList.setSelectedIndex(this.#selectedIndex);
59
+ this.#selectList.onCancel = onCancel;
60
+
61
+ this.addChild(this.#selectList);
62
+ this.addChild(new DynamicBorder());
63
+ }
64
+
65
+ handleInput(keyData: string): void {
66
+ if (matchesKey(keyData, "alt+up")) {
67
+ this.#selectList.handleInput(RAW_UP);
68
+ return;
69
+ }
70
+ if (matchesKey(keyData, "alt+down")) {
71
+ this.#selectList.handleInput(RAW_DOWN);
72
+ return;
73
+ }
74
+ if (matchesKey(keyData, "ctrl+up") || matchesKey(keyData, "ctrl+shift+up")) {
75
+ if (this.#selectedEntry) this.#onMove(this.#selectedEntry, this.#selectedIndex, "up");
76
+ return;
77
+ }
78
+ if (matchesKey(keyData, "ctrl+down") || matchesKey(keyData, "ctrl+shift+down")) {
79
+ if (this.#selectedEntry) this.#onMove(this.#selectedEntry, this.#selectedIndex, "down");
80
+ return;
81
+ }
82
+ if (matchesKey(keyData, "delete")) {
83
+ if (this.#selectedEntry) this.#onDelete(this.#selectedEntry, this.#selectedIndex);
84
+ return;
85
+ }
86
+ this.#selectList.handleInput(keyData);
87
+ }
88
+
89
+ getSelectList(): SelectList {
90
+ return this.#selectList;
91
+ }
92
+ }
@@ -657,6 +657,8 @@ export interface SettingsRuntimeContext {
657
657
  thinkingLevel: ThinkingLevel | undefined;
658
658
  /** Available themes */
659
659
  availableThemes: string[];
660
+ /** Available model profile names (from the model registry) */
661
+ availableModelProfiles: string[];
660
662
  /** Working directory for plugins tab */
661
663
  cwd: string;
662
664
  }
@@ -847,6 +849,8 @@ export class SettingsSelectorComponent extends Container {
847
849
  });
848
850
  } else if (def.path === "theme.dark" || def.path === "theme.light") {
849
851
  options = this.context.availableThemes.map(t => ({ value: t, label: t }));
852
+ } else if (def.path === "modelProfile.default") {
853
+ options = this.context.availableModelProfiles.map(p => ({ value: p, label: p }));
850
854
  }
851
855
  if (def.path === "statusLine.preset") {
852
856
  options = options.filter(option => option.value !== "custom");