@artooi/ag-ui-web-component 0.8.1 → 0.9.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/dist/index.js CHANGED
@@ -506,8 +506,10 @@ var DEFAULT_UI_STRINGS = {
506
506
  newChat: "New chat",
507
507
  collapse: "Collapse",
508
508
  expand: "Expand",
509
+ toggleTheme: "Toggle theme",
509
510
  conversation: "Conversation",
510
511
  thinking: "Assistant is thinking\u2026",
512
+ thoughts: "Thoughts",
511
513
  stopped: "\u23F9 Stopped",
512
514
  connectionLost: "Connection lost",
513
515
  noResult: "No result returned.",
@@ -519,6 +521,10 @@ var DEFAULT_UI_STRINGS = {
519
521
  send: "Send",
520
522
  stop: "Stop",
521
523
  attachFiles: "Attach files",
524
+ recordVoice: "Record voice",
525
+ stopRecording: "Stop recording",
526
+ transcribing: "Transcribing\u2026",
527
+ transcriptionFailed: "Transcription failed",
522
528
  toolRunning: "running\u2026",
523
529
  toolDone: "\u2713 done",
524
530
  toolError: "\u26A0 error",
@@ -4018,6 +4024,69 @@ var STYLES = `
4018
4024
  }
4019
4025
  }
4020
4026
 
4027
+ /* \u2500\u2500 Thoughts region (THINK-1) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4028
+ A muted, collapsible chain-of-thought at the top of the answer group: open
4029
+ while the model reasons, folded once the answer text starts. */
4030
+ .thoughts {
4031
+ align-self: stretch;
4032
+ display: flex;
4033
+ flex-direction: column;
4034
+ gap: 4px;
4035
+ font-size: 12px;
4036
+ color: var(--ag-ui-muted);
4037
+ }
4038
+
4039
+ .thoughts-toggle {
4040
+ align-self: flex-start;
4041
+ border: none;
4042
+ padding: 0;
4043
+ background: none;
4044
+ font: inherit;
4045
+ font-size: 12px;
4046
+ font-weight: 600;
4047
+ color: var(--ag-ui-muted);
4048
+ cursor: pointer;
4049
+ }
4050
+
4051
+ .thoughts-toggle::before {
4052
+ content: "\u25BE ";
4053
+ }
4054
+
4055
+ .thoughts-toggle[aria-expanded="false"]::before {
4056
+ content: "\u25B8 ";
4057
+ }
4058
+
4059
+ /* A gentle pulse on the label while reasoning is still streaming. */
4060
+ .thoughts[data-streaming] .thoughts-label {
4061
+ animation: ag-ui-thoughts-pulse 1.4s ease-in-out infinite;
4062
+ }
4063
+
4064
+ @keyframes ag-ui-thoughts-pulse {
4065
+ 0%, 100% { opacity: 0.55; }
4066
+ 50% { opacity: 1; }
4067
+ }
4068
+
4069
+ .thoughts-body {
4070
+ margin: 0;
4071
+ padding: 4px 0 4px 10px;
4072
+ border-left: 2px solid var(--ag-ui-border);
4073
+ max-height: 220px;
4074
+ overflow: auto;
4075
+ white-space: pre-wrap;
4076
+ word-break: break-word;
4077
+ font-family: inherit;
4078
+ }
4079
+
4080
+ .thoughts-body[hidden] {
4081
+ display: none;
4082
+ }
4083
+
4084
+ @media (prefers-reduced-motion: reduce) {
4085
+ .thoughts[data-streaming] .thoughts-label {
4086
+ animation: none;
4087
+ }
4088
+ }
4089
+
4021
4090
  .tool-call {
4022
4091
  align-self: flex-start;
4023
4092
  max-width: 80%;
@@ -4226,6 +4295,49 @@ var STYLES = `
4226
4295
  display: none;
4227
4296
  }
4228
4297
 
4298
+ /* The \u{1F3A4} mic button (VOICE-1); shown only once #wireVoice mounts it. */
4299
+ .voice-slot {
4300
+ display: contents;
4301
+ }
4302
+
4303
+ .voice-btn {
4304
+ border: 1px solid var(--ag-ui-border);
4305
+ border-radius: 8px;
4306
+ padding: 0 10px;
4307
+ background: var(--ag-ui-input-bg);
4308
+ color: inherit;
4309
+ font: inherit;
4310
+ cursor: pointer;
4311
+ }
4312
+
4313
+ .voice-btn:hover {
4314
+ border-color: var(--ag-ui-accent);
4315
+ }
4316
+
4317
+ .voice-btn:disabled {
4318
+ cursor: default;
4319
+ opacity: 0.6;
4320
+ }
4321
+
4322
+ /* Recording: a red tint + a gentle pulse so it's clearly "live". */
4323
+ .voice-btn[data-state="recording"] {
4324
+ border-color: var(--ag-ui-danger);
4325
+ background: var(--ag-ui-danger);
4326
+ color: #ffffff;
4327
+ animation: ag-ui-voice-pulse 1.2s ease-in-out infinite;
4328
+ }
4329
+
4330
+ @keyframes ag-ui-voice-pulse {
4331
+ 0%, 100% { opacity: 1; }
4332
+ 50% { opacity: 0.6; }
4333
+ }
4334
+
4335
+ @media (prefers-reduced-motion: reduce) {
4336
+ .voice-btn[data-state="recording"] {
4337
+ animation: none;
4338
+ }
4339
+ }
4340
+
4229
4341
  /* Pending-attachments tray, above the input row; collapses (hidden) when empty. */
4230
4342
  .attachment-slot {
4231
4343
  display: contents;
@@ -4640,6 +4752,62 @@ var STYLES = `
4640
4752
  }
4641
4753
  `;
4642
4754
 
4755
+ // src/ui/thoughts_block.ts
4756
+ var ThoughtsBlock = class {
4757
+ /** The block's root element; insert this at the top of the answer group. */
4758
+ element;
4759
+ #label;
4760
+ #body;
4761
+ #toggle;
4762
+ #strings;
4763
+ #collapsed = false;
4764
+ constructor(strings = DEFAULT_UI_STRINGS) {
4765
+ this.#strings = strings;
4766
+ this.element = document.createElement("div");
4767
+ this.element.className = "thoughts";
4768
+ this.element.setAttribute("part", "thoughts");
4769
+ this.element.setAttribute("data-streaming", "");
4770
+ this.#toggle = document.createElement("button");
4771
+ this.#toggle.type = "button";
4772
+ this.#toggle.className = "thoughts-toggle";
4773
+ this.#toggle.setAttribute("part", "thoughts-toggle");
4774
+ this.#toggle.setAttribute("aria-expanded", "true");
4775
+ this.#label = document.createElement("span");
4776
+ this.#label.className = "thoughts-label";
4777
+ this.#label.textContent = strings.thinking;
4778
+ this.#toggle.append(this.#label);
4779
+ this.#body = document.createElement("pre");
4780
+ this.#body.className = "thoughts-body";
4781
+ this.#body.setAttribute("part", "thoughts-body");
4782
+ this.#toggle.addEventListener("click", () => {
4783
+ this.#setCollapsed(!this.#collapsed);
4784
+ });
4785
+ this.element.append(this.#toggle, this.#body);
4786
+ }
4787
+ /** Replace the reasoning body with the running buffer (the full text so far). */
4788
+ stream(buffer) {
4789
+ this.#body.textContent = buffer;
4790
+ }
4791
+ /**
4792
+ * Fold the region away — called when the answer's first text token arrives.
4793
+ * Idempotent (the per-token text handler calls it repeatedly) and flips the
4794
+ * header label from "thinking…" to the settled "Thoughts" affordance.
4795
+ */
4796
+ collapse() {
4797
+ if (this.#collapsed) {
4798
+ return;
4799
+ }
4800
+ this.element.removeAttribute("data-streaming");
4801
+ this.#label.textContent = this.#strings.thoughts;
4802
+ this.#setCollapsed(true);
4803
+ }
4804
+ #setCollapsed(collapsed) {
4805
+ this.#collapsed = collapsed;
4806
+ this.#body.hidden = collapsed;
4807
+ this.#toggle.setAttribute("aria-expanded", String(!collapsed));
4808
+ }
4809
+ };
4810
+
4643
4811
  // src/ui/relative_time.ts
