@astralform/js 9.5.0 → 9.7.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/dist/index.d.cts CHANGED
@@ -2334,6 +2334,31 @@ declare class ChatSession {
2334
2334
  * pages, and a doubled prompt is more visible than a missing one.
2335
2335
  */
2336
2336
  prependMessages(older: Message[]): void;
2337
+ /**
2338
+ * Re-fetch the NEWEST page of the conversation list and merge it in.
2339
+ *
2340
+ * The list the sidebar renders is seeded once at `connect()` and then only
2341
+ * changed by this client's own actions (create, delete, rename) — so a row's
2342
+ * `activeJobId` is as fresh as that seed, and a job started in ANOTHER tab or
2343
+ * on another device never appears, while one that ended elsewhere stays lit
2344
+ * forever. A caller that refreshes on a trigger (the tab becoming visible, or
2345
+ * a bounded interval while any job is live) keeps the rows honest without a
2346
+ * socket per conversation.
2347
+ *
2348
+ * Merge rules, and why each is what it is:
2349
+ *
2350
+ * - the server's ORDER wins for the rows it returns: an updated conversation
2351
+ * moves to its new place, which is the whole point of the call;
2352
+ * - rows paged in EARLIER (`loadMoreConversations`) are kept — this reads page
2353
+ * 1 only, and dropping the tail would make a refresh silently truncate a
2354
+ * list the reader has already scrolled;
2355
+ * - a row the caller MINTED locally and the server does not know yet is kept
2356
+ * too (it has no server row to be refreshed by);
2357
+ * - `conversationsGeneration` is bumped, for the same reason `connect()`
2358
+ * bumps it: a page request already in flight was cut against the old head
2359
+ * and its offset no longer means what it did.
2360
+ */
2361
+ refreshConversations(): Promise<Conversation[]>;
2337
2362
  loadMoreConversations(): Promise<Conversation[]>;
2338
2363
  /**
2339
2364
  * Rename a conversation, server first.
@@ -2688,6 +2713,18 @@ declare class StreamManager {
2688
2713
  have?: ReadonlyArray<{
2689
2714
  jobId: string;
2690
2715
  lastSeq: number;
2716
+ /**
2717
+ * The prompt this job's turn drew, when the caller knows it.
2718
+ *
2719
+ * The caller's cache holds the PROMPT too (it renders the turn), and a
2720
+ * prompt is a message-table row rather than a stream event — so the
2721
+ * plan's empty-walk rescue has to be told which prompts are already on
2722
+ * screen. Marks are matched against the page of jobs this restore walks,
2723
+ * so a mark for an OLDER turn is not otherwise visible to that rescue,
2724
+ * and a cache deeper than one turn page would re-emit those prompts as
2725
+ * steer bubbles.
2726
+ */
2727
+ messageId?: string | null;
2691
2728
  }>;
2692
2729
  }): Promise<void>;
2693
2730
  /**
package/dist/index.d.ts CHANGED
@@ -2334,6 +2334,31 @@ declare class ChatSession {
2334
2334
  * pages, and a doubled prompt is more visible than a missing one.
2335
2335
  */
2336
2336
  prependMessages(older: Message[]): void;
2337
+ /**
2338
+ * Re-fetch the NEWEST page of the conversation list and merge it in.
2339
+ *
2340
+ * The list the sidebar renders is seeded once at `connect()` and then only
2341
+ * changed by this client's own actions (create, delete, rename) — so a row's
2342
+ * `activeJobId` is as fresh as that seed, and a job started in ANOTHER tab or
2343
+ * on another device never appears, while one that ended elsewhere stays lit
2344
+ * forever. A caller that refreshes on a trigger (the tab becoming visible, or
2345
+ * a bounded interval while any job is live) keeps the rows honest without a
2346
+ * socket per conversation.
2347
+ *
2348
+ * Merge rules, and why each is what it is:
2349
+ *
2350
+ * - the server's ORDER wins for the rows it returns: an updated conversation
2351
+ * moves to its new place, which is the whole point of the call;
2352
+ * - rows paged in EARLIER (`loadMoreConversations`) are kept — this reads page
2353
+ * 1 only, and dropping the tail would make a refresh silently truncate a
2354
+ * list the reader has already scrolled;
2355
+ * - a row the caller MINTED locally and the server does not know yet is kept
2356
+ * too (it has no server row to be refreshed by);
2357
+ * - `conversationsGeneration` is bumped, for the same reason `connect()`
2358
+ * bumps it: a page request already in flight was cut against the old head
2359
+ * and its offset no longer means what it did.
2360
+ */
2361
+ refreshConversations(): Promise<Conversation[]>;
2337
2362
  loadMoreConversations(): Promise<Conversation[]>;
2338
2363
  /**
2339
2364
  * Rename a conversation, server first.
@@ -2688,6 +2713,18 @@ declare class StreamManager {
2688
2713
  have?: ReadonlyArray<{
2689
2714
  jobId: string;
2690
2715
  lastSeq: number;
2716
+ /**
2717
+ * The prompt this job's turn drew, when the caller knows it.
2718
+ *
2719
+ * The caller's cache holds the PROMPT too (it renders the turn), and a
2720
+ * prompt is a message-table row rather than a stream event — so the
2721
+ * plan's empty-walk rescue has to be told which prompts are already on
2722
+ * screen. Marks are matched against the page of jobs this restore walks,
2723
+ * so a mark for an OLDER turn is not otherwise visible to that rescue,
2724
+ * and a cache deeper than one turn page would re-emit those prompts as
2725
+ * steer bubbles.
2726
+ */
2727
+ messageId?: string | null;
2691
2728
  }>;
2692
2729
  }): Promise<void>;
