feat(v3): implement session continuity, workspace teardown, StatusBar rewrite, subagent routing fix

P6: persistSessionForProject() saves agent state + messages to SQLite on
session complete. registerSessionProject() maps sessionId -> projectId.
ClaudeSession restoreMessagesFromRecords() restores cached messages on mount.

P7: clearAllAgentSessions() clears sessions on group switch. switchGroup()
calls clearAllAgentSessions() + resets terminal tabs.

P10: StatusBar rewritten for workspace store (group name, project count,
agent count, "BTerminal v3"). Subagent routing fixed: project-scoped
sessions skip layout pane creation (render in TeamAgentsPanel instead).
This commit is contained in:
Hibryda 2026-03-07 16:33:27 +01:00
parent 9766a480ed
commit e0056f811f
5 changed files with 156 additions and 31 deletions

View file

@ -11,15 +11,28 @@ import {
appendAgentMessages,
updateAgentCost,
getAgentSessions,
getAgentSession,
createAgentSession,
findChildByToolUseId,
} from './stores/agents.svelte';
import { addPane, getPanes } from './stores/layout.svelte';
import { notify } from './stores/notifications.svelte';
import {
saveProjectAgentState,
saveAgentMessages,
type AgentMessageRecord,
} from './adapters/groups-bridge';
let unlistenMsg: (() => void) | null = null;
let unlistenExit: (() => void) | null = null;
// Map sessionId -> projectId for persistence routing
const sessionProjectMap = new Map<string, string>();
export function registerSessionProject(sessionId: string, projectId: string): void {
sessionProjectMap.set(sessionId, projectId);
}
// Sidecar liveness — checked by UI components
let sidecarAlive = true;
@ -160,6 +173,8 @@ function handleAgentEvent(sessionId: string, event: Record<string, unknown>): vo
updateAgentStatus(sessionId, 'done');
notify('success', `Agent done — $${cost.totalCostUsd.toFixed(4)}, ${cost.numTurns} turns`);
}
// Persist session state for project-scoped sessions
persistSessionForProject(sessionId);
break;
}
}
@ -220,15 +235,60 @@ function spawnSubagentPane(parentSessionId: string, tc: ToolCallContent): void {
});
updateAgentStatus(childId, 'running');
// Create layout pane, auto-grouped under parent's title
const parentPane = getPanes().find(p => p.id === parentSessionId);
const groupName = parentPane?.title ?? `Agent ${parentSessionId.slice(0, 8)}`;
addPane({
id: childId,
type: 'agent',
title: `Sub: ${label}`,
group: groupName,
});
// For project-scoped sessions, subagents render in TeamAgentsPanel (no layout pane)
// For non-project sessions (detached mode), create a layout pane
if (!sessionProjectMap.has(parentSessionId)) {
const parentPane = getPanes().find(p => p.id === parentSessionId);
const groupName = parentPane?.title ?? `Agent ${parentSessionId.slice(0, 8)}`;
addPane({
id: childId,
type: 'agent',
title: `Sub: ${label}`,
group: groupName,
});
}
}
/** Persist session state + messages to SQLite for the project that owns this session */
async function persistSessionForProject(sessionId: string): Promise<void> {
const projectId = sessionProjectMap.get(sessionId);
if (!projectId) return; // Not a project-scoped session
const session = getAgentSession(sessionId);
if (!session) return;
try {
// Save agent state
await saveProjectAgentState({
project_id: projectId,
last_session_id: sessionId,
sdk_session_id: session.sdkSessionId ?? null,
status: session.status,
cost_usd: session.costUsd,
input_tokens: session.inputTokens,
output_tokens: session.outputTokens,
last_prompt: session.prompt,
updated_at: Date.now(),
});
// Save messages
const records: AgentMessageRecord[] = session.messages.map((m, i) => ({
id: i,
session_id: sessionId,
project_id: projectId,
sdk_session_id: session.sdkSessionId ?? null,
message_type: m.type,
content: JSON.stringify(m.content),
parent_id: m.parentId ?? null,
created_at: Date.now(),
}));
if (records.length > 0) {
await saveAgentMessages(sessionId, projectId, session.sdkSessionId, records);
}
} catch (e) {
console.warn('Failed to persist agent session:', e);
}
}
export function stopAgentDispatcher(): void {