@artooi/ag-ui-web-component 0.26.1 → 0.27.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@artooi/ag-ui-web-component",
3
- "version": "0.26.1",
3
+ "version": "0.27.0",
4
4
  "description": "Framework-free <ag-ui-chat> Web Component over the AG-UI protocol. Drop-in chat sidebar with a pluggable client-side tool registry, DOM driver primitives, animations, and destructive-action confirmation modal.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -86,7 +86,7 @@ import { RemoteConversationStore } from "./remote_conversation_store.js";
86
86
  import { RunIndex } from "./run_index.js";
87
87
  import { type TranscribeHandler, transcribeAudio } from "./transcribe_audio.js";
88
88
  import { type UploadHandler, uploadAttachment } from "./upload_attachment.js";
89
- import { withCredentials } from "./utils.js";
89
+ import { mintThread, withCredentials } from "./utils.js";
90
90
 
91
91
  /** The role a rendered chat message takes. */
92
92
  export type MessageRole = (typeof MESSAGE_ROLE)[keyof typeof MESSAGE_ROLE];
@@ -1653,16 +1653,25 @@ export class AgUiChat extends HTMLElement {
1653
1653
  }
1654
1654
 
1655
1655
  /**
1656
- * Start a fresh conversation: forget the persisted history, drop the
1657
- * in-memory run state, clear the transcript, and mint a new thread id.
1656
+ * Start a fresh conversation: drop the in-memory run state, clear the
1657
+ * transcript, and mint a new thread id.
1658
+ *
1659
+ * The conversation being left is kept, and stays in the history drawer to
1660
+ * return to. Deleting one is the drawer row's own action; a button that
1661
+ * starts something new must not be the button that destroys what was there.
1658
1662
  */
