@opengeni/react 0.44.5 → 0.46.1

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 (35) hide show
  1. package/README.md +3 -0
  2. package/dist/{chunk-L3EWO3UK.js → chunk-3PB7MIT6.js} +243 -22
  3. package/dist/chunk-3PB7MIT6.js.map +1 -0
  4. package/dist/{chunk-EHSYZ4KP.js → chunk-FGZUCXZF.js} +2 -2
  5. package/dist/{chunk-EHSYZ4KP.js.map → chunk-FGZUCXZF.js.map} +1 -1
  6. package/dist/{chunk-LYRJUU5V.js → chunk-LGVMUIRV.js} +543 -200
  7. package/dist/chunk-LGVMUIRV.js.map +1 -0
  8. package/dist/components/user-message-body.d.ts +36 -0
  9. package/dist/composer.js +2 -2
  10. package/dist/hooks/use-voice-input.d.ts +8 -1
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +8 -4
  13. package/dist/index.js.map +1 -1
  14. package/dist/realtime.js +4 -4
  15. package/dist/realtime.js.map +1 -1
  16. package/dist/session-ui.d.ts +2 -0
  17. package/dist/session-ui.js +7 -3
  18. package/package.json +2 -2
  19. package/src/components/composer.tsx +4 -4
  20. package/src/components/copy-button.tsx +1 -1
  21. package/src/components/markdown.tsx +1 -1
  22. package/src/components/message-timeline.tsx +134 -25
  23. package/src/components/model-picker.tsx +1 -1
  24. package/src/components/model-policy-picker.tsx +1 -1
  25. package/src/components/user-message-body.tsx +341 -0
  26. package/src/hooks/use-codex-accounts.ts +1 -1
  27. package/src/hooks/use-voice-input.ts +320 -19
  28. package/src/index.ts +2 -0
  29. package/src/realtime/realtime-control.tsx +3 -3
  30. package/src/session-ui.ts +2 -0
  31. package/src/timeline/activity-rail.tsx +1 -1
  32. package/src/timeline/shared.tsx +3 -3
  33. package/src/timeline/turn-summary.tsx +4 -4
  34. package/dist/chunk-L3EWO3UK.js.map +0 -1
  35. package/dist/chunk-LYRJUU5V.js.map +0 -1
@@ -36,6 +36,11 @@ import { cn } from "../lib/cn";
36
36
  import { formatClockTime, formatRelativeTime, truncate } from "../lib/format";
37
37
  import { prefersReducedMotion } from "../lib/motion";
38
38
  import { Markdown } from "./markdown";
