authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-07-21 17:13:44-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-21 17:13:44-07:00
log62cf4617594c2a0d8c9c5dc4bbc2012d0ead802d
tree1bafc0486a2dd7fbaebe765f4783bd9a4e935aa7
parent23c18fda7a969634a30888e36b2da45f6d66a83b
signature Signed by PGP key B5690EEEBB952194

fix: better defaults — Claude 1M context, Codex gpt-5.6, worktrees from origin main (#4240)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

17 files changed, 218 insertions(+), 50 deletions(-)

apps/mobile/src/features/threads/new-task-flow-provider.tsx+29-10
...@@ -58,6 +58,8 @@ import { type VcsRef } from "@t3tools/client-runtime/state/vcs";...@@ -58,6 +58,8 @@ import { type VcsRef } from "@t3tools/client-runtime/state/vcs";
5858
59type WorkspaceMode = "local" | "worktree";59type WorkspaceMode = "local" | "worktree";
6060
61const EMPTY_BRANCH_REFS: ReadonlyArray<VcsRef> = [];
62
61function pendingTaskDraftKey(messageId: string): string {63function pendingTaskDraftKey(messageId: string): string {
62 return `pending-task:${messageId}`;64 return `pending-task:${messageId}`;
63}65}
...@@ -348,7 +350,14 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {...@@ -348,7 +350,14 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
348 const workspaceMode = selectedProjectDraft.workspaceSelection?.mode ?? "local";350 const workspaceMode = selectedProjectDraft.workspaceSelection?.mode ?? "local";
349 const selectedBranchName = selectedProjectDraft.workspaceSelection?.branch ?? null;351 const selectedBranchName = selectedProjectDraft.workspaceSelection?.branch ?? null;
350 const selectedWorktreePath = selectedProjectDraft.workspaceSelection?.worktreePath ?? null;352 const selectedWorktreePath = selectedProjectDraft.workspaceSelection?.worktreePath ?? null;
351 const startFromOrigin = selectedProjectDraft.workspaceSelection?.startFromOrigin ?? false;353 // Keep the user's explicit choice separate from the resolved display value:
354 // only the explicit flag is ever written back to the draft, so the resolved
355 // value keeps tracking the server setting when the config loads late.
356 const draftStartFromOrigin = selectedProjectDraft.workspaceSelection?.startFromOrigin;
357 const startFromOrigin =
358 draftStartFromOrigin ??
359 selectedEnvironmentServerConfig?.settings.newWorktreesStartFromOrigin ??
360 true;
352 const runtimeMode = selectedProjectDraft.runtimeMode ?? DEFAULT_RUNTIME_MODE;361 const runtimeMode = selectedProjectDraft.runtimeMode ?? DEFAULT_RUNTIME_MODE;
353 const interactionMode = selectedProjectDraft.interactionMode ?? DEFAULT_PROVIDER_INTERACTION_MODE;362 const interactionMode = selectedProjectDraft.interactionMode ?? DEFAULT_PROVIDER_INTERACTION_MODE;
354363
...@@ -368,6 +377,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {...@@ -368,6 +377,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
368 const selectedModel =377 const selectedModel =
369 selectedProjectDraft.modelSelection ??378 selectedProjectDraft.modelSelection ??
370 selectedProject?.defaultModelSelection ??379 selectedProject?.defaultModelSelection ??
380 modelOptions.find((option) => option.isDefault)?.selection ??
371 modelOptions[0]?.selection ??381 modelOptions[0]?.selection ??
372 null;382 null;
373 const selectedModelKey = selectedModel383 const selectedModelKey = selectedModel
...@@ -475,13 +485,14 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {...@@ -475,13 +485,14 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
475 );485 );
476 const branchState = useBranches(branchTarget);486 const branchState = useBranches(branchTarget);
477 const branchesLoading = branchState.isPending;487 const branchesLoading = branchState.isPending;
488 const allBranchRefs = branchState.data?.refs ?? EMPTY_BRANCH_REFS;
478 const availableBranches = useMemo(489 const availableBranches = useMemo(
479 () =>490 () =>
480 pipe(491 pipe(
481 branchState.data?.refs ?? [],492 allBranchRefs,
482 Arr.filter((branch) => !branch.isRemote),493 Arr.filter((branch) => !branch.isRemote),
483 ),494 ),
484 [branchState.data?.refs],495 [allBranchRefs],
485 );496 );
486497
487 const filteredBranches = useMemo(() => {498 const filteredBranches = useMemo(() => {
...@@ -543,11 +554,11 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {...@@ -543,11 +554,11 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
543 mode,554 mode,
544 branch: selectedBranchName,555 branch: selectedBranchName,
545 worktreePath: selectedWorktreePath,556 worktreePath: selectedWorktreePath,
546 startFromOrigin,557 ...(draftStartFromOrigin !== undefined ? { startFromOrigin: draftStartFromOrigin } : {}),
547 },558 },
548 });559 });
549 },560 },
550 [selectedBranchName, selectedProjectDraftKey, selectedWorktreePath, startFromOrigin],561 [draftStartFromOrigin, selectedBranchName, selectedProjectDraftKey, selectedWorktreePath],
551 );562 );
552563
553 const selectBranch = useCallback(564 const selectBranch = useCallback(
...@@ -560,11 +571,11 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {...@@ -560,11 +571,11 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
560 mode: workspaceMode,571 mode: workspaceMode,
561 branch: branch.name,572 branch: branch.name,
562 worktreePath: normalizeSelectedWorktreePath(selectedProject, branch),573 worktreePath: normalizeSelectedWorktreePath(selectedProject, branch),
563 startFromOrigin,574 ...(draftStartFromOrigin !== undefined ? { startFromOrigin: draftStartFromOrigin } : {}),
564 },575 },
565 });576 });
566 },577 },
567 [selectedProject, selectedProjectDraftKey, startFromOrigin, workspaceMode],578 [draftStartFromOrigin, selectedProject, selectedProjectDraftKey, workspaceMode],
568 );579 );
569580
570 const setStartFromOrigin = useCallback(581 const setStartFromOrigin = useCallback(
...@@ -597,14 +608,16 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {...@@ -597,14 +608,16 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
597 if (workspaceMode !== "worktree" || selectedBranchName !== null) {608 if (workspaceMode !== "worktree" || selectedBranchName !== null) {
598 return;609 return;
599 }610 }
611 // The default may only exist as origin/<default> (isRemote), which
612 // availableBranches filters out — search the unfiltered refs for it.
600 const preferredBranch =613 const preferredBranch =
614 allBranchRefs.find((branch) => branch.isDefault) ??
601 availableBranches.find((branch) => branch.current) ??615 availableBranches.find((branch) => branch.current) ??
602 availableBranches.find((branch) => branch.isDefault) ??
603 null;616 null;
604 if (preferredBranch) {617 if (preferredBranch) {
605 selectBranch(preferredBranch);618 selectBranch(preferredBranch);
606 }619 }
607 }, [availableBranches, selectBranch, selectedBranchName, workspaceMode]);620 }, [allBranchRefs, availableBranches, selectBranch, selectedBranchName, workspaceMode]);
608621
609 const setRuntimeMode = useCallback(622 const setRuntimeMode = useCallback(
610 (value: RuntimeMode) => {623 (value: RuntimeMode) => {
...@@ -696,7 +709,12 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {...@@ -696,7 +709,12 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
696 workspaceMode: mode,709 workspaceMode: mode,
697 branch: workspaceSelection?.branch ?? null,710 branch: workspaceSelection?.branch ?? null,
698 worktreePath: mode === "worktree" ? null : (workspaceSelection?.worktreePath ?? null),711 worktreePath: mode === "worktree" ? null : (workspaceSelection?.worktreePath ?? null),
699 ...(workspaceSelection?.startFromOrigin ? { startFromOrigin: true } : {}),712 // The draft only carries the flag when the user touched it; fall
713 // back to the resolved default (server settings) so queued tasks
714 // drain with the same origin mode the composer displayed.
715 ...((workspaceSelection?.startFromOrigin ?? startFromOrigin)
716 ? { startFromOrigin: true }
717 : {}),
700 },718 },
701 createdAt: metadata.createdAt,719 createdAt: metadata.createdAt,
702 };720 };
...@@ -707,6 +725,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {...@@ -707,6 +725,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
707 selectedModel,725 selectedModel,
708 selectedProject,726 selectedProject,
709 selectedProjectDraftKey,727 selectedProjectDraftKey,
728 startFromOrigin,
710 ],729 ],
711 );730 );
712731
apps/mobile/src/lib/modelOptions.ts+3
...@@ -15,6 +15,7 @@ export type ModelOption = {...@@ -15,6 +15,7 @@ export type ModelOption = {
15 readonly providerKey: string;15 readonly providerKey: string;
16 readonly providerLabel: string;16 readonly providerLabel: string;
17 readonly providerDriver: string;17 readonly providerDriver: string;
18 readonly isDefault: boolean;
18 readonly capabilities: ModelCapabilities | null;19 readonly capabilities: ModelCapabilities | null;
19 readonly selection: ModelSelection;20 readonly selection: ModelSelection;
20};21};
...@@ -78,6 +79,7 @@ export function buildModelOptions(...@@ -78,6 +79,7 @@ export function buildModelOptions(
78 providerKey: provider.instanceId,79 providerKey: provider.instanceId,
79 providerLabel,80 providerLabel,
80 providerDriver: provider.driver,81 providerDriver: provider.driver,
82 isDefault: model.isDefault === true,
81 capabilities: model.capabilities,83 capabilities: model.capabilities,
82 selection: normalizeSelectionOptions(84 selection: normalizeSelectionOptions(
83 {85 {
...@@ -107,6 +109,7 @@ export function buildModelOptions(...@@ -107,6 +109,7 @@ export function buildModelOptions(
107 providerKey: fallbackModelSelection.instanceId,109 providerKey: fallbackModelSelection.instanceId,
108 providerLabel,110 providerLabel,
109 providerDriver: fallbackModelSelection.instanceId,111 providerDriver: fallbackModelSelection.instanceId,
112 isDefault: false,
110 capabilities: null,113 capabilities: null,
111 selection: fallbackModelSelection,114 selection: fallbackModelSelection,
112 });115 });
apps/server/src/provider/Layers/ClaudeAdapter.test.ts+6-5
...@@ -3045,7 +3045,7 @@ describe("ClaudeAdapterLive", () => {...@@ -3045,7 +3045,7 @@ describe("ClaudeAdapterLive", () => {
3045 attachments: [],3045 attachments: [],
3046 });3046 });
30473047
3048 assert.deepEqual(harness.query.setModelCalls, ["claude-opus-4-6"]);3048 assert.deepEqual(harness.query.setModelCalls, ["claude-opus-4-6[1m]"]);
3049 }).pipe(3049 }).pipe(
3050 Effect.provideService(Random.Random, makeDeterministicRandomService()),3050 Effect.provideService(Random.Random, makeDeterministicRandomService()),
3051 Effect.provide(harness.layer),3051 Effect.provide(harness.layer),
...@@ -3143,10 +3143,11 @@ describe("ClaudeAdapterLive", () => {...@@ -3143,10 +3143,11 @@ describe("ClaudeAdapterLive", () => {
3143 yield* adapter.sendTurn({3143 yield* adapter.sendTurn({
3144 threadId: session.threadId,3144 threadId: session.threadId,
3145 input: "hello again",3145 input: "hello again",
3146 modelSelection: {3146 modelSelection: createModelSelection(
3147 instanceId: ProviderInstanceId.make("claudeAgent"),3147 ProviderInstanceId.make("claudeAgent"),
3148 model: "claude-opus-4-6",3148 "claude-opus-4-6",
3149 },3149 [{ id: "contextWindow", value: "200k" }],
3150 ),
3150 attachments: [],3151 attachments: [],
3151 });3152 });
31523153
apps/server/src/provider/Layers/ClaudeAdapter.ts+9-14
...@@ -77,6 +77,7 @@ import {...@@ -77,6 +77,7 @@ import {
77 isClaudeUltracodeEffort,77 isClaudeUltracodeEffort,
78 normalizeClaudeCliEffort,78 normalizeClaudeCliEffort,
79 resolveClaudeApiModelId,79 resolveClaudeApiModelId,
80 resolveClaudeContextWindow,
80 resolveClaudeEffort,81 resolveClaudeEffort,
81} from "./ClaudeProvider.ts";82} from "./ClaudeProvider.ts";
82import {83import {
...@@ -341,24 +342,18 @@ function selectedClaudeContextWindow(...@@ -341,24 +342,18 @@ function selectedClaudeContextWindow(
341 switch (modelSelection?.model) {342 switch (modelSelection?.model) {
342 case "claude-opus-4-8":343 case "claude-opus-4-8":
343 case "claude-opus-4-7":344 case "claude-opus-4-7":
345 // Always 1M at the API; these models expose no contextWindow option.
344 return 1_000_000;346 return 1_000_000;
345 }347 }
346348
347 const optionValue = getModelSelectionStringOptionValue(modelSelection, "contextWindow");349 switch (resolveClaudeContextWindow(modelSelection)) {
348 if (optionValue === "1m") {350 case "1m":
349 return 1_000_000;351 return 1_000_000;
350 }352 case "200k":
351 if (optionValue === "200k") {353 return 200_000;
352 return 200_000;354 default:
353 }355 return undefined;
354 const caps = getClaudeModelCapabilities(modelSelection?.model);
355 const hasContextWindowOption = getProviderOptionDescriptors({ caps }).some(
356 (descriptor) => descriptor.type === "select" && descriptor.id === "contextWindow",
357 );
358 if (hasContextWindowOption) {
359 return 200_000;
360 }356 }
361 return undefined;
362}357}
363358
364function finiteNonNegativeInteger(value: unknown): number | undefined {359function finiteNonNegativeInteger(value: unknown): number | undefined {
apps/server/src/provider/Layers/ClaudeProvider.ts+21-5
...@@ -76,8 +76,8 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [...@@ -76,8 +76,8 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
76 id: "contextWindow",76 id: "contextWindow",
77 label: "Context Window",77 label: "Context Window",
78 options: [78 options: [
79 { value: "200k", label: "200k", isDefault: true },79 { value: "200k", label: "200k" },
80 { value: "1m", label: "1M" },80 { value: "1m", label: "1M", isDefault: true },
81 ],81 ],
82 }),82 }),
83 ],83 ],
...@@ -162,8 +162,8 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [...@@ -162,8 +162,8 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
162 id: "contextWindow",162 id: "contextWindow",
163 label: "Context Window",163 label: "Context Window",
164 options: [164 options: [
165 { value: "200k", label: "200k", isDefault: true },165 { value: "200k", label: "200k" },
166 { value: "1m", label: "1M" },166 { value: "1m", label: "1M", isDefault: true },
167 ],167 ],
168 }),168 }),
169 ],169 ],
...@@ -214,6 +214,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [...@@ -214,6 +214,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
214 buildSelectOptionDescriptor({214 buildSelectOptionDescriptor({
215 id: "contextWindow",215 id: "contextWindow",
216 label: "Context Window",216 label: "Context Window",
217 // Sonnet is 200k-default in Claude Code (1M is opt-in there too).
217 options: [218 options: [
218 { value: "200k", label: "200k", isDefault: true },219 { value: "200k", label: "200k", isDefault: true },
219 { value: "1m", label: "1M" },220 { value: "1m", label: "1M" },
...@@ -243,6 +244,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [...@@ -243,6 +244,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
243 buildSelectOptionDescriptor({244 buildSelectOptionDescriptor({
244 id: "contextWindow",245 id: "contextWindow",
245 label: "Context Window",246 label: "Context Window",
247 // Sonnet is 200k-default in Claude Code (1M is opt-in there too).
246 options: [248 options: [
247 { value: "200k", label: "200k", isDefault: true },249 { value: "200k", label: "200k", isDefault: true },
248 { value: "1m", label: "1M" },250 { value: "1m", label: "1M" },
...@@ -369,8 +371,22 @@ export function isClaudeUltracodeEffort(effort: string | null | undefined): bool...@@ -369,8 +371,22 @@ export function isClaudeUltracodeEffort(effort: string | null | undefined): bool
369 return effort === "ultracode";371 return effort === "ultracode";
370}372}
371373
374export function resolveClaudeContextWindow(
375 modelSelection: ModelSelection | undefined,
376): string | undefined {
377 const caps = getClaudeModelCapabilities(modelSelection?.model);
378 const raw = getModelSelectionStringOptionValue(modelSelection, "contextWindow");
379 const descriptors = getProviderOptionDescriptors({
380 caps,
381 ...(raw ? { selections: [{ id: "contextWindow", value: raw }] } : {}),
382 });
383 const descriptor = descriptors.find((candidate) => candidate.id === "contextWindow");
384 const value = getProviderOptionCurrentValue(descriptor);
385 return typeof value === "string" ? value : undefined;
386}
387
372export function resolveClaudeApiModelId(modelSelection: ModelSelection): string {388export function resolveClaudeApiModelId(modelSelection: ModelSelection): string {
373 switch (getModelSelectionStringOptionValue(modelSelection, "contextWindow")) {389 switch (resolveClaudeContextWindow(modelSelection)) {
374 case "1m":390 case "1m":
375 return `${modelSelection.model}[1m]`;391 return `${modelSelection.model}[1m]`;
376 default:392 default:
apps/server/src/provider/Layers/CodexProvider.test.ts+43-1
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { assert, it } from "@effect/vitest";1import { assert, it } from "@effect/vitest";
22
3import { mapCodexModelCapabilities } from "./CodexProvider.ts";3import { applyPreferredCodexDefaultModel, mapCodexModelCapabilities } from "./CodexProvider.ts";
44
5it("maps current Codex model capability fields", () => {5it("maps current Codex model capability fields", () => {
6 const capabilities = mapCodexModelCapabilities({6 const capabilities = mapCodexModelCapabilities({
...@@ -102,3 +102,45 @@ it("uses standard routing when the catalog has no default service tier", () => {...@@ -102,3 +102,45 @@ it("uses standard routing when the catalog has no default service tier", () => {
102 },102 },
103 ]);103 ]);
104});104});
105
106it("marks the most preferred available model as default", () => {
107 const models = applyPreferredCodexDefaultModel([
108 { slug: "gpt-5.6-terra", name: "GPT-5.6-Terra", isCustom: false, capabilities: null },
109 { slug: "gpt-5.4", name: "GPT-5.4", isCustom: false, isDefault: true, capabilities: null },
110 ]);
111
112 assert.deepStrictEqual(
113 models.map((model) => ({ slug: model.slug, isDefault: model.isDefault })),
114 [
115 { slug: "gpt-5.6-terra", isDefault: true },
116 { slug: "gpt-5.4", isDefault: undefined },
117 ],
118 );
119});
120
121it("prefers sol over terra when both are available", () => {
122 const models = applyPreferredCodexDefaultModel([
123 { slug: "gpt-5.6-terra", name: "GPT-5.6-Terra", isCustom: false, capabilities: null },
124 { slug: "gpt-5.6-sol", name: "GPT-5.6-Sol", isCustom: false, capabilities: null },
125 ]);
126
127 assert.deepStrictEqual(models.find((model) => model.isDefault)?.slug, "gpt-5.6-sol");
128});
129
130it("keeps Codex's own default when no preferred model is available", () => {
131 const models = applyPreferredCodexDefaultModel([
132 { slug: "gpt-5.5", name: "GPT-5.5", isCustom: false, capabilities: null },
133 { slug: "gpt-5.4", name: "GPT-5.4", isCustom: false, isDefault: true, capabilities: null },
134 ]);
135
136 assert.deepStrictEqual(models.find((model) => model.isDefault)?.slug, "gpt-5.4");
137});
138
139it("ignores custom models that shadow a preferred slug", () => {
140 const models = applyPreferredCodexDefaultModel([
141 { slug: "gpt-5.6-sol", name: "gpt-5.6-sol", isCustom: true, capabilities: null },
142 { slug: "gpt-5.4", name: "GPT-5.4", isCustom: false, isDefault: true, capabilities: null },
143 ]);
144
145 assert.deepStrictEqual(models.find((model) => model.isDefault)?.slug, "gpt-5.4");
146});
apps/server/src/provider/Layers/CodexProvider.ts+30-2
...@@ -22,7 +22,7 @@ import type {...@@ -22,7 +22,7 @@ import type {
22 ServerProviderModel,22 ServerProviderModel,
23 ServerProviderSkill,23 ServerProviderSkill,
24} from "@t3tools/contracts";24} from "@t3tools/contracts";
25import { ServerSettingsError } from "@t3tools/contracts";25import { PREFERRED_DEFAULT_CODEX_MODELS, ServerSettingsError } from "@t3tools/contracts";
2626
27import { createModelCapabilities } from "@t3tools/shared/model";27import { createModelCapabilities } from "@t3tools/shared/model";
28import { resolveSpawnCommand } from "@t3tools/shared/shell";28import { resolveSpawnCommand } from "@t3tools/shared/shell";
...@@ -189,10 +189,36 @@ function parseCodexModelListResponse(...@@ -189,10 +189,36 @@ function parseCodexModelListResponse(
189 slug: model.model,189 slug: model.model,
190 name: toDisplayName(model),190 name: toDisplayName(model),
191 isCustom: false,191 isCustom: false,
192 ...(model.isDefault ? { isDefault: true } : {}),
192 capabilities: mapCodexModelCapabilities(model),193 capabilities: mapCodexModelCapabilities(model),
193 }));194 }));
194}195}
195196
197/**
198 * Prefer our own default-model ranking when one of the preferred slugs is in
199 * the live catalog; otherwise keep whatever Codex itself flagged as default.
200 */
201export function applyPreferredCodexDefaultModel(
202 models: ReadonlyArray<ServerProviderModel>,
203): ReadonlyArray<ServerProviderModel> {
204 const preferredSlug = PREFERRED_DEFAULT_CODEX_MODELS.find((slug) =>
205 models.some((model) => model.slug === slug && !model.isCustom),
206 );
207 if (!preferredSlug) {
208 return models;
209 }
210 return models.map((model) => {
211 if (model.slug === preferredSlug) {
212 return model.isDefault ? model : { ...model, isDefault: true };
213 }
214 if (!model.isDefault) {
215 return model;
216 }
217 const { isDefault: _isDefault, ...rest } = model;
218 return rest;
219 });
220}
221
196function appendCustomCodexModels(222function appendCustomCodexModels(
197 models: ReadonlyArray<ServerProviderModel>,223 models: ReadonlyArray<ServerProviderModel>,
198 customModels: ReadonlyArray<string>,224 customModels: ReadonlyArray<string>,
...@@ -376,7 +402,9 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun...@@ -376,7 +402,9 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun
376 return {402 return {
377 account: accountResponse,403 account: accountResponse,
378 version,404 version,
379 models: appendCustomCodexModels(models, input.customModels ?? []),405 models: applyPreferredCodexDefaultModel(
406 appendCustomCodexModels(models, input.customModels ?? []),
407 ),
380 skills: parseCodexSkillsListResponse(skillsResponse, input.cwd),408 skills: parseCodexSkillsListResponse(skillsResponse, input.cwd),
381 } satisfies CodexAppServerProviderSnapshot;409 } satisfies CodexAppServerProviderSnapshot;
382});410});
apps/server/src/vcs/GitVcsDriverCore.test.ts+20
...@@ -596,6 +596,26 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {...@@ -596,6 +596,26 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
596 }),596 }),
597 );597 );
598598
599 it.effect("marks the origin default ref as default when no local copy exists", () =>
600 Effect.gen(function* () {
601 const cwd = yield* makeTmpDir();
602 const remote = yield* makeTmpDir("git-vcs-driver-remote-");
603 const { initialBranch } = yield* initRepoWithCommit(cwd);
604 yield* git(remote, ["init", "--bare"]);
605 yield* git(cwd, ["remote", "add", "origin", remote]);
606 yield* git(cwd, ["push", "-u", "origin", initialBranch]);
607 yield* git(cwd, ["remote", "set-head", "origin", initialBranch]);
608 yield* git(cwd, ["checkout", "-b", "feature/only-local"]);
609 yield* git(cwd, ["branch", "-D", initialBranch]);
610 const driver = yield* GitVcsDriver.GitVcsDriver;
611
612 const refs = yield* driver.listRefs({ cwd });
613 const remoteDefault = refs.refs.find((ref) => ref.name === `origin/${initialBranch}`);
614 assert.equal(remoteDefault?.isRemote, true);
615 assert.equal(remoteDefault?.isDefault, true);
616 }),
617 );
618
599 it.effect("creates, checks out, renames, and lists refs", () =>619 it.effect("creates, checks out, renames, and lists refs", () =>
600 Effect.gen(function* () {620 Effect.gen(function* () {
601 const cwd = yield* makeTmpDir();621 const cwd = yield* makeTmpDir();
apps/server/src/vcs/GitVcsDriverCore.ts+14-2
...@@ -2217,7 +2217,12 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -2217,7 +2217,12 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
2217 name: refName.name,2217 name: refName.name,
2218 current: false,2218 current: false,
2219 isRemote: true,2219 isRemote: true,
2220 isDefault: false,2220 // origin/HEAD's target is the repo default even when no local
2221 // copy of the default branch exists.
2222 isDefault:
2223 defaultBranch !== null &&
2224 parsedRemoteRef?.remoteName === "origin" &&
2225 parsedRemoteRef.branchName === defaultBranch,
2221 worktreePath: null,2226 worktreePath: null,
2222 };2227 };
2223 if (parsedRemoteRef) {2228 if (parsedRemoteRef) {
...@@ -2232,9 +2237,16 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -2232,9 +2237,16 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
2232 })2237 })
2233 : [];2238 : [];
22342239
2235 const allBranches = input.includeMatchingRemoteRefs2240 const combinedBranches = input.includeMatchingRemoteRefs
2236 ? [...localBranches, ...remoteBranches]2241 ? [...localBranches, ...remoteBranches]
2237 : dedupeRemoteBranchesWithLocalMatches([...localBranches, ...remoteBranches]);2242 : dedupeRemoteBranchesWithLocalMatches([...localBranches, ...remoteBranches]);
2243 // Keep current/default refs on the first page even when the default
2244 // only exists as origin/<default> (remote refs sort after all locals).
2245 const allBranches = combinedBranches.toSorted((a, b) => {
2246 const aPriority = a.current ? 0 : a.isDefault ? 1 : 2;
2247 const bPriority = b.current ? 0 : b.isDefault ? 1 : 2;
2248 return aPriority - bPriority;
2249 });
2238 const branchesForKind =2250 const branchesForKind =
2239 input.refKind === "local"2251 input.refKind === "local"
2240 ? allBranches.filter((ref) => !ref.isRemote)2252 ? allBranches.filter((ref) => !ref.isRemote)
apps/web/src/components/BranchToolbarBranchSelector.tsx+18-3
...@@ -422,17 +422,32 @@ export function BranchToolbarBranchSelector({...@@ -422,17 +422,32 @@ export function BranchToolbarBranchSelector({
422 });422 });
423 };423 };
424424
425 // Default the worktree base to the repo default branch (origin/HEAD), only
426 // falling back to the checked-out branch when no default is known.
427 const defaultBranchName = useMemo(
428 () => refs.find((refName) => refName.isDefault)?.name ?? null,
429 [refs],
430 );
431 const worktreeBaseBranchCandidate = isInitialBranchesLoadPending
432 ? null
433 : (defaultBranchName ?? currentGitBranch);
425 useEffect(() => {434 useEffect(() => {
426 if (435 if (
427 effectiveEnvMode !== "worktree" ||436 effectiveEnvMode !== "worktree" ||
428 activeWorktreePath ||437 activeWorktreePath ||
429 activeThreadBranch ||438 activeThreadBranch ||
430 !currentGitBranch439 !worktreeBaseBranchCandidate
431 ) {440 ) {
432 return;441 return;
433 }442 }
434 setThreadBranch(currentGitBranch, null);443 setThreadBranch(worktreeBaseBranchCandidate, null);
435 }, [activeThreadBranch, activeWorktreePath, currentGitBranch, effectiveEnvMode, setThreadBranch]);444 }, [
445 activeThreadBranch,
446 activeWorktreePath,
447 effectiveEnvMode,
448 setThreadBranch,
449 worktreeBaseBranchCandidate,
450 ]);
436451
437 // ---------------------------------------------------------------------------452 // ---------------------------------------------------------------------------
438 // Combobox / list plumbing453 // Combobox / list plumbing
apps/web/src/modelSelection.ts+4
...@@ -75,6 +75,7 @@ export interface AppModelOption {...@@ -75,6 +75,7 @@ export interface AppModelOption {
75 shortName?: string;75 shortName?: string;
76 subProvider?: string;76 subProvider?: string;
77 isCustom: boolean;77 isCustom: boolean;
78 isDefault?: boolean;
78}79}
7980
80function toAppModelOption(model: ServerProvider["models"][number]): AppModelOption {81function toAppModelOption(model: ServerProvider["models"][number]): AppModelOption {
...@@ -85,6 +86,7 @@ function toAppModelOption(model: ServerProvider["models"][number]): AppModelOpti...@@ -85,6 +86,7 @@ function toAppModelOption(model: ServerProvider["models"][number]): AppModelOpti
85 };86 };
86 if (model.shortName) option.shortName = model.shortName;87 if (model.shortName) option.shortName = model.shortName;
87 if (model.subProvider) option.subProvider = model.subProvider;88 if (model.subProvider) option.subProvider = model.subProvider;
89 if (model.isDefault) option.isDefault = true;
88 return option;90 return option;
89}91}
9092
...@@ -247,7 +249,9 @@ export function resolveAppModelSelectionForInstance(...@@ -247,7 +249,9 @@ export function resolveAppModelSelectionForInstance(
247 const options = getAppModelOptionsForInstance(settings, entry);249 const options = getAppModelOptionsForInstance(settings, entry);
248 return (250 return (
249 resolveSelectableModel(entry.driverKind, selectedModel, options) ??251 resolveSelectableModel(entry.driverKind, selectedModel, options) ??
252 options.find((option) => option.isDefault)?.slug ??
250 options[0]?.slug ??253 options[0]?.slug ??
254 entry.models.find((model) => model.isDefault)?.slug ??
251 entry.models[0]?.slug ??255 entry.models[0]?.slug ??
252 null256 null
253 );257 );
apps/web/src/providerModels.ts+1
...@@ -92,6 +92,7 @@ export function getDefaultServerModel(...@@ -92,6 +92,7 @@ export function getDefaultServerModel(
92): string {92): string {
93 const models = getProviderModels(providers, provider);93 const models = getProviderModels(providers, provider);
94 return (94 return (
95 models.find((model) => model.isDefault && !model.isCustom)?.slug ??
95 models.find((model) => !model.isCustom)?.slug ??96 models.find((model) => !model.isCustom)?.slug ??
96 models[0]?.slug ??97 models[0]?.slug ??
97 DEFAULT_MODEL_BY_PROVIDER[provider] ??98 DEFAULT_MODEL_BY_PROVIDER[provider] ??
packages/client-runtime/src/operations/projects.test.ts+2-1
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import { describe, expect, it } from "vite-plus/test";1import { describe, expect, it } from "vite-plus/test";
2import {2import {
3 DEFAULT_MODEL,
3 EnvironmentId,4 EnvironmentId,
4 ProjectId,5 ProjectId,
5 CommandId,6 CommandId,
...@@ -139,7 +140,7 @@ describe("add project shared logic", () => {...@@ -139,7 +140,7 @@ describe("add project shared logic", () => {
139 createWorkspaceRootIfMissing: true,140 createWorkspaceRootIfMissing: true,
140 defaultModelSelection: {141 defaultModelSelection: {
141 instanceId: "codex",142 instanceId: "codex",
142 model: "gpt-5.4",143 model: DEFAULT_MODEL,
143 },144 },
144 });145 });
145 });146 });
packages/contracts/src/model.ts+12-2
...@@ -133,8 +133,18 @@ const CURSOR_DRIVER_KIND = ProviderDriverKind.make("cursor");...@@ -133,8 +133,18 @@ const CURSOR_DRIVER_KIND = ProviderDriverKind.make("cursor");
133const GROK_DRIVER_KIND = ProviderDriverKind.make("grok");133const GROK_DRIVER_KIND = ProviderDriverKind.make("grok");
134const OPENCODE_DRIVER_KIND = ProviderDriverKind.make("opencode");134const OPENCODE_DRIVER_KIND = ProviderDriverKind.make("opencode");
135135
136export const DEFAULT_MODEL = "gpt-5.4";136export const DEFAULT_MODEL = "gpt-5.6-sol";
137export const DEFAULT_GIT_TEXT_GENERATION_MODEL = "gpt-5.4-mini";137
138/**
139 * Codex default-model preference, most preferred first. The provider snapshot
140 * marks the first of these present in the live `model/list` response as
141 * default; when none are available, Codex's own `isDefault` flag wins.
142 */
143export const PREFERRED_DEFAULT_CODEX_MODELS: ReadonlyArray<string> = [
144 "gpt-5.6-sol",
145 "gpt-5.6-terra",
146];
147export const DEFAULT_GIT_TEXT_GENERATION_MODEL = "gpt-5.6-luna";
138148
139export const DEFAULT_MODEL_BY_PROVIDER: Partial<Record<ProviderDriverKind, string>> = {149export const DEFAULT_MODEL_BY_PROVIDER: Partial<Record<ProviderDriverKind, string>> = {
140 [CODEX_DRIVER_KIND]: DEFAULT_MODEL,150 [CODEX_DRIVER_KIND]: DEFAULT_MODEL,
packages/contracts/src/server.ts+1
...@@ -64,6 +64,7 @@ export const ServerProviderModel = Schema.Struct({...@@ -64,6 +64,7 @@ export const ServerProviderModel = Schema.Struct({
64 shortName: Schema.optional(TrimmedNonEmptyString),64 shortName: Schema.optional(TrimmedNonEmptyString),
65 subProvider: Schema.optional(TrimmedNonEmptyString),65 subProvider: Schema.optional(TrimmedNonEmptyString),
66 isCustom: Schema.Boolean,66 isCustom: Schema.Boolean,
67 isDefault: Schema.optional(Schema.Boolean),
67 capabilities: Schema.NullOr(ModelCapabilities),68 capabilities: Schema.NullOr(ModelCapabilities),
68});69});
69export type ServerProviderModel = typeof ServerProviderModel.Type;70export type ServerProviderModel = typeof ServerProviderModel.Type;
packages/contracts/src/settings.test.ts+4-4
...@@ -88,14 +88,14 @@ describe("ServerSettings.providerInstances (slice-2 invariant)", () => {...@@ -88,14 +88,14 @@ describe("ServerSettings.providerInstances (slice-2 invariant)", () => {
88});88});
8989
90describe("ServerSettings worktree defaults", () => {90describe("ServerSettings worktree defaults", () => {
91 it("defaults start-from-origin off for legacy configs", () => {91 it("defaults start-from-origin on for legacy configs", () => {
92 expect(decodeServerSettings({}).newWorktreesStartFromOrigin).toBe(false);92 expect(decodeServerSettings({}).newWorktreesStartFromOrigin).toBe(true);
93 });93 });
9494
95 it("accepts start-from-origin updates", () => {95 it("accepts start-from-origin updates", () => {
96 expect(96 expect(
97 decodeServerSettingsPatch({ newWorktreesStartFromOrigin: true }).newWorktreesStartFromOrigin,97 decodeServerSettingsPatch({ newWorktreesStartFromOrigin: false }).newWorktreesStartFromOrigin,
98 ).toBe(true);98 ).toBe(false);
99 });99 });
100});100});
101101
packages/contracts/src/settings.ts+1-1
...@@ -382,7 +382,7 @@ export const ServerSettings = Schema.Struct({...@@ -382,7 +382,7 @@ export const ServerSettings = Schema.Struct({
382 Schema.withDecodingDefault(Effect.succeed("local" as const satisfies ThreadEnvMode)),382 Schema.withDecodingDefault(Effect.succeed("local" as const satisfies ThreadEnvMode)),
383 ),383 ),
384 newWorktreesStartFromOrigin: Schema.Boolean.pipe(384 newWorktreesStartFromOrigin: Schema.Boolean.pipe(
385 Schema.withDecodingDefault(Effect.succeed(false)),385 Schema.withDecodingDefault(Effect.succeed(true)),
386 ),386 ),
387 addProjectBaseDirectory: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed(""))),387 addProjectBaseDirectory: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed(""))),
388 textGenerationModelSelection: ModelSelection.pipe(388 textGenerationModelSelection: ModelSelection.pipe(