| author | |
| committer | |
| log | 62cf4617594c2a0d8c9c5dc4bbc2012d0ead802d |
| tree | 1bafc0486a2dd7fbaebe765f4783bd9a4e935aa7 |
| parent | 23c18fda7a969634a30888e36b2da45f6d66a83b |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | 58 | |
| 59 | 59 | type WorkspaceMode = "local" | "worktree"; |
| 60 | 60 | |
| 61 | const EMPTY_BRANCH_REFS: ReadonlyArray<VcsRef> = []; | |
| 62 | ||
| 61 | 63 | function pendingTaskDraftKey(messageId: string): string { |
| 62 | 64 | return `pending-task:${messageId}`; |
| 63 | 65 | } |
| ... | ... | @@ -348,7 +350,14 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { |
| 348 | 350 | const workspaceMode = selectedProjectDraft.workspaceSelection?.mode ?? "local"; |
| 349 | 351 | const selectedBranchName = selectedProjectDraft.workspaceSelection?.branch ?? null; |
| 350 | 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 | 361 | const runtimeMode = selectedProjectDraft.runtimeMode ?? DEFAULT_RUNTIME_MODE; |
| 353 | 362 | const interactionMode = selectedProjectDraft.interactionMode ?? DEFAULT_PROVIDER_INTERACTION_MODE; |
| 354 | 363 | |
| ... | ... | @@ -368,6 +377,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { |
| 368 | 377 | const selectedModel = |
| 369 | 378 | selectedProjectDraft.modelSelection ?? |
| 370 | 379 | selectedProject?.defaultModelSelection ?? |
| 380 | modelOptions.find((option) => option.isDefault)?.selection ?? | |
| 371 | 381 | modelOptions[0]?.selection ?? |
| 372 | 382 | null; |
| 373 | 383 | const selectedModelKey = selectedModel |
| ... | ... | @@ -475,13 +485,14 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { |
| 475 | 485 | ); |
| 476 | 486 | const branchState = useBranches(branchTarget); |
| 477 | 487 | const branchesLoading = branchState.isPending; |
| 488 | const allBranchRefs = branchState.data?.refs ?? EMPTY_BRANCH_REFS; | |
| 478 | 489 | const availableBranches = useMemo( |
| 479 | 490 | () => |
| 480 | 491 | pipe( |
| 481 | branchState.data?.refs ?? [], | |
| 492 | allBranchRefs, | |
| 482 | 493 | Arr.filter((branch) => !branch.isRemote), |
| 483 | 494 | ), |
| 484 | [branchState.data?.refs], | |
| 495 | [allBranchRefs], | |
| 485 | 496 | ); |
| 486 | 497 | |
| 487 | 498 | const filteredBranches = useMemo(() => { |
| ... | ... | @@ -543,11 +554,11 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { |
| 543 | 554 | mode, |
| 544 | 555 | branch: selectedBranchName, |
| 545 | 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 | ); |
| 552 | 563 | |
| 553 | 564 | const selectBranch = useCallback( |
| ... | ... | @@ -560,11 +571,11 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { |
| 560 | 571 | mode: workspaceMode, |
| 561 | 572 | branch: branch.name, |
| 562 | 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 | ); |
| 569 | 580 | |
| 570 | 581 | const setStartFromOrigin = useCallback( |
| ... | ... | @@ -597,14 +608,16 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { |
| 597 | 608 | if (workspaceMode !== "worktree" || selectedBranchName !== null) { |
| 598 | 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 | 613 | const preferredBranch = |
| 614 | allBranchRefs.find((branch) => branch.isDefault) ?? | |
| 601 | 615 | availableBranches.find((branch) => branch.current) ?? |
| 602 | availableBranches.find((branch) => branch.isDefault) ?? | |
| 603 | 616 | null; |
| 604 | 617 | if (preferredBranch) { |
| 605 | 618 | selectBranch(preferredBranch); |
| 606 | 619 | } |
| 607 | }, [availableBranches, selectBranch, selectedBranchName, workspaceMode]); | |
| 620 | }, [allBranchRefs, availableBranches, selectBranch, selectedBranchName, workspaceMode]); | |
| 608 | 621 | |
| 609 | 622 | const setRuntimeMode = useCallback( |
| 610 | 623 | (value: RuntimeMode) => { |
| ... | ... | @@ -696,7 +709,12 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { |
| 696 | 709 | workspaceMode: mode, |
| 697 | 710 | branch: workspaceSelection?.branch ?? null, |
| 698 | 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 | 719 | createdAt: metadata.createdAt, |
| 702 | 720 | }; |
| ... | ... | @@ -707,6 +725,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { |
| 707 | 725 | selectedModel, |
| 708 | 726 | selectedProject, |
| 709 | 727 | selectedProjectDraftKey, |
| 728 | startFromOrigin, | |
| 710 | 729 | ], |
| 711 | 730 | ); |
| 712 | 731 |
apps/mobile/src/lib/modelOptions.ts+3| ... | ... | @@ -15,6 +15,7 @@ export type ModelOption = { |
| 15 | 15 | readonly providerKey: string; |
| 16 | 16 | readonly providerLabel: string; |
| 17 | 17 | readonly providerDriver: string; |
| 18 | readonly isDefault: boolean; | |
| 18 | 19 | readonly capabilities: ModelCapabilities | null; |
| 19 | 20 | readonly selection: ModelSelection; |
| 20 | 21 | }; |
| ... | ... | @@ -78,6 +79,7 @@ export function buildModelOptions( |
| 78 | 79 | providerKey: provider.instanceId, |
| 79 | 80 | providerLabel, |
| 80 | 81 | providerDriver: provider.driver, |
| 82 | isDefault: model.isDefault === true, | |
| 81 | 83 | capabilities: model.capabilities, |
| 82 | 84 | selection: normalizeSelectionOptions( |
| 83 | 85 | { |
| ... | ... | @@ -107,6 +109,7 @@ export function buildModelOptions( |
| 107 | 109 | providerKey: fallbackModelSelection.instanceId, |
| 108 | 110 | providerLabel, |
| 109 | 111 | providerDriver: fallbackModelSelection.instanceId, |
| 112 | isDefault: false, | |
| 110 | 113 | capabilities: null, |
| 111 | 114 | selection: fallbackModelSelection, |
| 112 | 115 | }); |
apps/server/src/provider/Layers/ClaudeAdapter.test.ts+6-5| ... | ... | @@ -3045,7 +3045,7 @@ describe("ClaudeAdapterLive", () => { |
| 3045 | 3045 | attachments: [], |
| 3046 | 3046 | }); |
| 3047 | 3047 | |
| 3048 | assert.deepEqual(harness.query.setModelCalls, ["claude-opus-4-6"]); | |
| 3048 | assert.deepEqual(harness.query.setModelCalls, ["claude-opus-4-6[1m]"]); | |
| 3049 | 3049 | }).pipe( |
| 3050 | 3050 | Effect.provideService(Random.Random, makeDeterministicRandomService()), |
| 3051 | 3051 | Effect.provide(harness.layer), |
| ... | ... | @@ -3143,10 +3143,11 @@ describe("ClaudeAdapterLive", () => { |
| 3143 | 3143 | yield* adapter.sendTurn({ |
| 3144 | 3144 | threadId: session.threadId, |
| 3145 | 3145 | input: "hello again", |
| 3146 | modelSelection: { | |
| 3147 | instanceId: ProviderInstanceId.make("claudeAgent"), | |
| 3148 | model: "claude-opus-4-6", | |
| 3149 | }, | |
| 3146 | modelSelection: createModelSelection( | |
| 3147 | ProviderInstanceId.make("claudeAgent"), | |
| 3148 | "claude-opus-4-6", | |
| 3149 | [{ id: "contextWindow", value: "200k" }], | |
| 3150 | ), | |
| 3150 | 3151 | attachments: [], |
| 3151 | 3152 | }); |
| 3152 | 3153 |
apps/server/src/provider/Layers/ClaudeAdapter.ts+9-14| ... | ... | @@ -77,6 +77,7 @@ import { |
| 77 | 77 | isClaudeUltracodeEffort, |
| 78 | 78 | normalizeClaudeCliEffort, |
| 79 | 79 | resolveClaudeApiModelId, |
| 80 | resolveClaudeContextWindow, | |
| 80 | 81 | resolveClaudeEffort, |
| 81 | 82 | } from "./ClaudeProvider.ts"; |
| 82 | 83 | import { |
| ... | ... | @@ -341,24 +342,18 @@ function selectedClaudeContextWindow( |
| 341 | 342 | switch (modelSelection?.model) { |
| 342 | 343 | case "claude-opus-4-8": |
| 343 | 344 | case "claude-opus-4-7": |
| 345 | // Always 1M at the API; these models expose no contextWindow option. | |
| 344 | 346 | return 1_000_000; |
| 345 | 347 | } |
| 346 | 348 | |
| 347 | const optionValue = getModelSelectionStringOptionValue(modelSelection, "contextWindow"); | |
| 348 | if (optionValue === "1m") { | |
| 349 | return 1_000_000; | |
| 350 | } | |
| 351 | if (optionValue === "200k") { | |
| 352 | return 200_000; | |
| 353 | } | |
| 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; | |
| 349 | switch (resolveClaudeContextWindow(modelSelection)) { | |
| 350 | case "1m": | |
| 351 | return 1_000_000; | |
| 352 | case "200k": | |
| 353 | return 200_000; | |
| 354 | default: | |
| 355 | return undefined; | |
| 360 | 356 | } |
| 361 | return undefined; | |
| 362 | 357 | } |
| 363 | 358 | |
| 364 | 359 | function 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 | 76 | id: "contextWindow", |
| 77 | 77 | label: "Context Window", |
| 78 | 78 | options: [ |
| 79 | { value: "200k", label: "200k", isDefault: true }, | |
| 80 | { value: "1m", label: "1M" }, | |
| 79 | { value: "200k", label: "200k" }, | |
| 80 | { value: "1m", label: "1M", isDefault: true }, | |
| 81 | 81 | ], |
| 82 | 82 | }), |
| 83 | 83 | ], |
| ... | ... | @@ -162,8 +162,8 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [ |
| 162 | 162 | id: "contextWindow", |
| 163 | 163 | label: "Context Window", |
| 164 | 164 | options: [ |
| 165 | { value: "200k", label: "200k", isDefault: true }, | |
| 166 | { value: "1m", label: "1M" }, | |
| 165 | { value: "200k", label: "200k" }, | |
| 166 | { value: "1m", label: "1M", isDefault: true }, | |
| 167 | 167 | ], |
| 168 | 168 | }), |
| 169 | 169 | ], |
| ... | ... | @@ -214,6 +214,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [ |
| 214 | 214 | buildSelectOptionDescriptor({ |
| 215 | 215 | id: "contextWindow", |
| 216 | 216 | label: "Context Window", |
| 217 | // Sonnet is 200k-default in Claude Code (1M is opt-in there too). | |
| 217 | 218 | options: [ |
| 218 | 219 | { value: "200k", label: "200k", isDefault: true }, |
| 219 | 220 | { value: "1m", label: "1M" }, |
| ... | ... | @@ -243,6 +244,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [ |
| 243 | 244 | buildSelectOptionDescriptor({ |
| 244 | 245 | id: "contextWindow", |
| 245 | 246 | label: "Context Window", |
| 247 | // Sonnet is 200k-default in Claude Code (1M is opt-in there too). | |
| 246 | 248 | options: [ |
| 247 | 249 | { value: "200k", label: "200k", isDefault: true }, |
| 248 | 250 | { value: "1m", label: "1M" }, |
| ... | ... | @@ -369,8 +371,22 @@ export function isClaudeUltracodeEffort(effort: string | null | undefined): bool |
| 369 | 371 | return effort === "ultracode"; |
| 370 | 372 | } |
| 371 | 373 | |
| 374 | export 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 | ||
| 372 | 388 | export function resolveClaudeApiModelId(modelSelection: ModelSelection): string { |
| 373 | switch (getModelSelectionStringOptionValue(modelSelection, "contextWindow")) { | |
| 389 | switch (resolveClaudeContextWindow(modelSelection)) { | |
| 374 | 390 | case "1m": |
| 375 | 391 | return `${modelSelection.model}[1m]`; |
| 376 | 392 | default: |
apps/server/src/provider/Layers/CodexProvider.test.ts+43-1| ... | ... | @@ -1,6 +1,6 @@ |
| 1 | 1 | import { assert, it } from "@effect/vitest"; |
| 2 | 2 | |
| 3 | import { mapCodexModelCapabilities } from "./CodexProvider.ts"; | |
| 3 | import { applyPreferredCodexDefaultModel, mapCodexModelCapabilities } from "./CodexProvider.ts"; | |
| 4 | 4 | |
| 5 | 5 | it("maps current Codex model capability fields", () => { |
| 6 | 6 | const capabilities = mapCodexModelCapabilities({ |
| ... | ... | @@ -102,3 +102,45 @@ it("uses standard routing when the catalog has no default service tier", () => { |
| 102 | 102 | }, |
| 103 | 103 | ]); |
| 104 | 104 | }); |
| 105 | ||
| 106 | it("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 | ||
| 121 | it("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 | ||
| 130 | it("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 | ||
| 139 | it("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 | 22 | ServerProviderModel, |
| 23 | 23 | ServerProviderSkill, |
| 24 | 24 | } from "@t3tools/contracts"; |
| 25 | import { ServerSettingsError } from "@t3tools/contracts"; | |
| 25 | import { PREFERRED_DEFAULT_CODEX_MODELS, ServerSettingsError } from "@t3tools/contracts"; | |
| 26 | 26 | |
| 27 | 27 | import { createModelCapabilities } from "@t3tools/shared/model"; |
| 28 | 28 | import { resolveSpawnCommand } from "@t3tools/shared/shell"; |
| ... | ... | @@ -189,10 +189,36 @@ function parseCodexModelListResponse( |
| 189 | 189 | slug: model.model, |
| 190 | 190 | name: toDisplayName(model), |
| 191 | 191 | isCustom: false, |
| 192 | ...(model.isDefault ? { isDefault: true } : {}), | |
| 192 | 193 | capabilities: mapCodexModelCapabilities(model), |
| 193 | 194 | })); |
| 194 | 195 | } |
| 195 | 196 | |
| 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 | */ | |
| 201 | export 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 | ||
| 196 | 222 | function appendCustomCodexModels( |
| 197 | 223 | models: ReadonlyArray<ServerProviderModel>, |
| 198 | 224 | customModels: ReadonlyArray<string>, |
| ... | ... | @@ -376,7 +402,9 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun |
| 376 | 402 | return { |
| 377 | 403 | account: accountResponse, |
| 378 | 404 | version, |
| 379 | models: appendCustomCodexModels(models, input.customModels ?? []), | |
| 405 | models: applyPreferredCodexDefaultModel( | |
| 406 | appendCustomCodexModels(models, input.customModels ?? []), | |
| 407 | ), | |
| 380 | 408 | skills: parseCodexSkillsListResponse(skillsResponse, input.cwd), |
| 381 | 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 | 596 | }), |
| 597 | 597 | ); |
| 598 | 598 | |
| 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 | 619 | it.effect("creates, checks out, renames, and lists refs", () => |
| 600 | 620 | Effect.gen(function* () { |
| 601 | 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 | 2217 | name: refName.name, |
| 2218 | 2218 | current: false, |
| 2219 | 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 | 2226 | worktreePath: null, |
| 2222 | 2227 | }; |
| 2223 | 2228 | if (parsedRemoteRef) { |
| ... | ... | @@ -2232,9 +2237,16 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* |
| 2232 | 2237 | }) |
| 2233 | 2238 | : []; |
| 2234 | 2239 | |
| 2235 | const allBranches = input.includeMatchingRemoteRefs | |
| 2240 | const combinedBranches = input.includeMatchingRemoteRefs | |
| 2236 | 2241 | ? [...localBranches, ...remoteBranches] |
| 2237 | 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 | 2250 | const branchesForKind = |
| 2239 | 2251 | input.refKind === "local" |
| 2240 | 2252 | ? allBranches.filter((ref) => !ref.isRemote) |
apps/web/src/components/BranchToolbarBranchSelector.tsx+18-3| ... | ... | @@ -422,17 +422,32 @@ export function BranchToolbarBranchSelector({ |
| 422 | 422 | }); |
| 423 | 423 | }; |
| 424 | 424 | |
| 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 | 434 | useEffect(() => { |
| 426 | 435 | if ( |
| 427 | 436 | effectiveEnvMode !== "worktree" || |
| 428 | 437 | activeWorktreePath || |
| 429 | 438 | activeThreadBranch || |
| 430 | !currentGitBranch | |
| 439 | !worktreeBaseBranchCandidate | |
| 431 | 440 | ) { |
| 432 | 441 | return; |
| 433 | 442 | } |
| 434 | setThreadBranch(currentGitBranch, null); | |
| 435 | }, [activeThreadBranch, activeWorktreePath, currentGitBranch, effectiveEnvMode, setThreadBranch]); | |
| 443 | setThreadBranch(worktreeBaseBranchCandidate, null); | |
| 444 | }, [ | |
| 445 | activeThreadBranch, | |
| 446 | activeWorktreePath, | |
| 447 | effectiveEnvMode, | |
| 448 | setThreadBranch, | |
| 449 | worktreeBaseBranchCandidate, | |
| 450 | ]); | |
| 436 | 451 | |
| 437 | 452 | // --------------------------------------------------------------------------- |
| 438 | 453 | // Combobox / list plumbing |
apps/web/src/modelSelection.ts+4| ... | ... | @@ -75,6 +75,7 @@ export interface AppModelOption { |
| 75 | 75 | shortName?: string; |
| 76 | 76 | subProvider?: string; |
| 77 | 77 | isCustom: boolean; |
| 78 | isDefault?: boolean; | |
| 78 | 79 | } |
| 79 | 80 | |
| 80 | 81 | function toAppModelOption(model: ServerProvider["models"][number]): AppModelOption { |
| ... | ... | @@ -85,6 +86,7 @@ function toAppModelOption(model: ServerProvider["models"][number]): AppModelOpti |
| 85 | 86 | }; |
| 86 | 87 | if (model.shortName) option.shortName = model.shortName; |
| 87 | 88 | if (model.subProvider) option.subProvider = model.subProvider; |
| 89 | if (model.isDefault) option.isDefault = true; | |
| 88 | 90 | return option; |
| 89 | 91 | } |
| 90 | 92 | |
| ... | ... | @@ -247,7 +249,9 @@ export function resolveAppModelSelectionForInstance( |
| 247 | 249 | const options = getAppModelOptionsForInstance(settings, entry); |
| 248 | 250 | return ( |
| 249 | 251 | resolveSelectableModel(entry.driverKind, selectedModel, options) ?? |
| 252 | options.find((option) => option.isDefault)?.slug ?? | |
| 250 | 253 | options[0]?.slug ?? |
| 254 | entry.models.find((model) => model.isDefault)?.slug ?? | |
| 251 | 255 | entry.models[0]?.slug ?? |
| 252 | 256 | null |
| 253 | 257 | ); |
apps/web/src/providerModels.ts+1| ... | ... | @@ -92,6 +92,7 @@ export function getDefaultServerModel( |
| 92 | 92 | ): string { |
| 93 | 93 | const models = getProviderModels(providers, provider); |
| 94 | 94 | return ( |
| 95 | models.find((model) => model.isDefault && !model.isCustom)?.slug ?? | |
| 95 | 96 | models.find((model) => !model.isCustom)?.slug ?? |
| 96 | 97 | models[0]?.slug ?? |
| 97 | 98 | DEFAULT_MODEL_BY_PROVIDER[provider] ?? |
packages/client-runtime/src/operations/projects.test.ts+2-1| ... | ... | @@ -1,5 +1,6 @@ |
| 1 | 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | 2 | import { |
| 3 | DEFAULT_MODEL, | |
| 3 | 4 | EnvironmentId, |
| 4 | 5 | ProjectId, |
| 5 | 6 | CommandId, |
| ... | ... | @@ -139,7 +140,7 @@ describe("add project shared logic", () => { |
| 139 | 140 | createWorkspaceRootIfMissing: true, |
| 140 | 141 | defaultModelSelection: { |
| 141 | 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 | 133 | const GROK_DRIVER_KIND = ProviderDriverKind.make("grok"); |
| 134 | 134 | const OPENCODE_DRIVER_KIND = ProviderDriverKind.make("opencode"); |
| 135 | 135 | |
| 136 | export const DEFAULT_MODEL = "gpt-5.4"; | |
| 137 | export const DEFAULT_GIT_TEXT_GENERATION_MODEL = "gpt-5.4-mini"; | |
| 136 | export const DEFAULT_MODEL = "gpt-5.6-sol"; | |
| 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 | */ | |
| 143 | export const PREFERRED_DEFAULT_CODEX_MODELS: ReadonlyArray<string> = [ | |
| 144 | "gpt-5.6-sol", | |
| 145 | "gpt-5.6-terra", | |
| 146 | ]; | |
| 147 | export const DEFAULT_GIT_TEXT_GENERATION_MODEL = "gpt-5.6-luna"; | |
| 138 | 148 | |
| 139 | 149 | export const DEFAULT_MODEL_BY_PROVIDER: Partial<Record<ProviderDriverKind, string>> = { |
| 140 | 150 | [CODEX_DRIVER_KIND]: DEFAULT_MODEL, |
packages/contracts/src/server.ts+1| ... | ... | @@ -64,6 +64,7 @@ export const ServerProviderModel = Schema.Struct({ |
| 64 | 64 | shortName: Schema.optional(TrimmedNonEmptyString), |
| 65 | 65 | subProvider: Schema.optional(TrimmedNonEmptyString), |
| 66 | 66 | isCustom: Schema.Boolean, |
| 67 | isDefault: Schema.optional(Schema.Boolean), | |
| 67 | 68 | capabilities: Schema.NullOr(ModelCapabilities), |
| 68 | 69 | }); |
| 69 | 70 | export type ServerProviderModel = typeof ServerProviderModel.Type; |
packages/contracts/src/settings.test.ts+4-4| ... | ... | @@ -88,14 +88,14 @@ describe("ServerSettings.providerInstances (slice-2 invariant)", () => { |
| 88 | 88 | }); |
| 89 | 89 | |
| 90 | 90 | describe("ServerSettings worktree defaults", () => { |
| 91 | it("defaults start-from-origin off for legacy configs", () => { | |
| 92 | expect(decodeServerSettings({}).newWorktreesStartFromOrigin).toBe(false); | |
| 91 | it("defaults start-from-origin on for legacy configs", () => { | |
| 92 | expect(decodeServerSettings({}).newWorktreesStartFromOrigin).toBe(true); | |
| 93 | 93 | }); |
| 94 | 94 | |
| 95 | 95 | it("accepts start-from-origin updates", () => { |
| 96 | 96 | expect( |
| 97 | decodeServerSettingsPatch({ newWorktreesStartFromOrigin: true }).newWorktreesStartFromOrigin, | |
| 98 | ).toBe(true); | |
| 97 | decodeServerSettingsPatch({ newWorktreesStartFromOrigin: false }).newWorktreesStartFromOrigin, | |
| 98 | ).toBe(false); | |
| 99 | 99 | }); |
| 100 | 100 | }); |
| 101 | 101 |
packages/contracts/src/settings.ts+1-1| ... | ... | @@ -382,7 +382,7 @@ export const ServerSettings = Schema.Struct({ |
| 382 | 382 | Schema.withDecodingDefault(Effect.succeed("local" as const satisfies ThreadEnvMode)), |
| 383 | 383 | ), |
| 384 | 384 | newWorktreesStartFromOrigin: Schema.Boolean.pipe( |
| 385 | Schema.withDecodingDefault(Effect.succeed(false)), | |
| 385 | Schema.withDecodingDefault(Effect.succeed(true)), | |
| 386 | 386 | ), |
| 387 | 387 | addProjectBaseDirectory: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed(""))), |
| 388 | 388 | textGenerationModelSelection: ModelSelection.pipe( |