agent-ecosystem/src/renderer/services/contextStorage.ts
matt 524a62438e feat(visualization): enhance team and subagent visualization in README
- Updated the README to reflect new features in Claude Code, including the ability to spawn subagents via the Task tool and coordinate teams with improved visibility.
- Added details on the rendering of subagent sessions as expandable inline cards, including execution traces, metrics, and tool calls.
- Enhanced description of teammate messages, highlighting color-coded cards and team lifecycle visibility.
- Clarified session summary metrics to differentiate between teammate and subagent counts for better user insights.

This commit significantly improves the documentation of team and subagent visualization features, providing users with a clearer understanding of the capabilities and enhancements in the application.
2026-02-12 23:43:28 +09:00

201 lines
5.5 KiB
TypeScript

/**
* Context Storage - IndexedDB persistence layer for context snapshots.
*
* Provides TTL-based storage for workspace state snapshots, enabling
* instant restoration when switching between local and SSH contexts.
*/
import { del, get, keys, set } from 'idb-keyval';
import type { DetectedError, Project, RepositoryGroup, Session } from '@renderer/types/data';
import type { PaneLayout } from '@renderer/types/panes';
import type { Tab } from '@renderer/types/tabs';
// =============================================================================
// Constants
// =============================================================================
const SNAPSHOT_TTL_MS = 5 * 60 * 1000; // 5 minutes
const STORAGE_KEY_PREFIX = 'context-snapshot:';
const SNAPSHOT_VERSION = 1; // Increment when ContextSnapshot structure changes
// =============================================================================
// Types
// =============================================================================
/**
* Context snapshot - persistable state for instant workspace switching.
* Excludes transient state (loading flags, errors, search, non-serializable Maps/Sets).
*/
export interface ContextSnapshot {
// Data state (persistable)
projects: Project[];
selectedProjectId: string | null;
repositoryGroups: RepositoryGroup[];
selectedRepositoryId: string | null;
selectedWorktreeId: string | null;
viewMode: 'flat' | 'grouped';
sessions: Session[];
selectedSessionId: string | null;
sessionsCursor: string | null;
sessionsHasMore: boolean;
sessionsTotalCount: number;
pinnedSessionIds: string[];
notifications: DetectedError[];
unreadCount: number;
// Tab/pane state
openTabs: Tab[];
activeTabId: string | null;
selectedTabIds: string[];
activeProjectId: string | null;
paneLayout: PaneLayout;
// UI state
sidebarCollapsed: boolean;
// Metadata
_metadata: {
contextId: string;
capturedAt: number;
version: number;
};
}
/**
* Stored snapshot - wraps ContextSnapshot with timestamp and version.
*/
interface StoredSnapshot {
snapshot: ContextSnapshot;
timestamp: number;
version: number;
}
// =============================================================================
// Storage Implementation
// =============================================================================
/**
* Save a context snapshot to IndexedDB.
*/
async function saveSnapshot(contextId: string, snapshot: ContextSnapshot): Promise<void> {
try {
const stored: StoredSnapshot = {
snapshot,
timestamp: Date.now(),
version: SNAPSHOT_VERSION,
};
const key = `${STORAGE_KEY_PREFIX}${contextId}`;
await set(key, stored);
} catch (error) {
console.error(`[contextStorage] Failed to save snapshot for ${contextId}:`, error);
}
}
/**
* Load a context snapshot from IndexedDB.
* Returns null if not found, expired, or invalid.
*/
async function loadSnapshot(contextId: string): Promise<ContextSnapshot | null> {
try {
const key = `${STORAGE_KEY_PREFIX}${contextId}`;
const stored = await get<StoredSnapshot>(key);
if (!stored) {
return null;
}
// Check TTL
const age = Date.now() - stored.timestamp;
if (age > SNAPSHOT_TTL_MS) {
// Expired - delete and return null
void deleteSnapshot(contextId);
return null;
}
// Check version compatibility (simple check for now)
if (stored.version !== SNAPSHOT_VERSION) {
console.warn(
`[contextStorage] Snapshot version mismatch for ${contextId}: expected ${SNAPSHOT_VERSION}, got ${stored.version}`
);
void deleteSnapshot(contextId);
return null;
}
return stored.snapshot;
} catch (error) {
console.error(`[contextStorage] Failed to load snapshot for ${contextId}:`, error);
return null;
}
}
/**
* Delete a context snapshot from IndexedDB.
*/
async function deleteSnapshot(contextId: string): Promise<void> {
try {
const key = `${STORAGE_KEY_PREFIX}${contextId}`;
await del(key);
} catch (error) {
console.error(`[contextStorage] Failed to delete snapshot for ${contextId}:`, error);
}
}
/**
* Clean up expired snapshots.
* Iterates all context snapshots and deletes expired ones.
*/
async function cleanupExpired(): Promise<void> {
try {
const allKeys = await keys();
const snapshotKeys = allKeys.filter(
(k): k is IDBValidKey & string => typeof k === 'string' && k.startsWith(STORAGE_KEY_PREFIX)
);
const now = Date.now();
for (const key of snapshotKeys) {
try {
const stored = await get<StoredSnapshot>(key);
if (stored) {
const age = now - stored.timestamp;
if (age > SNAPSHOT_TTL_MS) {
await del(key);
}
}
} catch (error) {
// Skip individual key errors
console.error(`[contextStorage] Failed to check/delete key ${String(key)}:`, error);
}
}
} catch (error) {
console.error('[contextStorage] Failed to cleanup expired snapshots:', error);
}
}
/**
* Check if IndexedDB is available.
* Returns true if storage is accessible, false otherwise.
*/
async function isAvailable(): Promise<boolean> {
try {
const testKey = '__idb_test__';
await set(testKey, true);
await del(testKey);
return true;
} catch {
return false;
}
}
// =============================================================================
// Exports
// =============================================================================
export const contextStorage = {
saveSnapshot,
loadSnapshot,
deleteSnapshot,
cleanupExpired,
isAvailable,
};