fix: preserve task create defaults after refresh (#1575)

This commit is contained in:
Carlos Florêncio
2026-07-02 13:59:57 +01:00
committed by GitHub
parent da5050a44a
commit 3f930f5029
10 changed files with 247 additions and 36 deletions
@@ -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,
+111 -12
View File
@@ -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(
<StateProvider
@@ -186,12 +221,76 @@ describe("StateProvider task-create cache clearing", () => {
</StateProvider>,
);
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(
<StateProvider
initialState={{
userSettings: {
...defaultSettingsState.userSettings,
loaded: true,
taskCreateLastUsed: {
repositoryId: null,
branch: null,
agentProfileId: null,
executorProfileId: null,
synced: false,
},
},
}}
>
<div>ready</div>
</StateProvider>,
);
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(
<StateProvider
initialState={{
userSettings: {
...defaultSettingsState.userSettings,
loaded: true,
taskCreateLastUsed: {
repositoryId: "repo-server",
branch: null,
agentProfileId: null,
executorProfileId: null,
synced: true,
},
},
}}
>
<div>ready</div>
</StateProvider>,
);
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();
});
});
});
+20 -16
View File
@@ -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
);
}
@@ -171,6 +171,7 @@ describe("useEnsureUserSettings", () => {
branch: "main",
agentProfileId: "agent-2",
executorProfileId: "exec-profile-1",
synced: true,
});
});
+29
View File
@@ -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<typeof buildCoreFields>[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<typeof buildCoreFields>[0];
const result = buildCoreFields(settings);
expect(result.taskCreateLastUsed).toMatchObject({
repositoryId: "repo-1",
synced: true,
});
});
});
describe("buildTerminalFields via buildCoreFields", () => {
+9
View File
@@ -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),
};
}
@@ -47,6 +47,7 @@ export const defaultSettingsState: SettingsSliceState = {
branch: null,
agentProfileId: null,
executorProfileId: null,
synced: false,
},
jiraSavedViews: undefined,
jiraTaskPresets: undefined,
@@ -181,6 +181,7 @@ export type TaskCreateLastUsedState = {
branch: string | null;
agentProfileId: string | null;
executorProfileId: string | null;
synced?: boolean;
};
export type VoiceModeState = {
+60
View File
@@ -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();
+13 -8
View File
@@ -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,