1659
1663
  newChat(): void {
1660
1664
  // Stop any in-flight run first — discarding the client mid-run would
1661
1665
  // leave the old agent streaming into a cleared transcript.
1662
1666
  this.#cancelRun();
1663
- this.conversationStore.clear(this.#threadId);
1667
+ // A thread nothing was ever sent in has nothing to come back to, and the
1668
+ // drawer never listed it — so reap it here rather than strand one record
1669
+ // per press of a button whose whole use is being pressed again.
1670
+ if (this.conversationStore.isUnsent?.(this.#threadId) === true) {
1671
+ this.conversationStore.clear(this.#threadId);
1672
+ }
1664
1673
  this.#resetState();
1665
- this.#threadId = this.conversationStore.threadId();
1674
+ this.#threadId = mintThread(this.conversationStore);
1666
1675
  this.#setRunning(false);
1667
1676
  this.#setUnread(0);
1668
1677
  }
@@ -408,6 +408,9 @@ export class AgUiClient {
408
408
  #buildSubscriber(pending: AgUiToolCall[], runState: RunState): AgentSubscriber {
409
409
  const h = this.#handlers;
410
410
  const closed = this.#closedMessageIds;
411
+ // Read at event time, not captured now: the flag flips mid-run, and the
412
+ // subscriber is built before the run that a later `cancel()` stops.
413
+ const cancelled = (): boolean => this.#cancelled;
411
414
  // Charts whose patch has been dispatched but not yet applied. Scoped to the
412
415
  // subscriber, so it cannot outlive the run that created it.
413
416
  const pendingDeltas = new Set<string>();
@@ -510,6 +513,15 @@ export class AgUiClient {
510
513
  onRunErrorEvent({ event }) {
511
514
  runState.terminal = true;
512
515
  runState.errored = true;
516
+ // Cancelling aborts the response mid-read, and the browser's own words
517
+ // for that can arrive here as a RUN_ERROR — Chrome's is
518
+ // "BodyStreamBuffer was aborted". The run is over either way, but a
519
+ // deliberate stop is not a failure, and reporting it would raise a
520
+ // warning bubble above the stopped note saying the same thing twice.
521
+ // The promise route in `#run` reports the cancellation.
522
+ if (cancelled()) {
523
+ return;
524
+ }
513
525
  h.onError(event.message);
514
526
  },
515
527
  onRunFinalized() {
@@ -532,7 +544,19 @@ interface RunState {
532
544
  * Whether a rejection came from aborting the run's fetch. Belt-and-suspenders
533
545
  * with the `#cancelled` flag: some `@ag-ui/client` versions re-throw the
534
546
  * `AbortError` instead of filtering it.
547
+ *
548
+ * Aborting a fetch whose body is mid-read does not always surface as an
549
+ * `AbortError`. Chrome raises `TypeError: BodyStreamBuffer was aborted`, which
550
+ * is the same event wearing a different name, so a message naming the abort is
551
+ * read as one too. Narrow on purpose: only a `TypeError`, and only when it says
552
+ * so — a genuine type error carries no such word, and misreading one as a
553
+ * cancellation would hide a real failure behind a stopped note.
535
554
  */
536
555
  function isAbortError(error: unknown): boolean {
537
- return error instanceof Error && error.name === "AbortError";
556
+ if (!(error instanceof Error)) {
557
+ return false;
558
+ }
559
+ return (
560
+ error.name === "AbortError" || (error instanceof TypeError && /abort/i.test(error.message))
561
+ );
538
562
  }
@@ -36,12 +36,24 @@ export interface ThreadMeta {
36
36
  * is a small local hint a server store can derive from history and no-op.
37
37
  *
38
38
  * Thread enumeration backs the chat-history drawer; deleting a thread reuses
39
- * {@link clear} and "new chat" reuses {@link threadId} after clearing the
40
- * active thread.
39
+ * {@link clear}, and "new chat" is {@link newThread} which leaves the
40
+ * conversation it moves off of intact, for the drawer to offer back.
41
41
  */
42
42
  export interface ClientConversationStore {
43
43
  /** The active conversation id, generated and persisted on first read. */
44
44
  threadId(): string;
45
+ /**
46
+ * Start a fresh conversation, make it active, and return its id.
47
+ *
48
+ * Existing threads are left where they are: "new chat" adds one, and
49
+ * {@link clear} is the only method that takes one away.
50
+ *
51
+ * Optional, so a store written before this method existed still works. The
52
+ * caller then mints the id itself and hands it to {@link setActiveThread},
53
+ * which loses only the store's own record that the thread is new (see
54
+ * {@link isUnsent}).
55
+ */
56
+ newThread?(): string;
45
57
  /** Load the persisted message history, or `null` when none exists. */
46
58
  loadMessages(threadId: string): Promise<readonly Message[] | null>;
47
59
  /** Persist the message history (and refresh the thread's drawer metadata). */
@@ -123,13 +135,12 @@ export class SessionStorageStore implements ClientConversationStore {
123
135
  }
124
136
 
125
137
  threadId(): string {
126
- const key = this.#key(THREAD_SUFFIX);
127
- const existing = sessionStorage.getItem(key);
128
- if (existing !== null) {
129
- return existing;
130
- }
138
+ return sessionStorage.getItem(this.#key(THREAD_SUFFIX)) ?? this.newThread();
139
+ }
140
+
141
+ newThread(): string {
131
142
  const id = randomUUID();
132
- sessionStorage.setItem(key, id);
143
+ sessionStorage.setItem(this.#key(THREAD_SUFFIX), id);
133
144
  sessionStorage.setItem(this.#key(MINTED_SUFFIX + id), "1");
134
145
  return id;
135
146
  }
@@ -5,7 +5,7 @@ import {
5
5
  SessionStorageStore,
6
6
  type ThreadMeta,
7
7
  } from "./conversation_store.js";
8
- import { withCredentials } from "./utils.js";
8
+ import { mintThread, withCredentials } from "./utils.js";
9
9
 
10
10
  /** One row of the server thread index (django-ag-ui's `ThreadsView` wire shape). */
11
11
  interface ServerThreadRow {
@@ -69,6 +69,15 @@ export class RemoteConversationStore implements ClientConversationStore {
69
69
  this.#local.setActiveThread(threadId);
70
70
  }
71
71
 
72
+ /**
73
+ * Delegated to the local store, which owns the active id — and deliberately
74
+ * silent on the wire: the server learns of a thread when its first message is
75
+ * persisted, so an abandoned new chat costs no round-trip and leaves no row.
76
+ */
77
+ newThread(): string {
78
+ return mintThread(this.#local);
79
+ }
80
+
72
81
  /** Delegated, so wrapping a store does not lose what it knows about its own ids. */
73
82
  isUnsent(threadId: string): boolean {
74
83
  return this.#local.isUnsent?.(threadId) === true;
package/src/core/utils.ts CHANGED
@@ -1,4 +1,7 @@
1
- // Non-exported-from-index helpers shared by the core transport modules.
1
+ // Non-exported-from-index helpers shared by the core modules.
2
+
3
+ import { randomUUID } from "@ag-ui/client";
4
+ import type { ClientConversationStore } from "./conversation_store.js";
2
5
 
3
6
  /**
4
7
  * Overlay a `credentials` mode onto a fetch `init`, or hand the `init` back
@@ -14,3 +17,24 @@ export function withCredentials(
14
17
  ): RequestInit | undefined {
15
18
  return credentials === undefined ? init : { ...init, credentials };
16
19
  }
20
+
21
+ /**
22
+ * Start a new conversation in `store` and return its id.
23
+ *
24
+ * `newThread` is optional on the interface, so a store that predates it is
25
+ * driven the only other way the interface allows: mint an id here and make it
26
+ * active. That path loses the store's own note that the thread is new, so a
27
+ * remote store would go on to ask the server for a conversation that cannot
28
+ * exist yet — which is why every store in this package implements the method.
29
+ *
30
+ * What neither path does is clear the thread being left behind. Starting a
31
+ * conversation is not a reason to destroy the previous one.
32
+ */
33
+ export function mintThread(store: ClientConversationStore): string {
34
+ if (store.newThread !== undefined) {
35
+ return store.newThread();
36
+ }
37
+ const id = randomUUID();
38
+ store.setActiveThread(id);
39
+ return id;
40
+ }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION: string = "0.26.1";
1
+ export const VERSION: string = "0.27.0";