2693
2730
  /**
package/dist/index.js CHANGED
@@ -2772,6 +2772,48 @@ var ChatSession = class {
2772
2772
  if (!fresh.length) return;
2773
2773
  this.setMessages([...fresh, ...this.messages]);
2774
2774
  }
2775
+ /**
2776
+ * Re-fetch the NEWEST page of the conversation list and merge it in.
2777
+ *
2778
+ * The list the sidebar renders is seeded once at `connect()` and then only
2779
+ * changed by this client's own actions (create, delete, rename) — so a row's
2780
+ * `activeJobId` is as fresh as that seed, and a job started in ANOTHER tab or
2781
+ * on another device never appears, while one that ended elsewhere stays lit
2782
+ * forever. A caller that refreshes on a trigger (the tab becoming visible, or
2783
+ * a bounded interval while any job is live) keeps the rows honest without a
2784
+ * socket per conversation.
2785
+ *
2786
+ * Merge rules, and why each is what it is:
2787
+ *
2788
+ * - the server's ORDER wins for the rows it returns: an updated conversation
2789
+ * moves to its new place, which is the whole point of the call;
2790
+ * - rows paged in EARLIER (`loadMoreConversations`) are kept — this reads page
2791
+ * 1 only, and dropping the tail would make a refresh silently truncate a
2792
+ * list the reader has already scrolled;
2793
+ * - a row the caller MINTED locally and the server does not know yet is kept
2794
+ * too (it has no server row to be refreshed by);
2795
+ * - `conversationsGeneration` is bumped, for the same reason `connect()`
2796
+ * bumps it: a page request already in flight was cut against the old head
2797
+ * and its offset no longer means what it did.
2798
+ */
2799
+ async refreshConversations() {
2800
+ const page = await this.client.getConversations(CONVERSATION_PAGE_SIZE, 0);
2801
+ const pageIds = new Set(page.map((c) => c.id));
2802
+ const known = new Set(this.conversations.map((c) => c.id));
2803
+ this.conversationsGeneration++;
2804
+ this.conversations = [
2805
+ ...page,
2806
+ // Everything the page did not mention: older pages, and rows that exist
2807
+ // only on this client (a minted conversation the server has not been told
2808
+ // about yet).
2809
+ ...this.conversations.filter((c) => !pageIds.has(c.id))
2810
+ ];
2811
+ for (const c of page) {
2812
+ this.serverConversationIds.add(c.id);
2813
+ known.delete(c.id);
2814
+ }
2815
+ return page;
2816
+ }
2775
2817
  async loadMoreConversations() {
2776
2818
  if (this.isLoadingConversations || !this.hasMoreConversations) return [];
2777
2819
  this.isLoadingConversations = true;
@@ -3367,6 +3409,7 @@ var StreamManager = class _StreamManager {
3367
3409
  const haveByJobId = new Map(
3368
3410
  (have ?? []).map((h) => [h.jobId, h.lastSeq])
3369
3411
  );
3412
+ const heldFromMarks = (have ?? []).map((h) => h.messageId).filter((id) => typeof id === "string" && id.length > 0);
3370
3413
  const superseded = () => gen !== this.generation;
3371
3414
  if (superseded()) return;
3372
3415
  const turn = this.turnCounter;
@@ -3394,7 +3437,8 @@ var StreamManager = class _StreamManager {
3394
3437
  activeJobId,
3395
3438
  turn,
3396
3439
  jobsRequest,
3397
- haveByJobId
3440
+ haveByJobId,
3441
+ heldFromMarks
3398
3442
  ))
3399
3443
  return;
3400
3444
  if (activeJobId) {
@@ -3591,7 +3635,7 @@ var StreamManager = class _StreamManager {
3591
3635
  session.isLoadingEarlierTurns = false;
3592
3636
  }
3593
3637
  }
3594
- async replayHistory(conversationId, gen, activeJobId, turn, jobsRequest, haveByJobId) {
3638
+ async replayHistory(conversationId, gen, activeJobId, turn, jobsRequest, haveByJobId, heldFromMarks) {
3595
3639
  const stopReplay = () => gen !== this.generation || this.viewTakenOverByLiveTurn() || this.turnStarted(turn);
3596
3640
  let complete = false;
3597
3641
  try {
@@ -3638,7 +3682,10 @@ var StreamManager = class _StreamManager {
3638
3682
  // primary cache scenario, a SETTLED conversation whose turns are all
3639
3683
  // held — and the empty-walk rescue would otherwise re-emit every one of
3640
3684
  // them as a steer bubble.
3641
- heldMessageIds: jobs.filter((j) => haveByJobId.has(j.job_id)).map((j) => j.message_id),
3685
+ heldMessageIds: [
3686
+ ...jobs.filter((j) => haveByJobId.has(j.job_id)).map((j) => j.message_id),
3687
+ ...heldFromMarks
3688
+ ],
3642
3689
  userMessages: userMessages.map((m) => ({
3643
3690
  id: m.id,
3644
3691
  content: m.content
@@ -3672,8 +3719,11 @@ var StreamManager = class _StreamManager {
3672
3719
  step.messageId
3673
3720
  );
3674
3721
  }
3675
- this.session.hasMoreTurns = page.hasMore;
3676
- this.session.oldestTurnCursor = page.nextBefore;
3722
+ const drewNothing = plan.length === 0;
3723
+ const holdsTurns = haveByJobId.size > 0;
3724
+ const noDrawableTurns = drewNothing && holdsTurns;
3725
+ this.session.hasMoreTurns = noDrawableTurns ? false : page.hasMore;
3726
+ this.session.oldestTurnCursor = noDrawableTurns ? null : page.nextBefore;
3677
3727
  this.oldestDrawnMessageId = plan[0]?.messageId ?? null;
3678
3728
  complete = true;
3679
3729
  if (stopReplay()) return false;