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>
80 lines
3.1 KiB
TypeScript
80 lines
3.1 KiB
TypeScript
import { describe, it, expect, beforeEach } from "vitest";
|
|
import { create } from "zustand";
|
|
import { immer } from "zustand/middleware/immer";
|
|
import { createUISlice } from "@/lib/state/slices/ui/ui-slice";
|
|
import { getStoredQuickChatNames } from "@/lib/local-storage";
|
|
import type { UISlice } from "@/lib/state/slices/ui/types";
|
|
|
|
const SESSION_ID = "sess-1";
|
|
const WORKSPACE_ID = "ws-1";
|
|
const PROFILE_ID = "profile-pass";
|
|
|
|
function makeStore() {
|
|
return create<UISlice>()(immer(createUISlice));
|
|
}
|
|
|
|
function findSession(store: ReturnType<typeof makeStore>) {
|
|
return store.getState().quickChat.sessions.find((s) => s.sessionId === SESSION_ID);
|
|
}
|
|
|
|
describe("openQuickChat agentProfileId persistence", () => {
|
|
it("stores agentProfileId on a new session entry", () => {
|
|
const store = makeStore();
|
|
store.getState().openQuickChat(SESSION_ID, WORKSPACE_ID, PROFILE_ID);
|
|
expect(findSession(store)?.agentProfileId).toBe(PROFILE_ID);
|
|
expect(store.getState().quickChat.activeSessionId).toBe(SESSION_ID);
|
|
});
|
|
|
|
it("backfills agentProfileId on an existing session entry without one", () => {
|
|
const store = makeStore();
|
|
store.getState().openQuickChat(SESSION_ID, WORKSPACE_ID);
|
|
expect(findSession(store)?.agentProfileId).toBeUndefined();
|
|
store.getState().openQuickChat(SESSION_ID, WORKSPACE_ID, PROFILE_ID);
|
|
expect(findSession(store)?.agentProfileId).toBe(PROFILE_ID);
|
|
});
|
|
|
|
it("does not duplicate sessions when reopening with the same id", () => {
|
|
const store = makeStore();
|
|
store.getState().openQuickChat(SESSION_ID, WORKSPACE_ID, "profile-a");
|
|
store.getState().openQuickChat(SESSION_ID, WORKSPACE_ID, "profile-a");
|
|
expect(
|
|
store.getState().quickChat.sessions.filter((s) => s.sessionId === SESSION_ID),
|
|
).toHaveLength(1);
|
|
});
|
|
|
|
it("keeps existing agentProfileId when reopened without one", () => {
|
|
const store = makeStore();
|
|
store.getState().openQuickChat(SESSION_ID, WORKSPACE_ID, PROFILE_ID);
|
|
store.getState().openQuickChat(SESSION_ID, WORKSPACE_ID); // no profile
|
|
expect(findSession(store)?.agentProfileId).toBe(PROFILE_ID);
|
|
});
|
|
});
|
|
|
|
describe("renameQuickChatSession local persistence", () => {
|
|
beforeEach(() => {
|
|
window.localStorage.clear();
|
|
});
|
|
|
|
it("updates the session name in the store", () => {
|
|
const store = makeStore();
|
|
store.getState().openQuickChat(SESSION_ID, WORKSPACE_ID);
|
|
store.getState().renameQuickChatSession(SESSION_ID, "My renamed chat");
|
|
expect(findSession(store)?.name).toBe("My renamed chat");
|
|
});
|
|
|
|
it("persists the rename to localStorage so it survives reload", () => {
|
|
const store = makeStore();
|
|
store.getState().openQuickChat(SESSION_ID, WORKSPACE_ID);
|
|
store.getState().renameQuickChatSession(SESSION_ID, "Persisted name");
|
|
expect(getStoredQuickChatNames()).toEqual({ [SESSION_ID]: "Persisted name" });
|
|
});
|
|
|
|
it("does not write to storage when the session does not exist in state", () => {
|
|
const store = makeStore();
|
|
expect(() =>
|
|
store.getState().renameQuickChatSession("ghost-session", "Whatever"),
|
|
).not.toThrow();
|
|
expect(getStoredQuickChatNames()).toEqual({});
|
|
});
|
|
});
|