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
@@ -1,12 +1,14 @@
import { create } from 'zustand';
import type { OpencodeClient, Session } from '@opencode-ai/sdk/v2';
import { opencodeClient } from '@/lib/opencode/client';
import { listGlobalSessionPages, splitGlobalSessionsByArchived } from '@/stores/globalSessions';
import { filterManagedChatsForRuntime, listGlobalSessionPages, splitGlobalSessionsByArchived } from '@/stores/globalSessions';
import { getReviewTransferDirection, type ReviewTransferDirection } from '@/lib/reviewFlow';
import { getOriginalSessionID, getReviewSessionID } from '@/lib/sessionReviewMetadata';
import { normalizePath } from '@/lib/pathNormalization';
import { raiseSessionOrderingBaselines } from '@/sync/session-ordering';
import { mapWithConcurrency } from '@/lib/concurrency';
import { persistManagedChatSessions, readManagedChatSessions } from '@/sync/persist-cache';
import { isVSCodeRuntime } from '@/lib/desktop';
type GlobalSessionsStatus = 'idle' | 'loading' | 'ready' | 'error';
@@ -363,6 +365,10 @@ const applySnapshot = (
archivedSessions: Session[],
status: GlobalSessionsStatus,
): Partial<GlobalSessionsState> | GlobalSessionsState => {
if (isVSCodeRuntime()) {
activeSessions = filterManagedChatsForRuntime(activeSessions, true);
archivedSessions = filterManagedChatsForRuntime(archivedSessions, true);
}
const nextActiveSessions = sameSessionList(state.activeSessions, activeSessions)
? state.activeSessions
: activeSessions;
@@ -430,6 +436,10 @@ const mutationRevisionPatch = (state: GlobalSessionsState, ids: Iterable<string>
};
const applySessionUpserts = (state: GlobalSessionsState, sessions: Session[]): Partial<GlobalSessionsState> => {
if (isVSCodeRuntime()) {
sessions = filterManagedChatsForRuntime(sessions, true);
if (sessions.length === 0) return state;
}
const revisionPatch = mutationRevisionPatch(state, sessions.map((session) => session.id));
let nextActiveSessions = state.activeSessions;
let nextArchivedSessions = state.archivedSessions;
@@ -483,11 +493,13 @@ const buildReviewTransferMap = (sessions: Session[]): Map<string, ReviewTransfer
return next
}
const initialManagedChatSessions = readManagedChatSessions();
export const useGlobalSessionsStore = create<GlobalSessionsState>((set, get) => ({
activeSessions: [],
activeSessions: initialManagedChatSessions,
archivedSessions: [],
sessionsByDirectory: new Map(),
reviewTransferBySessionId: new Map(),
sessionsByDirectory: buildSessionsByDirectory(initialManagedChatSessions),
reviewTransferBySessionId: buildReviewTransferMap(initialManagedChatSessions),
mutationRevision: 0,
mutationRevisionBySessionId: new Map(),
hasLoaded: false,
@@ -504,11 +516,12 @@ export const useGlobalSessionsStore = create<GlobalSessionsState>((set, get) =>
resetForRuntimeSwitch: () => {
loadGeneration += 1;
inflightLoad = null;
const managedChatSessions = readManagedChatSessions();
set({
activeSessions: [],
activeSessions: managedChatSessions,
archivedSessions: [],
sessionsByDirectory: new Map(),
reviewTransferBySessionId: new Map(),
sessionsByDirectory: buildSessionsByDirectory(managedChatSessions),
reviewTransferBySessionId: buildReviewTransferMap(managedChatSessions),
mutationRevision: 0,
mutationRevisionBySessionId: new Map(),
hasLoaded: false,
@@ -722,6 +735,15 @@ export const useGlobalSessionsStore = create<GlobalSessionsState>((set, get) =>
},
}));
useGlobalSessionsStore.subscribe((state, previous) => {
if (
state.activeSessions !== previous.activeSessions
&& (state.status !== 'idle' || state.activeSessions.length > 0)
) {
persistManagedChatSessions(state.activeSessions);
}
});
export const ensureGlobalSessionsLoaded = async (fallbackActive?: Session[]): Promise<LoadResult> => {
const state = useGlobalSessionsStore.getState();
if (state.hasLoaded && state.status !== 'error') {