@gram-ai/elements 1.35.0 → 1.37.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/dist/components/assistant-ui/markdown-text.d.ts +1 -1
  2. package/dist/components/assistant-ui/thinking-indicator.d.ts +8 -0
  3. package/dist/components/ui/tool-ui.d.ts +3 -1
  4. package/dist/elements.cjs +1 -1
  5. package/dist/elements.css +1 -1
  6. package/dist/elements.js +2 -2
  7. package/dist/hooks/useGramThreadListAdapter.d.ts +8 -0
  8. package/dist/{index-Dz13dSDa.js → index-Bv-yE4G1.js} +2874 -2803
  9. package/dist/index-Bv-yE4G1.js.map +1 -0
  10. package/dist/{index-D0jIGQr7.cjs → index-CGBkMd0d.cjs} +48 -48
  11. package/dist/index-CGBkMd0d.cjs.map +1 -0
  12. package/dist/index-Dpk3C8VH.cjs +194 -0
  13. package/dist/index-Dpk3C8VH.cjs.map +1 -0
  14. package/dist/{index-BhIowiZF.js → index-Em1Ot0b6.js} +8665 -8534
  15. package/dist/index-Em1Ot0b6.js.map +1 -0
  16. package/dist/plugins.cjs +1 -1
  17. package/dist/plugins.js +1 -1
  18. package/dist/{profiler-CtGKTWWP.js → profiler-BnInDjd4.js} +2 -2
  19. package/dist/{profiler-CtGKTWWP.js.map → profiler-BnInDjd4.js.map} +1 -1
  20. package/dist/{profiler-l7_HjTyw.cjs → profiler-DIwReaSQ.cjs} +2 -2
  21. package/dist/{profiler-l7_HjTyw.cjs.map → profiler-DIwReaSQ.cjs.map} +1 -1
  22. package/dist/{startRecording-DEw2Aeq4.cjs → startRecording-Cg4fxzWw.cjs} +2 -2
  23. package/dist/{startRecording-DEw2Aeq4.cjs.map → startRecording-Cg4fxzWw.cjs.map} +1 -1
  24. package/dist/{startRecording-iYEL0-vr.js → startRecording-P_J6QFPD.js} +2 -2
  25. package/dist/{startRecording-iYEL0-vr.js.map → startRecording-P_J6QFPD.js.map} +1 -1
  26. package/dist/types/index.d.ts +8 -0
  27. package/package.json +4 -4
  28. package/src/components/ShadowRoot.tsx +26 -0
  29. package/src/components/assistant-ui/thinking-indicator.tsx +175 -0
  30. package/src/components/assistant-ui/thread.tsx +2 -16
  31. package/src/components/assistant-ui/tool-fallback.tsx +8 -8
  32. package/src/components/assistant-ui/tool-group.tsx +4 -13
  33. package/src/components/ui/tool-ui.tsx +50 -31
  34. package/src/contexts/ElementsProvider.tsx +31 -5
  35. package/src/global.css +55 -16
  36. package/src/hooks/useGramThreadListAdapter.tsx +29 -9
  37. package/src/types/index.ts +9 -0
  38. package/dist/index-BhIowiZF.js.map +0 -1
  39. package/dist/index-D0jIGQr7.cjs.map +0 -1
  40. package/dist/index-Dz13dSDa.js.map +0 -1
  41. package/dist/index-PXd3rs95.cjs +0 -194
  42. package/dist/index-PXd3rs95.cjs.map +0 -1
