pinomad 0.1.1 → 0.3.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/README.md +60 -180
- package/dist/{chunk-tpd3trvd.js → chunk-3qn8gbjb.js} +1 -1
- package/dist/{chunk-fpb6f9n5.js → chunk-3tf23vjq.js} +1 -1
- package/dist/{chunk-ne6a5q2j.js → chunk-4dj2vb8t.js} +1 -1
- package/dist/{chunk-y73t2jmc.js → chunk-bggesvnj.js} +253 -69
- package/dist/chunk-c155p0t0.js +154 -0
- package/dist/{chunk-9z7echn7.js → chunk-kxws8b83.js} +4 -4
- package/dist/{chunk-8h3wrn84.js → chunk-n60kkf1m.js} +4 -4
- package/dist/{chunk-hsdzb73r.js → chunk-syf0z68f.js} +10 -4
- package/dist/{chunk-zx77w1nm.js → chunk-t9y2wfk5.js} +2 -2
- package/dist/{chunk-mqbg2cbw.js → chunk-tfqc1djy.js} +1390 -749
- package/dist/pinomad.js +7 -7
- package/package.json +7 -7
- package/web/assets/index-CBXdQLua.js +383 -0
- package/web/assets/index-CG33GBxM.css +1 -0
- package/web/index.html +2 -2
- package/dist/chunk-wgn3cx5q.js +0 -56
- package/web/assets/index-B9yvK0AN.js +0 -383
- package/web/assets/index-t5dAiohJ.css +0 -1
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
HOST_STREAM_NAMES,
|
|
3
|
+
INVALID_FRAME_CLOSE_CODE,
|
|
4
|
+
PROTOCOL,
|
|
3
5
|
UNAUTHORIZED_CLOSE_CODE,
|
|
4
6
|
conversationStream,
|
|
5
|
-
docStream
|
|
6
|
-
|
|
7
|
+
docStream,
|
|
8
|
+
hostStreams,
|
|
9
|
+
hostSupportsCall,
|
|
10
|
+
hostSupportsFeature
|
|
11
|
+
} from "./chunk-c155p0t0.js";
|
|
7
12
|
|
|
8
13
|
// packages/protocol/src/remote-durable.ts
|
|
9
14
|
import { applyImmutable } from "@earendil-works/chord/delta";
|
|
@@ -125,13 +130,19 @@ class UnauthorizedError extends Error {
|
|
|
125
130
|
}
|
|
126
131
|
|
|
127
132
|
class ProtocolMismatchError extends Error {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
133
|
+
hostMajor;
|
|
134
|
+
clientMajor;
|
|
135
|
+
hostVersion;
|
|
136
|
+
direction;
|
|
137
|
+
constructor(hostMajor, clientMajor, hostVersion) {
|
|
138
|
+
const direction = hostMajor > clientMajor ? "client-older" : "host-older";
|
|
139
|
+
super(direction === "client-older" ? `Client out of date: the host speaks protocol major ${hostMajor}, this client speaks ${clientMajor}` : `Host out of date: this client speaks protocol major ${clientMajor}, the host speaks ${hostMajor}`);
|
|
132
140
|
this.name = "ProtocolMismatchError";
|
|
133
|
-
this.
|
|
134
|
-
this.
|
|
141
|
+
this.hostMajor = hostMajor;
|
|
142
|
+
this.clientMajor = clientMajor;
|
|
143
|
+
if (hostVersion !== undefined)
|
|
144
|
+
this.hostVersion = hostVersion;
|
|
145
|
+
this.direction = direction;
|
|
135
146
|
}
|
|
136
147
|
}
|
|
137
148
|
|
|
@@ -140,7 +151,8 @@ class RemoteClient {
|
|
|
140
151
|
#options;
|
|
141
152
|
#listeners = new Set;
|
|
142
153
|
#values = new Map;
|
|
143
|
-
#wanted = new Set
|
|
154
|
+
#wanted = new Set;
|
|
155
|
+
#fresh = new Set;
|
|
144
156
|
#docs = [];
|
|
145
157
|
#toolPresentations = {};
|
|
146
158
|
#snapshotWaiters = new Map;
|
|
@@ -149,8 +161,13 @@ class RemoteClient {
|
|
|
149
161
|
#connection;
|
|
150
162
|
#state;
|
|
151
163
|
#current;
|
|
164
|
+
#side;
|
|
152
165
|
#nextCall = 1;
|
|
153
166
|
#nextNotice = 1;
|
|
167
|
+
#hostMinor = 0;
|
|
168
|
+
#helloModels = [];
|
|
169
|
+
#helloDefaults = {};
|
|
170
|
+
#hostVersion;
|
|
154
171
|
#attempt = 0;
|
|
155
172
|
#closed = false;
|
|
156
173
|
#notifying = false;
|
|
@@ -192,10 +209,16 @@ class RemoteClient {
|
|
|
192
209
|
return;
|
|
193
210
|
if (code === UNAUTHORIZED_CLOSE_CODE) {
|
|
194
211
|
this.#closed = true;
|
|
195
|
-
this.#update({ connection: "closed" });
|
|
212
|
+
this.#update({ connection: "closed", unauthorized: true });
|
|
196
213
|
this.#notice("error", new UnauthorizedError().message);
|
|
197
214
|
return;
|
|
198
215
|
}
|
|
216
|
+
if (code === INVALID_FRAME_CLOSE_CODE) {
|
|
217
|
+
this.#closed = true;
|
|
218
|
+
this.#update({ connection: "closed" });
|
|
219
|
+
this.#notice("error", `The host rejected a malformed frame from this client${reason === undefined ? "" : `: ${reason}`}`);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
199
222
|
this.#update({ connection: "reconnecting" });
|
|
200
223
|
const { min, max } = this.#options.reconnectDelayMs ?? { min: 200, max: 2000 };
|
|
201
224
|
const delay = Math.min(max, min * 2 ** this.#attempt++);
|
|
@@ -208,44 +231,76 @@ class RemoteClient {
|
|
|
208
231
|
this.#connection?.send(JSON.stringify(frame));
|
|
209
232
|
}
|
|
210
233
|
#receive(frame) {
|
|
234
|
+
if (frame === null || typeof frame !== "object")
|
|
235
|
+
return;
|
|
211
236
|
switch (frame.type) {
|
|
212
237
|
case "hello": {
|
|
213
|
-
|
|
214
|
-
|
|
238
|
+
const announced = frame.protocol;
|
|
239
|
+
const hostMajor = typeof announced === "number" ? announced : announced !== null && typeof announced === "object" ? announced.major : undefined;
|
|
240
|
+
if (hostMajor !== PROTOCOL.major) {
|
|
241
|
+
const mismatch = new ProtocolMismatchError(typeof hostMajor === "number" ? hostMajor : Number.NaN, PROTOCOL.major, frame.hostVersion);
|
|
215
242
|
this.#closed = true;
|
|
216
243
|
if (this.#state === undefined) {
|
|
217
244
|
this.#rejectReady(mismatch);
|
|
218
245
|
} else {
|
|
219
|
-
this.#update({
|
|
246
|
+
this.#update({
|
|
247
|
+
connection: "outdated",
|
|
248
|
+
protocolMismatch: {
|
|
249
|
+
hostMajor: mismatch.hostMajor,
|
|
250
|
+
clientMajor: mismatch.clientMajor,
|
|
251
|
+
...mismatch.hostVersion === undefined ? {} : { hostVersion: mismatch.hostVersion },
|
|
252
|
+
direction: mismatch.direction
|
|
253
|
+
}
|
|
254
|
+
});
|
|
220
255
|
this.#notice("error", mismatch.message);
|
|
221
256
|
}
|
|
222
257
|
this.#connection?.close();
|
|
223
258
|
return;
|
|
224
259
|
}
|
|
260
|
+
const hostMinor = announced !== null && typeof announced === "object" && typeof announced.minor === "number" ? announced.minor : 0;
|
|
261
|
+
this.#hostMinor = hostMinor;
|
|
225
262
|
this.#attempt = 0;
|
|
226
263
|
this.#docs = frame.docs;
|
|
227
264
|
this.#toolPresentations = frame.toolPresentations;
|
|
265
|
+
this.#helloModels = frame.models;
|
|
266
|
+
this.#helloDefaults = frame.defaults ?? {};
|
|
267
|
+
this.#hostVersion = frame.hostVersion;
|
|
268
|
+
this.#fresh.clear();
|
|
269
|
+
const covered = new Set(hostStreams(hostMinor));
|
|
270
|
+
for (const name of HOST_STREAM_NAMES) {
|
|
271
|
+
if (covered.has(name)) {
|
|
272
|
+
this.#wanted.add(name);
|
|
273
|
+
} else {
|
|
274
|
+
this.#wanted.delete(name);
|
|
275
|
+
this.#values.delete(name);
|
|
276
|
+
this.#fresh.delete(name);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
228
279
|
if (this.#current !== undefined)
|
|
229
280
|
for (const stream of this.#conversationStreams(this.#current))
|
|
230
281
|
this.#wanted.add(stream);
|
|
282
|
+
if (this.#side !== undefined)
|
|
283
|
+
for (const stream of this.#conversationStreams(this.#side))
|
|
284
|
+
this.#wanted.add(stream);
|
|
231
285
|
for (const stream of this.#wanted)
|
|
232
286
|
this.#send({ type: "subscribe", stream });
|
|
233
287
|
if (this.#state !== undefined) {
|
|
234
288
|
this.#update({
|
|
235
289
|
session: frame.session,
|
|
236
|
-
models: frame.models,
|
|
237
290
|
toolPresentations: frame.toolPresentations,
|
|
238
|
-
|
|
239
|
-
|
|
291
|
+
connection: "connected",
|
|
292
|
+
host: this.#hostInfo()
|
|
240
293
|
});
|
|
294
|
+
this.#refresh();
|
|
241
295
|
} else
|
|
242
|
-
this.#awaitFirstView(frame);
|
|
296
|
+
this.#awaitFirstView(frame, hostMinor);
|
|
243
297
|
return;
|
|
244
298
|
}
|
|
245
299
|
case "snapshot":
|
|
246
300
|
if (!this.#wanted.has(frame.stream))
|
|
247
301
|
return;
|
|
248
302
|
this.#values.set(frame.stream, frame.value);
|
|
303
|
+
this.#fresh.add(frame.stream);
|
|
249
304
|
this.#refresh();
|
|
250
305
|
for (const waiter of this.#snapshotWaiters.get(frame.stream)?.splice(0) ?? [])
|
|
251
306
|
waiter.resolve();
|
|
@@ -259,6 +314,7 @@ class RemoteClient {
|
|
|
259
314
|
case "ended":
|
|
260
315
|
this.#wanted.delete(frame.stream);
|
|
261
316
|
this.#values.delete(frame.stream);
|
|
317
|
+
this.#fresh.delete(frame.stream);
|
|
262
318
|
for (const waiter of this.#snapshotWaiters.get(frame.stream)?.splice(0) ?? []) {
|
|
263
319
|
waiter.reject(new Error(`Stream ${frame.stream} ended: ${frame.reason}`));
|
|
264
320
|
}
|
|
@@ -278,19 +334,23 @@ class RemoteClient {
|
|
|
278
334
|
return;
|
|
279
335
|
}
|
|
280
336
|
}
|
|
281
|
-
#awaitFirstView(hello) {
|
|
282
|
-
Promise.all(
|
|
337
|
+
#awaitFirstView(hello, hostMinor) {
|
|
338
|
+
Promise.all(hostStreams(hostMinor).map((stream) => this.#snapshot(stream))).then(() => {
|
|
283
339
|
this.#state = {
|
|
284
340
|
session: hello.session,
|
|
285
341
|
organized: { chats: [], projects: [] },
|
|
286
|
-
models: hello.models,
|
|
287
|
-
defaults: hello.defaults ?? {},
|
|
342
|
+
models: this.#host("models") ?? hello.models,
|
|
343
|
+
defaults: this.#effectiveDefaults(hello.defaults ?? {}),
|
|
288
344
|
notices: [],
|
|
289
345
|
connection: "connected",
|
|
290
346
|
docs: [],
|
|
291
347
|
toolPresentations: hello.toolPresentations,
|
|
292
348
|
devices: [],
|
|
293
|
-
mcp: null
|
|
349
|
+
mcp: null,
|
|
350
|
+
auth: null,
|
|
351
|
+
settings: null,
|
|
352
|
+
agents: null,
|
|
353
|
+
host: this.#hostInfo()
|
|
294
354
|
};
|
|
295
355
|
this.#refresh();
|
|
296
356
|
this.#resolveReady({ view: this.#viewSource(), controller: this.#controller(), close: () => this.#close() });
|
|
@@ -306,28 +366,91 @@ class RemoteClient {
|
|
|
306
366
|
#conversationStreams(id) {
|
|
307
367
|
return [conversationStream(id), ...this.#docs.map((doc) => docStream(doc.kind, id))];
|
|
308
368
|
}
|
|
369
|
+
async#acquire(id) {
|
|
370
|
+
const missing = this.#conversationStreams(id).filter((stream) => !this.#wanted.has(stream));
|
|
371
|
+
const stale = this.#conversationStreams(id).filter((stream) => this.#wanted.has(stream) && !this.#fresh.has(stream));
|
|
372
|
+
const shown = Promise.all([...missing, ...stale].map((stream) => this.#snapshot(stream)));
|
|
373
|
+
for (const stream of missing) {
|
|
374
|
+
this.#wanted.add(stream);
|
|
375
|
+
this.#send({ type: "subscribe", stream });
|
|
376
|
+
}
|
|
377
|
+
await shown;
|
|
378
|
+
}
|
|
379
|
+
#release(id, other) {
|
|
380
|
+
const keep = new Set(other === undefined ? [] : this.#conversationStreams(other));
|
|
381
|
+
for (const stream of this.#conversationStreams(id)) {
|
|
382
|
+
if (keep.has(stream))
|
|
383
|
+
continue;
|
|
384
|
+
this.#wanted.delete(stream);
|
|
385
|
+
this.#values.delete(stream);
|
|
386
|
+
this.#fresh.delete(stream);
|
|
387
|
+
this.#send({ type: "unsubscribe", stream });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
#host(name) {
|
|
391
|
+
return this.#values.get(name);
|
|
392
|
+
}
|
|
393
|
+
#hostInfo() {
|
|
394
|
+
return {
|
|
395
|
+
protocol: { major: PROTOCOL.major, minor: this.#hostMinor },
|
|
396
|
+
...this.#hostVersion === undefined ? {} : { version: this.#hostVersion }
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
#effectiveDefaults(fallback) {
|
|
400
|
+
const settings = this.#host("settings");
|
|
401
|
+
if (settings === undefined)
|
|
402
|
+
return fallback;
|
|
403
|
+
return {
|
|
404
|
+
...settings.defaultModel === null ? {} : { model: settings.defaultModel },
|
|
405
|
+
...settings.defaultThinkingLevel === null ? {} : { thinkingLevel: settings.defaultThinkingLevel }
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
#descendsFrom(id, ancestor) {
|
|
409
|
+
const summaries = this.#host("conversations") ?? [];
|
|
410
|
+
const parentOf = new Map(summaries.map((summary) => [summary.id, summary.parent]));
|
|
411
|
+
let cursor = id;
|
|
412
|
+
for (let depth = 0;cursor !== undefined && depth < 8; depth++) {
|
|
413
|
+
if (cursor === ancestor)
|
|
414
|
+
return true;
|
|
415
|
+
cursor = parentOf.get(cursor);
|
|
416
|
+
}
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
#docsOf(id) {
|
|
420
|
+
return this.#docs.flatMap((doc) => {
|
|
421
|
+
const stream = docStream(doc.kind, id);
|
|
422
|
+
return this.#values.has(stream) ? [{ kind: doc.kind, presentation: doc.presentation, value: this.#values.get(stream) }] : [];
|
|
423
|
+
});
|
|
424
|
+
}
|
|
309
425
|
#refresh() {
|
|
310
426
|
if (this.#state === undefined)
|
|
311
427
|
return;
|
|
312
428
|
const current = this.#current;
|
|
429
|
+
const side = this.#side;
|
|
313
430
|
const conversation = current === undefined ? undefined : this.#values.get(conversationStream(current));
|
|
314
|
-
const summaries = this.#
|
|
315
|
-
const index = this.#
|
|
316
|
-
const devices = this.#
|
|
431
|
+
const summaries = this.#host("conversations") ?? [];
|
|
432
|
+
const index = this.#host("index") ?? { projects: [], conversations: [] };
|
|
433
|
+
const devices = this.#host("devices")?.devices ?? [];
|
|
317
434
|
const tasks = this.#wanted.has("tasks") ? this.#values.get("tasks") : undefined;
|
|
318
|
-
const docs = current === undefined ? [] : this.#docs.flatMap((doc) => {
|
|
319
|
-
const stream = docStream(doc.kind, current);
|
|
320
|
-
return this.#values.has(stream) ? [{ kind: doc.kind, presentation: doc.presentation, value: this.#values.get(stream) }] : [];
|
|
321
|
-
});
|
|
322
435
|
this.#update({
|
|
323
436
|
conversation,
|
|
437
|
+
models: this.#host("models") ?? this.#helloModels,
|
|
324
438
|
organized: organize(index, summaries),
|
|
325
439
|
home: current === undefined ? undefined : homeOf(index, summaries, current),
|
|
326
440
|
checkout: current === undefined ? undefined : checkoutOf(index, summaries, current),
|
|
327
441
|
tasks,
|
|
328
|
-
docs,
|
|
442
|
+
docs: current === undefined ? [] : this.#docsOf(current),
|
|
443
|
+
side: side === undefined ? undefined : {
|
|
444
|
+
id: side,
|
|
445
|
+
conversation: this.#values.get(conversationStream(side)),
|
|
446
|
+
docs: this.#docsOf(side)
|
|
447
|
+
},
|
|
329
448
|
devices,
|
|
330
|
-
mcp: this.#
|
|
449
|
+
mcp: this.#host("mcp") ?? null,
|
|
450
|
+
auth: this.#host("auth") ?? null,
|
|
451
|
+
settings: this.#host("settings") ?? null,
|
|
452
|
+
agents: this.#host("agents") ?? null,
|
|
453
|
+
defaults: this.#effectiveDefaults(this.#helloDefaults)
|
|
331
454
|
});
|
|
332
455
|
}
|
|
333
456
|
#update(patch) {
|
|
@@ -362,6 +485,11 @@ class RemoteClient {
|
|
|
362
485
|
this.#commands = this.#commands.then(operation).then(() => {}, (error) => this.#notice("error", error instanceof Error ? error.message : String(error)));
|
|
363
486
|
return this.#commands;
|
|
364
487
|
}
|
|
488
|
+
#request(operation) {
|
|
489
|
+
const request = this.#commands.then(operation);
|
|
490
|
+
this.#commands = request.then(() => {}, () => {});
|
|
491
|
+
return request;
|
|
492
|
+
}
|
|
365
493
|
#viewSource() {
|
|
366
494
|
return {
|
|
367
495
|
current: () => this.#state,
|
|
@@ -379,16 +507,33 @@ class RemoteClient {
|
|
|
379
507
|
return this.#current;
|
|
380
508
|
};
|
|
381
509
|
return {
|
|
382
|
-
addProject: (path) => this.#
|
|
510
|
+
addProject: (path) => this.#request(() => this.#call("addProject", { path })),
|
|
511
|
+
listFolders: (path) => this.#request(() => {
|
|
512
|
+
if (!hostSupportsCall(this.#hostMinor, "listFolders")) {
|
|
513
|
+
throw new Error("The host is too old for this (needs protocol 5.3); run `pinomad upgrade` on the host");
|
|
514
|
+
}
|
|
515
|
+
return this.#call("listFolders", path === undefined ? {} : { path });
|
|
516
|
+
}),
|
|
383
517
|
removeProject: (path) => this.#command(() => this.#call("removeProject", { path })),
|
|
518
|
+
renameConversation: (id, title) => this.#request(async () => {
|
|
519
|
+
if (!hostSupportsCall(this.#hostMinor, "renameConversation")) {
|
|
520
|
+
throw new Error("The host is too old for this (needs protocol 5.4); run `pinomad upgrade` on the host");
|
|
521
|
+
}
|
|
522
|
+
await this.#call("renameConversation", { conversationId: id, title });
|
|
523
|
+
}),
|
|
384
524
|
createConversation: (home, text, options) => this.#command(async () => {
|
|
525
|
+
const images = options?.images ?? [];
|
|
526
|
+
if (images.length > 0 && !hostSupportsFeature(this.#hostMinor, "images")) {
|
|
527
|
+
throw new Error("The host is too old for images (needs protocol 5.5); run `pinomad upgrade` on the host");
|
|
528
|
+
}
|
|
385
529
|
const { conversationId: conversationId2 } = await this.#call("createConversation", {
|
|
386
530
|
home,
|
|
387
531
|
text,
|
|
388
532
|
requestId: newRequestId(),
|
|
389
533
|
...options?.checkout === undefined ? {} : { checkout: options.checkout },
|
|
390
534
|
...options?.model === undefined ? {} : { model: options.model },
|
|
391
|
-
...options?.thinkingLevel === undefined ? {} : { thinkingLevel: options.thinkingLevel }
|
|
535
|
+
...options?.thinkingLevel === undefined ? {} : { thinkingLevel: options.thinkingLevel },
|
|
536
|
+
...images.length === 0 ? {} : { images }
|
|
392
537
|
});
|
|
393
538
|
await this.#switch(conversationId2);
|
|
394
539
|
}),
|
|
@@ -396,15 +541,30 @@ class RemoteClient {
|
|
|
396
541
|
await this.#call("archive", { conversationId: id, archived });
|
|
397
542
|
if (archived && id === this.#current)
|
|
398
543
|
this.#unshow();
|
|
544
|
+
if (archived && this.#side !== undefined && this.#descendsFrom(this.#side, id))
|
|
545
|
+
await this.#showSide(undefined);
|
|
546
|
+
}),
|
|
547
|
+
submit: (text, whenBusy, options) => this.#command(() => {
|
|
548
|
+
const images = options?.images ?? [];
|
|
549
|
+
if (images.length > 0 && !hostSupportsFeature(this.#hostMinor, "images")) {
|
|
550
|
+
throw new Error("The host is too old for images (needs protocol 5.5); run `pinomad upgrade` on the host");
|
|
551
|
+
}
|
|
552
|
+
return this.#call("submit", {
|
|
553
|
+
conversationId: options?.target ?? conversationId(),
|
|
554
|
+
text,
|
|
555
|
+
whenBusy,
|
|
556
|
+
requestId: options?.requestId ?? newRequestId(),
|
|
557
|
+
...images.length === 0 ? {} : { images }
|
|
558
|
+
});
|
|
399
559
|
}),
|
|
400
|
-
submit: (text, whenBusy) => this.#command(() => this.#call("submit", { conversationId: conversationId(), text, whenBusy, requestId: newRequestId() })),
|
|
401
560
|
compact: (instructions) => this.#command(() => this.#call("compact", { conversationId: conversationId(), ...instructions === undefined ? {} : { instructions } })),
|
|
402
|
-
abort: () => {
|
|
403
|
-
|
|
561
|
+
abort: (target) => {
|
|
562
|
+
const targetConversation = target ?? this.#current;
|
|
563
|
+
if (targetConversation === undefined) {
|
|
404
564
|
this.#notice("error", "No conversation selected");
|
|
405
565
|
return Promise.resolve();
|
|
406
566
|
}
|
|
407
|
-
return this.#call("abort", { conversationId:
|
|
567
|
+
return this.#call("abort", { conversationId: targetConversation }).then(() => {}, (error) => this.#notice("error", error instanceof Error ? error.message : String(error)));
|
|
408
568
|
},
|
|
409
569
|
setThinkingLevel: (level) => this.#command(() => this.#call("setThinkingLevel", { conversationId: conversationId(), level })),
|
|
410
570
|
setModel: (model) => this.#command(() => this.#call("setModel", { conversationId: conversationId(), model })),
|
|
@@ -421,46 +581,63 @@ class RemoteClient {
|
|
|
421
581
|
await shown;
|
|
422
582
|
}),
|
|
423
583
|
switchConversation: (id) => this.#command(() => this.#switch(id)),
|
|
424
|
-
|
|
584
|
+
showSide: (id) => this.#command(() => this.#showSide(id)),
|
|
585
|
+
fork: (entryId, prompt, removeTools, options) => this.#command(async () => {
|
|
425
586
|
const { conversationId: forked } = await this.#call("fork", {
|
|
426
587
|
conversationId: conversationId(),
|
|
427
588
|
entryId,
|
|
428
589
|
...removeTools === undefined ? {} : { removeTools }
|
|
429
590
|
});
|
|
430
|
-
|
|
591
|
+
if (options?.show === "side")
|
|
592
|
+
await this.#showSide(forked);
|
|
593
|
+
else
|
|
594
|
+
await this.#switch(forked);
|
|
431
595
|
await this.#call("submit", { conversationId: forked, text: prompt, whenBusy: "followUp", requestId: newRequestId() });
|
|
432
596
|
}),
|
|
433
|
-
answer: (kind, requestId, answers) => this.#command(() => this.#call("answer", { conversationId: conversationId(), kind, requestId, answers })),
|
|
434
|
-
changes: () => {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
this.#
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
597
|
+
answer: (kind, requestId, answers, target) => this.#command(() => this.#call("answer", { conversationId: target ?? conversationId(), kind, requestId, answers })),
|
|
598
|
+
changes: () => this.#request(() => this.#call("changes", { conversationId: conversationId() })),
|
|
599
|
+
createPairing: () => this.#request(() => this.#call("createPairing", {})),
|
|
600
|
+
revokeDevice: (publicKey) => this.#command(() => this.#call("revokeDevice", { publicKey })),
|
|
601
|
+
logout: (providerId) => this.#command(() => {
|
|
602
|
+
if (!hostSupportsCall(this.#hostMinor, "logout")) {
|
|
603
|
+
throw new Error("The host is too old for this (needs protocol 5.1); run `pinomad upgrade` on the host");
|
|
604
|
+
}
|
|
605
|
+
return this.#call("logout", { providerId });
|
|
606
|
+
}),
|
|
607
|
+
updateSettings: (patch) => this.#command(() => {
|
|
608
|
+
if (!hostSupportsCall(this.#hostMinor, "updateSettings")) {
|
|
609
|
+
throw new Error("The host is too old for this (needs protocol 5.2); run `pinomad upgrade` on the host");
|
|
610
|
+
}
|
|
611
|
+
return this.#call("updateSettings", { patch });
|
|
612
|
+
}),
|
|
613
|
+
createAgent: (role) => this.#request(async () => {
|
|
614
|
+
if (!hostSupportsCall(this.#hostMinor, "createAgent")) {
|
|
615
|
+
throw new Error("The host is too old for this (needs protocol 5.6); run `pinomad upgrade` on the host");
|
|
616
|
+
}
|
|
617
|
+
await this.#call("createAgent", { role });
|
|
618
|
+
}),
|
|
619
|
+
updateAgent: (role) => this.#request(async () => {
|
|
620
|
+
if (!hostSupportsCall(this.#hostMinor, "updateAgent")) {
|
|
621
|
+
throw new Error("The host is too old for this (needs protocol 5.6); run `pinomad upgrade` on the host");
|
|
622
|
+
}
|
|
623
|
+
await this.#call("updateAgent", { role });
|
|
624
|
+
}),
|
|
625
|
+
deleteAgent: (name) => this.#request(async () => {
|
|
626
|
+
if (!hostSupportsCall(this.#hostMinor, "deleteAgent")) {
|
|
627
|
+
throw new Error("The host is too old for this (needs protocol 5.6); run `pinomad upgrade` on the host");
|
|
628
|
+
}
|
|
629
|
+
await this.#call("deleteAgent", { name });
|
|
630
|
+
})
|
|
445
631
|
};
|
|
446
632
|
}
|
|
447
633
|
async#switch(id) {
|
|
448
634
|
const previous = this.#current;
|
|
449
635
|
if (id === previous)
|
|
450
636
|
return;
|
|
451
|
-
|
|
452
|
-
for (const stream of next)
|
|
453
|
-
this.#wanted.add(stream);
|
|
454
|
-
const shown = Promise.all(next.map((stream) => this.#snapshot(stream)));
|
|
455
|
-
for (const stream of next)
|
|
456
|
-
this.#send({ type: "subscribe", stream });
|
|
457
|
-
await shown;
|
|
637
|
+
await this.#acquire(id);
|
|
458
638
|
this.#current = id;
|
|
459
|
-
|
|
460
|
-
this.#
|
|
461
|
-
this.#values.delete(stream);
|
|
462
|
-
this.#send({ type: "unsubscribe", stream });
|
|
463
|
-
}
|
|
639
|
+
if (previous !== undefined)
|
|
640
|
+
this.#release(previous, this.#side);
|
|
464
641
|
this.#refresh();
|
|
465
642
|
}
|
|
466
643
|
#unshow() {
|
|
@@ -468,11 +645,18 @@ class RemoteClient {
|
|
|
468
645
|
if (previous === undefined)
|
|
469
646
|
return;
|
|
470
647
|
this.#current = undefined;
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
648
|
+
this.#release(previous, this.#side);
|
|
649
|
+
this.#refresh();
|
|
650
|
+
}
|
|
651
|
+
async#showSide(id) {
|
|
652
|
+
const previous = this.#side;
|
|
653
|
+
if (id === previous)
|
|
654
|
+
return;
|
|
655
|
+
if (id !== undefined)
|
|
656
|
+
await this.#acquire(id);
|
|
657
|
+
this.#side = id;
|
|
658
|
+
if (previous !== undefined)
|
|
659
|
+
this.#release(previous, this.#current);
|
|
476
660
|
this.#refresh();
|
|
477
661
|
}
|
|
478
662
|
#close() {
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// packages/protocol/src/settings.ts
|
|
2
|
+
var HOST_SETTING_KEY_SET = {
|
|
3
|
+
defaultModel: true,
|
|
4
|
+
defaultThinkingLevel: true,
|
|
5
|
+
steeringMode: true,
|
|
6
|
+
followUpMode: true
|
|
7
|
+
};
|
|
8
|
+
var HOST_SETTING_KEYS = Object.keys(HOST_SETTING_KEY_SET);
|
|
9
|
+
var THINKING_LEVEL_KEYS = { off: true, minimal: true, low: true, medium: true, high: true, xhigh: true, max: true };
|
|
10
|
+
var THINKING_LEVELS = Object.keys(THINKING_LEVEL_KEYS);
|
|
11
|
+
var isThinkingLevel = (value) => typeof value === "string" && Object.prototype.hasOwnProperty.call(THINKING_LEVEL_KEYS, value);
|
|
12
|
+
var isQueueMode = (value) => value === "all" || value === "one-at-a-time";
|
|
13
|
+
|
|
14
|
+
// packages/protocol/src/frames.ts
|
|
15
|
+
var HOST_STREAMS = { conversations: 0, index: 0, devices: 0, mcp: 0, models: 1, auth: 1, settings: 2, agents: 6 };
|
|
16
|
+
var HOST_STREAM_NAMES = Object.keys(HOST_STREAMS);
|
|
17
|
+
function hostStreams(hostMinor) {
|
|
18
|
+
return HOST_STREAM_NAMES.filter((name) => HOST_STREAMS[name] <= hostMinor);
|
|
19
|
+
}
|
|
20
|
+
var conversationStream = (id) => `conversation:${id}`;
|
|
21
|
+
var docStream = (kind, id) => `doc:${kind}:${id}`;
|
|
22
|
+
var UNAUTHORIZED_CLOSE_CODE = 4401;
|
|
23
|
+
var INVALID_FRAME_CLOSE_CODE = 4400;
|
|
24
|
+
var PROTOCOL = { major: 5, minor: 6 };
|
|
25
|
+
var CALL_SINCE = {
|
|
26
|
+
logout: 1,
|
|
27
|
+
updateSettings: 2,
|
|
28
|
+
listFolders: 3,
|
|
29
|
+
renameConversation: 4,
|
|
30
|
+
createAgent: 6,
|
|
31
|
+
updateAgent: 6,
|
|
32
|
+
deleteAgent: 6
|
|
33
|
+
};
|
|
34
|
+
function hostSupportsCall(hostMinor, method) {
|
|
35
|
+
return hostMinor >= (CALL_SINCE[method] ?? 0);
|
|
36
|
+
}
|
|
37
|
+
var FEATURE_SINCE = { images: 5 };
|
|
38
|
+
function hostSupportsFeature(hostMinor, feature) {
|
|
39
|
+
return hostMinor >= FEATURE_SINCE[feature];
|
|
40
|
+
}
|
|
41
|
+
function parseClientFrame(value) {
|
|
42
|
+
if (!record(value) || typeof value.type !== "string")
|
|
43
|
+
return { kind: "malformed" };
|
|
44
|
+
if (value.type === "subscribe" || value.type === "unsubscribe") {
|
|
45
|
+
const stream = value.stream;
|
|
46
|
+
if (typeof stream !== "string")
|
|
47
|
+
return { kind: "malformed" };
|
|
48
|
+
if (isStreamName(stream)) {
|
|
49
|
+
return { kind: "frame", frame: { type: value.type, stream } };
|
|
50
|
+
}
|
|
51
|
+
const prefix = stream.slice(0, stream.indexOf(":") === -1 ? undefined : stream.indexOf(":"));
|
|
52
|
+
if (STREAM_KINDS.has(prefix))
|
|
53
|
+
return { kind: "malformed" };
|
|
54
|
+
return { kind: "unsupportedStream", type: value.type, stream };
|
|
55
|
+
}
|
|
56
|
+
if (value.type !== "call")
|
|
57
|
+
return { kind: "unknownType" };
|
|
58
|
+
const { id, method, args } = value;
|
|
59
|
+
if (typeof id !== "number" || !Number.isSafeInteger(id) || !record(args) || typeof method !== "string") {
|
|
60
|
+
return { kind: "malformed" };
|
|
61
|
+
}
|
|
62
|
+
if (!isCallMethod(method))
|
|
63
|
+
return { kind: "unsupportedCall", id, method };
|
|
64
|
+
return validCallArgs(method, args) ? { kind: "frame", frame: { type: "call", id, method, args } } : { kind: "malformed" };
|
|
65
|
+
}
|
|
66
|
+
var STREAM_KINDS = new Set(["conversation", "doc", "tasks", ...HOST_STREAM_NAMES]);
|
|
67
|
+
function isStreamName(stream) {
|
|
68
|
+
return stream === "tasks" || HOST_STREAM_NAMES.includes(stream) || /^(?:conversation:|doc:.+:)[1-9]\d*$/.test(stream);
|
|
69
|
+
}
|
|
70
|
+
var isCallMethod = (method) => CALL_METHODS.includes(method);
|
|
71
|
+
var CALL_METHODS = [
|
|
72
|
+
"addProject",
|
|
73
|
+
"removeProject",
|
|
74
|
+
"listFolders",
|
|
75
|
+
"createConversation",
|
|
76
|
+
"archive",
|
|
77
|
+
"renameConversation",
|
|
78
|
+
"submit",
|
|
79
|
+
"abort",
|
|
80
|
+
"compact",
|
|
81
|
+
"setModel",
|
|
82
|
+
"setThinkingLevel",
|
|
83
|
+
"fork",
|
|
84
|
+
"answer",
|
|
85
|
+
"changes",
|
|
86
|
+
"createPairing",
|
|
87
|
+
"revokeDevice",
|
|
88
|
+
"logout",
|
|
89
|
+
"updateSettings",
|
|
90
|
+
"createAgent",
|
|
91
|
+
"updateAgent",
|
|
92
|
+
"deleteAgent"
|
|
93
|
+
];
|
|
94
|
+
var SETTING_VALIDATORS = {
|
|
95
|
+
defaultModel: (value) => value === null || record(value) && nonempty(value.provider) && nonempty(value.modelId),
|
|
96
|
+
defaultThinkingLevel: (value) => value === null || isThinkingLevel(value),
|
|
97
|
+
steeringMode: (value) => value === null || isQueueMode(value),
|
|
98
|
+
followUpMode: (value) => value === null || isQueueMode(value)
|
|
99
|
+
};
|
|
100
|
+
function validCallArgs(method, args) {
|
|
101
|
+
const conversationId = () => typeof args.conversationId === "number" && Number.isSafeInteger(args.conversationId) && args.conversationId >= 1;
|
|
102
|
+
const images = () => args.images === undefined || Array.isArray(args.images) && args.images.every((image) => record(image) && typeof image.data === "string" && typeof image.mimeType === "string");
|
|
103
|
+
const role = () => record(args.role) && typeof args.role.name === "string" && typeof args.role.description === "string" && typeof args.role.prompt === "string" && (args.role.model === undefined || typeof args.role.model === "string") && (args.role.thinkingLevel === undefined || typeof args.role.thinkingLevel === "string");
|
|
104
|
+
switch (method) {
|
|
105
|
+
case "submit":
|
|
106
|
+
return conversationId() && typeof args.text === "string" && (args.whenBusy === "steer" || args.whenBusy === "followUp") && nonempty(args.requestId) && images();
|
|
107
|
+
case "abort":
|
|
108
|
+
return conversationId();
|
|
109
|
+
case "setThinkingLevel":
|
|
110
|
+
return conversationId() && nonempty(args.level);
|
|
111
|
+
case "compact":
|
|
112
|
+
return conversationId() && (args.instructions === undefined || typeof args.instructions === "string");
|
|
113
|
+
case "setModel":
|
|
114
|
+
return conversationId() && record(args.model) && nonempty(args.model.provider) && nonempty(args.model.modelId);
|
|
115
|
+
case "fork":
|
|
116
|
+
return conversationId() && typeof args.entryId === "string" && /^[1-9]\d*$/.test(args.entryId) && (args.removeTools === undefined || Array.isArray(args.removeTools) && args.removeTools.every(nonempty));
|
|
117
|
+
case "answer":
|
|
118
|
+
return conversationId() && nonempty(args.kind) && nonempty(args.requestId) && Array.isArray(args.answers) && args.answers.every((answer) => record(answer) && Array.isArray(answer.selected) && answer.selected.every((l) => typeof l === "string") && (answer.other === undefined || typeof answer.other === "string"));
|
|
119
|
+
case "changes":
|
|
120
|
+
return conversationId();
|
|
121
|
+
case "addProject":
|
|
122
|
+
case "removeProject":
|
|
123
|
+
return nonempty(args.path);
|
|
124
|
+
case "listFolders":
|
|
125
|
+
return args.path === undefined || nonempty(args.path);
|
|
126
|
+
case "createConversation":
|
|
127
|
+
return record(args.home) && (args.home.kind === "chat" || args.home.kind === "project" && nonempty(args.home.path)) && typeof args.text === "string" && (nonempty(args.text) || Array.isArray(args.images) && args.images.length > 0) && nonempty(args.requestId) && images() && (args.checkout === undefined || args.checkout === "worktree" || args.checkout === "project") && (args.model === undefined || record(args.model) && nonempty(args.model.provider) && nonempty(args.model.modelId)) && (args.thinkingLevel === undefined || nonempty(args.thinkingLevel));
|
|
128
|
+
case "archive":
|
|
129
|
+
return conversationId() && typeof args.archived === "boolean";
|
|
130
|
+
case "renameConversation":
|
|
131
|
+
return conversationId() && typeof args.title === "string";
|
|
132
|
+
case "createPairing":
|
|
133
|
+
return Object.keys(args).length === 0;
|
|
134
|
+
case "revokeDevice":
|
|
135
|
+
return nonempty(args.publicKey) && /^[A-Za-z0-9_-]+$/.test(args.publicKey);
|
|
136
|
+
case "logout":
|
|
137
|
+
return nonempty(args.providerId);
|
|
138
|
+
case "createAgent":
|
|
139
|
+
case "updateAgent":
|
|
140
|
+
return role();
|
|
141
|
+
case "deleteAgent":
|
|
142
|
+
return typeof args.name === "string";
|
|
143
|
+
case "updateSettings":
|
|
144
|
+
return record(args.patch) && Object.keys(args.patch).every((key) => HOST_SETTING_KEYS.includes(key)) && Object.entries(args.patch).every(([key, value]) => SETTING_VALIDATORS[key]?.(value) === true);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function record(value) {
|
|
148
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
149
|
+
}
|
|
150
|
+
function nonempty(value) {
|
|
151
|
+
return typeof value === "string" && value.length > 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export { HOST_SETTING_KEYS, THINKING_LEVELS, isThinkingLevel, isQueueMode, HOST_STREAM_NAMES, hostStreams, conversationStream, docStream, UNAUTHORIZED_CLOSE_CODE, INVALID_FRAME_CLOSE_CODE, PROTOCOL, hostSupportsCall, hostSupportsFeature, parseClientFrame };
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import {
|
|
3
3
|
main,
|
|
4
4
|
restartService
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import"./chunk-
|
|
7
|
-
import"./chunk-
|
|
8
|
-
import"./chunk-
|
|
5
|
+
} from "./chunk-t9y2wfk5.js";
|
|
6
|
+
import"./chunk-3tf23vjq.js";
|
|
7
|
+
import"./chunk-bggesvnj.js";
|
|
8
|
+
import"./chunk-c155p0t0.js";
|
|
9
9
|
import"./chunk-cjfmfyjb.js";
|
|
10
10
|
import"./chunk-df32avw6.js";
|
|
11
11
|
export {
|