Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions src/browser/components/ChatInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -253,12 +253,21 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
}
);

// When entering creation mode (or when the default model changes), reset the
// project-scoped model to the explicit default so manual picks don't bleed
// into subsequent creation flows.
// When entering creation mode, initialize the project-scoped model to the
// default so previous manual picks don't bleed into new creation flows.
// Only runs once per creation session (not when defaultModel changes, which
// would clobber the user's intentional model selection).
const creationModelInitialized = useRef<string | null>(null);
useEffect(() => {
if (variant === "creation" && defaultModel) {
updatePersistedState(storageKeys.modelKey, defaultModel);
// Only initialize once per project scope
if (creationModelInitialized.current !== storageKeys.modelKey) {
creationModelInitialized.current = storageKeys.modelKey;
updatePersistedState(storageKeys.modelKey, defaultModel);
}
} else if (variant !== "creation") {
// Reset when leaving creation mode so re-entering triggers initialization
creationModelInitialized.current = null;
}
}, [variant, defaultModel, storageKeys.modelKey]);

Expand Down
23 changes: 8 additions & 15 deletions src/browser/components/ChatInput/useCreationWorkspace.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import type { APIClient } from "@/browser/contexts/API";
import type { DraftWorkspaceSettings } from "@/browser/hooks/useDraftWorkspaceSettings";
import {
getInputKey,
getModelKey,
getModeKey,
getPendingScopeId,
getProjectScopeId,
getThinkingLevelKey,
} from "@/common/constants/storage";
import type { SendMessageError } from "@/common/types/errors";
import type { SendMessageOptions, WorkspaceChatMessage } from "@/common/orpc/types";
import type { WorkspaceChatMessage } from "@/common/orpc/types";
import type { RuntimeMode } from "@/common/types/runtime";
import type {
FrontendWorkspaceMetadata,
Expand Down Expand Up @@ -60,13 +61,6 @@ void mock.module("@/browser/hooks/useDraftWorkspaceSettings", () => ({
useDraftWorkspaceSettings: useDraftWorkspaceSettingsMock,
}));

let currentSendOptions: SendMessageOptions;
const useSendMessageOptionsMock = mock(() => currentSendOptions);

void mock.module("@/browser/hooks/useSendMessageOptions", () => ({
useSendMessageOptions: useSendMessageOptionsMock,
}));

let currentORPCClient: MockOrpcClient | null = null;
void mock.module("@/browser/contexts/API", () => ({
useAPI: () => {
Expand Down Expand Up @@ -278,11 +272,6 @@ describe("useCreationWorkspace", () => {
updatePersistedStateCalls.length = 0;
draftSettingsInvocations = [];
draftSettingsState = createDraftSettingsHarness();
currentSendOptions = {
model: "gpt-4",
thinkingLevel: "medium",
mode: "exec",
} satisfies SendMessageOptions;
});

afterEach(() => {
Expand Down Expand Up @@ -376,6 +365,8 @@ describe("useCreationWorkspace", () => {

persistedPreferences[getModeKey(getProjectScopeId(TEST_PROJECT_PATH))] = "plan";
persistedPreferences[getThinkingLevelKey(getProjectScopeId(TEST_PROJECT_PATH))] = "high";
// Set model preference for the project scope (read by getSendOptionsFromStorage)
persistedPreferences[getModelKey(getProjectScopeId(TEST_PROJECT_PATH))] = "gpt-4";

draftSettingsState = createDraftSettingsHarness({
runtimeMode: "ssh",
Expand Down Expand Up @@ -412,8 +403,10 @@ describe("useCreationWorkspace", () => {
expect(options?.projectPath).toBe(TEST_PROJECT_PATH);
expect(options?.trunkBranch).toBe("dev");
expect(options?.model).toBe("gpt-4");
expect(options?.mode).toBe("exec");
expect(options?.thinkingLevel).toBe("medium");
// Mode was set to "plan" in persistedPreferences, so that's what we expect
expect(options?.mode).toBe("plan");
// thinkingLevel was set to "high" in persistedPreferences
expect(options?.thinkingLevel).toBe("high");
expect(options?.runtimeConfig).toEqual({
type: "ssh",
host: "example.com",
Expand Down
13 changes: 9 additions & 4 deletions src/browser/components/ChatInput/useCreationWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { ThinkingLevel } from "@/common/types/thinking";
import { parseRuntimeString } from "@/browser/utils/chatCommands";
import { useDraftWorkspaceSettings } from "@/browser/hooks/useDraftWorkspaceSettings";
import { readPersistedState, updatePersistedState } from "@/browser/hooks/usePersistedState";
import { useSendMessageOptions } from "@/browser/hooks/useSendMessageOptions";
import { getSendOptionsFromStorage } from "@/browser/utils/messages/sendOptions";
import {
getInputKey,
getModelKey,
Expand Down Expand Up @@ -95,8 +95,8 @@ export function useCreationWorkspace({
getRuntimeString,
} = useDraftWorkspaceSettings(projectPath, branches, recommendedTrunk);

// Get send options from shared hook (uses project-scoped storage key)
const sendMessageOptions = useSendMessageOptions(getProjectScopeId(projectPath));
// Project scope ID for reading send options at send time
const projectScopeId = getProjectScopeId(projectPath);

// Load branches on mount
useEffect(() => {
Expand Down Expand Up @@ -131,6 +131,11 @@ export function useCreationWorkspace({
? parseRuntimeString(runtimeString, "")
: undefined;

// Read send options fresh from localStorage at send time to avoid
// race conditions with React state updates (requestAnimationFrame batching
// in usePersistedState can delay state updates after model selection)
const sendMessageOptions = getSendOptionsFromStorage(projectScopeId);

// Send message with runtime config and creation-specific params
const result = await api.workspace.sendMessage({
workspaceId: null,
Expand Down Expand Up @@ -188,9 +193,9 @@ export function useCreationWorkspace({
api,
isSending,
projectPath,
projectScopeId,
onWorkspaceCreated,
getRuntimeString,
sendMessageOptions,
settings.trunkBranch,
]
);
Expand Down
1 change: 1 addition & 0 deletions src/browser/utils/messages/sendOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export function getSendOptionsFromStorage(workspaceId: string): SendMessageOptio

return {
model,
mode: mode === "exec" || mode === "plan" ? mode : "exec", // Only pass exec/plan to backend
thinkingLevel: effectiveThinkingLevel,
toolPolicy: modeToToolPolicy(mode),
additionalSystemInstructions,
Expand Down