@oh-my-pi/pi-web-ui 1.337.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/README.md +609 -0
- package/example/README.md +61 -0
- package/example/index.html +13 -0
- package/example/package.json +24 -0
- package/example/src/app.css +1 -0
- package/example/src/custom-messages.ts +99 -0
- package/example/src/main.ts +420 -0
- package/example/tsconfig.json +23 -0
- package/example/vite.config.ts +6 -0
- package/package.json +57 -0
- package/scripts/count-prompt-tokens.ts +88 -0
- package/src/ChatPanel.ts +218 -0
- package/src/app.css +68 -0
- package/src/components/AgentInterface.ts +390 -0
- package/src/components/AttachmentTile.ts +107 -0
- package/src/components/ConsoleBlock.ts +74 -0
- package/src/components/CustomProviderCard.ts +96 -0
- package/src/components/ExpandableSection.ts +46 -0
- package/src/components/Input.ts +113 -0
- package/src/components/MessageEditor.ts +404 -0
- package/src/components/MessageList.ts +97 -0
- package/src/components/Messages.ts +384 -0
- package/src/components/ProviderKeyInput.ts +152 -0
- package/src/components/SandboxedIframe.ts +626 -0
- package/src/components/StreamingMessageContainer.ts +107 -0
- package/src/components/ThinkingBlock.ts +45 -0
- package/src/components/message-renderer-registry.ts +28 -0
- package/src/components/sandbox/ArtifactsRuntimeProvider.ts +219 -0
- package/src/components/sandbox/AttachmentsRuntimeProvider.ts +66 -0
- package/src/components/sandbox/ConsoleRuntimeProvider.ts +186 -0
- package/src/components/sandbox/FileDownloadRuntimeProvider.ts +110 -0
- package/src/components/sandbox/RuntimeMessageBridge.ts +82 -0
- package/src/components/sandbox/RuntimeMessageRouter.ts +216 -0
- package/src/components/sandbox/SandboxRuntimeProvider.ts +52 -0
- package/src/dialogs/ApiKeyPromptDialog.ts +75 -0
- package/src/dialogs/AttachmentOverlay.ts +640 -0
- package/src/dialogs/CustomProviderDialog.ts +274 -0
- package/src/dialogs/ModelSelector.ts +314 -0
- package/src/dialogs/PersistentStorageDialog.ts +146 -0
- package/src/dialogs/ProvidersModelsTab.ts +212 -0
- package/src/dialogs/SessionListDialog.ts +157 -0
- package/src/dialogs/SettingsDialog.ts +216 -0
- package/src/index.ts +115 -0
- package/src/prompts/prompts.ts +282 -0
- package/src/storage/app-storage.ts +60 -0
- package/src/storage/backends/indexeddb-storage-backend.ts +193 -0
- package/src/storage/store.ts +33 -0
- package/src/storage/stores/custom-providers-store.ts +62 -0
- package/src/storage/stores/provider-keys-store.ts +33 -0
- package/src/storage/stores/sessions-store.ts +136 -0
- package/src/storage/stores/settings-store.ts +34 -0
- package/src/storage/types.ts +206 -0
- package/src/tools/artifacts/ArtifactElement.ts +14 -0
- package/src/tools/artifacts/ArtifactPill.ts +26 -0
- package/src/tools/artifacts/Console.ts +102 -0
- package/src/tools/artifacts/DocxArtifact.ts +213 -0
- package/src/tools/artifacts/ExcelArtifact.ts +231 -0
- package/src/tools/artifacts/GenericArtifact.ts +118 -0
- package/src/tools/artifacts/HtmlArtifact.ts +203 -0
- package/src/tools/artifacts/ImageArtifact.ts +116 -0
- package/src/tools/artifacts/MarkdownArtifact.ts +83 -0
- package/src/tools/artifacts/PdfArtifact.ts +201 -0
- package/src/tools/artifacts/SvgArtifact.ts +82 -0
- package/src/tools/artifacts/TextArtifact.ts +148 -0
- package/src/tools/artifacts/artifacts-tool-renderer.ts +371 -0
- package/src/tools/artifacts/artifacts.ts +713 -0
- package/src/tools/artifacts/index.ts +7 -0
- package/src/tools/extract-document.ts +271 -0
- package/src/tools/index.ts +46 -0
- package/src/tools/javascript-repl.ts +316 -0
- package/src/tools/renderer-registry.ts +127 -0
- package/src/tools/renderers/BashRenderer.ts +52 -0
- package/src/tools/renderers/CalculateRenderer.ts +58 -0
- package/src/tools/renderers/DefaultRenderer.ts +95 -0
- package/src/tools/renderers/GetCurrentTimeRenderer.ts +92 -0
- package/src/tools/types.ts +15 -0
- package/src/utils/attachment-utils.ts +472 -0
- package/src/utils/auth-token.ts +22 -0
- package/src/utils/format.ts +42 -0
- package/src/utils/i18n.ts +653 -0
- package/src/utils/model-discovery.ts +277 -0
- package/src/utils/proxy-utils.ts +134 -0
- package/src/utils/test-sessions.ts +2357 -0
- package/tsconfig.build.json +20 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type { IndexedDBConfig, StorageBackend, StorageTransaction } from "../types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* IndexedDB implementation of StorageBackend.
|
|
5
|
+
* Provides multi-store key-value storage with transactions and quota management.
|
|
6
|
+
*/
|
|
7
|
+
export class IndexedDBStorageBackend implements StorageBackend {
|
|
8
|
+
private dbPromise: Promise<IDBDatabase> | null = null;
|
|
9
|
+
|
|
10
|
+
constructor(private config: IndexedDBConfig) {}
|
|
11
|
+
|
|
12
|
+
private async getDB(): Promise<IDBDatabase> {
|
|
13
|
+
if (!this.dbPromise) {
|
|
14
|
+
this.dbPromise = new Promise((resolve, reject) => {
|
|
15
|
+
const request = indexedDB.open(this.config.dbName, this.config.version);
|
|
16
|
+
|
|
17
|
+
request.onerror = () => reject(request.error);
|
|
18
|
+
request.onsuccess = () => resolve(request.result);
|
|
19
|
+
|
|
20
|
+
request.onupgradeneeded = (_event) => {
|
|
21
|
+
const db = request.result;
|
|
22
|
+
|
|
23
|
+
// Create object stores from config
|
|
24
|
+
for (const storeConfig of this.config.stores) {
|
|
25
|
+
if (!db.objectStoreNames.contains(storeConfig.name)) {
|
|
26
|
+
const store = db.createObjectStore(storeConfig.name, {
|
|
27
|
+
keyPath: storeConfig.keyPath,
|
|
28
|
+
autoIncrement: storeConfig.autoIncrement,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Create indices
|
|
32
|
+
if (storeConfig.indices) {
|
|
33
|
+
for (const indexConfig of storeConfig.indices) {
|
|
34
|
+
store.createIndex(indexConfig.name, indexConfig.keyPath, {
|
|
35
|
+
unique: indexConfig.unique,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return this.dbPromise;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private promisifyRequest<T>(request: IDBRequest<T>): Promise<T> {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
request.onsuccess = () => resolve(request.result);
|
|
51
|
+
request.onerror = () => reject(request.error);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async get<T = unknown>(storeName: string, key: string): Promise<T | null> {
|
|
56
|
+
const db = await this.getDB();
|
|
57
|
+
const tx = db.transaction(storeName, "readonly");
|
|
58
|
+
const store = tx.objectStore(storeName);
|
|
59
|
+
const result = await this.promisifyRequest(store.get(key));
|
|
60
|
+
return result ?? null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async set<T = unknown>(storeName: string, key: string, value: T): Promise<void> {
|
|
64
|
+
const db = await this.getDB();
|
|
65
|
+
const tx = db.transaction(storeName, "readwrite");
|
|
66
|
+
const store = tx.objectStore(storeName);
|
|
67
|
+
// If store has keyPath, only pass value (in-line key)
|
|
68
|
+
// Otherwise pass both value and key (out-of-line key)
|
|
69
|
+
if (store.keyPath) {
|
|
70
|
+
await this.promisifyRequest(store.put(value));
|
|
71
|
+
} else {
|
|
72
|
+
await this.promisifyRequest(store.put(value, key));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async delete(storeName: string, key: string): Promise<void> {
|
|
77
|
+
const db = await this.getDB();
|
|
78
|
+
const tx = db.transaction(storeName, "readwrite");
|
|
79
|
+
const store = tx.objectStore(storeName);
|
|
80
|
+
await this.promisifyRequest(store.delete(key));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async keys(storeName: string, prefix?: string): Promise<string[]> {
|
|
84
|
+
const db = await this.getDB();
|
|
85
|
+
const tx = db.transaction(storeName, "readonly");
|
|
86
|
+
const store = tx.objectStore(storeName);
|
|
87
|
+
|
|
88
|
+
if (prefix) {
|
|
89
|
+
// Use IDBKeyRange for efficient prefix filtering
|
|
90
|
+
const range = IDBKeyRange.bound(prefix, `${prefix}\uffff`, false, false);
|
|
91
|
+
const keys = await this.promisifyRequest(store.getAllKeys(range));
|
|
92
|
+
return keys.map((k) => String(k));
|
|
93
|
+
} else {
|
|
94
|
+
const keys = await this.promisifyRequest(store.getAllKeys());
|
|
95
|
+
return keys.map((k) => String(k));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async getAllFromIndex<T = unknown>(
|
|
100
|
+
storeName: string,
|
|
101
|
+
indexName: string,
|
|
102
|
+
direction: "asc" | "desc" = "asc",
|
|
103
|
+
): Promise<T[]> {
|
|
104
|
+
const db = await this.getDB();
|
|
105
|
+
const tx = db.transaction(storeName, "readonly");
|
|
106
|
+
const store = tx.objectStore(storeName);
|
|
107
|
+
const index = store.index(indexName);
|
|
108
|
+
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
const results: T[] = [];
|
|
111
|
+
const request = index.openCursor(null, direction === "desc" ? "prev" : "next");
|
|
112
|
+
|
|
113
|
+
request.onsuccess = () => {
|
|
114
|
+
const cursor = request.result;
|
|
115
|
+
if (cursor) {
|
|
116
|
+
results.push(cursor.value as T);
|
|
117
|
+
cursor.continue();
|
|
118
|
+
} else {
|
|
119
|
+
resolve(results);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
request.onerror = () => reject(request.error);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async clear(storeName: string): Promise<void> {
|
|
128
|
+
const db = await this.getDB();
|
|
129
|
+
const tx = db.transaction(storeName, "readwrite");
|
|
130
|
+
const store = tx.objectStore(storeName);
|
|
131
|
+
await this.promisifyRequest(store.clear());
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async has(storeName: string, key: string): Promise<boolean> {
|
|
135
|
+
const db = await this.getDB();
|
|
136
|
+
const tx = db.transaction(storeName, "readonly");
|
|
137
|
+
const store = tx.objectStore(storeName);
|
|
138
|
+
const result = await this.promisifyRequest(store.getKey(key));
|
|
139
|
+
return result !== undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async transaction<T>(
|
|
143
|
+
storeNames: string[],
|
|
144
|
+
mode: "readonly" | "readwrite",
|
|
145
|
+
operation: (tx: StorageTransaction) => Promise<T>,
|
|
146
|
+
): Promise<T> {
|
|
147
|
+
const db = await this.getDB();
|
|
148
|
+
const idbTx = db.transaction(storeNames, mode);
|
|
149
|
+
|
|
150
|
+
const storageTx: StorageTransaction = {
|
|
151
|
+
get: async <T>(storeName: string, key: string) => {
|
|
152
|
+
const store = idbTx.objectStore(storeName);
|
|
153
|
+
const result = await this.promisifyRequest(store.get(key));
|
|
154
|
+
return (result ?? null) as T | null;
|
|
155
|
+
},
|
|
156
|
+
set: async <T>(storeName: string, key: string, value: T) => {
|
|
157
|
+
const store = idbTx.objectStore(storeName);
|
|
158
|
+
// If store has keyPath, only pass value (in-line key)
|
|
159
|
+
// Otherwise pass both value and key (out-of-line key)
|
|
160
|
+
if (store.keyPath) {
|
|
161
|
+
await this.promisifyRequest(store.put(value));
|
|
162
|
+
} else {
|
|
163
|
+
await this.promisifyRequest(store.put(value, key));
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
delete: async (storeName: string, key: string) => {
|
|
167
|
+
const store = idbTx.objectStore(storeName);
|
|
168
|
+
await this.promisifyRequest(store.delete(key));
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
return operation(storageTx);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async getQuotaInfo(): Promise<{ usage: number; quota: number; percent: number }> {
|
|
176
|
+
if (navigator.storage?.estimate) {
|
|
177
|
+
const estimate = await navigator.storage.estimate();
|
|
178
|
+
return {
|
|
179
|
+
usage: estimate.usage || 0,
|
|
180
|
+
quota: estimate.quota || 0,
|
|
181
|
+
percent: estimate.quota ? ((estimate.usage || 0) / estimate.quota) * 100 : 0,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return { usage: 0, quota: 0, percent: 0 };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async requestPersistence(): Promise<boolean> {
|
|
188
|
+
if (navigator.storage?.persist) {
|
|
189
|
+
return await navigator.storage.persist();
|
|
190
|
+
}
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { StorageBackend, StoreConfig } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base class for all storage stores.
|
|
5
|
+
* Each store defines its IndexedDB schema and provides domain-specific methods.
|
|
6
|
+
*/
|
|
7
|
+
export abstract class Store {
|
|
8
|
+
private backend: StorageBackend | null = null;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns the IndexedDB configuration for this store.
|
|
12
|
+
* Defines store name, key path, and indices.
|
|
13
|
+
*/
|
|
14
|
+
abstract getConfig(): StoreConfig;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Sets the storage backend. Called by AppStorage after backend creation.
|
|
18
|
+
*/
|
|
19
|
+
setBackend(backend: StorageBackend): void {
|
|
20
|
+
this.backend = backend;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Gets the storage backend. Throws if backend not set.
|
|
25
|
+
* Concrete stores must use this to access the backend.
|
|
26
|
+
*/
|
|
27
|
+
protected getBackend(): StorageBackend {
|
|
28
|
+
if (!this.backend) {
|
|
29
|
+
throw new Error(`Backend not set on ${this.constructor.name}`);
|
|
30
|
+
}
|
|
31
|
+
return this.backend;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Model } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { Store } from "../store.js";
|
|
3
|
+
import type { StoreConfig } from "../types.js";
|
|
4
|
+
|
|
5
|
+
export type AutoDiscoveryProviderType = "ollama" | "llama.cpp" | "vllm" | "lmstudio";
|
|
6
|
+
|
|
7
|
+
export type CustomProviderType =
|
|
8
|
+
| AutoDiscoveryProviderType // Auto-discovery - models fetched on-demand
|
|
9
|
+
| "openai-completions" // Manual models - stored in provider.models
|
|
10
|
+
| "openai-responses" // Manual models - stored in provider.models
|
|
11
|
+
| "anthropic-messages"; // Manual models - stored in provider.models
|
|
12
|
+
|
|
13
|
+
export interface CustomProvider {
|
|
14
|
+
id: string; // UUID
|
|
15
|
+
name: string; // Display name, also used as Model.provider
|
|
16
|
+
type: CustomProviderType;
|
|
17
|
+
baseUrl: string;
|
|
18
|
+
apiKey?: string; // Optional, applies to all models
|
|
19
|
+
|
|
20
|
+
// For manual types ONLY - models stored directly on provider
|
|
21
|
+
// Auto-discovery types: models fetched on-demand, never stored
|
|
22
|
+
models?: Model<any>[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Store for custom LLM providers (auto-discovery servers + manual providers).
|
|
27
|
+
*/
|
|
28
|
+
export class CustomProvidersStore extends Store {
|
|
29
|
+
getConfig(): StoreConfig {
|
|
30
|
+
return {
|
|
31
|
+
name: "custom-providers",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async get(id: string): Promise<CustomProvider | null> {
|
|
36
|
+
return this.getBackend().get("custom-providers", id);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async set(provider: CustomProvider): Promise<void> {
|
|
40
|
+
await this.getBackend().set("custom-providers", provider.id, provider);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async delete(id: string): Promise<void> {
|
|
44
|
+
await this.getBackend().delete("custom-providers", id);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async getAll(): Promise<CustomProvider[]> {
|
|
48
|
+
const keys = await this.getBackend().keys("custom-providers");
|
|
49
|
+
const providers: CustomProvider[] = [];
|
|
50
|
+
for (const key of keys) {
|
|
51
|
+
const provider = await this.get(key);
|
|
52
|
+
if (provider) {
|
|
53
|
+
providers.push(provider);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return providers;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async has(id: string): Promise<boolean> {
|
|
60
|
+
return this.getBackend().has("custom-providers", id);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Store } from "../store.js";
|
|
2
|
+
import type { StoreConfig } from "../types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Store for LLM provider API keys (Anthropic, OpenAI, etc.).
|
|
6
|
+
*/
|
|
7
|
+
export class ProviderKeysStore extends Store {
|
|
8
|
+
getConfig(): StoreConfig {
|
|
9
|
+
return {
|
|
10
|
+
name: "provider-keys",
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async get(provider: string): Promise<string | null> {
|
|
15
|
+
return this.getBackend().get("provider-keys", provider);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async set(provider: string, key: string): Promise<void> {
|
|
19
|
+
await this.getBackend().set("provider-keys", provider, key);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async delete(provider: string): Promise<void> {
|
|
23
|
+
await this.getBackend().delete("provider-keys", provider);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async list(): Promise<string[]> {
|
|
27
|
+
return this.getBackend().keys("provider-keys");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async has(provider: string): Promise<boolean> {
|
|
31
|
+
return this.getBackend().has("provider-keys", provider);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { AgentState } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import { Store } from "../store.js";
|
|
3
|
+
import type { SessionData, SessionMetadata, StoreConfig } from "../types.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Store for chat sessions (data and metadata).
|
|
7
|
+
* Uses two object stores: sessions (full data) and sessions-metadata (lightweight).
|
|
8
|
+
*/
|
|
9
|
+
export class SessionsStore extends Store {
|
|
10
|
+
getConfig(): StoreConfig {
|
|
11
|
+
return {
|
|
12
|
+
name: "sessions",
|
|
13
|
+
keyPath: "id",
|
|
14
|
+
indices: [{ name: "lastModified", keyPath: "lastModified" }],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Additional config for sessions-metadata store.
|
|
20
|
+
* Must be included when creating the backend.
|
|
21
|
+
*/
|
|
22
|
+
static getMetadataConfig(): StoreConfig {
|
|
23
|
+
return {
|
|
24
|
+
name: "sessions-metadata",
|
|
25
|
+
keyPath: "id",
|
|
26
|
+
indices: [{ name: "lastModified", keyPath: "lastModified" }],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async save(data: SessionData, metadata: SessionMetadata): Promise<void> {
|
|
31
|
+
await this.getBackend().transaction(["sessions", "sessions-metadata"], "readwrite", async (tx) => {
|
|
32
|
+
await tx.set("sessions", data.id, data);
|
|
33
|
+
await tx.set("sessions-metadata", metadata.id, metadata);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async get(id: string): Promise<SessionData | null> {
|
|
38
|
+
return this.getBackend().get("sessions", id);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async getMetadata(id: string): Promise<SessionMetadata | null> {
|
|
42
|
+
return this.getBackend().get("sessions-metadata", id);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async getAllMetadata(): Promise<SessionMetadata[]> {
|
|
46
|
+
// Use the lastModified index to get sessions sorted by most recent first
|
|
47
|
+
return this.getBackend().getAllFromIndex<SessionMetadata>("sessions-metadata", "lastModified", "desc");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async delete(id: string): Promise<void> {
|
|
51
|
+
await this.getBackend().transaction(["sessions", "sessions-metadata"], "readwrite", async (tx) => {
|
|
52
|
+
await tx.delete("sessions", id);
|
|
53
|
+
await tx.delete("sessions-metadata", id);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Alias for backward compatibility
|
|
58
|
+
async deleteSession(id: string): Promise<void> {
|
|
59
|
+
return this.delete(id);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async updateTitle(id: string, title: string): Promise<void> {
|
|
63
|
+
const metadata = await this.getMetadata(id);
|
|
64
|
+
if (metadata) {
|
|
65
|
+
metadata.title = title;
|
|
66
|
+
await this.getBackend().set("sessions-metadata", id, metadata);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Also update in full session data
|
|
70
|
+
const data = await this.get(id);
|
|
71
|
+
if (data) {
|
|
72
|
+
data.title = title;
|
|
73
|
+
await this.getBackend().set("sessions", id, data);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async getQuotaInfo(): Promise<{ usage: number; quota: number; percent: number }> {
|
|
78
|
+
return this.getBackend().getQuotaInfo();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async requestPersistence(): Promise<boolean> {
|
|
82
|
+
return this.getBackend().requestPersistence();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Alias methods for backward compatibility
|
|
86
|
+
async saveSession(
|
|
87
|
+
id: string,
|
|
88
|
+
state: AgentState,
|
|
89
|
+
metadata: SessionMetadata | undefined,
|
|
90
|
+
title?: string,
|
|
91
|
+
): Promise<void> {
|
|
92
|
+
// If metadata is provided, use it; otherwise create it from state
|
|
93
|
+
const meta: SessionMetadata = metadata || {
|
|
94
|
+
id,
|
|
95
|
+
title: title || "",
|
|
96
|
+
createdAt: new Date().toISOString(),
|
|
97
|
+
lastModified: new Date().toISOString(),
|
|
98
|
+
messageCount: state.messages?.length || 0,
|
|
99
|
+
usage: {
|
|
100
|
+
input: 0,
|
|
101
|
+
output: 0,
|
|
102
|
+
cacheRead: 0,
|
|
103
|
+
cacheWrite: 0,
|
|
104
|
+
totalTokens: 0,
|
|
105
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
106
|
+
},
|
|
107
|
+
thinkingLevel: state.thinkingLevel || "off",
|
|
108
|
+
preview: "",
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const data: SessionData = {
|
|
112
|
+
id,
|
|
113
|
+
title: title || meta.title,
|
|
114
|
+
model: state.model,
|
|
115
|
+
thinkingLevel: state.thinkingLevel,
|
|
116
|
+
messages: state.messages || [],
|
|
117
|
+
createdAt: meta.createdAt,
|
|
118
|
+
lastModified: new Date().toISOString(),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
await this.save(data, meta);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async loadSession(id: string): Promise<SessionData | null> {
|
|
125
|
+
return this.get(id);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async getLatestSessionId(): Promise<string | null> {
|
|
129
|
+
const allMetadata = await this.getAllMetadata();
|
|
130
|
+
if (allMetadata.length === 0) return null;
|
|
131
|
+
|
|
132
|
+
// Sort by lastModified descending
|
|
133
|
+
allMetadata.sort((a, b) => b.lastModified.localeCompare(a.lastModified));
|
|
134
|
+
return allMetadata[0].id;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Store } from "../store.js";
|
|
2
|
+
import type { StoreConfig } from "../types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Store for application settings (theme, proxy config, etc.).
|
|
6
|
+
*/
|
|
7
|
+
export class SettingsStore extends Store {
|
|
8
|
+
getConfig(): StoreConfig {
|
|
9
|
+
return {
|
|
10
|
+
name: "settings",
|
|
11
|
+
// No keyPath - uses out-of-line keys
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async get<T>(key: string): Promise<T | null> {
|
|
16
|
+
return this.getBackend().get("settings", key);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async set<T>(key: string, value: T): Promise<void> {
|
|
20
|
+
await this.getBackend().set("settings", key, value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async delete(key: string): Promise<void> {
|
|
24
|
+
await this.getBackend().delete("settings", key);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async list(): Promise<string[]> {
|
|
28
|
+
return this.getBackend().keys("settings");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async clear(): Promise<void> {
|
|
32
|
+
await this.getBackend().clear("settings");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import type { Model } from "@oh-my-pi/pi-ai";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Transaction interface for atomic operations across stores.
|
|
6
|
+
*/
|
|
7
|
+
export interface StorageTransaction {
|
|
8
|
+
/**
|
|
9
|
+
* Get a value by key from a specific store.
|
|
10
|
+
*/
|
|
11
|
+
get<T = unknown>(storeName: string, key: string): Promise<T | null>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Set a value for a key in a specific store.
|
|
15
|
+
*/
|
|
16
|
+
set<T = unknown>(storeName: string, key: string, value: T): Promise<void>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Delete a key from a specific store.
|
|
20
|
+
*/
|
|
21
|
+
delete(storeName: string, key: string): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Base interface for all storage backends.
|
|
26
|
+
* Multi-store key-value storage abstraction that can be implemented
|
|
27
|
+
* by IndexedDB, remote APIs, or any other multi-collection storage system.
|
|
28
|
+
*/
|
|
29
|
+
export interface StorageBackend {
|
|
30
|
+
/**
|
|
31
|
+
* Get a value by key from a specific store. Returns null if key doesn't exist.
|
|
32
|
+
*/
|
|
33
|
+
get<T = unknown>(storeName: string, key: string): Promise<T | null>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Set a value for a key in a specific store.
|
|
37
|
+
*/
|
|
38
|
+
set<T = unknown>(storeName: string, key: string, value: T): Promise<void>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Delete a key from a specific store.
|
|
42
|
+
*/
|
|
43
|
+
delete(storeName: string, key: string): Promise<void>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get all keys from a specific store, optionally filtered by prefix.
|
|
47
|
+
*/
|
|
48
|
+
keys(storeName: string, prefix?: string): Promise<string[]>;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get all values from a specific store, ordered by an index.
|
|
52
|
+
* @param storeName - The store to query
|
|
53
|
+
* @param indexName - The index to use for ordering
|
|
54
|
+
* @param direction - Sort direction ("asc" or "desc")
|
|
55
|
+
*/
|
|
56
|
+
getAllFromIndex<T = unknown>(storeName: string, indexName: string, direction?: "asc" | "desc"): Promise<T[]>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Clear all data from a specific store.
|
|
60
|
+
*/
|
|
61
|
+
clear(storeName: string): Promise<void>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Check if a key exists in a specific store.
|
|
65
|
+
*/
|
|
66
|
+
has(storeName: string, key: string): Promise<boolean>;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Execute atomic operations across multiple stores.
|
|
70
|
+
*/
|
|
71
|
+
transaction<T>(
|
|
72
|
+
storeNames: string[],
|
|
73
|
+
mode: "readonly" | "readwrite",
|
|
74
|
+
operation: (tx: StorageTransaction) => Promise<T>,
|
|
75
|
+
): Promise<T>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get storage quota information.
|
|
79
|
+
* Used for warning users when approaching limits.
|
|
80
|
+
*/
|
|
81
|
+
getQuotaInfo(): Promise<{ usage: number; quota: number; percent: number }>;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Request persistent storage (prevents eviction).
|
|
85
|
+
* Returns true if granted, false otherwise.
|
|
86
|
+
*/
|
|
87
|
+
requestPersistence(): Promise<boolean>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Lightweight session metadata for listing and searching.
|
|
92
|
+
* Stored separately from full session data for performance.
|
|
93
|
+
*/
|
|
94
|
+
export interface SessionMetadata {
|
|
95
|
+
/** Unique session identifier (UUID v4) */
|
|
96
|
+
id: string;
|
|
97
|
+
|
|
98
|
+
/** User-defined title or auto-generated from first message */
|
|
99
|
+
title: string;
|
|
100
|
+
|
|
101
|
+
/** ISO 8601 UTC timestamp of creation */
|
|
102
|
+
createdAt: string;
|
|
103
|
+
|
|
104
|
+
/** ISO 8601 UTC timestamp of last modification */
|
|
105
|
+
lastModified: string;
|
|
106
|
+
|
|
107
|
+
/** Total number of messages (user + assistant + tool results) */
|
|
108
|
+
messageCount: number;
|
|
109
|
+
|
|
110
|
+
/** Cumulative usage statistics */
|
|
111
|
+
usage: {
|
|
112
|
+
/** Total input tokens */
|
|
113
|
+
input: number;
|
|
114
|
+
/** Total output tokens */
|
|
115
|
+
output: number;
|
|
116
|
+
/** Total cache read tokens */
|
|
117
|
+
cacheRead: number;
|
|
118
|
+
/** Total cache write tokens */
|
|
119
|
+
cacheWrite: number;
|
|
120
|
+
/** Total tokens processed */
|
|
121
|
+
totalTokens: number;
|
|
122
|
+
/** Total cost breakdown */
|
|
123
|
+
cost: {
|
|
124
|
+
input: number;
|
|
125
|
+
output: number;
|
|
126
|
+
cacheRead: number;
|
|
127
|
+
cacheWrite: number;
|
|
128
|
+
total: number;
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/** Last used thinking level */
|
|
133
|
+
thinkingLevel: ThinkingLevel;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Preview text for search and display.
|
|
137
|
+
* First 2KB of conversation text (user + assistant messages in sequence).
|
|
138
|
+
* Tool calls and tool results are excluded.
|
|
139
|
+
*/
|
|
140
|
+
preview: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Full session data including all messages.
|
|
145
|
+
* Only loaded when user opens a specific session.
|
|
146
|
+
*/
|
|
147
|
+
export interface SessionData {
|
|
148
|
+
/** Unique session identifier (UUID v4) */
|
|
149
|
+
id: string;
|
|
150
|
+
|
|
151
|
+
/** User-defined title or auto-generated from first message */
|
|
152
|
+
title: string;
|
|
153
|
+
|
|
154
|
+
/** Last selected model */
|
|
155
|
+
model: Model<any>;
|
|
156
|
+
|
|
157
|
+
/** Last selected thinking level */
|
|
158
|
+
thinkingLevel: ThinkingLevel;
|
|
159
|
+
|
|
160
|
+
/** Full conversation history (with attachments inline) */
|
|
161
|
+
messages: AgentMessage[];
|
|
162
|
+
|
|
163
|
+
/** ISO 8601 UTC timestamp of creation */
|
|
164
|
+
createdAt: string;
|
|
165
|
+
|
|
166
|
+
/** ISO 8601 UTC timestamp of last modification */
|
|
167
|
+
lastModified: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Configuration for IndexedDB backend.
|
|
172
|
+
*/
|
|
173
|
+
export interface IndexedDBConfig {
|
|
174
|
+
/** Database name */
|
|
175
|
+
dbName: string;
|
|
176
|
+
/** Database version */
|
|
177
|
+
version: number;
|
|
178
|
+
/** Object stores to create */
|
|
179
|
+
stores: StoreConfig[];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Configuration for an IndexedDB object store.
|
|
184
|
+
*/
|
|
185
|
+
export interface StoreConfig {
|
|
186
|
+
/** Store name */
|
|
187
|
+
name: string;
|
|
188
|
+
/** Key path (optional, for auto-extracting keys from objects) */
|
|
189
|
+
keyPath?: string;
|
|
190
|
+
/** Auto-increment keys (optional) */
|
|
191
|
+
autoIncrement?: boolean;
|
|
192
|
+
/** Indices to create on this store */
|
|
193
|
+
indices?: IndexConfig[];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Configuration for an IndexedDB index.
|
|
198
|
+
*/
|
|
199
|
+
export interface IndexConfig {
|
|
200
|
+
/** Index name */
|
|
201
|
+
name: string;
|
|
202
|
+
/** Key path to index on */
|
|
203
|
+
keyPath: string;
|
|
204
|
+
/** Unique constraint (optional) */
|
|
205
|
+
unique?: boolean;
|
|
206
|
+
}
|