Files
openchamber/packages/ui/src/lib/chatDirectories.ts
T
Bohdan Triapitsyn 9e87d7fdb9 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.
2026-08-21 12:12:40 +03:00

89 lines
3.8 KiB
TypeScript

import { opencodeClient } from '@/lib/opencode/client';
import { normalizePath } from '@/lib/pathNormalization';
import { runtimeFetch } from '@/lib/runtime-fetch';
import { getRuntimeKey } from '@/lib/runtime-switch';
export const CHAT_DRAFT_PROJECT_ID = 'openchamber:chats';
const MANAGED_CHATS_PATH_SEGMENT = '/.config/openchamber/chats/';
const chatsRootByRuntime = new Map<string, Promise<string>>();
const joinPath = (base: string, ...parts: string[]): string => {
const separator = base.includes('\\') ? '\\' : '/';
return [base.replace(/[\\/]+$/, ''), ...parts].join(separator);
};
export function isChatDirectoryForHome(directory: string | null | undefined, home: string | null | undefined): boolean {
const normalized = normalizePath(directory ?? null);
if (normalized?.includes(MANAGED_CHATS_PATH_SEGMENT)) return true;
const normalizedHome = normalizePath(home ?? null);
if (!normalized || !normalizedHome) return false;
const root = normalizePath(joinPath(normalizedHome, '.config', 'openchamber', 'chats'));
return Boolean(root && normalized.startsWith(`${root}/`));
}
export function isChatDirectoryPath(directory: string | null | undefined): boolean {
return normalizePath(directory ?? null)?.includes(MANAGED_CHATS_PATH_SEGMENT) === true;
}
export function getChatsRootFromDirectory(directory: string | null | undefined): string | null {
const normalized = normalizePath(directory ?? null);
const index = normalized?.indexOf(MANAGED_CHATS_PATH_SEGMENT) ?? -1;
return normalized && index >= 0
? normalized.slice(0, index + MANAGED_CHATS_PATH_SEGMENT.length - 1)
: null;
}
export function getChatsRootForHome(home: string | null | undefined): string | null {
const normalizedHome = normalizePath(home ?? null);
return normalizedHome ? normalizePath(joinPath(normalizedHome, '.config', 'openchamber', 'chats')) : null;
}
async function getChatsRootDirectory(): Promise<string> {
const runtimeKey = getRuntimeKey();
const existing = chatsRootByRuntime.get(runtimeKey);
if (existing) return existing;
const pending = opencodeClient.getFilesystemHome().then((home) => {
if (!home) throw new Error('Unable to resolve the home directory');
return joinPath(home, '.config', 'openchamber', 'chats');
}).catch((error) => {
chatsRootByRuntime.delete(runtimeKey);
throw error;
});
chatsRootByRuntime.set(runtimeKey, pending);
return pending;
}
export function warmChatsRootDirectory(): void {
void getChatsRootDirectory().catch(() => undefined);
}
export async function createChatDirectory(now = new Date()): Promise<string> {
const root = await getChatsRootDirectory();
const date = [now.getFullYear(), String(now.getMonth() + 1).padStart(2, '0'), String(now.getDate()).padStart(2, '0')].join('-');
const dateDirectory = joinPath(root, date);
const id = globalThis.crypto?.randomUUID?.() ?? `${now.getTime()}-${Math.random().toString(36).slice(2)}`;
const directory = joinPath(dateDirectory, `session-${id}`);
await opencodeClient.createDirectory(directory);
return directory;
}
async function isChatDirectory(directory: string | null | undefined): Promise<boolean> {
const normalized = normalizePath(directory ?? null);
if (!normalized) return false;
const root = normalizePath(await getChatsRootDirectory());
return Boolean(root && (normalized === root || normalized.startsWith(`${root}/`)));
}
export async function deleteChatDirectory(directory: string): Promise<void> {
if (!await isChatDirectory(directory)) return;
const response = await runtimeFetch('/api/fs/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: directory }),
});
if (!response.ok && response.status !== 404) {
throw new Error(`Failed to delete chat directory (${response.status})`);
}
}