@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/CHANGELOG.md +45 -1
- package/README.md +5 -2
- package/dist/ag-ui-web-component.bundle.js +20 -20
- package/dist/ag-ui-web-component.bundle.js.map +3 -3
- package/dist/core/ag_ui_chat.d.ts +6 -2
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/core/conversation_store.d.ts +15 -2
- package/dist/core/conversation_store.d.ts.map +1 -1
- package/dist/core/remote_conversation_store.d.ts +6 -0
- package/dist/core/remote_conversation_store.d.ts.map +1 -1
- package/dist/core/utils.d.ts +14 -0
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/index.js +40 -12
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
- package/src/core/ag_ui_chat.ts +14 -5
- package/src/core/agui_client.ts +25 -1
- package/src/core/conversation_store.ts +19 -8
- package/src/core/remote_conversation_store.ts +10 -1
- package/src/core/utils.ts +25 -1
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@artooi/ag-ui-web-component",
|
|
3
|
-
"version": "0.
|
|
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",
|
package/src/core/ag_ui_chat.ts
CHANGED
|
@@ -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:
|
|
1657
|
-
*
|
|
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
|
-
|
|
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
|
|
1674
|
+
this.#threadId = mintThread(this.conversationStore);
|
|
1666
1675
|
this.#setRunning(false);
|
|
1667
1676
|
this.#setUnread(0);
|
|
1668
1677
|
}
|
package/src/core/agui_client.ts
CHANGED
|
@@ -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
|
-
|
|
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"
|
|
40
|
-
*
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
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.
|
|
1
|
+
export const VERSION: string = "0.27.0";
|