@gram-ai/elements 1.38.0 → 1.38.2

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 (29) hide show
  1. package/dist/components/ActiveChatTitle.d.ts +22 -0
  2. package/dist/components/ActiveChatTitle.test.d.ts +1 -0
  3. package/dist/components/activeChatTitle.helpers.d.ts +12 -0
  4. package/dist/components/ui/tool-ui.d.ts +12 -8
  5. package/dist/elements.cjs +1 -1
  6. package/dist/elements.css +1 -1
  7. package/dist/elements.js +25 -24
  8. package/dist/{index--UMkUr53.js → index-CNSYMffp.js} +24353 -24258
  9. package/dist/index-CNSYMffp.js.map +1 -0
  10. package/dist/{index-Cz9y5YHw.cjs → index-DTZOelvp.cjs} +72 -72
  11. package/dist/index-DTZOelvp.cjs.map +1 -0
  12. package/dist/index.d.ts +1 -0
  13. package/dist/{profiler-jAEvoPXB.cjs → profiler-CZDIhdNN.cjs} +2 -2
  14. package/dist/{profiler-jAEvoPXB.cjs.map → profiler-CZDIhdNN.cjs.map} +1 -1
  15. package/dist/{profiler-BHXyuGiY.js → profiler-DiD0upYa.js} +2 -2
  16. package/dist/{profiler-BHXyuGiY.js.map → profiler-DiD0upYa.js.map} +1 -1
  17. package/dist/{startRecording-Dw4aGDrV.cjs → startRecording-B2vd2HGG.cjs} +2 -2
  18. package/dist/{startRecording-Dw4aGDrV.cjs.map → startRecording-B2vd2HGG.cjs.map} +1 -1
  19. package/dist/{startRecording-D8IbKhJo.js → startRecording-DAV031k-.js} +2 -2
  20. package/dist/{startRecording-D8IbKhJo.js.map → startRecording-DAV031k-.js.map} +1 -1
  21. package/package.json +1 -1
  22. package/src/components/ActiveChatTitle.test.ts +39 -0
  23. package/src/components/ActiveChatTitle.tsx +152 -0
  24. package/src/components/activeChatTitle.helpers.ts +16 -0
  25. package/src/components/ui/tool-ui.tsx +48 -37
  26. package/src/hooks/useGramThreadListAdapter.tsx +37 -2
  27. package/src/index.ts +1 -0
  28. package/dist/index--UMkUr53.js.map +0 -1
  29. package/dist/index-Cz9y5YHw.cjs.map +0 -1
@@ -94,6 +94,12 @@ interface SectionHighlight {
94
94
  /** Mark colour: "risk" (red, default) for findings, "search" (yellow) for a
95
95
  * text-search hit. */
96
96
  tone?: "risk" | "search";
97
+ /** Search tone only: index of the active query occurrence within THIS section
98
+ * (the unified thread navigator's current target). The host owns occurrence
99
+ * stepping, so this is controlled: the occurrence at this index renders bright
100
+ * and scrolls into view; null/undefined means this section holds no active
101
+ * occurrence, so all its hits render pale. */
102
+ activeOccurrence?: number | null;
97
103
  }
98
104
 
