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>
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
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";
|
||||
@@ -48,3 +49,31 @@ describe("openQuickChat agentProfileId persistence", () => {
|
||||
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({});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,11 +4,12 @@ import { memo } from "react";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { Dialog, DialogContent, DialogTitle } from "@kandev/ui/dialog";
|
||||
import { Button } from "@kandev/ui/button";
|
||||
import { IconLoader2, IconMessageCircle, IconPlus, IconX } from "@tabler/icons-react";
|
||||
import { IconLoader2, IconMessageCircle, IconPlus } from "@tabler/icons-react";
|
||||
import { useAppStore } from "@/components/state-provider";
|
||||
import { PassthroughTerminal } from "@/components/task/passthrough-terminal";
|
||||
import { QuickChatContent } from "./quick-chat-content";
|
||||
import { QuickChatDeleteDialog } from "./quick-chat-delete-dialog";
|
||||
import { QuickChatTabItem } from "./quick-chat-tab-item";
|
||||
import { useQuickChatModal } from "./use-quick-chat-modal";
|
||||
|
||||
type QuickChatModalProps = {
|
||||
@@ -21,12 +22,14 @@ function QuickChatTabs({
|
||||
onTabChange,
|
||||
onTabClose,
|
||||
onNewChat,
|
||||
onRename,
|
||||
}: {
|
||||
sessions: Array<{ sessionId: string; workspaceId: string; name?: string }>;
|
||||
activeSessionId: string;
|
||||
onTabChange: (sessionId: string) => void;
|
||||
onTabClose: (sessionId: string) => void;
|
||||
onNewChat: () => void;
|
||||
onRename: (sessionId: string, name: string) => void;
|
||||
}) {
|
||||
if (sessions.length === 0) return null;
|
||||
|
||||
@@ -34,34 +37,18 @@ function QuickChatTabs({
|
||||
<div className="flex items-center gap-1 px-2 py-1 border-b bg-muted/20">
|
||||
<div className="flex items-center gap-1 overflow-x-auto flex-1 scrollbar-hide">
|
||||
{sessions.map((s, index) => {
|
||||
const isActive = s.sessionId === activeSessionId;
|
||||
// Show "New Chat" for empty session IDs (agent picker tabs)
|
||||
const tabName = s.sessionId === "" ? "New Chat" : s.name || `Chat ${index + 1}`;
|
||||
return (
|
||||
<div
|
||||
<QuickChatTabItem
|
||||
key={s.sessionId || `new-${index}`}
|
||||
className={`flex items-center gap-1 rounded transition-colors whitespace-nowrap ${
|
||||
isActive
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onTabChange(s.sessionId)}
|
||||
className="flex items-center px-2.5 py-1 text-xs cursor-pointer"
|
||||
>
|
||||
<span className="truncate max-w-[100px]">{tabName}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Close ${tabName}`}
|
||||
className="p-1 cursor-pointer opacity-60 hover:opacity-100"
|
||||
onClick={() => onTabClose(s.sessionId)}
|
||||
>
|
||||
<IconX className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
name={tabName}
|
||||
isActive={s.sessionId === activeSessionId}
|
||||
isRenameable={s.sessionId !== ""}
|
||||
onActivate={() => onTabChange(s.sessionId)}
|
||||
onClose={() => onTabClose(s.sessionId)}
|
||||
onRename={(name) => onRename(s.sessionId, name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -172,6 +159,7 @@ export const QuickChatModal = memo(function QuickChatModal({ workspaceId }: Quic
|
||||
handleSelectAgent,
|
||||
handleCloseTab,
|
||||
handleConfirmClose,
|
||||
handleRename,
|
||||
} = useQuickChatModal(workspaceId);
|
||||
|
||||
return (
|
||||
@@ -189,6 +177,7 @@ export const QuickChatModal = memo(function QuickChatModal({ workspaceId }: Quic
|
||||
onTabChange={setActiveQuickChatSession}
|
||||
onTabClose={handleCloseTab}
|
||||
onNewChat={handleNewChat}
|
||||
onRename={handleRename}
|
||||
/>
|
||||
{activeSessionId && !activeSessionNeedsAgent && (
|
||||
<QuickChatSessionView sessionId={activeSessionId} />
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { afterEach, describe, it, expect, vi } from "vitest";
|
||||
import { cleanup, render, fireEvent } from "@testing-library/react";
|
||||
import { QuickChatTabItem } from "./quick-chat-tab-item";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
function makeProps(overrides: Partial<Parameters<typeof QuickChatTabItem>[0]> = {}) {
|
||||
return {
|
||||
name: "Original",
|
||||
isActive: true,
|
||||
isRenameable: true,
|
||||
onActivate: vi.fn(),
|
||||
onClose: vi.fn(),
|
||||
onRename: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
const RENAME_LABEL = "Rename chat";
|
||||
|
||||
function startEditing(label: HTMLElement) {
|
||||
fireEvent.doubleClick(label);
|
||||
}
|
||||
|
||||
describe("QuickChatTabItem rename", () => {
|
||||
it("commits the rename on Enter, calling onRename exactly once", () => {
|
||||
const onRename = vi.fn();
|
||||
const { getByText, getByLabelText } = render(<QuickChatTabItem {...makeProps({ onRename })} />);
|
||||
startEditing(getByText("Original"));
|
||||
|
||||
const input = getByLabelText(RENAME_LABEL) as HTMLInputElement;
|
||||
fireEvent.change(input, { target: { value: "New name" } });
|
||||
fireEvent.keyDown(input, { key: "Enter" });
|
||||
|
||||
expect(onRename).toHaveBeenCalledTimes(1);
|
||||
expect(onRename).toHaveBeenCalledWith("New name");
|
||||
});
|
||||
|
||||
it("discards the draft on Escape — onRename is NOT called even after blur fires on unmount", () => {
|
||||
const onRename = vi.fn();
|
||||
const { getByText, getByLabelText } = render(<QuickChatTabItem {...makeProps({ onRename })} />);
|
||||
startEditing(getByText("Original"));
|
||||
|
||||
const input = getByLabelText(RENAME_LABEL) as HTMLInputElement;
|
||||
fireEvent.change(input, { target: { value: "Should be discarded" } });
|
||||
fireEvent.keyDown(input, { key: "Escape" });
|
||||
|
||||
expect(onRename).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not call onRename when the trimmed draft equals the original name", () => {
|
||||
const onRename = vi.fn();
|
||||
const { getByText, getByLabelText } = render(<QuickChatTabItem {...makeProps({ onRename })} />);
|
||||
startEditing(getByText("Original"));
|
||||
|
||||
const input = getByLabelText(RENAME_LABEL) as HTMLInputElement;
|
||||
fireEvent.change(input, { target: { value: " Original " } });
|
||||
fireEvent.keyDown(input, { key: "Enter" });
|
||||
|
||||
expect(onRename).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("ignores Enter while IME composition is active", () => {
|
||||
const onRename = vi.fn();
|
||||
const { getByText, getByLabelText } = render(<QuickChatTabItem {...makeProps({ onRename })} />);
|
||||
startEditing(getByText("Original"));
|
||||
|
||||
const input = getByLabelText(RENAME_LABEL) as HTMLInputElement;
|
||||
fireEvent.change(input, { target: { value: "Composing candidate" } });
|
||||
// Enter pressed while IME is composing — should be a no-op (IME confirms candidate).
|
||||
fireEvent.keyDown(input, { key: "Enter", isComposing: true });
|
||||
|
||||
expect(onRename).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not enter edit mode when isRenameable is false", () => {
|
||||
const { getByText, queryByLabelText } = render(
|
||||
<QuickChatTabItem {...makeProps({ isRenameable: false })} />,
|
||||
);
|
||||
startEditing(getByText("Original"));
|
||||
|
||||
expect(queryByLabelText(RENAME_LABEL)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { IconX } from "@tabler/icons-react";
|
||||
|
||||
type QuickChatTabItemProps = {
|
||||
name: string;
|
||||
isActive: boolean;
|
||||
isRenameable: boolean;
|
||||
onActivate: () => void;
|
||||
onClose: () => void;
|
||||
onRename: (name: string) => void;
|
||||
};
|
||||
|
||||
/** Tab in the quick-chat modal. Double-click the label to rename (local-only). */
|
||||
export const QuickChatTabItem = memo(function QuickChatTabItem({
|
||||
name,
|
||||
isActive,
|
||||
isRenameable,
|
||||
onActivate,
|
||||
onClose,
|
||||
onRename,
|
||||
}: QuickChatTabItemProps) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [draft, setDraft] = useState(name);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
// Both Enter and Escape close edit mode by blurring the input so onBlur is
|
||||
// the single commit path. Escape additionally sets this ref so commit knows
|
||||
// to skip the rename — the blur fires synchronously with the typed draft
|
||||
// still in the closure, and we'd otherwise rename to whatever the user typed.
|
||||
const cancelledRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
inputRef.current.select();
|
||||
}
|
||||
}, [isEditing]);
|
||||
|
||||
const commit = useCallback(() => {
|
||||
if (cancelledRef.current) {
|
||||
cancelledRef.current = false;
|
||||
setIsEditing(false);
|
||||
return;
|
||||
}
|
||||
const trimmed = draft.trim();
|
||||
if (trimmed && trimmed !== name) onRename(trimmed);
|
||||
setIsEditing(false);
|
||||
}, [draft, name, onRename]);
|
||||
|
||||
const cancel = useCallback(() => {
|
||||
cancelledRef.current = true;
|
||||
setDraft(name);
|
||||
inputRef.current?.blur();
|
||||
}, [name]);
|
||||
|
||||
const handleStartEdit = useCallback(() => {
|
||||
if (!isRenameable) return;
|
||||
setDraft(name);
|
||||
setIsEditing(true);
|
||||
}, [isRenameable, name]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center gap-1 rounded transition-colors whitespace-nowrap ${
|
||||
isActive
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
{isEditing ? (
|
||||
<input
|
||||
ref={inputRef}
|
||||
aria-label="Rename chat"
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onBlur={commit}
|
||||
onKeyDown={(e) => {
|
||||
// IME composition uses Enter to confirm a candidate; let the IME
|
||||
// handle it instead of committing the rename.
|
||||
if (e.nativeEvent.isComposing) return;
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
// Trigger blur instead of calling commit() directly: the input
|
||||
// unmount on setIsEditing(false) would otherwise fire onBlur and
|
||||
// call commit() a second time.
|
||||
inputRef.current?.blur();
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
cancel();
|
||||
}
|
||||
}}
|
||||
className="px-2.5 py-1 text-xs bg-background border border-input rounded outline-none focus:ring-1 focus:ring-ring max-w-[160px]"
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onActivate}
|
||||
onDoubleClick={handleStartEdit}
|
||||
title={isRenameable ? "Double-click to rename" : undefined}
|
||||
className="flex items-center px-2.5 py-1 text-xs cursor-pointer"
|
||||
>
|
||||
<span className="truncate max-w-[160px]">{name}</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Close ${name}`}
|
||||
className="p-1 cursor-pointer opacity-60 hover:opacity-100"
|
||||
onClick={onClose}
|
||||
>
|
||||
<IconX className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -155,6 +155,14 @@ export function useQuickChatModal(workspaceId: string) {
|
||||
[reset, store],
|
||||
);
|
||||
|
||||
const handleRename = useCallback(
|
||||
(sessionId: string, name: string) => {
|
||||
if (!sessionId) return;
|
||||
store.renameQuickChatSession(sessionId, name);
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
const handleConfirmClose = useCallback(async () => {
|
||||
if (!sessionToClose) return;
|
||||
const sessionId = sessionToClose;
|
||||
@@ -188,5 +196,6 @@ export function useQuickChatModal(workspaceId: string) {
|
||||
handleSelectAgent,
|
||||
handleCloseTab,
|
||||
handleConfirmClose,
|
||||
handleRename,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -642,6 +642,7 @@ export function cleanupTaskStorage(
|
||||
|
||||
// Session-keyed storage — drafts, files panel state, scroll, etc.
|
||||
for (const sessionId of sessionIds) {
|
||||
removeStoredQuickChatName(sessionId);
|
||||
removeSessionStorage(`${PR_PANEL_OFFERED_PREFIX}${sessionId}`);
|
||||
removeSessionStorage(`${CHAT_DRAFT_TEXT_KEY}.${sessionId}`);
|
||||
removeSessionStorage(`${CHAT_DRAFT_CONTENT_KEY}.${sessionId}`);
|
||||
@@ -658,6 +659,36 @@ export function cleanupTaskStorage(
|
||||
}
|
||||
}
|
||||
|
||||
// --- Quick chat custom names (localStorage, global) ---
|
||||
//
|
||||
// Local-only: not persisted to the backend. Keyed by sessionId so the rename
|
||||
// follows the underlying chat across reloads but vanishes if the user clears
|
||||
// browser storage. The backend task title remains the auto-generated name.
|
||||
|
||||
const QUICK_CHAT_NAMES_KEY = "kandev.quickChat.names";
|
||||
|
||||
export function getStoredQuickChatNames(): Record<string, string> {
|
||||
const raw = getLocalStorage<Record<string, string>>(QUICK_CHAT_NAMES_KEY, {});
|
||||
if (!raw || typeof raw !== "object") return {};
|
||||
return raw;
|
||||
}
|
||||
|
||||
export function setStoredQuickChatName(sessionId: string, name: string): void {
|
||||
if (!sessionId) return;
|
||||
const all = getStoredQuickChatNames();
|
||||
if (name) all[sessionId] = name;
|
||||
else delete all[sessionId];
|
||||
setLocalStorage(QUICK_CHAT_NAMES_KEY, all);
|
||||
}
|
||||
|
||||
export function removeStoredQuickChatName(sessionId: string): void {
|
||||
if (!sessionId) return;
|
||||
const all = getStoredQuickChatNames();
|
||||
if (!(sessionId in all)) return;
|
||||
delete all[sessionId];
|
||||
setLocalStorage(QUICK_CHAT_NAMES_KEY, all);
|
||||
}
|
||||
|
||||
// --- Sidebar filter views (localStorage, global) ---
|
||||
|
||||
const SIDEBAR_VIEWS_KEY = "kandev.sidebar.views";
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
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"]);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Draft } from "immer";
|
||||
import type { AppState } from "../store";
|
||||
import { migrateView } from "../slices/ui/ui-slice";
|
||||
import { getStoredQuickChatNames } from "@/lib/local-storage";
|
||||
import { deepMerge, mergeSessionMap, mergeLoadingState } from "./merge-strategies";
|
||||
|
||||
/**
|
||||
@@ -205,14 +206,21 @@ function hydrateSessionRuntime(
|
||||
}
|
||||
|
||||
/** Hydrate UI slices without overwriting active connection state. */
|
||||
function hydrateUI(draft: Draft<AppState>, state: Partial<AppState>): void {
|
||||
export function hydrateUI(draft: Draft<AppState>, state: Partial<AppState>): void {
|
||||
if (state.previewPanel) deepMerge(draft.previewPanel, state.previewPanel);
|
||||
if (state.rightPanel) deepMerge(draft.rightPanel, state.rightPanel);
|
||||
if (state.diffs) deepMerge(draft.diffs, state.diffs);
|
||||
if (state.quickChat) {
|
||||
// Merge quick chat sessions, preserving isOpen from client
|
||||
if (state.quickChat.sessions) {
|
||||
draft.quickChat.sessions = state.quickChat.sessions;
|
||||
// Local renames live in localStorage and override the SSR-provided name
|
||||
// (which derives from the backend task title). Apply on every hydration
|
||||
// so a renamed chat keeps its local name across reloads and tab switches.
|
||||
const storedNames = getStoredQuickChatNames();
|
||||
draft.quickChat.sessions = state.quickChat.sessions.map((s) => {
|
||||
const local = storedNames[s.sessionId];
|
||||
return local ? { ...s, name: local } : s;
|
||||
});
|
||||
// Validate activeSessionId exists in sessions after merge
|
||||
if (
|
||||
draft.quickChat.activeSessionId &&
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
removeStoredSidebarDraft,
|
||||
setLocalStorage,
|
||||
setStoredCollapsedSubtaskParents,
|
||||
setStoredQuickChatName,
|
||||
setStoredSidebarActiveViewId,
|
||||
setStoredSidebarDraft,
|
||||
setStoredSidebarUserViews,
|
||||
@@ -584,13 +585,17 @@ export const createUISlice: StateCreator<UISlice, [["zustand/immer", never]], []
|
||||
set((draft) => {
|
||||
draft.quickChat.activeSessionId = sessionId;
|
||||
}),
|
||||
renameQuickChatSession: (sessionId, name) =>
|
||||
renameQuickChatSession: (sessionId, name) => {
|
||||
let renamed = false;
|
||||
set((draft) => {
|
||||
const session = draft.quickChat.sessions.find((s) => s.sessionId === sessionId);
|
||||
if (session) {
|
||||
session.name = name;
|
||||
renamed = true;
|
||||
}
|
||||
}),
|
||||
});
|
||||
if (renamed) setStoredQuickChatName(sessionId, name);
|
||||
},
|
||||
setSessionFailureNotification: (n) =>
|
||||
set((draft) => {
|
||||
draft.sessionFailureNotification = n;
|
||||
|
||||
Reference in New Issue
Block a user