5f4b3ec0c0
* feat(web): allow renaming quick-chat tabs locally Double-click a quick-chat tab to rename it. The name is stored in localStorage and overlays the SSR-derived task title on hydration, so renames survive reload without touching the backend. * fix(web): address quick-chat rename review feedback - Drop unused sessionId prop from QuickChatTabItem - Enter handler now blurs the input instead of calling commit(), preventing a duplicate commit when onBlur fires on unmount - Add hydrator tests covering the localStorage name overlay * fix(web): make Escape actually cancel quick-chat rename Escape was setting isEditing(false), which unmounted the focused input, causing onBlur (commit) to fire synchronously with the typed draft still in the closure — committing the rename instead of discarding it. Cancel now flips a cancelledRef and blurs the input; commit short-circuits when the ref is set, so onBlur is the single edit-exit path for both Enter and Escape. Adds a render test covering both flows. * fix(web): guard quick-chat rename against IME and orphan storage writes - Skip Enter/Escape handling while IME composition is active so candidate confirmation does not commit/cancel the rename - renameQuickChatSession only writes to localStorage when the session was actually found in state, preventing orphan entries for stale session IDs --------- Co-authored-by: Kandev Agent <agent@kandev.dev>
74 lines
2.3 KiB
TypeScript
74 lines
2.3 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|
import { produce } from "immer";
|
|
import type { Draft } from "immer";
|
|
import { hydrateUI } from "./hydrator";
|
|
import { defaultUIState } from "@/lib/state/slices/ui/ui-slice";
|
|
import type { AppState } from "@/lib/state/store";
|
|
|
|
function makeDraft(): AppState {
|
|
// hydrateUI only touches UI-slice fields; an empty object cast satisfies
|
|
// the rest without dragging the full AppState shape into this test.
|
|
return { ...defaultUIState } as unknown as AppState;
|
|
}
|
|
|
|
describe("hydrateUI — quick chat name overlay", () => {
|
|
beforeEach(() => {
|
|
window.localStorage.clear();
|
|
});
|
|
|
|
it("overlays a locally-renamed name onto the SSR-provided session name", () => {
|
|
window.localStorage.setItem(
|
|
"kandev.quickChat.names",
|
|
JSON.stringify({ "sess-1": "My custom name" }),
|
|
);
|
|
|
|
const result = produce(makeDraft(), (draft: Draft<AppState>) => {
|
|
hydrateUI(draft, {
|
|
quickChat: {
|
|
isOpen: false,
|
|
activeSessionId: null,
|
|
sessions: [{ sessionId: "sess-1", workspaceId: "ws-1", name: "Agent A - Chat 1" }],
|
|
},
|
|
});
|
|
});
|
|
|
|
expect(result.quickChat.sessions[0].name).toBe("My custom name");
|
|
});
|
|
|
|
it("keeps the SSR-provided name when no local rename exists", () => {
|
|
const result = produce(makeDraft(), (draft: Draft<AppState>) => {
|
|
hydrateUI(draft, {
|
|
quickChat: {
|
|
isOpen: false,
|
|
activeSessionId: null,
|
|
sessions: [{ sessionId: "sess-2", workspaceId: "ws-1", name: "Agent A - Chat 1" }],
|
|
},
|
|
});
|
|
});
|
|
|
|
expect(result.quickChat.sessions[0].name).toBe("Agent A - Chat 1");
|
|
});
|
|
|
|
it("only overlays sessions that have a stored rename, leaving siblings untouched", () => {
|
|
window.localStorage.setItem(
|
|
"kandev.quickChat.names",
|
|
JSON.stringify({ "sess-a": "Renamed A" }),
|
|
);
|
|
|
|
const result = produce(makeDraft(), (draft: Draft<AppState>) => {
|
|
hydrateUI(draft, {
|
|
quickChat: {
|
|
isOpen: false,
|
|
activeSessionId: null,
|
|
sessions: [
|
|
{ sessionId: "sess-a", workspaceId: "ws-1", name: "Original A" },
|
|
{ sessionId: "sess-b", workspaceId: "ws-1", name: "Original B" },
|
|
],
|
|
},
|
|
});
|
|
});
|
|
|
|
expect(result.quickChat.sessions.map((s) => s.name)).toEqual(["Renamed A", "Original B"]);
|
|
});
|
|
});
|