@astralbeam/sdk 0.19.1 → 0.20.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.
@@ -10990,6 +10990,7 @@ function selectedThread(key, id) {
10990
10990
  }
10991
10991
  //#endregion
10992
10992
  //#region src/core/session.ts
10993
+ const HISTORY_FAILED = "Reopen this conversation or start a new one before sending.";
10993
10994
  const CORE_OPTION_KEYS = Object.keys({
10994
10995
  agentId: true,
10995
10996
  threadId: true,
@@ -11027,21 +11028,58 @@ function createAstralBeamChat(options, deferStart = false) {
11027
11028
  commands: []
11028
11029
  },
11029
11030
  thread: void 0,
11030
- threadLoading: false,
11031
- threadLoadFailed: false,
11031
+ history: "idle",
11032
11032
  messagesCursor: void 0,
11033
11033
  olderMessagesLoading: false,
11034
- unsentMessage: void 0,
11034
+ submission: void 0,
11035
11035
  activeToolCallIds: [],
11036
11036
  pendingInteractions: []
11037
11037
  };
11038
+ let submission;
11039
+ const submissions = /* @__PURE__ */ new Map();
11040
+ let clientStatus = "ready";
11038
11041
  const update = (next) => {
11042
+ const status = submission?.phase === "submitting" && clientStatus !== "streaming" ? "submitted" : clientStatus;
11039
11043
  state = {
11040
11044
  ...state,
11041
- ...next
11045
+ ...next,
11046
+ status
11042
11047
  };
11043
11048
  for (const listener of listeners) listener();
11044
11049
  };
11050
+ const project = ({ id, content, phase, error }) => ({
11051
+ id,
11052
+ content,
11053
+ phase,
11054
+ ...error ? { error } : {}
11055
+ });
11056
+ const nextPhases = {
11057
+ submitting: [
11058
+ "accepted",
11059
+ "failed",
11060
+ "refused"
11061
+ ],
11062
+ failed: ["submitting"],
11063
+ accepted: [],
11064
+ refused: []
11065
+ };
11066
+ const settle = (sent, phase, error) => {
11067
+ if (!nextPhases[sent.phase].includes(phase)) return false;
11068
+ sent.phase = phase;
11069
+ sent.error = error;
11070
+ if (sent === submission) update({ submission: project(sent) });
11071
+ if (phase === "refused") sent.onRefused?.();
11072
+ return true;
11073
+ };
11074
+ const current = (sent, attempt) => sent === submission && sent.attempt === attempt;
11075
+ const owns = (sent, attempt) => current(sent, attempt) && sent.phase === "submitting";
11076
+ const endUnaccepted = (sent, error) => settle(sent, sent.tools ? "failed" : "refused", error ?? (sent.tools ? /* @__PURE__ */ new Error("This message may not have been sent. Retry to send it.") : void 0));
11077
+ const releaseSubmission = (reason) => {
11078
+ const released = submission;
11079
+ if (released?.phase === "submitting") endUnaccepted(released, released.tools ? void 0 : new Error(reason));
11080
+ submission = void 0;
11081
+ return released;
11082
+ };
11045
11083
  const authentication = createChatAuthentication({
11046
11084
  apiUrl: live.apiUrl,
11047
11085
  scope: "tenant",
@@ -11052,7 +11090,6 @@ function createAstralBeamChat(options, deferStart = false) {
11052
11090
  let identity;
11053
11091
  let started = false;
11054
11092
  let selectionGeneration = 0;
11055
- let navigationGeneration = 0;
11056
11093
  let historyGeneration = 0;
11057
11094
  let hydration = Promise.resolve();
11058
11095
  let requestController = new AbortController();
@@ -11060,17 +11097,13 @@ function createAstralBeamChat(options, deferStart = false) {
11060
11097
  let startWithNewThread = false;
11061
11098
  let threadRecords = [];
11062
11099
  const clientId = newUuid();
11063
- let pendingSend;
11064
- let sending = false;
11065
- const pendingSends = /* @__PURE__ */ new Map();
11066
- const acceptPendingSend = () => {
11067
- if (!pendingSend || pendingSend.accepted) return;
11068
- pendingSend.accepted = true;
11100
+ const acceptSubmission = () => {
11101
+ if (!submission || !settle(submission, "accepted")) return;
11069
11102
  if (state.thread) update({ thread: {
11070
11103
  ...state.thread,
11071
11104
  hasMessages: true
11072
11105
  } });
11073
- pendingSend.callbacks?.onAccepted?.();
11106
+ submission.callbacks?.onAccepted?.();
11074
11107
  };
11075
11108
  const replaceAcceptedMessage = (record) => {
11076
11109
  const messages = [...client.getMessages()];
@@ -11102,14 +11135,14 @@ function createAstralBeamChat(options, deferStart = false) {
11102
11135
  toolResults.clear();
11103
11136
  for (const [id, result] of retained) toolResults.set(id, result);
11104
11137
  };
11138
+ const notifyError = (error) => {
11139
+ if (state.auth.status !== "error" || state.auth.error !== error) live.streamCallbacks?.onError?.(error);
11140
+ };
11105
11141
  const reportError = (error) => {
11106
11142
  if (error instanceof Error && error.name === "AbortError") return;
11107
11143
  const failure = error instanceof Error ? error : new Error(String(error));
11108
- update({
11109
- error: failure,
11110
- status: "error"
11111
- });
11112
- if (state.auth.status !== "error" || state.auth.error !== failure) live.streamCallbacks?.onError?.(failure);
11144
+ update({ error: failure });
11145
+ notifyError(failure);
11113
11146
  };
11114
11147
  const clearUnavailableMcpConnection = (id) => {
11115
11148
  update({ unavailableMcpConnections: state.unavailableMcpConnections?.filter((entry) => entry !== id) ?? [] });
@@ -11278,15 +11311,15 @@ function createAstralBeamChat(options, deferStart = false) {
11278
11311
  const body = JSON.parse(init.body);
11279
11312
  let path = getRunChatUrl();
11280
11313
  let payload;
11281
- const submission = body.messages.at(-1)?.role === "user" ? pendingSend : void 0;
11282
- const previouslyAttempted = submission?.tools !== void 0;
11283
- if (submission) payload = {
11314
+ const sent = body.messages.at(-1)?.role === "user" && submission?.phase === "submitting" ? submission : void 0;
11315
+ const previouslyAttempted = sent?.tools !== void 0;
11316
+ if (sent) payload = {
11284
11317
  ...body,
11285
11318
  threadId: thread.id,
11286
11319
  resume: void 0,
11287
11320
  parentRunId: void 0,
11288
11321
  messages: [body.messages.at(-1)],
11289
- tools: submission.tools ??= body.tools,
11322
+ tools: sent.tools ??= body.tools,
11290
11323
  forwardedProps: {
11291
11324
  ...body.forwardedProps,
11292
11325
  clientId
@@ -11321,7 +11354,7 @@ function createAstralBeamChat(options, deferStart = false) {
11321
11354
  liveToolCalls.clear();
11322
11355
  update({ activeToolCallIds: [] });
11323
11356
  const headers = new Headers(init.headers);
11324
- if (submission) headers.set("Idempotency-Key", submission.key);
11357
+ if (sent) headers.set("Idempotency-Key", sent.key);
11325
11358
  const response = await astralBeamChatFetch(path, {
11326
11359
  ...auth,
11327
11360
  ...init,
@@ -11333,7 +11366,7 @@ function createAstralBeamChat(options, deferStart = false) {
11333
11366
  name: limit.name,
11334
11367
  resetsAt: limit.resets_at
11335
11368
  } });
11336
- if (submission && !previouslyAttempted && isAstralBeamApiError(error) && (error.status === 400 || error.status === 413 || error.status === 429)) delete submission.tools;
11369
+ if (sent && !previouslyAttempted && isAstralBeamApiError(error) && (error.status === 400 || error.status === 413 || error.status === 429)) delete sent.tools;
11337
11370
  throw error;
11338
11371
  });
11339
11372
  if (generation !== selectionGeneration) throw new DOMException("Conversation changed", "AbortError");
@@ -11350,12 +11383,12 @@ function createAstralBeamChat(options, deferStart = false) {
11350
11383
  resetsAt: receipt.usage_limit.resets_at
11351
11384
  } } : {}
11352
11385
  });
11353
- if (submission) {
11386
+ if (sent) {
11354
11387
  const history = await loadThreadMessages(thread.id, auth, void 0, liveToolMessageIds);
11355
11388
  if (generation !== selectionGeneration) throw new DOMException("Conversation changed", "AbortError");
11356
11389
  const accepted = history.messages.find((message) => message.id === receipt.accepted_message_id);
11357
11390
  if (accepted) replaceAcceptedMessage(accepted);
11358
- acceptPendingSend();
11391
+ acceptSubmission();
11359
11392
  }
11360
11393
  return new Response("", { headers: { "Content-Type": "text/event-stream" } });
11361
11394
  }
@@ -11416,70 +11449,76 @@ function createAstralBeamChat(options, deferStart = false) {
11416
11449
  throw error instanceof Error && isAstralBeamApiError(error.cause) ? error.cause : error;
11417
11450
  }
11418
11451
  };
11419
- const createClient = (threadId = state.thread?.id) => {
11420
- const generation = selectionGeneration;
11421
- const adapter = { ...connection };
11422
- delete adapter.hydrate;
11423
- if (threadId) adapter.hydrate = (_id, options) => {
11424
- const readGeneration = ++historyGeneration;
11425
- if (!options?.before) update({ threadLoading: true });
11426
- const read = (async () => {
11427
- let history = await loadThreadMessages(threadId, await requestOptions(), options?.before, liveToolMessageIds);
11428
- while (options?.before && history.messages.every((message) => message.role === "tool") && history.page_after) {
11429
- const earlier = await loadThreadMessages(threadId, await requestOptions(), history.page_after, liveToolMessageIds);
11430
- history = {
11431
- ...earlier,
11432
- messages: [...earlier.messages, ...history.messages]
11433
- };
11434
- }
11435
- if (generation !== selectionGeneration || readGeneration !== historyGeneration) throw new DOMException("Conversation changed", "AbortError");
11436
- retainToolResults(history);
11437
- threadRecords = options?.before ? [...history.messages, ...threadRecords] : history.messages;
11438
- const pageIds = new Set(history.messages.map((message) => message.id));
11439
- const messages = projectThreadMessages(threadRecords, history.pendingInteractions, liveToolMessageIds).filter((message) => pageIds.has(message.id));
11440
- if (history.thread.role === "viewer") disposeRenders();
11441
- const agentChanged = state.thread?.agentId !== history.thread.agent_id;
11442
- update({
11443
- thread: threadFromRecord(history.thread, history.messages.length > 0),
11444
- messagesCursor: history.page_after ?? void 0,
11445
- pendingInteractions: history.pendingInteractions,
11446
- threadLoadFailed: false
11447
- });
11448
- restoreCompletedWidgets(messages);
11449
- if (!options?.before && agentChanged) resolveCapabilities();
11450
- if (!options?.before && messages.length === 0) next.setMessagesManually([]);
11451
- return {
11452
- messages,
11453
- activeRun: null,
11454
- interrupts: null,
11455
- page: history.page_after ? {
11456
- truncated: true,
11457
- cursor: history.page_after
11458
- } : { truncated: false }
11459
- };
11460
- })();
11461
- if (!options?.before) {
11462
- hydration = read;
11463
- const settled = (failed) => queueMicrotask(() => {
11464
- if (generation === selectionGeneration && readGeneration === historyGeneration) update({
11465
- threadLoading: false,
11466
- threadLoadFailed: failed
11467
- });
11468
- });
11469
- read.then(() => settled(false), () => settled(true));
11470
- }
11471
- return read;
11472
- };
11452
+ const createClient = () => {
11473
11453
  const next = new ChatClient({
11474
- connection: adapter,
11475
- threadId: threadId ?? clientId,
11454
+ connection: {
11455
+ ...connection,
11456
+ hydrate: (_id, options) => {
11457
+ const threadId = selectedId;
11458
+ if (!threadId) return Promise.resolve({
11459
+ messages: [],
11460
+ activeRun: null,
11461
+ interrupts: null,
11462
+ page: { truncated: false }
11463
+ });
11464
+ const older = options?.before !== void 0;
11465
+ const readGeneration = older ? historyGeneration : ++historyGeneration;
11466
+ if (!options?.before) update({ history: state.messages.length > 0 || state.thread?.hasMessages === false ? "refreshing" : "loading" });
11467
+ const read = (async () => {
11468
+ let history = await loadThreadMessages(threadId, await requestOptions(), options?.before, liveToolMessageIds);
11469
+ while (options?.before && history.messages.every((message) => message.role === "tool") && history.page_after) {
11470
+ const earlier = await loadThreadMessages(threadId, await requestOptions(), history.page_after, liveToolMessageIds);
11471
+ history = {
11472
+ ...earlier,
11473
+ messages: [...earlier.messages, ...history.messages]
11474
+ };
11475
+ }
11476
+ if (client !== next || readGeneration !== historyGeneration) throw new DOMException("Conversation changed", "AbortError");
11477
+ if (!older) retainToolResults(history);
11478
+ threadRecords = older ? [...history.messages, ...threadRecords] : history.messages;
11479
+ const pageIds = new Set(history.messages.map((message) => message.id));
11480
+ const messages = projectThreadMessages(threadRecords, older ? state.pendingInteractions : history.pendingInteractions, liveToolMessageIds).filter((message) => pageIds.has(message.id));
11481
+ if (older) update({ messagesCursor: history.page_after ?? void 0 });
11482
+ else {
11483
+ if (history.thread.role === "viewer") disposeRenders();
11484
+ const agentChanged = state.thread?.agentId !== history.thread.agent_id;
11485
+ update({
11486
+ thread: threadFromRecord(history.thread, history.messages.length > 0),
11487
+ messagesCursor: history.page_after ?? void 0,
11488
+ pendingInteractions: history.pendingInteractions
11489
+ });
11490
+ if (agentChanged) resolveCapabilities();
11491
+ if (messages.length === 0) next.setMessagesManually([]);
11492
+ }
11493
+ restoreCompletedWidgets(messages);
11494
+ return {
11495
+ messages,
11496
+ activeRun: null,
11497
+ interrupts: null,
11498
+ page: history.page_after ? {
11499
+ truncated: true,
11500
+ cursor: history.page_after
11501
+ } : { truncated: false }
11502
+ };
11503
+ })();
11504
+ if (!older) {
11505
+ hydration = read.then(() => "idle", () => "failed").then((history) => new Promise((resolve) => queueMicrotask(() => {
11506
+ if (client === next && readGeneration === historyGeneration) update({ history });
11507
+ resolve();
11508
+ }))).then(() => read);
11509
+ hydration.catch(() => void 0);
11510
+ }
11511
+ return read;
11512
+ }
11513
+ },
11514
+ threadId: selectedId ?? clientId,
11476
11515
  persistence: true,
11477
11516
  history: { pageSize: 100 },
11478
11517
  queue: "drop",
11479
11518
  tools: declareTools(),
11480
11519
  forwardedProps: forwardedProps(),
11481
11520
  onMessagesChange: (messages) => {
11482
- if (client !== next || generation !== selectionGeneration) return;
11521
+ if (client !== next) return;
11483
11522
  restoreCompletedWidgets(messages);
11484
11523
  for (const message of messages) {
11485
11524
  if (message.role !== "user") continue;
@@ -11488,16 +11527,13 @@ function createAstralBeamChat(options, deferStart = false) {
11488
11527
  }
11489
11528
  update({
11490
11529
  messages,
11491
- sandbox: collectSandboxActivity(messages),
11492
- error: next.getError()
11530
+ sandbox: collectSandboxActivity(messages)
11493
11531
  });
11494
11532
  },
11495
11533
  onStatusChange: (status) => {
11496
- if (client === next && generation === selectionGeneration) update({
11497
- status,
11498
- error: next.getError(),
11499
- ...status === "submitted" ? { usageLimitReached: void 0 } : {}
11500
- });
11534
+ if (client !== next) return;
11535
+ clientStatus = status;
11536
+ update(status === "submitted" ? { usageLimitReached: void 0 } : {});
11501
11537
  },
11502
11538
  onInterruptStateChange: (interrupts) => {
11503
11539
  if (client === next) debug?.("tool", "interrupt state changed", interrupts);
@@ -11507,8 +11543,8 @@ function createAstralBeamChat(options, deferStart = false) {
11507
11543
  if (eventType === "astralbeam_thread") {
11508
11544
  const event = data;
11509
11545
  if (state.thread?.id === event.threadId) {
11510
- if (event.acceptedMessage && pendingSend && !pendingSend.accepted) replaceAcceptedMessage(event.acceptedMessage);
11511
- if (event.acceptedMessageId) acceptPendingSend();
11546
+ if (event.acceptedMessage && submission?.phase === "submitting") replaceAcceptedMessage(event.acceptedMessage);
11547
+ if (event.acceptedMessageId) acceptSubmission();
11512
11548
  if (event.saved) for (const id of event.executableToolCallIds ?? []) {
11513
11549
  if (!liveToolCalls.has(id)) toolResults.delete(toolResultKey(id));
11514
11550
  liveToolCalls.add(id);
@@ -11518,8 +11554,7 @@ function createAstralBeamChat(options, deferStart = false) {
11518
11554
  ...state.thread,
11519
11555
  version: event.version
11520
11556
  },
11521
- activeToolCallIds: [...liveToolCalls],
11522
- ...event.acceptedMessageId ? { unsentMessage: void 0 } : {}
11557
+ activeToolCallIds: [...liveToolCalls]
11523
11558
  });
11524
11559
  }
11525
11560
  return;
@@ -11565,20 +11600,24 @@ function createAstralBeamChat(options, deferStart = false) {
11565
11600
  if (client === next) live.streamCallbacks?.onFinish?.(message);
11566
11601
  },
11567
11602
  onError: (error) => {
11568
- if (client === next && (state.auth.status !== "error" || state.auth.error !== error)) live.streamCallbacks?.onError?.(error);
11603
+ if (client !== next) return;
11604
+ if (submission?.phase !== "submitting" && error.name !== "AbortError") update({ error });
11605
+ notifyError(error);
11569
11606
  }
11570
11607
  });
11571
11608
  return next;
11572
11609
  };
11610
+ let selectedId;
11573
11611
  let client = createClient();
11574
- const replaceClient = (threadId = state.thread?.id) => {
11575
- client.stop();
11612
+ const replaceClient = () => {
11576
11613
  const previous = client;
11614
+ client = createClient();
11615
+ clientStatus = "ready";
11616
+ previous.stop();
11577
11617
  previous.detach();
11578
11618
  liveToolCalls.clear();
11579
11619
  liveToolMessageIds.clear();
11580
11620
  update({ activeToolCallIds: [] });
11581
- client = createClient(threadId);
11582
11621
  if (started) client.attach();
11583
11622
  previous.dispose();
11584
11623
  };
@@ -11609,17 +11648,16 @@ function createAstralBeamChat(options, deferStart = false) {
11609
11648
  };
11610
11649
  const changeSelection = (threadId) => {
11611
11650
  const previousId = state.thread?.id;
11651
+ const released = releaseSubmission("The conversation changed before this was sent.");
11612
11652
  if (previousId) {
11613
- if (pendingSend && !pendingSend.accepted) pendingSends.set(previousId, pendingSend);
11614
- else pendingSends.delete(previousId);
11653
+ if (released?.phase === "failed") submissions.set(previousId, released);
11654
+ else submissions.delete(previousId);
11615
11655
  threadToolResults.set(previousId, toolResults);
11616
11656
  }
11617
11657
  selectionGeneration++;
11618
- historyGeneration++;
11619
- sending = false;
11658
+ selectedId = threadId;
11620
11659
  requestController.abort();
11621
11660
  requestController = new AbortController();
11622
- pendingSend = void 0;
11623
11661
  toolResults = threadToolResults.get(threadId ?? "") ?? /* @__PURE__ */ new Map();
11624
11662
  historicalToolIds.clear();
11625
11663
  threadRecords = [];
@@ -11627,9 +11665,8 @@ function createAstralBeamChat(options, deferStart = false) {
11627
11665
  update({
11628
11666
  thread: void 0,
11629
11667
  messages: [],
11630
- unsentMessage: void 0,
11668
+ submission: void 0,
11631
11669
  error: void 0,
11632
- status: "ready",
11633
11670
  sandbox: {
11634
11671
  files: [],
11635
11672
  commands: []
@@ -11639,41 +11676,30 @@ function createAstralBeamChat(options, deferStart = false) {
11639
11676
  pendingInteractions: [],
11640
11677
  serverToolTitles: {},
11641
11678
  unavailableMcpConnections: [],
11642
- threadLoadFailed: false,
11679
+ history: "idle",
11643
11680
  messagesCursor: void 0,
11644
11681
  olderMessagesLoading: false
11645
11682
  });
11646
- replaceClient(threadId);
11683
+ replaceClient();
11647
11684
  };
11648
11685
  const openThread = async (id, restoring = false) => {
11649
- if (!restoring) navigationGeneration++;
11650
11686
  changeSelection(id);
11651
11687
  const generation = selectionGeneration;
11652
- update({ threadLoading: true });
11688
+ submission = submissions.get(id);
11689
+ if (submission) update({ submission: project(submission) });
11653
11690
  try {
11654
- pendingSend = pendingSends.get(id);
11655
- update({ unsentMessage: pendingSend?.content });
11656
11691
  if (!started) client.attach();
11657
11692
  await hydration;
11658
11693
  if (generation !== selectionGeneration) return;
11659
11694
  if (threadKey) selectedThread(threadKey, id);
11660
11695
  } catch (error) {
11661
- if (generation === selectionGeneration) {
11662
- if (restoring && (live.threadId === void 0 || live.threadId === "auto") && isAstralBeamApiError(error) && error.status === 404) {
11663
- newThread();
11664
- return;
11665
- }
11666
- update({ threadLoadFailed: true });
11667
- reportError(error);
11668
- }
11669
- } finally {
11670
- if (generation === selectionGeneration) update({ threadLoading: false });
11696
+ if (generation !== selectionGeneration) return;
11697
+ if (restoring && (live.threadId === void 0 || live.threadId === "auto") && isAstralBeamApiError(error) && error.status === 404) newThread();
11698
+ else reportError(error);
11671
11699
  }
11672
11700
  };
11673
11701
  const newThread = () => {
11674
- navigationGeneration++;
11675
11702
  changeSelection();
11676
- update({ threadLoading: false });
11677
11703
  if (started) resolveCapabilities();
11678
11704
  if (threadKey) selectedThread(threadKey, null);
11679
11705
  else startWithNewThread = true;
@@ -11687,53 +11713,33 @@ function createAstralBeamChat(options, deferStart = false) {
11687
11713
  if (selected && selected !== "new") openThread(selected, restoring);
11688
11714
  else if (!restoring) newThread();
11689
11715
  };
11690
- const ensureThread = async () => {
11716
+ const ensureThread = async (sent, attempt) => {
11691
11717
  if (state.thread) return;
11692
- const generation = selectionGeneration;
11693
11718
  const options = await requestOptions();
11694
11719
  const record = await createChatThread(live.agentId ? { agent_id: live.agentId } : {}, options);
11695
- if (generation !== selectionGeneration) throw new DOMException("Conversation changed", "AbortError");
11696
- const thread = threadFromRecord(record, false);
11697
- update({ thread });
11698
- replaceClient();
11699
- await hydration;
11720
+ if (!owns(sent, attempt)) throw new DOMException("Send cancelled", "AbortError");
11721
+ selectedId = record.id;
11722
+ update({ thread: threadFromRecord(record, false) });
11700
11723
  resolveCapabilities();
11701
- if (threadKey) selectedThread(threadKey, thread.id);
11724
+ if (threadKey) selectedThread(threadKey, record.id);
11702
11725
  };
11703
- const refreshThread = async (preserveError = false, resetClient = true) => {
11704
- if (!state.thread?.id) return;
11726
+ const refreshThread = async (explicit = true) => {
11727
+ if (!state.thread) return;
11705
11728
  const generation = selectionGeneration;
11706
- const previousError = state.error;
11707
- update({
11708
- threadLoading: true,
11709
- olderMessagesLoading: false
11710
- });
11729
+ update({ olderMessagesLoading: false });
11711
11730
  try {
11712
- if (resetClient) replaceClient();
11713
- else {
11714
- client.detach();
11715
- client.attach();
11716
- }
11731
+ client.detach();
11732
+ client.attach();
11717
11733
  await hydration;
11718
- if (generation !== selectionGeneration) return;
11719
- if (preserveError && previousError) update({
11720
- error: previousError,
11721
- status: "error"
11722
- });
11734
+ if (explicit && generation === selectionGeneration) update({ error: void 0 });
11723
11735
  } catch (error) {
11724
- if (generation === selectionGeneration) {
11725
- if (isAstralBeamApiError(error) && error.status === 404) {
11726
- newThread();
11727
- return;
11728
- }
11729
- reportError(error);
11730
- }
11731
- } finally {
11732
- if (generation === selectionGeneration) update({ threadLoading: false });
11736
+ if (generation !== selectionGeneration) return;
11737
+ if (isAstralBeamApiError(error) && error.status === 404) newThread();
11738
+ else reportError(error);
11733
11739
  }
11734
11740
  };
11735
11741
  const loadOlderMessages = async () => {
11736
- if (!client.getHasOlderMessages() || state.olderMessagesLoading || state.threadLoading || sending) return;
11742
+ if (!client.getHasOlderMessages() || state.olderMessagesLoading || state.history !== "idle" || submission?.phase === "submitting") return;
11737
11743
  const generation = selectionGeneration;
11738
11744
  update({ olderMessagesLoading: true });
11739
11745
  try {
@@ -11755,11 +11761,7 @@ function createAstralBeamChat(options, deferStart = false) {
11755
11761
  }, await requestOptions());
11756
11762
  if (generation !== selectionGeneration) return false;
11757
11763
  const renamed = threadFromRecord(record, thread.hasMessages);
11758
- update({
11759
- thread: renamed,
11760
- error: client.getError(),
11761
- status: client.getStatus()
11762
- });
11764
+ update({ thread: renamed });
11763
11765
  return true;
11764
11766
  } catch (error) {
11765
11767
  if (generation === selectionGeneration) reportError(error);
@@ -11780,7 +11782,7 @@ function createAstralBeamChat(options, deferStart = false) {
11780
11782
  };
11781
11783
  const addToolResult = async (result) => {
11782
11784
  const generation = selectionGeneration;
11783
- if (sending || client.getIsLoading()) {
11785
+ if (submission?.phase === "submitting" || client.getIsLoading()) {
11784
11786
  reportError(/* @__PURE__ */ new Error("Wait for this client's current request to finish before responding."));
11785
11787
  return;
11786
11788
  }
@@ -11805,96 +11807,76 @@ function createAstralBeamChat(options, deferStart = false) {
11805
11807
  }
11806
11808
  scopeCompletedToolCalls();
11807
11809
  if (pending) {
11808
- replaceClient();
11809
- await hydration;
11810
+ client.stop();
11811
+ await refreshThread(false);
11810
11812
  if (generation !== selectionGeneration) return;
11811
- update({ threadLoading: false });
11812
11813
  await client.append({
11813
11814
  role: "tool",
11814
11815
  toolCallId: result.toolCallId,
11815
11816
  content: JSON.stringify(toolResults.get(toolResultKey(result.toolCallId)))
11816
11817
  });
11817
11818
  } else if ("tool" in result) await client.addToolResult(result);
11818
- if (generation === selectionGeneration) await refreshThread(client.getError() !== void 0, false);
11819
+ if (generation === selectionGeneration) await refreshThread(false);
11819
11820
  };
11820
11821
  const abandonToolCall = (toolCallId) => addToolResult({ toolCallId });
11821
- const sendMessage = async (content, callbacks) => {
11822
- const navigation = navigationGeneration;
11822
+ const deliver = async (sent) => {
11823
+ const attempt = ++sent.attempt;
11823
11824
  try {
11824
11825
  await getValidChatAuthToken(authentication);
11825
- } catch (error) {
11826
- if (navigation !== navigationGeneration) return;
11827
- update({ unsentMessage: content });
11828
- reportError(error);
11829
- return;
11830
- }
11831
- if (navigation !== navigationGeneration) return;
11832
- if (sending || state.threadLoading || state.status === "streaming") {
11833
- reportError(/* @__PURE__ */ new Error("Wait for this client's current request to finish before sending."));
11834
- return;
11835
- }
11836
- if (state.threadLoadFailed) {
11837
- reportError(/* @__PURE__ */ new Error("Reopen this conversation or start a new one before sending."));
11838
- return;
11839
- }
11840
- const samePendingContent = pendingSend !== void 0 && JSON.stringify(pendingSend.content) === JSON.stringify(content);
11841
- const uncertainSend = pendingSend?.tools !== void 0 && !pendingSend.accepted ? pendingSend : void 0;
11842
- const retrying = uncertainSend !== void 0 && samePendingContent;
11843
- if (state.thread?.agentId === null && !retrying) {
11844
- reportError(/* @__PURE__ */ new Error("This conversation’s agent is unavailable. Start a new conversation to continue."));
11845
- return;
11846
- }
11847
- if (state.thread?.role === "viewer" && !retrying) {
11848
- reportError(/* @__PURE__ */ new Error("You have read-only access to this conversation."));
11849
- return;
11850
- }
11851
- if (uncertainSend && !samePendingContent) {
11852
- update({ unsentMessage: uncertainSend.content });
11853
- reportError(/* @__PURE__ */ new Error("The previous message’s acceptance is unconfirmed. Retry that message before sending different text, or start a new conversation."));
11854
- return;
11855
- }
11856
- const generation = selectionGeneration;
11857
- historyGeneration++;
11858
- update({ olderMessagesLoading: false });
11859
- sending = true;
11860
- if (!pendingSend || pendingSend.accepted || !samePendingContent) {
11861
- pendingSend = {
11862
- content,
11863
- accepted: false,
11864
- key: newUuid(),
11865
- callbacks
11866
- };
11867
- client.stop();
11868
- liveToolCalls.clear();
11826
+ await hydration.catch(() => void 0);
11827
+ if (!owns(sent, attempt)) return project(sent);
11828
+ if (state.history === "failed") throw new Error(HISTORY_FAILED);
11829
+ await ensureThread(sent, attempt);
11830
+ if (!owns(sent, attempt)) return project(sent);
11831
+ sent.callbacks?.onThreadReady?.(state.thread.id);
11832
+ if (!owns(sent, attempt)) return project(sent);
11869
11833
  scopeCompletedToolCalls();
11870
- liveToolMessageIds.clear();
11871
- update({ activeToolCallIds: [] });
11834
+ await client.sendMessage(sent.content);
11835
+ if (!current(sent, attempt)) return project(sent);
11836
+ endUnaccepted(sent, client.getError());
11837
+ await refreshThread(false);
11838
+ } catch (error) {
11839
+ if (!current(sent, attempt)) return project(sent);
11840
+ const failure = error instanceof Error ? error : new Error(String(error));
11841
+ if (failure.name === "AbortError") endUnaccepted(sent);
11842
+ else if (endUnaccepted(sent, failure)) notifyError(failure);
11872
11843
  }
11873
- pendingSend.callbacks ??= callbacks;
11844
+ return project(sent);
11845
+ };
11846
+ const sendMessage = async (content, callbacks) => {
11847
+ const rejection = submission?.phase === "submitting" || state.status === "streaming" || client.getIsLoading() ? "Wait for this client's current request to finish before sending." : state.history === "loading" ? "Wait for this conversation to load before sending." : state.history === "failed" ? HISTORY_FAILED : state.thread?.agentId === null ? "This conversation’s agent is unavailable. Start a new conversation to continue." : state.thread?.role === "viewer" ? "You have read-only access to this conversation." : submission?.phase === "failed" ? "The previous message’s acceptance is unconfirmed. Retry that message before sending different text, or start a new conversation." : void 0;
11848
+ if (rejection) return {
11849
+ id: newUuid(),
11850
+ content,
11851
+ phase: "refused",
11852
+ error: new Error(rejection)
11853
+ };
11854
+ const sent = {
11855
+ id: newUuid(),
11856
+ content,
11857
+ phase: "submitting",
11858
+ key: newUuid(),
11859
+ callbacks,
11860
+ attempt: 0
11861
+ };
11862
+ submission = sent;
11874
11863
  update({
11875
- unsentMessage: content,
11864
+ submission: project(sent),
11876
11865
  error: void 0,
11877
- status: "submitted"
11866
+ olderMessagesLoading: false
11878
11867
  });
11879
- try {
11880
- await ensureThread();
11881
- if (generation !== selectionGeneration) return;
11882
- pendingSend.callbacks?.onThreadReady?.(state.thread.id);
11883
- if (generation !== selectionGeneration) return;
11884
- scopeCompletedToolCalls();
11885
- await client.sendMessage(content);
11886
- if (generation !== selectionGeneration) return;
11887
- const failed = client.getError() !== void 0;
11888
- if (pendingSend?.accepted) {
11889
- pendingSend = void 0;
11890
- update({ unsentMessage: void 0 });
11891
- }
11892
- await refreshThread(failed, false);
11893
- } catch (error) {
11894
- if (generation === selectionGeneration) reportError(error);
11895
- } finally {
11896
- if (generation === selectionGeneration) sending = false;
11897
- }
11868
+ client.stop();
11869
+ liveToolCalls.clear();
11870
+ scopeCompletedToolCalls();
11871
+ liveToolMessageIds.clear();
11872
+ update({ activeToolCallIds: [] });
11873
+ const refused = new Promise((resolve) => sent.onRefused = resolve);
11874
+ return Promise.race([deliver(sent), refused.then(() => project(sent))]);
11875
+ };
11876
+ const retry = async () => {
11877
+ if (!submission || !settle(submission, "submitting")) return void 0;
11878
+ update({ error: void 0 });
11879
+ return deliver(submission);
11898
11880
  };
11899
11881
  const observeAuthentication = () => {
11900
11882
  const auth = authenticationState(authentication);
@@ -11902,16 +11884,15 @@ function createAstralBeamChat(options, deferStart = false) {
11902
11884
  if (auth.status === "ready") {
11903
11885
  const nextIdentity = authenticationIdentity(auth.currentUser);
11904
11886
  if (identity !== void 0 && identity !== nextIdentity) {
11905
- navigationGeneration++;
11906
- pendingSends.clear();
11887
+ releaseSubmission("The account changed before this was sent.");
11888
+ submissions.clear();
11907
11889
  threadToolResults.clear();
11908
11890
  toolResults.clear();
11909
- pendingSend = void 0;
11910
11891
  }
11911
11892
  identity = nextIdentity;
11912
11893
  const nextThreadKey = `astralbeam:thread:${live.apiUrl ?? "https://astralbeam.ai/api"}:${nextIdentity}`;
11913
11894
  if (threadKey !== nextThreadKey) {
11914
- changeSelection();
11895
+ if (threadKey !== void 0) changeSelection();
11915
11896
  threadKey = nextThreadKey;
11916
11897
  if (startWithNewThread) {
11917
11898
  selectedThread(nextThreadKey, null);
@@ -11963,22 +11944,24 @@ function createAstralBeamChat(options, deferStart = false) {
11963
11944
  forwardedProps: forwardedProps()
11964
11945
  });
11965
11946
  if (live.apiUrl !== apiUrl) {
11966
- navigationGeneration++;
11967
- pendingSends.clear();
11947
+ releaseSubmission("The chat service changed before this was sent.");
11948
+ submissions.clear();
11968
11949
  threadToolResults.clear();
11969
11950
  toolResults.clear();
11970
- pendingSend = void 0;
11971
11951
  changeSelection();
11972
11952
  threadKey = void 0;
11973
- update({ threadLoading: false });
11974
11953
  observeAuthentication();
11975
11954
  } else if (live.threadId !== selectedId) selectConfiguredThread();
11976
11955
  if (widgets !== live.widgets || onRenderWidget !== live.onRenderWidget) restoreCompletedWidgets(state.messages);
11977
11956
  if (started && (live.agentId !== agent || live.apiUrl !== apiUrl)) resolveCapabilities();
11978
11957
  },
11979
11958
  sendMessage,
11959
+ retry,
11980
11960
  addToolResult,
11981
- stop: () => client.stop(),
11961
+ stop: () => {
11962
+ if (submission?.phase === "submitting") endUnaccepted(submission);
11963
+ client.stop();
11964
+ },
11982
11965
  retryAuthentication: () => {
11983
11966
  getValidChatAuthToken({
11984
11967
  ...authentication,
@@ -12016,7 +11999,7 @@ function createAstralBeamChat(options, deferStart = false) {
12016
11999
  openThread,
12017
12000
  renameThread,
12018
12001
  deleteThread,
12019
- refreshThread,
12002
+ refreshThread: () => refreshThread(),
12020
12003
  loadOlderMessages,
12021
12004
  uploadAttachment: async ({ data, filename, mimeType }) => {
12022
12005
  const agentId = state.thread ? state.thread.agentId : live.agentId;
@@ -12062,6 +12045,7 @@ function createAstralBeamChat(options, deferStart = false) {
12062
12045
  newThread();
12063
12046
  },
12064
12047
  dispose: () => {
12048
+ releaseSubmission("The chat closed before this was sent.");
12065
12049
  client.detach();
12066
12050
  disposeRenders();
12067
12051
  client.stop();