4644
4812
  function relativeTime(timestamp, now = Date.now(), strings = DEFAULT_UI_STRINGS) {
4645
4813
  const seconds = Math.round((now - timestamp) / 1e3);
@@ -4943,6 +5111,115 @@ ${text2}`;
4943
5111
  }
4944
5112
  };
4945
5113
 
5114
+ // src/ui/voice_input.ts
5115
+ var VoiceInput = class {
5116
+ /** The mic button; mount this in the composer. */
5117
+ element;
5118
+ #transcribe;
5119
+ #onText;
5120
+ #strings;
5121
+ #state = "idle";
5122
+ #recorder = null;
5123
+ #stream = null;
5124
+ #chunks = [];
5125
+ constructor(options) {
5126
+ this.#transcribe = options.transcribe;
5127
+ this.#onText = options.onText;
5128
+ this.#strings = options.strings ?? DEFAULT_UI_STRINGS;
5129
+ this.element = document.createElement("button");
5130
+ this.element.type = "button";
5131
+ this.element.className = "voice-btn";
5132
+ this.element.setAttribute("part", "voice-button");
5133
+ this.element.textContent = "\u{1F3A4}";
5134
+ this.#setState("idle");
5135
+ this.element.addEventListener("click", () => {
5136
+ void this.toggle();
5137
+ });
5138
+ }
5139
+ /** Start recording when idle, stop (and transcribe) when recording. */
5140
+ async toggle() {
5141
+ if (this.#state === "recording") {
5142
+ this.#stop();
5143
+ return;
5144
+ }
5145
+ if (this.#state === "transcribing") {
5146
+ return;
5147
+ }
5148
+ await this.#start();
5149
+ }
5150
+ async #start() {
5151
+ let stream;
5152
+ try {
5153
+ stream = await navigator.mediaDevices.getUserMedia({ audio: true });
5154
+ } catch {
5155
+ this.#fail(this.#strings.transcriptionFailed);
5156
+ return;
5157
+ }
5158
+ this.#stream = stream;
5159
+ this.#chunks = [];
5160
+ const recorder = new MediaRecorder(stream);
5161
+ recorder.addEventListener("dataavailable", (event) => {
5162
+ this.#chunks.push(event.data);
5163
+ });
5164
+ recorder.addEventListener("stop", () => {
5165
+ void this.#finish(recorder.mimeType);
5166
+ });
5167
+ this.#recorder = recorder;
5168
+ recorder.start();
5169
+ this.#setState("recording");
5170
+ }
5171
+ #stop() {
5172
+ this.#recorder?.stop();
5173
+ }
5174
+ async #finish(mimeType) {
5175
+ this.#releaseStream();
5176
+ this.#setState("transcribing");
5177
+ const audio = new Blob(this.#chunks, { type: mimeType || "audio/webm" });
5178
+ try {
5179
+ const text2 = await this.#transcribe(audio);
5180
+ this.#setState("idle");
5181
+ if (text2 !== "") {
5182
+ this.#onText(text2);
5183
+ }
5184
+ } catch (error) {
5185
+ this.#fail(error instanceof Error ? error.message : this.#strings.transcriptionFailed);
5186
+ } finally {
5187
+ this.#recorder = null;
5188
+ }
5189
+ }
5190
+ /** Stop the mic tracks so the browser's recording indicator clears. */
5191
+ #releaseStream() {
5192
+ for (const track of this.#stream?.getTracks() ?? []) {
5193
+ track.stop();
5194
+ }
5195
+ this.#stream = null;
5196
+ }
5197
+ #fail(message) {
5198
+ this.#releaseStream();
5199
+ this.#recorder = null;
5200
+ this.#setState("idle");
5201
+ this.element.title = message;
5202
+ }
5203
+ #setState(state) {
5204
+ this.#state = state;
5205
+ this.element.dataset["state"] = state;
5206
+ const label = this.#labelFor(state);
5207
+ this.element.title = label;
5208
+ this.element.setAttribute("aria-label", label);
5209
+ this.element.setAttribute("aria-pressed", String(state === "recording"));
5210
+ this.element.disabled = state === "transcribing";
5211
+ }
5212
+ #labelFor(state) {
5213
+ if (state === "recording") {
5214
+ return this.#strings.stopRecording;
5215
+ }
5216
+ if (state === "transcribing") {
5217
+ return this.#strings.transcribing;
5218
+ }
5219
+ return this.#strings.recordVoice;
5220
+ }
5221
+ };
5222
+
4946
5223
  // src/core/agui_client.ts
4947
5224
  import { randomUUID as randomUUID2 } from "@ag-ui/client";
4948
5225
  var ConnectionLostError = class extends Error {
@@ -5114,6 +5391,18 @@ var AgUiClient = class {
5114
5391
  onToolCallResultEvent({ event }) {
5115
5392
  h.onToolResult(event.toolCallId, event.content);
5116
5393
  },
5394
+ // Reasoning (THINK-1). `@ag-ui/client` already maps the deprecated
5395
+ // THINKING_* events onto these REASONING_* callbacks, so handling the
5396
+ // reasoning family alone covers both protocol versions.
5397
+ onReasoningStartEvent() {
5398
+ h.onReasoningStart();
5399
+ },
5400
+ onReasoningMessageContentEvent({ reasoningMessageBuffer }) {
5401
+ h.onReasoningDelta(reasoningMessageBuffer);
5402
+ },
5403
+ onReasoningEndEvent() {
5404
+ h.onReasoningEnd();
5405
+ },
5117
5406
  onRunErrorEvent({ event }) {
5118
5407
  runState.terminal = true;
5119
5408
  h.onError(event.message);
@@ -5390,6 +5679,35 @@ var RemoteConversationStore = class {
5390
5679
  }
5391
5680
  };
5392
5681
 
5682
+ // src/core/transcribe_audio.ts
5683
+ async function transcribeAudio(audio, options) {
5684
+ const form = new FormData();
5685
+ form.append("audio", audio, "recording.webm");
5686
+ const response = await fetch(options.url, {
5687
+ method: "POST",
5688
+ headers: { ...options.headers ?? {} },
5689
+ body: form
5690
+ });
5691
+ if (!response.ok) {
5692
+ throw new Error(await errorMessage(response));
5693
+ }
5694
+ const body = await response.json();
5695
+ if (typeof body === "object" && body !== null && typeof body.text === "string") {
5696
+ return body.text;
5697
+ }
5698
+ throw new Error("transcription returned an unreadable response");
5699
+ }
5700
+ async function errorMessage(response) {
5701
+ try {
5702
+ const body = await response.json();
5703
+ if (typeof body.error === "string") {
5704
+ return body.error;
5705
+ }
5706
+ } catch {
5707
+ }
5708
+ return `transcription failed (${response.status})`;
5709
+ }
5710
+
5393
5711
  // src/core/upload_attachment.ts
5394
5712
  function uploadAttachment(file, options) {
5395
5713
  return new Promise((resolve, reject) => {
@@ -5416,7 +5734,7 @@ function uploadAttachment(file, options) {
5416
5734
  reject(new Error("upload returned an unreadable response"));
5417
5735
  }
5418
5736
  } else {
5419
- reject(new Error(errorMessage(xhr)));
5737
+ reject(new Error(errorMessage2(xhr)));
5420
5738
  }
5421
5739
  });
5422
5740
  xhr.addEventListener("error", () => reject(new Error("upload failed")));
@@ -5443,7 +5761,7 @@ function parseRef(body) {
5443
5761
  }
5444
5762
  return typeof url === "string" ? { id, name, mime, size, url } : { id, name, mime, size };
5445
5763
  }
5446
- function errorMessage(xhr) {
5764
+ function errorMessage2(xhr) {
5447
5765
  try {
5448
5766
  const body = JSON.parse(xhr.responseText);
5449
5767
  if (typeof body.error === "string") {
@@ -5456,6 +5774,7 @@ function errorMessage(xhr) {
5456
5774
 
5457
5775
  // src/core/ag_ui_chat.ts
5458
5776
  var COLLAPSED_KEY = "ag-ui-chat:collapsed";
5777
+ var THEME_KEY = "ag-ui-chat:theme";
5459
5778
  var AgUiChat = class extends HTMLElement {
5460
5779
  /** Agent factory; override to inject a custom or fake agent (tests). */
5461
5780
  agentFactory = createHttpAgent;
@@ -5533,6 +5852,15 @@ var AgUiChat = class extends HTMLElement {
5533
5852
  * with no `data-attachments-url`; the handler owns its own endpoint + headers.
5534
5853
  */
5535
5854
  uploadHandler = null;
5855
+ /**
5856
+ * How recorded voice clips are transcribed. `null` (default) POSTs the clip
5857
+ * to `data-transcribe-url` (django-ag-ui's `TranscribeView`). Set a custom
5858
+ * {@link TranscribeHandler} — `(audio: Blob) => Promise<string>` — to swap the
5859
+ * transport (a different STT endpoint, a browser Web Speech adapter) without
5860
+ * touching the mic button. When set, the 🎤 affordance appears even with no
5861
+ * `data-transcribe-url`.
5862
+ */
5863
+ transcribeHandler = null;
5536
5864
  /**
5537
5865
  * Builds the tool result a navigating tool resumes with after the page
5538
5866
  * reloads. Defaults to the landed URL; a host (e.g. the admin package) can
@@ -5601,12 +5929,18 @@ var AgUiChat = class extends HTMLElement {
5601
5929
  #attachButton;
5602
5930
  #fileInput;
5603
5931
  #attachSlot;
5932
+ /** Optional built-in header theme toggle (THEME-1); shown only with `data-theme-toggle`. */
5933
+ #themeToggle;
5604
5934
  /** The collapsed-sidebar rail (an expand affordance; shown only for `placement="sidebar"`). */
5605
5935
  #rail;
5606
5936
  /** Empty-state region at the top of the message list; hidden once anything renders. */
5607
5937
  #emptyWrap;
5608
5938
  /** Upload tray; created on connect only when `data-attachments-url` is set. */
5609
5939
  #attachTray = null;
5940
+ /** Mic button mount point (input row); the control mounts on connect when enabled. */
5941
+ #voiceSlot;
5942
+ /** Voice-input control; created on connect when transcription is available. */
5943
+ #voice = null;
5610
5944
  /** Refs attached to the message currently being sent (the context manifest). */
5611
5945
  #runAttachments = [];
5612
5946
  #client = null;
@@ -5630,6 +5964,10 @@ var AgUiChat = class extends HTMLElement {
5630
5964
  // frontend-tool loop (which is several AG-UI runs), not one run. `null`
5631
5965
  // between turns; user bubbles never enter it.
5632
5966
  #currentGroup = null;
5967
+ // The current turn's streamed-reasoning region (THINK-1), shown at the top of
5968
+ // the answer group while a reasoning model thinks and collapsed once the
5969
+ // answer's first text token arrives. `null` outside a reasoning turn.
5970
+ #thoughts = null;
5633
5971
  #threadId = "";
5634
5972
  #initialMessages = [];
5635
5973
  // Skill catalog by source; merged backend → embed → client (later wins).
@@ -5648,6 +5986,8 @@ var AgUiChat = class extends HTMLElement {
5648
5986
  this.#attachButton = document.createElement("button");
5649
5987
  this.#fileInput = document.createElement("input");
5650
5988
  this.#attachSlot = document.createElement("div");
5989
+ this.#voiceSlot = document.createElement("span");
5990
+ this.#themeToggle = document.createElement("button");
5651
5991
  this.#rail = document.createElement("button");
5652
5992
  this.#emptyWrap = document.createElement("div");
5653
5993
  this.#skillsMenu = new SkillsMenu((skill) => this.#applySkill(skill));
@@ -5773,6 +6113,12 @@ var AgUiChat = class extends HTMLElement {
5773
6113
  }
5774
6114
  connectedCallback() {
5775
6115
  this.#strings = mergeUiStrings({ ...this.#readStringOverrides(), ...this.strings });
6116
+ if (this.getAttribute("data-theme-toggle") !== null) {
6117
+ const saved = sessionStorage.getItem(THEME_KEY);
6118
+ if (saved !== null) {
6119
+ this.setAttribute("theme", saved);
6120
+ }
6121
+ }
5776
6122
  this.#render();
5777
6123
  this.#drawer.setStrings(this.#strings);
5778
6124
  if (sessionStorage.getItem(COLLAPSED_KEY) === "1") {
@@ -5783,6 +6129,7 @@ var AgUiChat = class extends HTMLElement {
5783
6129
  void this.#fetchToolCatalog();
5784
6130
  this.#wireThreadStore();
5785
6131
  this.#wireAttachments();
6132
+ this.#wireVoice();
5786
6133
  this.#threadId = this.conversationStore.threadId();
5787
6134
  void this.#rehydrate();
5788
6135
  }
@@ -5833,6 +6180,40 @@ var AgUiChat = class extends HTMLElement {
5833
6180
  }
5834
6181
  return (file, onProgress) => uploadAttachment(file, { url, headers: this.headers, onProgress });
5835
6182
  }
6183
+ /**
6184
+ * Reveal the composer's 🎤 mic button when transcription is possible — either
6185
+ * a custom {@link transcribeHandler} is set or `data-transcribe-url` provides
6186
+ * the built-in POST endpoint. The control records via `MediaRecorder` and
6187
+ * drops the transcript into the composer; with neither configured the mic
6188
+ * stays hidden and the chat is text-only.
6189
+ */
6190
+ #wireVoice() {
6191
+ const url = this.getAttribute("data-transcribe-url");
6192
+ const transcribe = this.transcribeHandler ?? this.#defaultTranscribeHandler(url);
6193
+ if (transcribe === null) {
6194
+ return;
6195
+ }
6196
+ this.#voice = new VoiceInput({
6197
+ transcribe,
6198
+ onText: (text2) => this.#insertVoiceText(text2),
6199
+ strings: this.#strings
6200
+ });
6201
+ this.#voiceSlot.appendChild(this.#voice.element);
6202
+ }
6203
+ /** The built-in transcription handler for `data-transcribe-url`, or `null`. */
6204
+ #defaultTranscribeHandler(url) {
6205
+ if (url === null) {
6206
+ return null;
6207
+ }
6208
+ return (audio) => transcribeAudio(audio, { url, headers: this.headers });
6209
+ }
6210
+ /** Drop a voice transcript into the composer (appended to any typed text). */
6211
+ #insertVoiceText(text2) {
6212
+ const current = this.#input.value.trim();
6213
+ this.#input.value = current === "" ? text2 : `${current} ${text2}`;
6214
+ this.#onInput();
6215
+ this.#input.focus();
6216
+ }
5836
6217
  /** The client-side upload size cap from `data-attachment-max-bytes`. */
5837
6218
  #attachmentMaxBytes() {
5838
6219
  const attr = this.getAttribute("data-attachment-max-bytes");
@@ -6015,6 +6396,23 @@ Use the read_attachment tool with an id to read a file's contents.`
6015
6396
  toggleCollapsed() {
6016
6397
  this.setCollapsed(!this.collapsed);
6017
6398
  }
6399
+ /**
6400
+ * Flip the `theme` attribute between `light` and `dark` and persist the choice
6401
+ * per tab. Bound to the optional built-in header theme toggle
6402
+ * (`data-theme-toggle`); any non-dark theme (incl. `auto` / `code`) flips to
6403
+ * `dark` first.
6404
+ */
6405
+ toggleTheme() {
6406
+ const next = this.getAttribute("theme") === "dark" ? "light" : "dark";
6407
+ this.setAttribute("theme", next);
6408
+ sessionStorage.setItem(THEME_KEY, next);
6409
+ this.#syncThemeGlyph();
6410
+ }
6411
+ /** Reflect the current theme on the toggle: show the destination's glyph. */
6412
+ #syncThemeGlyph() {
6413
+ const dark = this.getAttribute("theme") === "dark";
6414
+ this.#themeToggle.textContent = dark ? "\u2600\uFE0F" : "\u{1F319}";
6415
+ }
6018
6416
  /**
6019
6417
  * Start a fresh conversation: forget the persisted history, drop the
6020
6418
  * in-memory run state, clear the transcript, and mint a new thread id.
@@ -6031,6 +6429,7 @@ Use the read_attachment tool with an id to read a file's contents.`
6031
6429
  this.#client = null;
6032
6430
  this.#streamingBubble = null;
6033
6431
  this.#currentGroup = null;
6432
+ this.#thoughts = null;
6034
6433
  this.#hidePending();
6035
6434
  this.#toolCards.clear();
6036
6435
  this.#serverSettled.clear();
@@ -6230,7 +6629,18 @@ Use the read_attachment tool with an id to read a file's contents.`
6230
6629
  newChat.addEventListener("click", () => this.newChat());
6231
6630
  const collapse = this.#headerButton("collapse", this.#strings.collapse, "\u2014");
6232
6631
  collapse.addEventListener("click", () => this.toggleCollapsed());
6233
- controls.append(history, newChat, collapse);
6632
+ controls.append(history, newChat);
6633
+ if (this.getAttribute("data-theme-toggle") !== null) {
6634
+ this.#themeToggle.type = "button";
6635
+ this.#themeToggle.className = "header-btn header-btn--theme";
6636
+ this.#themeToggle.setAttribute("part", "header-button theme-toggle");
6637
+ this.#themeToggle.title = this.#strings.toggleTheme;
6638
+ this.#themeToggle.setAttribute("aria-label", this.#strings.toggleTheme);
6639
+ this.#themeToggle.addEventListener("click", () => this.toggleTheme());
6640
+ this.#syncThemeGlyph();
6641
+ controls.append(this.#themeToggle);
6642
+ }
6643
+ controls.append(collapse);
6234
6644
  header.append(title, headerActions, controls);
6235
6645
  this.#messages.className = "messages";
6236
6646
  this.#messages.setAttribute("part", "messages");
@@ -6282,9 +6692,10 @@ Use the read_attachment tool with an id to read a file's contents.`
6282
6692
  this.#fileInput.hidden = true;
6283
6693
  this.#fileInput.addEventListener("change", () => this.#onFilesPicked());
6284
6694
  this.#attachSlot.className = "attachment-slot";
6695
+ this.#voiceSlot.className = "voice-slot";
6285
6696
  const footer = document.createElement("slot");
6286
6697
  footer.name = "footer";
6287
- inputRow.append(this.#attachButton, this.#input, this.#send, this.#fileInput);
6698
+ inputRow.append(this.#attachButton, this.#voiceSlot, this.#input, this.#send, this.#fileInput);
6288
6699
  this.#chat.append(
6289
6700
  header,
6290
6701
  this.#messages,
@@ -6509,8 +6920,18 @@ Use the read_attachment tool with an id to read a file's contents.`
6509
6920
  this.#ensureGroup();
6510
6921
  this.#showPending();
6511
6922
  },
6923
+ onReasoningStart: () => {
6924
+ this.#hidePending();
6925
+ this.#showThoughts();
6926
+ },
6927
+ onReasoningDelta: (buffer) => {
6928
+ this.#showThoughts().stream(buffer);
6929
+ },
6930
+ onReasoningEnd: () => {
6931
+ },
6512
6932
  onTextDelta: (buffer) => {
6513
6933
  this.#hidePending();
6934
+ this.#thoughts?.collapse();
6514
6935
  this.#streamInto(buffer);
6515
6936
  this.#streamDeltas += 1;
6516
6937
  },
@@ -6562,6 +6983,7 @@ Use the read_attachment tool with an id to read a file's contents.`
6562
6983
  this.#updateEmptyState();
6563
6984
  }
6564
6985
  this.#currentGroup = null;
6986
+ this.#thoughts = null;
6565
6987
  }
6566
6988
  };
6567
6989
  }
@@ -6605,6 +7027,21 @@ Use the read_attachment tool with an id to read a file's contents.`
6605
7027
  this.#pending?.remove();
6606
7028
  this.#pending = null;
6607
7029
  }
7030
+ /**
7031
+ * The current turn's thoughts region, creating it (at the top of the answer
7032
+ * group, above any streamed text or tool cards) on first sight. Idempotent
7033
+ * across a turn's reasoning tokens.
7034
+ */
7035
+ #showThoughts() {
7036
+ if (this.#thoughts === null) {
7037
+ this.#thoughts = new ThoughtsBlock(this.#strings);
7038
+ const group = this.#ensureGroup();
7039
+ group.insertBefore(this.#thoughts.element, group.firstChild);
7040
+ this.#updateEmptyState();
7041
+ this.#messages.scrollTop = this.#messages.scrollHeight;
7042
+ }
7043
+ return this.#thoughts;
7044
+ }
6608
7045
  #streamInto(buffer) {
6609
7046
  if (this.#streamingBubble === null) {
6610
7047
  this.#streamingBubble = this.appendMessage(MESSAGE_ROLE.ASSISTANT, "");
@@ -6676,7 +7113,7 @@ function setControlValue(el, value) {
6676
7113
  }
6677
7114
 
6678
7115
  // src/version.ts
6679
- var VERSION = "0.8.1";
7116
+ var VERSION = "0.9.0";
6680
7117
  export {
6681
7118
  AgUiChat,
6682
7119
  AgUiClient,
@@ -6728,6 +7165,7 @@ export {
6728
7165
  setNativeValue,
6729
7166
  toggleCheckbox,
6730
7167
  toggleControl,
7168
+ transcribeAudio,
6731
7169
  typeInto,
6732
7170
  uploadAttachment
6733
7171
  };