@gajae-code/coding-agent 0.9.4 → 0.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.6] - 2026-07-10
6
+ ### Changed
7
+
8
+ - Moved the `codex-eco`/`codex-medium`/`codex-pro` presets and the `opus-codex`/`codex-opencodego`/`fable-opus-codex` combo presets from `gpt-5.5` onto the GPT-5.6 tier family: Sol drives `default` and `architect` on every codex preset (eco `sol:medium`, medium `sol:high`, pro `sol:xhigh`/`sol:max`), with Luna/Terra covering the lighter executor/planner/critic roles by tier.
9
+
10
+ ### Fixed
11
+
12
+ - Fixed v0.9.3–v0.9.6 compiled release binaries crashing at first real launch with `Cannot find module '/$bunfs/root/node_modules/handlebars/lib/index.js'` while `--version`/`--help` still worked (#1939). `--minify` silently dropped the handlebars bunfs extra entrypoint; handlebars is now bundled through a statically-traceable `require("handlebars")` in `@gajae-code/utils` prompt rendering (still lazy at runtime), and the fragile extra entrypoint is gone from both release and dev compile args. `--minify` and its startup-RSS win are retained.
13
+
14
+ ## [0.9.5] - 2026-07-09
15
+ ### Fixed
16
+
17
+ - ACP permission prompts now honor `clientCapabilities._meta.gjc.permissionHandling` and the `GJC_ACP_PERMISSION_MODE` fallback, so `auto` and `always-allow` no longer emit `session/request_permission` calls while invalid values fail safely to `prompt`.
18
+ - Model selector batch assignments ("Set for all role agents" / "Set for all targets") now open the reasoning-effort menu whenever any batch target requires an explicit choice (e.g. Anthropic reasoning models like `claude-fable-5`), and the chosen effort plus the full batch survive the menu. Previously the menu never appeared for Anthropic models (silently persisting `:off` selectors for every role agent), and for OpenAI/Codex models picking an effort collapsed the batch to a DEFAULT-only assignment.
19
+ ### Added
20
+
21
+ - ACP clients now receive GJC automatic-compaction start/end state through additive `session_info_update` metadata, including the compaction action, trigger, retry/abort/error outcome, and busy-to-idle phase transitions.
22
+
5
23
  ## [0.9.4] - 2026-07-09
6
24
  ### Fixed
7
25
 
@@ -16,6 +16,8 @@ interface AcpEventMapperOptions {
16
16
  * before emitting `ToolCallLocation` entries.
17
17
  */
18
18
  cwd?: string;
19
+ /** Phase to expose after compaction ends. Prompt-bound compaction resumes responding; idle maintenance returns idle. */
20
+ compactionEndPhase?: "responding" | "idle";
19
21
  }
20
22
  export declare function mapToolKind(toolName: string): ToolKind;
21
23
  export declare function mapAgentWireEventPayloadToAcpSessionUpdates(payload: AgentWireEventPayload, sessionId: string, options?: AcpEventMapperOptions): SessionNotification[];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/coding-agent",
4
- "version": "0.9.4",
4
+ "version": "0.9.6",
5
5
  "description": "Gajae Code CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo and Gajae Code Contributors",
@@ -49,12 +49,12 @@
49
49
  "@agentclientprotocol/sdk": "0.21.0",
50
50
  "@babel/parser": "^7.29.3",
51
51
  "@mozilla/readability": "^0.6.0",
52
- "@gajae-code/stats": "0.9.4",
53
- "@gajae-code/agent-core": "0.9.4",
54
- "@gajae-code/ai": "0.9.4",
55
- "@gajae-code/natives": "0.9.4",
56
- "@gajae-code/tui": "0.9.4",
57
- "@gajae-code/utils": "0.9.4",
52
+ "@gajae-code/stats": "0.9.6",
53
+ "@gajae-code/agent-core": "0.9.6",
54
+ "@gajae-code/ai": "0.9.6",
55
+ "@gajae-code/natives": "0.9.6",
56
+ "@gajae-code/tui": "0.9.6",
57
+ "@gajae-code/utils": "0.9.6",
58
58
  "@puppeteer/browsers": "^2.13.0",
59
59
  "@types/turndown": "5.0.6",
60
60
  "@xterm/headless": "^6.0.0",
@@ -30,7 +30,10 @@ export const releaseEntrypoints = [
30
30
  // NOTE: models.json must NOT be listed here — `bun build --compile` does not
31
31
  // emit `.json` extra entrypoints into the bunfs. It is embedded via the
32
32
  // `with { type: "file" }` import in packages/ai/src/models.ts instead.
33
- "./node_modules/handlebars/lib/index.js",
33
+ // NOTE: handlebars must NOT be listed here either — the extra entrypoint
34
+ // silently vanished from minified bundles and crashed v0.9.3–v0.9.6
35
+ // releases at startup (#1939). It is bundled via the statically-traceable
36
+ // `require("handlebars")` in packages/utils/src/prompt.ts instead.
34
37
  ];
35
38
 
36
39
  export const devEntrypoints = [
@@ -39,7 +42,6 @@ export const devEntrypoints = [
39
42
  "./src/tools/browser/tab-worker-entry.ts",
40
43
  "./src/eval/js/worker-entry.ts",
41
44
  "./src/notifications/telegram-daemon-cli.ts",
42
- "../../node_modules/handlebars/lib/index.js",
43
45
  ];
44
46
 
45
47
  export function buildReleaseCompileArgs(target: string, outfile: string): string[] {
@@ -72,6 +74,10 @@ export function buildCompileArgs(options: CompileArgOptions): string[] {
72
74
  // Minify shrinks the bundled JS the compiled binary must parse at
73
75
  // startup (302MB → ~114MB --help RSS measured on darwin-arm64).
74
76
  // --keep-names below preserves identifiers for error reports.
77
+ // SAFE with handlebars since #1939: the module is bundled via a
78
+ // statically-traceable require in packages/utils/src/prompt.ts, not a
79
+ // bunfs extra entrypoint (which --minify silently dropped, crashing
80
+ // v0.9.3–v0.9.6 releases at startup).
75
81
  "--minify",
76
82
  ...compileAutoloadDisableFlags,
77
83
  "--keep-names",
@@ -70,25 +70,25 @@ const profile = (
70
70
 
71
71
  export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
72
72
  profile("codex-eco", ["openai-codex"], {
73
- default: "openai-codex/gpt-5.5:low",
74
- executor: "openai-codex/gpt-5.5:minimal",
75
- planner: "openai-codex/gpt-5.5:low",
76
- critic: "openai-codex/gpt-5.5:medium",
77
- architect: "openai-codex/gpt-5.5:high",
73
+ default: "openai-codex/gpt-5.6-sol:medium",
74
+ executor: "openai-codex/gpt-5.6-luna:low",
75
+ planner: "openai-codex/gpt-5.6-luna:medium",
76
+ critic: "openai-codex/gpt-5.6-luna:medium",
77
+ architect: "openai-codex/gpt-5.6-sol:medium",
78
78
  }),
79
79
  profile("codex-medium", ["openai-codex"], {
80
- default: "openai-codex/gpt-5.5:medium",
81
- executor: "openai-codex/gpt-5.5:low",
82
- planner: "openai-codex/gpt-5.5:medium",
83
- critic: "openai-codex/gpt-5.5:high",
84
- architect: "openai-codex/gpt-5.5:xhigh",
80
+ default: "openai-codex/gpt-5.6-sol:high",
81
+ executor: "openai-codex/gpt-5.6-luna:medium",
82
+ planner: "openai-codex/gpt-5.6-terra:medium",
83
+ critic: "openai-codex/gpt-5.6-terra:high",
84
+ architect: "openai-codex/gpt-5.6-sol:high",
85
85
  }),
86
86
  profile("codex-pro", ["openai-codex"], {
87
- default: "openai-codex/gpt-5.5:xhigh",
88
- executor: "openai-codex/gpt-5.5:medium",
89
- planner: "openai-codex/gpt-5.5:high",
90
- critic: "openai-codex/gpt-5.5:xhigh",
91
- architect: "openai-codex/gpt-5.5:xhigh",
87
+ default: "openai-codex/gpt-5.6-sol:xhigh",
88
+ executor: "openai-codex/gpt-5.6-terra:medium",
89
+ planner: "openai-codex/gpt-5.6-sol:high",
90
+ critic: "openai-codex/gpt-5.6-sol:xhigh",
91
+ architect: "openai-codex/gpt-5.6-sol:max",
92
92
  }),
93
93
  profile("opencodego", ["opencode-go"], {
94
94
  default: "opencode-go/kimi-k2.6",
@@ -256,24 +256,24 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
256
256
  }),
257
257
  profile("opus-codex", ["anthropic", "openai-codex"], {
258
258
  default: "anthropic/claude-opus-4-8:xhigh",
259
- executor: "openai-codex/gpt-5.5:low",
260
- planner: "openai-codex/gpt-5.5:medium",
261
- critic: "openai-codex/gpt-5.5:high",
262
- architect: "openai-codex/gpt-5.5:xhigh",
259
+ executor: "openai-codex/gpt-5.6-terra:low",
260
+ planner: "openai-codex/gpt-5.6-terra:medium",
261
+ critic: "openai-codex/gpt-5.6-terra:high",
262
+ architect: "openai-codex/gpt-5.6-sol:xhigh",
263
263
  }),
264
264
  profile("codex-opencodego", ["openai-codex", "opencode-go"], {
265
- default: "openai-codex/gpt-5.5:medium",
265
+ default: "openai-codex/gpt-5.6-sol:medium",
266
266
  executor: "opencode-go/deepseek-v4-pro",
267
267
  planner: "opencode-go/kimi-k2.6",
268
268
  critic: "opencode-go/mimo-v2.5-pro",
269
- architect: "openai-codex/gpt-5.5:xhigh",
269
+ architect: "openai-codex/gpt-5.6-sol:xhigh",
270
270
  }),
271
271
  profile("fable-opus-codex", ["anthropic", "openai-codex"], {
272
272
  default: "anthropic/claude-fable-5:high",
273
- executor: "openai-codex/gpt-5.5:high",
273
+ executor: "openai-codex/gpt-5.6-terra:high",
274
274
  planner: "anthropic/claude-opus-4-8:medium",
275
275
  critic: "anthropic/claude-opus-4-8:high",
276
- architect: "openai-codex/gpt-5.5:xhigh",
276
+ architect: "openai-codex/gpt-5.6-sol:xhigh",
277
277
  }),
278
278
  ];
279
279
 
@@ -152,8 +152,8 @@ type ManagedSessionRecord = {
152
152
  liveMessageProgress: { textEmitted: boolean; thoughtEmitted: boolean } | undefined;
153
153
  toolArgsById: Map<string, unknown>;
154
154
  extensionsConfigured: boolean;
155
- // Installed inside `#scheduleBootstrapUpdates` (post-race-guard); released
156
- // in `#disposeSessionRecord`. Lives independent of any prompt turn.
155
+ // Installed after the bootstrap race guard or eagerly when the client starts a prompt;
156
+ // released in `#disposeSessionRecord`. Lives independent of any prompt turn.
157
157
  lifetimeUnsubscribe: (() => void) | undefined;
158
158
  };
159
159
 
@@ -619,6 +619,7 @@ export class AcpAgent implements Agent {
619
619
  promise: pendingPrompt.promise,
620
620
  };
621
621
 
622
+ this.#ensureLifetimeSubscription(record);
622
623
  record.promptTurn.unsubscribe = record.session.subscribe(event => {
623
624
  void this.#handlePromptEvent(record, event);
624
625
  });
@@ -831,6 +832,7 @@ export class AcpAgent implements Agent {
831
832
  }
832
833
  promptTurn.cancelRequested = true;
833
834
  promptTurn.unsubscribe?.();
835
+ void this.#emitCancelledPromptIdleUpdate(record);
834
836
  const cleanup = this.#runCancelCleanup(record, promptTurn);
835
837
  promptTurn.cleanup = cleanup;
836
838
  this.#finishPrompt(record, {
@@ -1043,8 +1045,8 @@ export class AcpAgent implements Agent {
1043
1045
  async #registerPreparedSession(session: AgentSession, mcpServers: McpServer[]): Promise<ManagedSessionRecord> {
1044
1046
  const record = this.#createManagedSessionRecord(session);
1045
1047
  session.setClientBridge(createAcpClientBridge(this.#connection, session.sessionId, this.#clientCapabilities));
1046
- // `record.lifetimeUnsubscribe` is installed in `#scheduleBootstrapUpdates`
1047
- // so it shares the bootstrap race guard — see that comment for why.
1048
+ // The lifetime subscription normally follows the bootstrap race guard, but
1049
+ // prompt() installs it eagerly once the client has demonstrated session ownership.
1048
1050
  try {
1049
1051
  await this.#configureExtensions(record);
1050
1052
  await this.#configureMcpServers(record, mcpServers);
@@ -1070,7 +1072,38 @@ export class AcpAgent implements Agent {
1070
1072
  };
1071
1073
  }
1072
1074
 
1075
+ #ensureLifetimeSubscription(record: ManagedSessionRecord): void {
1076
+ if (record.lifetimeUnsubscribe) {
1077
+ return;
1078
+ }
1079
+ record.lifetimeUnsubscribe = record.session.subscribe(event => {
1080
+ void this.#handleLifetimeEvent(record, event);
1081
+ });
1082
+ }
1083
+
1073
1084
  async #handleLifetimeEvent(record: ManagedSessionRecord, event: AgentSessionEvent): Promise<void> {
1085
+ if (event.type === "auto_compaction_start" || event.type === "auto_compaction_end") {
1086
+ // Prompt-bound compaction is normally forwarded by #handlePromptEvent. The lifetime
1087
+ // subscription covers idle maintenance and the end event after prompt cancellation.
1088
+ const promptTurn = record.promptTurn;
1089
+ if (
1090
+ isPromptTurnInFlight(promptTurn) &&
1091
+ (!promptTurn.cancelRequested || event.type === "auto_compaction_start")
1092
+ ) {
1093
+ return;
1094
+ }
1095
+ for (const notification of mapAgentWireEventPayloadToAcpSessionUpdates(
1096
+ toAgentWireEventPayload(event),
1097
+ record.session.sessionId,
1098
+ {
1099
+ cwd: record.session.sessionManager.getCwd(),
1100
+ compactionEndPhase: "idle",
1101
+ },
1102
+ )) {
1103
+ await this.#connection.sessionUpdate(notification);
1104
+ }
1105
+ return;
1106
+ }
1074
1107
  if (event.type !== "thinking_level_changed") {
1075
1108
  return;
1076
1109
  }
@@ -1140,6 +1173,7 @@ export class AcpAgent implements Agent {
1140
1173
  getMessageProgress: message => this.#getLiveMessageProgress(record, message),
1141
1174
  getToolArgs: toolCallId => record.toolArgsById.get(toolCallId),
1142
1175
  cwd: record.session.sessionManager.getCwd(),
1176
+ compactionEndPhase: "responding",
1143
1177
  },
1144
1178
  )) {
1145
1179
  await this.#connection.sessionUpdate(notification);
@@ -1551,13 +1585,10 @@ export class AcpAgent implements Agent {
1551
1585
  // enough that the response future has scheduled before our timer fires
1552
1586
  // on stdio-only transports.
1553
1587
  //
1554
- // The session-lifetime subscription is installed inside the same timer
1555
- // so it shares this guard — without it, an extension's `session_start`
1556
- // handler (or any async work it schedules) calling `setThinkingLevel`
1557
- // would push a `config_option_update` for a session id the client
1558
- // hasn't been told about yet. The pre-bootstrap thinking level is
1559
- // reported in the response's `configOptions`, so deferring the
1560
- // notification loses no state.
1588
+ // The session-lifetime subscription normally shares this guard so extension
1589
+ // work cannot notify an unknown session id. prompt() may install it earlier:
1590
+ // receipt of a prompt proves the client already owns the returned session id,
1591
+ // and closes the cancellation window before this timer fires.
1561
1592
  setTimeout(() => {
1562
1593
  if (this.#connection.signal.aborted) {
1563
1594
  return;
@@ -1566,11 +1597,7 @@ export class AcpAgent implements Agent {
1566
1597
  if (!record) {
1567
1598
  return;
1568
1599
  }
1569
- if (!record.lifetimeUnsubscribe) {
1570
- record.lifetimeUnsubscribe = record.session.subscribe(event => {
1571
- void this.#handleLifetimeEvent(record, event);
1572
- });
1573
- }
1600
+ this.#ensureLifetimeSubscription(record);
1574
1601
  void this.#emitBootstrapUpdates(sessionId, record);
1575
1602
  }, ACP_BOOTSTRAP_RACE_GUARD_MS);
1576
1603
  }
@@ -1606,6 +1633,30 @@ export class AcpAgent implements Agent {
1606
1633
  });
1607
1634
  }
1608
1635
 
1636
+ /** Ensure cancellation clears a previously emitted busy/compacting phase even if abort never produces an end event. */
1637
+ async #emitCancelledPromptIdleUpdate(record: ManagedSessionRecord): Promise<void> {
1638
+ if (this.#connection.signal.aborted) {
1639
+ return;
1640
+ }
1641
+ try {
1642
+ await this.#connection.sessionUpdate({
1643
+ sessionId: record.session.sessionId,
1644
+ update: {
1645
+ sessionUpdate: "session_info_update",
1646
+ title: record.session.sessionName,
1647
+ updatedAt: new Date().toISOString(),
1648
+ _meta: {
1649
+ gjcPhase: "idle",
1650
+ running: false,
1651
+ gjcRunning: false,
1652
+ },
1653
+ },
1654
+ });
1655
+ } catch (error) {
1656
+ logger.warn("Failed to emit cancelled ACP prompt idle update", { error });
1657
+ }
1658
+ }
1659
+
1609
1660
  /**
1610
1661
  * Reload plugin/registry state for an ACP session. Mirrors the interactive
1611
1662
  * `/reload-plugins` and `/move` flows: invalidates the plugin-roots cache,
@@ -1647,6 +1698,11 @@ export class AcpAgent implements Agent {
1647
1698
  sessionUpdate: "session_info_update",
1648
1699
  title: record.session.sessionName,
1649
1700
  updatedAt: new Date().toISOString(),
1701
+ _meta: {
1702
+ gjcPhase: "idle",
1703
+ running: false,
1704
+ gjcRunning: false,
1705
+ },
1650
1706
  },
1651
1707
  });
1652
1708
  }
@@ -22,18 +22,38 @@ import type {
22
22
  ClientBridgeTerminalHandle,
23
23
  } from "../../session/client-bridge";
24
24
 
25
+ type AcpPermissionMode = "auto" | "prompt" | "always-allow";
26
+
27
+ const ACP_PERMISSION_MODE_ENV = "GJC_ACP_PERMISSION_MODE";
28
+
29
+ function parseAcpPermissionMode(value: unknown): AcpPermissionMode {
30
+ if (value === "auto" || value === "prompt" || value === "always-allow") return value;
31
+ return "prompt";
32
+ }
33
+
34
+ /** Client metadata is authoritative; the process environment is only a fallback when that field is absent. */
35
+ function resolveAcpPermissionMode(clientCapabilities: ClientCapabilities | undefined): AcpPermissionMode {
36
+ const meta = clientCapabilities?._meta;
37
+ if (typeof meta === "object" && meta !== null) {
38
+ const gjc = (meta as { gjc?: unknown }).gjc;
39
+ if (typeof gjc === "object" && gjc !== null && "permissionHandling" in gjc) {
40
+ return parseAcpPermissionMode((gjc as { permissionHandling?: unknown }).permissionHandling);
41
+ }
42
+ }
43
+ return parseAcpPermissionMode(process.env[ACP_PERMISSION_MODE_ENV]);
44
+ }
45
+
25
46
  export function createAcpClientBridge(
26
47
  connection: AgentSideConnection,
27
48
  sessionId: string,
28
49
  clientCapabilities: ClientCapabilities | undefined,
29
50
  ): ClientBridge {
51
+ const promptPermission = resolveAcpPermissionMode(clientCapabilities) === "prompt";
30
52
  const capabilities: ClientBridgeCapabilities = {
31
53
  readTextFile: clientCapabilities?.fs?.readTextFile === true,
32
54
  writeTextFile: clientCapabilities?.fs?.writeTextFile === true,
33
55
  terminal: clientCapabilities?.terminal === true,
34
- // Permission requests are always usable on the connection; gating is
35
- // the agent's policy choice rather than a client capability.
36
- requestPermission: true,
56
+ requestPermission: promptPermission,
37
57
  };
38
58
 
39
59
  const bridge: ClientBridge = { capabilities, deferAgentInitiatedTurns: true };
@@ -65,8 +85,10 @@ export function createAcpClientBridge(
65
85
  createTerminalHandle(connection, sessionId, params);
66
86
  }
67
87
 
68
- bridge.requestPermission = (toolCall, options, signal) =>
69
- requestPermission(connection, sessionId, toolCall, options, signal);
88
+ if (promptPermission) {
89
+ bridge.requestPermission = (toolCall, options, signal) =>
90
+ requestPermission(connection, sessionId, toolCall, options, signal);
91
+ }
70
92
 
71
93
  return bridge;
72
94
  }
@@ -27,6 +27,8 @@ interface AcpEventMapperOptions {
27
27
  * before emitting `ToolCallLocation` entries.
28
28
  */
29
29
  cwd?: string;
30
+ /** Phase to expose after compaction ends. Prompt-bound compaction resumes responding; idle maintenance returns idle. */
31
+ compactionEndPhase?: "responding" | "idle";
30
32
  }
31
33
 
32
34
  interface ContentArrayContainer {
@@ -236,8 +238,44 @@ export function mapAgentSessionEventToAcpSessionUpdates(
236
238
  case "turn_start":
237
239
  case "turn_end":
238
240
  case "message_start":
241
+ return [];
239
242
  case "auto_compaction_start":
240
- case "auto_compaction_end":
243
+ return [
244
+ toSessionNotification(sessionId, {
245
+ sessionUpdate: "session_info_update",
246
+ _meta: {
247
+ gjcPhase: "compacting",
248
+ gjcCompactionState: "start",
249
+ gjcCompactionTrigger: event.reason,
250
+ gjcCompactionAction: event.action,
251
+ running: true,
252
+ gjcRunning: true,
253
+ },
254
+ }),
255
+ ];
256
+ case "auto_compaction_end": {
257
+ const phase = options.compactionEndPhase ?? "responding";
258
+ const running = phase !== "idle";
259
+ const meta: Record<string, unknown> = {
260
+ gjcPhase: phase,
261
+ gjcCompactionState: "end",
262
+ gjcCompactionAction: event.action,
263
+ gjcCompactionAborted: event.aborted,
264
+ gjcCompactionWillRetry: event.willRetry,
265
+ running,
266
+ gjcRunning: running,
267
+ };
268
+ if (event.skipped !== undefined) {
269
+ meta.gjcCompactionSkipped = event.skipped;
270
+ }
271
+ if (event.errorMessage !== undefined) {
272
+ meta.gjcCompactionErrorMessage = event.errorMessage;
273
+ }
274
+ if (event.continuationSkipReason !== undefined) {
275
+ meta.gjcCompactionContinuationSkipReason = event.continuationSkipReason;
276
+ }
277
+ return [toSessionNotification(sessionId, { sessionUpdate: "session_info_update", _meta: meta })];
278
+ }
241
279
  case "auto_retry_start":
242
280
  case "auto_retry_end":
243
281
  case "retry_fallback_applied":
@@ -121,6 +121,7 @@ export type ModelSelectorSelection =
121
121
  interface PendingThinkingChoice {
122
122
  item: ModelItem | CanonicalModelItem;
123
123
  role: GjcModelAssignmentTargetId | null;
124
+ roles?: readonly GjcModelAssignmentTargetId[];
124
125
  levels: ThinkingLevel[];
125
126
  }
126
127
 
@@ -1373,7 +1374,13 @@ export class ModelSelectorComponent extends Container {
1373
1374
  }
1374
1375
 
1375
1376
  #renderThinkingMenu(choice: PendingThinkingChoice): void {
1376
- const targetLabel = choice.role === null ? "temporary model" : GJC_MODEL_ASSIGNMENT_TARGETS[choice.role].name;
1377
+ const targetLabel = choice.roles
1378
+ ? choice.roles.includes("default")
1379
+ ? "all targets"
1380
+ : "all role agents"
1381
+ : choice.role === null
1382
+ ? "temporary model"
1383
+ : GJC_MODEL_ASSIGNMENT_TARGETS[choice.role].name;
1377
1384
  this.#listContainer.addChild(new Spacer(1));
1378
1385
  this.#listContainer.addChild(
1379
1386
  new Text(theme.fg("muted", ` Reasoning for ${targetLabel}: ${choice.item.model.id}`), 0, 0),
@@ -1700,14 +1707,16 @@ export class ModelSelectorComponent extends Container {
1700
1707
  const level = choice.levels[this.#selectedThinkingIndex];
1701
1708
  if (!level) return;
1702
1709
  this.#pendingThinkingChoice = undefined;
1703
- this.#handleSelect(choice.item, choice.role, level);
1710
+ this.#handleSelect(choice.item, choice.role, level, choice.roles);
1704
1711
  return;
1705
1712
  }
1706
1713
  if (getKeybindings().matches(keyData, "tui.select.cancel")) {
1707
1714
  this.#pendingThinkingChoice = undefined;
1708
1715
  if (choice.role !== null) {
1709
1716
  this.#pendingActionItem = choice.item;
1710
- this.#selectedActionIndex = Math.max(0, GJC_MODEL_ASSIGNMENT_TARGET_IDS.indexOf(choice.role));
1717
+ this.#selectedActionIndex = choice.roles
1718
+ ? GJC_MODEL_ASSIGNMENT_TARGET_IDS.length + (choice.roles.includes("default") ? 1 : 0)
1719
+ : Math.max(0, GJC_MODEL_ASSIGNMENT_TARGET_IDS.indexOf(choice.role));
1711
1720
  }
1712
1721
  this.#updateList();
1713
1722
  }
@@ -1736,9 +1745,12 @@ export class ModelSelectorComponent extends Container {
1736
1745
  ): void {
1737
1746
  const itemThinkingLevel = thinkingLevel ?? item.thinkingLevel;
1738
1747
  const hasExplicitThinkingChoice = thinkingLevel !== undefined || item.explicitThinkingLevel === true;
1739
- if (!hasExplicitThinkingChoice && requiresExplicitThinkingChoice(item.model, role)) {
1748
+ const needsExplicitThinkingChoice = roles
1749
+ ? roles.some(targetRole => requiresExplicitThinkingChoice(item.model, targetRole))
1750
+ : requiresExplicitThinkingChoice(item.model, role);
1751
+ if (!hasExplicitThinkingChoice && needsExplicitThinkingChoice) {
1740
1752
  const levels = getSelectableThinkingLevels(item.model);
1741
- this.#pendingThinkingChoice = { item, role, levels };
1753
+ this.#pendingThinkingChoice = { item, role, roles, levels };
1742
1754
  this.#selectedThinkingIndex = this.#getInitialThinkingChoiceIndex(item, levels);
1743
1755
  this.#updateList();
1744
1756
  return;