@heroui/agent 0.2.0-beta.7 → 0.2.0-beta.8

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.
@@ -1,25 +1,32 @@
1
1
  import {
2
- ComposerControlTooltip,
2
+ AGENT_MODEL_OPTIONS,
3
+ AgentLoadingState,
4
+ DEFAULT_AGENT_PICKER_MODEL_ID,
5
+ EmbedSessionManager,
3
6
  PanelShell,
4
- PoweredByHero,
5
- clearAgentComposerDraftsForProject,
7
+ ShellConversation,
8
+ containsComposerFocus,
9
+ createSubmittedAgentShellHandoff,
6
10
  isHeroUIAgentAttachmentContentType,
7
- readAgentShellHandoff,
8
11
  readSidebarWidth,
9
- resizeComposerTextarea,
10
12
  resolveOptions,
11
- scheduleIdleTask,
13
+ setFocus,
14
+ writeAgentShellAttachments,
12
15
  writeAgentShellHandoff
13
- } from "./chunk-OQGXZY5L.js";
16
+ } from "./chunk-W7SCMB3O.js";
17
+ import {
18
+ renderInlineMarkdown
19
+ } from "./chunk-Q7LGTVBL.js";
14
20
 
15
21
  // src/embed/provider.tsx
16
22
  import {
17
23
  useCallback,
18
24
  useEffect as useEffect2,
25
+ useEffectEvent,
19
26
  useLayoutEffect,
20
27
  useMemo,
21
28
  useRef as useRef2,
22
- useState as useState2,
29
+ useState,
23
30
  useSyncExternalStore
24
31
  } from "react";
25
32
  import { createRoot } from "react-dom/client";
