@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +158 -1
  2. package/README.md +42 -4
  3. package/dist/ag-ui-web-component.bundle.js +91 -58
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +21 -0
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +41 -2
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/core/agui_client.d.ts.map +1 -1
  10. package/dist/core/conversation_store.d.ts.map +1 -1
  11. package/dist/core/remote_conversation_store.d.ts.map +1 -1
  12. package/dist/core/run_index.d.ts.map +1 -1
  13. package/dist/index.d.ts +2 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +534 -325
  16. package/dist/index.js.map +4 -4
  17. package/dist/tools/client_tool_registry.d.ts.map +1 -1
  18. package/dist/tools/page_action_tools.d.ts.map +1 -1
  19. package/dist/tools/route_map.d.ts.map +1 -1
  20. package/dist/ui/attachment_tray.d.ts +2 -0
  21. package/dist/ui/attachment_tray.d.ts.map +1 -1
  22. package/dist/ui/checkpoint_menu.d.ts.map +1 -1
  23. package/dist/ui/skills_menu.d.ts.map +1 -1
  24. package/dist/ui/styles.d.ts +1 -1
  25. package/dist/ui/styles.d.ts.map +1 -1
  26. package/dist/ui/thoughts_block.d.ts.map +1 -1
  27. package/dist/ui/thread_drawer.d.ts.map +1 -1
  28. package/dist/ui/tool_call_card.d.ts.map +1 -1
  29. package/dist/ui/ui_strings.d.ts +6 -0
  30. package/dist/ui/ui_strings.d.ts.map +1 -1
  31. package/dist/ui/voice_input.d.ts.map +1 -1
  32. package/package.json +9 -8
  33. package/src/constants.ts +23 -0
  34. package/src/core/ag_ui_chat.ts +243 -15
  35. package/src/core/remote_conversation_store.ts +2 -2
  36. package/src/index.ts +2 -0
  37. package/src/tools/route_map.ts +5 -8
  38. package/src/ui/attachment_tray.ts +5 -0
  39. package/src/ui/checkpoint_menu.ts +2 -0
  40. package/src/ui/styles.ts +38 -6
  41. package/src/ui/ui_strings.ts +11 -0
  42. package/src/version.ts +1 -1
@@ -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.getContext(),
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 whose late changes must reflect in already-rendered chrome. */
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(_name: string, _previous: string | null, value: string | null): void {
484
- // Only `title-text` is observed (other attributes are read at use-time or
485
- // are CSS-reactive), so update the header title directly. `#strings` is the
486
- // resolved table once connected, the English defaults before then.
487
- this.#title.textContent = value ?? this.#strings.title;
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: "read_page",
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
- this.#attachTray = new AttachmentTray({
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.getContext(),
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 + encodeURIComponent(threadId) + "/");
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 + encodeURIComponent(threadId) + "/", {
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,
@@ -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
- const names: string[] = [];
37
- for (const match of path.matchAll(PATH_PARAM_RE)) {
38
- const name = match[1];
39
- if (name !== undefined) {
40
- names.push(name);
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(--agui-border, #d7d7dc);
1299
+ border: 1px solid var(--ag-ui-border);
1268
1300
  border-radius: 0.5rem;
1269
- background: var(--agui-surface, #fff);
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(--agui-hover, #f3f3f5);
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(--agui-hover, #f3f3f5);
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(--agui-border, #d7d7dc);
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(--agui-hover, #f3f3f5);
1363
+ background: var(--ag-ui-hover);
1332
1364
  }
1333
1365
 
1334
1366
  .drawer-backdrop {
@@ -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.15.0";
1
+ export const VERSION: string = "0.17.0";