@zetaloop/chappie 0.5.0 → 1.0.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/src/session.ts DELETED
@@ -1,848 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { access } from "node:fs/promises";
3
- import { hostname } from "node:os";
4
- import { resolve } from "node:path";
5
- import type {
6
- AssistantMessage,
7
- ToolResultMessage,
8
- UserMessage,
9
- } from "@earendil-works/pi-ai";
10
- import type {
11
- ExtensionAPI,
12
- ExtensionContext,
13
- SessionEntry,
14
- } from "@earendil-works/pi-coding-agent";
15
- import { Text } from "@earendil-works/pi-tui";
16
- import { type Activity, chatLabel, source } from "./activity.ts";
17
- import type { DeliveryRecord } from "./delivery.ts";
18
- import { historyResult } from "./history.ts";
19
- import {
20
- type BrokerMessage,
21
- IpcClient,
22
- type SessionDescription,
23
- type SessionInput,
24
- type SessionInspection,
25
- type SessionRequest,
26
- type SessionResult,
27
- type SessionStatus,
28
- } from "./ipc.ts";
29
- import type { ProviderOutput } from "./provider.ts";
30
- import {
31
- type ResourceDescriptor,
32
- readSessionResource,
33
- rememberImages,
34
- resourceSessionId,
35
- } from "./resources.ts";
36
- import { copyFiles, transfer, transferResult } from "./transfer.ts";
37
-
38
- type RemoteRequest = Extract<BrokerMessage, { type: "chat" | "call" }>;
39
-
40
- interface Notice extends Activity {
41
- message: string;
42
- type: "info" | "warning" | "error";
43
- }
44
-
45
- interface SyncRequest {
46
- resolve(): void;
47
- reject(error: Error): void;
48
- }
49
-
50
- interface StoreRequest {
51
- resolve(): void;
52
- reject(error: Error): void;
53
- }
54
-
55
- interface PendingRequest {
56
- resolve(result: SessionResult): void;
57
- reject(error: Error): void;
58
- }
59
-
60
- interface HistoryRequest {
61
- request: Extract<BrokerMessage, { type: "history" }>;
62
- timeout: NodeJS.Timeout;
63
- }
64
-
65
- interface ActiveRequest {
66
- request: RemoteRequest;
67
- session: SessionDescription;
68
- message: AssistantMessage;
69
- completed: boolean;
70
- cancelled: string | undefined;
71
- toolResults: ToolResultMessage[];
72
- }
73
-
74
- export class LocalSession {
75
- readonly #pi: ExtensionAPI;
76
- readonly #agentDir: string;
77
- readonly #connect: string | undefined;
78
- readonly #syncs = new Map<number, SyncRequest>();
79
- readonly #stores = new Map<string, StoreRequest>();
80
- readonly #queue: RemoteRequest[] = [];
81
- readonly #pendingInputs = new Map<string, SessionInput>();
82
- readonly #deliveries = new Map<string, DeliveryRecord>();
83
- readonly #histories = new Map<number, HistoryRequest>();
84
- readonly #requests = new Map<number, PendingRequest>();
85
- readonly #copies = new Map<number, AbortController>();
86
- #context: ExtensionContext | undefined;
87
- #connection: IpcClient | undefined;
88
- #output: ProviderOutput | undefined;
89
- #active: ActiveRequest | undefined;
90
- #status: SessionStatus = "idle";
91
- #nextRequestId = 1;
92
- #starting = false;
93
- #sessionId: string | undefined;
94
- #inputCursor: string | null = null;
95
- #flushing = Promise.resolve();
96
-
97
- constructor(pi: ExtensionAPI, agentDir: string, connect?: string) {
98
- this.#pi = pi;
99
- this.#agentDir = agentDir;
100
- this.#connect = connect;
101
- }
102
-
103
- install(): void {
104
- this.#pi.registerEntryRenderer<Notice>(
105
- "chappie.notice",
106
- ({ data }, _options, theme) => {
107
- if (!data) return;
108
- return new Text(
109
- theme.fg(data.type === "info" ? "dim" : data.type, data.message),
110
- 1,
111
- 0,
112
- );
113
- },
114
- );
115
- this.#pi.on("session_start", (_event, context) => this.#update(context));
116
- this.#pi.on("model_select", (event, context) =>
117
- this.#update(context, event.model.provider === "chappie"),
118
- );
119
- this.#pi.on("session_info_changed", (_event, context) => {
120
- this.#context = context;
121
- void this.#sync().catch(() => {});
122
- });
123
- this.#pi.on("session_tree", (event, context) => {
124
- this.#context = context;
125
- if (context.model?.provider === "chappie") {
126
- this.#resetInputs(context, event.newLeafId);
127
- }
128
- this.#historyChanged();
129
- });
130
- // Pi persists messages after message_end handlers finish.
131
- this.#pi.on("message_start", (_event, context) => {
132
- this.#context = context;
133
- this.#historyChanged();
134
- });
135
- this.#pi.on("tool_call", (_event, context) => {
136
- this.#context = context;
137
- this.#historyChanged();
138
- });
139
- this.#pi.on("session_compact", (_event, context) => {
140
- this.#context = context;
141
- this.#historyChanged();
142
- });
143
- this.#pi.on("context", (event, context) => ({
144
- messages:
145
- context.model?.provider === "chappie"
146
- ? []
147
- : event.messages.filter(
148
- (message) =>
149
- message.role !== "custom" ||
150
- message.customType !== "chappie.request",
151
- ),
152
- }));
153
- this.#pi.on("turn_end", (event, context) =>
154
- this.#turnEnd(event.message, event.toolResults, context),
155
- );
156
- this.#pi.on("agent_settled", async (_event, context) => {
157
- this.#context = context;
158
- this.#starting = false;
159
- this.#collectInputs();
160
- this.#historyChanged();
161
- await this.#completeActive();
162
- this.#dispatch();
163
- });
164
- this.#pi.on("session_shutdown", () => this.close());
165
- }
166
-
167
- #notify(
168
- message: string,
169
- type: Notice["type"] = "info",
170
- activity: Activity = {},
171
- ): void {
172
- if (this.#context)
173
- this.#pi.appendEntry<Notice>("chappie.notice", {
174
- message,
175
- type,
176
- ...activity,
177
- });
178
- if (activity.event !== "history") this.#historyChanged();
179
- }
180
-
181
- async start(output: ProviderOutput): Promise<void> {
182
- const context = this.#context;
183
- const connection = this.#connection;
184
- if (context?.model?.provider !== "chappie" || !connection) {
185
- throw new Error("Chappie is not active for this session");
186
- }
187
- if (this.#output && !this.#output.closed) {
188
- throw new Error("Chappie already has an active provider request");
189
- }
190
-
191
- this.#starting = false;
192
- this.#output = output;
193
- try {
194
- await connection.connect();
195
- if (output.closed) return;
196
- this.#collectInputs();
197
- this.#historyChanged();
198
- await this.#completeActive();
199
- this.#status = "ready";
200
- await this.#sync();
201
- if (output.closed) return;
202
- output.begin();
203
- this.#dispatch();
204
- await output.finished;
205
- } finally {
206
- if (this.#output === output) this.#output = undefined;
207
- this.#status = this.#active ? "executing" : "idle";
208
- void this.#sync().catch(() => {});
209
- }
210
- }
211
-
212
- async transfer(
213
- ...parameters: Parameters<typeof transfer.execute>
214
- ): ReturnType<typeof transfer.execute> {
215
- const [id, args, signal, update, context] = parameters;
216
- if (!args.to) return transfer.execute(...parameters);
217
- if (args.files) throw new Error("files and to are mutually exclusive");
218
- if (args.paths.length !== args.to.paths.length)
219
- throw new Error("Source and destination counts must match");
220
- const inspected = await this.#request(
221
- { type: "inspect", sessionId: args.to.sessionId },
222
- signal,
223
- );
224
- if (!("inspection" in inspected))
225
- throw new Error("Pi session returned no environment");
226
- const exported = await transfer.execute(
227
- id,
228
- { paths: args.paths },
229
- signal,
230
- undefined,
231
- context,
232
- );
233
- update?.({
234
- content: [],
235
- details: {
236
- ...exported.details,
237
- to: {
238
- sessionId: args.to.sessionId,
239
- device: inspected.inspection.session.device,
240
- },
241
- },
242
- });
243
- const result = await this.#request(
244
- {
245
- type: "copy",
246
- sessionId: args.to.sessionId,
247
- paths: args.to.paths,
248
- resources: exported.details.resources,
249
- ...(args.overwrite !== undefined ? { overwrite: args.overwrite } : {}),
250
- },
251
- signal,
252
- );
253
- if (!("transfer" in result))
254
- throw new Error("Pi session returned no transfer result");
255
- return transferResult({ ...result.transfer, device: hostname() });
256
- }
257
-
258
- close(): void {
259
- const sessionId = this.#context?.sessionManager.getSessionId();
260
- if (sessionId && this.#connection?.connected) {
261
- void this.#connection
262
- .send({ type: "unregister", sessionId })
263
- .catch(() => {});
264
- }
265
- this.#output?.fail(new Error("Chappie session ended"), true);
266
- this.#output = undefined;
267
- this.#active = undefined;
268
- this.#queue.length = 0;
269
- this.#starting = false;
270
- this.#connection?.close();
271
- this.#connection = undefined;
272
- this.#context = undefined;
273
- this.#resetInputs();
274
- this.#cancelRequests(new Error("Chappie session ended"));
275
- this.#rejectSyncs(new Error("Chappie session ended"));
276
- this.#rejectStores(new Error("Chappie session ended"));
277
- for (const id of this.#histories.keys()) this.#finishHistory(id);
278
- }
279
-
280
- #update(
281
- context: ExtensionContext,
282
- active = context.model?.provider === "chappie",
283
- ): void {
284
- this.#context = context;
285
- if (!active) {
286
- this.close();
287
- return;
288
- }
289
- if (this.#sessionId !== context.sessionManager.getSessionId()) {
290
- this.#resetInputs(context);
291
- }
292
- if (!this.#connection) {
293
- this.#connection = new IpcClient(this.#agentDir, this.#connect, {
294
- onOpen: async () => {
295
- await this.#sync();
296
- await this.#flushDeliveries();
297
- },
298
- onMessage: (message) => this.#receive(message),
299
- onClose: (error) => {
300
- this.#cancelRequests(error);
301
- for (const id of this.#histories.keys()) this.#finishHistory(id);
302
- this.#rejectSyncs(error);
303
- this.#rejectStores(error);
304
- if (this.#output && !this.#output.closed) this.#output.fail(error);
305
- else this.#notify(error.message, "error");
306
- this.#active = undefined;
307
- this.#queue.length = 0;
308
- this.#starting = false;
309
- this.#status = "idle";
310
- },
311
- });
312
- this.#connection.start();
313
- } else {
314
- void this.#sync().catch(() => {});
315
- }
316
- }
317
-
318
- #description(): SessionDescription {
319
- const context = this.#context;
320
- if (!context) throw new Error("Chappie session is not available");
321
- const name = this.#pi.getSessionName();
322
- return {
323
- id: context.sessionManager.getSessionId(),
324
- cwd: context.cwd,
325
- device: hostname(),
326
- status: this.#status,
327
- ...(name ? { name } : {}),
328
- };
329
- }
330
-
331
- async #sync(): Promise<void> {
332
- const connection = this.#connection;
333
- if (
334
- !connection?.connected ||
335
- !this.#context ||
336
- this.#context.model?.provider !== "chappie"
337
- )
338
- return;
339
- const id = this.#nextRequestId++;
340
- const completion = Promise.withResolvers<void>();
341
- this.#syncs.set(id, completion);
342
- try {
343
- await connection.send({ type: "sync", id, session: this.#description() });
344
- await completion.promise;
345
- } finally {
346
- this.#syncs.delete(id);
347
- }
348
- }
349
-
350
- async #receive(message: BrokerMessage): Promise<void> {
351
- switch (message.type) {
352
- case "response": {
353
- const pending = this.#requests.get(message.id);
354
- if (!pending) break;
355
- if ("error" in message) pending.reject(new Error(message.error));
356
- else {
357
- const { type: _type, id: _id, ...result } = message;
358
- pending.resolve(result);
359
- }
360
- break;
361
- }
362
- case "synced":
363
- this.#syncs.get(message.id)?.resolve();
364
- break;
365
- case "stored":
366
- this.#stores.get(message.id)?.resolve();
367
- break;
368
- case "notice":
369
- if (
370
- message.sessionId === this.#context?.sessionManager.getSessionId()
371
- ) {
372
- this.#notify(message.message, "info", message.activity);
373
- }
374
- break;
375
- case "inspect":
376
- await this.#reply(message.id, message.sessionId, async () => {
377
- const globalAgents = await this.#globalAgents();
378
- return {
379
- type: "result",
380
- id: message.id,
381
- inspection: this.#inspection(),
382
- inputs: this.#inputs(),
383
- ...(globalAgents ? { globalAgents } : {}),
384
- };
385
- });
386
- break;
387
- case "history":
388
- await this.#readHistory(message);
389
- break;
390
- case "ackInputs":
391
- if (
392
- message.sessionId === this.#context?.sessionManager.getSessionId()
393
- ) {
394
- for (const id of message.ids) this.#pendingInputs.delete(id);
395
- }
396
- break;
397
- case "readResource":
398
- await this.#reply(message.id, message.sessionId, async () => ({
399
- type: "result",
400
- id: message.id,
401
- resource: await readSessionResource(
402
- message.sessionId,
403
- message.uri,
404
- message.offset,
405
- ),
406
- }));
407
- break;
408
- case "copy": {
409
- const controller = new AbortController();
410
- this.#copies.set(message.id, controller);
411
- void this.#reply(message.id, message.sessionId, async () => {
412
- const context = this.#context;
413
- if (!context) throw new Error("Chappie session is not available");
414
- const files = await copyFiles(
415
- message.paths,
416
- message.resources,
417
- context.cwd,
418
- message.overwrite === true,
419
- (resource) => this.#readChunks(resource, controller.signal),
420
- controller.signal,
421
- );
422
- return {
423
- type: "result",
424
- id: message.id,
425
- transfer: {
426
- device: hostname(),
427
- files,
428
- resources: [],
429
- to: { sessionId: message.sessionId, device: hostname() },
430
- },
431
- };
432
- })
433
- .finally(() => this.#copies.delete(message.id))
434
- .catch(() => {});
435
- break;
436
- }
437
- case "cancel": {
438
- const copying = this.#copies.get(message.id);
439
- if (copying) {
440
- copying.abort(new Error(message.reason));
441
- break;
442
- }
443
- if (this.#histories.has(message.id)) {
444
- this.#finishHistory(message.id);
445
- break;
446
- }
447
- const queued = this.#queue.findIndex(
448
- (request) => request.id === message.id,
449
- );
450
- const request =
451
- this.#queue[queued] ??
452
- (this.#active?.request.id === message.id
453
- ? this.#active.request
454
- : undefined);
455
- if (!request) break;
456
- const name =
457
- request.type === "call"
458
- ? [...new Set(request.calls.map((call) => call.name))].join(", ")
459
- : "chat";
460
- this.#notify(
461
- `${name} cancelled for ${chatLabel(request)}: ${message.reason}`,
462
- "warning",
463
- {
464
- event: "cancelled",
465
- ...source(request.chatId, request.requestId),
466
- },
467
- );
468
- if (queued !== -1) {
469
- this.#queue.splice(queued, 1);
470
- break;
471
- }
472
- const active = this.#active;
473
- if (!active) break;
474
- active.cancelled = message.reason;
475
- if (active.completed) await this.#completeActive();
476
- else this.#context?.abort();
477
- break;
478
- }
479
- case "chat":
480
- case "call":
481
- if (
482
- message.sessionId !== this.#context?.sessionManager.getSessionId()
483
- ) {
484
- await this.#sendError(
485
- message.id,
486
- "The requested Pi session is no longer active",
487
- );
488
- break;
489
- }
490
- this.#queue.push(message);
491
- this.#dispatch();
492
- break;
493
- }
494
- }
495
-
496
- async #request(
497
- request: SessionRequest,
498
- signal?: AbortSignal,
499
- ): Promise<SessionResult> {
500
- signal?.throwIfAborted();
501
- const connection = this.#connection;
502
- if (!connection?.connected) throw new Error("Chappie is not connected");
503
- const id = this.#nextRequestId++;
504
- const completion = Promise.withResolvers<SessionResult>();
505
- const onAbort = (): void => {
506
- completion.reject(signal?.reason);
507
- void connection.send({ type: "cancelRequest", id }).catch(() => {});
508
- };
509
- this.#requests.set(id, completion);
510
- signal?.addEventListener("abort", onAbort, { once: true });
511
- void connection
512
- .send({ type: "request", id, request })
513
- .catch(completion.reject);
514
- try {
515
- return await completion.promise;
516
- } finally {
517
- this.#requests.delete(id);
518
- signal?.removeEventListener("abort", onAbort);
519
- }
520
- }
521
-
522
- async *#readChunks(
523
- resource: ResourceDescriptor,
524
- signal: AbortSignal,
525
- ): AsyncGenerator<Uint8Array> {
526
- for (let offset = 0; offset < resource.size; ) {
527
- const result = await this.#request(
528
- {
529
- type: "readResource",
530
- sessionId: resourceSessionId(resource.uri),
531
- uri: resource.uri,
532
- offset,
533
- },
534
- signal,
535
- );
536
- if (!("resource" in result))
537
- throw new Error("Pi session returned no resource");
538
- const data = Buffer.from(result.resource.blob, "base64");
539
- if (data.length === 0)
540
- throw new Error(
541
- `Source ended before ${resource.size} bytes: ${resource.name}`,
542
- );
543
- yield data;
544
- offset += data.length;
545
- }
546
- }
547
-
548
- #cancelRequests(error: Error): void {
549
- for (const pending of this.#requests.values()) pending.reject(error);
550
- this.#requests.clear();
551
- for (const controller of this.#copies.values()) controller.abort(error);
552
- this.#copies.clear();
553
- }
554
-
555
- async #readHistory(
556
- request: HistoryRequest["request"],
557
- wait = request.range.wait,
558
- ): Promise<void> {
559
- try {
560
- const context = this.#context;
561
- if (context?.sessionManager.getSessionId() !== request.sessionId)
562
- throw new Error("The requested Pi session is no longer active");
563
- const history = historyResult(
564
- context.sessionManager.getBranch(),
565
- request.sessionId,
566
- request.range,
567
- );
568
- if (wait && !request.range.before && history.count === 0) {
569
- if (!this.#histories.has(request.id)) {
570
- this.#histories.set(request.id, {
571
- request,
572
- timeout: setTimeout(() => {
573
- void this.#readHistory(request, false).catch(() => {});
574
- }, 30_000),
575
- });
576
- }
577
- return;
578
- }
579
- this.#finishHistory(request.id);
580
- if (!request.range.observer) {
581
- this.#notify(
582
- `${chatLabel(request)} read history: ${history.count} entries`,
583
- "info",
584
- { event: "history", ...source(request.chatId, request.requestId) },
585
- );
586
- }
587
- await this.#connection?.send({
588
- type: "result",
589
- id: request.id,
590
- cwd: context.cwd,
591
- history,
592
- });
593
- } catch (error) {
594
- this.#finishHistory(request.id);
595
- await this.#sendError(
596
- request.id,
597
- error instanceof Error ? error.message : String(error),
598
- );
599
- }
600
- }
601
-
602
- #historyChanged(): void {
603
- for (const { request } of this.#histories.values()) {
604
- void this.#readHistory(request).catch(() => {});
605
- }
606
- }
607
-
608
- #finishHistory(id: number): void {
609
- const pending = this.#histories.get(id);
610
- if (!pending) return;
611
- clearTimeout(pending.timeout);
612
- this.#histories.delete(id);
613
- }
614
-
615
- #inspection(): SessionInspection {
616
- const activeTools = new Set(this.#pi.getActiveTools());
617
- this.#collectInputs();
618
- return {
619
- session: this.#description(),
620
- tools: this.#pi
621
- .getAllTools()
622
- .filter((tool) => activeTools.has(tool.name)),
623
- skills: this.#pi
624
- .getCommands()
625
- .filter((command) => command.source === "skill"),
626
- };
627
- }
628
-
629
- async #globalAgents(): Promise<{ path: string } | undefined> {
630
- const path = resolve(this.#agentDir, "AGENTS.md");
631
- try {
632
- await access(path);
633
- return { path };
634
- } catch (error) {
635
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
636
- throw error;
637
- }
638
- }
639
-
640
- #dispatch(): void {
641
- if (this.#active) return;
642
- const request = this.#queue[0];
643
- if (!request) return;
644
- const output = this.#output;
645
- if (!output || output.closed) {
646
- this.#wake();
647
- return;
648
- }
649
-
650
- this.#queue.shift();
651
- output.message.chappie = source(request.chatId, request.requestId);
652
- this.#active = {
653
- request,
654
- session: this.#description(),
655
- message: output.message,
656
- completed: false,
657
- cancelled: undefined,
658
- toolResults: [],
659
- };
660
- this.#status = "executing";
661
- void this.#sync().catch(() => {});
662
- if (request.type === "chat") {
663
- output.text(request.text);
664
- output.done();
665
- } else {
666
- output.toolCalls(request.calls);
667
- output.done("toolUse");
668
- }
669
- }
670
-
671
- #wake(): void {
672
- if (
673
- this.#starting ||
674
- this.#output ||
675
- this.#active ||
676
- this.#queue.length === 0 ||
677
- !this.#context?.isIdle()
678
- )
679
- return;
680
- this.#starting = true;
681
- this.#pi.sendMessage(
682
- {
683
- customType: "chappie.request",
684
- content: "",
685
- display: false,
686
- },
687
- { triggerTurn: true },
688
- );
689
- }
690
-
691
- async #turnEnd(
692
- message: unknown,
693
- toolResults: ToolResultMessage[],
694
- context: ExtensionContext,
695
- ): Promise<void> {
696
- this.#context = context;
697
- this.#historyChanged();
698
- const active = this.#active;
699
- if (!active || message !== active.message) return;
700
- const sessionId = active.session.id;
701
- for (const result of toolResults) rememberImages(sessionId, result.content);
702
- active.completed = true;
703
- active.toolResults = toolResults;
704
- }
705
-
706
- async #completeActive(): Promise<void> {
707
- const active = this.#active;
708
- if (!active?.completed) return;
709
- this.#collectInputs();
710
- const inputs = this.#inputs();
711
- if (active.cancelled !== undefined) {
712
- const delivery: DeliveryRecord = {
713
- id: randomUUID(),
714
- ...source(active.request.chatId, active.request.requestId),
715
- sessionId: active.session.id,
716
- cwd: active.session.cwd,
717
- toolResults: active.toolResults,
718
- error: active.cancelled,
719
- };
720
- this.#deliveries.set(delivery.id, delivery);
721
- void this.#flushDeliveries().catch(() => {});
722
- } else {
723
- await this.#connection?.send({
724
- type: "result",
725
- id: active.request.id,
726
- cwd: active.session.cwd,
727
- message: active.message,
728
- inputs,
729
- ...(active.request.type === "call"
730
- ? { toolResults: active.toolResults }
731
- : {}),
732
- });
733
- }
734
- this.#active = undefined;
735
- this.#status = "idle";
736
- void this.#sync().catch(() => {});
737
- }
738
-
739
- #resetInputs(
740
- context?: ExtensionContext,
741
- cursor = context?.sessionManager.getLeafId() ?? null,
742
- ): void {
743
- this.#sessionId = context?.sessionManager.getSessionId();
744
- this.#inputCursor = cursor;
745
- this.#pendingInputs.clear();
746
- }
747
-
748
- #collectInputs(): void {
749
- const context = this.#context;
750
- if (!context) return;
751
- const sessionManager = context.sessionManager;
752
- const sessionId = sessionManager.getSessionId();
753
- if (this.#sessionId !== sessionId) {
754
- this.#resetInputs(context);
755
- return;
756
- }
757
- const leafId = sessionManager.getLeafId();
758
- if (leafId === this.#inputCursor) return;
759
- const entries: SessionEntry[] = [];
760
- let current = sessionManager.getLeafEntry();
761
- while (current && current.id !== this.#inputCursor) {
762
- entries.push(current);
763
- current = current.parentId
764
- ? sessionManager.getEntry(current.parentId)
765
- : undefined;
766
- }
767
- if (this.#inputCursor !== null && !current) {
768
- this.#resetInputs(context);
769
- return;
770
- }
771
- for (const entry of entries.reverse()) {
772
- if (entry.type !== "message" || entry.message.role !== "user") continue;
773
- const message = entry.message as UserMessage;
774
- if (typeof message.content !== "string") {
775
- rememberImages(sessionId, message.content);
776
- }
777
- this.#pendingInputs.set(entry.id, { id: entry.id, sessionId, message });
778
- }
779
- this.#inputCursor = leafId;
780
- }
781
-
782
- #inputs(): SessionInput[] {
783
- this.#collectInputs();
784
- return [...this.#pendingInputs.values()];
785
- }
786
-
787
- async #flushDeliveries(): Promise<void> {
788
- const flushed = this.#flushing.then(async () => {
789
- const connection = this.#connection;
790
- if (!connection?.connected) return;
791
- for (const delivery of this.#deliveries.values()) {
792
- const completion = Promise.withResolvers<void>();
793
- this.#stores.set(delivery.id, completion);
794
- try {
795
- await connection.send({ type: "delivery", delivery });
796
- await completion.promise;
797
- this.#deliveries.delete(delivery.id);
798
- this.#notify(
799
- `Result saved for ChatGPT ${delivery.chatId.slice(-4)}`,
800
- "info",
801
- {
802
- event: "result_saved",
803
- ...source(delivery.chatId, delivery.requestId),
804
- },
805
- );
806
- } finally {
807
- this.#stores.delete(delivery.id);
808
- }
809
- }
810
- });
811
- this.#flushing = flushed.catch(() => {});
812
- return flushed;
813
- }
814
-
815
- async #reply(
816
- id: number,
817
- sessionId: string,
818
- response: () =>
819
- | Parameters<IpcClient["send"]>[0]
820
- | Promise<Parameters<IpcClient["send"]>[0]>,
821
- ): Promise<void> {
822
- try {
823
- if (sessionId !== this.#context?.sessionManager.getSessionId()) {
824
- throw new Error("The requested Pi session is no longer active");
825
- }
826
- await this.#connection?.send(await response());
827
- } catch (error) {
828
- await this.#sendError(
829
- id,
830
- error instanceof Error ? error.message : String(error),
831
- );
832
- }
833
- }
834
-
835
- async #sendError(id: number, error: string): Promise<void> {
836
- await this.#connection?.send({ type: "result", id, error });
837
- }
838
-
839
- #rejectSyncs(error: Error): void {
840
- for (const sync of this.#syncs.values()) sync.reject(error);
841
- this.#syncs.clear();
842
- }
843
-
844
- #rejectStores(error: Error): void {
845
- for (const store of this.#stores.values()) store.reject(error);
846
- this.#stores.clear();
847
- }
848
- }