fix: preserve task create defaults after refresh (#1575)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user