39
+ import {
40
+ UserMessageBody,
41
+ UserMessageDisclosureProvider,
42
+ type UserMessageDisclosureContextValue,
43
+ } from "./user-message-body";
39
44
  import {
40
45
  createTipFollowState,
41
46
  readerScrollUpPx,
@@ -358,6 +363,13 @@ export function MessageTimeline({
358
363
  * one scroll event (or fired two) — use a count, and clear to 0 on echo.
359
364
  */
360
365
  const programmaticScrollRef = useRef(0);
366
+ /**
367
+ * Disclosure height changes are not reader navigation. While an unpinned
368
+ * Show more/less state is active, its clamp/native-anchor scroll echoes must
369
+ * never geometrically re-enable bottom-follow. A later real reader navigation
370
+ * or explicit Jump to latest releases this fence.
371
+ */
372
+ const disclosureKeepsUnpinnedRef = useRef(false);
361
373
  /**
362
374
  * Unarmed scroll-away observed; waiting for scrollend (or rAF fallback).
363
375
  * Blocks layout tip-follow so a stream token cannot yank before leave settles.
@@ -369,6 +381,7 @@ export function MessageTimeline({
369
381
  // deliberate chip remounts (activity→turn wrap, nested key flips) so a fold
370
382
  // that already settled closed — or that the reader closed — never reopens.
371
383
  const foldMemoryRef = useRef<Map<string, FoldRestingState>>(new Map());
384
+ const userMessageDisclosureMemoryRef = useRef<Map<string, boolean>>(new Map());
372
385
  const seenActivityIdsRef = useRef<Set<string>>(new Set());
373
386
  const groupKeyByItemIdRef = useRef<Map<string, string>>(new Map());
374
387
  const groupOffsetByKeyRef = useRef<Map<string, number>>(new Map());
@@ -513,16 +526,20 @@ export function MessageTimeline({
513
526
  target: EventTarget | null;
514
527
  currentTarget: EventTarget | null;
515
528
  }) => {
516
- // Nested overflow (code / notice pre) or mostly-horizontal pan: not tip leave.
517
- if (event.deltaY >= 0) {
518
- return;
519
- }
529
+ // Nested overflow (code / notice pre) or mostly-horizontal pan: not
530
+ // timeline reader intent. A real timeline wheel in either direction
531
+ // releases the disclosure fence; downward movement may then re-pin
532
+ // naturally when it reaches the bottom.
520
533
  if (Math.abs(event.deltaY) <= Math.abs(event.deltaX)) {
521
534
  return;
522
535
  }
523
536
  if (wheelConsumedByNestedScrollable(event)) {
524
537
  return;
525
538
  }
539
+ disclosureKeepsUnpinnedRef.current = false;
540
+ if (event.deltaY >= 0) {
541
+ return;
542
+ }
526
543
  const node =
527
544
  event.currentTarget instanceof HTMLElement ? event.currentTarget : scrollRef.current;
528
545
  releasePinFromReader(node);
@@ -546,10 +563,21 @@ export function MessageTimeline({
546
563
  ) {
547
564
  return;
548
565
  }
566
+ disclosureKeepsUnpinnedRef.current = false;
549
567
  readerIntentArmRef.current = true;
550
568
  };
551
569
 
552
570
  const onKeyDown = (event: { key: string; currentTarget: EventTarget | null }) => {
571
+ if (
572
+ event.key === "ArrowUp" ||
573
+ event.key === "ArrowDown" ||
574
+ event.key === "PageUp" ||
575
+ event.key === "PageDown" ||
576
+ event.key === "Home" ||
577
+ event.key === "End"
578
+ ) {
579
+ disclosureKeepsUnpinnedRef.current = false;
580
+ }
553
581
  if (event.key !== "ArrowUp" && event.key !== "PageUp" && event.key !== "Home") {
554
582
  return;
555
583
  }
@@ -574,6 +602,65 @@ export function MessageTimeline({
574
602
  [cancelLeaveFallback, stopFollow, syncScrollBaseline, writeScrollTop],
575
603
  );
576
604
 
605
+ const beginUserMessageDisclosureChange = useCallback(
606
+ (messageBody: HTMLElement, disclosureControl: HTMLElement) => {
607
+ const node = scrollRef.current;
608
+ if (!node || !node.contains(messageBody)) {
609
+ return null;
610
+ }
611
+ const keepBottom = autoFollow && pinnedRef.current && !hasNewerRef.current;
612
+ if (keepBottom) {
613
+ return () => {
614
+ const current = scrollRef.current;
615
+ if (current) {
616
+ snapToBottom(current);
617
+ }
618
+ };
619
+ }
620
+
621
+ disclosureKeepsUnpinnedRef.current = true;
622
+
623
+ const scrollerRect = node.getBoundingClientRect();
624
+ const group = messageBody.closest<HTMLElement>("[data-og-timeline-group-anchor]");
625
+ const groupRect = group?.getBoundingClientRect();
626
+ // Expanding from a visible message top keeps the beginning in place.
627
+ // Collapsing after reading deep in the message keeps the disclosure
628
+ // control in place because the message top is already above the viewport.
629
+ const anchor =
630
+ group &&
631
+ groupRect &&
632
+ groupRect.top >= scrollerRect.top - 1 &&
633
+ groupRect.top < scrollerRect.bottom
634
+ ? group
635
+ : disclosureControl;
636
+ const beforeTop = anchor.getBoundingClientRect().top - scrollerRect.top;
637
+
638
+ return () => {
639
+ const current = scrollRef.current;
640
+ if (!current || !current.contains(anchor)) {
641
+ return;
642
+ }
643
+ const currentScrollerTop = current.getBoundingClientRect().top;
644
+ const afterTop = anchor.getBoundingClientRect().top - currentScrollerTop;
645
+ const delta = afterTop - beforeTop;
646
+ if (Math.abs(delta) > 0.5) {
647
+ writeScrollTop(current, current.scrollTop + delta);
648
+ }
649
+ applyPinned(false);
650
+ syncScrollBaseline(current);
651
+ };
652
+ },
653
+ [applyPinned, autoFollow, snapToBottom, syncScrollBaseline, writeScrollTop],
654
+ );
655
+
656
+ const userMessageDisclosureContext = useMemo<UserMessageDisclosureContextValue>(
657
+ () => ({
658
+ expandedByMessageId: userMessageDisclosureMemoryRef.current,
659
+ beginChange: beginUserMessageDisclosureChange,
660
+ }),
661
+ [beginUserMessageDisclosureChange],
662
+ );
663
+
577
664
  const driveFollowRef = useRef<(node: HTMLElement, now?: number) => void>(() => undefined);
578
665
  const driveFollow = useCallback(
579
666
  (node: HTMLElement, nowMs?: number) => {
@@ -868,6 +955,8 @@ export function MessageTimeline({
868
955
  groupKeyByItemIdRef.current = new Map();
869
956
  groupOffsetByKeyRef.current = new Map();
870
957
  foldMemoryRef.current.clear();
958
+ userMessageDisclosureMemoryRef.current.clear();
959
+ disclosureKeepsUnpinnedRef.current = false;
871
960
  seenActivityIdsRef.current.clear();
872
961
  applyPinned(true);
873
962
  }, [allGroups.length, revealed, applyPinned]);
@@ -1045,6 +1134,12 @@ export function MessageTimeline({
1045
1134
  if (programmatic) {
1046
1135
  programmaticScrollRef.current = 0;
1047
1136
  }
1137
+ if (disclosureKeepsUnpinnedRef.current) {
1138
+ stopFollow();
1139
+ applyPinned(false);
1140
+ syncScrollBaseline(node);
1141
+ return;
1142
+ }
1048
1143
 
1049
1144
  if (autoFollow && pinnedRef.current && !hasNewer) {
1050
1145
  // Fold / composer / SessionChrome: viewport shrink raises maxScroll without
@@ -1144,6 +1239,13 @@ export function MessageTimeline({
1144
1239
  return;
1145
1240
  }
1146
1241
  cancelLeaveFallback();
1242
+ if (disclosureKeepsUnpinnedRef.current) {
1243
+ programmaticScrollRef.current = 0;
1244
+ stopFollow();
1245
+ applyPinned(false);
1246
+ syncScrollBaseline(node);
1247
+ return;
1248
+ }
1147
1249
  if (programmaticScrollRef.current > 0) {
1148
1250
  programmaticScrollRef.current = 0;
1149
1251
  syncScrollBaseline(node);
@@ -1164,6 +1266,8 @@ export function MessageTimeline({
1164
1266
  Unpinned: native scroll anchoring holds the reader's place. */}
1165
1267
  <div
1166
1268
  ref={scrollRef}
1269
+ data-og-timeline-scroller=""
1270
+ data-og-bottom-follow={autoFollow && pinned && !hasNewer ? "true" : "false"}
1167
1271
  tabIndex={-1}
1168
1272
  onScroll={onScroll}
1169
1273
  onScrollEnd={onScrollEnd}
@@ -1224,21 +1328,23 @@ export function MessageTimeline({
1224
1328
  turnSummary,
1225
1329
  ]}
1226
1330
  >
1227
- <TimelineGroupView
1228
- group={group}
1229
- renderMessageText={renderMessageText}
1230
- onOpenSession={onOpenSession}
1231
- onMemoryClick={onMemoryClick}
1232
- onReconnect={onReconnect}
1233
- resolveProviderLogo={resolveProviderLogo}
1234
- toolRegistry={toolRegistry}
1235
- turnSummary={turnSummary}
1236
- foldLiveCluster={isAgentProgress(next)}
1237
- trailingAgentText={trailingAgentTextAfterTurn(group, next)}
1238
- contextCompactionCount={
1239
- contextCompactionCount > 0 ? contextCompactionCount : undefined
1240
- }
1241
- />
1331
+ <UserMessageDisclosureProvider value={userMessageDisclosureContext}>
1332
+ <TimelineGroupView
1333
+ group={group}
1334
+ renderMessageText={renderMessageText}
1335
+ onOpenSession={onOpenSession}
1336
+ onMemoryClick={onMemoryClick}
1337
+ onReconnect={onReconnect}
1338
+ resolveProviderLogo={resolveProviderLogo}
1339
+ toolRegistry={toolRegistry}
1340
+ turnSummary={turnSummary}
1341
+ foldLiveCluster={isAgentProgress(next)}
1342
+ trailingAgentText={trailingAgentTextAfterTurn(group, next)}
1343
+ contextCompactionCount={
1344
+ contextCompactionCount > 0 ? contextCompactionCount : undefined
1345
+ }
1346
+ />
1347
+ </UserMessageDisclosureProvider>
1242
1348
  </TimelineGroupRenderBoundary>
1243
1349
  </div>
1244
1350
  );
@@ -1354,6 +1460,7 @@ export function MessageTimeline({
1354
1460
  exit={{ opacity: 0, y: 8 }}
1355
1461
  transition={{ duration: 0.15, ease: "easeOut" }}
1356
1462
  onClick={() => {
1463
+ disclosureKeepsUnpinnedRef.current = false;
1357
1464
  if (hasNewer) {
1358
1465
  // Do not pin against the current history page — its bottom
1359
1466
  // is not the tip. The pin + snap run when the tip window
@@ -2077,12 +2184,12 @@ function CompactionRow({ item }: { item: ContextCompactionItem }) {
2077
2184
  : null;
2078
2185
  const title =
2079
2186
  item.phase === "started"
2080
- ? "Compacting conversation memory…"
2187
+ ? "Compacting conversation history…"
2081
2188
  : item.phase === "compacted"
2082
2189
  ? before && after
2083
- ? `Conversation memory compacted · ~${before} → ~${after} tokens`
2084
- : "Conversation memory compacted"
2085
- : "Couldn’t compact conversation memory";
2190
+ ? `Conversation history compacted · ~${before} → ~${after} estimated history tokens`
2191
+ : "Conversation history compacted"
2192
+ : "Couldn’t compact conversation history";
2086
2193
  const subtitle =
2087
2194
  item.phase === "compacted"
2088
2195
  ? "Chat history above is unchanged"
@@ -2140,7 +2247,7 @@ function MessageFooterTime({ occurredAt }: { occurredAt: string }) {
2140
2247
  className={cn(
2141
2248
  "shrink-0 tabular-nums text-og-xs text-og-fg-subtle",
2142
2249
  "opacity-0 transition-opacity duration-150",
2143
- "group-hover/copy:opacity-100 group-focus-within/copy:opacity-100 pointer-coarse:opacity-70",
2250
+ "group-hover/copy:opacity-100 group-focus-within/copy:opacity-100 pointer-coarse:opacity-100",
2144
2251
  )}
2145
2252
  >
2146
2253
  {formatClockTime(occurredAt)}
@@ -2171,7 +2278,9 @@ function UserMessageRow({
2171
2278
  {renderMessageText ? (
2172
2279
  renderMessageText(item.text, item)
2173
2280
  ) : (
2174
- <Markdown>{item.text}</Markdown>
2281
+ <UserMessageBody messageId={item.id} text={item.text}>
2282
+ <Markdown>{item.text}</Markdown>
2283
+ </UserMessageBody>
2175
2284
  )}
2176
2285
  </div>
2177
2286
  </CopyHoverFrame>
@@ -100,7 +100,7 @@ export function ModelPicker({
100
100
  disabled={disabled === true}
101
101
  aria-label={label}
102
102
  className={cn(
103
- "h-8 max-w-[180px] cursor-pointer appearance-none truncate rounded-og-md bg-transparent",
103
+ "h-8 max-w-[180px] cursor-pointer appearance-none truncate rounded-og-md bg-transparent pointer-coarse:h-11",
104
104
  "py-0 pl-2 pr-6 text-[13px] text-og-fg-muted",
105
105
  "transition-colors duration-150 hover:bg-og-surface-2 hover:text-og-fg",
106
106
  "focus:outline-none focus-visible:outline-none",
@@ -573,7 +573,7 @@ export function ModelPolicyPicker(props: ModelPolicyPickerProps) {
573
573
  disabled={props.disabled}
574
574
  aria-label={messages.label}
575
575
  className={cn(
576
- "og-root inline-flex h-[var(--og-model-picker-trigger-height)] min-w-0 max-w-64 items-center gap-1 rounded-full border border-transparent px-2.5 text-og-control text-og-fg-muted outline-none transition-colors hover:border-og-border hover:bg-og-surface-2 hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent/40 disabled:cursor-not-allowed disabled:opacity-50 max-sm:h-[var(--og-model-picker-trigger-height-mobile)] max-sm:max-w-[7.5rem] max-sm:px-2",
576
+ "og-root inline-flex h-[var(--og-model-picker-trigger-height)] min-w-0 max-w-64 items-center gap-1 rounded-full border border-transparent px-2.5 text-og-control text-og-fg-muted outline-none transition-colors hover:border-og-border hover:bg-og-surface-2 hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent/40 disabled:cursor-not-allowed disabled:opacity-50 max-sm:h-11 max-sm:max-w-[7.5rem] max-sm:px-2",
577
577
  props.className,
578
578
  )}
579
579
  >
@@ -0,0 +1,341 @@
1
+ import {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useId,
6
+ useLayoutEffect,
7
+ useRef,
8
+ useState,
9
+ type ReactNode,
10
+ } from "react";
11
+ import { cn } from "../lib/cn";
12
+
13
+ const FALLBACK_LINE_THRESHOLD = 12;
14
+ const FALLBACK_TEXT_THRESHOLD = 900;
15
+ const FALLBACK_UNBROKEN_THRESHOLD = 260;
16
+ const INTERACTIVE_DESCENDANT_SELECTOR = [
17
+ "a[href]",
18
+ "area[href]",
19
+ "button",
20
+ "input:not([type='hidden'])",
21
+ "select",
22
+ "textarea",
23
+ "iframe",
24
+ "object",
25
+ "embed",
26
+ "audio[controls]",
27
+ "video[controls]",
28
+ "summary",
29
+ "[contenteditable]:not([contenteditable='false'])",
30
+ "[tabindex]",
31
+ "[role='button']",
32
+ "[role='link']",
33
+ "[role='checkbox']",
34
+ "[role='radio']",
35
+ "[role='switch']",
36
+ "[role='slider']",
37
+ "[role='spinbutton']",
38
+ "[role='textbox']",
39
+ "[role='combobox']",
40
+ "[role='listbox']",
41
+ "[role='menuitem']",
42
+ "[role='option']",
43
+ "[role='tab']",
44
+ "[role='treeitem']",
45
+ ].join(",");
46
+
47
+ function composedParentElement(element: Element): Element | null {
48
+ if (element.assignedSlot) {
49
+ return element.assignedSlot;
50
+ }
51
+ if (element.parentElement) {
52
+ return element.parentElement;
53
+ }
54
+ const root = element.getRootNode();
55
+ return typeof ShadowRoot !== "undefined" && root instanceof ShadowRoot ? root.host : null;
56
+ }
57
+
58
+ function hasComposedInertAncestor(element: Element, boundary: Element): boolean {
59
+ let current: Element | null = element;
60
+ while (current) {
61
+ if (current.hasAttribute("inert")) {
62
+ return true;
63
+ }
64
+ if (current === boundary) {
65
+ return false;
66
+ }
67
+ current = composedParentElement(current);
68
+ }
69
+ return false;
70
+ }
71
+
72
+ /**
73
+ * Native inert crosses shadow boundaries, but ordinary selectors do not.
74
+ * Recursively inspect open roots control-by-control so visible shadow content
75
+ * stays interactive. An opaque custom-element host is the conservative focus
76
+ * boundary for a closed root that cannot be inspected.
77
+ */
78
+ function collectInteractionBoundaries(root: ParentNode): HTMLElement[] {
79
+ const boundaries: HTMLElement[] = [];
80
+ const scopes: ParentNode[] = [root];
81
+ for (let scopeIndex = 0; scopeIndex < scopes.length; scopeIndex += 1) {
82
+ for (const element of scopes[scopeIndex]!.querySelectorAll("*")) {
83
+ if (!(element instanceof HTMLElement)) {
84
+ continue;
85
+ }
86
+ const shadowRoot = element.shadowRoot;
87
+ const opaqueCustomElement = element.localName.includes("-") && !shadowRoot;
88
+ if (element.matches(INTERACTIVE_DESCENDANT_SELECTOR) || opaqueCustomElement) {
89
+ boundaries.push(element);
90
+ }
91
+ if (shadowRoot) {
92
+ scopes.push(shadowRoot);
93
+ }
94
+ }
95
+ }
96
+ return boundaries;
97
+ }
98
+
99
+ function isFullyInsideVisiblePreview(rect: DOMRect, clipRect: DOMRect): boolean {
100
+ return (
101
+ rect.width > 0 &&
102
+ rect.height > 0 &&
103
+ rect.top >= clipRect.top - 1 &&
104
+ rect.bottom <= clipRect.bottom + 1 &&
105
+ rect.left >= clipRect.left - 1 &&
106
+ rect.right <= clipRect.right + 1
107
+ );
108
+ }
109
+
110
+ type RestoreDisclosureAnchor = (() => void) | null;
111
+
112
+ export type UserMessageDisclosureContextValue = {
113
+ expandedByMessageId: Map<string, boolean>;
114
+ beginChange: (
115
+ messageBody: HTMLElement,
116
+ disclosureControl: HTMLElement,
117
+ ) => RestoreDisclosureAnchor;
118
+ };
119
+
120
+ const UserMessageDisclosureContext = createContext<UserMessageDisclosureContextValue | null>(null);
121
+
122
+ export function UserMessageDisclosureProvider({
123
+ value,
124
+ children,
125
+ }: {
126
+ value: UserMessageDisclosureContextValue;
127
+ children: ReactNode;
128
+ }) {
129
+ return (
130
+ <UserMessageDisclosureContext.Provider value={value}>
131
+ {children}
132
+ </UserMessageDisclosureContext.Provider>
133
+ );
134
+ }
135
+
136
+ /**
137
+ * Deterministic first-paint fallback for runtimes without layout measurement.
138
+ * Real browsers replace this estimate with the rendered-height decision in the
139
+ * first layout effect. The complete text always remains mounted either way.
140
+ */
141
+ export function userMessageLikelyNeedsDisclosure(text: string): boolean {
142
+ const lines = text.split(/\r?\n/);
143
+ return (
144
+ lines.length > FALLBACK_LINE_THRESHOLD ||
145
+ text.length > FALLBACK_TEXT_THRESHOLD ||
146
+ lines.some((line) => line.length > FALLBACK_UNBROKEN_THRESHOLD)
147
+ );
148
+ }
149
+
150
+ export type UserMessageBodyProps = {
151
+ /** Durable timeline item id. Expansion memory is keyed by this value. */
152
+ messageId: string;
153
+ /** Complete source text. Used only for the deterministic measurement fallback. */
154
+ text: string;
155
+ children: ReactNode;
156
+ className?: string | undefined;
157
+ };
158
+
159
+ /**
160
+ * Lossless disclosure boundary for already-sent user-message text.
161
+ *
162
+ * The full rendered subtree always remains in the DOM. A real browser decides
163
+ * whether disclosure is needed from rendered height (including Markdown
164
+ * structure and wrapping); the source-text heuristic is only a deterministic
165
+ * fallback for SSR/test environments without layout. Timeline-owned context
166
+ * remembers expansion per durable message id and preserves the reader's scroll
167
+ * anchor when height changes.
168
+ */
169
+ export function UserMessageBody({ messageId, text, children, className }: UserMessageBodyProps) {
170
+ const disclosure = useContext(UserMessageDisclosureContext);
171
+ const [expanded, setExpanded] = useState(
172
+ () => disclosure?.expandedByMessageId.get(messageId) ?? false,
173
+ );
174
+ const [collapsible, setCollapsible] = useState(() => userMessageLikelyNeedsDisclosure(text));
175
+ const rootRef = useRef<HTMLDivElement | null>(null);
176
+ const clipRef = useRef<HTMLDivElement | null>(null);
177
+ const contentRef = useRef<HTMLDivElement | null>(null);
178
+ const thresholdRef = useRef<HTMLSpanElement | null>(null);
179
+ const pendingRestoreRef = useRef<RestoreDisclosureAnchor>(null);
180
+ const managedInertDescendantsRef = useRef(new Set<HTMLElement>());
181
+ const contentId = `og-user-message-${useId().replace(/:/g, "")}`;
182
+ const collapsed = collapsible && !expanded;
183
+
184
+ const restoreManagedInertDescendants = useCallback(() => {
185
+ for (const descendant of managedInertDescendantsRef.current) {
186
+ if (descendant.hasAttribute("data-og-user-message-managed-inert")) {
187
+ descendant.removeAttribute("inert");
188
+ descendant.removeAttribute("data-og-user-message-managed-inert");
189
+ }
190
+ }
191
+ managedInertDescendantsRef.current.clear();
192
+ }, []);
193
+
194
+ const syncCollapsedInteractivity = useCallback(() => {
195
+ restoreManagedInertDescendants();
196
+ if (!collapsed) {
197
+ return;
198
+ }
199
+
200
+ const clip = clipRef.current;
201
+ const content = contentRef.current;
202
+ if (!clip || !content) {
203
+ return;
204
+ }
205
+
206
+ const clipRect = clip.getBoundingClientRect();
207
+ if (clipRect.height <= 0) {
208
+ return;
209
+ }
210
+
211
+ for (const descendant of collectInteractionBoundaries(content)) {
212
+ if (hasComposedInertAncestor(descendant, content)) {
213
+ continue;
214
+ }
215
+ const rect = descendant.getBoundingClientRect();
216
+ if (isFullyInsideVisiblePreview(rect, clipRect)) {
217
+ continue;
218
+ }
219
+ descendant.setAttribute("inert", "");
220
+ descendant.setAttribute("data-og-user-message-managed-inert", "");
221
+ managedInertDescendantsRef.current.add(descendant);
222
+ }
223
+ }, [collapsed, restoreManagedInertDescendants]);
224
+
225
+ const measure = useCallback(() => {
226
+ const content = contentRef.current;
227
+ const threshold = thresholdRef.current;
228
+ if (!content || !threshold) {
229
+ return;
230
+ }
231
+ const renderedHeight = Math.max(content.scrollHeight, content.getBoundingClientRect().height);
232
+ const collapseHeight = Math.max(
233
+ threshold.offsetHeight,
234
+ threshold.getBoundingClientRect().height,
235
+ );
236
+ setCollapsible(
237
+ renderedHeight > 0 && collapseHeight > 0
238
+ ? renderedHeight > collapseHeight + 1
239
+ : userMessageLikelyNeedsDisclosure(text),
240
+ );
241
+ }, [text]);
242
+
243
+ useLayoutEffect(() => {
244
+ measure();
245
+ const content = contentRef.current;
246
+ const threshold = thresholdRef.current;
247
+ const observer =
248
+ typeof ResizeObserver === "undefined"
249
+ ? null
250
+ : new ResizeObserver(() => {
251
+ measure();
252
+ syncCollapsedInteractivity();
253
+ });
254
+ if (content) {
255
+ observer?.observe(content);
256
+ }
257
+ if (threshold) {
258
+ observer?.observe(threshold);
259
+ }
260
+ const handleResize = () => {
261
+ measure();
262
+ syncCollapsedInteractivity();
263
+ };
264
+ window.addEventListener("resize", handleResize);
265
+ return () => {
266
+ observer?.disconnect();
267
+ window.removeEventListener("resize", handleResize);
268
+ };
269
+ }, [measure, syncCollapsedInteractivity]);
270
+
271
+ useLayoutEffect(() => {
272
+ syncCollapsedInteractivity();
273
+ return restoreManagedInertDescendants;
274
+ });
275
+
276
+ useLayoutEffect(() => {
277
+ const restore = pendingRestoreRef.current;
278
+ pendingRestoreRef.current = null;
279
+ restore?.();
280
+ }, [expanded]);
281
+
282
+ const toggle = (control: HTMLButtonElement) => {
283
+ const root = rootRef.current;
284
+ pendingRestoreRef.current = root && disclosure ? disclosure.beginChange(root, control) : null;
285
+ const next = !expanded;
286
+ if (!next && contentRef.current?.contains(document.activeElement)) {
287
+ control.focus({ preventScroll: true });
288
+ }
289
+ disclosure?.expandedByMessageId.set(messageId, next);
290
+ setExpanded(next);
291
+ };
292
+
293
+ return (
294
+ <div
295
+ ref={rootRef}
296
+ data-og-user-message-body=""
297
+ data-og-message-id={messageId}
298
+ data-og-expanded={expanded ? "true" : "false"}
299
+ className={cn("relative min-w-0", className)}
300
+ >
301
+ <span
302
+ ref={thresholdRef}
303
+ aria-hidden="true"
304
+ className="pointer-events-none absolute h-56 w-0 invisible sm:h-72"
305
+ />
306
+ <div
307
+ ref={clipRef}
308
+ id={contentId}
309
+ data-og-user-message-clip=""
310
+ className={cn("relative min-w-0", collapsed && "max-h-56 overflow-hidden sm:max-h-72")}
311
+ >
312
+ <div
313
+ ref={contentRef}
314
+ data-og-user-message-content=""
315
+ className="min-w-0 [overflow-wrap:anywhere]"
316
+ >
317
+ {children}
318
+ </div>
319
+ {collapsed ? (
320
+ <span
321
+ aria-hidden="true"
322
+ data-og-user-message-fade=""
323
+ className="pointer-events-none absolute inset-x-0 bottom-0 h-16 bg-gradient-to-t from-og-surface-2 via-og-surface-2/90 to-transparent"
324
+ />
325
+ ) : null}
326
+ </div>
327
+ {collapsible ? (
328
+ <button
329
+ type="button"
330
+ aria-controls={contentId}
331
+ aria-expanded={expanded}
332
+ data-og-user-message-disclosure=""
333
+ className="mt-1.5 inline-flex min-h-7 items-center rounded-og-sm px-1.5 text-og-xs font-medium text-og-fg-muted outline-none transition-colors hover:bg-og-surface-3/60 hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent/45 pointer-coarse:min-h-11"
334
+ onClick={(event) => toggle(event.currentTarget)}
335
+ >
336
+ {expanded ? "Show less" : "Show more"}
337
+ </button>
338
+ ) : null}
339
+ </div>
340
+ );
341
+ }
@@ -80,7 +80,7 @@ export type UseCodexAccountsResult = {
80
80
 
81
81
  const EMPTY_SETTINGS: CodexRotationSettings = {
82
82
  rotationEnabled: false,
83
- rotationStrategy: "most_remaining",
83
+ rotationStrategy: "sharded",
84
84
  activeCredentialId: null,
85
85
  };
86
86