@vinaverse/client 0.1.0-alpha.1 → 0.1.0-alpha.3
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/LICENSE +202 -0
- package/README.md +3 -1
- package/dist/conversation/index.cjs +140 -4
- package/dist/conversation/index.cjs.map +1 -1
- package/dist/conversation/index.d.cts +37 -1
- package/dist/conversation/index.d.ts +37 -1
- package/dist/conversation/index.js +140 -4
- package/dist/conversation/index.js.map +1 -1
- package/package.json +9 -9
|
@@ -102,13 +102,17 @@ interface SystemItem {
|
|
|
102
102
|
readonly event: SystemEvent;
|
|
103
103
|
/**
|
|
104
104
|
* `takeover_started`: `by`; `handoff_created`: `expected_response`; `flow_completed`: `result`, `code`;
|
|
105
|
-
* `turn_failed` (a turn that failed before any of its reply streamed): `code`, `message`, `retryable
|
|
105
|
+
* `turn_failed` (a turn that failed before any of its reply streamed): `code`, `message`, `retryable`, `turn` (its `turn_id`), and `retry`:
|
|
106
|
+
* the server allows a retry and the person's text that started the turn is known, so `retryTurn(key)` sends it
|
|
107
|
+
* again (false once it did).
|
|
106
108
|
*/
|
|
107
109
|
readonly data: Readonly<Record<string, string | boolean | null>>;
|
|
108
110
|
}
|
|
109
111
|
type TimelineItem = MessageItem | CardItem | SystemItem;
|
|
110
112
|
interface ConversationView {
|
|
111
113
|
readonly status: ConversationStatus;
|
|
114
|
+
/** The conversation the sessions belong to, once a session named it; for the tenant's own records and support. */
|
|
115
|
+
readonly conversationId: string | null;
|
|
112
116
|
readonly banner: ConversationBanner;
|
|
113
117
|
/**
|
|
114
118
|
* How the conversation goes on after the session ended: `continue` on the person's next action (it resumes on its
|
|
@@ -219,6 +223,12 @@ interface ConversationOptions {
|
|
|
219
223
|
readonly createId?: () => string;
|
|
220
224
|
/** Something failed that no call returned: getting a session, an input that could not be delivered. */
|
|
221
225
|
readonly onError?: (error: unknown) => void;
|
|
226
|
+
/**
|
|
227
|
+
* Close the connection while the app is in the background, and open it again in the foreground (a backend that
|
|
228
|
+
* mints a client secret per connection: with `refreshSecret`, a fresh one is asked for first). Default false: the
|
|
229
|
+
* connection stays, with no pings, until the session idles out. Nothing typed or answered is lost either way.
|
|
230
|
+
*/
|
|
231
|
+
readonly closeInBackground?: boolean;
|
|
222
232
|
}
|
|
223
233
|
/** A text is at most 2000 code points (`TurnInput`). */
|
|
224
234
|
declare const MAX_TEXT = 2000;
|
|
@@ -243,12 +253,18 @@ declare class ConversationStore {
|
|
|
243
253
|
/** Proposals whose tenant decision threw: shown as recoverable, the person may decide again. */
|
|
244
254
|
private readonly decisionErrors;
|
|
245
255
|
private system;
|
|
256
|
+
/** The person's text that started each recent turn, by `turn_id` (`turn.started` echoes its `client_event_id`). */
|
|
257
|
+
private readonly turnInputs;
|
|
246
258
|
/** Numbers system rows that carry no id of their own. */
|
|
247
259
|
private systemSeq;
|
|
248
260
|
private history;
|
|
249
261
|
private historyState;
|
|
250
262
|
/** A resumed session.ready carried a transcript: the server's history replaced what the device kept. */
|
|
251
263
|
private transcriptSeen;
|
|
264
|
+
/** The session of the last session.ready: a card of another one takes no tap (§6). */
|
|
265
|
+
private readySession;
|
|
266
|
+
private cardOrder;
|
|
267
|
+
private kept;
|
|
252
268
|
private locale;
|
|
253
269
|
/** The request for a session in flight, to abort when it is no longer wanted. */
|
|
254
270
|
private fetching;
|
|
@@ -284,6 +300,11 @@ declare class ConversationStore {
|
|
|
284
300
|
setLocale(locale: string | undefined): void;
|
|
285
301
|
/** The app came to the foreground or went to the background (AppState, `visibilitychange`). */
|
|
286
302
|
setForeground(foreground: boolean): void;
|
|
303
|
+
/** In the background (`closeInBackground`): the connection is closed; the engine keeps what was sent, to resend. */
|
|
304
|
+
private suspended;
|
|
305
|
+
private suspend;
|
|
306
|
+
/** Back in the foreground: a new connection to the same session, with a fresh secret when the backend gives one. */
|
|
307
|
+
private wake;
|
|
287
308
|
/** The network as the app sees it; null when unknown. Back online reconnects a connection that gave up. */
|
|
288
309
|
setOnline(online: boolean | null): void;
|
|
289
310
|
/** Send a typed message; returns its `client_event_id`. Typed before a session exists, it is sent once one does. */
|
|
@@ -306,6 +327,12 @@ declare class ConversationStore {
|
|
|
306
327
|
decideProposal(cardId: string, decide: () => Promise<ProposalOutcome>): Promise<ProposalOutcome | null>;
|
|
307
328
|
/** Send a "not sent" message again, as a new message (the person's action). Returns the new id. */
|
|
308
329
|
retry(key: string): string | null;
|
|
330
|
+
/**
|
|
331
|
+
* Send again the person's text whose turn failed before any reply (a `turn_failed` row with `data.retry`), as a new
|
|
332
|
+
* message (the person's action). Once per row: its `retry` turns false. Returns the new id, or null when the row
|
|
333
|
+
* allows no retry.
|
|
334
|
+
*/
|
|
335
|
+
retryTurn(key: string): string | null;
|
|
309
336
|
/** After the conversation continued on another device: continue it here (the person's choice). */
|
|
310
337
|
continueHere(): Promise<void>;
|
|
311
338
|
/** Stop the turn in progress. */
|
|
@@ -323,6 +350,15 @@ declare class ConversationStore {
|
|
|
323
350
|
private reconnectIfDown;
|
|
324
351
|
private statusChanged;
|
|
325
352
|
private event;
|
|
353
|
+
/** The person's text that started a turn, while its message is on screen. */
|
|
354
|
+
private turnText;
|
|
355
|
+
/**
|
|
356
|
+
* A resumed session's transcript replaced the messages, and a chat transcript carries text only: the engine drops
|
|
357
|
+
* the cards it drew, and draws the pending ones again at the end. Each card on screen is anchored just after the
|
|
358
|
+
* message it followed (by key; the transcript keeps the live keys), so a pending proposal stays where it was, and a
|
|
359
|
+
* card the transcript dropped stays drawn there; taking no tap when it belongs to an earlier session.
|
|
360
|
+
*/
|
|
361
|
+
private anchorCards;
|
|
326
362
|
/**
|
|
327
363
|
* Recomputes now (getSnapshot and getStreaming are always current) and notifies from the scheduler, at most once
|
|
328
364
|
* per frame, both channels in the same flush: a streamed reply that completes becomes its message in one frame.
|
|
@@ -102,13 +102,17 @@ interface SystemItem {
|
|
|
102
102
|
readonly event: SystemEvent;
|
|
103
103
|
/**
|
|
104
104
|
* `takeover_started`: `by`; `handoff_created`: `expected_response`; `flow_completed`: `result`, `code`;
|
|
105
|
-
* `turn_failed` (a turn that failed before any of its reply streamed): `code`, `message`, `retryable
|
|
105
|
+
* `turn_failed` (a turn that failed before any of its reply streamed): `code`, `message`, `retryable`, `turn` (its `turn_id`), and `retry`:
|
|
106
|
+
* the server allows a retry and the person's text that started the turn is known, so `retryTurn(key)` sends it
|
|
107
|
+
* again (false once it did).
|
|
106
108
|
*/
|
|
107
109
|
readonly data: Readonly<Record<string, string | boolean | null>>;
|
|
108
110
|
}
|
|
109
111
|
type TimelineItem = MessageItem | CardItem | SystemItem;
|
|
110
112
|
interface ConversationView {
|
|
111
113
|
readonly status: ConversationStatus;
|
|
114
|
+
/** The conversation the sessions belong to, once a session named it; for the tenant's own records and support. */
|
|
115
|
+
readonly conversationId: string | null;
|
|
112
116
|
readonly banner: ConversationBanner;
|
|
113
117
|
/**
|
|
114
118
|
* How the conversation goes on after the session ended: `continue` on the person's next action (it resumes on its
|
|
@@ -219,6 +223,12 @@ interface ConversationOptions {
|
|
|
219
223
|
readonly createId?: () => string;
|
|
220
224
|
/** Something failed that no call returned: getting a session, an input that could not be delivered. */
|
|
221
225
|
readonly onError?: (error: unknown) => void;
|
|
226
|
+
/**
|
|
227
|
+
* Close the connection while the app is in the background, and open it again in the foreground (a backend that
|
|
228
|
+
* mints a client secret per connection: with `refreshSecret`, a fresh one is asked for first). Default false: the
|
|
229
|
+
* connection stays, with no pings, until the session idles out. Nothing typed or answered is lost either way.
|
|
230
|
+
*/
|
|
231
|
+
readonly closeInBackground?: boolean;
|
|
222
232
|
}
|
|
223
233
|
/** A text is at most 2000 code points (`TurnInput`). */
|
|
224
234
|
declare const MAX_TEXT = 2000;
|
|
@@ -243,12 +253,18 @@ declare class ConversationStore {
|
|
|
243
253
|
/** Proposals whose tenant decision threw: shown as recoverable, the person may decide again. */
|
|
244
254
|
private readonly decisionErrors;
|
|
245
255
|
private system;
|
|
256
|
+
/** The person's text that started each recent turn, by `turn_id` (`turn.started` echoes its `client_event_id`). */
|
|
257
|
+
private readonly turnInputs;
|
|
246
258
|
/** Numbers system rows that carry no id of their own. */
|
|
247
259
|
private systemSeq;
|
|
248
260
|
private history;
|
|
249
261
|
private historyState;
|
|
250
262
|
/** A resumed session.ready carried a transcript: the server's history replaced what the device kept. */
|
|
251
263
|
private transcriptSeen;
|
|
264
|
+
/** The session of the last session.ready: a card of another one takes no tap (§6). */
|
|
265
|
+
private readySession;
|
|
266
|
+
private cardOrder;
|
|
267
|
+
private kept;
|
|
252
268
|
private locale;
|
|
253
269
|
/** The request for a session in flight, to abort when it is no longer wanted. */
|
|
254
270
|
private fetching;
|
|
@@ -284,6 +300,11 @@ declare class ConversationStore {
|
|
|
284
300
|
setLocale(locale: string | undefined): void;
|
|
285
301
|
/** The app came to the foreground or went to the background (AppState, `visibilitychange`). */
|
|
286
302
|
setForeground(foreground: boolean): void;
|
|
303
|
+
/** In the background (`closeInBackground`): the connection is closed; the engine keeps what was sent, to resend. */
|
|
304
|
+
private suspended;
|
|
305
|
+
private suspend;
|
|
306
|
+
/** Back in the foreground: a new connection to the same session, with a fresh secret when the backend gives one. */
|
|
307
|
+
private wake;
|
|
287
308
|
/** The network as the app sees it; null when unknown. Back online reconnects a connection that gave up. */
|
|
288
309
|
setOnline(online: boolean | null): void;
|
|
289
310
|
/** Send a typed message; returns its `client_event_id`. Typed before a session exists, it is sent once one does. */
|
|
@@ -306,6 +327,12 @@ declare class ConversationStore {
|
|
|
306
327
|
decideProposal(cardId: string, decide: () => Promise<ProposalOutcome>): Promise<ProposalOutcome | null>;
|
|
307
328
|
/** Send a "not sent" message again, as a new message (the person's action). Returns the new id. */
|
|
308
329
|
retry(key: string): string | null;
|
|
330
|
+
/**
|
|
331
|
+
* Send again the person's text whose turn failed before any reply (a `turn_failed` row with `data.retry`), as a new
|
|
332
|
+
* message (the person's action). Once per row: its `retry` turns false. Returns the new id, or null when the row
|
|
333
|
+
* allows no retry.
|
|
334
|
+
*/
|
|
335
|
+
retryTurn(key: string): string | null;
|
|
309
336
|
/** After the conversation continued on another device: continue it here (the person's choice). */
|
|
310
337
|
continueHere(): Promise<void>;
|
|
311
338
|
/** Stop the turn in progress. */
|
|
@@ -323,6 +350,15 @@ declare class ConversationStore {
|
|
|
323
350
|
private reconnectIfDown;
|
|
324
351
|
private statusChanged;
|
|
325
352
|
private event;
|
|
353
|
+
/** The person's text that started a turn, while its message is on screen. */
|
|
354
|
+
private turnText;
|
|
355
|
+
/**
|
|
356
|
+
* A resumed session's transcript replaced the messages, and a chat transcript carries text only: the engine drops
|
|
357
|
+
* the cards it drew, and draws the pending ones again at the end. Each card on screen is anchored just after the
|
|
358
|
+
* message it followed (by key; the transcript keeps the live keys), so a pending proposal stays where it was, and a
|
|
359
|
+
* card the transcript dropped stays drawn there; taking no tap when it belongs to an earlier session.
|
|
360
|
+
*/
|
|
361
|
+
private anchorCards;
|
|
326
362
|
/**
|
|
327
363
|
* Recomputes now (getSnapshot and getStreaming are always current) and notifies from the scheduler, at most once
|
|
328
364
|
* per frame, both channels in the same flush: a streamed reply that completes becomes its message in one frame.
|
|
@@ -156,10 +156,13 @@ function buildTimeline(input, previous) {
|
|
|
156
156
|
busy: input.busy.has(card.id),
|
|
157
157
|
...input.decisionErrors?.has(card.id) ? { decisionError: true } : {}
|
|
158
158
|
},
|
|
159
|
-
order: card.seq
|
|
159
|
+
order: input.cardOrder?.get(card.id) ?? card.seq
|
|
160
160
|
});
|
|
161
161
|
}
|
|
162
162
|
for (const { item, order } of input.system) live.push({ item, order });
|
|
163
|
+
const held = new Set(state.cards.map((c) => c.id));
|
|
164
|
+
for (const { item, order } of input.kept ?? [])
|
|
165
|
+
if (!held.has(item.key)) live.push({ item, order });
|
|
163
166
|
for (const [id, sent] of input.sent) {
|
|
164
167
|
const pending = pendingState(id, input);
|
|
165
168
|
if (pending === null) continue;
|
|
@@ -233,6 +236,7 @@ function persistable(item) {
|
|
|
233
236
|
|
|
234
237
|
// src/conversation/store.ts
|
|
235
238
|
var MAX_TEXT = 2e3;
|
|
239
|
+
var TURN_INPUTS = 50;
|
|
236
240
|
var PERSIST_DELAY = 500;
|
|
237
241
|
var PERSIST_VERSION = 1;
|
|
238
242
|
var NO_ITEMS = [];
|
|
@@ -263,12 +267,18 @@ var ConversationStore = class {
|
|
|
263
267
|
/** Proposals whose tenant decision threw: shown as recoverable, the person may decide again. */
|
|
264
268
|
decisionErrors = /* @__PURE__ */ new Set();
|
|
265
269
|
system = [];
|
|
270
|
+
/** The person's text that started each recent turn, by `turn_id` (`turn.started` echoes its `client_event_id`). */
|
|
271
|
+
turnInputs = /* @__PURE__ */ new Map();
|
|
266
272
|
/** Numbers system rows that carry no id of their own. */
|
|
267
273
|
systemSeq = 0;
|
|
268
274
|
history = NO_ITEMS;
|
|
269
275
|
historyState = "none";
|
|
270
276
|
/** A resumed session.ready carried a transcript: the server's history replaced what the device kept. */
|
|
271
277
|
transcriptSeen = false;
|
|
278
|
+
/** The session of the last session.ready: a card of another one takes no tap (§6). */
|
|
279
|
+
readySession = null;
|
|
280
|
+
cardOrder = /* @__PURE__ */ new Map();
|
|
281
|
+
kept = [];
|
|
272
282
|
locale;
|
|
273
283
|
/** The request for a session in flight, to abort when it is no longer wanted. */
|
|
274
284
|
fetching = null;
|
|
@@ -350,9 +360,49 @@ var ConversationStore = class {
|
|
|
350
360
|
/** The app came to the foreground or went to the background (AppState, `visibilitychange`). */
|
|
351
361
|
setForeground(foreground) {
|
|
352
362
|
this.foreground = foreground;
|
|
363
|
+
if (this.options.closeInBackground && !this.disposed) {
|
|
364
|
+
if (!foreground) {
|
|
365
|
+
this.suspend();
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (this.suspended) {
|
|
369
|
+
void this.wake();
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
353
373
|
this.session?.setForeground(foreground);
|
|
354
374
|
if (foreground) this.reconnectIfDown();
|
|
355
375
|
}
|
|
376
|
+
/** In the background (`closeInBackground`): the connection is closed; the engine keeps what was sent, to resend. */
|
|
377
|
+
suspended = false;
|
|
378
|
+
suspend() {
|
|
379
|
+
const session = this.session;
|
|
380
|
+
if (!session || this.suspended || session.status === "ended") return;
|
|
381
|
+
this.suspended = true;
|
|
382
|
+
for (const off of this.unsubscribe) off();
|
|
383
|
+
this.unsubscribe = [];
|
|
384
|
+
session.close();
|
|
385
|
+
this.update();
|
|
386
|
+
}
|
|
387
|
+
/** Back in the foreground: a new connection to the same session, with a fresh secret when the backend gives one. */
|
|
388
|
+
async wake() {
|
|
389
|
+
const info = this.info;
|
|
390
|
+
this.suspended = false;
|
|
391
|
+
if (!info) return;
|
|
392
|
+
let next = info;
|
|
393
|
+
if (this.options.refreshSecret) {
|
|
394
|
+
try {
|
|
395
|
+
next = {
|
|
396
|
+
...info,
|
|
397
|
+
client_secret: await this.options.refreshSecret(info.id)
|
|
398
|
+
};
|
|
399
|
+
} catch (error) {
|
|
400
|
+
this.options.onError?.(error);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (this.disposed || this.suspended) return;
|
|
404
|
+
this.open(next);
|
|
405
|
+
}
|
|
356
406
|
/** The network as the app sees it; null when unknown. Back online reconnects a connection that gave up. */
|
|
357
407
|
setOnline(online) {
|
|
358
408
|
this.online = online;
|
|
@@ -374,6 +424,12 @@ var ConversationStore = class {
|
|
|
374
424
|
this.update();
|
|
375
425
|
return id;
|
|
376
426
|
}
|
|
427
|
+
if (this.suspended) {
|
|
428
|
+
this.early.set(id, text);
|
|
429
|
+
void this.wake();
|
|
430
|
+
this.update();
|
|
431
|
+
return id;
|
|
432
|
+
}
|
|
377
433
|
if (this.nextFrom(this.engine.state()) === "continue_here") {
|
|
378
434
|
this.early.set(id, text);
|
|
379
435
|
void this.resumeWith("continue_here");
|
|
@@ -442,6 +498,24 @@ var ConversationStore = class {
|
|
|
442
498
|
this.sent.delete(key);
|
|
443
499
|
return this.send(sent.text);
|
|
444
500
|
}
|
|
501
|
+
/**
|
|
502
|
+
* Send again the person's text whose turn failed before any reply (a `turn_failed` row with `data.retry`), as a new
|
|
503
|
+
* message (the person's action). Once per row: its `retry` turns false. Returns the new id, or null when the row
|
|
504
|
+
* allows no retry.
|
|
505
|
+
*/
|
|
506
|
+
retryTurn(key) {
|
|
507
|
+
const at = this.system.findIndex(
|
|
508
|
+
(s) => s.item.key === key && s.item.event === "turn_failed"
|
|
509
|
+
);
|
|
510
|
+
const entry = this.system[at];
|
|
511
|
+
if (entry?.item.data.retry !== true) return null;
|
|
512
|
+
const text = this.turnText(String(entry.item.data.turn ?? ""));
|
|
513
|
+
if (text === void 0) return null;
|
|
514
|
+
this.system = this.system.map(
|
|
515
|
+
(s, i) => i === at ? { ...s, item: { ...s.item, data: { ...s.item.data, retry: false } } } : s
|
|
516
|
+
);
|
|
517
|
+
return this.send(text);
|
|
518
|
+
}
|
|
445
519
|
/** After the conversation continued on another device: continue it here (the person's choice). */
|
|
446
520
|
continueHere() {
|
|
447
521
|
return this.resumeWith("continue_here");
|
|
@@ -575,10 +649,16 @@ var ConversationStore = class {
|
|
|
575
649
|
];
|
|
576
650
|
};
|
|
577
651
|
switch (frame.type) {
|
|
578
|
-
case "session.ready":
|
|
579
|
-
|
|
652
|
+
case "session.ready": {
|
|
653
|
+
const session = frame.session?.id;
|
|
654
|
+
const moved = typeof session === "string" && session !== this.readySession;
|
|
655
|
+
if (typeof session === "string") this.readySession = session;
|
|
656
|
+
if (frame.resumed === true && Array.isArray(frame.transcript)) {
|
|
580
657
|
this.transcriptSeen = true;
|
|
658
|
+
this.anchorCards(moved);
|
|
659
|
+
}
|
|
581
660
|
return;
|
|
661
|
+
}
|
|
582
662
|
// `at` is required (v1-draft.15); the same takeover announced again after a reconnect has the same `at`, so it
|
|
583
663
|
// draws one row. A frame without one still gets a row of its own.
|
|
584
664
|
case "takeover.started":
|
|
@@ -608,6 +688,17 @@ var ConversationStore = class {
|
|
|
608
688
|
code: str(frame.code)
|
|
609
689
|
});
|
|
610
690
|
return;
|
|
691
|
+
case "turn.started": {
|
|
692
|
+
const turn = str(frame.turn_id);
|
|
693
|
+
const input = str(frame.client_event_id);
|
|
694
|
+
if (turn === null || input === null) return;
|
|
695
|
+
this.turnInputs.set(turn, input);
|
|
696
|
+
if (this.turnInputs.size > TURN_INPUTS) {
|
|
697
|
+
const oldest = this.turnInputs.keys().next().value;
|
|
698
|
+
if (oldest !== void 0) this.turnInputs.delete(oldest);
|
|
699
|
+
}
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
611
702
|
case "turn.failed": {
|
|
612
703
|
const turn = str(frame.turn_id) ?? "";
|
|
613
704
|
if (this.engine.state().messages.some((m) => m.key === `a:${turn}`))
|
|
@@ -616,7 +707,9 @@ var ConversationStore = class {
|
|
|
616
707
|
add(`failed:${turn}`, "turn_failed", {
|
|
617
708
|
code: str(frame.code),
|
|
618
709
|
message: str(frame.message),
|
|
619
|
-
retryable: frame.retryable === true
|
|
710
|
+
retryable: frame.retryable === true,
|
|
711
|
+
retry: frame.retryable === true && this.turnText(turn) !== void 0,
|
|
712
|
+
turn
|
|
620
713
|
});
|
|
621
714
|
return;
|
|
622
715
|
}
|
|
@@ -624,6 +717,45 @@ var ConversationStore = class {
|
|
|
624
717
|
return;
|
|
625
718
|
}
|
|
626
719
|
}
|
|
720
|
+
/** The person's text that started a turn, while its message is on screen. */
|
|
721
|
+
turnText(turn) {
|
|
722
|
+
const input = this.turnInputs.get(turn);
|
|
723
|
+
if (input === void 0) return void 0;
|
|
724
|
+
const message = this.engine.state().messages.find((m) => m.key === input && m.role === "person");
|
|
725
|
+
return message?.text || void 0;
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* A resumed session's transcript replaced the messages, and a chat transcript carries text only: the engine drops
|
|
729
|
+
* the cards it drew, and draws the pending ones again at the end. Each card on screen is anchored just after the
|
|
730
|
+
* message it followed (by key; the transcript keeps the live keys), so a pending proposal stays where it was, and a
|
|
731
|
+
* card the transcript dropped stays drawn there; taking no tap when it belongs to an earlier session.
|
|
732
|
+
*/
|
|
733
|
+
anchorCards(sessionChanged) {
|
|
734
|
+
const state = this.engine.state();
|
|
735
|
+
const seqOf = new Map(state.messages.map((m) => [m.key, m.seq]));
|
|
736
|
+
const first = Math.min(...state.messages.map((m) => m.seq), 0);
|
|
737
|
+
const held = new Set(state.cards.map((c) => c.id));
|
|
738
|
+
const order = /* @__PURE__ */ new Map();
|
|
739
|
+
const kept = [];
|
|
740
|
+
let anchor = first - 1;
|
|
741
|
+
let step = 0;
|
|
742
|
+
for (const item of this.view.items) {
|
|
743
|
+
if (item.type === "message" && seqOf.has(item.key)) {
|
|
744
|
+
anchor = seqOf.get(item.key);
|
|
745
|
+
step = 0;
|
|
746
|
+
} else if (item.type === "card") {
|
|
747
|
+
const at = anchor + 0.5 + ++step / 1e3;
|
|
748
|
+
order.set(item.key, at);
|
|
749
|
+
if (!held.has(item.key))
|
|
750
|
+
kept.push({
|
|
751
|
+
item: sessionChanged ? { ...item, busy: false, restored: true } : item,
|
|
752
|
+
order: at
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
this.cardOrder = order;
|
|
757
|
+
this.kept = kept;
|
|
758
|
+
}
|
|
627
759
|
// ------------------------------------------------------------ the view
|
|
628
760
|
/**
|
|
629
761
|
* Recomputes now (getSnapshot and getStreaming are always current) and notifies from the scheduler, at most once
|
|
@@ -680,6 +812,8 @@ var ConversationStore = class {
|
|
|
680
812
|
busy: this.busy,
|
|
681
813
|
decisionErrors: this.decisionErrors,
|
|
682
814
|
transcriptSeen: this.transcriptSeen,
|
|
815
|
+
cardOrder: this.cardOrder,
|
|
816
|
+
kept: this.kept,
|
|
683
817
|
system: this.system
|
|
684
818
|
},
|
|
685
819
|
prev?.items ?? NO_ITEMS
|
|
@@ -691,6 +825,7 @@ var ConversationStore = class {
|
|
|
691
825
|
const avatarUrl = this.info?.agent?.avatar_url ?? null;
|
|
692
826
|
const view = {
|
|
693
827
|
status,
|
|
828
|
+
conversationId: s.conversationId,
|
|
694
829
|
banner: banner(status, next),
|
|
695
830
|
next,
|
|
696
831
|
agent: sameAgent(prev?.agent, name, avatarUrl) ? prev.agent : { name, avatarUrl },
|
|
@@ -709,6 +844,7 @@ var ConversationStore = class {
|
|
|
709
844
|
}
|
|
710
845
|
status() {
|
|
711
846
|
if (this.disposed) return "closed";
|
|
847
|
+
if (this.suspended) return "idle";
|
|
712
848
|
const session = this.session;
|
|
713
849
|
if (!session) {
|
|
714
850
|
if (this.failed) return "unavailable";
|