chore: goose2 UI state refactor (part 1) (#9049)
This commit is contained in:
+100
-75
@@ -1,4 +1,6 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { Sidebar } from "@/features/sidebar/ui/Sidebar";
|
||||
import { CreateProjectDialog } from "@/features/projects/ui/CreateProjectDialog";
|
||||
import { archiveProject } from "@/features/projects/api/projects";
|
||||
@@ -8,12 +10,21 @@ import type { SectionId } from "@/features/settings/ui/SettingsModal";
|
||||
import { OPEN_SETTINGS_EVENT } from "@/features/settings/lib/settingsEvents";
|
||||
import { TopBar } from "./ui/TopBar";
|
||||
import { useChatStore } from "@/features/chat/stores/chatStore";
|
||||
import { selectMessagesBySession } from "@/features/chat/stores/chatSelectors";
|
||||
import {
|
||||
type ChatSession,
|
||||
useChatSessionStore,
|
||||
} from "@/features/chat/stores/chatSessionStore";
|
||||
import {
|
||||
selectActiveSessionId,
|
||||
selectHasHydratedSessions,
|
||||
selectSessions,
|
||||
selectSessionsLoading,
|
||||
} from "@/features/chat/stores/chatSessionSelectors";
|
||||
import { useAgentStore } from "@/features/agents/stores/agentStore";
|
||||
import { selectSelectedProvider } from "@/features/agents/stores/agentSelectors";
|
||||
import { useProjectStore } from "@/features/projects/stores/projectStore";
|
||||
import { selectProjects } from "@/features/projects/stores/projectSelectors";
|
||||
import { findExistingDraft } from "@/features/chat/lib/newChat";
|
||||
import { DEFAULT_CHAT_TITLE } from "@/features/chat/lib/sessionTitle";
|
||||
import { useAppStartup } from "./hooks/useAppStartup";
|
||||
@@ -23,6 +34,10 @@ import { resolveSupportedSessionModelPreference } from "./lib/resolveSupportedSe
|
||||
import { useCreatePersonaNavigation } from "./hooks/useCreatePersonaNavigation";
|
||||
import { AppShellContent } from "./ui/AppShellContent";
|
||||
import { acpPrepareSession, acpSetModel } from "@/shared/api/acp";
|
||||
import {
|
||||
updateSessionProject,
|
||||
updateSessionTitle,
|
||||
} from "@/features/chat/stores/chatSessionOperations";
|
||||
import {
|
||||
clearReplayBuffer,
|
||||
getAndDeleteReplayBuffer,
|
||||
@@ -103,6 +118,7 @@ async function syncWindowMinimumSize() {
|
||||
}
|
||||
|
||||
export function AppShell({ children }: { children?: React.ReactNode }) {
|
||||
const { t } = useTranslation("chat");
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
const [sidebarWidth, setSidebarWidth] = useState(SIDEBAR_DEFAULT_WIDTH);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
@@ -120,10 +136,21 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
||||
loadStoredHomeSessionId(),
|
||||
);
|
||||
|
||||
const chatStore = useChatStore();
|
||||
const sessionStore = useChatSessionStore();
|
||||
const agentStore = useAgentStore();
|
||||
const projectStore = useProjectStore();
|
||||
const messagesBySession = useChatStore(selectMessagesBySession);
|
||||
const setChatActiveSession = useChatStore((s) => s.setActiveSession);
|
||||
const cleanupChatSession = useChatStore((s) => s.cleanupSession);
|
||||
const sessions = useChatSessionStore(selectSessions);
|
||||
const activeSessionId = useChatSessionStore(selectActiveSessionId);
|
||||
const hasHydratedSessions = useChatSessionStore(selectHasHydratedSessions);
|
||||
const sessionsLoading = useChatSessionStore(selectSessionsLoading);
|
||||
const createSession = useChatSessionStore((s) => s.createSession);
|
||||
const patchSession = useChatSessionStore((s) => s.patchSession);
|
||||
const setActiveSession = useChatSessionStore((s) => s.setActiveSession);
|
||||
const archiveSession = useChatSessionStore((s) => s.archiveSession);
|
||||
const selectedProvider = useAgentStore(selectSelectedProvider);
|
||||
const projects = useProjectStore(selectProjects);
|
||||
const fetchProjects = useProjectStore((s) => s.fetchProjects);
|
||||
const reorderProjects = useProjectStore((s) => s.reorderProjects);
|
||||
const providerInventoryEntries = useProviderInventoryStore((s) => s.entries);
|
||||
const startup = useAppStartup();
|
||||
const onboardingGate = useOnboardingGate(startup.ready);
|
||||
@@ -182,10 +209,8 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
projectStore.fetchProjects();
|
||||
}, [projectStore.fetchProjects]);
|
||||
|
||||
const { activeSessionId } = sessionStore;
|
||||
fetchProjects();
|
||||
}, [fetchProjects]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeView === "chat" && activeSessionId) {
|
||||
@@ -194,23 +219,23 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
||||
}, [activeSessionId, activeView]);
|
||||
|
||||
const activeSession = activeSessionId
|
||||
? sessionStore.getSession(activeSessionId)
|
||||
? sessions.find((session) => session.id === activeSessionId)
|
||||
: undefined;
|
||||
const homeSession = homeSessionId
|
||||
? sessionStore.getSession(homeSessionId)
|
||||
? sessions.find((session) => session.id === homeSessionId)
|
||||
: undefined;
|
||||
|
||||
useHomeSessionStateSync({
|
||||
homeSessionId,
|
||||
homeSession,
|
||||
messagesBySession: chatStore.messagesBySession,
|
||||
hasHydratedSessions: sessionStore.hasHydratedSessions,
|
||||
isLoading: sessionStore.isLoading,
|
||||
messagesBySession,
|
||||
hasHydratedSessions,
|
||||
isLoading: sessionsLoading,
|
||||
setHomeSessionId,
|
||||
});
|
||||
|
||||
const ensureHomeSession = useCallback(async () => {
|
||||
if (!sessionStore.hasHydratedSessions || sessionStore.isLoading) {
|
||||
if (!hasHydratedSessions || sessionsLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -226,11 +251,11 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
||||
) {
|
||||
const sessionModelPreference =
|
||||
await resolveSupportedSessionModelPreference(
|
||||
agentStore.selectedProvider ?? "goose",
|
||||
selectedProvider ?? "goose",
|
||||
providerInventoryEntries,
|
||||
);
|
||||
const project = homeSession.projectId
|
||||
? (projectStore.projects.find(
|
||||
? (projects.find(
|
||||
(candidate) => candidate.id === homeSession.projectId,
|
||||
) ?? null)
|
||||
: null;
|
||||
@@ -243,14 +268,14 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
||||
const shouldClearHomeModel =
|
||||
sessionModelPreference.providerId !== homeSession.providerId ||
|
||||
!sessionModelPreference.modelId;
|
||||
sessionStore.updateSession(homeSession.id, {
|
||||
patchSession(homeSession.id, {
|
||||
providerId: sessionModelPreference.providerId,
|
||||
modelId: shouldClearHomeModel ? undefined : homeSession.modelId,
|
||||
modelName: shouldClearHomeModel ? undefined : homeSession.modelName,
|
||||
});
|
||||
if (sessionModelPreference.modelId) {
|
||||
await acpSetModel(homeSession.id, sessionModelPreference.modelId);
|
||||
sessionStore.updateSession(homeSession.id, {
|
||||
patchSession(homeSession.id, {
|
||||
modelId: sessionModelPreference.modelId,
|
||||
modelName: sessionModelPreference.modelName,
|
||||
});
|
||||
@@ -261,10 +286,10 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
||||
const workingDir = await resolveSessionCwd(null);
|
||||
const sessionModelPreference =
|
||||
await resolveSupportedSessionModelPreference(
|
||||
agentStore.selectedProvider ?? "goose",
|
||||
selectedProvider ?? "goose",
|
||||
providerInventoryEntries,
|
||||
);
|
||||
const session = await sessionStore.createSession({
|
||||
const session = await createSession({
|
||||
title: DEFAULT_CHAT_TITLE,
|
||||
providerId: sessionModelPreference.providerId,
|
||||
workingDir,
|
||||
@@ -284,13 +309,14 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
||||
}
|
||||
}
|
||||
}, [
|
||||
agentStore.selectedProvider,
|
||||
selectedProvider,
|
||||
createSession,
|
||||
hasHydratedSessions,
|
||||
homeSession,
|
||||
providerInventoryEntries,
|
||||
projectStore.projects,
|
||||
sessionStore.hasHydratedSessions,
|
||||
sessionStore,
|
||||
sessionStore.isLoading,
|
||||
projects,
|
||||
sessionsLoading,
|
||||
patchSession,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -309,7 +335,7 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
||||
`[perf:newtab] createNewTab start (project=${project?.id ?? "none"})`,
|
||||
);
|
||||
const providerId =
|
||||
project?.preferredProvider ?? agentStore.selectedProvider ?? "goose";
|
||||
project?.preferredProvider ?? selectedProvider ?? "goose";
|
||||
const sessionModelPreference =
|
||||
await resolveSupportedSessionModelPreference(
|
||||
providerId,
|
||||
@@ -330,9 +356,9 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
||||
});
|
||||
|
||||
if (existingDraft) {
|
||||
sessionStore.setActiveSession(existingDraft.id);
|
||||
setActiveSession(existingDraft.id);
|
||||
setActiveView("chat");
|
||||
chatStore.setActiveSession(existingDraft.id);
|
||||
setChatActiveSession(existingDraft.id);
|
||||
perfLog(
|
||||
`[perf:newtab] ${existingDraft.id.slice(0, 8)} reused draft in ${(performance.now() - tStart).toFixed(1)}ms`,
|
||||
);
|
||||
@@ -340,7 +366,7 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
||||
}
|
||||
|
||||
const workingDir = await resolveSessionCwd(project);
|
||||
const session = await sessionStore.createSession({
|
||||
const session = await createSession({
|
||||
title,
|
||||
projectId: project?.id,
|
||||
providerId: sessionModelPreference.providerId,
|
||||
@@ -348,19 +374,20 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
||||
modelId: sessionModelPreference.modelId,
|
||||
modelName: sessionModelPreference.modelName,
|
||||
});
|
||||
sessionStore.setActiveSession(session.id);
|
||||
setActiveSession(session.id);
|
||||
setActiveView("chat");
|
||||
chatStore.setActiveSession(session.id);
|
||||
setChatActiveSession(session.id);
|
||||
perfLog(
|
||||
`[perf:newtab] ${session.id.slice(0, 8)} created session in ${(performance.now() - tStart).toFixed(1)}ms`,
|
||||
);
|
||||
return session;
|
||||
},
|
||||
[
|
||||
agentStore.selectedProvider,
|
||||
chatStore,
|
||||
selectedProvider,
|
||||
createSession,
|
||||
providerInventoryEntries,
|
||||
sessionStore,
|
||||
setActiveSession,
|
||||
setChatActiveSession,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -374,7 +401,7 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
||||
const handleStartChatWithSkill = useCallback(
|
||||
(skill: SkillInfo, projectId?: string | null) => {
|
||||
const project = projectId
|
||||
? projectStore.projects.find((candidate) => candidate.id === projectId)
|
||||
? projects.find((candidate) => candidate.id === projectId)
|
||||
: undefined;
|
||||
|
||||
void createNewTab(DEFAULT_CHAT_TITLE, project)
|
||||
@@ -387,38 +414,38 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
||||
console.error("Failed to start chat with skill:", error);
|
||||
});
|
||||
},
|
||||
[createNewTab, projectStore.projects],
|
||||
[createNewTab, projects],
|
||||
);
|
||||
|
||||
const handleNewChatInProject = useCallback(
|
||||
(projectId: string) => {
|
||||
const project = projectStore.projects.find((p) => p.id === projectId);
|
||||
const project = projects.find((p) => p.id === projectId);
|
||||
if (project) {
|
||||
void createNewTab(DEFAULT_CHAT_TITLE, project);
|
||||
}
|
||||
},
|
||||
[createNewTab, projectStore.projects],
|
||||
[createNewTab, projects],
|
||||
);
|
||||
|
||||
const handleArchiveProject = useCallback(
|
||||
async (projectId: string) => {
|
||||
try {
|
||||
await archiveProject(projectId);
|
||||
projectStore.fetchProjects();
|
||||
fetchProjects();
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
},
|
||||
[projectStore.fetchProjects],
|
||||
[fetchProjects],
|
||||
);
|
||||
|
||||
const clearActiveSession = useCallback(
|
||||
(sessionId: string) => {
|
||||
chatStore.cleanupSession(sessionId);
|
||||
sessionStore.setActiveSession(null);
|
||||
cleanupChatSession(sessionId);
|
||||
setActiveSession(null);
|
||||
setActiveView("home");
|
||||
},
|
||||
[chatStore, sessionStore],
|
||||
[cleanupChatSession, setActiveSession],
|
||||
);
|
||||
const openSettings = useCallback((section: SectionId = "appearance") => {
|
||||
setSettingsInitialSection(section);
|
||||
@@ -456,43 +483,43 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
||||
const wasActiveSession = currentActiveSessionId === sessionId;
|
||||
|
||||
try {
|
||||
await sessionStore.archiveSession(sessionId);
|
||||
chatStore.cleanupSession(sessionId);
|
||||
await archiveSession(sessionId);
|
||||
cleanupChatSession(sessionId);
|
||||
|
||||
if (!wasActiveSession) {
|
||||
return;
|
||||
}
|
||||
|
||||
sessionStore.setActiveSession(null);
|
||||
setActiveSession(null);
|
||||
setActiveView("home");
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
},
|
||||
[chatStore, sessionStore],
|
||||
[archiveSession, cleanupChatSession, setActiveSession],
|
||||
);
|
||||
|
||||
const handleEditProject = useCallback(
|
||||
(projectId: string) => {
|
||||
const project = projectStore.projects.find((p) => p.id === projectId);
|
||||
const project = projects.find((p) => p.id === projectId);
|
||||
if (project) {
|
||||
setEditingProject(project);
|
||||
setCreateProjectOpen(true);
|
||||
}
|
||||
},
|
||||
[projectStore.projects],
|
||||
[projects],
|
||||
);
|
||||
|
||||
const handleMoveToProject = useCallback(
|
||||
(sessionId: string, projectId: string | null) => {
|
||||
sessionStore.updateSession(sessionId, { projectId });
|
||||
|
||||
const session = useChatSessionStore.getState().getSession(sessionId);
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
void (async () => {
|
||||
await updateSessionProject(sessionId, projectId);
|
||||
|
||||
const nextProject =
|
||||
projectId == null
|
||||
? null
|
||||
@@ -505,27 +532,25 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
||||
}
|
||||
await acpPrepareSession(
|
||||
sessionId,
|
||||
session.providerId ?? agentStore.selectedProvider ?? "goose",
|
||||
session.providerId ?? selectedProvider ?? "goose",
|
||||
workingDir,
|
||||
);
|
||||
})().catch((error) => {
|
||||
console.error(
|
||||
"Failed to update ACP session project working directory:",
|
||||
error,
|
||||
);
|
||||
console.error("Failed to move chat to project:", error);
|
||||
toast.error(t("notifications.moveError"));
|
||||
});
|
||||
},
|
||||
[agentStore.selectedProvider, sessionStore],
|
||||
[selectedProvider, t],
|
||||
);
|
||||
|
||||
const handleRenameChat = useCallback(
|
||||
(sessionId: string, nextTitle: string) => {
|
||||
sessionStore.updateSession(sessionId, {
|
||||
title: nextTitle,
|
||||
userSetName: true,
|
||||
void updateSessionTitle(sessionId, nextTitle).catch((error) => {
|
||||
console.error("Failed to rename session:", error);
|
||||
toast.error(t("notifications.renameError"));
|
||||
});
|
||||
},
|
||||
[sessionStore],
|
||||
[t],
|
||||
);
|
||||
|
||||
const openCreateProjectDialog = useCallback(
|
||||
@@ -546,23 +571,23 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
||||
if (homeSessionId === sessionId) {
|
||||
setHomeSessionId(null);
|
||||
}
|
||||
sessionStore.setActiveSession(sessionId);
|
||||
setActiveSession(sessionId);
|
||||
setActiveView("chat");
|
||||
chatStore.setActiveSession(sessionId);
|
||||
setChatActiveSession(sessionId);
|
||||
useChatStore.getState().markSessionRead(sessionId);
|
||||
},
|
||||
[chatStore, homeSessionId, sessionStore],
|
||||
[homeSessionId, setActiveSession, setChatActiveSession],
|
||||
);
|
||||
|
||||
const handleSelectSession = useCallback(
|
||||
(id: string) => {
|
||||
sessionStore.setActiveSession(id);
|
||||
setActiveSession(id);
|
||||
setActiveView("chat");
|
||||
chatStore.setActiveSession(id);
|
||||
setChatActiveSession(id);
|
||||
useChatStore.getState().markSessionRead(id);
|
||||
loadSessionMessages(id);
|
||||
},
|
||||
[sessionStore, chatStore, loadSessionMessages],
|
||||
[setActiveSession, setChatActiveSession, loadSessionMessages],
|
||||
);
|
||||
|
||||
const handleSelectSearchResult = useCallback(
|
||||
@@ -580,11 +605,11 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
||||
const handleNavigate = useCallback(
|
||||
(view: AppView) => {
|
||||
if (view !== "chat") {
|
||||
sessionStore.setActiveSession(null);
|
||||
setActiveSession(null);
|
||||
}
|
||||
setActiveView(view);
|
||||
},
|
||||
[sessionStore],
|
||||
[setActiveSession],
|
||||
);
|
||||
|
||||
const handleCreatePersona = useCreatePersonaNavigation(() =>
|
||||
@@ -717,13 +742,13 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
||||
// Cmd+N opens new conversation screen
|
||||
if (e.key === "n" && e.metaKey) {
|
||||
e.preventDefault();
|
||||
sessionStore.setActiveSession(null);
|
||||
setActiveSession(null);
|
||||
setActiveView("home");
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handler);
|
||||
return () => window.removeEventListener("keydown", handler);
|
||||
}, [clearActiveSession, sessionStore, toggleSidebar]);
|
||||
}, [clearActiveSession, setActiveSession, toggleSidebar]);
|
||||
|
||||
if (!startup.ready) {
|
||||
return (
|
||||
@@ -768,7 +793,7 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
||||
onNavigate={handleNavigate}
|
||||
onNewChatInProject={handleNewChatInProject}
|
||||
onNewChat={() => {
|
||||
sessionStore.setActiveSession(null);
|
||||
setActiveSession(null);
|
||||
setActiveView("home");
|
||||
}}
|
||||
onCreateProject={() => openCreateProjectDialog()}
|
||||
@@ -777,12 +802,12 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
||||
onArchiveChat={handleArchiveChat}
|
||||
onRenameChat={handleRenameChat}
|
||||
onMoveToProject={handleMoveToProject}
|
||||
onReorderProject={projectStore.reorderProjects}
|
||||
onReorderProject={reorderProjects}
|
||||
onSelectSession={handleSelectSession}
|
||||
onSelectSearchResult={handleSelectSearchResult}
|
||||
activeView={activeView}
|
||||
activeSessionId={activeSessionId}
|
||||
projects={projectStore.projects}
|
||||
projects={projects}
|
||||
className="h-full rounded-xl"
|
||||
/>
|
||||
</div>
|
||||
@@ -832,7 +857,7 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
||||
pendingProjectCreatedRef.current = null;
|
||||
}}
|
||||
onCreated={(project) => {
|
||||
projectStore.fetchProjects();
|
||||
fetchProjects();
|
||||
pendingProjectCreatedRef.current?.(project.id);
|
||||
pendingProjectCreatedRef.current = null;
|
||||
setCreateProjectInitialWorkingDir(null);
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { useEffect, useCallback, useRef } from "react";
|
||||
import { useAgentStore } from "../stores/agentStore";
|
||||
import {
|
||||
selectPersonas,
|
||||
selectPersonasLoading,
|
||||
} from "../stores/agentSelectors";
|
||||
import type {
|
||||
CreatePersonaRequest,
|
||||
UpdatePersonaRequest,
|
||||
@@ -9,32 +13,36 @@ import * as api from "@/shared/api/agents";
|
||||
const REFRESH_INTERVAL_MS = 60_000;
|
||||
|
||||
export function usePersonas() {
|
||||
const store = useAgentStore();
|
||||
const personas = useAgentStore(selectPersonas);
|
||||
const personasLoading = useAgentStore(selectPersonasLoading);
|
||||
const setPersonas = useAgentStore((s) => s.setPersonas);
|
||||
const addPersona = useAgentStore((s) => s.addPersona);
|
||||
const updatePersonaInStore = useAgentStore((s) => s.updatePersona);
|
||||
const removePersona = useAgentStore((s) => s.removePersona);
|
||||
const setPersonasLoading = useAgentStore((s) => s.setPersonasLoading);
|
||||
const refreshTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: store is stable and should not trigger re-creation
|
||||
const loadPersonas = useCallback(async () => {
|
||||
store.setPersonasLoading(true);
|
||||
setPersonasLoading(true);
|
||||
try {
|
||||
const personas = await api.listPersonas();
|
||||
store.setPersonas(personas);
|
||||
setPersonas(personas);
|
||||
} catch (error) {
|
||||
console.error("Failed to load personas:", error);
|
||||
// Fall back to empty list - builtins will come from backend
|
||||
} finally {
|
||||
store.setPersonasLoading(false);
|
||||
setPersonasLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [setPersonas, setPersonasLoading]);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: store is stable and should not trigger re-creation
|
||||
const refreshFromDisk = useCallback(async () => {
|
||||
try {
|
||||
const personas = await api.refreshPersonas();
|
||||
store.setPersonas(personas);
|
||||
setPersonas(personas);
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh personas from disk:", error);
|
||||
}
|
||||
}, []);
|
||||
}, [setPersonas]);
|
||||
|
||||
useEffect(() => {
|
||||
loadPersonas();
|
||||
@@ -57,32 +65,35 @@ export function usePersonas() {
|
||||
};
|
||||
}, [refreshFromDisk]);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: store is stable and should not trigger re-creation
|
||||
const createPersona = useCallback(async (req: CreatePersonaRequest) => {
|
||||
const persona = await api.createPersona(req);
|
||||
store.addPersona(persona);
|
||||
return persona;
|
||||
}, []);
|
||||
const createPersona = useCallback(
|
||||
async (req: CreatePersonaRequest) => {
|
||||
const persona = await api.createPersona(req);
|
||||
addPersona(persona);
|
||||
return persona;
|
||||
},
|
||||
[addPersona],
|
||||
);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: store is stable and should not trigger re-creation
|
||||
const updatePersona = useCallback(
|
||||
async (id: string, req: UpdatePersonaRequest) => {
|
||||
const persona = await api.updatePersona(id, req);
|
||||
store.updatePersona(id, persona);
|
||||
updatePersonaInStore(id, persona);
|
||||
return persona;
|
||||
},
|
||||
[],
|
||||
[updatePersonaInStore],
|
||||
);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: store is stable and should not trigger re-creation
|
||||
const deletePersona = useCallback(async (id: string) => {
|
||||
await api.deletePersona(id);
|
||||
store.removePersona(id);
|
||||
}, []);
|
||||
const deletePersona = useCallback(
|
||||
async (id: string) => {
|
||||
await api.deletePersona(id);
|
||||
removePersona(id);
|
||||
},
|
||||
[removePersona],
|
||||
);
|
||||
|
||||
return {
|
||||
personas: store.personas,
|
||||
isLoading: store.personasLoading,
|
||||
personas,
|
||||
isLoading: personasLoading,
|
||||
createPersona,
|
||||
updatePersona,
|
||||
deletePersona,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useCallback } from "react";
|
||||
import { useAgentStore } from "../stores/agentStore";
|
||||
import { selectSelectedProvider } from "../stores/agentSelectors";
|
||||
|
||||
export function useProviderSelection() {
|
||||
const providers = useAgentStore((s) => s.providers);
|
||||
const providersLoading = useAgentStore((s) => s.providersLoading);
|
||||
const selectedProvider = useAgentStore((s) => s.selectedProvider);
|
||||
const selectedProvider = useAgentStore(selectSelectedProvider);
|
||||
const storeSetSelectedProvider = useAgentStore((s) => s.setSelectedProvider);
|
||||
|
||||
const setSelectedProvider = useCallback(
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { AgentStore } from "./agentStore";
|
||||
|
||||
export const selectPersonas = (state: AgentStore) => state.personas;
|
||||
|
||||
export const selectPersonasLoading = (state: AgentStore) =>
|
||||
state.personasLoading;
|
||||
|
||||
export const selectSelectedProvider = (state: AgentStore) =>
|
||||
state.selectedProvider;
|
||||
@@ -16,6 +16,10 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from "@/shared/ui/alert-dialog";
|
||||
import { useAgentStore } from "@/features/agents/stores/agentStore";
|
||||
import {
|
||||
selectPersonas,
|
||||
selectPersonasLoading,
|
||||
} from "@/features/agents/stores/agentSelectors";
|
||||
import { PersonaGallery } from "@/features/agents/ui/PersonaGallery";
|
||||
import { PersonaEditor } from "@/features/agents/ui/PersonaEditor";
|
||||
import {
|
||||
@@ -41,8 +45,8 @@ export function AgentsView() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [deletingPersona, setDeletingPersona] = useState<Persona | null>(null);
|
||||
|
||||
const personas = useAgentStore((s) => s.personas);
|
||||
const personasLoading = useAgentStore((s) => s.personasLoading);
|
||||
const personas = useAgentStore(selectPersonas);
|
||||
const personasLoading = useAgentStore(selectPersonasLoading);
|
||||
const personaEditorOpen = useAgentStore((s) => s.personaEditorOpen);
|
||||
const editingPersona = useAgentStore((s) => s.editingPersona);
|
||||
const personaEditorMode = useAgentStore((s) => s.personaEditorMode);
|
||||
|
||||
+2
-2
@@ -246,7 +246,7 @@ describe("useChatSessionController compaction behavior", () => {
|
||||
useChatStore
|
||||
.getState()
|
||||
.replaceTokenState("session-1", mockTokenState, true);
|
||||
useChatSessionStore.getState().updateSession("session-1", {
|
||||
useChatSessionStore.getState().patchSession("session-1", {
|
||||
providerId: "goose",
|
||||
});
|
||||
|
||||
@@ -300,7 +300,7 @@ describe("useChatSessionController compaction behavior", () => {
|
||||
useChatStore
|
||||
.getState()
|
||||
.replaceTokenState("session-1", mockTokenState, true);
|
||||
useChatSessionStore.getState().updateSession("session-1", {
|
||||
useChatSessionStore.getState().patchSession("session-1", {
|
||||
providerId: "goose",
|
||||
personaId: "persona-b",
|
||||
});
|
||||
|
||||
@@ -4,10 +4,12 @@ import { useChatSessionStore } from "../stores/chatSessionStore";
|
||||
import { clearReplayBuffer, getAndDeleteReplayBuffer } from "./replayBuffer";
|
||||
import {
|
||||
type ChatAttachmentDraft,
|
||||
type Message,
|
||||
createSystemNotificationMessage,
|
||||
createUserMessage,
|
||||
} from "@/shared/types/messages";
|
||||
import type { ChatState, TokenState } from "@/shared/types/chat";
|
||||
import { INITIAL_SESSION_CHAT_RUNTIME } from "@/shared/types/chat";
|
||||
import {
|
||||
acpSendMessage,
|
||||
acpCancelSession,
|
||||
@@ -18,7 +20,6 @@ import {
|
||||
getSessionTitleFromDraft,
|
||||
isDefaultChatTitle,
|
||||
} from "../lib/sessionTitle";
|
||||
import { findLastIndex } from "@/shared/lib/arrays";
|
||||
import { perfLog } from "@/shared/lib/perfLog";
|
||||
import {
|
||||
appendAttachmentPaths,
|
||||
@@ -28,10 +29,10 @@ import {
|
||||
import { sanitizeReplayMessages } from "../lib/replaySanitizer";
|
||||
import { i18n } from "@/shared/i18n";
|
||||
import type { ChatSendOptions } from "../types";
|
||||
import { buildSkillRetryOptions } from "../lib/skillSendPayload";
|
||||
|
||||
// TODO: Remove this fallback once goose2 has first-class /-commands.
|
||||
const MANUAL_COMPACT_TRIGGER = "/compact";
|
||||
const EMPTY_MESSAGES: Message[] = [];
|
||||
type CompactConversationResult = "completed" | "failed" | "skipped";
|
||||
|
||||
function createCompactionConfirmationMessage() {
|
||||
@@ -104,12 +105,28 @@ export function useChat(
|
||||
ensurePrepared?: (personaId?: string) => Promise<void>;
|
||||
},
|
||||
) {
|
||||
const store = useChatStore();
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const messages = store.messagesBySession[sessionId] ?? [];
|
||||
const { chatState, tokenState, error, streamingMessageId } =
|
||||
store.getSessionRuntime(sessionId);
|
||||
const messages = useChatStore(
|
||||
(s) => s.messagesBySession[sessionId] ?? EMPTY_MESSAGES,
|
||||
);
|
||||
const runtime = useChatStore(
|
||||
(s) => s.sessionStateById[sessionId] ?? INITIAL_SESSION_CHAT_RUNTIME,
|
||||
);
|
||||
const setActiveSession = useChatStore((s) => s.setActiveSession);
|
||||
const addMessage = useChatStore((s) => s.addMessage);
|
||||
const setMessages = useChatStore((s) => s.setMessages);
|
||||
const clearMessages = useChatStore((s) => s.clearMessages);
|
||||
const setChatState = useChatStore((s) => s.setChatState);
|
||||
const setError = useChatStore((s) => s.setError);
|
||||
const setStreamingMessageId = useChatStore((s) => s.setStreamingMessageId);
|
||||
const setPendingAssistantProvider = useChatStore(
|
||||
(s) => s.setPendingAssistantProvider,
|
||||
);
|
||||
const clearDraft = useChatStore((s) => s.clearDraft);
|
||||
const setSessionLoading = useChatStore((s) => s.setSessionLoading);
|
||||
|
||||
const { chatState, tokenState, error, streamingMessageId } = runtime;
|
||||
const isStreaming = chatState === "streaming" || streamingMessageId !== null;
|
||||
|
||||
const resolvePersonaInfo = useCallback(
|
||||
@@ -166,8 +183,8 @@ export function useChat(
|
||||
systemPromptOverride ?? agent?.systemPrompt ?? undefined;
|
||||
|
||||
// Ensure active session
|
||||
store.setActiveSession(sessionId);
|
||||
store.setPendingAssistantProvider(sessionId, providerId);
|
||||
setActiveSession(sessionId);
|
||||
setPendingAssistantProvider(sessionId, providerId);
|
||||
|
||||
// Create and add user message
|
||||
const userMessage = createUserMessage(
|
||||
@@ -195,9 +212,9 @@ export function useChat(
|
||||
});
|
||||
}
|
||||
}
|
||||
store.addMessage(sessionId, userMessage);
|
||||
store.setChatState(sessionId, "thinking");
|
||||
store.setError(sessionId, null);
|
||||
addMessage(sessionId, userMessage);
|
||||
setChatState(sessionId, "thinking");
|
||||
setError(sessionId, null);
|
||||
|
||||
const sessionStore = useChatSessionStore.getState();
|
||||
const session = sessionStore.getSession(sessionId);
|
||||
@@ -208,19 +225,19 @@ export function useChat(
|
||||
// A better backend-generated title will overwrite this if it arrives
|
||||
// via the acp:session_info event.
|
||||
if (session && isDefaultChatTitle(session.title)) {
|
||||
sessionStore.updateSession(sessionId, {
|
||||
sessionStore.patchSession(sessionId, {
|
||||
title: getSessionTitleFromDraft(text, attachments),
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
sessionStore.updateSession(sessionId, {
|
||||
sessionStore.patchSession(sessionId, {
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
options?.onMessageAccepted?.(sessionId);
|
||||
|
||||
store.clearDraft(sessionId);
|
||||
clearDraft(sessionId);
|
||||
|
||||
const abort = new AbortController();
|
||||
abortRef.current = abort;
|
||||
@@ -228,7 +245,7 @@ export function useChat(
|
||||
try {
|
||||
await options?.ensurePrepared?.(effectivePersonaInfo?.id);
|
||||
|
||||
store.setChatState(sessionId, "streaming");
|
||||
setChatState(sessionId, "streaming");
|
||||
const promptWithPaths = appendAttachmentPaths(text.trim(), attachments);
|
||||
const acpPrompt =
|
||||
promptWithPaths || (images?.length ? " " : promptWithPaths);
|
||||
@@ -251,11 +268,11 @@ export function useChat(
|
||||
`[perf:send] ${sid} acpSendMessage returned after ${(performance.now() - tAcp).toFixed(1)}ms (total sendMessage ${(performance.now() - tSendStart).toFixed(1)}ms)`,
|
||||
);
|
||||
|
||||
store.setChatState(sessionId, "idle");
|
||||
store.setStreamingMessageId(sessionId, null);
|
||||
setChatState(sessionId, "idle");
|
||||
setStreamingMessageId(sessionId, null);
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === "AbortError") {
|
||||
store.setChatState(sessionId, "idle");
|
||||
setChatState(sessionId, "idle");
|
||||
} else {
|
||||
const errorMessage = getErrorMessage(err);
|
||||
const liveStore = useChatStore.getState();
|
||||
@@ -278,18 +295,24 @@ export function useChat(
|
||||
sessionId,
|
||||
createSystemNotificationMessage(errorMessage, "error"),
|
||||
);
|
||||
store.setError(sessionId, errorMessage);
|
||||
store.setChatState(sessionId, "idle");
|
||||
store.setStreamingMessageId(sessionId, null);
|
||||
setError(sessionId, errorMessage);
|
||||
setChatState(sessionId, "idle");
|
||||
setStreamingMessageId(sessionId, null);
|
||||
}
|
||||
store.setPendingAssistantProvider(sessionId, null);
|
||||
setPendingAssistantProvider(sessionId, null);
|
||||
} finally {
|
||||
abortRef.current = null;
|
||||
}
|
||||
},
|
||||
[
|
||||
sessionId,
|
||||
store,
|
||||
setActiveSession,
|
||||
setPendingAssistantProvider,
|
||||
addMessage,
|
||||
setChatState,
|
||||
setError,
|
||||
clearDraft,
|
||||
setStreamingMessageId,
|
||||
providerOverride,
|
||||
systemPromptOverride,
|
||||
resolvePersonaInfo,
|
||||
@@ -303,9 +326,9 @@ export function useChat(
|
||||
.getState()
|
||||
.getSessionRuntime(sessionId).streamingMessageId;
|
||||
|
||||
store.setChatState(sessionId, "idle");
|
||||
store.setStreamingMessageId(sessionId, null);
|
||||
store.setPendingAssistantProvider(sessionId, null);
|
||||
setChatState(sessionId, "idle");
|
||||
setStreamingMessageId(sessionId, null);
|
||||
setPendingAssistantProvider(sessionId, null);
|
||||
// Cancel the backend ACP session to stop orphaned streaming events
|
||||
acpCancelSession(sessionId)
|
||||
.then((wasCancelled) => {
|
||||
@@ -316,50 +339,26 @@ export function useChat(
|
||||
.catch(() => {
|
||||
// Best-effort cancellation — ignore errors
|
||||
});
|
||||
}, [store, sessionId]);
|
||||
|
||||
const retryLastMessage = useCallback(async () => {
|
||||
const sessionMessages = store.messagesBySession[sessionId] ?? [];
|
||||
// Find the last user message
|
||||
const lastUserIndex = findLastIndex(
|
||||
sessionMessages,
|
||||
(m) => m.role === "user",
|
||||
);
|
||||
if (lastUserIndex === -1) return;
|
||||
|
||||
const lastUserMessage = sessionMessages[lastUserIndex];
|
||||
|
||||
// Remove all messages after (and including) the last assistant response
|
||||
const messagesToKeep = sessionMessages.slice(0, lastUserIndex);
|
||||
store.setMessages(sessionId, messagesToKeep);
|
||||
|
||||
// Extract the text and resend
|
||||
const textContent = lastUserMessage.content.find((c) => c.type === "text");
|
||||
if (textContent && "text" in textContent) {
|
||||
const targetPersonaId = lastUserMessage.metadata?.targetPersonaId;
|
||||
const targetPersonaName = lastUserMessage.metadata?.targetPersonaName;
|
||||
const retryOptions = buildSkillRetryOptions(
|
||||
textContent.text,
|
||||
lastUserMessage.metadata?.chips,
|
||||
);
|
||||
await sendMessage(
|
||||
textContent.text || (retryOptions ? " " : ""),
|
||||
targetPersonaId
|
||||
? { id: targetPersonaId, name: targetPersonaName }
|
||||
: undefined,
|
||||
undefined,
|
||||
retryOptions,
|
||||
);
|
||||
}
|
||||
}, [sessionId, store, sendMessage]);
|
||||
}, [
|
||||
setChatState,
|
||||
setPendingAssistantProvider,
|
||||
setStreamingMessageId,
|
||||
sessionId,
|
||||
]);
|
||||
|
||||
const clearChat = useCallback(() => {
|
||||
abortRef.current?.abort();
|
||||
store.clearMessages(sessionId);
|
||||
store.setChatState(sessionId, "idle");
|
||||
store.setStreamingMessageId(sessionId, null);
|
||||
store.setPendingAssistantProvider(sessionId, null);
|
||||
}, [sessionId, store]);
|
||||
clearMessages(sessionId);
|
||||
setChatState(sessionId, "idle");
|
||||
setStreamingMessageId(sessionId, null);
|
||||
setPendingAssistantProvider(sessionId, null);
|
||||
}, [
|
||||
sessionId,
|
||||
clearMessages,
|
||||
setChatState,
|
||||
setStreamingMessageId,
|
||||
setPendingAssistantProvider,
|
||||
]);
|
||||
|
||||
const getWorkingDir = useCallback(
|
||||
() =>
|
||||
@@ -381,25 +380,25 @@ export function useChat(
|
||||
overridePersona?.name,
|
||||
);
|
||||
|
||||
store.setActiveSession(sessionId);
|
||||
store.setChatState(sessionId, "compacting");
|
||||
store.setStreamingMessageId(sessionId, null);
|
||||
store.setError(sessionId, null);
|
||||
setActiveSession(sessionId);
|
||||
setChatState(sessionId, "compacting");
|
||||
setStreamingMessageId(sessionId, null);
|
||||
setError(sessionId, null);
|
||||
|
||||
try {
|
||||
await options?.ensurePrepared?.(effectivePersonaInfo?.id);
|
||||
} catch (err) {
|
||||
const errorMessage = getErrorMessage(err);
|
||||
store.addMessage(
|
||||
addMessage(
|
||||
sessionId,
|
||||
createSystemNotificationMessage(errorMessage, "error"),
|
||||
);
|
||||
store.setError(sessionId, errorMessage);
|
||||
store.setChatState(sessionId, "idle");
|
||||
setError(sessionId, errorMessage);
|
||||
setChatState(sessionId, "idle");
|
||||
return "failed" as CompactConversationResult;
|
||||
}
|
||||
|
||||
store.setSessionLoading(sessionId, true);
|
||||
setSessionLoading(sessionId, true);
|
||||
clearReplayBuffer(sessionId);
|
||||
|
||||
try {
|
||||
@@ -415,37 +414,50 @@ export function useChat(
|
||||
const workingDir = getWorkingDir();
|
||||
await acpLoadSession(sessionId, workingDir);
|
||||
|
||||
store.setSessionLoading(sessionId, false);
|
||||
setSessionLoading(sessionId, false);
|
||||
|
||||
const buffer = getAndDeleteReplayBuffer(sessionId);
|
||||
if (buffer) {
|
||||
store.setMessages(sessionId, [
|
||||
setMessages(sessionId, [
|
||||
...sanitizeReplayMessages(buffer),
|
||||
createCompactionConfirmationMessage(),
|
||||
]);
|
||||
} else {
|
||||
store.addMessage(sessionId, createCompactionConfirmationMessage());
|
||||
addMessage(sessionId, createCompactionConfirmationMessage());
|
||||
}
|
||||
return "completed" as CompactConversationResult;
|
||||
} catch (err) {
|
||||
clearReplayBuffer(sessionId);
|
||||
store.setSessionLoading(sessionId, false);
|
||||
setSessionLoading(sessionId, false);
|
||||
|
||||
const errorMessage = getErrorMessage(err);
|
||||
store.addMessage(
|
||||
addMessage(
|
||||
sessionId,
|
||||
createSystemNotificationMessage(errorMessage, "error"),
|
||||
);
|
||||
store.setError(sessionId, errorMessage);
|
||||
setError(sessionId, errorMessage);
|
||||
return "failed" as CompactConversationResult;
|
||||
} finally {
|
||||
store.setChatState(sessionId, "idle");
|
||||
store.setStreamingMessageId(sessionId, null);
|
||||
store.setPendingAssistantProvider(sessionId, null);
|
||||
store.setSessionLoading(sessionId, false);
|
||||
setChatState(sessionId, "idle");
|
||||
setStreamingMessageId(sessionId, null);
|
||||
setPendingAssistantProvider(sessionId, null);
|
||||
setSessionLoading(sessionId, false);
|
||||
}
|
||||
},
|
||||
[getWorkingDir, options, resolvePersonaInfo, sessionId, store],
|
||||
[
|
||||
getWorkingDir,
|
||||
options,
|
||||
resolvePersonaInfo,
|
||||
sessionId,
|
||||
setActiveSession,
|
||||
setChatState,
|
||||
setStreamingMessageId,
|
||||
setError,
|
||||
addMessage,
|
||||
setSessionLoading,
|
||||
setMessages,
|
||||
setPendingAssistantProvider,
|
||||
],
|
||||
);
|
||||
|
||||
const stopStreaming = stopGeneration;
|
||||
@@ -459,7 +471,6 @@ export function useChat(
|
||||
sendMessage,
|
||||
stopGeneration,
|
||||
stopStreaming,
|
||||
retryLastMessage,
|
||||
clearChat,
|
||||
compactConversation,
|
||||
isStreaming,
|
||||
|
||||
@@ -8,8 +8,10 @@ import { useMessageQueue } from "./useMessageQueue";
|
||||
import { useChatStore } from "../stores/chatStore";
|
||||
import { useChatSessionStore } from "../stores/chatSessionStore";
|
||||
import { useAgentStore } from "@/features/agents/stores/agentStore";
|
||||
import { selectPersonas } from "@/features/agents/stores/agentSelectors";
|
||||
import { useProviderSelection } from "@/features/agents/hooks/useProviderSelection";
|
||||
import { useProjectStore } from "@/features/projects/stores/projectStore";
|
||||
import { selectProjects } from "@/features/projects/stores/projectSelectors";
|
||||
import { resolveAgentProviderCatalogIdStrictFromEntries } from "@/features/providers/providerCatalog";
|
||||
import { useProviderCatalogStore } from "@/features/providers/stores/providerCatalogStore";
|
||||
import {
|
||||
@@ -25,6 +27,7 @@ import {
|
||||
} from "../lib/autoCompact";
|
||||
import { resolveSessionCwd } from "@/features/projects/lib/sessionCwdSelection";
|
||||
import { acpPrepareSession, acpSetModel } from "@/shared/api/acp";
|
||||
import { updateSessionProject } from "../stores/chatSessionOperations";
|
||||
import {
|
||||
useResolvedAgentModelPicker,
|
||||
type PreferredModelSelection,
|
||||
@@ -51,7 +54,7 @@ export function useChatSessionController({
|
||||
selectedProvider: globalSelectedProvider,
|
||||
setSelectedProvider: setGlobalSelectedProvider,
|
||||
} = useProviderSelection();
|
||||
const personas = useAgentStore((s) => s.personas);
|
||||
const personas = useAgentStore(selectPersonas);
|
||||
const session = useChatSessionStore((s) =>
|
||||
sessionId
|
||||
? s.sessions.find((candidate) => candidate.id === sessionId)
|
||||
@@ -63,7 +66,7 @@ export function useChatSessionController({
|
||||
const clearActiveWorkspace = useChatSessionStore(
|
||||
(s) => s.clearActiveWorkspace,
|
||||
);
|
||||
const projects = useProjectStore((s) => s.projects);
|
||||
const projects = useProjectStore(selectProjects);
|
||||
const projectsLoading = useProjectStore((s) => s.loading);
|
||||
const catalogEntries = useProviderCatalogStore((s) => s.entries);
|
||||
const [pendingPersonaId, setPendingPersonaId] = useState<string | null>();
|
||||
@@ -184,7 +187,7 @@ export function useChatSessionController({
|
||||
}
|
||||
|
||||
await acpSetModel(sessionId, modelSelection.id);
|
||||
sessionStore.updateSession(sessionId, {
|
||||
sessionStore.patchSession(sessionId, {
|
||||
modelId: modelSelection.id,
|
||||
modelName: modelSelection.name,
|
||||
});
|
||||
@@ -315,16 +318,18 @@ export function useChatSessionController({
|
||||
.projects.find((candidate) => candidate.id === projectId) ??
|
||||
null);
|
||||
|
||||
useChatSessionStore.getState().updateSession(sessionId, { projectId });
|
||||
if (!selectedProvider) {
|
||||
return;
|
||||
}
|
||||
void prepareCurrentSession(
|
||||
selectedProvider,
|
||||
nextProject,
|
||||
activeWorkspace?.path,
|
||||
effectiveModelSelection,
|
||||
).catch((error) => {
|
||||
void (async () => {
|
||||
await updateSessionProject(sessionId, projectId);
|
||||
if (!selectedProvider) {
|
||||
return;
|
||||
}
|
||||
await prepareCurrentSession(
|
||||
selectedProvider,
|
||||
nextProject,
|
||||
activeWorkspace?.path,
|
||||
effectiveModelSelection,
|
||||
);
|
||||
})().catch((error) => {
|
||||
console.error("Failed to update ACP session working directory:", error);
|
||||
});
|
||||
},
|
||||
@@ -374,7 +379,7 @@ export function useChatSessionController({
|
||||
}
|
||||
useChatSessionStore
|
||||
.getState()
|
||||
.updateSession(sessionId, { personaId: personaId ?? undefined });
|
||||
.patchSession(sessionId, { personaId: personaId ?? undefined });
|
||||
},
|
||||
[
|
||||
handleProviderChange,
|
||||
@@ -395,7 +400,7 @@ export function useChatSessionController({
|
||||
if (sessionId) {
|
||||
useChatSessionStore
|
||||
.getState()
|
||||
.updateSession(sessionId, { personaId: undefined });
|
||||
.patchSession(sessionId, { personaId: undefined });
|
||||
} else {
|
||||
setPendingPersonaId(undefined);
|
||||
}
|
||||
@@ -703,7 +708,6 @@ export function useChatSessionController({
|
||||
const patch: {
|
||||
providerId?: string;
|
||||
personaId?: string | undefined;
|
||||
projectId?: string | null;
|
||||
modelId?: string | undefined;
|
||||
modelName?: string | undefined;
|
||||
} = {};
|
||||
@@ -716,13 +720,14 @@ export function useChatSessionController({
|
||||
if (hasPendingPersona) {
|
||||
patch.personaId = nextPersonaId;
|
||||
}
|
||||
if (hasPendingProject) {
|
||||
patch.projectId = nextProjectId ?? null;
|
||||
if (Object.keys(patch).length > 0) {
|
||||
useChatSessionStore.getState().patchSession(sessionId, patch);
|
||||
}
|
||||
|
||||
useChatSessionStore.getState().updateSession(sessionId, patch);
|
||||
|
||||
try {
|
||||
if (hasPendingProject) {
|
||||
await updateSessionProject(sessionId, nextProjectId ?? null);
|
||||
}
|
||||
await prepareCurrentSession(
|
||||
nextProviderId,
|
||||
nextProject,
|
||||
|
||||
@@ -310,7 +310,7 @@ export function useResolvedAgentModelPicker({
|
||||
setGlobalSelectedProvider(nextProviderId);
|
||||
}
|
||||
|
||||
useChatSessionStore.getState().updateSession(sessionId, {
|
||||
useChatSessionStore.getState().patchSession(sessionId, {
|
||||
modelId,
|
||||
modelName,
|
||||
});
|
||||
@@ -335,7 +335,7 @@ export function useResolvedAgentModelPicker({
|
||||
} else {
|
||||
clearStoredModelPreference(selectedAgentId);
|
||||
}
|
||||
useChatSessionStore.getState().updateSession(sessionId, {
|
||||
useChatSessionStore.getState().patchSession(sessionId, {
|
||||
providerId: previousProviderId,
|
||||
modelId: previousModelId,
|
||||
modelName: previousModelName,
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
formatSkillInstructionPrompt,
|
||||
type SkillCommandMatch,
|
||||
} from "@/features/skills/lib/skillChatPrompt";
|
||||
import type { MessageChip } from "@/shared/types/messages";
|
||||
import type { ChatSendOptions, ChatSkillDraft } from "../types";
|
||||
|
||||
interface SkillSendPayload {
|
||||
@@ -50,19 +49,3 @@ export function buildSkillSendPayload(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function buildSkillRetryOptions(
|
||||
text: string,
|
||||
chips?: MessageChip[],
|
||||
): ChatSendOptions | undefined {
|
||||
const skillChips = chips?.filter((chip) => chip.type === "skill") ?? [];
|
||||
if (skillChips.length === 0) return undefined;
|
||||
|
||||
return {
|
||||
displayText: text,
|
||||
assistantPrompt: formatSkillInstructionPrompt(
|
||||
skillChips.map((chip) => ({ name: chip.label })),
|
||||
),
|
||||
chips: skillChips,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useChatSessionStore, type ChatSession } from "../chatSessionStore";
|
||||
import {
|
||||
updateSessionProject,
|
||||
updateSessionTitle,
|
||||
} from "../chatSessionOperations";
|
||||
|
||||
const mockRenameSession = vi.fn();
|
||||
const mockUpdateSessionProject = vi.fn();
|
||||
|
||||
vi.mock("@/shared/api/acpApi", () => ({
|
||||
renameSession: (...args: unknown[]) => mockRenameSession(...args),
|
||||
updateSessionProject: (...args: unknown[]) =>
|
||||
mockUpdateSessionProject(...args),
|
||||
}));
|
||||
|
||||
function resetStore() {
|
||||
useChatSessionStore.setState({
|
||||
sessions: [],
|
||||
activeSessionId: null,
|
||||
isLoading: false,
|
||||
hasHydratedSessions: false,
|
||||
contextPanelOpenBySession: {},
|
||||
activeWorkspaceBySession: {},
|
||||
});
|
||||
}
|
||||
|
||||
function seedSession(overrides: Partial<ChatSession> = {}) {
|
||||
useChatSessionStore.setState({
|
||||
sessions: [
|
||||
{
|
||||
id: "session-1",
|
||||
title: "Original Title",
|
||||
createdAt: "2026-04-01T00:00:00.000Z",
|
||||
updatedAt: "2026-04-01T00:00:00.000Z",
|
||||
messageCount: 0,
|
||||
...overrides,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
describe("chatSessionOperations", () => {
|
||||
beforeEach(() => {
|
||||
resetStore();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("updateSessionTitle", () => {
|
||||
it("renames in backend before patching local state", async () => {
|
||||
seedSession({ userSetName: false });
|
||||
mockRenameSession.mockResolvedValue(undefined);
|
||||
|
||||
await updateSessionTitle("session-1", "Manual Title");
|
||||
|
||||
expect(mockRenameSession).toHaveBeenCalledWith(
|
||||
"session-1",
|
||||
"Manual Title",
|
||||
);
|
||||
expect(
|
||||
useChatSessionStore.getState().getSession("session-1"),
|
||||
).toMatchObject({
|
||||
title: "Manual Title",
|
||||
userSetName: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not patch local state when backend rename fails", async () => {
|
||||
seedSession({ userSetName: false });
|
||||
mockRenameSession.mockRejectedValue(new Error("rename failed"));
|
||||
|
||||
await expect(
|
||||
updateSessionTitle("session-1", "Manual Title"),
|
||||
).rejects.toThrow("rename failed");
|
||||
|
||||
expect(
|
||||
useChatSessionStore.getState().getSession("session-1"),
|
||||
).toMatchObject({
|
||||
title: "Original Title",
|
||||
userSetName: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateSessionProject", () => {
|
||||
it("updates project in backend before patching local state", async () => {
|
||||
seedSession({ projectId: "project-old" });
|
||||
mockUpdateSessionProject.mockResolvedValue(undefined);
|
||||
|
||||
await updateSessionProject("session-1", "project-new");
|
||||
|
||||
expect(mockUpdateSessionProject).toHaveBeenCalledWith(
|
||||
"session-1",
|
||||
"project-new",
|
||||
);
|
||||
expect(
|
||||
useChatSessionStore.getState().getSession("session-1")?.projectId,
|
||||
).toBe("project-new");
|
||||
});
|
||||
|
||||
it("does not patch local state when backend project update fails", async () => {
|
||||
seedSession({ projectId: "project-old" });
|
||||
mockUpdateSessionProject.mockRejectedValue(new Error("project failed"));
|
||||
|
||||
await expect(
|
||||
updateSessionProject("session-1", "project-new"),
|
||||
).rejects.toThrow("project failed");
|
||||
|
||||
expect(
|
||||
useChatSessionStore.getState().getSession("session-1")?.projectId,
|
||||
).toBe("project-old");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -225,54 +225,32 @@ describe("chatSessionStore", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateSession", () => {
|
||||
it("updates session properties", () => {
|
||||
describe("patchSession", () => {
|
||||
it("patches session properties while preserving updatedAt when omitted", () => {
|
||||
const session = seedSession();
|
||||
const originalUpdatedAt = session.updatedAt;
|
||||
|
||||
useChatSessionStore.getState().updateSession(session.id, {
|
||||
useChatSessionStore.getState().patchSession(session.id, {
|
||||
title: "Updated Title",
|
||||
projectId: "new-project",
|
||||
});
|
||||
|
||||
const updated = useChatSessionStore.getState().getSession(session.id);
|
||||
expect(updated?.title).toBe("Updated Title");
|
||||
expect(updated?.projectId).toBe("new-project");
|
||||
});
|
||||
|
||||
it("preserves updatedAt when not explicitly provided in patch", () => {
|
||||
const session = seedSession();
|
||||
const originalUpdatedAt = session.updatedAt;
|
||||
|
||||
vi.useFakeTimers();
|
||||
vi.advanceTimersByTime(1000);
|
||||
|
||||
useChatSessionStore.getState().updateSession(session.id, {
|
||||
title: "New Title",
|
||||
expect(updated).toMatchObject({
|
||||
title: "Updated Title",
|
||||
projectId: "new-project",
|
||||
updatedAt: originalUpdatedAt,
|
||||
});
|
||||
|
||||
vi.useRealTimers();
|
||||
|
||||
const updated = useChatSessionStore.getState().getSession(session.id);
|
||||
expect(updated?.updatedAt).toBe(originalUpdatedAt);
|
||||
});
|
||||
|
||||
it("updates updatedAt when explicitly provided in patch", () => {
|
||||
const session = seedSession();
|
||||
const originalUpdatedAt = session.updatedAt;
|
||||
|
||||
vi.useFakeTimers();
|
||||
vi.advanceTimersByTime(1000);
|
||||
|
||||
const newTimestamp = new Date().toISOString();
|
||||
useChatSessionStore.getState().updateSession(session.id, {
|
||||
title: "New Title",
|
||||
const newTimestamp = "2026-04-01T00:01:00.000Z";
|
||||
useChatSessionStore.getState().patchSession(session.id, {
|
||||
updatedAt: newTimestamp,
|
||||
});
|
||||
|
||||
vi.useRealTimers();
|
||||
|
||||
const updated = useChatSessionStore.getState().getSession(session.id);
|
||||
expect(updated?.updatedAt).not.toBe(originalUpdatedAt);
|
||||
expect(updated?.updatedAt).toBe(newTimestamp);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { ChatStore } from "./chatStore";
|
||||
|
||||
export const selectMessagesBySession = (state: ChatStore) =>
|
||||
state.messagesBySession;
|
||||
|
||||
export const selectSessionStateById = (state: ChatStore) =>
|
||||
state.sessionStateById;
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
renameSession,
|
||||
updateSessionProject as updateSessionProjectApi,
|
||||
} from "@/shared/api/acpApi";
|
||||
import { useChatSessionStore } from "./chatSessionStore";
|
||||
|
||||
export async function updateSessionTitle(
|
||||
sessionId: string,
|
||||
title: string,
|
||||
): Promise<void> {
|
||||
await renameSession(sessionId, title);
|
||||
|
||||
useChatSessionStore.getState().patchSession(sessionId, {
|
||||
title,
|
||||
userSetName: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateSessionProject(
|
||||
sessionId: string,
|
||||
projectId: string | null,
|
||||
): Promise<void> {
|
||||
await updateSessionProjectApi(sessionId, projectId);
|
||||
|
||||
useChatSessionStore.getState().patchSession(sessionId, {
|
||||
projectId,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { ChatSessionStore } from "./chatSessionStore";
|
||||
|
||||
export const selectSessions = (state: ChatSessionStore) => state.sessions;
|
||||
|
||||
export const selectActiveSessionId = (state: ChatSessionStore) =>
|
||||
state.activeSessionId;
|
||||
|
||||
export const selectHasHydratedSessions = (state: ChatSessionStore) =>
|
||||
state.hasHydratedSessions;
|
||||
|
||||
export const selectSessionsLoading = (state: ChatSessionStore) =>
|
||||
state.isLoading;
|
||||
@@ -12,8 +12,6 @@ import {
|
||||
import {
|
||||
archiveSession as acpArchiveSession,
|
||||
unarchiveSession as acpUnarchiveSession,
|
||||
renameSession as acpRenameSession,
|
||||
updateSessionProject,
|
||||
} from "@/shared/api/acpApi";
|
||||
|
||||
export interface ChatSession {
|
||||
@@ -77,7 +75,7 @@ interface CreateSessionOpts {
|
||||
interface ChatSessionStoreActions {
|
||||
createSession: (opts?: CreateSessionOpts) => Promise<ChatSession>;
|
||||
loadSessions: () => Promise<void>;
|
||||
updateSession: (id: string, patch: Partial<ChatSession>) => void;
|
||||
patchSession: (id: string, patch: Partial<ChatSession>) => void;
|
||||
addSession: (session: ChatSession) => void;
|
||||
archiveSession: (id: string) => Promise<void>;
|
||||
unarchiveSession: (id: string) => Promise<void>;
|
||||
@@ -195,7 +193,7 @@ export const useChatSessionStore = create<ChatSessionStore>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
updateSession: (id, patch) => {
|
||||
patchSession: (id, patch) => {
|
||||
set((state) => ({
|
||||
sessions: state.sessions.map((session) =>
|
||||
session.id === id
|
||||
@@ -207,29 +205,6 @@ export const useChatSessionStore = create<ChatSessionStore>((set, get) => ({
|
||||
: session,
|
||||
),
|
||||
}));
|
||||
|
||||
const updatedSession = get().sessions.find((session) => session.id === id);
|
||||
|
||||
// Persist title rename to backend
|
||||
if (
|
||||
"title" in patch &&
|
||||
"userSetName" in patch &&
|
||||
patch.userSetName &&
|
||||
updatedSession &&
|
||||
patch.title
|
||||
) {
|
||||
acpRenameSession(updatedSession.id, patch.title).catch((err: unknown) =>
|
||||
console.error("Failed to rename session in backend:", err),
|
||||
);
|
||||
}
|
||||
|
||||
// Persist projectId change to backend
|
||||
if ("projectId" in patch && updatedSession) {
|
||||
updateSessionProject(updatedSession.id, patch.projectId ?? null).catch(
|
||||
(err: unknown) =>
|
||||
console.error("Failed to update session project in backend:", err),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
addSession: (session) => {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import type { ProjectStore } from "./projectStore";
|
||||
|
||||
export const selectProjects = (state: ProjectStore) => state.projects;
|
||||
@@ -34,7 +34,7 @@ function persistProjects(projects: ProjectInfo[]): void {
|
||||
}
|
||||
}
|
||||
|
||||
interface ProjectState {
|
||||
export interface ProjectStore {
|
||||
projects: ProjectInfo[];
|
||||
loading: boolean;
|
||||
activeProjectId: string | null;
|
||||
@@ -70,7 +70,7 @@ interface ProjectState {
|
||||
getActiveProject: () => ProjectInfo | null;
|
||||
}
|
||||
|
||||
export const useProjectStore = create<ProjectState>((set, get) => ({
|
||||
export const useProjectStore = create<ProjectStore>((set, get) => ({
|
||||
projects: loadCachedProjects(),
|
||||
loading: false,
|
||||
activeProjectId: null,
|
||||
|
||||
@@ -12,8 +12,11 @@ import {
|
||||
getVisibleSessions,
|
||||
useChatSessionStore,
|
||||
} from "@/features/chat/stores/chatSessionStore";
|
||||
import { selectSessions } from "@/features/chat/stores/chatSessionSelectors";
|
||||
import { useChatStore } from "@/features/chat/stores/chatStore";
|
||||
import { selectMessagesBySession } from "@/features/chat/stores/chatSelectors";
|
||||
import { useProjectStore } from "@/features/projects/stores/projectStore";
|
||||
import { selectProjects } from "@/features/projects/stores/projectSelectors";
|
||||
import {
|
||||
acpDuplicateSession,
|
||||
acpExportSession,
|
||||
@@ -41,8 +44,8 @@ export function SessionHistoryView({
|
||||
onArchiveChat,
|
||||
}: SessionHistoryViewProps) {
|
||||
const { t, i18n } = useTranslation(["sessions", "common"]);
|
||||
const sessions = useChatSessionStore((s) => s.sessions);
|
||||
const messagesBySession = useChatStore((s) => s.messagesBySession);
|
||||
const sessions = useChatSessionStore(selectSessions);
|
||||
const messagesBySession = useChatStore(selectMessagesBySession);
|
||||
const loadSessions = useChatSessionStore((s) => s.loadSessions);
|
||||
const activeSessions = useMemo(
|
||||
() =>
|
||||
@@ -59,7 +62,7 @@ export function SessionHistoryView({
|
||||
[],
|
||||
);
|
||||
|
||||
const projects = useProjectStore((s) => s.projects);
|
||||
const projects = useProjectStore(selectProjects);
|
||||
const getProjectName = useCallback(
|
||||
(projectId: string) => projects.find((p) => p.id === projectId)?.name,
|
||||
[projects],
|
||||
|
||||
@@ -17,13 +17,20 @@ import { cn } from "@/shared/lib/cn";
|
||||
import type { AppView } from "@/app/AppShell";
|
||||
import type { ProjectInfo } from "@/features/projects/api/projects";
|
||||
import { useChatStore } from "@/features/chat/stores/chatStore";
|
||||
import {
|
||||
selectMessagesBySession,
|
||||
selectSessionStateById,
|
||||
} from "@/features/chat/stores/chatSelectors";
|
||||
import { INITIAL_SESSION_CHAT_RUNTIME } from "@/shared/types/chat";
|
||||
import {
|
||||
getVisibleSessions,
|
||||
useChatSessionStore,
|
||||
} from "@/features/chat/stores/chatSessionStore";
|
||||
import { selectSessions } from "@/features/chat/stores/chatSessionSelectors";
|
||||
import { isSessionRunning } from "@/features/chat/lib/sessionActivity";
|
||||
import { useAgentStore } from "@/features/agents/stores/agentStore";
|
||||
import { useProjectStore } from "@/features/projects/stores/projectStore";
|
||||
import { selectProjects } from "@/features/projects/stores/projectSelectors";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { useSessionSearch } from "@/features/sessions/hooks/useSessionSearch";
|
||||
import { SidebarProjectsSection } from "./SidebarProjectsSection";
|
||||
@@ -101,12 +108,12 @@ export function Sidebar({
|
||||
}
|
||||
});
|
||||
|
||||
const chatStore = useChatStore();
|
||||
const { sessions } = useChatSessionStore();
|
||||
const visibleSessions = getVisibleSessions(
|
||||
sessions,
|
||||
chatStore.messagesBySession,
|
||||
);
|
||||
const messagesBySession = useChatStore(selectMessagesBySession);
|
||||
const sessionStateById = useChatStore(selectSessionStateById);
|
||||
const sessions = useChatSessionStore(selectSessions);
|
||||
const getPersonaById = useAgentStore((s) => s.getPersonaById);
|
||||
const projectStoreProjects = useProjectStore(selectProjects);
|
||||
const visibleSessions = getVisibleSessions(sessions, messagesBySession);
|
||||
const activeSessions = visibleSessions.filter(
|
||||
(session) => !session.archivedAt,
|
||||
);
|
||||
@@ -162,7 +169,8 @@ export function Sidebar({
|
||||
const standalone: SessionItem[] = [];
|
||||
for (const session of visibleSessions) {
|
||||
if (session.archivedAt) continue;
|
||||
const runtime = chatStore.getSessionRuntime(session.id);
|
||||
const runtime =
|
||||
sessionStateById[session.id] ?? INITIAL_SESSION_CHAT_RUNTIME;
|
||||
const item: SessionItem = {
|
||||
id: session.id,
|
||||
title: session.title,
|
||||
@@ -194,15 +202,11 @@ export function Sidebar({
|
||||
return { byProject, standalone: limitedStandalone };
|
||||
})();
|
||||
|
||||
const agentStoreState = useAgentStore();
|
||||
const projectStoreState = useProjectStore();
|
||||
|
||||
const sidebarResolvers = {
|
||||
getPersonaName: (personaId: string) =>
|
||||
agentStoreState.getPersonaById(personaId)?.displayName,
|
||||
getPersonaById(personaId)?.displayName,
|
||||
getProjectName: (projectId: string) =>
|
||||
projectStoreState.projects.find((p: { id: string }) => p.id === projectId)
|
||||
?.name,
|
||||
projectStoreProjects.find((p) => p.id === projectId)?.name,
|
||||
};
|
||||
const sidebarSearch = useSessionSearch({
|
||||
sessions: activeSessions,
|
||||
|
||||
@@ -13,33 +13,34 @@ const mockSessions: Array<{
|
||||
}> = [];
|
||||
|
||||
vi.mock("@/features/chat/stores/chatStore", () => ({
|
||||
useChatStore: () => ({
|
||||
messagesBySession: {},
|
||||
getSessionRuntime: () => ({
|
||||
chatState: "idle",
|
||||
hasUnread: false,
|
||||
useChatStore: (selector: (state: unknown) => unknown) =>
|
||||
selector({
|
||||
messagesBySession: {},
|
||||
sessionStateById: {},
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/features/chat/stores/chatSessionStore", () => ({
|
||||
getVisibleSessions: (sessions: typeof mockSessions) =>
|
||||
sessions.filter((session) => session.messageCount > 0),
|
||||
useChatSessionStore: () => ({
|
||||
sessions: mockSessions,
|
||||
}),
|
||||
useChatSessionStore: (selector: (state: unknown) => unknown) =>
|
||||
selector({
|
||||
sessions: mockSessions,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/features/agents/stores/agentStore", () => ({
|
||||
useAgentStore: () => ({
|
||||
getPersonaById: () => undefined,
|
||||
}),
|
||||
useAgentStore: (selector: (state: unknown) => unknown) =>
|
||||
selector({
|
||||
getPersonaById: () => undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/features/projects/stores/projectStore", () => ({
|
||||
useProjectStore: () => ({
|
||||
projects: [],
|
||||
}),
|
||||
useProjectStore: (selector: (state: unknown) => unknown) =>
|
||||
selector({
|
||||
projects: [],
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("Sidebar", () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { Plus, Upload } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useProjectStore } from "@/features/projects/stores/projectStore";
|
||||
import { selectProjects } from "@/features/projects/stores/projectSelectors";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { PageHeader, PageShell } from "@/shared/ui/page-shell";
|
||||
import { revealInFileManager } from "@/shared/lib/fileManager";
|
||||
@@ -32,7 +33,7 @@ interface SkillsViewProps {
|
||||
|
||||
export function SkillsView({ onStartChatWithSkill }: SkillsViewProps) {
|
||||
const { t } = useTranslation(["skills", "common"]);
|
||||
const projects = useProjectStore((state) => state.projects);
|
||||
const projects = useProjectStore(selectProjects);
|
||||
const [search, setSearch] = useState("");
|
||||
const [activeFilter, setActiveFilter] = useState<SkillsFilter>("all");
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
@@ -506,7 +506,7 @@ function handleShared(sessionId: string, update: SessionUpdate): void {
|
||||
currentModelId;
|
||||
|
||||
const sessionStore = useChatSessionStore.getState();
|
||||
sessionStore.updateSession(sessionId, {
|
||||
sessionStore.patchSession(sessionId, {
|
||||
modelId: currentModelId,
|
||||
modelName: currentModelName,
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ export function handleSessionInfoUpdate(
|
||||
}
|
||||
|
||||
const meta = isRecord(info._meta) ? info._meta : {};
|
||||
const patch: Parameters<typeof sessionStore.updateSession>[1] = {};
|
||||
const patch: Parameters<typeof sessionStore.patchSession>[1] = {};
|
||||
|
||||
if (typeof info.title === "string" && info.title && !session.userSetName) {
|
||||
patch.title = info.title;
|
||||
@@ -40,6 +40,6 @@ export function handleSessionInfoUpdate(
|
||||
}
|
||||
|
||||
if (Object.keys(patch).length > 0) {
|
||||
sessionStore.updateSession(sessionId, patch);
|
||||
sessionStore.patchSession(sessionId, patch);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,9 @@
|
||||
"filesTitle": "Files"
|
||||
},
|
||||
"notifications": {
|
||||
"compactionComplete": "Conversation compacted. Older context was summarized."
|
||||
"compactionComplete": "Conversation compacted. Older context was summarized.",
|
||||
"moveError": "Failed to move chat",
|
||||
"renameError": "Failed to rename chat"
|
||||
},
|
||||
"message": {
|
||||
"copied": "Copied",
|
||||
|
||||
@@ -119,7 +119,9 @@
|
||||
"filesTitle": "Archivos"
|
||||
},
|
||||
"notifications": {
|
||||
"compactionComplete": "Conversacion compactada. El contexto anterior se resumio."
|
||||
"compactionComplete": "Conversacion compactada. El contexto anterior se resumio.",
|
||||
"moveError": "No se pudo mover el chat",
|
||||
"renameError": "No se pudo cambiar el nombre del chat"
|
||||
},
|
||||
"message": {
|
||||
"copied": "Copiado",
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
/** Find the last index in an array matching a predicate. */
|
||||
export function findLastIndex<T>(
|
||||
arr: readonly T[],
|
||||
predicate: (item: T) => boolean,
|
||||
): number {
|
||||
for (let i = arr.length - 1; i >= 0; i--) {
|
||||
if (predicate(arr[i])) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
Reference in New Issue
Block a user