Refactor provider model selections to option arrays#2246
Refactor provider model selections to option arrays#2246juliusmarminge merged 10 commits intomainfrom
Conversation
- Replace provider option objects with shared selection arrays - Update Claude, Codex, Cursor, and OpenCode routing and adapters - Refresh tests to use the new model selection shape
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Missing xhigh-to-max effort mapping for Claude CLI
- Added normalizeClaudeEffortForCli() that maps xhigh→max and filters ultrathink, and applied it in ClaudeTextGeneration.ts where resolveClaudeEffort's result was being passed directly to the --effort CLI flag.
- ✅ Fixed: Duplicated prompt-injected descriptor lookup logic across files
- Extracted the duplicated .find() logic into a shared findPromptInjectedDescriptor() utility in @t3tools/shared/model and updated both ClaudeAdapter.ts and ChatView.tsx to use it.
Or push these changes by commenting:
@cursor push 90d8261e21
Preview (90d8261e21)
diff --git a/apps/server/src/git/Layers/ClaudeTextGeneration.ts b/apps/server/src/git/Layers/ClaudeTextGeneration.ts
--- a/apps/server/src/git/Layers/ClaudeTextGeneration.ts
+++ b/apps/server/src/git/Layers/ClaudeTextGeneration.ts
@@ -31,6 +31,7 @@
import { getProviderOptionCurrentValue, getProviderOptionDescriptors } from "@t3tools/shared/model";
import {
getClaudeModelCapabilities,
+ normalizeClaudeEffortForCli,
resolveClaudeApiModelId,
resolveClaudeEffort,
} from "../../provider/Layers/ClaudeProvider.ts";
@@ -95,7 +96,7 @@
const findDescriptor = (id: string) => descriptors.find((descriptor) => descriptor.id === id);
const rawEffortValue = getProviderOptionCurrentValue(findDescriptor("effort"));
const rawEffort = typeof rawEffortValue === "string" ? rawEffortValue : undefined;
- const resolvedEffort = resolveClaudeEffort(caps, rawEffort);
+ const resolvedEffort = normalizeClaudeEffortForCli(resolveClaudeEffort(caps, rawEffort));
const thinkingDescriptor = findDescriptor("thinking");
const fastModeDescriptor = findDescriptor("fastMode");
const thinking =
diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts
--- a/apps/server/src/provider/Layers/ClaudeAdapter.ts
+++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts
@@ -43,6 +43,7 @@
} from "@t3tools/contracts";
import {
applyClaudePromptEffortPrefix,
+ findPromptInjectedDescriptor,
getProviderOptionDescriptors,
getModelSelectionOptionValue,
trimOrNull,
@@ -67,6 +68,7 @@
import { ServerSettingsService } from "../../serverSettings.ts";
import {
getClaudeModelCapabilities,
+ normalizeClaudeEffortForCli,
resolveClaudeApiModelId,
resolveClaudeEffort,
} from "./ClaudeProvider.ts";
@@ -220,16 +222,8 @@
}
function getEffectiveClaudeAgentEffort(effort: string | null | undefined): ClaudeSdkEffort | null {
- if (!effort) {
- return null;
- }
- if (effort === "ultrathink") {
- return null;
- }
- if (effort === "xhigh") {
- return "max";
- }
- return effort as ClaudeSdkEffort;
+ const normalized = normalizeClaudeEffortForCli(effort);
+ return normalized ? (normalized as ClaudeSdkEffort) : null;
}
function isClaudeInterruptedMessage(message: string): boolean {
@@ -569,23 +563,10 @@
input.modelSelection?.provider === "claudeAgent" ? input.modelSelection.model : undefined;
const caps = getClaudeModelCapabilities(claudeModel);
- // For prompt injection, we check if the raw effort is a prompt-injected level (e.g. "ultrathink").
- // Normal Claude effort resolution strips prompt-injected values back to the model default,
- // so prompt formatting must look at the raw selection value directly.
const trimmedEffort = trimOrNull(rawEffort);
- const promptInjectedDescriptor = getProviderOptionDescriptors({ caps }).find(
- (descriptor) =>
- descriptor.type === "select" &&
- (descriptor.id === "effort" ||
- descriptor.id === "reasoningEffort" ||
- descriptor.id === "reasoning" ||
- descriptor.id === "variant") &&
- (descriptor.promptInjectedValues?.length ?? 0) > 0,
- );
+ const promptInjectedDescriptor = findPromptInjectedDescriptor(caps);
const promptEffort =
- trimmedEffort &&
- promptInjectedDescriptor?.type === "select" &&
- promptInjectedDescriptor.promptInjectedValues?.includes(trimmedEffort)
+ trimmedEffort && promptInjectedDescriptor?.promptInjectedValues?.includes(trimmedEffort)
? trimmedEffort
: null;
return applyClaudePromptEffortPrefix(input.input?.trim() ?? "", promptEffort);
diff --git a/apps/server/src/provider/Layers/ClaudeProvider.ts b/apps/server/src/provider/Layers/ClaudeProvider.ts
--- a/apps/server/src/provider/Layers/ClaudeProvider.ts
+++ b/apps/server/src/provider/Layers/ClaudeProvider.ts
@@ -221,6 +221,20 @@
return typeof value === "string" ? value : undefined;
}
+/**
+ * Maps a T3-internal effort value to a Claude CLI/SDK-compatible effort value.
+ * - `"xhigh"` (T3-internal level between high and max) maps to `"max"`.
+ * - `"ultrathink"` is prompt-injected only; returns `undefined` so the CLI
+ * flag is omitted entirely.
+ * - All other values pass through unchanged.
+ */
+export function normalizeClaudeEffortForCli(effort: string | null | undefined): string | undefined {
+ if (!effort) return undefined;
+ if (effort === "ultrathink") return undefined;
+ if (effort === "xhigh") return "max";
+ return effort;
+}
+
export function resolveClaudeApiModelId(modelSelection: ClaudeModelSelection): string {
switch (getModelSelectionOptionValue(modelSelection, "contextWindow")) {
case "1m":
diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -28,7 +28,7 @@
import {
applyClaudePromptEffortPrefix,
createModelSelection,
- getProviderOptionDescriptors,
+ findPromptInjectedDescriptor,
} from "@t3tools/shared/model";
import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts";
import { truncate } from "@t3tools/shared/String";
@@ -309,20 +309,8 @@
text: string;
}): string {
const caps = getProviderModelCapabilities(params.models, params.model, params.provider);
- const promptInjectedDescriptor = getProviderOptionDescriptors({ caps }).find(
- (descriptor) =>
- descriptor.type === "select" &&
- (descriptor.id === "reasoningEffort" ||
- descriptor.id === "effort" ||
- descriptor.id === "reasoning" ||
- descriptor.id === "variant") &&
- (descriptor.promptInjectedValues?.length ?? 0) > 0,
- );
- if (
- params.effort &&
- promptInjectedDescriptor?.type === "select" &&
- promptInjectedDescriptor.promptInjectedValues?.includes(params.effort)
- ) {
+ const promptInjectedDescriptor = findPromptInjectedDescriptor(caps);
+ if (params.effort && promptInjectedDescriptor?.promptInjectedValues?.includes(params.effort)) {
return applyClaudePromptEffortPrefix(params.text, params.effort);
}
return params.text;
diff --git a/packages/shared/src/model.ts b/packages/shared/src/model.ts
--- a/packages/shared/src/model.ts
+++ b/packages/shared/src/model.ts
@@ -292,6 +292,25 @@
} as ModelSelection;
}
+const PROMPT_INJECTED_DESCRIPTOR_IDS = new Set([
+ "effort",
+ "reasoningEffort",
+ "reasoning",
+ "variant",
+]);
+
+export function findPromptInjectedDescriptor(
+ caps: ModelCapabilities,
+): Extract<ProviderOptionDescriptor, { type: "select" }> | undefined {
+ const descriptor = getProviderOptionDescriptors({ caps }).find(
+ (d) =>
+ d.type === "select" &&
+ PROMPT_INJECTED_DESCRIPTOR_IDS.has(d.id) &&
+ (d.promptInjectedValues?.length ?? 0) > 0,
+ );
+ return descriptor?.type === "select" ? descriptor : undefined;
+}
+
export function applyClaudePromptEffortPrefix(
text: string,
effort: string | null | undefined,You can send follow-ups to the cloud agent here.
ApprovabilityVerdict: Needs human review Diff is too large for automated approval analysis. A human reviewer should evaluate this PR. You can customize Macroscope's approvability policy. Learn more. |
| readonly onCommit: (next: string) => void; | ||
| }; | ||
|
|
||
| /** |
There was a problem hiding this comment.
🟢 Low ui/draft-input.tsx:11
The spread order {...rest} {...bag} causes caller-provided onFocus, onBlur, and onKeyDown handlers to be silently overwritten by useCommitOnBlur's handlers. Since DraftInputProps extends InputProps (minus value/onChange/defaultValue), callers can legally pass these handlers, but they never execute at runtime. Consider composing the handlers so both the caller's and the internal handlers fire.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/components/ui/draft-input.tsx around line 11:
The spread order `{...rest} {...bag}` causes caller-provided `onFocus`, `onBlur`, and `onKeyDown` handlers to be silently overwritten by `useCommitOnBlur`'s handlers. Since `DraftInputProps` extends `InputProps` (minus `value`/`onChange`/`defaultValue`), callers can legally pass these handlers, but they never execute at runtime. Consider composing the handlers so both the caller's and the internal handlers fire.
Evidence trail:
apps/web/src/components/ui/draft-input.tsx lines 6-9 (DraftInputProps extends InputProps minus only value/onChange/defaultValue), line 20 (spread order `{...rest} {...bag}`); apps/web/src/components/ui/input.tsx lines 8-11 (InputProps includes all standard input props including onFocus/onBlur/onKeyDown); apps/web/src/hooks/useCommitOnBlur.ts lines 31-45 (hook returns onFocus, onBlur, onKeyDown handlers)
8a82b53 to
29261cf
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 4 total unresolved issues (including 2 from previous reviews).
Autofix Details
Bugbot Autofix prepared a fix for 1 of the 2 issues found in the latest run.
- ✅ Fixed: Prompt injection descriptor search overly broad, mismatched with effort source
- Extracted a shared
resolvePromptInjectedEfforthelper that iterates all descriptors and checks each one'spromptInjectedValuesindependently, replacing the fragilefind()across four candidate IDs in bothClaudeAdapter.tsandChatView.tsx.
- Extracted a shared
Or push these changes by commenting:
@cursor push 78fa16c7e7
Preview (78fa16c7e7)
diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts
--- a/apps/server/src/provider/Layers/ClaudeAdapter.ts
+++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts
@@ -43,9 +43,9 @@
} from "@t3tools/contracts";
import {
applyClaudePromptEffortPrefix,
+ getModelSelectionOptionValue,
getProviderOptionDescriptors,
- getModelSelectionOptionValue,
- trimOrNull,
+ resolvePromptInjectedEffort,
} from "@t3tools/shared/model";
import {
Cause,
@@ -569,25 +569,7 @@
input.modelSelection?.provider === "claudeAgent" ? input.modelSelection.model : undefined;
const caps = getClaudeModelCapabilities(claudeModel);
- // For prompt injection, we check if the raw effort is a prompt-injected level (e.g. "ultrathink").
- // Normal Claude effort resolution strips prompt-injected values back to the model default,
- // so prompt formatting must look at the raw selection value directly.
- const trimmedEffort = trimOrNull(rawEffort);
- const promptInjectedDescriptor = getProviderOptionDescriptors({ caps }).find(
- (descriptor) =>
- descriptor.type === "select" &&
- (descriptor.id === "effort" ||
- descriptor.id === "reasoningEffort" ||
- descriptor.id === "reasoning" ||
- descriptor.id === "variant") &&
- (descriptor.promptInjectedValues?.length ?? 0) > 0,
- );
- const promptEffort =
- trimmedEffort &&
- promptInjectedDescriptor?.type === "select" &&
- promptInjectedDescriptor.promptInjectedValues?.includes(trimmedEffort)
- ? trimmedEffort
- : null;
+ const promptEffort = resolvePromptInjectedEffort(caps, rawEffort);
return applyClaudePromptEffortPrefix(input.input?.trim() ?? "", promptEffort);
}
diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -28,7 +28,7 @@
import {
applyClaudePromptEffortPrefix,
createModelSelection,
- getProviderOptionDescriptors,
+ resolvePromptInjectedEffort,
} from "@t3tools/shared/model";
import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts";
import { truncate } from "@t3tools/shared/String";
@@ -309,23 +309,8 @@
text: string;
}): string {
const caps = getProviderModelCapabilities(params.models, params.model, params.provider);
- const promptInjectedDescriptor = getProviderOptionDescriptors({ caps }).find(
- (descriptor) =>
- descriptor.type === "select" &&
- (descriptor.id === "reasoningEffort" ||
- descriptor.id === "effort" ||
- descriptor.id === "reasoning" ||
- descriptor.id === "variant") &&
- (descriptor.promptInjectedValues?.length ?? 0) > 0,
- );
- if (
- params.effort &&
- promptInjectedDescriptor?.type === "select" &&
- promptInjectedDescriptor.promptInjectedValues?.includes(params.effort)
- ) {
- return applyClaudePromptEffortPrefix(params.text, params.effort);
- }
- return params.text;
+ const promptEffort = resolvePromptInjectedEffort(caps, params.effort);
+ return applyClaudePromptEffortPrefix(params.text, promptEffort);
}
const SCRIPT_TERMINAL_COLS = 120;
const SCRIPT_TERMINAL_ROWS = 30;
diff --git a/packages/shared/src/model.ts b/packages/shared/src/model.ts
--- a/packages/shared/src/model.ts
+++ b/packages/shared/src/model.ts
@@ -292,6 +292,29 @@
} as ModelSelection;
}
+/**
+ * Returns the effort value if it is a prompt-injected value according to
+ * any select descriptor in the given capabilities, or null otherwise.
+ *
+ * Unlike a single `find`, this checks every descriptor so that the
+ * correct descriptor's `promptInjectedValues` list is consulted even when
+ * multiple select descriptors exist.
+ */
+export function resolvePromptInjectedEffort(
+ caps: ModelCapabilities,
+ rawEffort: string | null | undefined,
+): string | null {
+ const trimmed = trimOrNull(rawEffort);
+ if (!trimmed) return null;
+ const descriptors = getProviderOptionDescriptors({ caps });
+ for (const descriptor of descriptors) {
+ if (descriptor.type === "select" && descriptor.promptInjectedValues?.includes(trimmed)) {
+ return trimmed;
+ }
+ }
+ return null;
+}
+
export function applyClaudePromptEffortPrefix(
text: string,
effort: string | null | undefined,You can send follow-ups to the cloud agent here.
Resolves conflict in apps/server/src/provider/Layers/OpenCodeProvider.ts: both sides added a constant after `const PROVIDER` (OPENCODE_PRESENTATION from our side, MINIMUM_OPENCODE_VERSION from main). Kept both, and added `presentation: OPENCODE_PRESENTATION` to the new too-old-version buildServerProvider call from main so it satisfies the required field introduced on this branch.
Migration 016 introduced modelSelection with `options` stored as a
per-provider object (e.g. `{ effort: "max", fastMode: true }`). The
recent contracts refactor reshaped `options` to an array of generic
`{ id, value }` selections. Stored rows from before the reshape still
carry the object shape and fail to decode with `Schema.fromJsonString(
ModelSelection)`.
This migration rewrites the legacy object shape into the canonical
array shape in three places:
- projection_threads.model_selection_json.options
- projection_projects.default_model_selection_json.options
- orchestration_events.payload_json modelSelection / defaultModelSelection
(thread.created, thread.meta-updated, thread.turn-start-requested,
project.created, project.meta-updated)
String values are kept if non-empty after trim; boolean values are
always kept; any other value type (nested objects, numbers, null) is
dropped. This matches the permissive client-side normalizer in
composerDraftStore.
Records whose `options` is already an array are left untouched.
…escriptor search The prompt injection descriptor lookup in both ClaudeAdapter.ts and ChatView.tsx used find() across four candidate descriptor IDs, which could match the wrong descriptor if multiple select descriptors had promptInjectedValues. Replace the fragile pattern with a shared helper that iterates all descriptors and checks each one's promptInjectedValues list independently.
|
Bugbot Autofix prepared fixes for both issues found in the latest run.
Or push these changes by commenting: Preview (ed70ae0e94)diff --git a/apps/server/integration/orchestrationEngine.integration.test.ts b/apps/server/integration/orchestrationEngine.integration.test.ts
--- a/apps/server/integration/orchestrationEngine.integration.test.ts
+++ b/apps/server/integration/orchestrationEngine.integration.test.ts
@@ -12,6 +12,7 @@
ProviderKind,
ThreadId,
ModelSelection,
+ ProviderInstanceId,
} from "@t3tools/contracts";
import { assert, it } from "@effect/vitest";
import { Effect, Option, Schema } from "effect";
@@ -116,7 +117,7 @@
title: "Integration Project",
workspaceRoot: harness.workspaceDir,
defaultModelSelection: {
- provider,
+ instanceId: ProviderInstanceId.make(provider),
model: defaultModel,
},
createdAt,
@@ -129,7 +130,7 @@
projectId: PROJECT_ID,
title: "Integration Thread",
modelSelection: {
- provider,
+ instanceId: ProviderInstanceId.make(provider),
model: defaultModel,
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
@@ -265,7 +266,7 @@
title: "Integration Project",
workspaceRoot: harness.workspaceDir,
defaultModelSelection: {
- provider: "codex",
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5.3-codex",
},
createdAt,
@@ -278,7 +279,7 @@
projectId: PROJECT_ID,
title: "Integration Thread",
modelSelection: {
- provider: "codex",
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5.3-codex",
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
@@ -939,7 +940,7 @@
messageId: "msg-user-claude-initial",
text: "Use Claude",
modelSelection: {
- provider: "claudeAgent",
+ instanceId: ProviderInstanceId.make("claudeAgent"),
model: "claude-sonnet-4-6",
},
});
@@ -996,7 +997,7 @@
messageId: "msg-user-claude-recover-1",
text: "Before restart",
modelSelection: {
- provider: "claudeAgent",
+ instanceId: ProviderInstanceId.make("claudeAgent"),
model: "claude-sonnet-4-6",
},
});
@@ -1106,7 +1107,7 @@
messageId: "msg-user-claude-approval",
text: "Need approval",
modelSelection: {
- provider: "claudeAgent",
+ instanceId: ProviderInstanceId.make("claudeAgent"),
model: "claude-sonnet-4-6",
},
});
@@ -1178,7 +1179,7 @@
messageId: "msg-user-claude-interrupt",
text: "Start long turn",
modelSelection: {
- provider: "claudeAgent",
+ instanceId: ProviderInstanceId.make("claudeAgent"),
model: "claude-sonnet-4-6",
},
});
@@ -1251,7 +1252,7 @@
messageId: "msg-user-claude-revert-1",
text: "First Claude edit",
modelSelection: {
- provider: "claudeAgent",
+ instanceId: ProviderInstanceId.make("claudeAgent"),
model: "claude-sonnet-4-6",
},
});
diff --git a/apps/server/src/git/Layers/ClaudeTextGeneration.test.ts b/apps/server/src/git/Layers/ClaudeTextGeneration.test.ts
--- a/apps/server/src/git/Layers/ClaudeTextGeneration.test.ts
+++ b/apps/server/src/git/Layers/ClaudeTextGeneration.test.ts
@@ -1,3 +1,4 @@
+import { ProviderInstanceId } from "@t3tools/contracts";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { it } from "@effect/vitest";
import { Effect, FileSystem, Layer, Path } from "effect";
@@ -264,7 +265,7 @@
cwd: process.cwd(),
message: "Please investigate reconnect failures after restarting the session.",
modelSelection: {
- provider: "claudeAgent",
+ instanceId: ProviderInstanceId.make("claudeAgent"),
model: "claude-sonnet-4-6",
},
});
@@ -294,7 +295,7 @@
cwd: process.cwd(),
message: "Name this thread.",
modelSelection: {
- provider: "claudeAgent",
+ instanceId: ProviderInstanceId.make("claudeAgent"),
model: "claude-sonnet-4-6",
},
});
diff --git a/apps/server/src/git/Layers/ClaudeTextGeneration.ts b/apps/server/src/git/Layers/ClaudeTextGeneration.ts
--- a/apps/server/src/git/Layers/ClaudeTextGeneration.ts
+++ b/apps/server/src/git/Layers/ClaudeTextGeneration.ts
@@ -10,7 +10,7 @@
import { Effect, Layer, Option, Schema, Stream } from "effect";
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
-import { ClaudeModelSelection } from "@t3tools/contracts";
+import { type ModelSelection } from "@t3tools/contracts";
import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git";
import { TextGenerationError } from "@t3tools/contracts";
@@ -28,7 +28,7 @@
sanitizeThreadTitle,
toJsonSchemaObject,
} from "../Utils.ts";
-import { getProviderOptionCurrentValue, getProviderOptionDescriptors } from "@t3tools/shared/model";
+import { getModelSelectionOptionValue, getProviderOptionDescriptors } from "@t3tools/shared/model";
import {
getClaudeModelCapabilities,
resolveClaudeApiModelId,
@@ -84,7 +84,7 @@
cwd: string;
prompt: string;
outputSchemaJson: S;
- modelSelection: ClaudeModelSelection;
+ modelSelection: ModelSelection;
}): Effect.fn.Return<S["Type"], TextGenerationError, S["DecodingServices"]> {
const jsonSchemaStr = JSON.stringify(toJsonSchemaObject(outputSchemaJson));
const caps = getClaudeModelCapabilities(modelSelection.model);
@@ -92,12 +92,13 @@
caps,
selections: modelSelection.options,
});
- const findDescriptor = (id: string) => descriptors.find((descriptor) => descriptor.id === id);
- const rawEffortValue = getProviderOptionCurrentValue(findDescriptor("effort"));
- const rawEffort = typeof rawEffortValue === "string" ? rawEffortValue : undefined;
- const resolvedEffort = resolveClaudeEffort(caps, rawEffort);
- const thinkingDescriptor = findDescriptor("thinking");
- const fastModeDescriptor = findDescriptor("fastMode");
+ const rawEffort = getModelSelectionOptionValue(modelSelection, "effort");
+ const resolvedEffort = resolveClaudeEffort(
+ caps,
+ typeof rawEffort === "string" ? rawEffort : undefined,
+ );
+ const thinkingDescriptor = descriptors.find((descriptor) => descriptor.id === "thinking");
+ const fastModeDescriptor = descriptors.find((descriptor) => descriptor.id === "fastMode");
const thinking =
thinkingDescriptor?.type === "boolean" ? thinkingDescriptor.currentValue : undefined;
const fastMode =
@@ -228,7 +229,7 @@
includeBranch: input.includeBranch === true,
});
- if (input.modelSelection.provider !== "claudeAgent") {
+ if (input.modelSelection.instanceId !== "claudeAgent") {
return yield* new TextGenerationError({
operation: "generateCommitMessage",
detail: "Invalid model selection.",
@@ -263,7 +264,7 @@
diffPatch: input.diffPatch,
});
- if (input.modelSelection.provider !== "claudeAgent") {
+ if (input.modelSelection.instanceId !== "claudeAgent") {
return yield* new TextGenerationError({
operation: "generatePrContent",
detail: "Invalid model selection.",
@@ -292,7 +293,7 @@
attachments: input.attachments,
});
- if (input.modelSelection.provider !== "claudeAgent") {
+ if (input.modelSelection.instanceId !== "claudeAgent") {
return yield* new TextGenerationError({
operation: "generateBranchName",
detail: "Invalid model selection.",
@@ -320,7 +321,7 @@
attachments: input.attachments,
});
- if (input.modelSelection.provider !== "claudeAgent") {
+ if (input.modelSelection.instanceId !== "claudeAgent") {
return yield* new TextGenerationError({
operation: "generateThreadTitle",
detail: "Invalid model selection.",
diff --git a/apps/server/src/git/Layers/CodexTextGeneration.test.ts b/apps/server/src/git/Layers/CodexTextGeneration.test.ts
--- a/apps/server/src/git/Layers/CodexTextGeneration.test.ts
+++ b/apps/server/src/git/Layers/CodexTextGeneration.test.ts
@@ -10,10 +10,7 @@
import { TextGeneration } from "../Services/TextGeneration.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
-const DEFAULT_TEST_MODEL_SELECTION = {
- provider: "codex" as const,
- model: "gpt-5.4-mini",
-};
+const DEFAULT_TEST_MODEL_SELECTION = createModelSelection("codex", "gpt-5.4-mini");
const CodexTextGenerationTestLayer = CodexTextGenerationLive.pipe(
Layer.provideMerge(ServerSettingsService.layerTest()),
diff --git a/apps/server/src/git/Layers/CodexTextGeneration.ts b/apps/server/src/git/Layers/CodexTextGeneration.ts
--- a/apps/server/src/git/Layers/CodexTextGeneration.ts
+++ b/apps/server/src/git/Layers/CodexTextGeneration.ts
@@ -3,7 +3,7 @@
import { Effect, FileSystem, Layer, Option, Path, Schema, Scope, Stream } from "effect";
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
-import { CodexModelSelection } from "@t3tools/contracts";
+import { type ModelSelection } from "@t3tools/contracts";
import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git";
import { resolveAttachmentPath } from "../../attachmentStore.ts";
@@ -140,7 +140,7 @@
outputSchemaJson: S;
imagePaths?: ReadonlyArray<string>;
cleanupPaths?: ReadonlyArray<string>;
- modelSelection: CodexModelSelection;
+ modelSelection: ModelSelection;
}): Effect.fn.Return<S["Type"], TextGenerationError, S["DecodingServices"]> {
const schemaPath = yield* writeTempFile(
operation,
@@ -285,7 +285,7 @@
includeBranch: input.includeBranch === true,
});
- if (input.modelSelection.provider !== "codex") {
+ if (input.modelSelection.instanceId !== "codex") {
return yield* new TextGenerationError({
operation: "generateCommitMessage",
detail: "Invalid model selection.",
@@ -320,7 +320,7 @@
diffPatch: input.diffPatch,
});
- if (input.modelSelection.provider !== "codex") {
+ if (input.modelSelection.instanceId !== "codex") {
return yield* new TextGenerationError({
operation: "generatePrContent",
detail: "Invalid model selection.",
@@ -353,7 +353,7 @@
attachments: input.attachments,
});
- if (input.modelSelection.provider !== "codex") {
+ if (input.modelSelection.instanceId !== "codex") {
return yield* new TextGenerationError({
operation: "generateBranchName",
detail: "Invalid model selection.",
@@ -386,7 +386,7 @@
attachments: input.attachments,
});
- if (input.modelSelection.provider !== "codex") {
+ if (input.modelSelection.instanceId !== "codex") {
return yield* new TextGenerationError({
operation: "generateThreadTitle",
detail: "Invalid model selection.",
diff --git a/apps/server/src/git/Layers/CursorTextGeneration.test.ts b/apps/server/src/git/Layers/CursorTextGeneration.test.ts
--- a/apps/server/src/git/Layers/CursorTextGeneration.test.ts
+++ b/apps/server/src/git/Layers/CursorTextGeneration.test.ts
@@ -9,7 +9,7 @@
import { createModelSelection } from "@t3tools/shared/model";
import { expect } from "vitest";
-import { ServerSettingsError } from "@t3tools/contracts";
+import { ServerSettingsError, ProviderInstanceId } from "@t3tools/contracts";
import { ServerConfig } from "../../config.ts";
import { TextGeneration } from "../Services/TextGeneration.ts";
@@ -223,7 +223,7 @@
stagedSummary: "M README.md",
stagedPatch: "diff --git a/README.md b/README.md",
modelSelection: {
- provider: "cursor",
+ instanceId: ProviderInstanceId.make("cursor"),
model: "composer-2",
},
});
@@ -248,7 +248,7 @@
cwd: process.cwd(),
message: "Fix the reconnect spinner after a resumed session.",
modelSelection: {
- provider: "cursor",
+ instanceId: ProviderInstanceId.make("cursor"),
model: "composer-2",
},
});
@@ -280,7 +280,7 @@
stagedPatch:
"diff --git a/apps/server/src/git/Layers/CursorTextGeneration.ts b/apps/server/src/git/Layers/CursorTextGeneration.ts",
modelSelection: {
- provider: "cursor",
+ instanceId: ProviderInstanceId.make("cursor"),
model: "composer-2",
},
});
diff --git a/apps/server/src/git/Layers/CursorTextGeneration.ts b/apps/server/src/git/Layers/CursorTextGeneration.ts
--- a/apps/server/src/git/Layers/CursorTextGeneration.ts
+++ b/apps/server/src/git/Layers/CursorTextGeneration.ts
@@ -1,7 +1,7 @@
import { Effect, Layer, Option, Ref, Schema } from "effect";
import { ChildProcessSpawner } from "effect/unstable/process";
-import { CursorModelSelection } from "@t3tools/contracts";
+import { type ModelSelection } from "@t3tools/contracts";
import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git";
import { TextGenerationError } from "@t3tools/contracts";
@@ -74,7 +74,7 @@
cwd: string;
prompt: string;
outputSchemaJson: S;
- modelSelection: CursorModelSelection;
+ modelSelection: ModelSelection;
}): Effect.Effect<S["Type"], TextGenerationError, S["DecodingServices"]> =>
Effect.gen(function* () {
const cursorSettings = yield* Effect.map(
@@ -186,7 +186,7 @@
includeBranch: input.includeBranch === true,
});
- if (input.modelSelection.provider !== "cursor") {
+ if (input.modelSelection.instanceId !== "cursor") {
return yield* new TextGenerationError({
operation: "generateCommitMessage",
detail: "Invalid model selection.",
@@ -221,7 +221,7 @@
diffPatch: input.diffPatch,
});
- if (input.modelSelection.provider !== "cursor") {
+ if (input.modelSelection.instanceId !== "cursor") {
return yield* new TextGenerationError({
operation: "generatePrContent",
detail: "Invalid model selection.",
@@ -250,7 +250,7 @@
attachments: input.attachments,
});
- if (input.modelSelection.provider !== "cursor") {
+ if (input.modelSelection.instanceId !== "cursor") {
return yield* new TextGenerationError({
operation: "generateBranchName",
detail: "Invalid model selection.",
@@ -278,7 +278,7 @@
attachments: input.attachments,
});
- if (input.modelSelection.provider !== "cursor") {
+ if (input.modelSelection.instanceId !== "cursor") {
return yield* new TextGenerationError({
operation: "generateThreadTitle",
detail: "Invalid model selection.",
diff --git a/apps/server/src/git/Layers/OpenCodeTextGeneration.test.ts b/apps/server/src/git/Layers/OpenCodeTextGeneration.test.ts
--- a/apps/server/src/git/Layers/OpenCodeTextGeneration.test.ts
+++ b/apps/server/src/git/Layers/OpenCodeTextGeneration.test.ts
@@ -1,3 +1,4 @@
+import { ProviderInstanceId } from "@t3tools/contracts";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { it } from "@effect/vitest";
import { Duration, Effect, Layer } from "effect";
@@ -97,7 +98,7 @@
};
const DEFAULT_TEST_MODEL_SELECTION = {
- provider: "opencode" as const,
+ instanceId: ProviderInstanceId.make("opencode"),
model: "openai/gpt-5",
};
diff --git a/apps/server/src/git/Layers/OpenCodeTextGeneration.ts b/apps/server/src/git/Layers/OpenCodeTextGeneration.ts
--- a/apps/server/src/git/Layers/OpenCodeTextGeneration.ts
+++ b/apps/server/src/git/Layers/OpenCodeTextGeneration.ts
@@ -1,11 +1,7 @@
import { Effect, Exit, Fiber, Layer, Schema, Scope } from "effect";
import * as Semaphore from "effect/Semaphore";
-import {
- TextGenerationError,
- type ChatAttachment,
- type OpenCodeModelSelection,
-} from "@t3tools/contracts";
+import { TextGenerationError, type ChatAttachment, type ModelSelection } from "@t3tools/contracts";
import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git";
import { getModelSelectionOptionValue } from "@t3tools/shared/model";
@@ -267,7 +263,7 @@
readonly cwd: string;
readonly prompt: string;
readonly outputSchemaJson: S;
- readonly modelSelection: OpenCodeModelSelection;
+ readonly modelSelection: ModelSelection;
readonly attachments?: ReadonlyArray<ChatAttachment> | undefined;
}) {
const parsedModel = parseOpenCodeModelSlug(input.modelSelection.model);
@@ -384,7 +380,7 @@
const generateCommitMessage: TextGenerationShape["generateCommitMessage"] = Effect.fn(
"OpenCodeTextGeneration.generateCommitMessage",
)(function* (input) {
- if (input.modelSelection.provider !== "opencode") {
+ if (input.modelSelection.instanceId !== "opencode") {
return yield* new TextGenerationError({
operation: "generateCommitMessage",
detail: "Invalid model selection.",
@@ -417,7 +413,7 @@
const generatePrContent: TextGenerationShape["generatePrContent"] = Effect.fn(
"OpenCodeTextGeneration.generatePrContent",
)(function* (input) {
- if (input.modelSelection.provider !== "opencode") {
+ if (input.modelSelection.instanceId !== "opencode") {
return yield* new TextGenerationError({
operation: "generatePrContent",
detail: "Invalid model selection.",
@@ -448,7 +444,7 @@
const generateBranchName: TextGenerationShape["generateBranchName"] = Effect.fn(
"OpenCodeTextGeneration.generateBranchName",
)(function* (input) {
- if (input.modelSelection.provider !== "opencode") {
+ if (input.modelSelection.instanceId !== "opencode") {
return yield* new TextGenerationError({
operation: "generateBranchName",
detail: "Invalid model selection.",
@@ -476,7 +472,7 @@
const generateThreadTitle: TextGenerationShape["generateThreadTitle"] = Effect.fn(
"OpenCodeTextGeneration.generateThreadTitle",
)(function* (input) {
- if (input.modelSelection.provider !== "opencode") {
+ if (input.modelSelection.instanceId !== "opencode") {
return yield* new TextGenerationError({
operation: "generateThreadTitle",
detail: "Invalid model selection.",
diff --git a/apps/server/src/git/Layers/RoutingTextGeneration.ts b/apps/server/src/git/Layers/RoutingTextGeneration.ts
--- a/apps/server/src/git/Layers/RoutingTextGeneration.ts
+++ b/apps/server/src/git/Layers/RoutingTextGeneration.ts
@@ -3,7 +3,7 @@
* Codex CLI or Claude CLI implementation based on the provider in each
* request input.
*
- * When `modelSelection.provider` is `"claudeAgent"` the request is forwarded to
+ * When `modelSelection.instanceId` resolves to a `"claudeAgent"` driver the request is forwarded to
* the Claude layer; for any other value (including the default `undefined`) it
* falls through to the Codex layer.
*
@@ -11,6 +11,8 @@
*/
import { Effect, Layer, Context } from "effect";
+import { isBuiltInDriverId, TextGenerationError } from "@t3tools/contracts";
+
import { TextGeneration, type TextGenerationShape } from "../Services/TextGeneration.ts";
import { CodexTextGenerationLive } from "./CodexTextGeneration.ts";
import { ClaudeTextGenerationLive } from "./ClaudeTextGeneration.ts";
@@ -47,17 +49,45 @@
claudeAgent: yield* ClaudeTextGen,
cursor: yield* CursorTextGen,
opencode: yield* OpenCodeTextGen,
+ } as const;
+
+ // `ModelSelection.provider` is an open driver-id slug — it may name a
+ // driver this build doesn't ship (fork / rollback case). Surface that
+ // as a structured `TextGenerationError` instead of an `undefined`-key
+ // crash; callers can decide whether to retry under a different driver
+ // or surface "driver not installed" to the user.
+ const route = <Op extends keyof TextGenerationShape>(
+ operation: Op,
+ provider: string,
+ ): Effect.Effect<TextGenerationShape[Op], TextGenerationError, never> => {
+ if (!isBuiltInDriverId(provider)) {
+ return Effect.fail(
+ new TextGenerationError({
+ operation,
+ detail: `No text-generation driver registered for provider "${provider}".`,
+ }),
+ );
+ }
+ return Effect.succeed(byProvider[provider][operation]);
};
return {
generateCommitMessage: (input) =>
- byProvider[input.modelSelection.provider].generateCommitMessage(input),
+ route("generateCommitMessage", input.modelSelection.instanceId).pipe(
+ Effect.flatMap((fn) => fn(input)),
+ ),
generatePrContent: (input) =>
- byProvider[input.modelSelection.provider].generatePrContent(input),
+ route("generatePrContent", input.modelSelection.instanceId).pipe(
+ Effect.flatMap((fn) => fn(input)),
+ ),
generateBranchName: (input) =>
- byProvider[input.modelSelection.provider].generateBranchName(input),
+ route("generateBranchName", input.modelSelection.instanceId).pipe(
+ Effect.flatMap((fn) => fn(input)),
+ ),
generateThreadTitle: (input) =>
- byProvider[input.modelSelection.provider].generateThreadTitle(input),
+ route("generateThreadTitle", input.modelSelection.instanceId).pipe(
+ Effect.flatMap((fn) => fn(input)),
+ ),
} satisfies TextGenerationShape;
});
diff --git a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts
--- a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts
+++ b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts
@@ -3,8 +3,13 @@
import path from "node:path";
import { execFileSync } from "node:child_process";
-import type { ProviderKind, ProviderRuntimeEvent, ProviderSession } from "@t3tools/contracts";
import {
+ ProviderKind,
+ ProviderRuntimeEvent,
+ ProviderSession,
+ ProviderInstanceId,
+} from "@t3tools/contracts";
+import {
CommandId,
DEFAULT_PROVIDER_INTERACTION_MODE,
EventId,
@@ -315,7 +320,7 @@
title: "Test Project",
workspaceRoot: options?.projectWorkspaceRoot ?? cwd,
defaultModelSelection: {
- provider: "codex",
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
createdAt,
@@ -329,7 +334,7 @@
projectId: asProjectId("project-1"),
title: "Thread",
modelSelection: {
- provider: "codex",
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
diff --git a/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts b/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts
--- a/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts
+++ b/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts
@@ -7,6 +7,7 @@
ThreadId,
TurnId,
type OrchestrationEvent,
+ ProviderInstanceId,
} from "@t3tools/contracts";
import { Effect, Layer, ManagedRuntime, Metric, Option, Queue, Stream } from "effect";
import { describe, expect, it } from "vitest";
@@ -104,7 +105,7 @@
title: "Bootstrap Project",
workspaceRoot: "/tmp/project-bootstrap",
defaultModelSelection: {
- provider: "codex" as const,
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
scripts: [],
@@ -119,7 +120,7 @@
projectId: asProjectId("project-bootstrap"),
title: "Bootstrap Thread",
modelSelection: {
- provider: "codex" as const,
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
@@ -198,7 +199,7 @@
title: "Project 1",
workspaceRoot: "/tmp/project-1",
defaultModelSelection: {
- provider: "codex",
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
createdAt,
@@ -212,7 +213,7 @@
projectId: asProjectId("project-1"),
title: "Thread",
modelSelection: {
- provider: "codex",
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
@@ -258,7 +259,7 @@
title: "Project Archive",
workspaceRoot: "/tmp/project-archive",
defaultModelSelection: {
- provider: "codex",
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
createdAt,
@@ -272,7 +273,7 @@
projectId: asProjectId("project-archive"),
title: "Archive me",
modelSelection: {
- provider: "codex",
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
@@ -325,7 +326,7 @@
title: "Replay Project",
workspaceRoot: "/tmp/project-replay",
defaultModelSelection: {
- provider: "codex",
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
createdAt,
@@ -339,7 +340,7 @@
projectId: asProjectId("project-replay"),
title: "replay",
modelSelection: {
- provider: "codex",
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
@@ -383,7 +384,7 @@
title: "Stream Project",
workspaceRoot: "/tmp/project-stream",
defaultModelSelection: {
- provider: "codex",
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
createdAt,
@@ -407,7 +408,7 @@
projectId: asProjectId("project-stream"),
title: "domain-stream",
modelSelection: {
- provider: "codex",
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
@@ -444,7 +445,7 @@
title: "Ack Project",
workspaceRoot: "/tmp/project-ack",
defaultModelSelection: {
- provider: "codex",
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
createdAt,
@@ -459,7 +460,7 @@
projectId: asProjectId("project-ack"),
title: "Ack Thread",
modelSelection: {
- provider: "codex",
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
@@ -496,7 +497,7 @@
projectId: asProjectId("project-missing"),
title: "Missing Project Thread",
modelSelection: {
- provider: "codex",
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
@@ -533,7 +534,7 @@
title: "Turn Diff Project",
workspaceRoot: "/tmp/project-turn-diff",
defaultModelSelection: {
- provider: "codex",
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
createdAt,
@@ -547,7 +548,7 @@
projectId: asProjectId("project-turn-diff"),
title: "Turn diff thread",
modelSelection: {
- provider: "codex",
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
@@ -652,7 +653,7 @@
title: "Flaky Project",
workspaceRoot: "/tmp/project-flaky",
defaultModelSelection: {
- provider: "codex",
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
createdAt,
@@ -668,7 +669,7 @@
projectId: asProjectId("project-flaky"),
title: "flaky-fail",
modelSelection: {
- provider: "codex",
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
@@ -688,7 +689,7 @@
projectId: asProjectId("project-flaky"),
title: "flaky-ok",
modelSelection: {
- provider: "codex",
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
@@ -747,7 +748,7 @@
title: "Atomic Project",
workspaceRoot: "/tmp/project-atomic",
defaultModelSelection: {
- provider: "codex",
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
createdAt,
@@ -761,7 +762,7 @@
projectId: asProjectId("project-atomic"),
title: "atomic",
modelSelection: {
- provider: "codex",
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
@@ -890,7 +891,7 @@
title: "Sync Project",
workspaceRoot: "/tmp/project-sync",
defaultModelSelection: {
- provider: "codex",
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
createdAt,
@@ -904,7 +905,7 @@
projectId: asProjectId("project-sync"),
title: "sync-before",
modelSelection: {
- provider: "codex",
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
@@ -975,7 +976,7 @@
title: "Duplicate Project",
workspaceRoot: "/tmp/project-duplicate",
defaultModelSelection: {
- provider: "codex",
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
createdAt,
@@ -990,7 +991,7 @@
projectId: asProjectId("project-duplicate"),
title: "duplicate",
modelSelection: {
- provider: "codex",
+ instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
@@ -1010,7 +1011,7 @@
projectId: asProjectId("project-duplicate"),
title: "duplicate",
modelSelection: {
- provider: "codex",
... diff truncated: showing 800 of 6098 linesYou can send follow-ups to the cloud agent here. |
…escriptor search The prompt injection descriptor lookup in both ClaudeAdapter.ts and ChatView.tsx used find() across four candidate descriptor IDs, which could match the wrong descriptor if multiple select descriptors had promptInjectedValues. Replace the fragile pattern with a shared helper that iterates all descriptors and checks each one's promptInjectedValues list independently. Applied via @cursor push command
|
Could not push Autofix changes. The PR branch may have changed since the Autofix ran, or the Autofix commit may no longer exist. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Unused
fakeOpenCodeSnapshotvariable in test file- Removed the unused
fakeOpenCodeSnapshotconstant and itsServerProvidertype annotation (which is still used elsewhere) from the test file.
- Removed the unused
Or push these changes by commenting:
@cursor push 0f34ecdfc8
Preview (0f34ecdfc8)
diff --git a/apps/server/src/provider/Layers/ProviderRegistry.test.ts b/apps/server/src/provider/Layers/ProviderRegistry.test.ts
--- a/apps/server/src/provider/Layers/ProviderRegistry.test.ts
+++ b/apps/server/src/provider/Layers/ProviderRegistry.test.ts
@@ -54,20 +54,6 @@
};
}
-const fakeOpenCodeSnapshot: ServerProvider = {
- provider: "opencode",
- status: "warning",
- enabled: true,
- installed: false,
- auth: { status: "unknown" },
- checkedAt: "2026-03-25T00:00:00.000Z",
- version: null,
- models: [],
- slashCommands: [],
- skills: [],
- message: "OpenCode test stub",
-};
-
function mockHandle(result: { stdout: string; stderr: string; code: number }) {
return ChildProcessSpawner.makeHandle({
pid: ChildProcessSpawner.ProcessId(1),You can send follow-ups to the cloud agent here.
- Fix Format CI: apply oxfmt to 026_CanonicalizeModelSelectionOptions.test.ts - Add shared normalizeClaudeCliEffort() in ClaudeProvider that maps the Opus 4.7 capability "xhigh" to the CLI-accepted "max" and filters "ultrathink" (prompt-prefix mode). Use it in ClaudeTextGeneration before passing --effort, fixing the medium-severity bug where resolveClaudeEffort's raw output could land on the CLI unmapped. - Re-point ClaudeAdapter's getEffectiveClaudeAgentEffort at the shared normalizer so SDK and CLI paths can't drift. - Simplify ClaudeTextGeneration effort resolution: read the raw selection via getModelSelectionOptionValue (matching ClaudeAdapter's pattern) instead of going through descriptors + currentValue, then passing to resolveClaudeEffort a second time. Resolves the low-severity "double effort resolution / misleading rawEffortValue naming" comment. - Remove unused fakeOpenCodeSnapshot fixture from ProviderRegistry.test.
The pre-refactor test asserted `toMatchObject({ options: { fastMode: true } })`
against an object-shaped `options`, which tolerated extra keys like
`reasoningEffort`. When the refactor converted `options` to a generic array
of `{ id, value }` selections the assertion was mechanically rewritten, but
`toMatchObject` compares arrays strictly (length + positional match), so the
extra sticky `reasoningEffort` entry now fails the match even though runtime
behaviour is unchanged.
Wrap the `options` array in `expect.arrayContaining([...])` so the assertion
keeps its original intent — "verify the sticky `fastMode` trait carries into
the new draft" — while allowing other sticky entries that previously flew
under the object-match radar.
No production code change; only ChatView.browser.tsx assertions adjusted.
- Add typed string/boolean selection accessors - Update provider adapters to use typed helpers - Cover selection helper behavior with tests Co-authored-by: codex <codex@users.noreply.github.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Default effort always sent when user selects none
- Added early return of undefined in resolveClaudeEffort when raw is null/undefined, preventing the function from falling through to getProviderOptionCurrentValue which would return the model's default effort.
Or push these changes by commenting:
@cursor push 93c24a8689
Preview (93c24a8689)
diff --git a/apps/server/src/provider/Layers/ClaudeProvider.ts b/apps/server/src/provider/Layers/ClaudeProvider.ts
--- a/apps/server/src/provider/Layers/ClaudeProvider.ts
+++ b/apps/server/src/provider/Layers/ClaudeProvider.ts
@@ -212,9 +212,12 @@
caps: ModelCapabilities,
raw: string | null | undefined,
): string | undefined {
+ if (!raw) {
+ return undefined;
+ }
const descriptors = getProviderOptionDescriptors({
caps,
- ...(raw ? { selections: [{ id: "effort", value: raw }] } : {}),
+ selections: [{ id: "effort", value: raw }],
});
const effortDescriptor = descriptors.find((descriptor) => descriptor.id === "effort");
const value = getProviderOptionCurrentValue(effortDescriptor);You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 8b2ab13. Configure here.
- Decode old object-shaped `options` into canonical arrays - Cover settings and orchestration round trips
Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: cursor[bot] <206951365+cursor[bot]@users.noreply.github.com> Co-authored-by: codex <codex@users.noreply.github.com> (cherry picked from commit 8d1d699)


Summary
Testing
bun fmtbun lintbun typecheckbun run testNote
Medium Risk
Broad refactor across provider adapters, text generation, and web composer plus a DB migration that rewrites persisted
modelSelection.options; mistakes could break provider routing or silently drop/ignore option values for existing users.Overview
Refactors provider/model configuration so
modelSelection.optionsis now a generic ordered array of{ id, value }selections, replacing provider-specific option objects across server providers (Claude/Codex/Cursor/OpenCode), orchestration routing, and web composer state.Provider capability reporting is reshaped around shared option descriptors (via
createModelCapabilities/descriptor builders), and provider snapshots now include presentation metadata (displayName, optionalbadgeLabel, andshowInteractionModeToggle).Adds migration
026_CanonicalizeModelSelectionOptionsto convert persisted legacy object-shapedoptionsin projections and orchestration events into the new array format (dropping non-boolean/non-nonempty-string values), and updates tests to usecreateModelSelectionand new descriptor semantics.Reviewed by Cursor Bugbot for commit bf23358. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Refactor provider model selections from typed option objects to generic
ProviderOptionSelectionarraysCodexModelOptions,ClaudeModelOptions,CursorModelOptions, etc.) with a unifiedProviderOptionSelectionarray ({id, value}[]) across contracts, server, and web layers.ProviderOptionDescriptortypes (select/boolean) toModelCapabilities, and introduces helpers likegetProviderOptionDescriptors,buildProviderOptionSelectionsFromDescriptors, andcreateModelCapabilitiesto drive UI and adapter logic from descriptors instead of hard-coded provider checks.reasoningEffortLevels,supportsFastMode,supportsThinkingToggle, etc.) and provider-specific normalizers;TraitsPickerandChatComposernow render and dispatch options driven entirely by descriptors.displayName,badgeLabel, andshowInteractionModeTogglepresentation fields surfaced from server to UI.RoutingTextGenerationno longer falls back to Codex for unrecognized providers — requests with unknown providers will fail at runtime. Draft store version bumped to 6; persisted drafts with old option shapes are coerced on load viacoerceProviderOptionSelections.Macroscope summarized bf23358.