@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.
- package/dist/components/ActiveChatTitle.d.ts +22 -0
- package/dist/components/ActiveChatTitle.test.d.ts +1 -0
- package/dist/components/activeChatTitle.helpers.d.ts +12 -0
- package/dist/components/ui/tool-ui.d.ts +12 -8
- package/dist/elements.cjs +1 -1
- package/dist/elements.css +1 -1
- package/dist/elements.js +25 -24
- package/dist/{index--UMkUr53.js → index-CNSYMffp.js} +24353 -24258
- package/dist/index-CNSYMffp.js.map +1 -0
- package/dist/{index-Cz9y5YHw.cjs → index-DTZOelvp.cjs} +72 -72
- package/dist/index-DTZOelvp.cjs.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/{profiler-jAEvoPXB.cjs → profiler-CZDIhdNN.cjs} +2 -2
- package/dist/{profiler-jAEvoPXB.cjs.map → profiler-CZDIhdNN.cjs.map} +1 -1
- package/dist/{profiler-BHXyuGiY.js → profiler-DiD0upYa.js} +2 -2
- package/dist/{profiler-BHXyuGiY.js.map → profiler-DiD0upYa.js.map} +1 -1
- package/dist/{startRecording-Dw4aGDrV.cjs → startRecording-B2vd2HGG.cjs} +2 -2
- package/dist/{startRecording-Dw4aGDrV.cjs.map → startRecording-B2vd2HGG.cjs.map} +1 -1
- package/dist/{startRecording-D8IbKhJo.js → startRecording-DAV031k-.js} +2 -2
- package/dist/{startRecording-D8IbKhJo.js.map → startRecording-DAV031k-.js.map} +1 -1
- package/package.json +1 -1
- package/src/components/ActiveChatTitle.test.ts +39 -0
- package/src/components/ActiveChatTitle.tsx +152 -0
- package/src/components/activeChatTitle.helpers.ts +16 -0
- package/src/components/ui/tool-ui.tsx +48 -37
- package/src/hooks/useGramThreadListAdapter.tsx +37 -2
- package/src/index.ts +1 -0
- package/dist/index--UMkUr53.js.map +0 -1
- 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
|
-
/**
|
|
122
|
-
*
|
|
123
|
-
|
|
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
|
-
|
|
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:
|
|
411
|
-
*
|
|
412
|
-
|
|
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
|
|
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[
|
|
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
|
-
}, [
|
|
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[
|
|
451
|
-
? matches[hits[
|
|
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
|
-
|
|
473
|
-
? //
|
|
474
|
-
//
|
|
475
|
-
|
|
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/
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
|
737
|
-
|
|
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
|
-
|
|
743
|
-
const
|
|
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={
|
|
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
|
-
|
|
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,
|
|
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
|
-
//
|
|
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";
|