diff --git a/apps/web/components/settings/editors-settings-state.tsx b/apps/web/components/settings/editors-settings-state.tsx
index 71b84b8a55..614ea98ef5 100644
--- a/apps/web/components/settings/editors-settings-state.tsx
+++ b/apps/web/components/settings/editors-settings-state.tsx
@@ -11,6 +11,7 @@ import {
parseChangesPanelLayout,
parseSystemMetricsDisplay,
parseTerminalLinkBehavior,
+ taskCreateLastUsedHasValue,
parseVoiceMode,
} from "@/lib/ssr/user-settings";
import { fromApiSidebarDraft, fromApiSidebarView } from "@/lib/state/slices/ui/sidebar-view-wire";
@@ -269,6 +270,7 @@ function mapEditorSyncedLocalFields(
branch: s.task_create_last_used?.branch || null,
agentProfileId: s.task_create_last_used?.agent_profile_id || null,
executorProfileId: s.task_create_last_used?.executor_profile_id || null,
+ synced: taskCreateLastUsedHasValue(s.task_create_last_used),
},
jiraSavedViews: s.jira_saved_views,
jiraTaskPresets: s.jira_task_presets,
diff --git a/apps/web/components/state-provider.test.tsx b/apps/web/components/state-provider.test.tsx
index 7dc3b6c55c..b7296974de 100644
--- a/apps/web/components/state-provider.test.tsx
+++ b/apps/web/components/state-provider.test.tsx
@@ -45,6 +45,47 @@ function ObserveLastUsedCache({ onSeen }: { onSeen: (value: string | null) => vo
return null;
}
+const cachedTaskCreateChoices = {
+ repositoryId: "repo-cached",
+ branch: "feature-cached",
+ agentProfileId: "agent-cached",
+ executorProfileId: "exec-cached",
+};
+
+function seedCachedTaskCreateChoices() {
+ window.localStorage.setItem(
+ STORAGE_KEYS.LAST_REPOSITORY_ID,
+ JSON.stringify(cachedTaskCreateChoices.repositoryId),
+ );
+ window.localStorage.setItem(
+ STORAGE_KEYS.LAST_BRANCH,
+ JSON.stringify(cachedTaskCreateChoices.branch),
+ );
+ window.localStorage.setItem(
+ STORAGE_KEYS.LAST_AGENT_PROFILE_ID,
+ JSON.stringify(cachedTaskCreateChoices.agentProfileId),
+ );
+ window.localStorage.setItem(
+ STORAGE_KEYS.LAST_EXECUTOR_PROFILE_ID,
+ JSON.stringify(cachedTaskCreateChoices.executorProfileId),
+ );
+}
+
+function expectCachedTaskCreateChoices() {
+ expect(window.localStorage.getItem(STORAGE_KEYS.LAST_REPOSITORY_ID)).toBe(
+ JSON.stringify(cachedTaskCreateChoices.repositoryId),
+ );
+ expect(window.localStorage.getItem(STORAGE_KEYS.LAST_BRANCH)).toBe(
+ JSON.stringify(cachedTaskCreateChoices.branch),
+ );
+ expect(window.localStorage.getItem(STORAGE_KEYS.LAST_AGENT_PROFILE_ID)).toBe(
+ JSON.stringify(cachedTaskCreateChoices.agentProfileId),
+ );
+ expect(window.localStorage.getItem(STORAGE_KEYS.LAST_EXECUTOR_PROFILE_ID)).toBe(
+ JSON.stringify(cachedTaskCreateChoices.executorProfileId),
+ );
+}
+
beforeEach(() => {
window.localStorage.clear();
resetTaskCreateLastUsedSync({ clearQueued: true });
@@ -156,16 +197,10 @@ describe("StateProvider task-create cache", () => {
});
});
-describe("StateProvider task-create cache clearing", () => {
- it("lets children read cached task-create choices before clearing missing backend values", async () => {
+describe("StateProvider task-create cache fallback", () => {
+ it("keeps cached task-create choices when loaded backend settings omit them", async () => {
const onSeen = vi.fn();
- window.localStorage.setItem(STORAGE_KEYS.LAST_REPOSITORY_ID, JSON.stringify("repo-cached"));
- window.localStorage.setItem(STORAGE_KEYS.LAST_BRANCH, JSON.stringify("feature-cached"));
- window.localStorage.setItem(STORAGE_KEYS.LAST_AGENT_PROFILE_ID, JSON.stringify("agent-cached"));
- window.localStorage.setItem(
- STORAGE_KEYS.LAST_EXECUTOR_PROFILE_ID,
- JSON.stringify("exec-cached"),
- );
+ seedCachedTaskCreateChoices();
render(
{
,
);
- expect(onSeen).toHaveBeenCalledWith(JSON.stringify("repo-cached"));
+ expect(onSeen).toHaveBeenCalledWith(JSON.stringify(cachedTaskCreateChoices.repositoryId));
+ // Give any potential deferred deletions a chance to fire (old code used setTimeout(0)).
+ await new Promise((resolve) => window.setTimeout(resolve, 10));
+ expectCachedTaskCreateChoices();
+ });
+
+ it("keeps cached task-create choices when loaded backend settings sync an empty object", async () => {
+ seedCachedTaskCreateChoices();
+
+ render(
+
+ ready
+ ,
+ );
+
await waitFor(() => {
- expect(window.localStorage.getItem(STORAGE_KEYS.LAST_REPOSITORY_ID)).toBeNull();
+ expectCachedTaskCreateChoices();
+ });
+ });
+
+ it("clears stale cached fields when loaded backend settings contain real task-create choices", async () => {
+ window.localStorage.setItem(
+ STORAGE_KEYS.LAST_BRANCH,
+ JSON.stringify(cachedTaskCreateChoices.branch),
+ );
+ window.localStorage.setItem(
+ STORAGE_KEYS.LAST_AGENT_PROFILE_ID,
+ JSON.stringify(cachedTaskCreateChoices.agentProfileId),
+ );
+
+ render(
+
+ ready
+ ,
+ );
+
+ await waitFor(() => {
+ expect(window.localStorage.getItem(STORAGE_KEYS.LAST_REPOSITORY_ID)).toBe(
+ JSON.stringify("repo-server"),
+ );
expect(window.localStorage.getItem(STORAGE_KEYS.LAST_BRANCH)).toBeNull();
expect(window.localStorage.getItem(STORAGE_KEYS.LAST_AGENT_PROFILE_ID)).toBeNull();
- expect(window.localStorage.getItem(STORAGE_KEYS.LAST_EXECUTOR_PROFILE_ID)).toBeNull();
});
});
});
diff --git a/apps/web/components/state-provider.tsx b/apps/web/components/state-provider.tsx
index f285f80780..8ad9a1efa4 100644
--- a/apps/web/components/state-provider.tsx
+++ b/apps/web/components/state-provider.tsx
@@ -66,32 +66,35 @@ export function StateProvider({ children, initialState }: StoreProviderProps) {
function syncTaskCreateLastUsedCache(state: AppState) {
if (!state.userSettings.loaded) return;
const lastUsed = state.userSettings.taskCreateLastUsed;
- syncTaskCreateLastUsedCacheField(STORAGE_KEYS.LAST_REPOSITORY_ID, lastUsed?.repositoryId);
- syncTaskCreateLastUsedCacheField(STORAGE_KEYS.LAST_BRANCH, lastUsed?.branch);
- syncTaskCreateLastUsedCacheField(STORAGE_KEYS.LAST_AGENT_PROFILE_ID, lastUsed?.agentProfileId);
+ syncTaskCreateLastUsedCacheField(
+ STORAGE_KEYS.LAST_REPOSITORY_ID,
+ lastUsed?.repositoryId,
+ lastUsed?.synced,
+ );
+ syncTaskCreateLastUsedCacheField(STORAGE_KEYS.LAST_BRANCH, lastUsed?.branch, lastUsed?.synced);
+ syncTaskCreateLastUsedCacheField(
+ STORAGE_KEYS.LAST_AGENT_PROFILE_ID,
+ lastUsed?.agentProfileId,
+ lastUsed?.synced,
+ );
syncTaskCreateLastUsedCacheField(
STORAGE_KEYS.LAST_EXECUTOR_PROFILE_ID,
lastUsed?.executorProfileId,
+ lastUsed?.synced,
);
clearQueuedTaskCreateLastUsedIfSynced(lastUsed);
}
-function syncTaskCreateLastUsedCacheField(key: string, value: string | null | undefined) {
+function syncTaskCreateLastUsedCacheField(
+ key: string,
+ value: string | null | undefined,
+ synced: boolean | undefined,
+) {
if (value) {
setLocalStorage(key, value);
return;
}
- deferRemoveTaskCreateLastUsedCacheField(key);
-}
-
-function deferRemoveTaskCreateLastUsedCacheField(key: string) {
- const cachedValue = window.localStorage.getItem(key);
- if (cachedValue === null) return;
- window.setTimeout(() => {
- if (window.localStorage.getItem(key) === cachedValue) {
- removeLocalStorage(key);
- }
- }, 0);
+ if (synced) removeLocalStorage(key);
}
function taskCreateLastUsedEqual(
@@ -102,7 +105,8 @@ function taskCreateLastUsedEqual(
a?.repositoryId === b?.repositoryId &&
a?.branch === b?.branch &&
a?.agentProfileId === b?.agentProfileId &&
- a?.executorProfileId === b?.executorProfileId
+ a?.executorProfileId === b?.executorProfileId &&
+ a?.synced === b?.synced
);
}
diff --git a/apps/web/hooks/use-ensure-user-settings.test.ts b/apps/web/hooks/use-ensure-user-settings.test.ts
index 33d3d2761b..7aa3d80c68 100644
--- a/apps/web/hooks/use-ensure-user-settings.test.ts
+++ b/apps/web/hooks/use-ensure-user-settings.test.ts
@@ -171,6 +171,7 @@ describe("useEnsureUserSettings", () => {
branch: "main",
agentProfileId: "agent-2",
executorProfileId: "exec-profile-1",
+ synced: true,
});
});
diff --git a/apps/web/lib/ssr/user-settings.test.ts b/apps/web/lib/ssr/user-settings.test.ts
index 0ff04c54e5..901f0bb325 100644
--- a/apps/web/lib/ssr/user-settings.test.ts
+++ b/apps/web/lib/ssr/user-settings.test.ts
@@ -59,6 +59,35 @@ describe("buildCoreFields", () => {
const result = buildCoreFields(settings);
expect(result.terminalFontFamily).toBeNull();
});
+
+ it("does not mark an empty task-create last-used object as synced", () => {
+ const settings = {
+ task_create_last_used: {},
+ } as unknown as Parameters[0];
+
+ const result = buildCoreFields(settings);
+ expect(result.taskCreateLastUsed).toEqual({
+ repositoryId: null,
+ branch: null,
+ agentProfileId: null,
+ executorProfileId: null,
+ synced: false,
+ });
+ });
+
+ it("marks task-create last-used as synced when a field is present", () => {
+ const settings = {
+ task_create_last_used: {
+ repository_id: "repo-1",
+ },
+ } as unknown as Parameters[0];
+
+ const result = buildCoreFields(settings);
+ expect(result.taskCreateLastUsed).toMatchObject({
+ repositoryId: "repo-1",
+ synced: true,
+ });
+ });
});
describe("buildTerminalFields via buildCoreFields", () => {
diff --git a/apps/web/lib/ssr/user-settings.ts b/apps/web/lib/ssr/user-settings.ts
index 9e330e13b2..70bcfeec55 100644
--- a/apps/web/lib/ssr/user-settings.ts
+++ b/apps/web/lib/ssr/user-settings.ts
@@ -64,12 +64,21 @@ function parseSidebarTaskPrefs(value: SidebarTaskPrefsApi | undefined) {
};
}
+export function taskCreateLastUsedHasValue(
+ value: UserSettingsData["task_create_last_used"] | undefined,
+) {
+ return Boolean(
+ value?.repository_id || value?.branch || value?.agent_profile_id || value?.executor_profile_id,
+ );
+}
+
function parseTaskCreateLastUsed(value: UserSettingsData["task_create_last_used"] | undefined) {
return {
repositoryId: value?.repository_id || null,
branch: value?.branch || null,
agentProfileId: value?.agent_profile_id || null,
executorProfileId: value?.executor_profile_id || null,
+ synced: taskCreateLastUsedHasValue(value),
};
}
diff --git a/apps/web/lib/state/slices/settings/settings-slice.ts b/apps/web/lib/state/slices/settings/settings-slice.ts
index cd8a0225ca..1d5b6d63c5 100644
--- a/apps/web/lib/state/slices/settings/settings-slice.ts
+++ b/apps/web/lib/state/slices/settings/settings-slice.ts
@@ -47,6 +47,7 @@ export const defaultSettingsState: SettingsSliceState = {
branch: null,
agentProfileId: null,
executorProfileId: null,
+ synced: false,
},
jiraSavedViews: undefined,
jiraTaskPresets: undefined,
diff --git a/apps/web/lib/state/slices/settings/types.ts b/apps/web/lib/state/slices/settings/types.ts
index 1c9d49e44c..38bd4abcfe 100644
--- a/apps/web/lib/state/slices/settings/types.ts
+++ b/apps/web/lib/state/slices/settings/types.ts
@@ -181,6 +181,7 @@ export type TaskCreateLastUsedState = {
branch: string | null;
agentProfileId: string | null;
executorProfileId: string | null;
+ synced?: boolean;
};
export type VoiceModeState = {
diff --git a/apps/web/lib/ws/handlers/users.test.ts b/apps/web/lib/ws/handlers/users.test.ts
index de13d9d186..b1994f6558 100644
--- a/apps/web/lib/ws/handlers/users.test.ts
+++ b/apps/web/lib/ws/handlers/users.test.ts
@@ -91,6 +91,66 @@ describe("user settings websocket handler", () => {
});
});
+describe("user settings websocket task-create last-used", () => {
+ it("does not mark empty task-create last-used broadcasts as synced", () => {
+ const store = makeStore();
+
+ registerUsersHandlers(store)["user.settings.updated"]?.(
+ userSettingsMessage({ task_create_last_used: {} }),
+ );
+
+ expect(store.getState().userSettings.taskCreateLastUsed).toEqual({
+ repositoryId: null,
+ branch: null,
+ agentProfileId: null,
+ executorProfileId: null,
+ synced: false,
+ });
+ });
+
+ it("marks task-create last-used broadcasts as synced when a field is present", () => {
+ const store = makeStore();
+
+ registerUsersHandlers(store)["user.settings.updated"]?.(
+ userSettingsMessage({ task_create_last_used: { repository_id: "repo-1" } }),
+ );
+
+ expect(store.getState().userSettings.taskCreateLastUsed).toMatchObject({
+ repositoryId: "repo-1",
+ synced: true,
+ });
+ });
+
+ it("preserves task-create last-used state when broadcasts omit it", () => {
+ const store = makeStore();
+ store.setState((state) => ({
+ ...state,
+ userSettings: {
+ ...state.userSettings,
+ taskCreateLastUsed: {
+ repositoryId: "repo-1",
+ branch: "main",
+ agentProfileId: "agent-1",
+ executorProfileId: "exec-1",
+ synced: true,
+ },
+ },
+ }));
+
+ registerUsersHandlers(store)["user.settings.updated"]?.(
+ userSettingsMessage({ keyboard_shortcuts: {} }),
+ );
+
+ expect(store.getState().userSettings.taskCreateLastUsed).toEqual({
+ repositoryId: "repo-1",
+ branch: "main",
+ agentProfileId: "agent-1",
+ executorProfileId: "exec-1",
+ synced: true,
+ });
+ });
+});
+
describe("user settings websocket sidebar settings", () => {
it("preserves userSettings sidebar fields when payload omits them", () => {
const store = makeStore();
diff --git a/apps/web/lib/ws/handlers/users.ts b/apps/web/lib/ws/handlers/users.ts
index 709ed9af16..b118998d83 100644
--- a/apps/web/lib/ws/handlers/users.ts
+++ b/apps/web/lib/ws/handlers/users.ts
@@ -5,6 +5,7 @@ import type { WsHandlers } from "@/lib/ws/handlers/types";
import {
parseChangesPanelLayout,
parseSystemMetricsDisplay,
+ taskCreateLastUsedHasValue,
parseVoiceMode,
} from "@/lib/ssr/user-settings";
import { fromApiSidebarDraft, fromApiSidebarView } from "@/lib/state/slices/ui/sidebar-view-wire";
@@ -29,7 +30,7 @@ function buildUserSettingsState(state: AppState, payload: UserSettingsUpdatedPay
...buildBehaviorSettings(payload),
...buildSidebarSettings(state, payload),
...buildLspSettings(payload),
- ...buildSyncedLocalSettings(payload),
+ ...buildSyncedLocalSettings(state, payload),
defaultUtilityAgentId: payload.default_utility_agent_id || null,
keyboardShortcuts: payload.keyboard_shortcuts ?? {},
changesPanelLayout: parseChangesPanelLayout(payload.changes_panel_layout),
@@ -46,15 +47,19 @@ function buildLspSettings(payload: UserSettingsUpdatedPayload) {
};
}
-function buildSyncedLocalSettings(payload: UserSettingsUpdatedPayload) {
+function buildSyncedLocalSettings(state: AppState, payload: UserSettingsUpdatedPayload) {
return {
savedLayouts: payload.saved_layouts ?? [],
- taskCreateLastUsed: {
- repositoryId: payload.task_create_last_used?.repository_id || null,
- branch: payload.task_create_last_used?.branch || null,
- agentProfileId: payload.task_create_last_used?.agent_profile_id || null,
- executorProfileId: payload.task_create_last_used?.executor_profile_id || null,
- },
+ taskCreateLastUsed:
+ payload.task_create_last_used === undefined
+ ? state.userSettings.taskCreateLastUsed
+ : {
+ repositoryId: payload.task_create_last_used?.repository_id || null,
+ branch: payload.task_create_last_used?.branch || null,
+ agentProfileId: payload.task_create_last_used?.agent_profile_id || null,
+ executorProfileId: payload.task_create_last_used?.executor_profile_id || null,
+ synced: taskCreateLastUsedHasValue(payload.task_create_last_used),
+ },
jiraSavedViews: payload.jira_saved_views,
jiraTaskPresets: payload.jira_task_presets,
githubSavedPresets: payload.github_saved_presets,