@raisindb/client 0.1.5 → 0.1.17
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/README.md +281 -70
- package/dist/chunk-DJDP5CX6.mjs +866 -0
- package/dist/chunk-DJDP5CX6.mjs.map +1 -0
- package/dist/chunk-JRFBP4WK.mjs +170 -0
- package/dist/chunk-JRFBP4WK.mjs.map +1 -0
- package/dist/chunk-OLJOZIIU.mjs +1348 -0
- package/dist/chunk-OLJOZIIU.mjs.map +1 -0
- package/dist/chunk-T2ZSNYFG.mjs +248 -0
- package/dist/chunk-T2ZSNYFG.mjs.map +1 -0
- package/dist/chunk-VZX3ACSO.mjs +190 -0
- package/dist/chunk-VZX3ACSO.mjs.map +1 -0
- package/dist/chunk-WFBAWOVM.mjs +532 -0
- package/dist/chunk-WFBAWOVM.mjs.map +1 -0
- package/dist/conversation-list-store-CNcVagmw.d.mts +6120 -0
- package/dist/conversation-list-store-CNcVagmw.d.ts +6120 -0
- package/dist/index-DQjE-4Oj.d.ts +301 -0
- package/dist/index-FkFH4HHQ.d.mts +301 -0
- package/dist/index.d.mts +66 -6108
- package/dist/index.d.ts +66 -6108
- package/dist/index.js +859 -109
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +639 -3108
- package/dist/index.mjs.map +1 -0
- package/dist/react.d.mts +4 -0
- package/dist/react.d.ts +4 -0
- package/dist/react.js +2500 -0
- package/dist/react.js.map +1 -0
- package/dist/react.mjs +9 -0
- package/dist/react.mjs.map +1 -0
- package/dist/svelte-sql-62EaYoeM.d.ts +52 -0
- package/dist/svelte-sql-CgT-Ij-P.d.mts +52 -0
- package/dist/svelte.d.mts +288 -0
- package/dist/svelte.d.ts +288 -0
- package/dist/svelte.js +2413 -0
- package/dist/svelte.js.map +1 -0
- package/dist/svelte.mjs +25 -0
- package/dist/svelte.mjs.map +1 -0
- package/dist/vue.d.mts +197 -0
- package/dist/vue.d.ts +197 -0
- package/dist/vue.js +1441 -0
- package/dist/vue.js.map +1 -0
- package/dist/vue.mjs +9 -0
- package/dist/vue.mjs.map +1 -0
- package/package.json +18 -3
package/dist/vue.js
ADDED
|
@@ -0,0 +1,1441 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/integrations/vue/index.ts
|
|
21
|
+
var vue_exports = {};
|
|
22
|
+
__export(vue_exports, {
|
|
23
|
+
createRaisinVue: () => createRaisinVue
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(vue_exports);
|
|
26
|
+
|
|
27
|
+
// src/integrations/svelte/svelte-auth.ts
|
|
28
|
+
function createAuthAdapter(client) {
|
|
29
|
+
let snapshot = {
|
|
30
|
+
user: client.getStoredUser(),
|
|
31
|
+
isAuthenticated: client.getStoredUser() !== null,
|
|
32
|
+
isLoading: false
|
|
33
|
+
};
|
|
34
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
35
|
+
function emit() {
|
|
36
|
+
for (const cb of listeners) cb(snapshot);
|
|
37
|
+
}
|
|
38
|
+
function update(partial) {
|
|
39
|
+
snapshot = { ...snapshot, ...partial };
|
|
40
|
+
emit();
|
|
41
|
+
}
|
|
42
|
+
const unsubAuth = client.onAuthStateChange(({ event, session }) => {
|
|
43
|
+
if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED" || event === "USER_UPDATED") {
|
|
44
|
+
update({ user: session.user, isAuthenticated: true });
|
|
45
|
+
} else if (event === "SIGNED_OUT" || event === "SESSION_EXPIRED") {
|
|
46
|
+
update({ user: null, isAuthenticated: false });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
const unsubUser = client.onUserChange(({ node }) => {
|
|
50
|
+
if (!snapshot.user) return;
|
|
51
|
+
update({
|
|
52
|
+
user: {
|
|
53
|
+
...snapshot.user,
|
|
54
|
+
home: node.path,
|
|
55
|
+
displayName: node.properties.display_name ?? snapshot.user.displayName,
|
|
56
|
+
avatarUrl: node.properties.avatar_url ?? snapshot.user.avatarUrl
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
subscribe(cb) {
|
|
62
|
+
listeners.add(cb);
|
|
63
|
+
return () => {
|
|
64
|
+
listeners.delete(cb);
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
getSnapshot: () => snapshot,
|
|
68
|
+
async login(email, password, repository) {
|
|
69
|
+
update({ isLoading: true });
|
|
70
|
+
try {
|
|
71
|
+
return await client.loginWithEmail(email, password, repository);
|
|
72
|
+
} finally {
|
|
73
|
+
update({ isLoading: false });
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
async register(email, password, repository, displayName) {
|
|
77
|
+
update({ isLoading: true });
|
|
78
|
+
try {
|
|
79
|
+
return await client.registerWithEmail(email, password, repository, displayName);
|
|
80
|
+
} finally {
|
|
81
|
+
update({ isLoading: false });
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
async logout(options) {
|
|
85
|
+
update({ isLoading: true });
|
|
86
|
+
try {
|
|
87
|
+
await client.logout(options);
|
|
88
|
+
} finally {
|
|
89
|
+
update({ isLoading: false });
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
async initSession(repository) {
|
|
93
|
+
update({ isLoading: true });
|
|
94
|
+
try {
|
|
95
|
+
return await client.initSession(repository);
|
|
96
|
+
} finally {
|
|
97
|
+
update({ isLoading: false });
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
destroy() {
|
|
101
|
+
unsubAuth();
|
|
102
|
+
unsubUser();
|
|
103
|
+
listeners.clear();
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/integrations/vue/vue-auth.ts
|
|
109
|
+
function createUseAuth(vue) {
|
|
110
|
+
return function useAuth(client) {
|
|
111
|
+
const adapter = createAuthAdapter(client);
|
|
112
|
+
const snapshot = vue.ref(adapter.getSnapshot());
|
|
113
|
+
const unsubscribe = adapter.subscribe((s) => {
|
|
114
|
+
snapshot.value = s;
|
|
115
|
+
});
|
|
116
|
+
vue.onUnmounted(() => {
|
|
117
|
+
unsubscribe();
|
|
118
|
+
adapter.destroy();
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
user: vue.computed(() => snapshot.value.user),
|
|
122
|
+
isAuthenticated: vue.computed(() => snapshot.value.isAuthenticated),
|
|
123
|
+
isLoading: vue.computed(() => snapshot.value.isLoading),
|
|
124
|
+
login: adapter.login,
|
|
125
|
+
register: adapter.register,
|
|
126
|
+
logout: adapter.logout,
|
|
127
|
+
initSession: adapter.initSession
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/connection.ts
|
|
133
|
+
var import_events = require("events");
|
|
134
|
+
|
|
135
|
+
// src/logger.ts
|
|
136
|
+
var config = {
|
|
137
|
+
level: 1 /* Error */,
|
|
138
|
+
prefix: "[RaisinDB]"
|
|
139
|
+
};
|
|
140
|
+
function setLogLevel(level) {
|
|
141
|
+
config.level = level;
|
|
142
|
+
}
|
|
143
|
+
function getLogLevel() {
|
|
144
|
+
return config.level;
|
|
145
|
+
}
|
|
146
|
+
var logger = {
|
|
147
|
+
/**
|
|
148
|
+
* Log debug message (only shown when level >= Debug)
|
|
149
|
+
*/
|
|
150
|
+
debug(...args) {
|
|
151
|
+
if (config.level >= 4 /* Debug */) {
|
|
152
|
+
console.log(config.prefix, ...args);
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
/**
|
|
156
|
+
* Log info message (only shown when level >= Info)
|
|
157
|
+
*/
|
|
158
|
+
info(...args) {
|
|
159
|
+
if (config.level >= 3 /* Info */) {
|
|
160
|
+
console.log(config.prefix, ...args);
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
/**
|
|
164
|
+
* Log warning message (only shown when level >= Warn)
|
|
165
|
+
*/
|
|
166
|
+
warn(...args) {
|
|
167
|
+
if (config.level >= 2 /* Warn */) {
|
|
168
|
+
console.warn(config.prefix, ...args);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
/**
|
|
172
|
+
* Log error message (only shown when level >= Error)
|
|
173
|
+
*/
|
|
174
|
+
error(...args) {
|
|
175
|
+
if (config.level >= 1 /* Error */) {
|
|
176
|
+
console.error(config.prefix, ...args);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
if (typeof globalThis !== "undefined") {
|
|
181
|
+
globalThis.__raisinDebug = (enable = true) => {
|
|
182
|
+
setLogLevel(enable ? 4 /* Debug */ : 1 /* Error */);
|
|
183
|
+
console.log(`[RaisinDB] Debug logging ${enable ? "ENABLED" : "DISABLED"} (includes server handler logs)`);
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/integrations/svelte/svelte-connection.ts
|
|
188
|
+
function createConnectionAdapter(client) {
|
|
189
|
+
let snapshot = {
|
|
190
|
+
state: client.getConnectionState(),
|
|
191
|
+
isConnected: client.getConnectionState() === "connected" /* Connected */,
|
|
192
|
+
isReady: client.isReady()
|
|
193
|
+
};
|
|
194
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
195
|
+
function emit() {
|
|
196
|
+
for (const cb of listeners) cb(snapshot);
|
|
197
|
+
}
|
|
198
|
+
const unsubConn = client.onConnectionStateChange((newState) => {
|
|
199
|
+
snapshot = {
|
|
200
|
+
state: newState,
|
|
201
|
+
isConnected: newState === "connected" /* Connected */,
|
|
202
|
+
isReady: snapshot.isReady
|
|
203
|
+
};
|
|
204
|
+
emit();
|
|
205
|
+
});
|
|
206
|
+
const unsubReady = client.onReadyStateChange((ready) => {
|
|
207
|
+
snapshot = { ...snapshot, isReady: ready };
|
|
208
|
+
emit();
|
|
209
|
+
});
|
|
210
|
+
return {
|
|
211
|
+
subscribe(cb) {
|
|
212
|
+
listeners.add(cb);
|
|
213
|
+
return () => {
|
|
214
|
+
listeners.delete(cb);
|
|
215
|
+
};
|
|
216
|
+
},
|
|
217
|
+
getSnapshot: () => snapshot,
|
|
218
|
+
async connect() {
|
|
219
|
+
await client.connect();
|
|
220
|
+
},
|
|
221
|
+
disconnect() {
|
|
222
|
+
client.disconnect();
|
|
223
|
+
},
|
|
224
|
+
destroy() {
|
|
225
|
+
unsubConn();
|
|
226
|
+
unsubReady();
|
|
227
|
+
listeners.clear();
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/integrations/vue/vue-connection.ts
|
|
233
|
+
function createUseConnection(vue) {
|
|
234
|
+
return function useConnection(client) {
|
|
235
|
+
const adapter = createConnectionAdapter(client);
|
|
236
|
+
const snapshot = vue.ref(adapter.getSnapshot());
|
|
237
|
+
const unsubscribe = adapter.subscribe((s) => {
|
|
238
|
+
snapshot.value = s;
|
|
239
|
+
});
|
|
240
|
+
vue.onUnmounted(() => {
|
|
241
|
+
unsubscribe();
|
|
242
|
+
adapter.destroy();
|
|
243
|
+
});
|
|
244
|
+
return {
|
|
245
|
+
state: vue.computed(() => snapshot.value.state),
|
|
246
|
+
isConnected: vue.computed(() => snapshot.value.isConnected),
|
|
247
|
+
isReady: vue.computed(() => snapshot.value.isReady),
|
|
248
|
+
connect: adapter.connect,
|
|
249
|
+
disconnect: adapter.disconnect
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/integrations/svelte/svelte-sql.ts
|
|
255
|
+
function rowsToObjects(result) {
|
|
256
|
+
return result.rows;
|
|
257
|
+
}
|
|
258
|
+
function createSqlAdapter(database, sql, params, options) {
|
|
259
|
+
let snapshot = {
|
|
260
|
+
data: null,
|
|
261
|
+
isLoading: false,
|
|
262
|
+
error: null
|
|
263
|
+
};
|
|
264
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
265
|
+
let destroyed = false;
|
|
266
|
+
function emit() {
|
|
267
|
+
for (const cb of listeners) cb(snapshot);
|
|
268
|
+
}
|
|
269
|
+
function update(partial) {
|
|
270
|
+
snapshot = { ...snapshot, ...partial };
|
|
271
|
+
emit();
|
|
272
|
+
}
|
|
273
|
+
async function fetchData() {
|
|
274
|
+
if (destroyed) return;
|
|
275
|
+
update({ isLoading: true, error: null });
|
|
276
|
+
try {
|
|
277
|
+
const result = await database.executeSql(sql, params);
|
|
278
|
+
if (!destroyed) {
|
|
279
|
+
update({ data: rowsToObjects(result), isLoading: false });
|
|
280
|
+
}
|
|
281
|
+
} catch (err) {
|
|
282
|
+
if (!destroyed) {
|
|
283
|
+
update({
|
|
284
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
285
|
+
isLoading: false
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const cleanups = [];
|
|
291
|
+
const enabled = options?.enabled !== false;
|
|
292
|
+
if (enabled) {
|
|
293
|
+
fetchData();
|
|
294
|
+
}
|
|
295
|
+
const refetchOnReconnect = options?.refetchOnReconnect !== false;
|
|
296
|
+
if (enabled && refetchOnReconnect) {
|
|
297
|
+
const client = database._client;
|
|
298
|
+
if (client?.onReconnected) {
|
|
299
|
+
const unsub = client.onReconnected(() => {
|
|
300
|
+
fetchData();
|
|
301
|
+
});
|
|
302
|
+
cleanups.push(unsub);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const realtime = options?.realtime;
|
|
306
|
+
if (enabled && realtime) {
|
|
307
|
+
let unsubscribe;
|
|
308
|
+
let cancelled = false;
|
|
309
|
+
database.events().subscribe(
|
|
310
|
+
{
|
|
311
|
+
workspace: realtime.workspace,
|
|
312
|
+
event_types: realtime.eventTypes,
|
|
313
|
+
path: realtime.path,
|
|
314
|
+
node_type: realtime.nodeType
|
|
315
|
+
},
|
|
316
|
+
() => {
|
|
317
|
+
fetchData();
|
|
318
|
+
}
|
|
319
|
+
).then((sub) => {
|
|
320
|
+
if (cancelled) {
|
|
321
|
+
sub.unsubscribe();
|
|
322
|
+
} else {
|
|
323
|
+
unsubscribe = () => sub.unsubscribe();
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
cleanups.push(() => {
|
|
327
|
+
cancelled = true;
|
|
328
|
+
unsubscribe?.();
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
subscribe(cb) {
|
|
333
|
+
listeners.add(cb);
|
|
334
|
+
return () => {
|
|
335
|
+
listeners.delete(cb);
|
|
336
|
+
};
|
|
337
|
+
},
|
|
338
|
+
getSnapshot: () => snapshot,
|
|
339
|
+
refetch: fetchData,
|
|
340
|
+
destroy() {
|
|
341
|
+
destroyed = true;
|
|
342
|
+
for (const cleanup of cleanups) cleanup();
|
|
343
|
+
listeners.clear();
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/integrations/vue/vue-sql.ts
|
|
349
|
+
function createUseSql(vue) {
|
|
350
|
+
return function useSql(database, sql, params, options) {
|
|
351
|
+
const adapter = createSqlAdapter(database, sql, params, options);
|
|
352
|
+
const snapshot = vue.ref(adapter.getSnapshot());
|
|
353
|
+
const unsubscribe = adapter.subscribe((s) => {
|
|
354
|
+
snapshot.value = s;
|
|
355
|
+
});
|
|
356
|
+
vue.onUnmounted(() => {
|
|
357
|
+
unsubscribe();
|
|
358
|
+
adapter.destroy();
|
|
359
|
+
});
|
|
360
|
+
return {
|
|
361
|
+
data: vue.computed(() => snapshot.value.data),
|
|
362
|
+
isLoading: vue.computed(() => snapshot.value.isLoading),
|
|
363
|
+
error: vue.computed(() => snapshot.value.error),
|
|
364
|
+
refetch: adapter.refetch
|
|
365
|
+
};
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/integrations/svelte/svelte-subscription.ts
|
|
370
|
+
function createSubscriptionAdapter(database, filters, callback) {
|
|
371
|
+
let unsubscribe;
|
|
372
|
+
let cancelled = false;
|
|
373
|
+
database.events().subscribe(filters, (event) => {
|
|
374
|
+
if (!cancelled) callback(event);
|
|
375
|
+
}).then((sub) => {
|
|
376
|
+
if (cancelled) {
|
|
377
|
+
sub.unsubscribe();
|
|
378
|
+
} else {
|
|
379
|
+
unsubscribe = () => sub.unsubscribe();
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
return {
|
|
383
|
+
destroy() {
|
|
384
|
+
cancelled = true;
|
|
385
|
+
unsubscribe?.();
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// src/integrations/vue/vue-subscription.ts
|
|
391
|
+
function createUseSubscription(vue) {
|
|
392
|
+
return function useSubscription(database, filters, callback) {
|
|
393
|
+
const adapter = createSubscriptionAdapter(database, filters, callback);
|
|
394
|
+
vue.onUnmounted(() => adapter.destroy());
|
|
395
|
+
return {
|
|
396
|
+
unsubscribe: () => adapter.destroy()
|
|
397
|
+
};
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/utils/plan-projection.ts
|
|
402
|
+
function asRecord(value) {
|
|
403
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
404
|
+
return value;
|
|
405
|
+
}
|
|
406
|
+
function asString(value) {
|
|
407
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
408
|
+
}
|
|
409
|
+
function parsePlanTasks(rawTasks) {
|
|
410
|
+
if (!Array.isArray(rawTasks)) return [];
|
|
411
|
+
return rawTasks.map((rawTask, index) => {
|
|
412
|
+
if (typeof rawTask === "string") {
|
|
413
|
+
return {
|
|
414
|
+
title: rawTask,
|
|
415
|
+
status: "pending"
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
const task = asRecord(rawTask);
|
|
419
|
+
if (!task) return null;
|
|
420
|
+
return {
|
|
421
|
+
taskId: asString(task.task_id) ?? asString(task.id),
|
|
422
|
+
title: asString(task.title) ?? `Task ${index + 1}`,
|
|
423
|
+
status: asString(task.status) ?? "pending",
|
|
424
|
+
description: asString(task.description),
|
|
425
|
+
priority: asString(task.priority)
|
|
426
|
+
};
|
|
427
|
+
}).filter((task) => task !== null);
|
|
428
|
+
}
|
|
429
|
+
function upsertTask(plan, nextTask) {
|
|
430
|
+
const existingIndex = plan.tasks.findIndex(
|
|
431
|
+
(task) => nextTask.taskId && task.taskId === nextTask.taskId || !nextTask.taskId && task.title === nextTask.title
|
|
432
|
+
);
|
|
433
|
+
if (existingIndex === -1) {
|
|
434
|
+
plan.tasks.push(nextTask);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
plan.tasks[existingIndex] = {
|
|
438
|
+
...plan.tasks[existingIndex],
|
|
439
|
+
...nextTask
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function ensurePlan(plans, key, fallbackTitle, timestamp) {
|
|
443
|
+
const existing = plans.get(key);
|
|
444
|
+
if (existing) {
|
|
445
|
+
if (timestamp) existing.updatedAt = timestamp;
|
|
446
|
+
return existing;
|
|
447
|
+
}
|
|
448
|
+
const created = {
|
|
449
|
+
key,
|
|
450
|
+
title: fallbackTitle,
|
|
451
|
+
status: "pending_approval",
|
|
452
|
+
requiresApproval: true,
|
|
453
|
+
tasks: [],
|
|
454
|
+
updatedAt: timestamp
|
|
455
|
+
};
|
|
456
|
+
plans.set(key, created);
|
|
457
|
+
return created;
|
|
458
|
+
}
|
|
459
|
+
function projectPlansFromMessages(messages) {
|
|
460
|
+
const ordered = [...messages].sort((a, b) => {
|
|
461
|
+
const aTime = Date.parse(a.timestamp || "");
|
|
462
|
+
const bTime = Date.parse(b.timestamp || "");
|
|
463
|
+
return (Number.isNaN(aTime) ? 0 : aTime) - (Number.isNaN(bTime) ? 0 : bTime);
|
|
464
|
+
});
|
|
465
|
+
const plans = /* @__PURE__ */ new Map();
|
|
466
|
+
for (const message of ordered) {
|
|
467
|
+
const timestamp = message.timestamp;
|
|
468
|
+
const data = asRecord(message.data);
|
|
469
|
+
if (message.messageType === "ai_plan" && data) {
|
|
470
|
+
const planPath = asString(data.plan_path);
|
|
471
|
+
const planId = asString(data.plan_id);
|
|
472
|
+
const key = planPath || planId || message.path || message.id || `plan:${timestamp}`;
|
|
473
|
+
const plan = ensurePlan(
|
|
474
|
+
plans,
|
|
475
|
+
key,
|
|
476
|
+
asString(data.title) || message.content || "Plan",
|
|
477
|
+
timestamp
|
|
478
|
+
);
|
|
479
|
+
plan.planPath = planPath ?? plan.planPath;
|
|
480
|
+
plan.planId = planId ?? plan.planId;
|
|
481
|
+
plan.title = asString(data.title) || plan.title;
|
|
482
|
+
plan.status = asString(data.status) || plan.status;
|
|
483
|
+
plan.requiresApproval = Boolean(data.requires_approval) || plan.status === "pending_approval";
|
|
484
|
+
plan.sourceMessagePath = message.path ?? plan.sourceMessagePath;
|
|
485
|
+
const parsedTasks = parsePlanTasks(data.tasks);
|
|
486
|
+
if (parsedTasks.length > 0) {
|
|
487
|
+
plan.tasks = parsedTasks;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (message.messageType === "ai_task_update" && data) {
|
|
491
|
+
const planPath = asString(data.plan_path);
|
|
492
|
+
const planId = asString(data.plan_id);
|
|
493
|
+
const key = planPath || planId;
|
|
494
|
+
if (!key) continue;
|
|
495
|
+
const plan = ensurePlan(plans, key, "Plan", timestamp);
|
|
496
|
+
plan.planPath = planPath ?? plan.planPath;
|
|
497
|
+
plan.planId = planId ?? plan.planId;
|
|
498
|
+
plan.status = asString(data.plan_status) || plan.status;
|
|
499
|
+
plan.requiresApproval = plan.status === "pending_approval";
|
|
500
|
+
const taskUpdate = {
|
|
501
|
+
taskId: asString(data.task_id),
|
|
502
|
+
title: asString(data.task_title) || asString(data.title) || "Task",
|
|
503
|
+
status: asString(data.status) || "pending"
|
|
504
|
+
};
|
|
505
|
+
upsertTask(plan, taskUpdate);
|
|
506
|
+
}
|
|
507
|
+
if (Array.isArray(message.children)) {
|
|
508
|
+
for (const child of message.children) {
|
|
509
|
+
if (child.type !== "plan") continue;
|
|
510
|
+
const key = child.path || message.path || message.id || `plan-child:${child.id}:${timestamp}`;
|
|
511
|
+
const plan = ensurePlan(plans, key, child.planTitle || message.content || "Plan", timestamp);
|
|
512
|
+
plan.title = child.planTitle || plan.title;
|
|
513
|
+
plan.status = child.status || plan.status;
|
|
514
|
+
plan.requiresApproval = plan.status === "pending_approval";
|
|
515
|
+
if (Array.isArray(child.tasks) && child.tasks.length > 0) {
|
|
516
|
+
plan.tasks = child.tasks.map((task) => ({
|
|
517
|
+
taskId: task.id,
|
|
518
|
+
title: task.title,
|
|
519
|
+
status: task.status
|
|
520
|
+
}));
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return [...plans.values()].sort((a, b) => {
|
|
526
|
+
const aTime = Date.parse(a.updatedAt || "");
|
|
527
|
+
const bTime = Date.parse(b.updatedAt || "");
|
|
528
|
+
return (Number.isNaN(bTime) ? 0 : bTime) - (Number.isNaN(aTime) ? 0 : aTime);
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/utils/chat-events.ts
|
|
533
|
+
function isRecoveredDoneEvent(event) {
|
|
534
|
+
if (event.type !== "done") return false;
|
|
535
|
+
const done = event;
|
|
536
|
+
if (done.recovered !== true) return false;
|
|
537
|
+
return typeof done.content !== "string" || done.content.trim().length === 0;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// src/stores/conversation-store.ts
|
|
541
|
+
var ConversationStore = class {
|
|
542
|
+
constructor(options) {
|
|
543
|
+
this.subscribers = /* @__PURE__ */ new Set();
|
|
544
|
+
// Persistent SSE subscription
|
|
545
|
+
this.subscription = null;
|
|
546
|
+
this._messages = [];
|
|
547
|
+
this._isStreaming = false;
|
|
548
|
+
this._isWaiting = false;
|
|
549
|
+
this._streamingText = "";
|
|
550
|
+
this._error = null;
|
|
551
|
+
this._activeToolCalls = [];
|
|
552
|
+
this._isLoading = false;
|
|
553
|
+
this._conversationType = null;
|
|
554
|
+
this._streamingTimer = null;
|
|
555
|
+
this._watchdogTimer = null;
|
|
556
|
+
this.manager = options.database.conversations;
|
|
557
|
+
this._conversationPath = options.conversationPath ?? null;
|
|
558
|
+
this.createOptions = options.createOptions;
|
|
559
|
+
this.onEvent = options.onEvent;
|
|
560
|
+
this._streamingTimeoutMs = options.streamingTimeoutMs ?? 12e4;
|
|
561
|
+
this._watchdogIntervalMs = options.watchdogIntervalMs ?? 3e4;
|
|
562
|
+
if (this._conversationPath) {
|
|
563
|
+
this.ensureSubscription();
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
// ==========================================================================
|
|
567
|
+
// Subscription
|
|
568
|
+
// ==========================================================================
|
|
569
|
+
/**
|
|
570
|
+
* Subscribe to state changes.
|
|
571
|
+
* Returns an unsubscribe function.
|
|
572
|
+
*/
|
|
573
|
+
subscribe(callback) {
|
|
574
|
+
this.subscribers.add(callback);
|
|
575
|
+
callback(this.getSnapshot());
|
|
576
|
+
return () => {
|
|
577
|
+
this.subscribers.delete(callback);
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Get the current state snapshot.
|
|
582
|
+
*/
|
|
583
|
+
getSnapshot() {
|
|
584
|
+
return {
|
|
585
|
+
conversation: this._conversationPath ? { conversationPath: this._conversationPath, type: this._conversationType || "ai_chat" } : null,
|
|
586
|
+
messages: [...this._messages],
|
|
587
|
+
isStreaming: this._isStreaming,
|
|
588
|
+
isWaiting: this._isWaiting,
|
|
589
|
+
streamingText: this._streamingText,
|
|
590
|
+
error: this._error,
|
|
591
|
+
activeToolCalls: [...this._activeToolCalls],
|
|
592
|
+
plans: projectPlansFromMessages(this._messages),
|
|
593
|
+
isLoading: this._isLoading,
|
|
594
|
+
conversationPath: this._conversationPath
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
// ==========================================================================
|
|
598
|
+
// Actions
|
|
599
|
+
// ==========================================================================
|
|
600
|
+
/**
|
|
601
|
+
* Send a user message and stream the AI response.
|
|
602
|
+
*
|
|
603
|
+
* If no conversation exists yet and `createOptions` was provided,
|
|
604
|
+
* the conversation is created automatically on the first call.
|
|
605
|
+
*/
|
|
606
|
+
async sendMessage(content) {
|
|
607
|
+
if (this._isStreaming) {
|
|
608
|
+
logger.warn("[ConversationStore] Cannot send message while streaming");
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
logger.info("[ConversationStore] Sending message", { path: this._conversationPath });
|
|
612
|
+
this._error = null;
|
|
613
|
+
if (!this._conversationPath) {
|
|
614
|
+
if (!this.createOptions) {
|
|
615
|
+
throw new Error("No conversationPath and no createOptions provided");
|
|
616
|
+
}
|
|
617
|
+
try {
|
|
618
|
+
const convo = await this.manager.create({
|
|
619
|
+
participant: this.createOptions.participant,
|
|
620
|
+
input: this.createOptions.input
|
|
621
|
+
});
|
|
622
|
+
this._conversationPath = convo.conversationPath;
|
|
623
|
+
this._conversationType = convo.type;
|
|
624
|
+
for (const event of convo.initialEvents ?? []) {
|
|
625
|
+
this.onEvent?.(event);
|
|
626
|
+
this.handleEvent(event);
|
|
627
|
+
}
|
|
628
|
+
this.notify();
|
|
629
|
+
} catch (err) {
|
|
630
|
+
this._error = err instanceof Error ? err.message : String(err);
|
|
631
|
+
this.notify();
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
const userMessage = {
|
|
636
|
+
role: "user",
|
|
637
|
+
content,
|
|
638
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
639
|
+
};
|
|
640
|
+
this._messages.push(userMessage);
|
|
641
|
+
this._isStreaming = true;
|
|
642
|
+
this._isWaiting = false;
|
|
643
|
+
this._streamingText = "";
|
|
644
|
+
this._activeToolCalls = [];
|
|
645
|
+
this.resetStreamingTimer();
|
|
646
|
+
this.startWatchdog();
|
|
647
|
+
this.notify();
|
|
648
|
+
try {
|
|
649
|
+
this.ensureSubscription();
|
|
650
|
+
await this.subscription.waitUntilConnected();
|
|
651
|
+
await this.manager.createUserMessage(this._conversationPath, content);
|
|
652
|
+
} catch (err) {
|
|
653
|
+
logger.error("[ConversationStore] Send failed", err);
|
|
654
|
+
this._error = err instanceof Error ? err.message : String(err);
|
|
655
|
+
this._isStreaming = false;
|
|
656
|
+
this.clearStreamingTimer();
|
|
657
|
+
this.stopWatchdog();
|
|
658
|
+
this.notify();
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Load messages from the persisted node tree.
|
|
663
|
+
* Useful for loading history on page reload.
|
|
664
|
+
*/
|
|
665
|
+
async loadMessages() {
|
|
666
|
+
if (!this._conversationPath) return [];
|
|
667
|
+
this._isLoading = true;
|
|
668
|
+
this.notify();
|
|
669
|
+
try {
|
|
670
|
+
const messages = await this.manager.getMessages(this._conversationPath);
|
|
671
|
+
logger.debug(`[ConversationStore] Loaded ${messages.length} messages`);
|
|
672
|
+
if (messages.length > 0) {
|
|
673
|
+
this._messages = messages;
|
|
674
|
+
this._activeToolCalls = [];
|
|
675
|
+
}
|
|
676
|
+
return messages;
|
|
677
|
+
} finally {
|
|
678
|
+
this._isLoading = false;
|
|
679
|
+
this.notify();
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Approve a pending plan.
|
|
684
|
+
*/
|
|
685
|
+
async approvePlan(planPath) {
|
|
686
|
+
await this.manager.approvePlan(planPath);
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Reject a pending plan.
|
|
690
|
+
*/
|
|
691
|
+
async rejectPlan(planPath, feedback) {
|
|
692
|
+
await this.manager.rejectPlan(planPath, feedback);
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Mark a single message as read.
|
|
696
|
+
*/
|
|
697
|
+
async markMessageAsRead(messagePath) {
|
|
698
|
+
await this.manager.markMessageAsRead(messagePath);
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Stop the current streaming response.
|
|
702
|
+
*/
|
|
703
|
+
stop() {
|
|
704
|
+
this._isStreaming = false;
|
|
705
|
+
this.clearStreamingTimer();
|
|
706
|
+
this.stopWatchdog();
|
|
707
|
+
this.notify();
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Clean up the store and release resources.
|
|
711
|
+
*/
|
|
712
|
+
destroy() {
|
|
713
|
+
this.stop();
|
|
714
|
+
this.clearStreamingTimer();
|
|
715
|
+
this.stopWatchdog();
|
|
716
|
+
this.subscription?.unsubscribe();
|
|
717
|
+
this.subscription = null;
|
|
718
|
+
this.subscribers.clear();
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Get the conversation path.
|
|
722
|
+
*/
|
|
723
|
+
getConversationPath() {
|
|
724
|
+
return this._conversationPath;
|
|
725
|
+
}
|
|
726
|
+
// ==========================================================================
|
|
727
|
+
// Internal
|
|
728
|
+
// ==========================================================================
|
|
729
|
+
ensureSubscription() {
|
|
730
|
+
if (this.subscription) return;
|
|
731
|
+
if (!this._conversationPath) return;
|
|
732
|
+
this.subscription = this.manager.subscribe(
|
|
733
|
+
this._conversationPath,
|
|
734
|
+
(event) => {
|
|
735
|
+
this.onEvent?.(event);
|
|
736
|
+
this.handleEvent(event);
|
|
737
|
+
}
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
handleEvent(event) {
|
|
741
|
+
logger.debug("[ConversationStore] SSE:", event.type);
|
|
742
|
+
if (this._isStreaming) {
|
|
743
|
+
this.resetStreamingTimer();
|
|
744
|
+
}
|
|
745
|
+
switch (event.type) {
|
|
746
|
+
case "text_chunk":
|
|
747
|
+
this.markStreamingActivity();
|
|
748
|
+
this._streamingText += event.text;
|
|
749
|
+
this.notify();
|
|
750
|
+
break;
|
|
751
|
+
case "thought_chunk":
|
|
752
|
+
this.markStreamingActivity();
|
|
753
|
+
this.notify();
|
|
754
|
+
break;
|
|
755
|
+
case "assistant_message":
|
|
756
|
+
this._messages.push(event.message);
|
|
757
|
+
this._streamingText = "";
|
|
758
|
+
this.notify();
|
|
759
|
+
break;
|
|
760
|
+
case "tool_call_started": {
|
|
761
|
+
this.markStreamingActivity();
|
|
762
|
+
logger.debug("[ConversationStore] Tool started:", event.functionName);
|
|
763
|
+
const info = {
|
|
764
|
+
id: event.toolCallId,
|
|
765
|
+
functionName: event.functionName,
|
|
766
|
+
arguments: event.arguments,
|
|
767
|
+
status: "running"
|
|
768
|
+
};
|
|
769
|
+
this._activeToolCalls.push(info);
|
|
770
|
+
this.notify();
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
case "tool_call_completed": {
|
|
774
|
+
this.markStreamingActivity();
|
|
775
|
+
logger.debug("[ConversationStore] Tool completed:", event.toolCallId);
|
|
776
|
+
const idx = this._activeToolCalls.findIndex((tc) => tc.id === event.toolCallId);
|
|
777
|
+
if (idx >= 0) {
|
|
778
|
+
this._activeToolCalls[idx] = {
|
|
779
|
+
...this._activeToolCalls[idx],
|
|
780
|
+
status: event.error ? "failed" : "completed",
|
|
781
|
+
result: event.result,
|
|
782
|
+
durationMs: event.durationMs
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
this.notify();
|
|
786
|
+
break;
|
|
787
|
+
}
|
|
788
|
+
case "waiting": {
|
|
789
|
+
logger.info("[ConversationStore] Waiting for input");
|
|
790
|
+
this.flushStreamingText(event.timestamp);
|
|
791
|
+
this._isStreaming = false;
|
|
792
|
+
this._isWaiting = true;
|
|
793
|
+
this._activeToolCalls = [];
|
|
794
|
+
this.clearStreamingTimer();
|
|
795
|
+
this.stopWatchdog();
|
|
796
|
+
this.notify();
|
|
797
|
+
this.deferredReload();
|
|
798
|
+
break;
|
|
799
|
+
}
|
|
800
|
+
case "done": {
|
|
801
|
+
if (isRecoveredDoneEvent(event)) {
|
|
802
|
+
logger.debug("[ConversationStore] Ignoring recovered done event");
|
|
803
|
+
this.reconcileRecoveredDone();
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
const doneEvent = event;
|
|
807
|
+
if (doneEvent.dispatchPhase && doneEvent.dispatchPhase !== "terminal") {
|
|
808
|
+
logger.debug("[ConversationStore] Ignoring non-terminal done event");
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
logger.info("[ConversationStore] Turn done");
|
|
812
|
+
this.flushStreamingText(event.timestamp);
|
|
813
|
+
if (doneEvent.content && !this.lastMessageIsAssistant()) {
|
|
814
|
+
this._messages.push({
|
|
815
|
+
role: doneEvent.role || "assistant",
|
|
816
|
+
content: doneEvent.content,
|
|
817
|
+
timestamp: event.timestamp,
|
|
818
|
+
senderDisplayName: doneEvent.senderDisplayName,
|
|
819
|
+
finishReason: doneEvent.finishReason
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
this._isStreaming = false;
|
|
823
|
+
this._isWaiting = true;
|
|
824
|
+
this._activeToolCalls = [];
|
|
825
|
+
this.clearStreamingTimer();
|
|
826
|
+
this.stopWatchdog();
|
|
827
|
+
this.notify();
|
|
828
|
+
this.deferredReload();
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
case "completed": {
|
|
832
|
+
this.flushStreamingText(event.timestamp);
|
|
833
|
+
if (event.messages && event.messages.length > 0) {
|
|
834
|
+
this._messages = event.messages;
|
|
835
|
+
}
|
|
836
|
+
this._isStreaming = false;
|
|
837
|
+
this._isWaiting = false;
|
|
838
|
+
this._activeToolCalls = [];
|
|
839
|
+
this.clearStreamingTimer();
|
|
840
|
+
this.stopWatchdog();
|
|
841
|
+
this.notify();
|
|
842
|
+
this.deferredReload();
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
case "failed":
|
|
846
|
+
logger.error("[ConversationStore] Turn failed", event.error);
|
|
847
|
+
this._error = event.error;
|
|
848
|
+
this._isStreaming = false;
|
|
849
|
+
this._isWaiting = false;
|
|
850
|
+
this._streamingText = "";
|
|
851
|
+
this._activeToolCalls = [];
|
|
852
|
+
this.clearStreamingTimer();
|
|
853
|
+
this.stopWatchdog();
|
|
854
|
+
this.notify();
|
|
855
|
+
break;
|
|
856
|
+
case "conversation_created":
|
|
857
|
+
this._conversationPath = event.conversationPath;
|
|
858
|
+
this.notify();
|
|
859
|
+
break;
|
|
860
|
+
case "message_saved":
|
|
861
|
+
this.silentReload().catch(() => {
|
|
862
|
+
});
|
|
863
|
+
this.notify();
|
|
864
|
+
break;
|
|
865
|
+
case "message_delivered":
|
|
866
|
+
this._messages.push(event.message);
|
|
867
|
+
this.notify();
|
|
868
|
+
break;
|
|
869
|
+
case "log": {
|
|
870
|
+
const logEvent = event;
|
|
871
|
+
const currentLevel = getLogLevel();
|
|
872
|
+
const levelMap = {
|
|
873
|
+
debug: 4 /* Debug */,
|
|
874
|
+
info: 3 /* Info */,
|
|
875
|
+
warn: 2 /* Warn */,
|
|
876
|
+
error: 1 /* Error */
|
|
877
|
+
};
|
|
878
|
+
const eventLevel = levelMap[logEvent.level] ?? 4 /* Debug */;
|
|
879
|
+
if (currentLevel >= eventLevel) {
|
|
880
|
+
const fn = logEvent.level === "error" ? console.error : logEvent.level === "warn" ? console.warn : logEvent.level === "debug" ? console.debug : console.info;
|
|
881
|
+
fn("[server]", logEvent.message);
|
|
882
|
+
}
|
|
883
|
+
break;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Agent activity observed while the store thought the turn was over.
|
|
889
|
+
*
|
|
890
|
+
* This happens when execution continues WITHOUT a user message — plan
|
|
891
|
+
* approval in approve_then_auto / step_by_step (approvePlan() returns an
|
|
892
|
+
* enqueue receipt and the agent starts executing), or agent-initiated
|
|
893
|
+
* follow-ups. The events arrive over the persistent live subscription
|
|
894
|
+
* (no history replay), so activity is always "now": re-enter streaming
|
|
895
|
+
* state so UIs show progress and the end-of-turn handling (done/waiting
|
|
896
|
+
* → deferred reload) runs again for this continuation.
|
|
897
|
+
*/
|
|
898
|
+
markStreamingActivity() {
|
|
899
|
+
if (this._isStreaming) return;
|
|
900
|
+
logger.debug("[ConversationStore] Agent activity while idle \u2014 re-entering streaming state");
|
|
901
|
+
this._isStreaming = true;
|
|
902
|
+
this._isWaiting = false;
|
|
903
|
+
this._streamingText = "";
|
|
904
|
+
this.resetStreamingTimer();
|
|
905
|
+
this.startWatchdog();
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* If accumulated streaming text hasn't been captured in an assistant_message,
|
|
909
|
+
* synthesize one.
|
|
910
|
+
*/
|
|
911
|
+
flushStreamingText(timestamp) {
|
|
912
|
+
if (this._streamingText && !this.lastMessageIsAssistant()) {
|
|
913
|
+
this._messages.push({
|
|
914
|
+
role: "assistant",
|
|
915
|
+
content: this._streamingText,
|
|
916
|
+
timestamp
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
this._streamingText = "";
|
|
920
|
+
}
|
|
921
|
+
lastMessageIsAssistant() {
|
|
922
|
+
const last = this._messages[this._messages.length - 1];
|
|
923
|
+
return last?.role === "assistant";
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* After a turn completes, reload canonical messages from the node tree
|
|
927
|
+
* to catch any events missed during SSE gaps.
|
|
928
|
+
*/
|
|
929
|
+
deferredReload() {
|
|
930
|
+
if (!this._conversationPath) return;
|
|
931
|
+
logger.debug("[ConversationStore] Scheduling deferred reload");
|
|
932
|
+
const attemptReload = (delay, retriesLeft) => {
|
|
933
|
+
setTimeout(async () => {
|
|
934
|
+
try {
|
|
935
|
+
const prevCount = this._messages.length;
|
|
936
|
+
await this.silentReload();
|
|
937
|
+
if (this._messages.length === prevCount && retriesLeft > 0) {
|
|
938
|
+
attemptReload(delay * 2, retriesLeft - 1);
|
|
939
|
+
}
|
|
940
|
+
} catch {
|
|
941
|
+
}
|
|
942
|
+
}, delay);
|
|
943
|
+
};
|
|
944
|
+
attemptReload(500, 2);
|
|
945
|
+
}
|
|
946
|
+
async silentReload() {
|
|
947
|
+
if (!this._conversationPath) return;
|
|
948
|
+
try {
|
|
949
|
+
const fresh = await this.manager.getMessages(this._conversationPath);
|
|
950
|
+
if (fresh.length === 0) return;
|
|
951
|
+
let matchCount = 0;
|
|
952
|
+
const limit = Math.min(this._messages.length, fresh.length);
|
|
953
|
+
for (let i = 0; i < limit; i++) {
|
|
954
|
+
if (this._messages[i].role !== fresh[i].role || this._messages[i].content !== fresh[i].content) break;
|
|
955
|
+
matchCount++;
|
|
956
|
+
}
|
|
957
|
+
if (matchCount === this._messages.length && fresh.length === matchCount) {
|
|
958
|
+
for (let i = 0; i < matchCount; i++) {
|
|
959
|
+
const f = fresh[i];
|
|
960
|
+
const e = this._messages[i];
|
|
961
|
+
if (f.id) e.id = f.id;
|
|
962
|
+
if (f.path) e.path = f.path;
|
|
963
|
+
if (f.messageType) e.messageType = f.messageType;
|
|
964
|
+
if (f.data) e.data = f.data;
|
|
965
|
+
if (f.children) e.children = f.children;
|
|
966
|
+
if (f.finishReason) e.finishReason = f.finishReason;
|
|
967
|
+
}
|
|
968
|
+
await this.rebuildToolCallState();
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
if (matchCount === this._messages.length && fresh.length > matchCount) {
|
|
972
|
+
for (let i = matchCount; i < fresh.length; i++) {
|
|
973
|
+
this._messages.push(fresh[i]);
|
|
974
|
+
}
|
|
975
|
+
for (let i = 0; i < matchCount; i++) {
|
|
976
|
+
const f = fresh[i];
|
|
977
|
+
const e = this._messages[i];
|
|
978
|
+
if (f.id) e.id = f.id;
|
|
979
|
+
if (f.path) e.path = f.path;
|
|
980
|
+
if (f.messageType) e.messageType = f.messageType;
|
|
981
|
+
if (f.data) e.data = f.data;
|
|
982
|
+
if (f.children) e.children = f.children;
|
|
983
|
+
}
|
|
984
|
+
await this.rebuildToolCallState();
|
|
985
|
+
this.notify();
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
if (matchCount === fresh.length && fresh.length < this._messages.length) {
|
|
989
|
+
for (let i = 0; i < matchCount; i++) {
|
|
990
|
+
const f = fresh[i];
|
|
991
|
+
const e = this._messages[i];
|
|
992
|
+
if (f.id) e.id = f.id;
|
|
993
|
+
if (f.path) e.path = f.path;
|
|
994
|
+
if (f.messageType) e.messageType = f.messageType;
|
|
995
|
+
if (f.data) e.data = f.data;
|
|
996
|
+
if (f.children) e.children = f.children;
|
|
997
|
+
}
|
|
998
|
+
await this.rebuildToolCallState();
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
this._messages = fresh;
|
|
1002
|
+
await this.rebuildToolCallState();
|
|
1003
|
+
this.notify();
|
|
1004
|
+
} catch (err) {
|
|
1005
|
+
logger.debug("[ConversationStore] Silent reload failed:", err);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
// ==========================================================================
|
|
1009
|
+
// Streaming timeout (Fix A) — auto-recover on SSE inactivity
|
|
1010
|
+
// ==========================================================================
|
|
1011
|
+
resetStreamingTimer() {
|
|
1012
|
+
this.clearStreamingTimer();
|
|
1013
|
+
if (!this._isStreaming) return;
|
|
1014
|
+
this._streamingTimer = setTimeout(() => {
|
|
1015
|
+
if (this._isStreaming) {
|
|
1016
|
+
logger.warn("[ConversationStore] Streaming timeout \u2014 no SSE activity for", this._streamingTimeoutMs, "ms");
|
|
1017
|
+
this.recoverFromHang();
|
|
1018
|
+
}
|
|
1019
|
+
}, this._streamingTimeoutMs);
|
|
1020
|
+
}
|
|
1021
|
+
clearStreamingTimer() {
|
|
1022
|
+
if (this._streamingTimer) {
|
|
1023
|
+
clearTimeout(this._streamingTimer);
|
|
1024
|
+
this._streamingTimer = null;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
// ==========================================================================
|
|
1028
|
+
// Activity watchdog (Fix B) — periodic backend health check
|
|
1029
|
+
// ==========================================================================
|
|
1030
|
+
startWatchdog() {
|
|
1031
|
+
this.stopWatchdog();
|
|
1032
|
+
if (!this._conversationPath) return;
|
|
1033
|
+
this._watchdogTimer = setInterval(async () => {
|
|
1034
|
+
if (!this._isStreaming || !this._conversationPath) {
|
|
1035
|
+
this.stopWatchdog();
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
try {
|
|
1039
|
+
const health = await this.manager.checkTurnHealth(this._conversationPath);
|
|
1040
|
+
if (health === "done" && this._isStreaming) {
|
|
1041
|
+
logger.warn("[ConversationStore] Watchdog detected completed turn while still streaming \u2014 recovering");
|
|
1042
|
+
this.recoverFromHang();
|
|
1043
|
+
}
|
|
1044
|
+
} catch {
|
|
1045
|
+
}
|
|
1046
|
+
}, this._watchdogIntervalMs);
|
|
1047
|
+
}
|
|
1048
|
+
stopWatchdog() {
|
|
1049
|
+
if (this._watchdogTimer) {
|
|
1050
|
+
clearInterval(this._watchdogTimer);
|
|
1051
|
+
this._watchdogTimer = null;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
// ==========================================================================
|
|
1055
|
+
// Hang recovery (shared by timeout + watchdog)
|
|
1056
|
+
// ==========================================================================
|
|
1057
|
+
recoverFromHang() {
|
|
1058
|
+
logger.info("[ConversationStore] Recovering from hang");
|
|
1059
|
+
this.clearStreamingTimer();
|
|
1060
|
+
this.stopWatchdog();
|
|
1061
|
+
this._isStreaming = false;
|
|
1062
|
+
this._isWaiting = true;
|
|
1063
|
+
this._streamingText = "";
|
|
1064
|
+
this.notify();
|
|
1065
|
+
this.silentReload().catch(() => {
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Reconcile a recovered done event against persisted state.
|
|
1070
|
+
* If the backend turn is actually complete, finalize the local state.
|
|
1071
|
+
* Otherwise keep streaming active and let normal events continue.
|
|
1072
|
+
*/
|
|
1073
|
+
reconcileRecoveredDone() {
|
|
1074
|
+
if (!this._conversationPath) return;
|
|
1075
|
+
void this.manager.checkTurnHealth(this._conversationPath).then((health) => {
|
|
1076
|
+
if (health !== "done") return;
|
|
1077
|
+
if (!this._isStreaming) return;
|
|
1078
|
+
this._isStreaming = false;
|
|
1079
|
+
this._isWaiting = true;
|
|
1080
|
+
this._activeToolCalls = [];
|
|
1081
|
+
this.clearStreamingTimer();
|
|
1082
|
+
this.stopWatchdog();
|
|
1083
|
+
this.notify();
|
|
1084
|
+
this.deferredReload();
|
|
1085
|
+
}).catch(() => {
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
// ==========================================================================
|
|
1089
|
+
// Tool call state rebuild (Fix C) — reconstruct from node tree on reload
|
|
1090
|
+
// ==========================================================================
|
|
1091
|
+
async rebuildToolCallState() {
|
|
1092
|
+
if (!this._conversationPath) {
|
|
1093
|
+
this._activeToolCalls = [];
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
try {
|
|
1097
|
+
const pendingCalls = await this.manager.getActiveToolCalls(this._conversationPath);
|
|
1098
|
+
if (pendingCalls.length > 0) {
|
|
1099
|
+
this._activeToolCalls = pendingCalls.map((tc) => ({
|
|
1100
|
+
id: tc.id,
|
|
1101
|
+
functionName: tc.name,
|
|
1102
|
+
arguments: void 0,
|
|
1103
|
+
status: "running"
|
|
1104
|
+
}));
|
|
1105
|
+
if (!this._isStreaming) {
|
|
1106
|
+
this._isStreaming = true;
|
|
1107
|
+
this.resetStreamingTimer();
|
|
1108
|
+
this.startWatchdog();
|
|
1109
|
+
}
|
|
1110
|
+
} else {
|
|
1111
|
+
this._activeToolCalls = [];
|
|
1112
|
+
if (this._isStreaming) {
|
|
1113
|
+
const lastMsg = this._messages[this._messages.length - 1];
|
|
1114
|
+
const isLastAssistantDone = lastMsg?.role === "assistant" && (lastMsg.dispatchPhase ? lastMsg.dispatchPhase === "terminal" : !!lastMsg.finishReason && lastMsg.finishReason !== "tool_calls");
|
|
1115
|
+
if (isLastAssistantDone) {
|
|
1116
|
+
this._isStreaming = false;
|
|
1117
|
+
this._isWaiting = true;
|
|
1118
|
+
this.clearStreamingTimer();
|
|
1119
|
+
this.stopWatchdog();
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
} catch {
|
|
1124
|
+
this._activeToolCalls = [];
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
notify() {
|
|
1128
|
+
const snapshot = this.getSnapshot();
|
|
1129
|
+
for (const sub of this.subscribers) {
|
|
1130
|
+
try {
|
|
1131
|
+
sub(snapshot);
|
|
1132
|
+
} catch (err) {
|
|
1133
|
+
logger.error("Error in conversation store subscriber:", err);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
// src/utils/home-path.ts
|
|
1140
|
+
var ACCESS_CONTROL_PREFIX = "/raisin:access_control";
|
|
1141
|
+
function normalizeHomePath(home) {
|
|
1142
|
+
if (home === null || home === void 0) return null;
|
|
1143
|
+
let path = home.trim();
|
|
1144
|
+
if (path.length === 0) return null;
|
|
1145
|
+
if (path === ACCESS_CONTROL_PREFIX) return "/";
|
|
1146
|
+
if (path.startsWith(`${ACCESS_CONTROL_PREFIX}/`)) {
|
|
1147
|
+
path = path.slice(ACCESS_CONTROL_PREFIX.length);
|
|
1148
|
+
}
|
|
1149
|
+
while (path.length > 1 && path.endsWith("/")) {
|
|
1150
|
+
path = path.slice(0, -1);
|
|
1151
|
+
}
|
|
1152
|
+
return path;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// src/stores/conversation-list-store.ts
|
|
1156
|
+
function buildChatsSubscriptionFilters(userHome) {
|
|
1157
|
+
const home = normalizeHomePath(userHome) ?? userHome;
|
|
1158
|
+
return {
|
|
1159
|
+
workspace: "raisin:access_control",
|
|
1160
|
+
path: `${home}/inbox/chats/**`,
|
|
1161
|
+
event_types: ["node:created", "node:updated"]
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
var ConversationListStore = class {
|
|
1165
|
+
constructor(options) {
|
|
1166
|
+
this.subscribers = /* @__PURE__ */ new Set();
|
|
1167
|
+
// Internal state
|
|
1168
|
+
this._conversations = [];
|
|
1169
|
+
this._isLoading = false;
|
|
1170
|
+
this._error = null;
|
|
1171
|
+
// Cached ConversationStore instances
|
|
1172
|
+
this._stores = /* @__PURE__ */ new Map();
|
|
1173
|
+
// Real-time subscription
|
|
1174
|
+
this._realtimeSubscription = null;
|
|
1175
|
+
this.database = options.database;
|
|
1176
|
+
this.type = options.type;
|
|
1177
|
+
this.realtime = options.realtime ?? false;
|
|
1178
|
+
}
|
|
1179
|
+
// ==========================================================================
|
|
1180
|
+
// Subscription
|
|
1181
|
+
// ==========================================================================
|
|
1182
|
+
subscribe(callback) {
|
|
1183
|
+
this.subscribers.add(callback);
|
|
1184
|
+
callback(this.getSnapshot());
|
|
1185
|
+
return () => {
|
|
1186
|
+
this.subscribers.delete(callback);
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
getSnapshot() {
|
|
1190
|
+
return {
|
|
1191
|
+
conversations: [...this._conversations],
|
|
1192
|
+
totalUnreadCount: this._conversations.reduce((sum, c) => sum + (c.unreadCount ?? 0), 0),
|
|
1193
|
+
isLoading: this._isLoading,
|
|
1194
|
+
error: this._error
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
// ==========================================================================
|
|
1198
|
+
// Actions
|
|
1199
|
+
// ==========================================================================
|
|
1200
|
+
/**
|
|
1201
|
+
* Load conversations from the server.
|
|
1202
|
+
*/
|
|
1203
|
+
async load() {
|
|
1204
|
+
this._isLoading = true;
|
|
1205
|
+
this._error = null;
|
|
1206
|
+
this.notify();
|
|
1207
|
+
try {
|
|
1208
|
+
const conversations = await this.database.conversations.list({
|
|
1209
|
+
type: this.type
|
|
1210
|
+
});
|
|
1211
|
+
logger.debug(`[ConversationListStore] Loaded ${conversations.length} conversations`);
|
|
1212
|
+
this._conversations = conversations;
|
|
1213
|
+
if (this.realtime) {
|
|
1214
|
+
this.startRealtimeSubscription();
|
|
1215
|
+
}
|
|
1216
|
+
} catch (err) {
|
|
1217
|
+
logger.error("[ConversationListStore] Failed to load conversations", err);
|
|
1218
|
+
this._error = err instanceof Error ? err.message : String(err);
|
|
1219
|
+
} finally {
|
|
1220
|
+
this._isLoading = false;
|
|
1221
|
+
this.notify();
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* Create a new conversation.
|
|
1226
|
+
*/
|
|
1227
|
+
async createConversation(options) {
|
|
1228
|
+
const convo = await this.database.conversations.create(options);
|
|
1229
|
+
const listItem = {
|
|
1230
|
+
id: convo.id,
|
|
1231
|
+
type: convo.type,
|
|
1232
|
+
conversationPath: convo.conversationPath,
|
|
1233
|
+
conversationWorkspace: convo.conversationWorkspace,
|
|
1234
|
+
agentRef: convo.agentRef,
|
|
1235
|
+
participants: convo.participants,
|
|
1236
|
+
unreadCount: 0,
|
|
1237
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1238
|
+
};
|
|
1239
|
+
this._conversations = [listItem, ...this._conversations];
|
|
1240
|
+
this.notify();
|
|
1241
|
+
return listItem;
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Delete a conversation.
|
|
1245
|
+
*/
|
|
1246
|
+
async deleteConversation(conversationPath) {
|
|
1247
|
+
const store = this._stores.get(conversationPath);
|
|
1248
|
+
if (store) {
|
|
1249
|
+
store.destroy();
|
|
1250
|
+
this._stores.delete(conversationPath);
|
|
1251
|
+
}
|
|
1252
|
+
await this.database.conversations.delete(conversationPath);
|
|
1253
|
+
this._conversations = this._conversations.filter((c) => c.conversationPath !== conversationPath);
|
|
1254
|
+
this.notify();
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Mark a conversation as read.
|
|
1258
|
+
*/
|
|
1259
|
+
async markAsRead(conversationPath) {
|
|
1260
|
+
await this.database.conversations.markAsRead(conversationPath);
|
|
1261
|
+
this._conversations = this._conversations.map(
|
|
1262
|
+
(c) => c.conversationPath === conversationPath ? { ...c, unreadCount: 0 } : c
|
|
1263
|
+
);
|
|
1264
|
+
this.notify();
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Get or create a ConversationStore for a specific conversation.
|
|
1268
|
+
* Caches instances so repeated calls return the same store.
|
|
1269
|
+
*/
|
|
1270
|
+
getConversationStore(conversationPath, overrides) {
|
|
1271
|
+
let store = this._stores.get(conversationPath);
|
|
1272
|
+
if (!store) {
|
|
1273
|
+
store = new ConversationStore({
|
|
1274
|
+
database: this.database,
|
|
1275
|
+
conversationPath,
|
|
1276
|
+
...overrides
|
|
1277
|
+
});
|
|
1278
|
+
this._stores.set(conversationPath, store);
|
|
1279
|
+
}
|
|
1280
|
+
return store;
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Clean up all resources.
|
|
1284
|
+
*/
|
|
1285
|
+
destroy() {
|
|
1286
|
+
this._realtimeSubscription?.unsubscribe();
|
|
1287
|
+
this._realtimeSubscription = null;
|
|
1288
|
+
for (const store of this._stores.values()) {
|
|
1289
|
+
store.destroy();
|
|
1290
|
+
}
|
|
1291
|
+
this._stores.clear();
|
|
1292
|
+
this.subscribers.clear();
|
|
1293
|
+
}
|
|
1294
|
+
// ==========================================================================
|
|
1295
|
+
// Internal
|
|
1296
|
+
// ==========================================================================
|
|
1297
|
+
async startRealtimeSubscription() {
|
|
1298
|
+
if (this._realtimeSubscription) return;
|
|
1299
|
+
logger.debug("[ConversationListStore] Starting realtime subscription");
|
|
1300
|
+
try {
|
|
1301
|
+
const workspace = this.database.workspace("raisin:access_control");
|
|
1302
|
+
const events = workspace.events();
|
|
1303
|
+
const result = await this.database.executeSql(
|
|
1304
|
+
`SELECT RAISIN_CURRENT_USER()->>'path'::String as home`
|
|
1305
|
+
);
|
|
1306
|
+
const rows = result.rows;
|
|
1307
|
+
const userHome = rows?.[0]?.home;
|
|
1308
|
+
if (!userHome) return;
|
|
1309
|
+
this._realtimeSubscription = await events.subscribe(
|
|
1310
|
+
buildChatsSubscriptionFilters(userHome),
|
|
1311
|
+
async (event) => {
|
|
1312
|
+
const eventData = event;
|
|
1313
|
+
const nodeId = eventData.payload?.node_id;
|
|
1314
|
+
if (!nodeId) return;
|
|
1315
|
+
try {
|
|
1316
|
+
const nodeResult = await this.database.executeSql(
|
|
1317
|
+
`SELECT id, path, node_type, properties, created_at, updated_at FROM 'raisin:access_control' WHERE id = $1 LIMIT 1`,
|
|
1318
|
+
[nodeId]
|
|
1319
|
+
);
|
|
1320
|
+
const row = nodeResult.rows?.[0];
|
|
1321
|
+
if (!row || row.node_type !== "raisin:Conversation") return;
|
|
1322
|
+
const convPath = row.path;
|
|
1323
|
+
const props = row.properties ?? {};
|
|
1324
|
+
const item = {
|
|
1325
|
+
id: row.id,
|
|
1326
|
+
type: props.conversation_type ?? "ai_chat",
|
|
1327
|
+
conversationPath: convPath,
|
|
1328
|
+
conversationWorkspace: "raisin:access_control",
|
|
1329
|
+
agentRef: props.agent_ref,
|
|
1330
|
+
participants: props.participants,
|
|
1331
|
+
unreadCount: props.unread_count ?? 0,
|
|
1332
|
+
lastMessage: props.last_message,
|
|
1333
|
+
updatedAt: row.updated_at ?? row.created_at
|
|
1334
|
+
};
|
|
1335
|
+
const existingIdx = this._conversations.findIndex((c) => c.conversationPath === convPath);
|
|
1336
|
+
if (existingIdx >= 0) {
|
|
1337
|
+
this._conversations.splice(existingIdx, 1);
|
|
1338
|
+
}
|
|
1339
|
+
this._conversations = [item, ...this._conversations];
|
|
1340
|
+
this.notify();
|
|
1341
|
+
} catch (err) {
|
|
1342
|
+
logger.debug("Failed to process conversation event:", err);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
);
|
|
1346
|
+
} catch (err) {
|
|
1347
|
+
logger.debug("Failed to start realtime subscription:", err);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
notify() {
|
|
1351
|
+
const snapshot = this.getSnapshot();
|
|
1352
|
+
for (const sub of this.subscribers) {
|
|
1353
|
+
try {
|
|
1354
|
+
sub(snapshot);
|
|
1355
|
+
} catch (err) {
|
|
1356
|
+
logger.error("Error in conversation list store subscriber:", err);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
|
|
1362
|
+
// src/integrations/vue/vue-conversation.ts
|
|
1363
|
+
function createUseConversation(vue) {
|
|
1364
|
+
return function useConversation(options) {
|
|
1365
|
+
const store = new ConversationStore(options);
|
|
1366
|
+
const snapshot = vue.ref(store.getSnapshot());
|
|
1367
|
+
const unsubscribe = store.subscribe((s) => {
|
|
1368
|
+
snapshot.value = s;
|
|
1369
|
+
});
|
|
1370
|
+
vue.onUnmounted(() => {
|
|
1371
|
+
unsubscribe();
|
|
1372
|
+
store.destroy();
|
|
1373
|
+
});
|
|
1374
|
+
return {
|
|
1375
|
+
conversation: vue.computed(() => snapshot.value.conversation),
|
|
1376
|
+
messages: vue.computed(() => snapshot.value.messages),
|
|
1377
|
+
isStreaming: vue.computed(() => snapshot.value.isStreaming),
|
|
1378
|
+
isWaiting: vue.computed(() => snapshot.value.isWaiting),
|
|
1379
|
+
streamingText: vue.computed(() => snapshot.value.streamingText),
|
|
1380
|
+
error: vue.computed(() => snapshot.value.error),
|
|
1381
|
+
activeToolCalls: vue.computed(() => snapshot.value.activeToolCalls),
|
|
1382
|
+
plans: vue.computed(() => snapshot.value.plans),
|
|
1383
|
+
isLoading: vue.computed(() => snapshot.value.isLoading),
|
|
1384
|
+
conversationPath: vue.computed(() => snapshot.value.conversationPath),
|
|
1385
|
+
sendMessage: (content) => store.sendMessage(content),
|
|
1386
|
+
approvePlan: async (planPath) => {
|
|
1387
|
+
await store.approvePlan(planPath);
|
|
1388
|
+
},
|
|
1389
|
+
rejectPlan: async (planPath, feedback) => {
|
|
1390
|
+
await store.rejectPlan(planPath, feedback);
|
|
1391
|
+
},
|
|
1392
|
+
stop: () => store.stop(),
|
|
1393
|
+
loadMessages: () => store.loadMessages(),
|
|
1394
|
+
store
|
|
1395
|
+
};
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
function createUseConversationList(vue) {
|
|
1399
|
+
return function useConversationList(options) {
|
|
1400
|
+
const store = new ConversationListStore(options);
|
|
1401
|
+
const snapshot = vue.ref(store.getSnapshot());
|
|
1402
|
+
const unsubscribe = store.subscribe((s) => {
|
|
1403
|
+
snapshot.value = s;
|
|
1404
|
+
});
|
|
1405
|
+
store.load().catch((err) => {
|
|
1406
|
+
logger.error("[useConversationList] Initial load failed:", err);
|
|
1407
|
+
});
|
|
1408
|
+
vue.onUnmounted(() => {
|
|
1409
|
+
unsubscribe();
|
|
1410
|
+
store.destroy();
|
|
1411
|
+
});
|
|
1412
|
+
return {
|
|
1413
|
+
conversations: vue.computed(() => snapshot.value.conversations),
|
|
1414
|
+
totalUnreadCount: vue.computed(() => snapshot.value.totalUnreadCount),
|
|
1415
|
+
isLoading: vue.computed(() => snapshot.value.isLoading),
|
|
1416
|
+
error: vue.computed(() => snapshot.value.error),
|
|
1417
|
+
createConversation: (opts) => store.createConversation(opts),
|
|
1418
|
+
deleteConversation: (conversationPath) => store.deleteConversation(conversationPath),
|
|
1419
|
+
markAsRead: (conversationPath) => store.markAsRead(conversationPath),
|
|
1420
|
+
refresh: () => store.load(),
|
|
1421
|
+
store
|
|
1422
|
+
};
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// src/integrations/vue/index.ts
|
|
1427
|
+
function createRaisinVue(vue) {
|
|
1428
|
+
return {
|
|
1429
|
+
useAuth: createUseAuth(vue),
|
|
1430
|
+
useConnection: createUseConnection(vue),
|
|
1431
|
+
useSql: createUseSql(vue),
|
|
1432
|
+
useSubscription: createUseSubscription(vue),
|
|
1433
|
+
useConversation: createUseConversation(vue),
|
|
1434
|
+
useConversationList: createUseConversationList(vue)
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1438
|
+
0 && (module.exports = {
|
|
1439
|
+
createRaisinVue
|
|
1440
|
+
});
|
|
1441
|
+
//# sourceMappingURL=vue.js.map
|