@artooi/ag-ui-web-component 0.15.0 → 0.17.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 +158 -1
- package/README.md +42 -4
- package/dist/ag-ui-web-component.bundle.js +91 -58
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +21 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +41 -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.map +1 -1
- package/dist/core/remote_conversation_store.d.ts.map +1 -1
- package/dist/core/run_index.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +534 -325
- package/dist/index.js.map +4 -4
- package/dist/tools/client_tool_registry.d.ts.map +1 -1
- package/dist/tools/page_action_tools.d.ts.map +1 -1
- package/dist/tools/route_map.d.ts.map +1 -1
- package/dist/ui/attachment_tray.d.ts +2 -0
- package/dist/ui/attachment_tray.d.ts.map +1 -1
- package/dist/ui/checkpoint_menu.d.ts.map +1 -1
- package/dist/ui/skills_menu.d.ts.map +1 -1
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/thoughts_block.d.ts.map +1 -1
- package/dist/ui/thread_drawer.d.ts.map +1 -1
- package/dist/ui/tool_call_card.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +6 -0
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/dist/ui/voice_input.d.ts.map +1 -1
- package/package.json +9 -8
- package/src/constants.ts +23 -0
- package/src/core/ag_ui_chat.ts +243 -15
- package/src/core/remote_conversation_store.ts +2 -2
- package/src/index.ts +2 -0
- package/src/tools/route_map.ts +5 -8
- package/src/ui/attachment_tray.ts +5 -0
- package/src/ui/checkpoint_menu.ts +2 -0
- package/src/ui/styles.ts +38 -6
- package/src/ui/ui_strings.ts +11 -0
- package/src/version.ts +1 -1
package/src/core/ag_ui_chat.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { Context, Interrupt, Message, Tool } from "@ag-ui/core";
|
|
2
2
|
import {
|
|
3
|
+
ATTACHMENT_EVENT,
|
|
3
4
|
COMPACTION_ACTIVITY_TYPE,
|
|
4
5
|
DEFAULT_ATTACHMENT_MAX_BYTES,
|
|
5
6
|
LOAD_CAPABILITY_TOOL,
|
|
6
7
|
MESSAGE_ROLE,
|
|
8
|
+
READ_PAGE_TOOL,
|
|
7
9
|
STATE_EVENT,
|
|
8
10
|
SUBMIT_EVENT,
|
|
9
11
|
TOGGLE_EVENT,
|
|
@@ -77,6 +79,14 @@ export interface SubmitDetail {
|
|
|
77
79
|
readonly attachments: readonly AttachmentRef[];
|
|
78
80
|
}
|
|
79
81
|
|
|
82
|
+
/** `detail` shape of the {@link ATTACHMENT_EVENT} CustomEvent. */
|
|
83
|
+
export interface AttachmentsDetail {
|
|
84
|
+
/** Durable refs for every file that has finished uploading. */
|
|
85
|
+
readonly attachments: readonly AttachmentRef[];
|
|
86
|
+
/** How many files are still uploading; a send now would leave these behind. */
|
|
87
|
+
readonly pending: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
80
90
|
/** `detail` shape of the {@link STATE_EVENT} CustomEvent. */
|
|
81
91
|
export interface StateDetail {
|
|
82
92
|
readonly state: Readonly<Record<string, unknown>>;
|
|
@@ -87,6 +97,35 @@ export interface ToggleDetail {
|
|
|
87
97
|
readonly collapsed: boolean;
|
|
88
98
|
}
|
|
89
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Attributes read once while connecting, to decide what chrome exists at all.
|
|
102
|
+
*
|
|
103
|
+
* Changing one afterwards is silently ignored: the tray, the mic, the skills
|
|
104
|
+
* menu and the header icon are built (or not) during connect, and no later read
|
|
105
|
+
* revisits the decision. Observed only so the element can say so — see
|
|
106
|
+
* `attributeChangedCallback`.
|
|
107
|
+
*
|
|
108
|
+
* Deliberately excludes the attributes that *are* re-read per use, where a late
|
|
109
|
+
* change works and a warning would be wrong: `data-runs-url`,
|
|
110
|
+
* `data-page-actions`, `data-text-animation`, `data-tool-display`, `endpoint`,
|
|
111
|
+
* and the CSS-reactive `theme` / `collapsed`.
|
|
112
|
+
*/
|
|
113
|
+
const CONNECT_TIME_ATTRIBUTES = [
|
|
114
|
+
"data-attachments-url",
|
|
115
|
+
"data-attachment-accept",
|
|
116
|
+
"data-attachment-max-bytes",
|
|
117
|
+
"data-transcribe-url",
|
|
118
|
+
"data-threads-url",
|
|
119
|
+
"data-tools-url",
|
|
120
|
+
"data-skills-url",
|
|
121
|
+
"data-skills",
|
|
122
|
+
"data-prompt-chips",
|
|
123
|
+
"data-slash-commands",
|
|
124
|
+
"data-theme-toggle",
|
|
125
|
+
"data-strings",
|
|
126
|
+
"data-icon-url",
|
|
127
|
+
] as const;
|
|
128
|
+
|
|
90
129
|
/** Per-tab persistence key for the collapsed state (survives MPA reloads). */
|
|
91
130
|
const COLLAPSED_KEY = "ag-ui-chat:collapsed";
|
|
92
131
|
|
|
@@ -327,6 +366,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
327
366
|
readonly #voiceSlot: HTMLSpanElement;
|
|
328
367
|
/** Voice-input control; created on connect when transcription is available. */
|
|
329
368
|
#voice: VoiceInput | null = null;
|
|
369
|
+
/** Whether the element is currently in the DOM; gates the connect-time warning. */
|
|
370
|
+
#connected = false;
|
|
371
|
+
|
|
330
372
|
/** Refs attached to the message currently being sent (the context manifest). */
|
|
331
373
|
#runAttachments: readonly AttachmentRef[] = [];
|
|
332
374
|
|
|
@@ -339,6 +381,10 @@ export class AgUiChat extends HTMLElement {
|
|
|
339
381
|
// the Send⇄Stop button: `agent.isRunning` is false between frontend-tool
|
|
340
382
|
// rounds, but the user must still be able to stop there.
|
|
341
383
|
#running = false;
|
|
384
|
+
// The page the current round's context describes, captured when that context
|
|
385
|
+
// was built. `null` until the first round. Compared in `#executeTool` to
|
|
386
|
+
// catch a page that moved under a round still in flight.
|
|
387
|
+
#contextHref: string | null = null;
|
|
342
388
|
// Aborting this dismisses (declines) an open confirmation card when the run
|
|
343
389
|
// is cancelled while the card awaits a decision. One controller per card.
|
|
344
390
|
#confirmAbort: AbortController | null = null;
|
|
@@ -461,7 +507,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
461
507
|
agent,
|
|
462
508
|
handlers: this.#handlers(),
|
|
463
509
|
getTools: () => this.getTools(),
|
|
464
|
-
getContext: () => this
|
|
510
|
+
getContext: () => this.#buildContext(),
|
|
465
511
|
executeTool: (call) => this.#executeTool(call),
|
|
466
512
|
resolveInterrupts: (interrupts) => this.#resolveInterrupts(interrupts),
|
|
467
513
|
connectionLostMessage: this.#strings.connectionLost,
|
|
@@ -475,16 +521,36 @@ export class AgUiChat extends HTMLElement {
|
|
|
475
521
|
this.#checkpoints.setRuns(index === null ? [] : await index.continuable());
|
|
476
522
|
}
|
|
477
523
|
|
|
478
|
-
/** Attributes
|
|
524
|
+
/** Attributes the element reacts to after it has been connected. */
|
|
479
525
|
static get observedAttributes(): string[] {
|
|
480
|
-
return ["title-text"];
|
|
526
|
+
return ["title-text", ...CONNECT_TIME_ATTRIBUTES];
|
|
481
527
|
}
|
|
482
528
|
|
|
483
|
-
attributeChangedCallback(
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
529
|
+
attributeChangedCallback(name: string, previous: string | null, value: string | null): void {
|
|
530
|
+
if (name === "title-text") {
|
|
531
|
+
// `#strings` is the resolved table once connected, the English defaults
|
|
532
|
+
// before then.
|
|
533
|
+
this.#title.textContent = value ?? this.#strings.title;
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
// Everything else here is read once, in connectedCallback, to build chrome
|
|
537
|
+
// that then exists (or does not). Setting it afterwards is silently
|
|
538
|
+
// ignored, and the symptom is an affordance that simply never appears —
|
|
539
|
+
// which reads as a broken component rather than a mis-timed assignment.
|
|
540
|
+
// This is the common React/Vue shape: the element mounts on the first
|
|
541
|
+
// render pass and the framework patches attributes in on the next one.
|
|
542
|
+
// Observed purely so this can be said out loud.
|
|
543
|
+
if (previous === value || !this.#connected) {
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
console.warn(
|
|
547
|
+
`<ag-ui-chat>: "${name}" was changed after the element connected, and is ` +
|
|
548
|
+
"read only while connecting — this assignment has no effect. Set it " +
|
|
549
|
+
"before the element enters the DOM (in the markup, or on the element " +
|
|
550
|
+
"before appending it); frameworks that patch attributes after mount " +
|
|
551
|
+
"should bind it at creation. To apply a new value now, remove and " +
|
|
552
|
+
"re-insert the element.",
|
|
553
|
+
);
|
|
488
554
|
}
|
|
489
555
|
|
|
490
556
|
/** Declare a frontend tool the agent may call. */
|
|
@@ -555,7 +621,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
555
621
|
}
|
|
556
622
|
return [
|
|
557
623
|
{
|
|
558
|
-
name:
|
|
624
|
+
name: READ_PAGE_TOOL,
|
|
559
625
|
description:
|
|
560
626
|
"Read the current page's structure (fields, buttons, route). Call after " +
|
|
561
627
|
"acting to observe the result within the same turn.",
|
|
@@ -746,6 +812,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
746
812
|
this.#wireVoice();
|
|
747
813
|
this.#threadId = this.conversationStore.threadId();
|
|
748
814
|
void this.#rehydrate();
|
|
815
|
+
// Last: everything above reads (and some of it sets) attributes, and none
|
|
816
|
+
// of that should trip the connect-time warning.
|
|
817
|
+
this.#connected = true;
|
|
749
818
|
}
|
|
750
819
|
|
|
751
820
|
/**
|
|
@@ -757,6 +826,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
757
826
|
* `MediaRecorder`.
|
|
758
827
|
*/
|
|
759
828
|
disconnectedCallback(): void {
|
|
829
|
+
this.#connected = false;
|
|
760
830
|
this.#cancelRun();
|
|
761
831
|
this.#attachTray?.dispose();
|
|
762
832
|
this.#voice?.dispose();
|
|
@@ -793,12 +863,20 @@ export class AgUiChat extends HTMLElement {
|
|
|
793
863
|
return;
|
|
794
864
|
}
|
|
795
865
|
const accept = this.getAttribute("data-attachment-accept") ?? "";
|
|
796
|
-
|
|
866
|
+
// Bound to the local rather than the field: the hook can only fire from a
|
|
867
|
+
// tray that exists, so passing it removes a null check no caller can reach.
|
|
868
|
+
const tray: AttachmentTray = new AttachmentTray({
|
|
797
869
|
upload,
|
|
798
870
|
maxBytes: this.#attachmentMaxBytes(),
|
|
799
871
|
accept,
|
|
800
872
|
strings: this.#strings,
|
|
873
|
+
// The tray's change hook, surfaced to the host as an event. A host
|
|
874
|
+
// driving its own composer could otherwise not tell a settled upload from
|
|
875
|
+
// one still in flight, which is the state sendMessage() has to be called
|
|
876
|
+
// with knowledge of.
|
|
877
|
+
onChange: () => this.#dispatchAttachments(tray),
|
|
801
878
|
});
|
|
879
|
+
this.#attachTray = tray;
|
|
802
880
|
this.#attachSlot.appendChild(this.#attachTray.element);
|
|
803
881
|
this.#fileInput.accept = accept;
|
|
804
882
|
this.#attachButton.hidden = false;
|
|
@@ -1181,7 +1259,62 @@ export class AgUiChat extends HTMLElement {
|
|
|
1181
1259
|
const checkpoint = this.conversationStore.loadCheckpoint(this.#threadId);
|
|
1182
1260
|
if (checkpoint !== null) {
|
|
1183
1261
|
await this.#resumeFrom(checkpoint);
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
this.#noticeIfRunUnfinished(messages);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/**
|
|
1268
|
+
* Notice a previous run that never produced a response.
|
|
1269
|
+
*
|
|
1270
|
+
* {@link AgUiClient.send} persists the user's message *before* starting the
|
|
1271
|
+
* run, so a transcript whose last entry is that user message means nothing
|
|
1272
|
+
* ever came back: the page navigated or reloaded mid-run, the tab closed, or
|
|
1273
|
+
* the process died. No extra persistence is needed to detect it — the shape
|
|
1274
|
+
* of the transcript already says so, which is why this needs no
|
|
1275
|
+
* {@link ClientConversationStore} method and no `pagehide` listener (neither
|
|
1276
|
+
* of which fires on a crash or a force-quit anyway).
|
|
1277
|
+
*
|
|
1278
|
+
* The *agent*-initiated reload is not this case: a navigating tool leaves a
|
|
1279
|
+
* checkpoint and resumes, which is why the caller returns early on one before
|
|
1280
|
+
* reaching here.
|
|
1281
|
+
*
|
|
1282
|
+
* Deliberately a notice and never a resume. AG-UI has no
|
|
1283
|
+
* resume-an-aborted-run primitive; re-sending the accumulated messages is
|
|
1284
|
+
* semantically a **new** run, so any server-side tool the agent had already
|
|
1285
|
+
* executed before the interruption would run a second time. Saying plainly
|
|
1286
|
+
* that the answer was lost is the honest option, and the user can re-ask.
|
|
1287
|
+
*/
|
|
1288
|
+
/**
|
|
1289
|
+
* Build a round's context, recording which page it describes.
|
|
1290
|
+
*
|
|
1291
|
+
* The AG-UI client re-invokes this at the top of **every** tool round, not
|
|
1292
|
+
* once per `send()`, so the page map the agent sees is already refreshed
|
|
1293
|
+
* between rounds and the href captured here is the page it was shown for
|
|
1294
|
+
* *this* round. {@link #executeTool} compares against it to catch a page that
|
|
1295
|
+
* moved under a round still in flight.
|
|
1296
|
+
*/
|
|
1297
|
+
#buildContext(): Context[] {
|
|
1298
|
+
this.#contextHref = window.location.href;
|
|
1299
|
+
return this.getContext();
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
/**
|
|
1303
|
+
* Whether the page moved since the current round's context was built.
|
|
1304
|
+
*
|
|
1305
|
+
* `null` means no round has built context yet (nothing to compare), which is
|
|
1306
|
+
* not a move.
|
|
1307
|
+
*/
|
|
1308
|
+
#pageMoved(): boolean {
|
|
1309
|
+
return this.#contextHref !== null && this.#contextHref !== window.location.href;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
#noticeIfRunUnfinished(messages: readonly Message[] | null): void {
|
|
1313
|
+
const last = messages?.at(-1);
|
|
1314
|
+
if (last === undefined || last.role !== MESSAGE_ROLE.USER) {
|
|
1315
|
+
return;
|
|
1184
1316
|
}
|
|
1317
|
+
this.#appendNotice("⚠", this.#strings.runInterrupted, "interrupted");
|
|
1185
1318
|
}
|
|
1186
1319
|
|
|
1187
1320
|
/**
|
|
@@ -1603,14 +1736,55 @@ export class AgUiChat extends HTMLElement {
|
|
|
1603
1736
|
if (content === "" && attachments.length === 0) {
|
|
1604
1737
|
return;
|
|
1605
1738
|
}
|
|
1739
|
+
this.#input.value = "";
|
|
1740
|
+
// A file still uploading does not ride along — `readyRefs()` returns only
|
|
1741
|
+
// settled ones, and `clearReady()` deliberately keeps the rest for a
|
|
1742
|
+
// follow-up. Nothing said so, which is the whole defect: attachments are
|
|
1743
|
+
// frequently the entire point of the message, and the user had no way to
|
|
1744
|
+
// tell theirs had been left behind. Say it before dropping the chips,
|
|
1745
|
+
// while `hasPending()` still describes this send.
|
|
1746
|
+
if (this.#attachTray?.hasPending() === true) {
|
|
1747
|
+
this.#appendNotice(
|
|
1748
|
+
"\u{1F4CE}",
|
|
1749
|
+
this.#strings.attachmentsStillUploading.replace(
|
|
1750
|
+
"{n}",
|
|
1751
|
+
String(this.#attachTray.pendingCount()),
|
|
1752
|
+
),
|
|
1753
|
+
"attachment-pending",
|
|
1754
|
+
);
|
|
1755
|
+
}
|
|
1756
|
+
// The refs ride the message from here; drop the settled chips, keep any
|
|
1757
|
+
// still uploading for a follow-up message.
|
|
1758
|
+
this.#attachTray?.clearReady();
|
|
1759
|
+
await this.sendMessage(content, attachments);
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
/**
|
|
1763
|
+
* Send a message as if the user had typed it — renders the user bubble,
|
|
1764
|
+
* dispatches {@link SUBMIT_EVENT}, and starts the run.
|
|
1765
|
+
*
|
|
1766
|
+
* The programmatic half of the composer, for a host driving its own input
|
|
1767
|
+
* (a "Ask about this order" button, a command palette, a custom composer
|
|
1768
|
+
* replacing the built-in one). Everything the built-in Send does happens
|
|
1769
|
+
* here; Send itself now reads the composer, clears it, and calls this.
|
|
1770
|
+
*
|
|
1771
|
+
* `attachments` are durable {@link AttachmentRef}s — the shape
|
|
1772
|
+
* {@link attachFile}'s upload resolves to, and the shape
|
|
1773
|
+
* {@link ATTACHMENT_EVENT} reports. Pass them to attach files to the message.
|
|
1774
|
+
*
|
|
1775
|
+
* No-ops while a run is in flight (a second concurrent run would orphan the
|
|
1776
|
+
* first) and for an entirely empty message. Unlike the built-in Send, this
|
|
1777
|
+
* does **not** consult the tray: what you pass is what is sent, so a host
|
|
1778
|
+
* composer stays in charge of its own state.
|
|
1779
|
+
*/
|
|
1780
|
+
async sendMessage(content: string, attachments: readonly AttachmentRef[] = []): Promise<void> {
|
|
1781
|
+
if (this.#running || (content === "" && attachments.length === 0)) {
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1606
1784
|
const bubble = this.appendMessage(MESSAGE_ROLE.USER, content);
|
|
1607
1785
|
if (attachments.length > 0) {
|
|
1608
1786
|
bubble.appendChild(renderAttachmentChips(attachments));
|
|
1609
1787
|
}
|
|
1610
|
-
this.#input.value = "";
|
|
1611
|
-
// The refs are now on the bubble; drop the settled chips, keep any still
|
|
1612
|
-
// uploading for a follow-up message.
|
|
1613
|
-
this.#attachTray?.clearReady();
|
|
1614
1788
|
// Surfaced to the run via the context manifest until the run settles.
|
|
1615
1789
|
this.#runAttachments = attachments;
|
|
1616
1790
|
this.dispatchEvent(
|
|
@@ -1623,6 +1797,37 @@ export class AgUiChat extends HTMLElement {
|
|
|
1623
1797
|
await this.#client_send(content, attachments);
|
|
1624
1798
|
}
|
|
1625
1799
|
|
|
1800
|
+
/**
|
|
1801
|
+
* Queue a file for upload into the attachment tray, exactly as the file
|
|
1802
|
+
* picker and drag-and-drop do — validation, progress chip, and all.
|
|
1803
|
+
*
|
|
1804
|
+
* Returns `false` when uploads are not configured (no `data-attachments-url`
|
|
1805
|
+
* and no {@link uploadHandler}), which is the only way for a host to tell;
|
|
1806
|
+
* the tray does not exist to report anything in that case.
|
|
1807
|
+
*
|
|
1808
|
+
* Uploading is asynchronous: watch {@link ATTACHMENT_EVENT} for the resulting
|
|
1809
|
+
* {@link AttachmentRef}, and pass it to {@link sendMessage} once `pending`
|
|
1810
|
+
* reaches zero.
|
|
1811
|
+
*/
|
|
1812
|
+
attachFile(file: File): boolean {
|
|
1813
|
+
if (this.#attachTray === null) {
|
|
1814
|
+
return false;
|
|
1815
|
+
}
|
|
1816
|
+
this.#attachTray.add(file);
|
|
1817
|
+
return true;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
/** Tell the host what the tray now holds — see {@link ATTACHMENT_EVENT}. */
|
|
1821
|
+
#dispatchAttachments(tray: AttachmentTray): void {
|
|
1822
|
+
this.dispatchEvent(
|
|
1823
|
+
new CustomEvent<AttachmentsDetail>(ATTACHMENT_EVENT, {
|
|
1824
|
+
detail: { attachments: tray.readyRefs(), pending: tray.pendingCount() },
|
|
1825
|
+
bubbles: true,
|
|
1826
|
+
composed: true,
|
|
1827
|
+
}),
|
|
1828
|
+
);
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1626
1831
|
async #client_send(content: string, attachments: readonly AttachmentRef[]): Promise<void> {
|
|
1627
1832
|
if (this.endpoint === "") {
|
|
1628
1833
|
return;
|
|
@@ -1647,7 +1852,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1647
1852
|
agent,
|
|
1648
1853
|
handlers: this.#handlers(),
|
|
1649
1854
|
getTools: () => this.getTools(),
|
|
1650
|
-
getContext: () => this
|
|
1855
|
+
getContext: () => this.#buildContext(),
|
|
1651
1856
|
executeTool: (call) => this.#executeTool(call),
|
|
1652
1857
|
resolveInterrupts: (interrupts) => this.#resolveInterrupts(interrupts),
|
|
1653
1858
|
onPersist: (messages) => this.conversationStore.saveMessages(this.#threadId, messages),
|
|
@@ -1704,6 +1909,29 @@ export class AgUiChat extends HTMLElement {
|
|
|
1704
1909
|
}
|
|
1705
1910
|
return null;
|
|
1706
1911
|
}
|
|
1912
|
+
// The page moved under this round. Acting now would target whatever
|
|
1913
|
+
// happens to match on the *new* page — usually nothing, which the handler
|
|
1914
|
+
// would report as a miss anyway, but occasionally a same-named control that
|
|
1915
|
+
// matches silently. That last case is the one worth preventing: it is the
|
|
1916
|
+
// only way the agent acts on the wrong page without either side noticing.
|
|
1917
|
+
//
|
|
1918
|
+
// Placed before the confirmation prompt so the user is never asked to
|
|
1919
|
+
// approve an action that is about to be refused. Navigating tools are
|
|
1920
|
+
// exempt (moving the page is their job — including re-navigating after a
|
|
1921
|
+
// move), as is `read_page`, which is the documented recovery. Gated on a
|
|
1922
|
+
// page-map provider: without one there is no `read_page` to recommend and
|
|
1923
|
+
// the host's tools are not page-scoped in the first place.
|
|
1924
|
+
if (
|
|
1925
|
+
this.getPageMap !== null &&
|
|
1926
|
+
call.name !== READ_PAGE_TOOL &&
|
|
1927
|
+
!isNavigates(tool.parameters) &&
|
|
1928
|
+
this.#pageMoved()
|
|
1929
|
+
) {
|
|
1930
|
+
const message = this.#strings.pageMoved;
|
|
1931
|
+
card.settle(TOOL_CALL_STATUS.ERROR, message);
|
|
1932
|
+
this.#showPending();
|
|
1933
|
+
return { content: `Error: ${message}`, error: message };
|
|
1934
|
+
}
|
|
1707
1935
|
if (await this.#needsConfirmation(call, tool)) {
|
|
1708
1936
|
const request: ConfirmationRequest = { toolName: call.name, args: call.args };
|
|
1709
1937
|
const confirmText = tool.parameters[X_CONFIRM_KEY];
|
|
@@ -92,7 +92,7 @@ export class RemoteConversationStore implements ClientConversationStore {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
async loadMessages(threadId: string): Promise<readonly Message[] | null> {
|
|
95
|
-
const response = await this.#get(this.#url
|
|
95
|
+
const response = await this.#get(`${this.#url}${encodeURIComponent(threadId)}/`);
|
|
96
96
|
if (response === null || !response.ok) {
|
|
97
97
|
return this.#local.loadMessages(threadId);
|
|
98
98
|
}
|
|
@@ -156,7 +156,7 @@ export class RemoteConversationStore implements ClientConversationStore {
|
|
|
156
156
|
): Promise<void> {
|
|
157
157
|
const headers = this.#headers();
|
|
158
158
|
try {
|
|
159
|
-
await fetch(this.#url
|
|
159
|
+
await fetch(`${this.#url}${encodeURIComponent(threadId)}/`, {
|
|
160
160
|
method,
|
|
161
161
|
headers: body === undefined ? headers : { ...headers, "content-type": "application/json" },
|
|
162
162
|
body: body === undefined ? null : JSON.stringify(body),
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Public surface re-exports. Per CLAUDE.md, this is the only re-export point.
|
|
2
2
|
|
|
3
3
|
export {
|
|
4
|
+
ATTACHMENT_EVENT,
|
|
4
5
|
COMPACTION_ACTIVITY_TYPE,
|
|
5
6
|
ELEMENT_TAG,
|
|
6
7
|
LOAD_CAPABILITY_TOOL,
|
|
@@ -18,6 +19,7 @@ export {
|
|
|
18
19
|
} from "./constants.js";
|
|
19
20
|
export {
|
|
20
21
|
AgUiChat,
|
|
22
|
+
type AttachmentsDetail,
|
|
21
23
|
type MessageRole,
|
|
22
24
|
type StateDetail,
|
|
23
25
|
type SubmitDetail,
|
package/src/tools/route_map.ts
CHANGED
|
@@ -33,14 +33,11 @@ const PATH_PARAM_RE = /:([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
|
33
33
|
|
|
34
34
|
/** The `:name` path-parameter names declared in a path template, in order. */
|
|
35
35
|
function pathParamNames(path: string): string[] {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return names;
|
|
36
|
+
// Slicing the ":" off the whole match rather than reading the capture group:
|
|
37
|
+
// the group is mandatory, so under `noUncheckedIndexedAccess` indexing it
|
|
38
|
+
// forces an `undefined` guard on a case the regex cannot produce — an
|
|
39
|
+
// unreachable branch no test can ever cover.
|
|
40
|
+
return [...path.matchAll(PATH_PARAM_RE)].map((match) => match[0].slice(1));
|
|
44
41
|
}
|
|
45
42
|
|
|
46
43
|
/**
|
|
@@ -102,6 +102,11 @@ export class AttachmentTray {
|
|
|
102
102
|
return this.#items.some((item) => item.status === ATTACHMENT_STATUS.UPLOADING);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/** How many chips are still uploading — the count a send-time notice names. */
|
|
106
|
+
pendingCount(): number {
|
|
107
|
+
return this.#items.filter((item) => item.status === ATTACHMENT_STATUS.UPLOADING).length;
|
|
108
|
+
}
|
|
109
|
+
|
|
105
110
|
/** Whether the tray holds no chips. */
|
|
106
111
|
isEmpty(): boolean {
|
|
107
112
|
return this.#items.length === 0;
|
|
@@ -48,6 +48,7 @@ export class CheckpointMenu {
|
|
|
48
48
|
header.setAttribute("part", "checkpoints-header");
|
|
49
49
|
this.#heading = document.createElement("span");
|
|
50
50
|
this.#heading.className = "checkpoints-title";
|
|
51
|
+
this.#heading.setAttribute("part", "checkpoints-title");
|
|
51
52
|
this.#heading.textContent = strings.checkpoints;
|
|
52
53
|
header.append(this.#heading);
|
|
53
54
|
|
|
@@ -112,6 +113,7 @@ export class CheckpointMenu {
|
|
|
112
113
|
|
|
113
114
|
const label = document.createElement("span");
|
|
114
115
|
label.className = "checkpoint-label";
|
|
116
|
+
label.setAttribute("part", "checkpoint-label");
|
|
115
117
|
// A run id is opaque to a person, so the time is the identifying detail;
|
|
116
118
|
// the id rides `title` for anyone who needs to correlate with server logs.
|
|
117
119
|
label.textContent =
|
package/src/ui/styles.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const STYLES = `
|
|
|
11
11
|
--ag-ui-user-bg: #4f46e5;
|
|
12
12
|
--ag-ui-user-fg: #ffffff;
|
|
13
13
|
--ag-ui-assistant-bg: #f1f1f6;
|
|
14
|
+
--ag-ui-hover: #e7e7ee;
|
|
14
15
|
--ag-ui-input-bg: var(--ag-ui-bg);
|
|
15
16
|
--ag-ui-tool-bg: var(--ag-ui-assistant-bg);
|
|
16
17
|
--ag-ui-tool-fg: var(--ag-ui-accent);
|
|
@@ -80,6 +81,7 @@ export const STYLES = `
|
|
|
80
81
|
--ag-ui-bg: #15151f;
|
|
81
82
|
--ag-ui-fg: #e8e8f2;
|
|
82
83
|
--ag-ui-assistant-bg: #25253a;
|
|
84
|
+
--ag-ui-hover: #303049;
|
|
83
85
|
--ag-ui-header-bg: #1f1f30;
|
|
84
86
|
--ag-ui-header-fg: #e8e8f2;
|
|
85
87
|
--ag-ui-border: #33334a;
|
|
@@ -92,6 +94,7 @@ export const STYLES = `
|
|
|
92
94
|
--ag-ui-bg: #15151f;
|
|
93
95
|
--ag-ui-fg: #e8e8f2;
|
|
94
96
|
--ag-ui-assistant-bg: #25253a;
|
|
97
|
+
--ag-ui-hover: #303049;
|
|
95
98
|
--ag-ui-header-bg: #1f1f30;
|
|
96
99
|
--ag-ui-header-fg: #e8e8f2;
|
|
97
100
|
--ag-ui-border: #33334a;
|
|
@@ -107,6 +110,7 @@ export const STYLES = `
|
|
|
107
110
|
--ag-ui-accent: #3fb950;
|
|
108
111
|
--ag-ui-user-bg: #238636;
|
|
109
112
|
--ag-ui-assistant-bg: #161b22;
|
|
113
|
+
--ag-ui-hover: #21262d;
|
|
110
114
|
--ag-ui-header-bg: #010409;
|
|
111
115
|
--ag-ui-header-fg: #c9d1d9;
|
|
112
116
|
--ag-ui-border: #30363d;
|
|
@@ -514,6 +518,34 @@ export const STYLES = `
|
|
|
514
518
|
color: var(--ag-ui-muted);
|
|
515
519
|
}
|
|
516
520
|
|
|
521
|
+
/* Markdown tables. table/thead/tbody/tr/th/td are all in
|
|
522
|
+
the sanitizer's ALLOWED_TAGS, so an agent emitting one renders it — and until
|
|
523
|
+
now rendered it entirely unstyled, overflowing the bubble. A wide table
|
|
524
|
+
scrolls inside its own box rather than stretching the message: the bubble is
|
|
525
|
+
width-constrained, so without this the columns either crush or push the
|
|
526
|
+
layout sideways. */
|
|
527
|
+
.message--assistant table {
|
|
528
|
+
display: block;
|
|
529
|
+
width: fit-content;
|
|
530
|
+
max-width: 100%;
|
|
531
|
+
overflow-x: auto;
|
|
532
|
+
border-collapse: collapse;
|
|
533
|
+
font-size: 0.95em;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.message--assistant th,
|
|
537
|
+
.message--assistant td {
|
|
538
|
+
padding: 6px 10px;
|
|
539
|
+
border: 1px solid var(--ag-ui-border);
|
|
540
|
+
text-align: left;
|
|
541
|
+
vertical-align: top;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.message--assistant th {
|
|
545
|
+
background: var(--ag-ui-hover);
|
|
546
|
+
font-weight: 600;
|
|
547
|
+
}
|
|
548
|
+
|
|
517
549
|
.pending {
|
|
518
550
|
align-self: flex-start;
|
|
519
551
|
display: inline-flex;
|
|
@@ -1264,9 +1296,9 @@ export const STYLES = `
|
|
|
1264
1296
|
flex-direction: column;
|
|
1265
1297
|
gap: 0.25rem;
|
|
1266
1298
|
padding: 0.5rem;
|
|
1267
|
-
border: 1px solid var(--
|
|
1299
|
+
border: 1px solid var(--ag-ui-border);
|
|
1268
1300
|
border-radius: 0.5rem;
|
|
1269
|
-
background: var(--
|
|
1301
|
+
background: var(--ag-ui-assistant-bg);
|
|
1270
1302
|
box-shadow: 0 6px 24px rgb(0 0 0 / 12%);
|
|
1271
1303
|
max-height: 60%;
|
|
1272
1304
|
overflow-y: auto;
|
|
@@ -1297,7 +1329,7 @@ export const STYLES = `
|
|
|
1297
1329
|
}
|
|
1298
1330
|
|
|
1299
1331
|
.checkpoint-row:hover {
|
|
1300
|
-
background: var(--
|
|
1332
|
+
background: var(--ag-ui-hover);
|
|
1301
1333
|
}
|
|
1302
1334
|
|
|
1303
1335
|
.checkpoint-label {
|
|
@@ -1312,7 +1344,7 @@ export const STYLES = `
|
|
|
1312
1344
|
font-size: 0.6875rem;
|
|
1313
1345
|
padding: 0 0.375rem;
|
|
1314
1346
|
border-radius: 999px;
|
|
1315
|
-
background: var(--
|
|
1347
|
+
background: var(--ag-ui-hover);
|
|
1316
1348
|
opacity: 0.8;
|
|
1317
1349
|
}
|
|
1318
1350
|
|
|
@@ -1321,14 +1353,14 @@ export const STYLES = `
|
|
|
1321
1353
|
font-size: 0.75rem;
|
|
1322
1354
|
cursor: pointer;
|
|
1323
1355
|
padding: 0.125rem 0.5rem;
|
|
1324
|
-
border: 1px solid var(--
|
|
1356
|
+
border: 1px solid var(--ag-ui-border);
|
|
1325
1357
|
border-radius: 0.375rem;
|
|
1326
1358
|
background: transparent;
|
|
1327
1359
|
color: inherit;
|
|
1328
1360
|
}
|
|
1329
1361
|
|
|
1330
1362
|
.checkpoint-action:hover {
|
|
1331
|
-
background: var(--
|
|
1363
|
+
background: var(--ag-ui-hover);
|
|
1332
1364
|
}
|
|
1333
1365
|
|
|
1334
1366
|
.drawer-backdrop {
|
package/src/ui/ui_strings.ts
CHANGED
|
@@ -53,6 +53,12 @@ export interface UiStrings {
|
|
|
53
53
|
historyCompacted: string;
|
|
54
54
|
/** Notice shown when the agent loads an agent skill. Token: `{name}`. */
|
|
55
55
|
usingSkill: string;
|
|
56
|
+
/** Notice shown on mount when the previous run never produced a response. */
|
|
57
|
+
runInterrupted: string;
|
|
58
|
+
/** Tool-result content (and card text) when the page moved mid-run. */
|
|
59
|
+
pageMoved: string;
|
|
60
|
+
/** Notice when Send ran while a file was still uploading. Token: `{n}`. */
|
|
61
|
+
attachmentsStillUploading: string;
|
|
56
62
|
|
|
57
63
|
// ── Composer ────────────────────────────────────────────────────────────────
|
|
58
64
|
/** `aria-label` of the message textarea. */
|
|
@@ -201,6 +207,11 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
|
|
|
201
207
|
navigating: "Navigating…",
|
|
202
208
|
historyCompacted: "Earlier turns condensed to fit the context window ({count} removed)",
|
|
203
209
|
usingSkill: "Using skill {name}",
|
|
210
|
+
runInterrupted: "The previous response didn’t finish — the page changed before it arrived.",
|
|
211
|
+
pageMoved:
|
|
212
|
+
"The page changed since you last looked at it. Call read_page to see the current page, then retry.",
|
|
213
|
+
attachmentsStillUploading:
|
|
214
|
+
"{n} file still uploading — it was not sent with this message and is still attached.",
|
|
204
215
|
skillNeeds: "“{title}” needs: {fields}",
|
|
205
216
|
|
|
206
217
|
message: "Message",
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION: string = "0.
|
|
1
|
+
export const VERSION: string = "0.17.0";
|