Files
kandev/apps/web/components/quick-chat/quick-chat-modal.test.ts
T
João Salavisa 5f4b3ec0c0 feat(web): allow renaming quick-chat tabs locally (#830)
* 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>
2026-05-05 21:47:35 +01:00

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({});
});
});