@olares/lares-core 0.15.4 → 0.15.6
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/files/draft-images.js +25 -0
- package/files/filename.js +6 -0
- package/files/preview-tabs.js +51 -0
- package/files/preview-workspace.js +18 -31
- package/i18n/mobile.js +222 -0
- package/i18n/preview.js +2 -0
- package/larepass/chat.js +23 -7
- package/larepass/commands.js +49 -0
- package/larepass/context-occupancy.js +30 -0
- package/larepass/host.js +17 -0
- package/larepass/mux.js +37 -0
- package/larepass/permissions.js +56 -0
- package/larepass/queue.js +33 -0
- package/larepass/references.js +101 -0
- package/larepass/rpc.js +6 -3
- package/larepass/runtime.js +403 -17
- package/larepass/settings.js +49 -0
- package/larepass/submission-settings.js +26 -0
- package/larepass/transcript.js +12 -1
- package/larepass/turn-metrics.js +142 -0
- package/larepass/unread.js +64 -0
- package/larepass/workspace.js +161 -0
- package/olares/trusted-host.js +9 -0
- package/package.json +14 -2
- package/router/session-model.js +1 -1
- package/workspace/constants.js +1 -0
- package/workspace/seed.js +2 -1
package/larepass/runtime.js
CHANGED
|
@@ -1,20 +1,38 @@
|
|
|
1
1
|
import { isAuthFailure } from "./host.js";
|
|
2
|
-
import {
|
|
2
|
+
import { listNavigableSessions, summarizeSession } from "./chat.js";
|
|
3
3
|
import { consumeMuxInbox } from "./mux.js";
|
|
4
|
+
import { permissionLine, permissionSelect } from "./permissions.js";
|
|
4
5
|
import { wrapClientResponse } from "./rpc.js";
|
|
6
|
+
import { sessionReferenceCandidates } from "./references.js";
|
|
5
7
|
import { createSessionCache } from "./session-cache.js";
|
|
6
8
|
import { foldTranscript } from "./transcript.js";
|
|
9
|
+
import { normalizeWorkspaceList, workspaceForSession } from "./workspace.js";
|
|
7
10
|
|
|
8
11
|
const DRAFT_SESSION = "@lares/draft";
|
|
12
|
+
const ACTIVITY_COALESCE_MS = 5000;
|
|
9
13
|
|
|
10
14
|
function isDraft(sessionId) {
|
|
11
15
|
return sessionId === DRAFT_SESSION;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
|
-
function
|
|
18
|
+
function composerProjections(source) {
|
|
19
|
+
const values = source?.projections?.values ?? {};
|
|
20
|
+
const permissions = permissionSelect(source);
|
|
21
|
+
return {
|
|
22
|
+
permissions: permissions ?? undefined,
|
|
23
|
+
plan: values.plan,
|
|
24
|
+
contextPressure: values.contextPressure,
|
|
25
|
+
contextBreakdown: values.contextBreakdown,
|
|
26
|
+
goal: values.goal,
|
|
27
|
+
todos: values.todos,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function viewOf(state, cache, pendingQuestions, pendingApprovals, queues) {
|
|
15
32
|
const page = cache.peek(state.sessionId);
|
|
16
33
|
const events = page?.events ?? [];
|
|
17
34
|
const snap = foldTranscript(events);
|
|
35
|
+
const session = state.sessions.find((row) => row.sessionId === state.sessionId);
|
|
18
36
|
return {
|
|
19
37
|
sessionId: isDraft(state.sessionId) ? "" : state.sessionId,
|
|
20
38
|
items: snap.items,
|
|
@@ -25,8 +43,20 @@ function viewOf(state, cache, pendingQuestions) {
|
|
|
25
43
|
phase: state.phase,
|
|
26
44
|
sessions: state.sessions,
|
|
27
45
|
sessionsReady: state.sessionsReady,
|
|
46
|
+
workspaces: state.workspaces,
|
|
47
|
+
workspacesReady: state.workspacesReady,
|
|
48
|
+
workspaceError: state.workspaceError,
|
|
49
|
+
archivedSessionIds: state.archivedSessionIds,
|
|
28
50
|
historyLoading: Boolean(state.sessionId) && !page?.ready && events.length === 0,
|
|
29
51
|
question: pendingQuestions.get(state.sessionId) ?? null,
|
|
52
|
+
approval: pendingApprovals.get(state.sessionId) ?? null,
|
|
53
|
+
queue: queues.get(state.sessionId) ?? [],
|
|
54
|
+
permissions: permissionSelect(session),
|
|
55
|
+
plan: session?.plan,
|
|
56
|
+
contextPressure: session?.contextPressure,
|
|
57
|
+
contextBreakdown: session?.contextBreakdown,
|
|
58
|
+
goal: session?.goal ?? null,
|
|
59
|
+
todos: session?.todos ?? null,
|
|
30
60
|
};
|
|
31
61
|
}
|
|
32
62
|
|
|
@@ -44,6 +74,9 @@ export function createChatRuntime(client) {
|
|
|
44
74
|
const listeners = new Set();
|
|
45
75
|
const cache = createSessionCache();
|
|
46
76
|
const pendingQuestions = new Map();
|
|
77
|
+
const pendingApprovals = new Map();
|
|
78
|
+
const queues = new Map();
|
|
79
|
+
const projectionSeqs = new Map();
|
|
47
80
|
const state = {
|
|
48
81
|
sessionId: "",
|
|
49
82
|
error: "",
|
|
@@ -51,16 +84,21 @@ export function createChatRuntime(client) {
|
|
|
51
84
|
phase: "idle",
|
|
52
85
|
sessions: [],
|
|
53
86
|
sessionsReady: false,
|
|
87
|
+
workspaces: [],
|
|
88
|
+
workspacesReady: false,
|
|
89
|
+
workspaceError: "",
|
|
90
|
+
archivedSessionIds: [],
|
|
54
91
|
};
|
|
55
92
|
let liveScroll = { top: 0, stick: true };
|
|
56
93
|
let abort = null;
|
|
57
94
|
let dead = false;
|
|
58
95
|
let muxLoop = null;
|
|
59
96
|
let listing = null;
|
|
97
|
+
let workspaceListing = null;
|
|
60
98
|
let creating = null;
|
|
61
99
|
|
|
62
100
|
const emit = () => {
|
|
63
|
-
const view = viewOf(state, cache, pendingQuestions);
|
|
101
|
+
const view = viewOf(state, cache, pendingQuestions, pendingApprovals, queues);
|
|
64
102
|
for (const listener of listeners) listener(view);
|
|
65
103
|
};
|
|
66
104
|
|
|
@@ -71,10 +109,57 @@ export function createChatRuntime(client) {
|
|
|
71
109
|
return true;
|
|
72
110
|
}
|
|
73
111
|
|
|
112
|
+
/**
|
|
113
|
+
* The mux carries events for every session, so a row that is not on screen
|
|
114
|
+
* can be dated here instead of waiting for the next `session.list` — that is
|
|
115
|
+
* what lets an unwatched session show as unseen while it works. Coalesced,
|
|
116
|
+
* because a streaming answer would otherwise re-emit on every token.
|
|
117
|
+
*/
|
|
118
|
+
function noteActivity(sessionId, now = Date.now()) {
|
|
119
|
+
if (!sessionId || sessionId === state.sessionId || isDraft(sessionId)) return false;
|
|
120
|
+
const row = state.sessions.find((item) => item.sessionId === sessionId);
|
|
121
|
+
if (!row || now - (Number(row.updatedAt) || 0) < ACTIVITY_COALESCE_MS) return false;
|
|
122
|
+
state.sessions = state.sessions.map((item) => (
|
|
123
|
+
item.sessionId === sessionId ? { ...item, updatedAt: now } : item
|
|
124
|
+
));
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function notePermissions(sessionId, select) {
|
|
129
|
+
if (!sessionId || !select) return false;
|
|
130
|
+
const row = state.sessions.find((item) => item.sessionId === sessionId);
|
|
131
|
+
if (!row || row.permissions?.currentValue === select.currentValue) return false;
|
|
132
|
+
state.sessions = state.sessions.map((item) => (
|
|
133
|
+
item.sessionId === sessionId ? { ...item, permissions: select } : item
|
|
134
|
+
));
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function noteComposerProjections(sessionId, source) {
|
|
139
|
+
if (!sessionId) return false;
|
|
140
|
+
const row = state.sessions.find((item) => item.sessionId === sessionId);
|
|
141
|
+
if (!row) return false;
|
|
142
|
+
const next = composerProjections(source);
|
|
143
|
+
const changed = Object.entries(next).some(([key, value]) => (
|
|
144
|
+
value !== undefined && JSON.stringify(row[key]) !== JSON.stringify(value)
|
|
145
|
+
));
|
|
146
|
+
if (!changed) return false;
|
|
147
|
+
state.sessions = state.sessions.map((item) => (
|
|
148
|
+
item.sessionId === sessionId
|
|
149
|
+
? {
|
|
150
|
+
...item,
|
|
151
|
+
...Object.fromEntries(Object.entries(next).filter(([, value]) => value !== undefined)),
|
|
152
|
+
}
|
|
153
|
+
: item
|
|
154
|
+
));
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
74
158
|
function ingest(sessionId, extra) {
|
|
75
159
|
const changed = cache.merge(sessionId, extra);
|
|
76
160
|
const filled = extra?.length ? noteFilled(sessionId) : false;
|
|
77
|
-
|
|
161
|
+
const moved = extra?.length ? noteActivity(sessionId) : false;
|
|
162
|
+
if (filled || moved || (changed && sessionId === state.sessionId)) emit();
|
|
78
163
|
}
|
|
79
164
|
|
|
80
165
|
function rememberListed(row) {
|
|
@@ -98,7 +183,10 @@ export function createChatRuntime(client) {
|
|
|
98
183
|
const events = history.value?.events;
|
|
99
184
|
const changed = cache.rememberHistory(sessionId, events);
|
|
100
185
|
const filled = events?.length ? noteFilled(sessionId) : false;
|
|
101
|
-
|
|
186
|
+
// The history tail carries the same projections a list row does, and it
|
|
187
|
+
// is the fresher of the two for the session being opened.
|
|
188
|
+
const projected = noteComposerProjections(sessionId, history.value);
|
|
189
|
+
if (filled || projected || (changed && sessionId === state.sessionId)) emit();
|
|
102
190
|
});
|
|
103
191
|
}
|
|
104
192
|
|
|
@@ -125,6 +213,22 @@ export function createChatRuntime(client) {
|
|
|
125
213
|
ingest(item.sessionId, [item.event]);
|
|
126
214
|
continue;
|
|
127
215
|
}
|
|
216
|
+
if (item.kind === "queue") {
|
|
217
|
+
queues.set(item.sessionId, item.items);
|
|
218
|
+
if (item.sessionId === state.sessionId) emit();
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (item.kind === "projection") {
|
|
222
|
+
const projectionKey = `${item.sessionId}\u0000${item.key}`;
|
|
223
|
+
const previousSeq = projectionSeqs.get(projectionKey) ?? -1;
|
|
224
|
+
if (Number.isFinite(item.seq) && item.seq <= previousSeq) continue;
|
|
225
|
+
if (Number.isFinite(item.seq)) projectionSeqs.set(projectionKey, item.seq);
|
|
226
|
+
state.sessions = state.sessions.map((row) => (
|
|
227
|
+
row.sessionId === item.sessionId ? { ...row, [item.key]: item.value } : row
|
|
228
|
+
));
|
|
229
|
+
if (item.sessionId === state.sessionId) emit();
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
128
232
|
if (item.kind === "question") {
|
|
129
233
|
pendingQuestions.set(item.sessionId, {
|
|
130
234
|
rpcId: item.rpcId,
|
|
@@ -133,6 +237,19 @@ export function createChatRuntime(client) {
|
|
|
133
237
|
emit();
|
|
134
238
|
continue;
|
|
135
239
|
}
|
|
240
|
+
if (item.kind === "approval") {
|
|
241
|
+
pendingApprovals.set(item.sessionId, item);
|
|
242
|
+
if (item.sessionId === state.sessionId) emit();
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (item.kind === "approval-resolved") {
|
|
246
|
+
const current = pendingApprovals.get(item.sessionId);
|
|
247
|
+
if (!current || current.approvalId === item.approvalId) {
|
|
248
|
+
pendingApprovals.delete(item.sessionId);
|
|
249
|
+
if (item.sessionId === state.sessionId) emit();
|
|
250
|
+
}
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
136
253
|
if (item.kind === "question-resolved") {
|
|
137
254
|
const current = pendingQuestions.get(item.sessionId);
|
|
138
255
|
if (!current || !item.rpcId || current.rpcId === item.rpcId) {
|
|
@@ -179,7 +296,7 @@ export function createChatRuntime(client) {
|
|
|
179
296
|
if (dead) return state.sessions;
|
|
180
297
|
if (listing) return listing;
|
|
181
298
|
listing = (async () => {
|
|
182
|
-
const listed = await
|
|
299
|
+
const listed = await listNavigableSessions(client.rpc);
|
|
183
300
|
if (listed.ok) state.sessions = listed.value.items;
|
|
184
301
|
state.sessionsReady = true;
|
|
185
302
|
emit();
|
|
@@ -190,6 +307,28 @@ export function createChatRuntime(client) {
|
|
|
190
307
|
return listing;
|
|
191
308
|
}
|
|
192
309
|
|
|
310
|
+
async function refreshWorkspaces() {
|
|
311
|
+
if (dead) return state.workspaces;
|
|
312
|
+
if (workspaceListing) return workspaceListing;
|
|
313
|
+
workspaceListing = (async () => {
|
|
314
|
+
const listed = await client.rpc("workspace.list", {});
|
|
315
|
+
if (listed.ok) {
|
|
316
|
+
const normalized = normalizeWorkspaceList(listed.value);
|
|
317
|
+
state.workspaces = normalized.items;
|
|
318
|
+
state.archivedSessionIds = normalized.archivedSessionIds;
|
|
319
|
+
state.workspaceError = "";
|
|
320
|
+
} else {
|
|
321
|
+
state.workspaceError = listed.error?.message || listed.error?.code || "workspace";
|
|
322
|
+
}
|
|
323
|
+
state.workspacesReady = true;
|
|
324
|
+
emit();
|
|
325
|
+
return state.workspaces;
|
|
326
|
+
})().finally(() => {
|
|
327
|
+
workspaceListing = null;
|
|
328
|
+
});
|
|
329
|
+
return workspaceListing;
|
|
330
|
+
}
|
|
331
|
+
|
|
193
332
|
function occupy(sessionId) {
|
|
194
333
|
state.sessionId = sessionId;
|
|
195
334
|
state.error = "";
|
|
@@ -198,15 +337,26 @@ export function createChatRuntime(client) {
|
|
|
198
337
|
emit();
|
|
199
338
|
}
|
|
200
339
|
|
|
201
|
-
function currentIsEmpty() {
|
|
340
|
+
function currentIsEmpty(workspaceId, scoped) {
|
|
202
341
|
if (!state.sessionId || isDraft(state.sessionId)) return false;
|
|
342
|
+
if (scoped) {
|
|
343
|
+
const currentWorkspace = workspaceForSession(state.workspaces, state.sessionId)?.workspaceId;
|
|
344
|
+
if ((currentWorkspace || "") !== (workspaceId || "")) return false;
|
|
345
|
+
}
|
|
203
346
|
const page = cache.peek(state.sessionId);
|
|
204
347
|
return Boolean(page?.ready && page.events.length === 0);
|
|
205
348
|
}
|
|
206
349
|
|
|
207
|
-
function reusableBlank() {
|
|
350
|
+
function reusableBlank(workspaceId, scoped) {
|
|
351
|
+
const target = state.workspaces.find((row) => row.workspaceId === workspaceId);
|
|
352
|
+
const account = scoped && workspaceId ? new Set(target?.sessionIds ?? []) : null;
|
|
353
|
+
const accounted = scoped && !workspaceId
|
|
354
|
+
? new Set(state.workspaces.flatMap((row) => row.sessionIds ?? []))
|
|
355
|
+
: null;
|
|
208
356
|
return state.sessions.find((row) => {
|
|
209
357
|
if (!row?.blank || !row.sessionId || row.sessionId === state.sessionId) return false;
|
|
358
|
+
if (account && !account.has(row.sessionId)) return false;
|
|
359
|
+
if (accounted && accounted.has(row.sessionId)) return false;
|
|
210
360
|
const page = cache.peek(row.sessionId);
|
|
211
361
|
return !page || page.events.length === 0;
|
|
212
362
|
})?.sessionId;
|
|
@@ -223,15 +373,17 @@ export function createChatRuntime(client) {
|
|
|
223
373
|
ensureMux();
|
|
224
374
|
}
|
|
225
375
|
|
|
226
|
-
async function createSession() {
|
|
376
|
+
async function createSession(workspaceId = null) {
|
|
227
377
|
if (dead) return;
|
|
228
378
|
if (creating) return creating;
|
|
379
|
+
const scoped = workspaceId !== null;
|
|
380
|
+
const targetWorkspaceId = workspaceId || undefined;
|
|
229
381
|
creating = (async () => {
|
|
230
|
-
if (currentIsEmpty()) {
|
|
382
|
+
if (currentIsEmpty(targetWorkspaceId, scoped)) {
|
|
231
383
|
emit();
|
|
232
384
|
return;
|
|
233
385
|
}
|
|
234
|
-
const blankId = reusableBlank();
|
|
386
|
+
const blankId = reusableBlank(targetWorkspaceId, scoped);
|
|
235
387
|
if (blankId) {
|
|
236
388
|
cache.readyEmpty(blankId);
|
|
237
389
|
occupy(blankId);
|
|
@@ -243,7 +395,10 @@ export function createChatRuntime(client) {
|
|
|
243
395
|
cache.readyEmpty(DRAFT_SESSION);
|
|
244
396
|
occupy(DRAFT_SESSION);
|
|
245
397
|
ensureMux();
|
|
246
|
-
const opened = await client.rpc(
|
|
398
|
+
const opened = await client.rpc(
|
|
399
|
+
"session.create",
|
|
400
|
+
targetWorkspaceId ? { workspaceId: targetWorkspaceId } : {},
|
|
401
|
+
);
|
|
247
402
|
if (dead) return;
|
|
248
403
|
if (!opened.ok) {
|
|
249
404
|
cache.drop(DRAFT_SESSION);
|
|
@@ -256,6 +411,13 @@ export function createChatRuntime(client) {
|
|
|
256
411
|
const sessionId = opened.value.sessionId;
|
|
257
412
|
cache.readyEmpty(sessionId);
|
|
258
413
|
rememberListed(summarizeSession({ ...opened.value, sessionId, blank: true }));
|
|
414
|
+
if (targetWorkspaceId) {
|
|
415
|
+
state.workspaces = state.workspaces.map((workspace) => (
|
|
416
|
+
workspace.workspaceId === targetWorkspaceId
|
|
417
|
+
? { ...workspace, sessionIds: [sessionId, ...(workspace.sessionIds ?? []).filter((id) => id !== sessionId)] }
|
|
418
|
+
: workspace
|
|
419
|
+
));
|
|
420
|
+
}
|
|
259
421
|
cache.drop(DRAFT_SESSION);
|
|
260
422
|
if (state.sessionId !== DRAFT_SESSION && state.sessionId !== sessionId) return;
|
|
261
423
|
occupy(sessionId);
|
|
@@ -267,13 +429,13 @@ export function createChatRuntime(client) {
|
|
|
267
429
|
}
|
|
268
430
|
|
|
269
431
|
return {
|
|
270
|
-
snapshot: () => viewOf(state, cache, pendingQuestions),
|
|
432
|
+
snapshot: () => viewOf(state, cache, pendingQuestions, pendingApprovals, queues),
|
|
271
433
|
subscribe(listener) {
|
|
272
434
|
listeners.add(listener);
|
|
273
|
-
listener(viewOf(state, cache, pendingQuestions));
|
|
435
|
+
listener(viewOf(state, cache, pendingQuestions, pendingApprovals, queues));
|
|
274
436
|
return () => listeners.delete(listener);
|
|
275
437
|
},
|
|
276
|
-
async start() {
|
|
438
|
+
async start(options = {}) {
|
|
277
439
|
if (dead) return;
|
|
278
440
|
if (state.phase === "starting") return;
|
|
279
441
|
if (state.phase === "live" && muxLoop) return;
|
|
@@ -292,6 +454,24 @@ export function createChatRuntime(client) {
|
|
|
292
454
|
emit();
|
|
293
455
|
return;
|
|
294
456
|
}
|
|
457
|
+
if (options.workspaces === true) {
|
|
458
|
+
await Promise.all([refreshSessions(), refreshWorkspaces()]);
|
|
459
|
+
const existing = state.sessions[0];
|
|
460
|
+
if (existing?.sessionId) {
|
|
461
|
+
state.sessionId = existing.sessionId;
|
|
462
|
+
liveScroll = cache.scroll(state.sessionId);
|
|
463
|
+
await pullHistory(state.sessionId);
|
|
464
|
+
ensureMux();
|
|
465
|
+
emit();
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const workspaceId = state.workspaces[0]?.workspaceId;
|
|
469
|
+
if (workspaceId) {
|
|
470
|
+
await createSession(workspaceId);
|
|
471
|
+
emit();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
295
475
|
const opened = await client.ensureSession();
|
|
296
476
|
if (!opened.ok) {
|
|
297
477
|
state.phase = "idle";
|
|
@@ -309,6 +489,7 @@ export function createChatRuntime(client) {
|
|
|
309
489
|
emit();
|
|
310
490
|
},
|
|
311
491
|
refreshSessions,
|
|
492
|
+
refreshWorkspaces,
|
|
312
493
|
async listSessions() {
|
|
313
494
|
if (state.sessionsReady) {
|
|
314
495
|
refreshSessions();
|
|
@@ -318,11 +499,197 @@ export function createChatRuntime(client) {
|
|
|
318
499
|
},
|
|
319
500
|
openSession,
|
|
320
501
|
createSession,
|
|
321
|
-
async
|
|
502
|
+
async searchSessions(query) {
|
|
503
|
+
return client.rpc("session.search", { query });
|
|
504
|
+
},
|
|
505
|
+
async renameSession(sessionId, title) {
|
|
506
|
+
const renamed = await client.rpc("session.rename", { sessionId, title });
|
|
507
|
+
if (!renamed.ok) return renamed;
|
|
508
|
+
state.sessions = state.sessions.map((row) => (
|
|
509
|
+
row.sessionId === sessionId ? { ...row, title: renamed.value?.title || title } : row
|
|
510
|
+
));
|
|
511
|
+
emit();
|
|
512
|
+
return renamed;
|
|
513
|
+
},
|
|
514
|
+
async forkSession(sessionId) {
|
|
515
|
+
if (!sessionId || isDraft(sessionId)) {
|
|
516
|
+
return { ok: false, error: { code: "no_session", message: "no session" } };
|
|
517
|
+
}
|
|
518
|
+
const forked = await client.rpc("session.fork", { sessionId });
|
|
519
|
+
const childId = forked.ok ? forked.value?.sessionId : "";
|
|
520
|
+
if (!childId) {
|
|
521
|
+
const failure = forked.ok
|
|
522
|
+
? { ok: false, error: { code: "fork", message: "fork returned no session" } }
|
|
523
|
+
: forked;
|
|
524
|
+
if (dead) return failure;
|
|
525
|
+
state.failed = failure.error?.message || failure.error?.code || "fork";
|
|
526
|
+
emit();
|
|
527
|
+
return failure;
|
|
528
|
+
}
|
|
529
|
+
// The child's title, order and workspace are decided host-side, so re-read
|
|
530
|
+
// whichever lists this runtime tracks rather than guessing them locally.
|
|
531
|
+
await Promise.all([refreshSessions(), state.workspacesReady ? refreshWorkspaces() : null]);
|
|
532
|
+
await openSession(childId);
|
|
533
|
+
return forked;
|
|
534
|
+
},
|
|
535
|
+
async createWorkspace(path) {
|
|
536
|
+
const created = await client.rpc("workspace.create", { path });
|
|
537
|
+
if (!created.ok) return created;
|
|
538
|
+
const workspace = created.value?.workspace;
|
|
539
|
+
if (workspace) {
|
|
540
|
+
state.workspaces = [
|
|
541
|
+
workspace,
|
|
542
|
+
...state.workspaces.filter((row) => row.workspaceId !== workspace.workspaceId),
|
|
543
|
+
];
|
|
544
|
+
state.workspacesReady = true;
|
|
545
|
+
emit();
|
|
546
|
+
}
|
|
547
|
+
return created;
|
|
548
|
+
},
|
|
549
|
+
async renameWorkspace(workspaceId, title) {
|
|
550
|
+
const renamed = await client.rpc("workspace.rename", { workspaceId, title });
|
|
551
|
+
if (!renamed.ok) return renamed;
|
|
552
|
+
const workspace = renamed.value?.workspace;
|
|
553
|
+
if (workspace) {
|
|
554
|
+
state.workspaces = state.workspaces.map((row) => (
|
|
555
|
+
row.workspaceId === workspaceId ? workspace : row
|
|
556
|
+
));
|
|
557
|
+
emit();
|
|
558
|
+
}
|
|
559
|
+
return renamed;
|
|
560
|
+
},
|
|
561
|
+
async deleteWorkspace(workspaceId) {
|
|
562
|
+
const deleted = await client.rpc("workspace.delete", { workspaceId });
|
|
563
|
+
if (!deleted.ok) return deleted;
|
|
564
|
+
state.workspaces = state.workspaces.filter((row) => row.workspaceId !== workspaceId);
|
|
565
|
+
emit();
|
|
566
|
+
return deleted;
|
|
567
|
+
},
|
|
568
|
+
async insertWorkspaceBefore(workspaceId, beforeWorkspaceId) {
|
|
569
|
+
const moved = await client.rpc("workspace.insertBefore", {
|
|
570
|
+
workspaceId,
|
|
571
|
+
...(beforeWorkspaceId ? { beforeWorkspaceId } : {}),
|
|
572
|
+
});
|
|
573
|
+
if (!moved.ok) return moved;
|
|
574
|
+
const byId = new Map(state.workspaces.map((row) => [row.workspaceId, row]));
|
|
575
|
+
state.workspaces = (moved.value?.workspaceIds ?? []).map((id) => byId.get(id)).filter(Boolean);
|
|
576
|
+
emit();
|
|
577
|
+
return moved;
|
|
578
|
+
},
|
|
579
|
+
async insertSessionBefore(workspaceId, sessionId, beforeSessionId) {
|
|
580
|
+
const moved = await client.rpc("workspace.insertSessionBefore", {
|
|
581
|
+
workspaceId,
|
|
582
|
+
sessionId,
|
|
583
|
+
...(beforeSessionId ? { beforeSessionId } : {}),
|
|
584
|
+
});
|
|
585
|
+
if (!moved.ok) return moved;
|
|
586
|
+
const workspace = moved.value?.workspace;
|
|
587
|
+
if (workspace) {
|
|
588
|
+
state.workspaces = state.workspaces.map((row) => (
|
|
589
|
+
row.workspaceId === workspaceId ? workspace : row
|
|
590
|
+
));
|
|
591
|
+
emit();
|
|
592
|
+
}
|
|
593
|
+
return moved;
|
|
594
|
+
},
|
|
595
|
+
async archiveSession(sessionId) {
|
|
596
|
+
const archived = await client.rpc("workspace.archiveSession", { sessionId });
|
|
597
|
+
if (!archived.ok) return archived;
|
|
598
|
+
state.archivedSessionIds = archived.value?.archivedSessionIds ?? state.archivedSessionIds;
|
|
599
|
+
emit();
|
|
600
|
+
return archived;
|
|
601
|
+
},
|
|
602
|
+
sessionModels(sessionId = state.sessionId) {
|
|
603
|
+
if (!sessionId || isDraft(sessionId)) {
|
|
604
|
+
return Promise.resolve({ ok: false, error: { code: "no_session", message: "no session" } });
|
|
605
|
+
}
|
|
606
|
+
return client.rpc("session.models", { sessionId });
|
|
607
|
+
},
|
|
608
|
+
selectModel(selection) {
|
|
609
|
+
const sessionId = state.sessionId;
|
|
610
|
+
if (!sessionId || isDraft(sessionId)) {
|
|
611
|
+
return Promise.resolve({ ok: false, error: { code: "no_session", message: "no session" } });
|
|
612
|
+
}
|
|
613
|
+
return client.rpc("session.selectModel", { sessionId, ...selection });
|
|
614
|
+
},
|
|
615
|
+
cancel() {
|
|
616
|
+
const sessionId = state.sessionId;
|
|
617
|
+
if (!sessionId || isDraft(sessionId)) {
|
|
618
|
+
return Promise.resolve({ ok: false, error: { code: "no_session", message: "no session" } });
|
|
619
|
+
}
|
|
620
|
+
return client.rpc("session.cancel", { sessionId });
|
|
621
|
+
},
|
|
622
|
+
updateQueue(itemId, action) {
|
|
623
|
+
const sessionId = state.sessionId;
|
|
624
|
+
if (!sessionId || isDraft(sessionId) || !itemId) {
|
|
625
|
+
return Promise.resolve({ ok: false, error: { code: "no_session", message: "no session" } });
|
|
626
|
+
}
|
|
627
|
+
return client.rpc("session.updateQueue", { sessionId, itemId, action });
|
|
628
|
+
},
|
|
629
|
+
listDirectory(path) {
|
|
630
|
+
return client.rpc("host.listDirectory", path ? { path } : {});
|
|
631
|
+
},
|
|
632
|
+
createDirectory(path, name) {
|
|
633
|
+
return client.rpc("host.createDirectory", { path, name });
|
|
634
|
+
},
|
|
635
|
+
/**
|
|
636
|
+
* The preset is switched with the host's own `/permission` command; there
|
|
637
|
+
* is no RPC for it. The projection behind the chip is recomputed host-side,
|
|
638
|
+
* so the switch shows at once and the following list read confirms it.
|
|
639
|
+
*/
|
|
640
|
+
async setPermission(value) {
|
|
641
|
+
const sessionId = state.sessionId;
|
|
642
|
+
if (!value || !sessionId || isDraft(sessionId)) {
|
|
643
|
+
return { ok: false, error: { code: "no_session", message: "no session" } };
|
|
644
|
+
}
|
|
645
|
+
const sent = await client.prompt(sessionId, permissionLine(value));
|
|
646
|
+
if (!sent.ok || dead) return sent;
|
|
647
|
+
const current = permissionSelect(state.sessions.find((row) => row.sessionId === sessionId));
|
|
648
|
+
if (current) notePermissions(sessionId, { ...current, currentValue: value });
|
|
649
|
+
emit();
|
|
650
|
+
void refreshSessions();
|
|
651
|
+
return sent;
|
|
652
|
+
},
|
|
653
|
+
listCommands() {
|
|
654
|
+
if (typeof client.commands !== "function") return Promise.resolve([]);
|
|
655
|
+
return client.commands(state.sessionId);
|
|
656
|
+
},
|
|
657
|
+
async listReferences(query, quoted = false) {
|
|
658
|
+
if (!state.sessionId || isDraft(state.sessionId)) return [];
|
|
659
|
+
const files = typeof client.references === "function"
|
|
660
|
+
? await client.references(state.sessionId, query, quoted)
|
|
661
|
+
: [];
|
|
662
|
+
if (quoted) return files.filter((row) => row.kind !== "session");
|
|
663
|
+
const sessions = sessionReferenceCandidates(state.sessions, state.sessionId, query);
|
|
664
|
+
const fileRows = files.filter((row) => row.kind !== "session");
|
|
665
|
+
return [...fileRows, ...sessions];
|
|
666
|
+
},
|
|
667
|
+
runCommand(line) {
|
|
668
|
+
if (!state.sessionId || isDraft(state.sessionId)) {
|
|
669
|
+
return Promise.resolve({ ok: false, error: { message: "no session" } });
|
|
670
|
+
}
|
|
671
|
+
return client.prompt(state.sessionId, line);
|
|
672
|
+
},
|
|
673
|
+
goalAction(kind, value) {
|
|
674
|
+
const sessionId = state.sessionId;
|
|
675
|
+
const projection = state.sessions.find((row) => row.sessionId === sessionId)?.goal;
|
|
676
|
+
const ref = projection?.goal
|
|
677
|
+
? { id: projection.goal.id, revision: projection.goal.revision }
|
|
678
|
+
: null;
|
|
679
|
+
if (!sessionId || isDraft(sessionId) || !ref || !["edit", "pause", "resume", "clear"].includes(kind)) {
|
|
680
|
+
return Promise.resolve({ ok: false, error: { code: "no-current-goal", message: "no current goal" } });
|
|
681
|
+
}
|
|
682
|
+
return client.rpc(`goal.${kind}`, {
|
|
683
|
+
sessionId,
|
|
684
|
+
ref,
|
|
685
|
+
...(kind === "edit" ? { objective: String(value ?? "") } : {}),
|
|
686
|
+
});
|
|
687
|
+
},
|
|
688
|
+
async send(content, mode = "queue") {
|
|
322
689
|
if (!state.sessionId || isDraft(state.sessionId)) {
|
|
323
690
|
return { ok: false, error: { message: "no session" } };
|
|
324
691
|
}
|
|
325
|
-
return client.prompt(state.sessionId,
|
|
692
|
+
return client.prompt(state.sessionId, content, mode);
|
|
326
693
|
},
|
|
327
694
|
async answerQuestion(answers) {
|
|
328
695
|
const pending = pendingQuestions.get(state.sessionId);
|
|
@@ -338,6 +705,24 @@ export function createChatRuntime(client) {
|
|
|
338
705
|
}
|
|
339
706
|
return { ok: true };
|
|
340
707
|
},
|
|
708
|
+
async answerApproval(outcome) {
|
|
709
|
+
const pending = pendingApprovals.get(state.sessionId);
|
|
710
|
+
if (!pending?.rpcId || !pending.approvalId || typeof client.respond !== "function") {
|
|
711
|
+
return { ok: false, error: { message: "no approval" } };
|
|
712
|
+
}
|
|
713
|
+
const receipt = await client.respond(wrapClientResponse(pending.rpcId, {
|
|
714
|
+
ok: true,
|
|
715
|
+
value: {
|
|
716
|
+
sessionId: state.sessionId,
|
|
717
|
+
approvalId: pending.approvalId,
|
|
718
|
+
outcome: outcome === "allowed-once" ? "allowed-once" : "rejected",
|
|
719
|
+
},
|
|
720
|
+
}));
|
|
721
|
+
if (!receipt?.accepted) {
|
|
722
|
+
return { ok: false, error: { message: receipt?.reason || "not-pending" } };
|
|
723
|
+
}
|
|
724
|
+
return { ok: true };
|
|
725
|
+
},
|
|
341
726
|
preview(path) {
|
|
342
727
|
if (!state.sessionId || typeof client.preview !== "function") {
|
|
343
728
|
return Promise.reject(new Error("file_preview_failed"));
|
|
@@ -386,6 +771,7 @@ export function createChatRuntime(client) {
|
|
|
386
771
|
abort?.abort();
|
|
387
772
|
muxLoop = null;
|
|
388
773
|
listing = null;
|
|
774
|
+
workspaceListing = null;
|
|
389
775
|
creating = null;
|
|
390
776
|
cache.clear();
|
|
391
777
|
pendingQuestions.clear();
|
package/larepass/settings.js
CHANGED
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
import { createSnapshotStore } from "../tools/async.js";
|
|
2
|
+
import { callRpc } from "./chat.js";
|
|
3
|
+
import {
|
|
4
|
+
CONVERSATION_SETTINGS_NAMESPACE,
|
|
5
|
+
busyEnterBehavior,
|
|
6
|
+
conversationSettings,
|
|
7
|
+
} from "./submission-settings.js";
|
|
2
8
|
|
|
3
9
|
const modelsStore = createSnapshotStore();
|
|
4
10
|
const voiceStore = createSnapshotStore();
|
|
5
11
|
const searchStore = createSnapshotStore();
|
|
12
|
+
const conversationStore = createSnapshotStore();
|
|
13
|
+
const conversationListeners = new Set();
|
|
14
|
+
|
|
15
|
+
function publishConversation(value) {
|
|
16
|
+
conversationStore.remember(value);
|
|
17
|
+
for (const listener of conversationListeners) listener(value);
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function subscribeConversationSettings(listener) {
|
|
22
|
+
conversationListeners.add(listener);
|
|
23
|
+
const current = conversationStore.peek();
|
|
24
|
+
if (current) listener(current);
|
|
25
|
+
return () => conversationListeners.delete(listener);
|
|
26
|
+
}
|
|
6
27
|
|
|
7
28
|
function fail(res, path) {
|
|
8
29
|
const err = res?.body?.error;
|
|
@@ -14,6 +35,11 @@ function payload(res, path) {
|
|
|
14
35
|
return res.body;
|
|
15
36
|
}
|
|
16
37
|
|
|
38
|
+
function rpcValue(result, method) {
|
|
39
|
+
if (result?.ok) return result.value;
|
|
40
|
+
throw new Error(result?.error?.message || result?.error?.code || method);
|
|
41
|
+
}
|
|
42
|
+
|
|
17
43
|
function sttModels(body) {
|
|
18
44
|
const list = Array.isArray(body?.stt) ? body.stt : [];
|
|
19
45
|
return list.map((item) => (typeof item === "string" ? item : item?.id)).filter(Boolean);
|
|
@@ -24,6 +50,7 @@ export function rememberedSettings() {
|
|
|
24
50
|
models: modelsStore.peek(),
|
|
25
51
|
voice: voiceStore.peek(),
|
|
26
52
|
search: searchStore.peek(),
|
|
53
|
+
conversation: conversationStore.peek(),
|
|
27
54
|
};
|
|
28
55
|
}
|
|
29
56
|
|
|
@@ -31,6 +58,7 @@ export function resetHostSettingsCache() {
|
|
|
31
58
|
modelsStore.remember(null);
|
|
32
59
|
voiceStore.remember(null);
|
|
33
60
|
searchStore.remember(null);
|
|
61
|
+
conversationStore.remember(null);
|
|
34
62
|
}
|
|
35
63
|
|
|
36
64
|
export function createHostSettings(request) {
|
|
@@ -96,5 +124,26 @@ export function createHostSettings(request) {
|
|
|
96
124
|
),
|
|
97
125
|
);
|
|
98
126
|
},
|
|
127
|
+
async conversation(options = {}) {
|
|
128
|
+
const value = await conversationStore.load(async () => {
|
|
129
|
+
const described = rpcValue(await callRpc(request, "settings.describe"), "settings.describe");
|
|
130
|
+
return conversationSettings(described);
|
|
131
|
+
}, options);
|
|
132
|
+
return publishConversation(value);
|
|
133
|
+
},
|
|
134
|
+
async setBusyEnter(value) {
|
|
135
|
+
const current = await this.conversation();
|
|
136
|
+
if (!current.writable) throw new Error("settings are read-only");
|
|
137
|
+
const next = rpcValue(await callRpc(request, "settings.update", {
|
|
138
|
+
ns: CONVERSATION_SETTINGS_NAMESPACE,
|
|
139
|
+
patch: { busyEnter: busyEnterBehavior(value) },
|
|
140
|
+
...(current.revision === undefined ? {} : { expectedRevision: current.revision }),
|
|
141
|
+
}), "settings.update");
|
|
142
|
+
return publishConversation({
|
|
143
|
+
busyEnter: busyEnterBehavior(next.value?.busyEnter),
|
|
144
|
+
revision: Number.isInteger(next.revision) ? next.revision : undefined,
|
|
145
|
+
writable: true,
|
|
146
|
+
});
|
|
147
|
+
},
|
|
99
148
|
};
|
|
100
149
|
}
|