feat(chats): add managed projectless chat sessions

Create projectless chat sessions under a managed, date-scoped Chats directory and clean abandoned or deleted session folders.

Add Chats to sidebar state, startup cache, shared context, and Electron Mini Chat while keeping VS Code project-only. Resolve managed chat directories to one server-side memory owner and document the runtime contracts.
This commit is contained in:
Bohdan Triapitsyn
2026-08-21 12:12:40 +03:00
parent 0d70a631f6
commit 9e87d7fdb9
46 changed files with 677 additions and 136 deletions
+15 -6
View File
@@ -22,6 +22,8 @@ import { useSessionUIStore } from '@/sync/session-ui-store';
import { useSessionWorktreeStore } from '@/sync/session-worktree-store';
import { formatSessionWorktreeBadge } from '@/sync/session-worktree-contract';
import { buildSessionMessageRecordsSnapshot, useDirectoryStore, useGlobalSessionStatus, useSessionMessagesResolved } from '@/sync/sync-context';
import { useDirectoryStore as useAppDirectoryStore } from '@/stores/useDirectoryStore';
import { isChatDirectoryForHome } from '@/lib/chatDirectories';
import { useSync } from '@/sync/use-sync';
import { useProjectsStore } from '@/stores/useProjectsStore';
import { useQuotaAutoRefresh, useQuotaStore } from '@/stores/useQuotaStore';
@@ -1052,6 +1054,10 @@ export const Header: React.FC<HeaderProps> = ({
}
return normalize(state.newSessionDraft.bootstrapPendingDirectory ?? state.newSessionDraft.directoryOverride ?? '');
});
const draftTarget = useSessionUIStore((state) => state.newSessionDraft.target);
const draftProjectId = useSessionUIStore((state) => state.newSessionDraft.selectedProjectId);
const selectedSessionDirectory = useSessionUIStore((state) => state.currentSessionDirectory);
const homeDirectory = useAppDirectoryStore((state) => state.homeDirectory);
const openDirectory = React.useMemo(() => {
return worktreeDirectory || sessionDirectory || draftDirectory;
@@ -1080,10 +1086,13 @@ export const Header: React.FC<HeaderProps> = ({
const gitBranchForDirectory = useGitBranchLabel(openDirectory || null);
const currentBranchLabel = gitBranchForDirectory || currentSessionWorktreeBranch || catalogWorktreeBranch;
const isChatContext = isNewSessionDraftOpen
? draftTarget === 'chat'
: isChatDirectoryForHome(sessionDirectory || selectedSessionDirectory, homeDirectory);
// Whether the title carries a second line under it. Hoisted because the
// session menu's vertical alignment depends on the same answer.
const showHeaderMetaRow = !workStatusPanelVisible
const showHeaderMetaRow = !isChatContext && !workStatusPanelVisible
&& Boolean(activeProjectLabel || currentBranchLabel || (!isNewSessionDraftOpen && worktreeBadgeKind));
@@ -1423,14 +1432,14 @@ export const Header: React.FC<HeaderProps> = ({
const handleOpenDraftMiniChat = React.useCallback(() => {
void invokeDesktop('desktop_open_draft_mini_chat_window', {
directory: normalize(openDirectory || activeProject?.path || ''),
projectId: activeProject?.id ?? null,
directory: isChatContext ? '' : draftDirectory,
projectId: isChatContext ? null : draftProjectId,
apiBaseUrl: getRuntimeApiBaseUrl(),
clientToken: getRuntimeBearerTokenSync(),
}).catch((error) => {
console.warn('[header] failed to open draft mini chat window', error);
});
}, [activeProject?.id, activeProject?.path, openDirectory]);
}, [draftDirectory, draftProjectId, isChatContext]);
const handleOpenCurrentMiniChat = React.useCallback(() => {
if (isNewSessionDraftOpen) {
@@ -1443,13 +1452,13 @@ export const Header: React.FC<HeaderProps> = ({
}
void invokeDesktop('desktop_open_session_mini_chat_window', {
sessionId: currentSessionId,
directory: normalize(openDirectory || activeProject?.path || ''),
directory: sessionDirectory || normalize(selectedSessionDirectory || '') || worktreeDirectory,
apiBaseUrl: getRuntimeApiBaseUrl(),
clientToken: getRuntimeBearerTokenSync(),
}).catch((error) => {
console.warn('[header] failed to open session mini chat window', error);
});
}, [activeProject?.path, currentSessionId, handleOpenDraftMiniChat, isNewSessionDraftOpen, openDirectory]);
}, [currentSessionId, handleOpenDraftMiniChat, isNewSessionDraftOpen, selectedSessionDirectory, sessionDirectory, worktreeDirectory]);
const handleOpenContextPanel = React.useCallback(() => {
const directory = normalize(openDirectory || '');
@@ -5,6 +5,9 @@ import { useGitStore } from '@/stores/useGitStore';
import { useProjectsStore } from '@/stores/useProjectsStore';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
import { formatDirectoryName } from '@/lib/utils';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { CHAT_DRAFT_PROJECT_ID, getChatsRootForHome, getChatsRootFromDirectory, isChatDirectoryPath } from '@/lib/chatDirectories';
import { useI18n } from '@/lib/i18n';
export const ProjectContextPanel: React.FC<{
onActionComplete?: () => void;
@@ -13,16 +16,28 @@ export const ProjectContextPanel: React.FC<{
const activeProjectId = useProjectsStore((state) => state.activeProjectId);
const projects = useProjectsStore((state) => state.projects);
const homeDirectory = useDirectoryStore((state) => state.homeDirectory);
const { t } = useI18n();
const gitDirectories = useGitStore((state) => state.directories);
const isChatContext = useSessionUIStore((state) => (
state.newSessionDraft.open
? state.newSessionDraft.target === 'chat'
: isChatDirectoryPath(state.currentSessionDirectory)
));
const chatSessionDirectory = useSessionUIStore((state) => state.currentSessionDirectory);
const chatsRoot = getChatsRootFromDirectory(chatSessionDirectory) ?? getChatsRootForHome(homeDirectory);
const activeProject = React.useMemo(() => {
if (isChatContext) return null;
if (activeProjectId) {
return projects.find((project) => project.id === activeProjectId) ?? projects[0] ?? null;
}
return projects[0] ?? null;
}, [activeProjectId, projects]);
}, [activeProjectId, isChatContext, projects]);
const projectRef = React.useMemo(() => {
if (isChatContext && chatsRoot) {
return { id: CHAT_DRAFT_PROJECT_ID, path: chatsRoot };
}
if (!activeProject) {
return null;
}
@@ -30,16 +45,17 @@ export const ProjectContextPanel: React.FC<{
id: activeProject.id,
path: activeProject.path,
};
}, [activeProject]);
}, [activeProject, chatsRoot, isChatContext]);
const projectLabel = React.useMemo(() => {
if (isChatContext) return t('sessions.sidebar.activity.chatsTitle');
if (!activeProject) {
return null;
}
return activeProject.label?.trim()
|| formatDirectoryName(activeProject.path, homeDirectory)
|| activeProject.path;
}, [activeProject, homeDirectory]);
}, [activeProject, homeDirectory, isChatContext, t]);
const canCreateWorktree = React.useMemo(() => {
if (!activeProject) {