feat(v3): implement Mission Control MVP (Phases 1-5)

Phase 1: Data model - groups.rs (Rust structs + load/save groups.json),
groups.ts (TypeScript interfaces), groups-bridge.ts (IPC adapter),
workspace.svelte.ts (replaces layout store), SQLite migrations
(agent_messages, project_agent_state tables, project_id column),
--group CLI argument.

Phase 2: Project shell layout - GlobalTabBar, ProjectGrid, ProjectBox,
ProjectHeader, CommandPalette, DocsTab, ContextTab, SettingsTab,
App.svelte full rewrite (no sidebar/TilingGrid).

Phase 3: ClaudeSession.svelte wrapping AgentPane per-project.
Phase 4: TerminalTabs.svelte with shell/SSH/agent tab types.
Phase 5: TeamAgentsPanel + AgentCard for compact subagent view.

Also fixes AgentPane Svelte 5 event modifier (on:click -> onclick).
This commit is contained in:
Hibryda 2026-03-07 16:06:07 +01:00
parent 293bed6dc5
commit ab79dac4b3
20 changed files with 2296 additions and 65 deletions

View file

@ -0,0 +1,216 @@
import { loadGroups, saveGroups, getCliGroup } from '../adapters/groups-bridge';
import type { GroupsFile, GroupConfig, ProjectConfig } from '../types/groups';
export type WorkspaceTab = 'sessions' | 'docs' | 'context' | 'settings';
export interface TerminalTab {
id: string;
title: string;
type: 'shell' | 'ssh' | 'agent-terminal';
/** SSH session ID if type === 'ssh' */
sshSessionId?: string;
}
// --- Core state ---
let groupsConfig = $state<GroupsFile | null>(null);
let activeGroupId = $state<string>('');
let activeTab = $state<WorkspaceTab>('sessions');
let activeProjectId = $state<string | null>(null);
/** Terminal tabs per project (keyed by project ID) */
let projectTerminals = $state<Map<string, TerminalTab[]>>(new Map());
// --- Getters ---
export function getGroupsConfig(): GroupsFile | null {
return groupsConfig;
}
export function getActiveGroupId(): string {
return activeGroupId;
}
export function getActiveTab(): WorkspaceTab {
return activeTab;
}
export function getActiveProjectId(): string | null {
return activeProjectId;
}
export function getActiveGroup(): GroupConfig | undefined {
return groupsConfig?.groups.find(g => g.id === activeGroupId);
}
export function getEnabledProjects(): ProjectConfig[] {
const group = getActiveGroup();
if (!group) return [];
return group.projects.filter(p => p.enabled);
}
export function getAllGroups(): GroupConfig[] {
return groupsConfig?.groups ?? [];
}
// --- Setters ---
export function setActiveTab(tab: WorkspaceTab): void {
activeTab = tab;
}
export function setActiveProject(projectId: string | null): void {
activeProjectId = projectId;
}
export async function switchGroup(groupId: string): Promise<void> {
if (groupId === activeGroupId) return;
// Clear terminal tabs for the old group
projectTerminals = new Map();
activeGroupId = groupId;
activeProjectId = null;
// Auto-focus first enabled project
const projects = getEnabledProjects();
if (projects.length > 0) {
activeProjectId = projects[0].id;
}
// Persist active group
if (groupsConfig) {
groupsConfig.activeGroupId = groupId;
saveGroups(groupsConfig).catch(e => console.warn('Failed to save groups:', e));
}
}
// --- Terminal tab management per project ---
export function getTerminalTabs(projectId: string): TerminalTab[] {
return projectTerminals.get(projectId) ?? [];
}
export function addTerminalTab(projectId: string, tab: TerminalTab): void {
const tabs = projectTerminals.get(projectId) ?? [];
tabs.push(tab);
projectTerminals.set(projectId, [...tabs]);
}
export function removeTerminalTab(projectId: string, tabId: string): void {
const tabs = projectTerminals.get(projectId) ?? [];
const filtered = tabs.filter(t => t.id !== tabId);
projectTerminals.set(projectId, filtered);
}
// --- Persistence ---
export async function loadWorkspace(initialGroupId?: string): Promise<void> {
try {
const config = await loadGroups();
groupsConfig = config;
projectTerminals = new Map();
// CLI --group flag takes priority, then explicit param, then persisted
let cliGroup: string | null = null;
if (!initialGroupId) {
cliGroup = await getCliGroup();
}
const targetId = initialGroupId || cliGroup || config.activeGroupId;
// Match by ID or by name (CLI users may pass name)
const targetGroup = config.groups.find(
g => g.id === targetId || g.name === targetId,
);
if (targetGroup) {
activeGroupId = targetGroup.id;
} else if (config.groups.length > 0) {
activeGroupId = config.groups[0].id;
}
// Auto-focus first enabled project
const projects = getEnabledProjects();
if (projects.length > 0) {
activeProjectId = projects[0].id;
}
} catch (e) {
console.warn('Failed to load groups config:', e);
groupsConfig = { version: 1, groups: [], activeGroupId: '' };
}
}
export async function saveWorkspace(): Promise<void> {
if (!groupsConfig) return;
await saveGroups(groupsConfig);
}
// --- Group/project mutation ---
export function addGroup(group: GroupConfig): void {
if (!groupsConfig) return;
groupsConfig = {
...groupsConfig,
groups: [...groupsConfig.groups, group],
};
saveGroups(groupsConfig).catch(e => console.warn('Failed to save groups:', e));
}
export function removeGroup(groupId: string): void {
if (!groupsConfig) return;
groupsConfig = {
...groupsConfig,
groups: groupsConfig.groups.filter(g => g.id !== groupId),
};
if (activeGroupId === groupId) {
activeGroupId = groupsConfig.groups[0]?.id ?? '';
activeProjectId = null;
}
saveGroups(groupsConfig).catch(e => console.warn('Failed to save groups:', e));
}
export function updateProject(groupId: string, projectId: string, updates: Partial<ProjectConfig>): void {
if (!groupsConfig) return;
groupsConfig = {
...groupsConfig,
groups: groupsConfig.groups.map(g => {
if (g.id !== groupId) return g;
return {
...g,
projects: g.projects.map(p => {
if (p.id !== projectId) return p;
return { ...p, ...updates };
}),
};
}),
};
saveGroups(groupsConfig).catch(e => console.warn('Failed to save groups:', e));
}
export function addProject(groupId: string, project: ProjectConfig): void {
if (!groupsConfig) return;
const group = groupsConfig.groups.find(g => g.id === groupId);
if (!group || group.projects.length >= 5) return;
groupsConfig = {
...groupsConfig,
groups: groupsConfig.groups.map(g => {
if (g.id !== groupId) return g;
return { ...g, projects: [...g.projects, project] };
}),
};
saveGroups(groupsConfig).catch(e => console.warn('Failed to save groups:', e));
}
export function removeProject(groupId: string, projectId: string): void {
if (!groupsConfig) return;
groupsConfig = {
...groupsConfig,
groups: groupsConfig.groups.map(g => {
if (g.id !== groupId) return g;
return { ...g, projects: g.projects.filter(p => p.id !== projectId) };
}),
};
if (activeProjectId === projectId) {
activeProjectId = null;
}
saveGroups(groupsConfig).catch(e => console.warn('Failed to save groups:', e));
}