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:
@@ -1,7 +1,29 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import type { OpencodeClient } from '@opencode-ai/sdk/v2'
|
||||
import type { OpencodeClient, Session } from '@opencode-ai/sdk/v2'
|
||||
|
||||
import { listGlobalSessionPages, splitGlobalSessionsByArchived } from './globalSessions'
|
||||
import { filterManagedChatsForRuntime, listGlobalSessionPages, splitGlobalSessionsByArchived } from './globalSessions'
|
||||
|
||||
describe('managed Chats runtime visibility', () => {
|
||||
const session = (id: string, directory: string): Session => ({
|
||||
id,
|
||||
slug: id,
|
||||
projectID: 'project',
|
||||
directory,
|
||||
title: id,
|
||||
version: '1',
|
||||
time: { created: 1, updated: 1 },
|
||||
})
|
||||
const chat = session('chat', '/home/user/.config/openchamber/chats/2026-08-21/session-a')
|
||||
const project = session('project', '/workspace/project')
|
||||
|
||||
test('VS Code rejects managed Chats before they enter global state', () => {
|
||||
expect(filterManagedChatsForRuntime([chat, project], true)).toEqual([project])
|
||||
})
|
||||
|
||||
test('other runtimes retain managed Chats', () => {
|
||||
expect(filterManagedChatsForRuntime([chat, project], false)).toEqual([chat, project])
|
||||
})
|
||||
})
|
||||
|
||||
describe('listGlobalSessionPages', () => {
|
||||
test('sanitizes session list records before returning them', async () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { runBackgroundNetworkTask } from '@/lib/background-network';
|
||||
import { retry } from "@/sync/retry";
|
||||
import { stripSessionListDetails } from "@/sync/sanitize";
|
||||
import { startSessionLoadPerformanceEvent } from "@/sync/session-load-performance";
|
||||
import { isChatDirectoryPath } from '@/lib/chatDirectories';
|
||||
|
||||
export type GlobalSessionRecord = Session & {
|
||||
project?: {
|
||||
@@ -12,6 +13,12 @@ export type GlobalSessionRecord = Session & {
|
||||
} | null;
|
||||
};
|
||||
|
||||
export const filterManagedChatsForRuntime = (sessions: Session[], vscode: boolean): Session[] => (
|
||||
vscode
|
||||
? sessions.filter((session) => !isChatDirectoryPath(session.directory))
|
||||
: sessions
|
||||
);
|
||||
|
||||
const toNumber = (value: string | null): number | null => {
|
||||
if (!value) {
|
||||
return null;
|
||||
|
||||
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user