feat: Agent Orchestrator — multi-project agent dashboard

Tauri + Svelte 5 + Rust application for orchestrating multiple AI coding agents.
Includes Claude, Aider, Codex, and Ollama provider support, multi-agent
communication (btmsg/bttask), session anchors, plugin sandbox, FTS5 search,
Landlock sandboxing, and 507 vitest + 110 cargo tests.
This commit is contained in:
DexterFromLab 2026-03-15 15:45:27 +01:00
commit 3672e92b7e
272 changed files with 68600 additions and 0 deletions

View file

@ -0,0 +1,193 @@
import {
listSessions,
saveSession,
deleteSession,
updateSessionTitle,
touchSession,
saveLayout,
loadLayout,
updateSessionGroup,
type PersistedSession,
} from '../adapters/session-bridge';
export type LayoutPreset = '1-col' | '2-col' | '3-col' | '2x2' | 'master-stack';
export type PaneType = 'terminal' | 'agent' | 'markdown' | 'ssh' | 'context' | 'empty';
export interface Pane {
id: string;
type: PaneType;
title: string;
shell?: string;
cwd?: string;
args?: string[];
group?: string;
focused: boolean;
remoteMachineId?: string;
}
let panes = $state<Pane[]>([]);
let activePreset = $state<LayoutPreset>('1-col');
let focusedPaneId = $state<string | null>(null);
let initialized = false;
// --- Persistence helpers (fire-and-forget with error logging) ---
function persistSession(pane: Pane): void {
const now = Math.floor(Date.now() / 1000);
const session: PersistedSession = {
id: pane.id,
type: pane.type,
title: pane.title,
shell: pane.shell,
cwd: pane.cwd,
args: pane.args,
group_name: pane.group ?? '',
created_at: now,
last_used_at: now,
};
saveSession(session).catch(e => console.warn('Failed to persist session:', e));
}
function persistLayout(): void {
saveLayout({
preset: activePreset,
pane_ids: panes.map(p => p.id),
}).catch(e => console.warn('Failed to persist layout:', e));
}
// --- Public API ---
export function getPanes(): Pane[] {
return panes;
}
export function getActivePreset(): LayoutPreset {
return activePreset;
}
export function getFocusedPaneId(): string | null {
return focusedPaneId;
}
export function addPane(pane: Omit<Pane, 'focused'>): void {
panes.push({ ...pane, focused: false });
focusPane(pane.id);
autoPreset();
persistSession({ ...pane, focused: false });
persistLayout();
}
export function removePane(id: string): void {
panes = panes.filter(p => p.id !== id);
if (focusedPaneId === id) {
focusedPaneId = panes.length > 0 ? panes[0].id : null;
}
autoPreset();
deleteSession(id).catch(e => console.warn('Failed to delete session:', e));
persistLayout();
}
export function focusPane(id: string): void {
focusedPaneId = id;
panes = panes.map(p => ({ ...p, focused: p.id === id }));
touchSession(id).catch(e => console.warn('Failed to touch session:', e));
}
export function focusPaneByIndex(index: number): void {
if (index >= 0 && index < panes.length) {
focusPane(panes[index].id);
}
}
export function setPreset(preset: LayoutPreset): void {
activePreset = preset;
persistLayout();
}
export function renamePaneTitle(id: string, title: string): void {
const pane = panes.find(p => p.id === id);
if (pane) {
pane.title = title;
updateSessionTitle(id, title).catch(e => console.warn('Failed to update title:', e));
}
}
export function setPaneGroup(id: string, group: string): void {
const pane = panes.find(p => p.id === id);
if (pane) {
pane.group = group || undefined;
updateSessionGroup(id, group).catch(e => console.warn('Failed to update group:', e));
}
}
/** Restore panes and layout from SQLite on app startup */
export async function restoreFromDb(): Promise<void> {
if (initialized) return;
initialized = true;
try {
const [sessions, layout] = await Promise.all([listSessions(), loadLayout()]);
if (layout.preset) {
activePreset = layout.preset as LayoutPreset;
}
// Restore panes in layout order, falling back to DB order
const sessionMap = new Map(sessions.map(s => [s.id, s]));
const orderedIds = layout.pane_ids.length > 0 ? layout.pane_ids : sessions.map(s => s.id);
for (const id of orderedIds) {
const s = sessionMap.get(id);
if (!s) continue;
panes.push({
id: s.id,
type: s.type as PaneType,
title: s.title,
shell: s.shell ?? undefined,
cwd: s.cwd ?? undefined,
args: s.args ?? undefined,
group: s.group_name || undefined,
focused: false,
});
}
if (panes.length > 0) {
focusPane(panes[0].id);
}
} catch (e) {
console.warn('Failed to restore sessions from DB:', e);
}
}
function autoPreset(): void {
const count = panes.length;
if (count <= 1) activePreset = '1-col';
else if (count === 2) activePreset = '2-col';
else if (count === 3) activePreset = 'master-stack';
else activePreset = '2x2';
}
/** CSS grid-template for current preset */
export function getGridTemplate(): { columns: string; rows: string } {
switch (activePreset) {
case '1-col':
return { columns: '1fr', rows: '1fr' };
case '2-col':
return { columns: '1fr 1fr', rows: '1fr' };
case '3-col':
return { columns: '1fr 1fr 1fr', rows: '1fr' };
case '2x2':
return { columns: '1fr 1fr', rows: '1fr 1fr' };
case 'master-stack':
return { columns: '2fr 1fr', rows: '1fr 1fr' };
}
}
/** For master-stack: first pane spans full height */
export function getPaneGridArea(index: number): string | undefined {
if (activePreset === 'master-stack' && index === 0) {
return '1 / 1 / 3 / 2';
}
return undefined;
}