99
105
  interface ToolUIProps {
@@ -118,9 +124,10 @@ interface ToolUIProps {
118
124
  /** When set, highlight occurrences of this query (case-insensitive) in the
119
125
  * tool name — e.g. a thread search for "customer" lights up `get_customer`. */
120
126
  nameQuery?: string;
121
- /** Whether this tool holds the active thread-search match: bright highlights
122
- * (name + sections) when true, pale when false. */
123
- searchActive?: boolean;
127
+ /** Index of the active query occurrence within the tool name (the unified
128
+ * navigator's current target), or null when the active occurrence isn't in the
129
+ * name. Per-section args/output active occurrences ride their `*Highlight`. */
130
+ nameActiveOccurrence?: number | null;
124
131
  /** Additional class names */
125
132
  className?: string;
126
133
  /** MCP tool annotations */
@@ -144,9 +151,6 @@ interface ToolUISectionProps {
144
151
  language?: BundledLanguage;
145
152
  /** Flagged substrings — renders a navigable highlighted view + header icon. */
146
153
  highlight?: SectionHighlight;
147
- /** Search tone only: whether this tool holds the active thread match (bright
148
- * vs pale marks). */
149
- searchActive?: boolean;
150
154
  }
151
155
 
152
156
  /* -----------------------------------------------------------------------------
@@ -401,15 +405,15 @@ function HighlightedCode({
401
405
  matches,
402
406
  masked,
403
407
  tone = "risk",
404
- searchActive = false,
408
+ activeOccurrence = null,
405
409
  }: {
406
410
  text: string;
407
411
  matches: SectionMatch[];
408
412
  masked?: boolean;
409
413
  tone?: "risk" | "search";
410
- /** Search tone only: whether this tool holds the active thread match. Active
411
- * bright marks; inactive pale. (Risk tone steps per-section instead.) */
412
- searchActive?: boolean;
414
+ /** Search tone only: controlled active occurrence index, owned by the host's
415
+ * unified navigator. Null when this section holds no active occurrence. */
416
+ activeOccurrence?: number | null;
413
417
  }): React.JSX.Element {
414
418
  const hits = React.useMemo(
415
419
  () =>
@@ -421,7 +425,12 @@ function HighlightedCode({
421
425
  [text, matches, tone],
422
426
  );
423
427
  const count = hits.length;
428
+ const isSearch = tone === "search";
429
+ // Risk tone steps occurrences per-section with its own ▲▼; search tone is
430
+ // controlled by the host (the thread-wide navigator), so its active index comes
431
+ // in via `activeOccurrence` (-1 = this section isn't the active one).
424
432
  const [active, setActive] = useState(0);
433
+ const effectiveActive = isSearch ? (activeOccurrence ?? -1) : active;
425
434
  const [revealed, setRevealed] = useState(!masked);
426
435
  const markRefs = React.useRef<Array<HTMLElement | null>>([]);
427
436
  const preRef = React.useRef<HTMLPreElement>(null);
@@ -431,24 +440,24 @@ function HighlightedCode({
431
440
  }, [count, active]);
432
441
  // Center the active match within the code block *only* — adjust the <pre>'s
433
442
  // own scrollTop rather than scrollIntoView(), which would also yank the
434
- // surrounding sheet. Runs on mount (focus the first match) and on each step.
443
+ // surrounding sheet. Runs on mount + each step (risk) or each host nav (search).
435
444
  useEffect(() => {
436
445
  const pre = preRef.current;
437
- const mark = markRefs.current[active];
446
+ const mark = markRefs.current[effectiveActive];
438
447
  if (!pre || !mark) return;
439
448
  const markRect = mark.getBoundingClientRect();
440
449
  const preRect = pre.getBoundingClientRect();
441
450
  pre.scrollTop +=
442
451
  markRect.top - preRect.top - pre.clientHeight / 2 + markRect.height / 2;
443
- }, [active, count]);
452
+ }, [effectiveActive, count]);
444
453
 
445
454
  const go = (delta: number) => {
446
455
  if (count === 0) return;
447
456
  setActive((a) => (a + delta + count) % count);
448
457
  };
449
458
 
450
- const activeMatch = hits[active]
451
- ? matches[hits[active]!.matchIndex]
459
+ const activeMatch = hits[effectiveActive]
460
+ ? matches[hits[effectiveActive]!.matchIndex]
452
461
  : undefined;
453
462
 
454
463
  const segments: React.ReactNode[] = [];
@@ -469,12 +478,13 @@ function HighlightedCode({
469
478
  // have a visible target; the rest stay a darker shade. Risk findings are
470
479
  // red; a plain text-search hit is yellow.
471
480
  "rounded-sm px-0.5 font-mono ring-1",
472
- tone === "search"
473
- ? // Search nav is per-row, so all occurrences here share the row's
474
- // active state: bright when this tool is the active match, else pale.
475
- searchActive
481
+ isSearch
482
+ ? // The single active occurrence (the navigator's current target)
483
+ // is bright; every other hit is pale. When this section isn't the
484
+ // active one, effectiveActive is -1 so all hits render pale.
485
+ i === effectiveActive
476
486
  ? "bg-yellow-400 text-yellow-950 ring-yellow-300"
477
- : "bg-yellow-800/50 text-yellow-200/90 ring-yellow-700/50"
487
+ : "bg-yellow-800/40 text-yellow-200/70 ring-yellow-700/40"
478
488
  : i === active
479
489
  ? "bg-red-700 text-red-50 ring-red-400"
480
490
  : "bg-red-900 text-red-300 ring-red-800",
@@ -504,12 +514,12 @@ function HighlightedCode({
504
514
  {count} flagged {count === 1 ? "match" : "matches"}
505
515
  </span>
506
516
  )}
507
- {activeMatch?.label && (
517
+ {!isSearch && activeMatch?.label && (
508
518
  <span className="truncate rounded bg-slate-700/60 px-1.5 py-0.5 font-mono text-slate-300">
509
519
  {activeMatch.label}
510
520
  </span>
511
521
  )}
512
- {activeMatch?.onExclude && (
522
+ {!isSearch && activeMatch?.onExclude && (
513
523
  <button
514
524
  type="button"
515
525
  onClick={activeMatch.onExclude}
@@ -535,7 +545,9 @@ function HighlightedCode({
535
545
  {revealed ? "Hide" : "Reveal"}
536
546
  </button>
537
547
  )}
538
- {count >= 1 && (
548
+ {/* Risk tone steps occurrences per-section; search tone is driven by
549
+ the thread-wide navigator, so no per-section prev/next. */}
550
+ {!isSearch && count >= 1 && (
539
551
  <div className="flex items-center gap-0.5">
540
552
  <button
541
553
  type="button"
@@ -651,7 +663,6 @@ function ToolUISection({
651
663
  highlightSyntax = true,
652
664
  language = "json",
653
665
  highlight,
654
- searchActive = false,
655
666
  }: ToolUISectionProps): React.JSX.Element {
656
667
  const [isExpanded, setIsExpanded] = useState(defaultExpanded);
657
668
 
@@ -705,7 +716,7 @@ function ToolUISection({
705
716
  matches={highlight!.matches}
706
717
  masked={highlight?.masked}
707
718
  tone={highlight?.tone}
708
- searchActive={searchActive}
719
+ activeOccurrence={highlight?.activeOccurrence ?? null}
709
720
  />
710
721
  ) : isStructured ? (
711
722
  <StructuredResultContent content={content} />
@@ -730,22 +741,23 @@ type ApprovalMode = "one-time" | "for-session";
730
741
 
731
742
  // Highlight every case-insensitive occurrence of `query` in a short label (the
732
743
  // tool name), preserving original casing. Matches over the original string so
733
- // offsets stay aligned; escapes regex metacharacters in the user query.
744
+ // offsets stay aligned; escapes regex metacharacters in the user query. The
745
+ // occurrence at `activeIndex` (the navigator's current target) is bright; the
746
+ // rest are pale. null when the active occurrence isn't in the name.
734
747
  function highlightLabel(
735
748
  text: string,
736
- query?: string,
737
- active = false,
749
+ query: string | undefined,
750
+ activeIndex: number | null,
738
751
  ): React.ReactNode {
739
752
  const q = query?.trim();
740
753
  if (!q) return text;
741
754
  const re = new RegExp(q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
742
- // Active match bright, others pale.
743
- const markClass = active
744
- ? "rounded-sm bg-yellow-300/80 px-0.5 text-foreground"
745
- : "rounded-sm bg-yellow-200/30 px-0.5 text-foreground";
755
+ const ACTIVE = "rounded-sm bg-yellow-300/80 px-0.5 text-foreground";
756
+ const INACTIVE = "rounded-sm bg-yellow-200/30 px-0.5 text-foreground";
746
757
  const nodes: React.ReactNode[] = [];
747
758
  let pos = 0;
748
759
  let k = 0;
760
+ let occ = 0;
749
761
  for (let m = re.exec(text); m !== null; m = re.exec(text)) {
750
762
  if (m[0].length === 0) {
751
763
  re.lastIndex++;
@@ -753,11 +765,12 @@ function highlightLabel(
753
765
  }
754
766
  if (m.index > pos) nodes.push(text.slice(pos, m.index));
755
767
  nodes.push(
756
- <mark key={k++} className={markClass}>
768
+ <mark key={k++} className={occ === activeIndex ? ACTIVE : INACTIVE}>
757
769
  {m[0]}
758
770
  </mark>,
759
771
  );
760
772
  pos = m.index + m[0].length;
773
+ occ++;
761
774
  }
762
775
  if (pos === 0) return text;
763
776
  if (pos < text.length) nodes.push(text.slice(pos));
@@ -775,7 +788,7 @@ function ToolUI({
775
788
  requestHighlight,
776
789
  resultHighlight,
777
790
  nameQuery,
778
- searchActive = false,
791
+ nameActiveOccurrence = null,
779
792
  className,
780
793
  annotations,
781
794
  onApproveOnce,
@@ -861,7 +874,7 @@ function ToolUI({
861
874
  !provider && isApprovalPending && "shimmer",
862
875
  )}
863
876
  >
864
- {highlightLabel(displayName, nameQuery, searchActive)}
877
+ {highlightLabel(displayName, nameQuery, nameActiveOccurrence)}
865
878
  </span>
866
879
  {hasContent && (
867
880
  <ChevronDownIcon
@@ -884,7 +897,6 @@ function ToolUI({
884
897
  highlightSyntax
885
898
  language="json"
886
899
  highlight={requestHighlight}
887
- searchActive={searchActive}
888
900
  defaultExpanded={(requestHighlight?.matches?.length ?? 0) > 0}
889
901
  />
890
902
  )}
@@ -896,7 +908,6 @@ function ToolUI({
896
908
  highlightSyntax
897
909
  language="json"
898
910
  highlight={resultHighlight}
899
- searchActive={searchActive}
900
911
  defaultExpanded={(resultHighlight?.matches?.length ?? 0) > 0}
901
912
  />
902
913
  )}
@@ -15,6 +15,7 @@ import {
15
15
  convertGramMessagesToUIMessages,
16
16
  } from "@/lib/messageConverter";
17
17
  import { sleep } from "@/lib/utils";
18
+ import { trackError } from "@/lib/errorTracking";
18
19
  import {
19
20
  ThreadMetaContext,
20
21
  type ThreadMeta,
@@ -473,8 +474,42 @@ export function useGramThreadListAdapter(
473
474
  };
474
475
  },
475
476
 
476
- async rename() {
477
- // No-op
477
+ async rename(remoteId: string, newTitle: string) {
478
+ // Brand-new threads only have a local id until the first message
479
+ // persists them server-side; there's nothing to rename yet.
480
+ if (!remoteId || isLocalThreadId(remoteId)) {
481
+ return;
482
+ }
483
+
484
+ // Reuses chat.generateTitle: passing `title` sets a manual title that
485
+ // auto-generation won't overwrite (empty string resets to auto-naming).
486
+ //
487
+ // On failure we track AND rethrow: assistant-ui applies the rename
488
+ // optimistically and only rolls the title back if this promise rejects.
489
+ // Swallowing the error would leave a title showing that never persisted.
490
+ try {
491
+ const response = await fetch(
492
+ `${optionsRef.current.apiUrl}/rpc/chat.generateTitle`,
493
+ {
494
+ method: "POST",
495
+ headers: {
496
+ ...(await resolveAdapterHeaders(optionsRef.current)),
497
+ "Content-Type": "application/json",
498
+ },
499
+ body: JSON.stringify({ id: remoteId, title: newTitle }),
500
+ },
501
+ );
502
+
503
+ if (!response.ok) {
504
+ throw new Error(`Failed to rename chat: ${response.status}`);
505
+ }
506
+ } catch (error) {
507
+ trackError(error, {
508
+ source: "custom",
509
+ context: "useGramThreadListAdapter.rename",
510
+ });
511
+ throw error;
512
+ }
478
513
  },
479
514
 
480
515
  async archive() {
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export { useChatId } from "./contexts/ChatIdContext";
15
15
  // Core Components
16
16
  export { Chat } from "@/components/Chat";
17
17
  export { ChatHistory } from "@/components/ChatHistory";
18
+ export { ActiveChatTitle } from "@/components/ActiveChatTitle";
18
19
  export { ShareButton } from "@/components/ShareButton";
19
20
  export type { ShareButtonProps } from "@/components/ShareButton";
20
21
  export { ToolFallback } from "@/components/assistant-ui/tool-fallback";