@@ -711,6 +711,8 @@ interface ToolUIGroupProps {
711
711
  status?: "running" | "complete";
712
712
  /** Whether the group starts expanded */
713
713
  defaultExpanded?: boolean;
714
+ /** Render without the group header, showing children directly. */
715
+ headerless?: boolean;
714
716
  /** Child tool UI components */
715
717
  children: React.ReactNode;
716
718
  /** Additional class names */
@@ -722,11 +724,23 @@ function ToolUIGroup({
722
724
  icon,
723
725
  status = "complete",
724
726
  defaultExpanded = false,
727
+ headerless = false,
725
728
  children,
726
729
  className,
727
730
  }: ToolUIGroupProps): React.JSX.Element {
728
731
  const [isExpanded, setIsExpanded] = useState(defaultExpanded);
729
732
 
733
+ // A headerless group shows its children unconditionally; when it gains a
734
+ // header mid-stream, start expanded — collapsing would hide content the
735
+ // user was already looking at.
736
+ const [prevHeaderless, setPrevHeaderless] = useState(headerless);
737
+ if (prevHeaderless !== headerless) {
738
+ setPrevHeaderless(headerless);
739
+ if (prevHeaderless) setIsExpanded(true);
740
+ }
741
+
742
+ const showChildren = headerless || isExpanded;
743
+
730
744
  return (
731
745
  <div
732
746
  data-slot="tool-ui-group"
@@ -736,40 +750,45 @@ function ToolUIGroup({
736
750
  )}
737
751
  >
738
752
  {/* Group header */}
739
- <button
740
- onClick={() => setIsExpanded(!isExpanded)}
741
- className="flex w-full items-center gap-2 px-4 py-3 text-left transition-colors hover:bg-accent/50"
742
- >
743
- {icon || (
744
- <StatusIndicator
745
- status={status === "running" ? "running" : "complete"}
746
- />
747
- )}
748
- <span
749
- className={cn(
750
- "flex-1 text-sm font-medium",
751
- status === "running" && "shimmer",
752
- )}
753
+ {!headerless && (
754
+ <button
755
+ onClick={() => setIsExpanded(!isExpanded)}
756
+ aria-expanded={isExpanded}
757
+ className="flex w-full items-center gap-2 px-4 py-3 text-left transition-colors hover:bg-accent/50"
753
758
  >
754
- {title}
755
- </span>
756
- <ChevronDownIcon
757
- className={cn(
758
- "size-4 text-muted-foreground transition-transform duration-200",
759
- isExpanded && "rotate-180",
759
+ {icon || (
760
+ <StatusIndicator
761
+ status={status === "running" ? "running" : "complete"}
762
+ />
760
763
  )}
761
- />
762
- </button>
763
-
764
- {/* Expandable children */}
765
- {isExpanded && (
766
- <div
767
- data-slot="tool-ui-group-content"
768
- className="border-t border-border"
769
- >
770
- {children}
771
- </div>
764
+ <span
765
+ className={cn(
766
+ "flex-1 text-sm font-medium",
767
+ status === "running" && "shimmer",
768
+ )}
769
+ >
770
+ {title}
771
+ </span>
772
+ <ChevronDownIcon
773
+ className={cn(
774
+ "size-4 text-muted-foreground transition-transform duration-200",
775
+ isExpanded && "rotate-180",
776
+ )}
777
+ />
778
+ </button>
772
779
  )}
780
+
781
+ {/* Collapsed children are hidden, not unmounted — unmounting would
782
+ reset their state (expansion, async syntax highlighting). */}
783
+ <div
784
+ data-slot="tool-ui-group-content"
785
+ className={cn(
786
+ !headerless && "border-t border-border",
787
+ !showChildren && "hidden",
788
+ )}
789
+ >
790
+ {children}
791
+ </div>
773
792
  </div>
774
793
  );
775
794
  }
@@ -190,6 +190,13 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
190
190
  // Ref to access ensureValidHeaders in async transport without stale closures
191
191
  const ensureValidHeadersRef = useRef(auth.ensureValidHeaders);
192
192
  ensureValidHeadersRef.current = auth.ensureValidHeaders;
193
+ // Stable async header resolution for the thread-list adapter: awaits the
194
+ // session fetch when auth hasn't settled yet, so the history runtime can
195
+ // mount before auth resolves.
196
+ const getValidHeaders = useCallback(
197
+ () => ensureValidHeadersRef.current(),
198
+ [],
199
+ );
193
200
  const toolApproval = useToolApproval();
194
201
 
195
202
  const [model, setModel] = useState<Model>(
@@ -581,12 +588,20 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
581
588
  };
582
589
  }, [mcpHeaders, setCurrentChatId]);
583
590
  const configTransport = config.transport;
584
- const transport = useMemo<ChatTransport<UIMessage>>(() => {
591
+ // Resolved separately from `defaultTransport` so that churn in the default
592
+ // transport's dependencies (MCP tool discovery settling, connection status,
593
+ // auth refresh) cannot change the transport identity while a custom
594
+ // transport is in use. Transport identity feeds the per-thread runtime hook
595
+ // (`useChatRuntimeHook` → `setRuntimeHook`), and an identity change there
596
+ // rebuilds the thread runtimes — wiping in-flight optimistic messages, e.g.
597
+ // a message sent right after a cold open while MCP tools are still loading.
598
+ const customTransport = useMemo<ChatTransport<UIMessage> | null>(() => {
585
599
  if (typeof configTransport === "function") {
586
600
  return configTransport({ getChatId, adoptChatId });
587
601
  }
588
- return configTransport ?? defaultTransport;
589
- }, [configTransport, defaultTransport, getChatId, adoptChatId]);
602
+ return configTransport ?? null;
603
+ }, [configTransport, getChatId, adoptChatId]);
604
+ const transport = customTransport ?? defaultTransport;
590
605
 
591
606
  const historyEnabled = config.history?.enabled ?? false;
592
607
 
@@ -629,12 +644,20 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
629
644
 
630
645
  // Render the appropriate runtime provider based on history config.
631
646
  // We use separate components to avoid conditional hook calls.
632
- if (historyEnabled && !auth.isLoading) {
647
+ //
648
+ // The history branch must NOT wait for auth: gating it on `!auth.isLoading`
649
+ // would mount the without-history runtime first and swap it for the history
650
+ // one when auth settles — replacing the runtime and wiping any message sent
651
+ // into the first one (e.g. a prompt queued before a cold open). Instead the
652
+ // history runtime mounts immediately and its adapter awaits auth via
653
+ // `getHeaders` before issuing requests.
654
+ if (historyEnabled) {
633
655
  return (
634
656
  <ElementsProviderWithHistory
635
657
  transport={transport}
636
658
  apiUrl={apiUrl}
637
- headers={auth.headers}
659
+ headers={auth.headers ?? {}}
660
+ getHeaders={getValidHeaders}
638
661
  contextValue={contextValue}
639
662
  runtimeRef={runtimeRef}
640
663
  frontendTools={frontendTools}
@@ -676,6 +699,7 @@ interface ElementsProviderWithHistoryProps {
676
699
  transport: ChatTransport<UIMessage>;
677
700
  apiUrl: string;
678
701
  headers: Record<string, string>;
702
+ getHeaders: () => Promise<Record<string, string>>;
679
703
  contextValue: React.ContextType<typeof ElementsContext>;
680
704
  runtimeRef: React.RefObject<ReturnType<typeof useChatRuntime> | null>;
681
705
  frontendTools: Record<string, AssistantTool>;
@@ -712,6 +736,7 @@ const ElementsProviderWithHistory = ({
712
736
  transport,
713
737
  apiUrl,
714
738
  headers,
739
+ getHeaders,
715
740
  contextValue,
716
741
  runtimeRef,
717
742
  frontendTools,
@@ -724,6 +749,7 @@ const ElementsProviderWithHistory = ({
724
749
  const threadListAdapter = useGramThreadListAdapter({
725
750
  apiUrl,
726
751
  headers,
752
+ getHeaders,
727
753
  localIdToUuidMap,
728
754
  threadListFilters: contextValue?.config.history?.threadListFilters,
729
755
  deferThreadIdMinting: contextValue?.config.history?.deferThreadIdMinting,
package/src/global.css CHANGED
@@ -258,14 +258,36 @@
258
258
 
259
259
  /* assistant-ui streaming indicator — rainbow gradient ring matches the
260
260
  Speakeasy brand palette used elsewhere (see `INSIGHTS_AI_RAINBOW_BORDER_CLASS`
261
- and the login page's BrandGradientBar). */
261
+ and the login page's BrandGradientBar). Shared as a variable so the inline
262
+ trailing dot and the standalone "thinking" dot stay in sync. */
263
+ .gram-elements {
264
+ --aui-rainbow: conic-gradient(
265
+ from 0deg,
266
+ #320f1e,
267
+ #c83228,
268
+ #fb873f,
269
+ #d2dc91,
270
+ #5a8250,
271
+ #002314,
272
+ #00143c,
273
+ #2873d7,
274
+ #9bc3ff,
275
+ #320f1e
276
+ );
277
+ /* Spin shorthand shared by both rainbow dots so a single reduced-motion
278
+ override (below) stops all of them at once. */
279
+ --aui-spin: aui-rainbow-spin 1.6s linear infinite;
280
+ }
281
+
262
282
  @keyframes aui-rainbow-spin {
263
283
  to {
264
284
  transform: rotate(360deg);
265
285
  }
266
286
  }
267
287
 
268
- .gram-elements :where(.aui-md[data-status="running"]):empty::after,
288
+ /* Trailing dot that follows answer text as it streams. The "before first
289
+ token" state is handled by <ThinkingIndicator> (cycling verbs), so there is
290
+ deliberately no `:empty::after` rule here. */
269
291
  .gram-elements
270
292
  :where(.aui-md[data-status="running"])
271
293
  > :where(:not(ol):not(ul):not(pre)):last-child::after,
@@ -301,19 +323,29 @@
301
323
  margin-right: 0.15rem;
302
324
  padding: 3px;
303
325
  border-radius: 50%;
304
- background: conic-gradient(
305
- from 0deg,
306
- #320f1e,
307
- #c83228,
308
- #fb873f,
309
- #d2dc91,
310
- #5a8250,
311
- #002314,
312
- #00143c,
313
- #2873d7,
314
- #9bc3ff,
315
- #320f1e
316
- );
326
+ background: var(--aui-rainbow);
327
+ -webkit-mask:
328
+ linear-gradient(#fff 0 0) content-box,
329
+ linear-gradient(#fff 0 0);
330
+ -webkit-mask-composite: xor;
331
+ mask:
332
+ linear-gradient(#fff 0 0) content-box,
333
+ linear-gradient(#fff 0 0);
334
+ mask-composite: exclude;
335
+ animation: var(--aui-spin);
336
+ }
337
+
338
+ /* Standalone rainbow dot for the cycling "thinking" indicator (shown before
339
+ the assistant streams any answer text). Same ring as the inline dot above. */
340
+ .gram-elements .aui-thinking-dot {
341
+ display: inline-block;
342
+ box-sizing: border-box;
343
+ flex-shrink: 0;
344
+ width: 0.95em;
345
+ height: 0.95em;
346
+ padding: 2.5px;
347
+ border-radius: 50%;
348
+ background: var(--aui-rainbow);
317
349
  -webkit-mask:
318
350
  linear-gradient(#fff 0 0) content-box,
319
351
  linear-gradient(#fff 0 0);
@@ -322,7 +354,14 @@
322
354
  linear-gradient(#fff 0 0) content-box,
323
355
  linear-gradient(#fff 0 0);
324
356
  mask-composite: exclude;
325
- animation: aui-rainbow-spin 1.6s linear infinite;
357
+ animation: var(--aui-spin);
358
+ }
359
+
360
+ @media (prefers-reduced-motion: reduce) {
361
+ /* Stops both the trailing streaming dot and the standalone thinking dot. */
362
+ .gram-elements {
363
+ --aui-spin: none;
364
+ }
326
365
  }
327
366
 
328
367
  /* Simple shimmer animation for title text */
@@ -72,6 +72,14 @@ export type ChatMessageTransform = (
72
72
  export interface ThreadListAdapterOptions {
73
73
  apiUrl: string;
74
74
  headers: Record<string, string>;
75
+ /**
76
+ * Async header resolution that waits for auth to settle (e.g. the session
77
+ * token fetch) before returning. When provided, every adapter request uses
78
+ * this instead of the `headers` snapshot — which lets the runtime mount
79
+ * before auth resolves: the initial `list()` fires immediately on bind and
80
+ * would otherwise go out with incomplete headers.
81
+ */
82
+ getHeaders?: () => Promise<Record<string, string>>;
75
83
  /** Map to translate local thread IDs to UUIDs (shared with transport) */
76
84
  localIdToUuidMap?: Map<string, string>;
77
85
  /**
@@ -100,6 +108,17 @@ interface ListChatsResponse {
100
108
  chats: GramChatOverview[];
101
109
  }
102
110
 
111
+ /**
112
+ * Resolves request headers from the live adapter options: the async
113
+ * `getHeaders` (which waits for auth to settle) when provided, otherwise the
114
+ * static `headers` snapshot.
115
+ */
116
+ async function resolveAdapterHeaders(
117
+ options: ThreadListAdapterOptions,
118
+ ): Promise<Record<string, string>> {
119
+ return options.getHeaders ? options.getHeaders() : options.headers;
120
+ }
121
+
103
122
  /**
104
123
  * Thread history adapter that loads messages from Gram API.
105
124
  * Note: We use `as ThreadHistoryAdapter` cast because the withFormat generic
@@ -107,7 +126,7 @@ interface ListChatsResponse {
107
126
  */
108
127
  class GramThreadHistoryAdapter {
109
128
  private apiUrl: string;
110
- private headers: Record<string, string>;
129
+ private getHeaders: () => Promise<Record<string, string>>;
111
130
  private store: AssistantApi;
112
131
  // Read lazily rather than captured: the adapter is constructed once, but the
113
132
  // consumer may swap `transformChatMessage` across renders, so resolve it from
@@ -116,12 +135,12 @@ class GramThreadHistoryAdapter {
116
135
 
117
136
  constructor(
118
137
  apiUrl: string,
119
- headers: Record<string, string>,
138
+ getHeaders: () => Promise<Record<string, string>>,
120
139
  store: AssistantApi,
121
140
  getTransformChatMessage?: () => ChatMessageTransform | undefined,
122
141
  ) {
123
142
  this.apiUrl = apiUrl;
124
- this.headers = headers;
143
+ this.getHeaders = getHeaders;
125
144
  this.store = store;
126
145
  this.getTransformChatMessage = getTransformChatMessage;
127
146
  }
@@ -155,7 +174,7 @@ class GramThreadHistoryAdapter {
155
174
  try {
156
175
  const response = await fetch(
157
176
  `${this.apiUrl}/rpc/chat.load?id=${encodeURIComponent(remoteId)}`,
158
- { headers: this.headers },
177
+ { headers: await this.getHeaders() },
159
178
  );
160
179
 
161
180
  if (!response.ok) {
@@ -189,7 +208,7 @@ class GramThreadHistoryAdapter {
189
208
 
190
209
  const response = await fetch(
191
210
  `${this.apiUrl}/rpc/chat.load?id=${encodeURIComponent(remoteId)}`,
192
- { headers: this.headers },
211
+ { headers: await this.getHeaders() },
193
212
  );
194
213
 
195
214
  if (!response.ok) {
@@ -253,7 +272,7 @@ function useGramThreadHistoryAdapter(
253
272
  () =>
254
273
  new GramThreadHistoryAdapter(
255
274
  optionsRef.current.apiUrl,
256
- optionsRef.current.headers,
275
+ () => resolveAdapterHeaders(optionsRef.current),
257
276
  store,
258
277
  () => optionsRef.current.transformChatMessage,
259
278
  ),
@@ -294,7 +313,8 @@ export function useGramThreadListAdapter(
294
313
 
295
314
  async list() {
296
315
  try {
297
- const { apiUrl, headers, threadListFilters } = optionsRef.current;
316
+ const { apiUrl, threadListFilters } = optionsRef.current;
317
+ const headers = await resolveAdapterHeaders(optionsRef.current);
298
318
  const qs = threadListFilters
299
319
  ? new URLSearchParams(threadListFilters).toString()
300
320
  : "";
@@ -416,7 +436,7 @@ export function useGramThreadListAdapter(
416
436
  {
417
437
  method: "POST",
418
438
  headers: {
419
- ...optionsRef.current.headers,
439
+ ...(await resolveAdapterHeaders(optionsRef.current)),
420
440
  "Content-Type": "application/json",
421
441
  },
422
442
  body: JSON.stringify({ id: remoteId }),
@@ -457,7 +477,7 @@ export function useGramThreadListAdapter(
457
477
  const response = await fetch(
458
478
  `${optionsRef.current.apiUrl}/rpc/chat.load?id=${encodeURIComponent(threadId)}`,
459
479
  {
460
- headers: optionsRef.current.headers,
480
+ headers: await resolveAdapterHeaders(optionsRef.current),
461
481
  },
462
482
  );
463
483
 
@@ -603,6 +603,15 @@ export interface ThemeConfig {
603
603
  * @default 'soft'
604
604
  */
605
605
  radius?: Radius;
606
+
607
+ /**
608
+ * Extra CSS injected into the Elements shadow root after the built-in
609
+ * stylesheet. Elements renders inside a shadow DOM, so host-page styles
610
+ * cannot reach its internals — this is the supported escape hatch for
611
+ * embedders that need to restyle specific components (targeting the
612
+ * stable `aui-*` class hooks).
613
+ */
614
+ customCss?: string;
606
615
  }
607
616
 
608
617
  export interface ComponentOverrides {