mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-19 22:01:38 +00:00
Refactor agent architecture and add session storage
Major architectural improvements: - Renamed AgentSession → Agent (state/ → agent/) - Removed id field from AgentState - Fixed transport abstraction to pass messages directly instead of using callbacks - Eliminated circular dependencies in transport creation Transport changes: - Changed signature: run(messages, userMessage, config, signal) - Removed getMessages callback from ProviderTransport and AppTransport - Transports now filter attachments internally Session storage: - Added SessionRepository with IndexedDB backend - Auto-save sessions after first exchange - Auto-generate titles from first user message - Session list dialog with search and delete - Persistent storage permission dialog - Browser extension now auto-loads last session UI improvements: - ChatPanel creates single AgentInterface instance in setAgent() - Added drag & drop file upload to MessageEditor - Fixed artifacts panel auto-opening on session load - Added "Drop files here" i18n strings - Changed "Continue Without Saving" → "Continue Anyway" Web example: - Complete rewrite of main.ts with clean architecture - Added check script to package.json - Session management with URL state - Editable session titles Browser extension: - Added full session storage support - History and new session buttons - Auto-load most recent session on open - Session titles in header
This commit is contained in:
parent
c18923a8c5
commit
e5cf25a267
23 changed files with 1787 additions and 289 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import { LocalStorageBackend } from "./backends/local-storage-backend.js";
|
||||
import { ProviderKeysRepository } from "./repositories/provider-keys-repository.js";
|
||||
import { SessionRepository } from "./repositories/session-repository.js";
|
||||
import { SettingsRepository } from "./repositories/settings-repository.js";
|
||||
import type { AppStorageConfig } from "./types.js";
|
||||
|
||||
|
|
@ -10,6 +11,7 @@ import type { AppStorageConfig } from "./types.js";
|
|||
export class AppStorage {
|
||||
readonly settings: SettingsRepository;
|
||||
readonly providerKeys: ProviderKeysRepository;
|
||||
readonly sessions?: SessionRepository;
|
||||
|
||||
constructor(config: AppStorageConfig = {}) {
|
||||
// Use LocalStorage with prefixes as defaults
|
||||
|
|
@ -18,6 +20,11 @@ export class AppStorage {
|
|||
|
||||
this.settings = new SettingsRepository(settingsBackend);
|
||||
this.providerKeys = new ProviderKeysRepository(providerKeysBackend);
|
||||
|
||||
// Session storage is optional
|
||||
if (config.sessions) {
|
||||
this.sessions = new SessionRepository(config.sessions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,200 @@
|
|||
import type { SessionData, SessionMetadata, SessionStorageBackend } from "../types.js";
|
||||
|
||||
/**
|
||||
* IndexedDB implementation of session storage.
|
||||
* Uses two object stores:
|
||||
* - "metadata": Fast access for listing/searching
|
||||
* - "data": Full session data loaded on demand
|
||||
*/
|
||||
export class SessionIndexedDBBackend implements SessionStorageBackend {
|
||||
private dbPromise: Promise<IDBDatabase> | null = null;
|
||||
private readonly DB_NAME: string;
|
||||
private readonly DB_VERSION = 1;
|
||||
|
||||
constructor(dbName = "pi-sessions") {
|
||||
this.DB_NAME = dbName;
|
||||
}
|
||||
|
||||
private async getDB(): Promise<IDBDatabase> {
|
||||
if (this.dbPromise) {
|
||||
return this.dbPromise;
|
||||
}
|
||||
|
||||
this.dbPromise = new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.DB_NAME, this.DB_VERSION);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
|
||||
// Object store for metadata (lightweight, frequently accessed)
|
||||
if (!db.objectStoreNames.contains("metadata")) {
|
||||
const metaStore = db.createObjectStore("metadata", { keyPath: "id" });
|
||||
// Index for sorting by last modified
|
||||
metaStore.createIndex("lastModified", "lastModified", { unique: false });
|
||||
}
|
||||
|
||||
// Object store for full session data (heavy, rarely accessed)
|
||||
if (!db.objectStoreNames.contains("data")) {
|
||||
db.createObjectStore("data", { keyPath: "id" });
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return this.dbPromise;
|
||||
}
|
||||
|
||||
async saveSession(data: SessionData, metadata: SessionMetadata): Promise<void> {
|
||||
const db = await this.getDB();
|
||||
|
||||
// Use transaction to ensure atomicity (both or neither)
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(["metadata", "data"], "readwrite");
|
||||
const metaStore = tx.objectStore("metadata");
|
||||
const dataStore = tx.objectStore("data");
|
||||
|
||||
// Save both in same transaction
|
||||
const metaReq = metaStore.put(metadata);
|
||||
const dataReq = dataStore.put(data);
|
||||
|
||||
// Handle errors
|
||||
metaReq.onerror = () => reject(metaReq.error);
|
||||
dataReq.onerror = () => reject(dataReq.error);
|
||||
|
||||
// Transaction complete = both saved
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
}
|
||||
|
||||
async getSession(id: string): Promise<SessionData | null> {
|
||||
const db = await this.getDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction("data", "readonly");
|
||||
const store = tx.objectStore("data");
|
||||
const request = store.get(id);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result !== undefined ? (request.result as SessionData) : null);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getMetadata(id: string): Promise<SessionMetadata | null> {
|
||||
const db = await this.getDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction("metadata", "readonly");
|
||||
const store = tx.objectStore("metadata");
|
||||
const request = store.get(id);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result !== undefined ? (request.result as SessionMetadata) : null);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getAllMetadata(): Promise<SessionMetadata[]> {
|
||||
const db = await this.getDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction("metadata", "readonly");
|
||||
const store = tx.objectStore("metadata");
|
||||
const request = store.getAll();
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result as SessionMetadata[]);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async deleteSession(id: string): Promise<void> {
|
||||
const db = await this.getDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(["metadata", "data"], "readwrite");
|
||||
const metaStore = tx.objectStore("metadata");
|
||||
const dataStore = tx.objectStore("data");
|
||||
|
||||
// Delete both in transaction
|
||||
const metaReq = metaStore.delete(id);
|
||||
const dataReq = dataStore.delete(id);
|
||||
|
||||
metaReq.onerror = () => reject(metaReq.error);
|
||||
dataReq.onerror = () => reject(dataReq.error);
|
||||
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
}
|
||||
|
||||
async updateTitle(id: string, title: string): Promise<void> {
|
||||
const db = await this.getDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(["metadata", "data"], "readwrite");
|
||||
|
||||
// Update metadata
|
||||
const metaStore = tx.objectStore("metadata");
|
||||
const metaReq = metaStore.get(id);
|
||||
|
||||
metaReq.onsuccess = () => {
|
||||
const metadata = metaReq.result as SessionMetadata;
|
||||
if (!metadata) {
|
||||
reject(new Error(`Session ${id} not found`));
|
||||
return;
|
||||
}
|
||||
metadata.title = title;
|
||||
metadata.lastModified = new Date().toISOString();
|
||||
metaStore.put(metadata);
|
||||
};
|
||||
|
||||
// Update data
|
||||
const dataStore = tx.objectStore("data");
|
||||
const dataReq = dataStore.get(id);
|
||||
|
||||
dataReq.onsuccess = () => {
|
||||
const data = dataReq.result as SessionData;
|
||||
if (!data) {
|
||||
reject(new Error(`Session ${id} not found`));
|
||||
return;
|
||||
}
|
||||
data.title = title;
|
||||
data.lastModified = new Date().toISOString();
|
||||
dataStore.put(data);
|
||||
};
|
||||
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
}
|
||||
|
||||
async getQuotaInfo(): Promise<{ usage: number; quota: number; percent: number }> {
|
||||
if (!navigator.storage || !navigator.storage.estimate) {
|
||||
return { usage: 0, quota: 0, percent: 0 };
|
||||
}
|
||||
|
||||
const estimate = await navigator.storage.estimate();
|
||||
const usage = estimate.usage || 0;
|
||||
const quota = estimate.quota || 0;
|
||||
const percent = quota > 0 ? (usage / quota) * 100 : 0;
|
||||
|
||||
return { usage, quota, percent };
|
||||
}
|
||||
|
||||
async requestPersistence(): Promise<boolean> {
|
||||
if (!navigator.storage || !navigator.storage.persist) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if already persistent
|
||||
const isPersisted = await navigator.storage.persisted();
|
||||
if (isPersisted) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Request persistence
|
||||
return await navigator.storage.persist();
|
||||
}
|
||||
}
|
||||
290
packages/web-ui/src/storage/repositories/session-repository.ts
Normal file
290
packages/web-ui/src/storage/repositories/session-repository.ts
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
import type { AgentState } from "../../agent/agent.js";
|
||||
import type { AppMessage } from "../../components/Messages.js";
|
||||
import type { SessionData, SessionMetadata, SessionStorageBackend } from "../types.js";
|
||||
|
||||
/**
|
||||
* Repository for managing chat sessions.
|
||||
* Handles business logic: title generation, metadata extraction, etc.
|
||||
*/
|
||||
export class SessionRepository {
|
||||
constructor(public backend: SessionStorageBackend) {}
|
||||
|
||||
/**
|
||||
* Generate a title from the first user message.
|
||||
* Takes first sentence or 50 chars, whichever is shorter.
|
||||
*/
|
||||
private generateTitle(messages: AppMessage[]): string {
|
||||
const firstUserMsg = messages.find((m) => m.role === "user");
|
||||
if (!firstUserMsg) return "New Session";
|
||||
|
||||
// Extract text content
|
||||
const content = firstUserMsg.content;
|
||||
let text = "";
|
||||
|
||||
if (typeof content === "string") {
|
||||
text = content;
|
||||
} else {
|
||||
const textBlocks = content.filter((c) => c.type === "text");
|
||||
text = textBlocks.map((c) => (c as any).text || "").join(" ");
|
||||
}
|
||||
|
||||
text = text.trim();
|
||||
if (!text) return "New Session";
|
||||
|
||||
// Find first sentence (up to 50 chars)
|
||||
const sentenceEnd = text.search(/[.!?]/);
|
||||
if (sentenceEnd > 0 && sentenceEnd <= 50) {
|
||||
return text.substring(0, sentenceEnd + 1);
|
||||
}
|
||||
|
||||
// Otherwise take first 50 chars
|
||||
return text.length <= 50 ? text : text.substring(0, 47) + "...";
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract preview text from messages.
|
||||
* Goes through all messages in sequence, extracts text content only
|
||||
* (excludes tool calls, tool results, thinking blocks), until 2KB.
|
||||
*/
|
||||
private extractPreview(messages: AppMessage[]): string {
|
||||
let preview = "";
|
||||
const MAX_SIZE = 2048; // 2KB total
|
||||
|
||||
for (const msg of messages) {
|
||||
// Skip tool result messages entirely
|
||||
if (msg.role === "toolResult") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// UserMessage can have string or array content
|
||||
if (msg.role === "user") {
|
||||
const content = msg.content;
|
||||
|
||||
if (typeof content === "string") {
|
||||
// Simple string content
|
||||
if (preview.length + content.length <= MAX_SIZE) {
|
||||
preview += content + " ";
|
||||
} else {
|
||||
preview += content.substring(0, MAX_SIZE - preview.length);
|
||||
return preview.trim();
|
||||
}
|
||||
} else {
|
||||
// Array of TextContent | ImageContent
|
||||
const textBlocks = content.filter((c) => c.type === "text");
|
||||
for (const block of textBlocks) {
|
||||
const text = (block as any).text || "";
|
||||
if (preview.length + text.length <= MAX_SIZE) {
|
||||
preview += text + " ";
|
||||
} else {
|
||||
preview += text.substring(0, MAX_SIZE - preview.length);
|
||||
return preview.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AssistantMessage has array of TextContent | ThinkingContent | ToolCall
|
||||
if (msg.role === "assistant") {
|
||||
// Filter to only TextContent (skip ThinkingContent and ToolCall)
|
||||
const textBlocks = msg.content.filter((c) => c.type === "text");
|
||||
for (const block of textBlocks) {
|
||||
const text = (block as any).text || "";
|
||||
if (preview.length + text.length <= MAX_SIZE) {
|
||||
preview += text + " ";
|
||||
} else {
|
||||
preview += text.substring(0, MAX_SIZE - preview.length);
|
||||
return preview.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop if we've hit the limit
|
||||
if (preview.length >= MAX_SIZE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return preview.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate total usage across all messages.
|
||||
*/
|
||||
private calculateTotals(messages: AppMessage[]): {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
cost: {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
total: number;
|
||||
};
|
||||
} {
|
||||
let input = 0;
|
||||
let output = 0;
|
||||
let cacheRead = 0;
|
||||
let cacheWrite = 0;
|
||||
const cost = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
total: 0,
|
||||
};
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.role === "assistant" && (msg as any).usage) {
|
||||
const usage = (msg as any).usage;
|
||||
input += usage.input || 0;
|
||||
output += usage.output || 0;
|
||||
cacheRead += usage.cacheRead || 0;
|
||||
cacheWrite += usage.cacheWrite || 0;
|
||||
if (usage.cost) {
|
||||
cost.input += usage.cost.input || 0;
|
||||
cost.output += usage.cost.output || 0;
|
||||
cost.cacheRead += usage.cost.cacheRead || 0;
|
||||
cost.cacheWrite += usage.cost.cacheWrite || 0;
|
||||
cost.total += usage.cost.total || 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { input, output, cacheRead, cacheWrite, cost };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract metadata from session data.
|
||||
*/
|
||||
private extractMetadata(data: SessionData): SessionMetadata {
|
||||
const usage = this.calculateTotals(data.messages);
|
||||
const preview = this.extractPreview(data.messages);
|
||||
|
||||
return {
|
||||
id: data.id,
|
||||
title: data.title,
|
||||
createdAt: data.createdAt,
|
||||
lastModified: data.lastModified,
|
||||
messageCount: data.messages.length,
|
||||
usage,
|
||||
modelId: data.model?.id || null,
|
||||
thinkingLevel: data.thinkingLevel,
|
||||
preview,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Save session state.
|
||||
* Extracts metadata and saves both atomically.
|
||||
*/
|
||||
async saveSession(
|
||||
sessionId: string,
|
||||
state: AgentState,
|
||||
existingCreatedAt?: string,
|
||||
existingTitle?: string,
|
||||
): Promise<void> {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const data: SessionData = {
|
||||
id: sessionId,
|
||||
title: existingTitle || this.generateTitle(state.messages),
|
||||
model: state.model,
|
||||
thinkingLevel: state.thinkingLevel,
|
||||
messages: state.messages,
|
||||
createdAt: existingCreatedAt || now,
|
||||
lastModified: now,
|
||||
};
|
||||
|
||||
const metadata = this.extractMetadata(data);
|
||||
|
||||
await this.backend.saveSession(data, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load full session data by ID.
|
||||
*/
|
||||
async loadSession(id: string): Promise<SessionData | null> {
|
||||
return this.backend.getSession(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all session metadata, sorted by lastModified descending.
|
||||
*/
|
||||
async listSessions(): Promise<SessionMetadata[]> {
|
||||
const allMetadata = await this.backend.getAllMetadata();
|
||||
// Sort by lastModified descending (most recent first)
|
||||
return allMetadata.sort((a, b) => {
|
||||
return new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the most recently modified session.
|
||||
* Returns undefined if no sessions exist.
|
||||
*/
|
||||
async getLatestSessionId(): Promise<string | undefined> {
|
||||
const sessions = await this.listSessions();
|
||||
return sessions.length > 0 ? sessions[0].id : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search sessions by keyword.
|
||||
* Searches in: title and preview (first 2KB of conversation text)
|
||||
* Returns results sorted by relevance (uses simple substring search for now).
|
||||
*/
|
||||
async searchSessions(query: string): Promise<SessionMetadata[]> {
|
||||
if (!query.trim()) {
|
||||
return this.listSessions();
|
||||
}
|
||||
|
||||
const allMetadata = await this.backend.getAllMetadata();
|
||||
|
||||
// Simple substring search for now (can upgrade to Fuse.js later)
|
||||
const lowerQuery = query.toLowerCase();
|
||||
const matches = allMetadata.filter((meta) => {
|
||||
return meta.title.toLowerCase().includes(lowerQuery) || meta.preview.toLowerCase().includes(lowerQuery);
|
||||
});
|
||||
|
||||
// Sort by lastModified descending
|
||||
return matches.sort((a, b) => {
|
||||
return new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session metadata by ID.
|
||||
*/
|
||||
async getMetadata(id: string): Promise<SessionMetadata | null> {
|
||||
return this.backend.getMetadata(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a session.
|
||||
*/
|
||||
async deleteSession(id: string): Promise<void> {
|
||||
await this.backend.deleteSession(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update session title.
|
||||
*/
|
||||
async updateTitle(id: string, title: string): Promise<void> {
|
||||
await this.backend.updateTitle(id, title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage quota information.
|
||||
*/
|
||||
async getQuotaInfo(): Promise<{ usage: number; quota: number; percent: number }> {
|
||||
return this.backend.getQuotaInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Request persistent storage.
|
||||
*/
|
||||
async requestPersistence(): Promise<boolean> {
|
||||
return this.backend.requestPersistence();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,7 @@
|
|||
import type { Model } from "@mariozechner/pi-ai";
|
||||
import type { ThinkingLevel } from "../agent/agent.js";
|
||||
import type { AppMessage } from "../components/Messages.js";
|
||||
|
||||
/**
|
||||
* Base interface for all storage backends.
|
||||
* Provides a simple key-value storage abstraction that can be implemented
|
||||
|
|
@ -35,6 +39,141 @@ export interface StorageBackend {
|
|||
has(key: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight session metadata for listing and searching.
|
||||
* Stored separately from full session data for performance.
|
||||
*/
|
||||
export interface SessionMetadata {
|
||||
/** Unique session identifier (UUID v4) */
|
||||
id: string;
|
||||
|
||||
/** User-defined title or auto-generated from first message */
|
||||
title: string;
|
||||
|
||||
/** ISO 8601 UTC timestamp of creation */
|
||||
createdAt: string;
|
||||
|
||||
/** ISO 8601 UTC timestamp of last modification */
|
||||
lastModified: string;
|
||||
|
||||
/** Total number of messages (user + assistant + tool results) */
|
||||
messageCount: number;
|
||||
|
||||
/** Cumulative usage statistics */
|
||||
usage: {
|
||||
/** Total input tokens */
|
||||
input: number;
|
||||
/** Total output tokens */
|
||||
output: number;
|
||||
/** Total cache read tokens */
|
||||
cacheRead: number;
|
||||
/** Total cache write tokens */
|
||||
cacheWrite: number;
|
||||
/** Total cost breakdown */
|
||||
cost: {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
total: number;
|
||||
};
|
||||
};
|
||||
|
||||
/** Last used model ID (e.g., "claude-sonnet-4") */
|
||||
modelId: string | null;
|
||||
|
||||
/** Last used thinking level */
|
||||
thinkingLevel: ThinkingLevel;
|
||||
|
||||
/**
|
||||
* Preview text for search and display.
|
||||
* First 2KB of conversation text (user + assistant messages in sequence).
|
||||
* Tool calls and tool results are excluded.
|
||||
*/
|
||||
preview: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full session data including all messages.
|
||||
* Only loaded when user opens a specific session.
|
||||
*/
|
||||
export interface SessionData {
|
||||
/** Unique session identifier (UUID v4) */
|
||||
id: string;
|
||||
|
||||
/** User-defined title or auto-generated from first message */
|
||||
title: string;
|
||||
|
||||
/** Last selected model */
|
||||
model: Model<any>;
|
||||
|
||||
/** Last selected thinking level */
|
||||
thinkingLevel: ThinkingLevel;
|
||||
|
||||
/** Full conversation history (with attachments inline) */
|
||||
messages: AppMessage[];
|
||||
|
||||
/** ISO 8601 UTC timestamp of creation */
|
||||
createdAt: string;
|
||||
|
||||
/** ISO 8601 UTC timestamp of last modification */
|
||||
lastModified: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backend interface for session storage.
|
||||
* Implementations: IndexedDB (browser/extension), VSCode global state, etc.
|
||||
*/
|
||||
export interface SessionStorageBackend {
|
||||
/**
|
||||
* Save both session data and metadata atomically.
|
||||
* Should use transactions to ensure consistency.
|
||||
*/
|
||||
saveSession(data: SessionData, metadata: SessionMetadata): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get full session data by ID.
|
||||
* Returns null if session doesn't exist.
|
||||
*/
|
||||
getSession(id: string): Promise<SessionData | null>;
|
||||
|
||||
/**
|
||||
* Get session metadata by ID.
|
||||
* Returns null if session doesn't exist.
|
||||
*/
|
||||
getMetadata(id: string): Promise<SessionMetadata | null>;
|
||||
|
||||
/**
|
||||
* Get all session metadata (for listing/searching).
|
||||
* Should be efficient - metadata is small (~2KB each).
|
||||
*/
|
||||
getAllMetadata(): Promise<SessionMetadata[]>;
|
||||
|
||||
/**
|
||||
* Delete a session (both data and metadata).
|
||||
* Should use transactions to ensure both are deleted.
|
||||
*/
|
||||
deleteSession(id: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Update session title (in both data and metadata).
|
||||
* Optimized operation - no need to save full session.
|
||||
*/
|
||||
updateTitle(id: string, title: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get storage quota information.
|
||||
* Used for warning users when approaching limits.
|
||||
*/
|
||||
getQuotaInfo(): Promise<{ usage: number; quota: number; percent: number }>;
|
||||
|
||||
/**
|
||||
* Request persistent storage (prevents eviction).
|
||||
* Returns true if granted, false otherwise.
|
||||
*/
|
||||
requestPersistence(): Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for configuring AppStorage.
|
||||
*/
|
||||
|
|
@ -43,6 +182,6 @@ export interface AppStorageConfig {
|
|||
settings?: StorageBackend;
|
||||
/** Backend for provider API keys */
|
||||
providerKeys?: StorageBackend;
|
||||
/** Backend for sessions (chat history, attachments) */
|
||||
sessions?: StorageBackend;
|
||||
/** Backend for sessions (optional - can be undefined if persistence not needed) */
|
||||
sessions?: SessionStorageBackend;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue