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
+11 -9
View File
@@ -370,16 +370,17 @@ describe('openNewSessionDraft project binding', () => {
useDirectoryStore.getState().setDirectory(projectB.path, { showOverlay: false });
});
test('keeps implicit draft on current directory when active project differs', () => {
test('defaults an implicit draft to Chat when active project differs', () => {
useSessionUIStore.getState().openNewSessionDraft();
const draft = useSessionUIStore.getState().newSessionDraft;
expect(draft.open).toBe(true);
expect(draft.selectedProjectId).toBe(projectB.id);
expect(draft.directoryOverride).toBe(projectB.path);
expect(draft.target).toBe('chat');
expect(draft.selectedProjectId).toBeNull();
expect(draft.directoryOverride).toBeNull();
});
test('does not attach active project when current directory is unmatched', () => {
test('defaults an implicit draft to Chat when current directory is unmatched', () => {
useDirectoryStore.getState().setDirectory('/external/worktree', { showOverlay: false });
useSessionUIStore.getState().openNewSessionDraft();
@@ -387,7 +388,8 @@ describe('openNewSessionDraft project binding', () => {
expect(draft.open).toBe(true);
expect(draft.selectedProjectId).toBeNull();
expect(draft.directoryOverride).toBe('/external/worktree');
expect(draft.target).toBe('chat');
expect(draft.directoryOverride).toBeNull();
});
test('respects explicit directoryOverride over active project', () => {
@@ -464,7 +466,7 @@ describe('createSession draft lifecycle', () => {
useDirectoryStore.getState().setDirectory('/private/deleted-worktree', { showOverlay: false });
opencodeClient.getDirectoryAvailability = async () => 'missing';
useSessionUIStore.getState().openNewSessionDraft();
useSessionUIStore.getState().openNewSessionDraft({ directoryOverride: '/private/deleted-worktree' });
await Bun.sleep(0);
expect(useSessionUIStore.getState().newSessionDraft.directoryOverride).toBe('/projects/main');
@@ -482,7 +484,7 @@ describe('createSession draft lifecycle', () => {
activeProjectId: 'project-active',
});
useDirectoryStore.getState().setDirectory('/private/deleted-worktree', { showOverlay: false });
useSessionUIStore.getState().openNewSessionDraft();
useSessionUIStore.getState().openNewSessionDraft({ directoryOverride: '/private/deleted-worktree' });
opencodeClient.getDirectoryAvailability = async () => 'missing';
opencodeClient.createSession = async (_params, directory) => {
createSessionCalls.push(directory);
@@ -542,7 +544,7 @@ describe('createSession draft lifecycle', () => {
activeProjectId: 'project-main',
});
useDirectoryStore.getState().setDirectory('/private/unavailable-worktree', { showOverlay: false });
useSessionUIStore.getState().openNewSessionDraft();
useSessionUIStore.getState().openNewSessionDraft({ directoryOverride: '/private/unavailable-worktree' });
opencodeClient.getDirectoryAvailability = async () => 'unknown';
opencodeClient.createSession = async (_params, directory) => {
createSessionCalls.push(directory);
@@ -571,7 +573,7 @@ describe('createSession draft lifecycle', () => {
return { id: 'session-race', directory };
};
useSessionUIStore.getState().openNewSessionDraft();
useSessionUIStore.getState().openNewSessionDraft({ directoryOverride: '/private/deleted-worktree' });
const createPromise = useSessionUIStore.getState().createSession('Draft title', '/private/deleted-worktree');
expect(availabilityResolvers.length).toBe(2);