@@ -288,17 +295,33 @@ function parseAgentRemoteConfig(value) {
288
295
  function cacheKey(agentId) {
289
296
  return `heroui-agent:config:${agentId}`;
290
297
  }
291
- function readCachedRemoteConfig(agentId) {
298
+ var REMOTE_CONFIG_CACHE_VERSION = 1;
299
+ function readCachedRemoteConfigSnapshot(agentId) {
292
300
  try {
293
301
  const raw = window.localStorage.getItem(cacheKey(agentId));
294
- return raw ? parseAgentRemoteConfig(JSON.parse(raw)) : null;
302
+ if (!raw) return null;
303
+ const value = JSON.parse(raw);
304
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
305
+ const envelope = value;
306
+ if (envelope.cacheVersion === REMOTE_CONFIG_CACHE_VERSION && typeof envelope.cachedAt === "number" && Number.isFinite(envelope.cachedAt)) {
307
+ const config = parseAgentRemoteConfig(envelope.config);
308
+ return config ? { cachedAt: envelope.cachedAt, config, legacy: false } : null;
309
+ }
310
+ }
311
+ const legacyConfig = parseAgentRemoteConfig(value);
312
+ return legacyConfig ? { cachedAt: 0, config: legacyConfig, legacy: true } : null;
295
313
  } catch {
296
314
  return null;
297
315
  }
298
316
  }
299
- function writeCachedRemoteConfig(agentId, config) {
317
+ function writeCachedRemoteConfig(agentId, config, cachedAt = Date.now()) {
300
318
  try {
301
- window.localStorage.setItem(cacheKey(agentId), JSON.stringify(config));
319
+ const envelope = {
320
+ cacheVersion: REMOTE_CONFIG_CACHE_VERSION,
321
+ cachedAt,
322
+ config
323
+ };
324
+ window.localStorage.setItem(cacheKey(agentId), JSON.stringify(envelope));
302
325
  } catch {
303
326
  }
304
327
  }
@@ -327,126 +350,125 @@ async function fetchRemoteConfig(apiBaseUrl, agentId, options = {}) {
327
350
  }
328
351
  }
329
352
 
330
- // src/embed/shell-conversation.tsx
331
- import { useEffect, useRef, useState } from "react";
353
+ // src/embed/shell-composer-presentation.tsx
354
+ import { ChevronDown, Microphone, Plus, ShieldCheck } from "@gravity-ui/icons";
355
+ import { useEffect, useRef } from "react";
332
356
  import { Fragment, jsx as jsx3, jsxs } from "react/jsx-runtime";
333
- function ShellConversation({
334
- agentId,
335
- greeting,
357
+ var PERMISSION_LABELS = {
358
+ ask: "Ask for approval",
359
+ auto: "Approve for me",
360
+ full: "Full access"
361
+ };
362
+ function createShellComposerPresentation({
363
+ onFileDialogActiveChange,
336
364
  onIntent,
337
- placeholder,
338
- suggestedPrompts
365
+ options
339
366
  }) {
340
- const [input, setInput] = useState(() => readAgentShellHandoff(agentId)?.prompt ?? "");
341
- const [submitted, setSubmitted] = useState(false);
342
- const textareaRef = useRef(null);
343
- useEffect(() => {
344
- if (submitted) return;
345
- writeAgentShellHandoff(agentId, { prompt: input, submitted: false });
346
- }, [input, agentId, submitted]);
347
- const submit = (prompt) => {
348
- const trimmed = prompt.trim();
349
- if (!trimmed) return;
350
- setSubmitted(true);
351
- setInput(trimmed);
352
- writeAgentShellHandoff(agentId, { prompt: trimmed, submitted: true });
353
- onIntent();
367
+ return {
368
+ end: /* @__PURE__ */ jsx3(ShellComposerControls, { options }),
369
+ footer: options.composerDisclaimer ? /* @__PURE__ */ jsx3("p", { className: "ha-footnote", children: renderInlineMarkdown(options.composerDisclaimer) }) : null,
370
+ start: options.composerAttachmentContentTypes.length > 0 ? /* @__PURE__ */ jsx3(
371
+ ShellAttachmentControl,
372
+ {
373
+ accept: options.composerAttachmentAccept,
374
+ agentId: options.agentId,
375
+ onFileDialogActiveChange,
376
+ onIntent
377
+ }
378
+ ) : null
354
379
  };
380
+ }
381
+ function ShellAttachmentControl({
382
+ accept,
383
+ agentId,
384
+ onFileDialogActiveChange,
385
+ onIntent
386
+ }) {
387
+ const inputRef = useRef(null);
388
+ useEffect(() => {
389
+ const input = inputRef.current;
390
+ let focusTimer;
391
+ const release = () => onFileDialogActiveChange(false);
392
+ const releaseAfterFocus = () => {
393
+ focusTimer = window.setTimeout(release, 0);
394
+ };
395
+ input?.addEventListener("cancel", release);
396
+ window.addEventListener("focus", releaseAfterFocus);
397
+ return () => {
398
+ input?.removeEventListener("cancel", release);
399
+ window.removeEventListener("focus", releaseAfterFocus);
400
+ window.clearTimeout(focusTimer);
401
+ };
402
+ }, [onFileDialogActiveChange]);
355
403
  return /* @__PURE__ */ jsxs(Fragment, { children: [
356
- /* @__PURE__ */ jsx3("div", { className: "ha-conversation", children: /* @__PURE__ */ jsxs("div", { "aria-live": "polite", className: "ha-messages", role: "log", children: [
357
- /* @__PURE__ */ jsxs("div", { className: "ha-empty", children: [
358
- /* @__PURE__ */ jsx3("h2", { children: greeting }),
359
- /* @__PURE__ */ jsx3("p", { children: "Live answers with charts, metrics, and tables." })
360
- ] }),
361
- submitted ? /* @__PURE__ */ jsx3("div", { className: "ha-message", "data-role": "user", children: /* @__PURE__ */ jsx3("div", { className: "ha-message-body", children: input }) }) : null
362
- ] }) }),
363
- /* @__PURE__ */ jsxs(
364
- "form",
404
+ /* @__PURE__ */ jsx3(
405
+ "input",
406
+ {
407
+ ref: inputRef,
408
+ multiple: true,
409
+ accept,
410
+ "aria-label": "Choose files to attach",
411
+ className: "ha-file-input",
412
+ type: "file",
413
+ onChange: (event) => {
414
+ writeAgentShellAttachments(agentId, Array.from(event.currentTarget.files ?? []));
415
+ event.currentTarget.value = "";
416
+ onFileDialogActiveChange(false);
417
+ onIntent();
418
+ }
419
+ }
420
+ ),
421
+ /* @__PURE__ */ jsx3(
422
+ "button",
365
423
  {
366
- className: "ha-composer-wrap",
367
- onSubmit: (event) => {
368
- event.preventDefault();
369
- submit(input);
424
+ "aria-label": "Attach files",
425
+ className: "ha-attach",
426
+ type: "button",
427
+ onClick: () => {
428
+ onFileDialogActiveChange(true);
429
+ inputRef.current?.click();
370
430
  },
371
- children: [
372
- !submitted && suggestedPrompts.length > 0 ? /* @__PURE__ */ jsx3("div", { "aria-label": "Suggested prompts", className: "ha-suggestions", role: "group", children: suggestedPrompts.map((prompt) => /* @__PURE__ */ jsxs(
373
- "button",
374
- {
375
- className: "ha-suggestion",
376
- type: "button",
377
- onClick: () => submit(prompt),
378
- children: [
379
- /* @__PURE__ */ jsx3("span", { children: prompt }),
380
- /* @__PURE__ */ jsx3(ArrowUpRightGlyph, {})
381
- ]
382
- },
383
- prompt
384
- )) }) : null,
385
- /* @__PURE__ */ jsxs("div", { className: "@container ha-composer", children: [
386
- /* @__PURE__ */ jsx3(
387
- "textarea",
388
- {
389
- ref: textareaRef,
390
- "aria-label": "Message the assistant",
391
- placeholder,
392
- rows: 1,
393
- value: input,
394
- onFocus: onIntent,
395
- onChange: (event) => {
396
- setInput(event.currentTarget.value);
397
- resizeComposerTextarea(event.currentTarget);
398
- onIntent();
399
- },
400
- onKeyDown: (event) => {
401
- if (event.key !== "Enter" || event.shiftKey) return;
402
- event.preventDefault();
403
- submit(input);
404
- }
405
- }
406
- ),
407
- /* @__PURE__ */ jsxs(
408
- "button",
409
- {
410
- "aria-label": "Send message",
411
- className: "ha-send",
412
- "data-shortcut": "\u21B5",
413
- "data-tooltip": "Send message",
414
- disabled: !input.trim() || submitted,
415
- type: "submit",
416
- children: [
417
- /* @__PURE__ */ jsx3(ArrowUpGlyph, {}),
418
- /* @__PURE__ */ jsx3(ComposerControlTooltip, { shortcut: "\u21B5", children: "Send message" })
419
- ]
420
- }
421
- )
422
- ] }),
423
- /* @__PURE__ */ jsx3(PoweredByHero, {})
424
- ]
431
+ children: /* @__PURE__ */ jsx3(Plus, { "aria-hidden": "true" })
425
432
  }
426
433
  )
427
434
  ] });
428
435
  }
429
- function ArrowUpGlyph() {
430
- return /* @__PURE__ */ jsx3("svg", { "aria-hidden": "true", fill: "none", height: "18", viewBox: "0 0 18 18", width: "18", children: /* @__PURE__ */ jsx3(
431
- "path",
432
- {
433
- d: "M9 14.5V3.5m0 0L4.5 8M9 3.5 13.5 8",
434
- stroke: "currentColor",
435
- strokeLinecap: "round",
436
- strokeLinejoin: "round",
437
- strokeWidth: "1.6"
438
- }
439
- ) });
436
+ function ShellComposerControls({ options }) {
437
+ const modelId = options.composerDefaultModel ?? DEFAULT_AGENT_PICKER_MODEL_ID;
438
+ const modelLabel = AGENT_MODEL_OPTIONS.find((option) => option.id === modelId)?.label ?? modelId;
439
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
440
+ options.permissionShowPicker && options.tools.length > 0 ? /* @__PURE__ */ jsx3(
441
+ ShellPicker,
442
+ {
443
+ icon: /* @__PURE__ */ jsx3(ShieldCheck, { "aria-hidden": "true", className: "aui-permission-picker__trigger-icon" }),
444
+ kind: "permission",
445
+ label: PERMISSION_LABELS[options.permissionDefaultMode],
446
+ tone: options.permissionDefaultMode === "full" ? "warning" : void 0
447
+ }
448
+ ) : null,
449
+ options.composerModelPicker ? /* @__PURE__ */ jsx3(ShellPicker, { kind: "model", label: modelLabel }) : null,
450
+ options.composerDictation ? /* @__PURE__ */ jsx3("button", { disabled: true, "aria-label": "Dictate message", className: "ha-dictation", type: "button", children: /* @__PURE__ */ jsx3(Microphone, { "aria-hidden": "true" }) }) : null
451
+ ] });
440
452
  }
441
- function ArrowUpRightGlyph() {
442
- return /* @__PURE__ */ jsx3("svg", { "aria-hidden": "true", fill: "none", height: "16", viewBox: "0 0 16 16", width: "16", children: /* @__PURE__ */ jsx3(
443
- "path",
453
+ function ShellPicker({
454
+ icon,
455
+ kind,
456
+ label,
457
+ tone
458
+ }) {
459
+ const name = `aui-${kind}-picker`;
460
+ return /* @__PURE__ */ jsx3("div", { className: `${name} ha-${kind}-picker`, "data-tone": tone, children: /* @__PURE__ */ jsxs(
461
+ "button",
444
462
  {
445
- d: "M5 11 11 5m0 0H6m5 0v5",
446
- stroke: "currentColor",
447
- strokeLinecap: "round",
448
- strokeLinejoin: "round",
449
- strokeWidth: "1.5"
463
+ disabled: true,
464
+ "aria-label": kind === "model" ? "AI model" : "Tool permissions",
465
+ className: `${name}__trigger ha-shell-picker-trigger`,
466
+ type: "button",
467
+ children: [
468
+ icon,
469
+ /* @__PURE__ */ jsx3("span", { className: `${name}__value`, children: label }),
470
+ /* @__PURE__ */ jsx3(ChevronDown, { "aria-hidden": "true", className: `${name}__indicator` })
471
+ ]
450
472
  }
451
473
  ) });
452
474
  }
@@ -507,8 +529,8 @@ function loadAgentWebfont(font) {
507
529
 
508
530
  // src/embed/provider.tsx
509
531
  import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
510
- var REMOTE_CONFIG_PAINT_BUDGET_MS = 600;
511
- var RUNTIME_PREFETCH_IDLE_TIMEOUT_MS = 1200;
532
+ var REMOTE_CONFIG_RESOLUTION_TIMEOUT_MS = 600;
533
+ var REMOTE_CONFIG_PRESENTATION_MAX_AGE_MS = 5 * 6e4;
512
534
  var OPEN_STATE_STORAGE_PREFIX = "heroui-agent:open:";
513
535
  function openStateStorageKey(agentId) {
514
536
  return `${OPEN_STATE_STORAGE_PREFIX}${agentId}`;
@@ -532,13 +554,24 @@ var embedRuntimePromise;
532
554
  function loadEmbedRuntime() {
533
555
  embedRuntimePromise ??= import(
534
556
  /* webpackPrefetch: true */
535
- "./embed-runtime-UF7XJIFA.js"
557
+ "./embed-runtime-VVAGDPYY.js"
536
558
  ).then(
537
559
  (module) => module.default
538
560
  );
539
561
  return embedRuntimePromise;
540
562
  }
541
563
  var controllers = /* @__PURE__ */ new Map();
564
+ var controllerListeners = /* @__PURE__ */ new Set();
565
+ function getController(agentId) {
566
+ return agentId ? controllers.get(agentId) : [...controllers.values()][controllers.size - 1];
567
+ }
568
+ function notifyControllerListeners() {
569
+ for (const listener of controllerListeners) listener();
570
+ }
571
+ function subscribeToControllers(listener) {
572
+ controllerListeners.add(listener);
573
+ return () => controllerListeners.delete(listener);
574
+ }
542
575
  function subscribeToColorScheme(onChange) {
543
576
  const media = window.matchMedia?.("(prefers-color-scheme: dark)");
544
577
  media?.addEventListener?.("change", onChange);
@@ -547,68 +580,98 @@ function subscribeToColorScheme(onChange) {
547
580
  function prefersDarkColorScheme() {
548
581
  return window.matchMedia?.("(prefers-color-scheme: dark)").matches === true;
549
582
  }
550
- function dispatch(agentId, method) {
551
- const controller = agentId ? controllers.get(agentId) : [...controllers.values()][controllers.size - 1];
583
+ function dispatch(agentId, method, ...args) {
584
+ const controller = getController(agentId);
552
585
  if (!controller) {
553
586
  console.warn("HeroUI Agent is not mounted yet. Add <HeroUIAgent /> to your layout.");
554
587
  return;
555
588
  }
556
- controller[method]();
589
+ const action = controller[method];
590
+ action(...args);
557
591
  }
558
592
  function useRemoteConfig(agentId, apiBaseUrl, enabled) {
559
- const [state, setState] = useState2(() => {
560
- if (!enabled || typeof window === "undefined") return { config: null, resolved: false };
561
- const cached = readCachedRemoteConfig(agentId);
562
- return { config: cached, resolved: Boolean(cached) };
593
+ const [cachedSnapshot] = useState(() => {
594
+ if (!enabled || typeof window === "undefined") return null;
595
+ return readCachedRemoteConfigSnapshot(agentId);
596
+ });
597
+ const [state, setState] = useState(() => {
598
+ if (!enabled) return { config: null, presentationReady: true };
599
+ const cacheAge = cachedSnapshot ? Date.now() - cachedSnapshot.cachedAt : Number.POSITIVE_INFINITY;
600
+ const fresh = cachedSnapshot !== null && !cachedSnapshot.legacy && cacheAge >= 0 && cacheAge <= REMOTE_CONFIG_PRESENTATION_MAX_AGE_MS;
601
+ return {
602
+ config: fresh ? cachedSnapshot.config : null,
603
+ presentationReady: fresh
604
+ };
563
605
  });
564
606
  useEffect2(() => {
565
607
  if (!enabled) return;
566
- const cached = readCachedRemoteConfig(agentId);
567
608
  const controller = new AbortController();
568
- const budget = window.setTimeout(
569
- () => setState((current) => ({ ...current, resolved: true })),
570
- REMOTE_CONFIG_PAINT_BUDGET_MS
571
- );
609
+ let active = true;
610
+ const settle = (config) => {
611
+ setState(
612
+ (current) => current.presentationReady ? current : { config, presentationReady: true }
613
+ );
614
+ };
615
+ const budget = window.setTimeout(() => {
616
+ if (!active) return;
617
+ controller.abort();
618
+ settle(cachedSnapshot?.config ?? null);
619
+ }, REMOTE_CONFIG_RESOLUTION_TIMEOUT_MS);
572
620
  void fetchRemoteConfig(apiBaseUrl, agentId, {
573
- ...cached?.revision ? { revision: cached.revision } : {},
621
+ ...cachedSnapshot?.config.revision ? { revision: cachedSnapshot.config.revision } : {},
574
622
  signal: controller.signal
575
623
  }).then((result) => {
576
- if (controller.signal.aborted) return;
624
+ if (!active || controller.signal.aborted) return;
577
625
  if (result.status === "ok") {
578
- setState({ config: result.config, resolved: true });
579
626
  writeCachedRemoteConfig(agentId, result.config);
627
+ settle(result.config);
628
+ return;
629
+ }
630
+ if (result.status === "not-modified") {
631
+ if (cachedSnapshot) writeCachedRemoteConfig(agentId, cachedSnapshot.config);
632
+ settle(cachedSnapshot?.config ?? null);
633
+ return;
580
634
  }
581
635
  if (result.status === "missing") {
582
- setState({ config: null, resolved: true });
583
636
  clearCachedRemoteConfig(agentId);
637
+ settle(null);
638
+ return;
584
639
  }
640
+ settle(cachedSnapshot?.config ?? null);
585
641
  }).finally(() => {
586
- if (controller.signal.aborted) return;
642
+ if (!active || controller.signal.aborted) return;
587
643
  window.clearTimeout(budget);
588
- setState((current) => ({ ...current, resolved: true }));
589
644
  });
590
645
  return () => {
646
+ active = false;
591
647
  controller.abort();
592
648
  window.clearTimeout(budget);
593
649
  };
594
- }, [apiBaseUrl, enabled, agentId]);
595
- return enabled ? state : { config: null, resolved: true };
650
+ }, [apiBaseUrl, cachedSnapshot, enabled, agentId]);
651
+ return enabled ? state : { config: null, presentationReady: true };
596
652
  }
597
653
  function useAgent(agentId) {
654
+ const getReadySnapshot = useCallback(() => getController(agentId)?.ready ?? false, [agentId]);
655
+ const ready = useSyncExternalStore(subscribeToControllers, getReadySnapshot, () => false);
598
656
  return useMemo(
599
657
  () => ({
600
658
  hide: () => dispatch(agentId, "hide"),
601
- newConversation: () => dispatch(agentId, "newConversation"),
659
+ newConversation: (prompt) => dispatch(agentId, "newConversation", prompt),
602
660
  preload: () => dispatch(agentId, "preload"),
661
+ ready,
603
662
  refreshAuth: () => dispatch(agentId, "refreshAuth"),
604
663
  show: () => dispatch(agentId, "show"),
605
664
  shutdown: () => dispatch(agentId, "shutdown"),
606
665
  toggle: () => dispatch(agentId, "toggle")
607
666
  }),
608
- [agentId]
667
+ [agentId, ready]
609
668
  );
610
669
  }
611
670
  function HeroUIAgent(props) {
671
+ const remoteSourceKey = `${props.agentId}\0${props._api?.baseUrl ?? ""}\0${props.remoteConfig !== false}`;
672
+ return /* @__PURE__ */ jsx4(HeroUIAgentMount, { ...props }, remoteSourceKey);
673
+ }
674
+ function HeroUIAgentMount(props) {
612
675
  const {
613
676
  _api,
614
677
  agentId,
@@ -620,6 +683,7 @@ function HeroUIAgent(props) {
620
683
  getAuthToken,
621
684
  markdown,
622
685
  onFeedback,
686
+ onReady,
623
687
  permissions,
624
688
  preload: preloadEnabled,
625
689
  remoteConfig: remoteConfigEnabled = true,
@@ -632,7 +696,7 @@ function HeroUIAgent(props) {
632
696
  } = props;
633
697
  const markdownPlugins = markdown?.plugins;
634
698
  const apiBaseUrl = (_api?.baseUrl ?? "https://api.heroui.com").replace(/\/$/, "");
635
- const { config: remoteConfig, resolved: remoteConfigResolved } = useRemoteConfig(
699
+ const { config: remoteConfig, presentationReady: remoteConfigPresentationReady } = useRemoteConfig(
636
700
  agentId,
637
701
  apiBaseUrl,
638
702
  remoteConfigEnabled
@@ -668,26 +732,34 @@ function HeroUIAgent(props) {
668
732
  tools
669
733
  });
670
734
  }, [context, getAuthToken, markdownPlugins, onFeedback, agentId, serializedConfig, tools]);
735
+ const tokenManager = useMemo(
736
+ () => new EmbedSessionManager(agentId, getAuthToken),
737
+ [agentId, getAuthToken]
738
+ );
671
739
  const hostFont = hostSuppliesFont(props);
672
740
  const remoteWebfont = remoteConfig?.webfont;
673
741
  useEffect2(() => {
674
742
  if (hostFont) return;
675
743
  return loadAgentWebfont(remoteWebfont);
676
744
  }, [hostFont, remoteWebfont]);
677
- const [restoredOpenState] = useState2(() => reopenOnRefresh === true && readOpenState(agentId));
678
- const [isOpen, setIsOpen] = useState2(restoredOpenState);
679
- const [conversationEpoch, setConversationEpoch] = useState2(1);
680
- const [identityEpoch, setIdentityEpoch] = useState2(1);
681
- const [identityReset, setIdentityReset] = useState2(false);
682
- const [requestedConversationId, setRequestedConversationId] = useState2(
745
+ const [restoredOpenState] = useState(() => reopenOnRefresh === true && readOpenState(agentId));
746
+ const [openRequested, setOpenRequested] = useState(restoredOpenState);
747
+ const [conversationEpoch, setConversationEpoch] = useState(1);
748
+ const [identityEpoch, setIdentityEpoch] = useState(1);
749
+ const [identityReset, setIdentityReset] = useState(false);
750
+ const [requestedConversationId, setRequestedConversationId] = useState(
683
751
  restoredOpenState || options.startNewConversationOnOpen ? null : void 0
684
752
  );
685
- const [portalRoot, setPortalRoot] = useState2(null);
686
- const [EmbedRuntime, setEmbedRuntime] = useState2(null);
753
+ const [portalRoot, setPortalRoot] = useState(null);
754
+ const [EmbedRuntime, setEmbedRuntime] = useState(null);
755
+ const [hasCompletedInitialLoad, setHasCompletedInitialLoad] = useState(false);
756
+ const [hasUnreadMessages, setHasUnreadMessages] = useState(false);
757
+ const [runtimeReadyEpoch, setRuntimeReadyEpoch] = useState(null);
687
758
  const conversationEpochRef = useRef2(1);
688
759
  const launcherRef = useRef2(null);
760
+ const restoreLauncherFocusRef = useRef2(false);
689
761
  const portalRootRef = useRef2(null);
690
- const readyEpochRef = useRef2(null);
762
+ const reportedReadyEpochRef = useRef2(null);
691
763
  const shouldOpenRef = useRef2(restoredOpenState);
692
764
  const hostRef = useRef2(null);
693
765
  const embedReactRootRef = useRef2(null);
@@ -698,20 +770,39 @@ function HeroUIAgent(props) {
698
770
  () => false
699
771
  );
700
772
  const designThemeClass = options.designTheme === "base" ? null : `${options.designTheme}-${options.colorScheme === "system" ? prefersDark ? "dark" : "light" : options.colorScheme}`;
701
- const [shouldLoadRuntime, setShouldLoadRuntime] = useState2(restoredOpenState);
773
+ const ready = remoteConfigPresentationReady && runtimeReadyEpoch === conversationEpoch && EmbedRuntime !== null;
774
+ const isOpen = remoteConfigPresentationReady && openRequested;
775
+ const [shouldLoadRuntime, setShouldLoadRuntime] = useState(restoredOpenState || options.preload);
702
776
  const requestRuntime = useCallback(() => setShouldLoadRuntime(true), []);
703
- const [preloadRequested, setPreloadRequested] = useState2(false);
704
- const shouldWarmSession = options.preload || preloadRequested;
777
+ const [composerControlsRevealed, setComposerControlsRevealed] = useState(false);
778
+ const [composerFileDialogActive, setComposerFileDialogActive] = useState(false);
779
+ const revealComposerControls = useCallback(() => setComposerControlsRevealed(true), []);
705
780
  useEffect2(() => {
706
- if (options.reopenOnRefresh) writeOpenState(options.agentId, isOpen);
707
- else writeOpenState(options.agentId, false);
708
- }, [isOpen, options.agentId, options.reopenOnRefresh]);
781
+ if (!composerControlsRevealed || !portalRoot) return;
782
+ const reconcileComposerFocus = () => {
783
+ queueMicrotask(() => {
784
+ const composer2 = portalRoot.querySelector(".ha-composer:not([hidden] *)");
785
+ if (composer2 && containsComposerFocus(composer2, document.activeElement)) return;
786
+ setComposerControlsRevealed(false);
787
+ });
788
+ };
789
+ portalRoot.addEventListener("focusout", reconcileComposerFocus);
790
+ return () => portalRoot.removeEventListener("focusout", reconcileComposerFocus);
791
+ }, [composerControlsRevealed, portalRoot]);
709
792
  useEffect2(() => {
710
- if (!options.preload || shouldLoadRuntime) return;
711
- return scheduleIdleTask(requestRuntime, RUNTIME_PREFETCH_IDLE_TIMEOUT_MS);
712
- }, [options.preload, requestRuntime, shouldLoadRuntime]);
793
+ if (!shouldLoadRuntime) return;
794
+ void tokenManager.get().catch(() => void 0);
795
+ }, [shouldLoadRuntime, tokenManager]);
796
+ useEffect2(() => {
797
+ if (options.reopenOnRefresh) writeOpenState(options.agentId, openRequested);
798
+ else writeOpenState(options.agentId, false);
799
+ }, [openRequested, options.agentId, options.reopenOnRefresh]);
713
800
  useEffect2(() => {
714
801
  if (!shouldLoadRuntime) return;
802
+ if (!remoteConfigPresentationReady) {
803
+ void loadEmbedRuntime().catch(() => void 0);
804
+ return;
805
+ }
715
806
  let active = true;
716
807
  void loadEmbedRuntime().then((runtime) => {
717
808
  if (active) setEmbedRuntime(() => runtime);
@@ -719,7 +810,7 @@ function HeroUIAgent(props) {
719
810
  return () => {
720
811
  active = false;
721
812
  };
722
- }, [shouldLoadRuntime]);
813
+ }, [remoteConfigPresentationReady, shouldLoadRuntime]);
723
814
  useEffect2(() => {
724
815
  if (!isOpen || options.viewMode !== "sidebar") return;
725
816
  if (window.innerWidth < 640) return;
@@ -755,6 +846,7 @@ function HeroUIAgent(props) {
755
846
  });
756
847
  return () => {
757
848
  active = false;
849
+ setFocus(options.agentId, false);
758
850
  const reactRoot = embedReactRootRef.current;
759
851
  portalRootRef.current = null;
760
852
  hostRef.current = null;
@@ -802,6 +894,7 @@ function HeroUIAgent(props) {
802
894
  designThemeClass,
803
895
  isOpen,
804
896
  options.colorScheme,
897
+ options.agentId,
805
898
  options.launcherOffset,
806
899
  options.launcherPosition,
807
900
  options.panelStyle,
@@ -809,43 +902,64 @@ function HeroUIAgent(props) {
809
902
  options.viewMode,
810
903
  portalRoot
811
904
  ]);
812
- const prepareNextConversation = useCallback((conversationId) => {
813
- readyEpochRef.current = null;
814
- setRequestedConversationId(conversationId);
815
- setConversationEpoch((epoch) => {
816
- const nextEpoch = epoch + 1;
905
+ const prepareNextConversation = useCallback(
906
+ (conversationId) => {
907
+ setFocus(options.agentId, false);
908
+ setComposerFileDialogActive(false);
909
+ setRuntimeReadyEpoch(null);
910
+ setRequestedConversationId(conversationId);
911
+ const nextEpoch = conversationEpochRef.current + 1;
817
912
  conversationEpochRef.current = nextEpoch;
818
- return nextEpoch;
819
- });
820
- }, []);
913
+ setConversationEpoch(nextEpoch);
914
+ },
915
+ [options.agentId]
916
+ );
821
917
  const handleRuntimeReady = useCallback((readyEpoch) => {
822
918
  if (conversationEpochRef.current !== readyEpoch) return;
823
- readyEpochRef.current = readyEpoch;
824
- if (shouldOpenRef.current) setIsOpen(true);
919
+ setHasCompletedInitialLoad(true);
920
+ setRuntimeReadyEpoch(readyEpoch);
825
921
  }, []);
922
+ const emitReady = useEffectEvent(() => onReady?.());
923
+ useEffect2(() => {
924
+ if (!ready || reportedReadyEpochRef.current === conversationEpoch) return;
925
+ reportedReadyEpochRef.current = conversationEpoch;
926
+ emitReady();
927
+ }, [conversationEpoch, ready]);
826
928
  const hide = useCallback(() => {
827
929
  shouldOpenRef.current = false;
828
- setIsOpen(false);
930
+ restoreLauncherFocusRef.current = true;
931
+ setOpenRequested(false);
932
+ setComposerControlsRevealed(false);
829
933
  prepareNextConversation(options.startNewConversationOnOpen ? null : void 0);
830
- requestAnimationFrame(() => launcherRef.current?.focus());
831
934
  }, [options.startNewConversationOnOpen, prepareNextConversation]);
935
+ useEffect2(() => {
936
+ if (isOpen || !ready || !restoreLauncherFocusRef.current) return;
937
+ restoreLauncherFocusRef.current = false;
938
+ requestAnimationFrame(() => launcherRef.current?.focus());
939
+ }, [isOpen, ready]);
832
940
  const show = useCallback(() => {
833
941
  shouldOpenRef.current = true;
834
- requestRuntime();
835
- setIsOpen(true);
836
- }, [requestRuntime]);
837
- const preload = useCallback(() => {
838
- setPreloadRequested(true);
942
+ setOpenRequested(true);
839
943
  requestRuntime();
840
944
  }, [requestRuntime]);
841
- const newConversation = useCallback(() => {
842
- shouldOpenRef.current = true;
843
- requestRuntime();
844
- prepareNextConversation(null);
845
- }, [prepareNextConversation, requestRuntime]);
945
+ const preload = requestRuntime;
946
+ const newConversation = useCallback(
947
+ (prompt) => {
948
+ const submission = prompt ? createSubmittedAgentShellHandoff(prompt) : null;
949
+ if (submission) writeAgentShellHandoff(options.agentId, submission);
950
+ shouldOpenRef.current = true;
951
+ setOpenRequested(true);
952
+ setComposerControlsRevealed(false);
953
+ requestRuntime();
954
+ prepareNextConversation(null);
955
+ },
956
+ [options.agentId, prepareNextConversation, requestRuntime]
957
+ );
846
958
  const selectConversation = useCallback(
847
959
  (conversationId) => {
848
960
  shouldOpenRef.current = true;
961
+ setOpenRequested(true);
962
+ setComposerControlsRevealed(false);
849
963
  requestRuntime();
850
964
  prepareNextConversation(conversationId);
851
965
  },
@@ -856,6 +970,7 @@ function HeroUIAgent(props) {
856
970
  else show();
857
971
  }, [hide, isOpen, show]);
858
972
  const refreshAuth = useCallback(() => {
973
+ setHasUnreadMessages(false);
859
974
  setIdentityReset(false);
860
975
  setIdentityEpoch((epoch) => epoch + 1);
861
976
  }, []);
@@ -863,21 +978,24 @@ function HeroUIAgent(props) {
863
978
  const prefixes = [
864
979
  `heroui-agent:anonymous:${options.agentId}`,
865
980
  `heroui-agent:conversation:${options.agentId}:`,
866
- `heroui-agent:history:${options.agentId}:`,
867
- `heroui-agent:session:${options.agentId}:`
981
+ `heroui-agent:history:${options.agentId}:`
868
982
  ];
869
983
  for (let index = localStorage.length - 1; index >= 0; index -= 1) {
870
984
  const key = localStorage.key(index);
871
985
  if (key && prefixes.some((prefix) => key.startsWith(prefix))) localStorage.removeItem(key);
872
986
  }
873
987
  shouldOpenRef.current = false;
874
- setIsOpen(false);
988
+ setHasUnreadMessages(false);
989
+ setOpenRequested(false);
990
+ setComposerControlsRevealed(false);
875
991
  writeOpenState(options.agentId, false);
876
992
  setIdentityReset(true);
877
993
  setIdentityEpoch((epoch) => epoch + 1);
878
994
  prepareNextConversation();
879
995
  window.setTimeout(() => {
880
- void clearAgentComposerDraftsForProject(options.agentId);
996
+ void import("./composer-draft-DTQAEO5K.js").then(
997
+ ({ clearAgentComposerDraftsForProject }) => clearAgentComposerDraftsForProject(options.agentId)
998
+ ).catch(() => void 0);
881
999
  }, 0);
882
1000
  }, [options.agentId, prepareNextConversation]);
883
1001
  useEffect2(() => {
@@ -885,53 +1003,68 @@ function HeroUIAgent(props) {
885
1003
  hide,
886
1004
  newConversation,
887
1005
  preload,
1006
+ ready,
888
1007
  refreshAuth,
889
1008
  show,
890
1009
  shutdown,
891
1010
  toggle
892
1011
  };
893
1012
  controllers.set(options.agentId, controller);
1013
+ notifyControllerListeners();
894
1014
  return () => {
895
1015
  if (controllers.get(options.agentId) === controller) {
896
1016
  controllers.delete(options.agentId);
1017
+ notifyControllerListeners();
897
1018
  }
898
1019
  };
899
- }, [hide, newConversation, options.agentId, preload, refreshAuth, show, shutdown, toggle]);
1020
+ }, [hide, newConversation, options.agentId, preload, ready, refreshAuth, show, shutdown, toggle]);
900
1021
  useLayoutEffect(() => {
901
1022
  const reactRoot = embedReactRootRef.current;
902
1023
  if (!portalRoot || !reactRoot) return;
1024
+ const shellComposer = createShellComposerPresentation({
1025
+ onFileDialogActiveChange: setComposerFileDialogActive,
1026
+ onIntent: requestRuntime,
1027
+ options
1028
+ });
903
1029
  reactRoot.render(
904
1030
  /* @__PURE__ */ jsxs2(Fragment2, { children: [
905
- options.showLauncher && remoteConfigResolved && !isOpen ? /* @__PURE__ */ jsx4(
1031
+ remoteConfigPresentationReady && options.showLauncher && !isOpen ? /* @__PURE__ */ jsxs2(
906
1032
  "button",
907
1033
  {
908
1034
  ref: launcherRef,
909
1035
  "aria-expanded": false,
910
1036
  "aria-haspopup": "dialog",
911
- "aria-label": "Open data assistant",
912
1037
  className: "ha-launcher",
913
1038
  style: options.launcherStyle,
914
1039
  type: "button",
1040
+ "aria-label": hasUnreadMessages ? "Open data assistant, unread messages" : "Open data assistant",
915
1041
  onClick: show,
916
- onFocus: options.preload ? requestRuntime : void 0,
917
- onPointerEnter: options.preload ? requestRuntime : void 0,
918
- children: /* @__PURE__ */ jsx4(LauncherIcon, { icon: options.launcherIcon })
1042
+ children: [
1043
+ /* @__PURE__ */ jsx4(LauncherIcon, { icon: options.launcherIcon }),
1044
+ hasUnreadMessages ? /* @__PURE__ */ jsx4("span", { "aria-hidden": "true", className: "ha-launcher__unread" }) : null
1045
+ ]
919
1046
  }
920
1047
  ) : null,
921
- EmbedRuntime ? /* @__PURE__ */ jsx4(
1048
+ EmbedRuntime && !composerFileDialogActive ? /* @__PURE__ */ jsx4(
922
1049
  EmbedRuntime,
923
1050
  {
1051
+ composerControlsRevealed,
924
1052
  conversationEpoch,
925
1053
  identityEpoch,
926
1054
  identityReset,
927
1055
  open: isOpen,
928
1056
  options,
929
1057
  requestedConversationId,
930
- warmSession: shouldWarmSession,
1058
+ shellComposerEndSlot: shellComposer.end,
1059
+ shellComposerFooterSlot: shellComposer.footer,
1060
+ showReadyLoader: !hasCompletedInitialLoad && requestedConversationId !== null,
1061
+ tokenManager,
931
1062
  onClose: hide,
1063
+ onComposerControlsReveal: revealComposerControls,
932
1064
  onNewConversation: newConversation,
933
1065
  onReady: handleRuntimeReady,
934
- onSelectConversation: selectConversation
1066
+ onSelectConversation: selectConversation,
1067
+ onUnreadChange: setHasUnreadMessages
935
1068
  }
936
1069
  ) : /* @__PURE__ */ jsx4(
937
1070
  PanelShell,
@@ -939,28 +1072,41 @@ function HeroUIAgent(props) {
939
1072
  agentId: options.agentId,
940
1073
  defaultExpanded: options.panelExpanded,
941
1074
  expandable: options.panelExpandable,
1075
+ historySlot: /* @__PURE__ */ jsx4(Fragment2, {}),
942
1076
  modal: options.viewMode !== "sidebar",
943
1077
  name: "Data assistant",
944
1078
  open: isOpen,
945
1079
  onClose: hide,
946
- children: /* @__PURE__ */ jsx4(
1080
+ children: requestedConversationId === null ? /* @__PURE__ */ jsx4(
947
1081
  ShellConversation,
948
1082
  {
1083
+ composerConnecting: true,
949
1084
  agentId: options.agentId,
1085
+ composerControlsAvailable: options.composerControlsAvailable,
1086
+ composerControlsRevealed,
1087
+ composerEndSlot: shellComposer.end,
1088
+ composerFooterSlot: shellComposer.footer,
1089
+ composerStartSlot: shellComposer.start,
950
1090
  greeting: options.greeting,
951
1091
  placeholder: options.composerPlaceholder,
952
1092
  suggestedPrompts: options.suggestedPrompts ?? [],
1093
+ onComposerControlsReveal: revealComposerControls,
953
1094
  onIntent: requestRuntime
954
- }
955
- )
1095
+ },
1096
+ `shell-${conversationEpoch}`
1097
+ ) : /* @__PURE__ */ jsx4(AgentLoadingState, {})
956
1098
  }
957
1099
  )
958
1100
  ] })
959
1101
  );
960
1102
  }, [
961
1103
  EmbedRuntime,
1104
+ composerControlsRevealed,
1105
+ composerFileDialogActive,
962
1106
  conversationEpoch,
963
1107
  handleRuntimeReady,
1108
+ hasUnreadMessages,
1109
+ hasCompletedInitialLoad,
964
1110
  hide,
965
1111
  identityEpoch,
966
1112
  identityReset,
@@ -968,12 +1114,14 @@ function HeroUIAgent(props) {
968
1114
  newConversation,
969
1115
  options,
970
1116
  portalRoot,
971
- remoteConfigResolved,
972
- requestRuntime,
1117
+ ready,
1118
+ remoteConfigPresentationReady,
973
1119
  requestedConversationId,
1120
+ requestRuntime,
1121
+ revealComposerControls,
974
1122
  selectConversation,
975
- shouldWarmSession,
976
- show
1123
+ show,
1124
+ tokenManager
977
1125
  ]);
978
1126
  return null;
979
1127
  }