@opengeni/react 0.15.0 → 0.23.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 (103) hide show
  1. package/README.md +110 -10
  2. package/dist/{chunk-TOJR776I.js → chunk-6XUS5VFM.js} +45 -369
  3. package/dist/chunk-6XUS5VFM.js.map +1 -0
  4. package/dist/chunk-CATATY77.js +2393 -0
  5. package/dist/chunk-CATATY77.js.map +1 -0
  6. package/dist/chunk-I3BJZIG5.js +439 -0
  7. package/dist/chunk-I3BJZIG5.js.map +1 -0
  8. package/dist/chunk-TK7G6XLT.js +18 -0
  9. package/dist/chunk-TK7G6XLT.js.map +1 -0
  10. package/dist/chunk-UAHPKLB6.js +2237 -0
  11. package/dist/chunk-UAHPKLB6.js.map +1 -0
  12. package/dist/chunk-UQS7OQDE.js +591 -0
  13. package/dist/chunk-UQS7OQDE.js.map +1 -0
  14. package/dist/chunk-YDNWMKXO.js +102 -0
  15. package/dist/chunk-YDNWMKXO.js.map +1 -0
  16. package/dist/composer-CVm5uoUI.d.ts +585 -0
  17. package/dist/composer.d.ts +6 -0
  18. package/dist/composer.js +56 -0
  19. package/dist/composer.js.map +1 -0
  20. package/dist/index.d.ts +1288 -1785
  21. package/dist/index.js +7140 -8817
  22. package/dist/index.js.map +1 -1
  23. package/dist/machines.d.ts +412 -3
  24. package/dist/machines.js +3 -1
  25. package/dist/noto-sans-arabic-loader-IA2T4X2A.js +21 -0
  26. package/dist/noto-sans-arabic-loader-IA2T4X2A.js.map +1 -0
  27. package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js +292 -0
  28. package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js.map +1 -0
  29. package/dist/queue-surface-implementation-3UW3MIIR.js +890 -0
  30. package/dist/queue-surface-implementation-3UW3MIIR.js.map +1 -0
  31. package/dist/session-Db00TRwl.d.ts +446 -0
  32. package/dist/session-context-rgrfElMl.d.ts +58 -0
  33. package/dist/session.d.ts +4 -0
  34. package/dist/session.js +54 -0
  35. package/dist/session.js.map +1 -0
  36. package/dist/use-composer-By1wn3MM.d.ts +99 -0
  37. package/package.json +20 -4
  38. package/src/approvals.ts +5 -3
  39. package/src/client.ts +44 -0
  40. package/src/components/approval-surface.tsx +198 -0
  41. package/src/components/chat-composer.tsx +94 -993
  42. package/src/components/code-editor.tsx +104 -26
  43. package/src/components/command-palette.tsx +8 -2
  44. package/src/components/composer-transcription-control.tsx +211 -0
  45. package/src/components/composer.tsx +1536 -0
  46. package/src/components/desktop-viewer.tsx +3 -3
  47. package/src/components/enrollment-consent.tsx +2 -2
  48. package/src/components/file-browser.tsx +367 -29
  49. package/src/components/human-input-form.tsx +417 -0
  50. package/src/components/machine-card.tsx +1 -1
  51. package/src/components/machines-dashboard.tsx +3 -3
  52. package/src/components/message-timeline.tsx +205 -25
  53. package/src/components/model-picker.tsx +12 -3
  54. package/src/components/pierre-diff.tsx +32 -6
  55. package/src/components/queue-draft-policy.ts +18 -0
  56. package/src/components/queue-surface-implementation.tsx +1037 -0
  57. package/src/components/queue-surface-state.tsx +94 -0
  58. package/src/components/queue-surface.tsx +42 -559
  59. package/src/components/sandbox-files.tsx +138 -14
  60. package/src/components/sandbox-terminal.tsx +8 -2
  61. package/src/components/sandbox-workspace.tsx +339 -119
  62. package/src/components/workbench-changes.tsx +84 -33
  63. package/src/components/workspace-dock.tsx +188 -18
  64. package/src/composer.ts +60 -0
  65. package/src/hooks/internal.ts +115 -34
  66. package/src/hooks/use-composer.ts +385 -104
  67. package/src/hooks/use-human-input.ts +131 -0
  68. package/src/hooks/use-machine-chip.ts +2 -2
  69. package/src/hooks/use-machines.ts +27 -11
  70. package/src/hooks/use-sandbox-files.ts +310 -62
  71. package/src/hooks/use-sandbox-git.ts +154 -40
  72. package/src/hooks/use-sandbox-terminal.ts +28 -6
  73. package/src/hooks/use-session-capabilities.ts +38 -9
  74. package/src/hooks/use-session-control.ts +37 -8
  75. package/src/hooks/use-session-events.ts +522 -48
  76. package/src/hooks/use-session-mcp-approval-policy.ts +161 -0
  77. package/src/hooks/use-slash-commands.ts +38 -38
  78. package/src/hooks/use-transcription.ts +757 -0
  79. package/src/hooks/use-turn-queue.ts +134 -66
  80. package/src/hooks/use-windowed-sections.ts +2 -2
  81. package/src/hooks/use-workspace-capture.ts +146 -40
  82. package/src/hooks/use-workspace-edit.ts +50 -14
  83. package/src/hooks/use-workspace-sessions.ts +3 -0
  84. package/src/human-input.ts +72 -0
  85. package/src/index.ts +85 -6
  86. package/src/lib/noto-sans-arabic-loader.ts +21 -0
  87. package/src/lib/noto-sans-jp-loader.generated.ts +295 -0
  88. package/src/lib/use-portal-token-style.ts +51 -0
  89. package/src/lib/use-unicode-fonts.ts +57 -0
  90. package/src/provider.tsx +4 -69
  91. package/src/session-context.ts +160 -0
  92. package/src/session.ts +89 -0
  93. package/src/timeline/parsers.ts +35 -0
  94. package/src/timeline/projection.ts +12 -4
  95. package/src/timeline/tool-renderers.tsx +41 -0
  96. package/src/timeline/turn-summary.tsx +2 -2
  97. package/src/timeline/types.ts +10 -9
  98. package/styles/index.css +56 -0
  99. package/styles/index.d.ts +2 -0
  100. package/styles/tokens.css +11 -6
  101. package/styles/tokens.d.ts +2 -0
  102. package/dist/chunk-TOJR776I.js.map +0 -1
  103. package/dist/machines-BpdwuQcD.d.ts +0 -449
@@ -20,6 +20,7 @@ import type { ComponentType } from "react";
20
20
  import { AnimatePresence, motion } from "motion/react";
21
21
  import { Collapsible } from "radix-ui";
22
22
  import {
23
+ memo,
23
24
  useCallback,
24
25
  useEffect,
25
26
  useLayoutEffect,
@@ -108,6 +109,8 @@ export type MessageTimelineProps = {
108
109
  className?: string | undefined;
109
110
  };
110
111
 
112
+ const INITIAL_MOUNTED_GROUPS = 1;
113
+
111
114
  /**
112
115
  * The session timeline: chat messages with streaming deltas, collapsed
113
116
  * activity clusters (reasoning, tool calls, sandbox work), spawned-worker
@@ -132,8 +135,10 @@ export function MessageTimeline({
132
135
  className,
133
136
  }: MessageTimelineProps) {
134
137
  const resolvedItems = useMemo(() => items ?? buildTimeline(events ?? []), [items, events]);
135
- const groups = useMemo(() => groupTimeline(resolvedItems), [resolvedItems]);
136
- const firstGroupKey = groups[0] ? timelineGroupKey(groups[0]) : null;
138
+ const allGroups = useMemo(() => groupTimeline(resolvedItems), [resolvedItems]);
139
+ const keyedGroups = useStableTimelineGroupKeys(allGroups);
140
+ const { mountedGroups: groups, mountingOlderGroups } = useProgressivelyMountedGroups(keyedGroups);
141
+ const firstGroupKey = allGroups[0] ? timelineGroupKey(allGroups[0]) : null;
137
142
 
138
143
  const scrollRef = useRef<HTMLDivElement | null>(null);
139
144
  const topSentinelRef = useRef<HTMLDivElement | null>(null);
@@ -178,7 +183,8 @@ export function MessageTimeline({
178
183
  const firstKeyChangedForBulk =
179
184
  previousBulkFirstKeyRef.current !== undefined &&
180
185
  previousBulkFirstKeyRef.current !== firstGroupKey;
181
- const bulkRender = groups.length > 0 && (bulkActive || firstKeyChangedForBulk);
186
+ const bulkRender =
187
+ allGroups.length > 0 && (bulkActive || firstKeyChangedForBulk || mountingOlderGroups);
182
188
 
183
189
  // Snapshot the topmost visible element and where it sits in the viewport, so a
184
190
  // later reflow can restore it to the same spot. Transient chrome (the backfill
@@ -221,10 +227,10 @@ export function MessageTimeline({
221
227
  // A cleared timeline (stream identity change) re-arms the reveal so the next
222
228
  // session also first paints at its bottom.
223
229
  useLayoutEffect(() => {
224
- if (groups.length === 0 && revealed) {
230
+ if (allGroups.length === 0 && revealed) {
225
231
  setRevealed(false);
226
232
  }
227
- }, [groups.length, revealed]);
233
+ }, [allGroups.length, revealed]);
228
234
 
229
235
  // Clear the bulk-paint marker a frame after it renders, so rows appended
230
236
  // live (streams, new turns) animate exactly as before.
@@ -234,9 +240,15 @@ export function MessageTimeline({
234
240
  return;
235
241
  }
236
242
  setBulkActive(true);
243
+ // Initial tails and prepended history mount from newest to oldest across
244
+ // frames. Keep every row born during that bulk window animation-free; only
245
+ // clear the marker after the authoritative group list is fully mounted.
246
+ if (mountingOlderGroups) {
247
+ return;
248
+ }
237
249
  const frame = requestFrame(() => setBulkActive(false));
238
250
  return () => cancelFrame(frame);
239
- }, [bulkRender, firstGroupKey]);
251
+ }, [bulkRender, firstGroupKey, mountingOlderGroups]);
240
252
 
241
253
  // Prefetch older history well before the reader reaches the top: the
242
254
  // sentinel sits above the first group and trips 1600px early, so backfill
@@ -248,6 +260,7 @@ export function MessageTimeline({
248
260
  if (
249
261
  !root ||
250
262
  !target ||
263
+ mountingOlderGroups ||
251
264
  !hasOlder ||
252
265
  loadingOlder ||
253
266
  !onLoadOlder ||
@@ -265,7 +278,7 @@ export function MessageTimeline({
265
278
  );
266
279
  observer.observe(target);
267
280
  return () => observer.disconnect();
268
- }, [hasOlder, loadingOlder, onLoadOlder, firstGroupKey]);
281
+ }, [hasOlder, loadingOlder, onLoadOlder, firstGroupKey, mountingOlderGroups]);
269
282
 
270
283
  // Scroll anchoring: when the content reflows (a fold expands/collapses, a
271
284
  // stream appends), keep following the bottom if pinned; otherwise pin the
@@ -338,7 +351,7 @@ export function MessageTimeline({
338
351
  <p className="py-10 text-center text-sm text-og-fg-subtle">No activity yet.</p>
339
352
  ))
340
353
  : null}
341
- {hasOlder ? (
354
+ {hasOlder && !mountingOlderGroups ? (
342
355
  <div
343
356
  ref={topSentinelRef}
344
357
  data-og-top-sentinel=""
@@ -352,9 +365,9 @@ export function MessageTimeline({
352
365
  <span className="og-shimmer-text font-medium">Loading earlier activity…</span>
353
366
  </div>
354
367
  ) : null}
355
- {groups.map((group, index) => (
368
+ {groups.map(({ group, key }, index) => (
356
369
  <TimelineGroupView
357
- key={timelineGroupKey(group)}
370
+ key={key}
358
371
  group={group}
359
372
  renderMessageText={renderMessageText}
360
373
  onOpenSession={onOpenSession}
@@ -362,7 +375,7 @@ export function MessageTimeline({
362
375
  onReconnect={onReconnect}
363
376
  resolveProviderLogo={resolveProviderLogo}
364
377
  toolRegistry={toolRegistry}
365
- foldLiveCluster={isAgentProgress(groups[index + 1])}
378
+ foldLiveCluster={isAgentProgress(groups[index + 1]?.group)}
366
379
  />
367
380
  ))}
368
381
  {working ? (
@@ -406,6 +419,141 @@ export function MessageTimeline({
406
419
  );
407
420
  }
408
421
 
422
+ type MountedGroupWindow = {
423
+ groupKeys: string[];
424
+ visibleStart: number;
425
+ };
426
+
427
+ type KeyedTimelineGroup = {
428
+ group: TimelineGroup;
429
+ key: string;
430
+ };
431
+
432
+ /**
433
+ * Projection can legitimately change a group's content-derived key while
434
+ * retaining its existing rows. The common pagination case is an older activity
435
+ * item merging into the first activity group; live appends grow the same group
436
+ * from the other side. Match the new authoritative groups to the previous
437
+ * committed groups by their durable item IDs so both the React key and the
438
+ * progressive-window anchor survive either change.
439
+ */
440
+ function useStableTimelineGroupKeys(allGroups: TimelineGroup[]): KeyedTimelineGroup[] {
441
+ const previousRef = useRef<KeyedTimelineGroup[]>([]);
442
+ const keyedGroups = useMemo(() => {
443
+ const previousByItemId = new Map<string, KeyedTimelineGroup>();
444
+ for (const previous of previousRef.current) {
445
+ for (const itemId of timelineGroupItemIds(previous.group)) {
446
+ previousByItemId.set(itemId, previous);
447
+ }
448
+ }
449
+
450
+ const usedKeys = new Set<string>();
451
+ return allGroups.map((group, index) => {
452
+ const itemIds = timelineGroupItemIds(group);
453
+ let retainedKey: string | undefined;
454
+ for (const itemId of itemIds) {
455
+ const previous = previousByItemId.get(itemId);
456
+ if (previous && !usedKeys.has(previous.key)) {
457
+ retainedKey = previous.key;
458
+ break;
459
+ }
460
+ }
461
+
462
+ const canonicalKey = timelineGroupKey(group);
463
+ let key = retainedKey ?? canonicalKey;
464
+ let collision = 0;
465
+ while (usedKeys.has(key)) {
466
+ key = `${canonicalKey}:${index}:${collision}`;
467
+ collision += 1;
468
+ }
469
+ usedKeys.add(key);
470
+ return { group, key };
471
+ });
472
+ }, [allGroups]);
473
+
474
+ useLayoutEffect(() => {
475
+ previousRef.current = keyedGroups;
476
+ }, [keyedGroups]);
477
+
478
+ return keyedGroups;
479
+ }
480
+
481
+ /**
482
+ * Bulk history is already projected into its authoritative order before this
483
+ * hook runs. Mount its newest group first, then prepend one older group per
484
+ * animation frame so low-end browsers can paint and service input between
485
+ * React/Markdown commits instead of doing the entire tail in one long task.
486
+ *
487
+ * A live append does not move the first mounted key, so it is included
488
+ * immediately in the mounted suffix. A history prepend moves that key deeper
489
+ * in the array; shifting `visibleStart` by that exact prefix keeps every
490
+ * existing row mounted while the new prefix is revealed. If projection
491
+ * coalesces the seam item itself away, the earliest surviving mounted key
492
+ * provides the same anchor. Replacements with no shared mounted key (for
493
+ * example a session switch or clear-view) start a fresh suffix.
494
+ */
495
+ function useProgressivelyMountedGroups(allGroups: KeyedTimelineGroup[]): {
496
+ mountedGroups: KeyedTimelineGroup[];
497
+ mountingOlderGroups: boolean;
498
+ } {
499
+ const previousGroupKeysRef = useRef<string[]>([]);
500
+ const groupKeys = useMemo(() => {
501
+ const nextKeys = allGroups.map((group) => group.key);
502
+ const previousKeys = previousGroupKeysRef.current;
503
+ return nextKeys.length === previousKeys.length &&
504
+ nextKeys.every((key, index) => key === previousKeys[index])
505
+ ? previousKeys
506
+ : nextKeys;
507
+ }, [allGroups]);
508
+ useLayoutEffect(() => {
509
+ previousGroupKeysRef.current = groupKeys;
510
+ }, [groupKeys]);
511
+ const [window, setWindow] = useState<MountedGroupWindow>(() => ({
512
+ groupKeys,
513
+ visibleStart: Math.max(0, allGroups.length - INITIAL_MOUNTED_GROUPS),
514
+ }));
515
+
516
+ const lastPossibleStart = Math.max(0, allGroups.length - INITIAL_MOUNTED_GROUPS);
517
+ let visibleStart = 0;
518
+ if (allGroups.length > 0) {
519
+ const currentIndexByKey = new Map(groupKeys.map((key, index) => [key, index]));
520
+ let retainedStart: number | undefined;
521
+ for (const key of window.groupKeys.slice(window.visibleStart)) {
522
+ const index = currentIndexByKey.get(key);
523
+ if (index !== undefined) {
524
+ retainedStart = index;
525
+ break;
526
+ }
527
+ }
528
+ visibleStart =
529
+ retainedStart === undefined ? lastPossibleStart : Math.min(retainedStart, lastPossibleStart);
530
+ }
531
+
532
+ useEffect(() => {
533
+ // Synchronize a prepend/replacement before scheduling its first reveal.
534
+ if (window.groupKeys !== groupKeys || window.visibleStart !== visibleStart) {
535
+ setWindow({ groupKeys, visibleStart });
536
+ return;
537
+ }
538
+ if (visibleStart === 0) {
539
+ return;
540
+ }
541
+ const frame = requestFrame(() => {
542
+ setWindow((current) =>
543
+ current.groupKeys === groupKeys && current.visibleStart > 0
544
+ ? { ...current, visibleStart: current.visibleStart - 1 }
545
+ : current,
546
+ );
547
+ });
548
+ return () => cancelFrame(frame);
549
+ }, [groupKeys, visibleStart, window.groupKeys, window.visibleStart]);
550
+
551
+ return {
552
+ mountedGroups: allGroups.slice(visibleStart),
553
+ mountingOlderGroups: visibleStart > 0,
554
+ };
555
+ }
556
+
409
557
  function requestFrame(callback: FrameRequestCallback): number {
410
558
  if (typeof requestAnimationFrame === "function") {
411
559
  return requestAnimationFrame(callback);
@@ -421,7 +569,11 @@ function cancelFrame(id: number): void {
421
569
  window.clearTimeout(id);
422
570
  }
423
571
 
424
- function TimelineGroupView({
572
+ // Progressive history mounting reuses the exact group objects from `allGroups`.
573
+ // Skip repainting those stable rows on every prefix reveal; live projection
574
+ // creates a new group object, and behavior/callback changes are separate props,
575
+ // so ordinary streaming and host updates still invalidate immediately.
576
+ const TimelineGroupView = memo(function TimelineGroupView({
425
577
  group,
426
578
  renderMessageText,
427
579
  onOpenSession,
@@ -531,7 +683,7 @@ function TimelineGroupView({
531
683
  />
532
684
  );
533
685
  }
534
- }
686
+ });
535
687
 
536
688
  function timelineGroupKey(group: TimelineGroup): string {
537
689
  switch (group.kind) {
@@ -544,6 +696,17 @@ function timelineGroupKey(group: TimelineGroup): string {
544
696
  }
545
697
  }
546
698
 
699
+ function timelineGroupItemIds(group: TimelineGroup): string[] {
700
+ switch (group.kind) {
701
+ case "item":
702
+ return [group.item.id];
703
+ case "activity":
704
+ return group.items.map((item) => item.id);
705
+ case "turn":
706
+ return group.groups.flatMap(timelineGroupItemIds);
707
+ }
708
+ }
709
+
547
710
  /** The agent has moved PAST a cluster only when what follows is more agent
548
711
  progress — new activity, a settled turn, or narration. A waiting notice
549
712
  (approval pause), a pending queued message, a goal pill, or nothing at all
@@ -998,12 +1161,12 @@ function NoticeRow({ item }: { item: NoticeItem }) {
998
1161
  }
999
1162
 
1000
1163
  /**
1001
- * The inline reconnect card: a lapsed connection surfaces as a calm, tappable
1002
- * affordance provider logo, one human line, one primary Reconnect button —
1003
- * instead of a raw "linear.app needs to be reconnected." string. The `reason`
1004
- * only shapes the helper copy; no domain or enum code is shown as a label.
1005
- * `onReconnect` (from the app, which owns the SDK client) runs the flow; without
1006
- * it, a pre-minted authorization link is offered, or the card stays informative.
1164
+ * The inline connection-recovery card: missing or lapsed access surfaces as a
1165
+ * calm, tappable affordance instead of a raw provider-domain error. The `reason`
1166
+ * only shapes human copy; no domain or enum code is shown as a label.
1167
+ * `onReconnect` (from the app, which owns the SDK client) starts the flow;
1168
+ * without it, a pre-minted authorization link is offered, or the card stays
1169
+ * informative. Recovery never claims to resume/replay the failed tool call.
1007
1170
  */
1008
1171
  function AuthNeededRow({
1009
1172
  item,
@@ -1018,6 +1181,10 @@ function AuthNeededRow({
1018
1181
  const [busy, setBusy] = useState(false);
1019
1182
  const [failed, setFailed] = useState(false);
1020
1183
  const provider = providerLabel(item.providerDomain);
1184
+ const unavailable =
1185
+ item.reason === "unsupported_auth" || item.reason === "resource_scope_unavailable";
1186
+ const missing = item.reason === "missing_connection";
1187
+ const actionLabel = missing ? "Connect" : "Reconnect";
1021
1188
 
1022
1189
  const start = async () => {
1023
1190
  if (!onReconnect || busy) {
@@ -1028,6 +1195,7 @@ function AuthNeededRow({
1028
1195
  try {
1029
1196
  // On success the app redirects to consent (or routes to credential entry),
1030
1197
  // so this row unmounts; a resolve without navigation just relaxes the button.
1198
+ // The callback starts authorization only. It never resumes this tool call.
1031
1199
  await onReconnect(item);
1032
1200
  setBusy(false);
1033
1201
  } catch {
@@ -1045,11 +1213,13 @@ function AuthNeededRow({
1045
1213
  label={provider}
1046
1214
  />
1047
1215
  <div className="min-w-0">
1048
- <p className="truncate text-og-md font-medium text-og-fg">Reconnect {provider}</p>
1216
+ <p className="truncate text-og-md font-medium text-og-fg">
1217
+ {unavailable ? `${provider} tools unavailable` : `${actionLabel} ${provider}`}
1218
+ </p>
1049
1219
  <p className="truncate text-og-sm text-og-fg-subtle">{authReasonLine(item.reason)}</p>
1050
1220
  </div>
1051
1221
  </div>
1052
- {onReconnect ? (
1222
+ {!unavailable && onReconnect ? (
1053
1223
  <button
1054
1224
  type="button"
1055
1225
  onClick={() => void start()}
@@ -1060,9 +1230,9 @@ function AuthNeededRow({
1060
1230
  )}
1061
1231
  >
1062
1232
  <RefreshCwIcon className={cn("size-3.5", busy && "animate-og-spin")} aria-hidden />
1063
- {busy ? "Reconnecting…" : "Reconnect"}
1233
+ {busy ? "Opening…" : actionLabel}
1064
1234
  </button>
1065
- ) : item.authorizationUrl ? (
1235
+ ) : !unavailable && item.authorizationUrl ? (
1066
1236
  <a
1067
1237
  href={item.authorizationUrl}
1068
1238
  rel="noreferrer"
@@ -1073,13 +1243,19 @@ function AuthNeededRow({
1073
1243
  )}
1074
1244
  >
1075
1245
  <RefreshCwIcon className="size-3.5" aria-hidden />
1076
- Reconnect
1246
+ {actionLabel}
1077
1247
  </a>
1078
1248
  ) : null}
1079
1249
  </div>
1250
+ {!unavailable ? (
1251
+ <p className="px-1 text-og-xs text-og-fg-subtle">
1252
+ This tool call wasn't replayed. After {missing ? "connecting" : "reconnecting"}, send a
1253
+ new message to try again.
1254
+ </p>
1255
+ ) : null}
1080
1256
  {failed ? (
1081
1257
  <p className="px-1 text-og-xs text-og-status-failed">
1082
- Couldn't start reconnecting {provider}. Try again.
1258
+ Couldn't start {missing ? "connecting" : "reconnecting"} {provider}. Try again.
1083
1259
  </p>
1084
1260
  ) : null}
1085
1261
  </div>
@@ -1160,6 +1336,10 @@ function authReasonLine(reason: AuthNeededItem["reason"]): string {
1160
1336
  case "expired":
1161
1337
  case "refresh_failed":
1162
1338
  return "Its access expired.";
1339
+ case "unsupported_auth":
1340
+ return "This connection cannot authenticate the configured tool endpoint.";
1341
+ case "resource_scope_unavailable":
1342
+ return "This tool endpoint cannot enforce the selected repository access.";
1163
1343
  default:
1164
1344
  return "Its connection needs attention.";
1165
1345
  }
@@ -12,6 +12,8 @@ export type ModelPickerProps = {
12
12
  onChange: (modelId: string) => void;
13
13
  disabled?: boolean | undefined;
14
14
  className?: string | undefined;
15
+ /** Visible-to-assistive-technology label. Defaults to "Model". */
16
+ label?: string | undefined;
15
17
  };
16
18
 
17
19
  /**
@@ -25,7 +27,14 @@ export type ModelPickerProps = {
25
27
  * the send path. Renders nothing when no models are exposed, so a single-model
26
28
  * deployment shows no chrome.
27
29
  */
28
- export function ModelPicker({ models, value, onChange, disabled, className }: ModelPickerProps) {
30
+ export function ModelPicker({
31
+ models,
32
+ value,
33
+ onChange,
34
+ disabled,
35
+ className,
36
+ label = "Model",
37
+ }: ModelPickerProps) {
29
38
  const selectId = useId();
30
39
  // Group by provider, preserving first-seen order for both the providers and
31
40
  // the models within each — the server already orders the list (default model
@@ -50,14 +59,14 @@ export function ModelPicker({ models, value, onChange, disabled, className }: Mo
50
59
  return (
51
60
  <span className={cn("relative inline-flex items-center", className)}>
52
61
  <label htmlFor={selectId} className="sr-only">
53
- Model
62
+ {label}
54
63
  </label>
55
64
  <select
56
65
  id={selectId}
57
66
  value={value ?? ""}
58
67
  onChange={(event) => onChange(event.target.value)}
59
68
  disabled={disabled === true}
60
- aria-label="Model"
69
+ aria-label={label}
61
70
  className={cn(
62
71
  // Sized like the other footer controls; the chevron overlay needs the
63
72
  // right padding so the value never collides with it.
@@ -78,6 +78,20 @@ export function PierreDiff({
78
78
  className,
79
79
  }: PierreDiffProps) {
80
80
  const [failed, setFailed] = useState(false);
81
+ const [forcedColors, setForcedColors] = useState(false);
82
+
83
+ // Pierre's rich renderer distinguishes additions/deletions primarily through
84
+ // foreground and background colors inside a shadow root. In Windows forced-
85
+ // colors mode those paints intentionally collapse, so expose the literal
86
+ // unified patch (+/- markers) instead of leaving changed lines ambiguous.
87
+ useEffect(() => {
88
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
89
+ const query = window.matchMedia("(forced-colors: active)");
90
+ const update = () => setForcedColors(query.matches);
91
+ update();
92
+ query.addEventListener?.("change", update);
93
+ return () => query.removeEventListener?.("change", update);
94
+ }, []);
81
95
 
82
96
  // Probe the import once so a hard failure (peer missing) shows `fallback`
83
97
  // rather than a Suspense boundary that never resolves. Skipped when `plain`.
@@ -92,11 +106,18 @@ export function PierreDiff({
92
106
  };
93
107
  }, [plain]);
94
108
 
95
- if (plain || failed) {
109
+ if (plain || failed || forcedColors) {
96
110
  // `plain` opts out of highlighting; `failed` = `@pierre/diffs` not installed.
97
111
  // Render the caller's fallback, or a plain (unhighlighted) patch dump — NOT a
98
112
  // second hunk renderer. One highlighted renderer (Pierre) + a text degrade.
99
- return <div className={className}>{fallback ?? <PlainPatch diff={diff} />}</div>;
113
+ return (
114
+ <div
115
+ className={className}
116
+ data-opengeni-plain-diff={forcedColors ? "forced-colors" : "fallback"}
117
+ >
118
+ {fallback ?? <PlainPatch diff={diff} />}
119
+ </div>
120
+ );
100
121
  }
101
122
 
102
123
  // Default to the dark theme: the host UI is dark-first, and Pierre's own
@@ -131,13 +152,15 @@ export function PierreDiff({
131
152
  "--diffs-fg-number-addition-override": "var(--og-color-status-idle)",
132
153
  "--diffs-fg-number-deletion-override": "var(--og-color-status-failed)",
133
154
  "--diffs-bg-addition-override":
134
- "color-mix(in oklch, var(--og-color-status-idle) 12%, var(--og-color-bg))",
155
+ "color-mix(in oklab, var(--og-color-status-idle) 12%, var(--og-color-bg))",
135
156
  "--diffs-bg-deletion-override":
136
- "color-mix(in oklch, var(--og-color-status-failed) 12%, var(--og-color-bg))",
157
+ "color-mix(in oklab, var(--og-color-status-failed) 12%, var(--og-color-bg))",
137
158
  "--diffs-bg-addition-emphasis-override":
138
- "color-mix(in oklch, var(--og-color-status-idle) 22%, var(--og-color-bg))",
159
+ "color-mix(in oklab, var(--og-color-status-idle) 22%, var(--og-color-bg))",
139
160
  "--diffs-bg-deletion-emphasis-override":
140
- "color-mix(in oklch, var(--og-color-status-failed) 22%, var(--og-color-bg))",
161
+ "color-mix(in oklab, var(--og-color-status-failed) 22%, var(--og-color-bg))",
162
+ "--diffs-header-font-family": "var(--og-font-sans)",
163
+ "--diffs-font-family": "var(--og-font-mono)",
141
164
  "--diffs-font-size": "var(--og-code-font-size)",
142
165
  "--diffs-line-height": "var(--og-code-line-height)",
143
166
  } as CSSProperties;
@@ -177,7 +200,10 @@ function PlainPatch({ diff }: { diff: GitFileDiff[] }) {
177
200
  {diff.map((file) => (
178
201
  <pre
179
202
  key={file.path}
203
+ aria-label={`Diff for ${file.path}`}
180
204
  className="mb-2 overflow-auto whitespace-pre rounded-og-sm border border-og-border bg-og-bg/60 p-2.5 font-og-mono text-og-xs leading-5"
205
+ role="region"
206
+ tabIndex={0}
181
207
  >
182
208
  {gitFileDiffToPatch(file)
183
209
  .split("\n")
@@ -0,0 +1,18 @@
1
+ import type { ComposerState } from "../hooks/use-composer";
2
+
3
+ /**
4
+ * Decide whether checking out a queued prompt would replace current composer
5
+ * content. `hasDraftContent` reads the synchronous draft refs, so this stays
6
+ * correct even when a controlled input update has not rendered yet.
7
+ */
8
+ export function requestQueueDraftEdit(
9
+ composer: Pick<ComposerState, "hasDraftContent">,
10
+ confirmReplacement: () => void,
11
+ editImmediately: () => void,
12
+ ): void {
13
+ if (composer.hasDraftContent()) {
14
+ confirmReplacement();
15
+ } else {
16
+ editImmediately();
17
+ }
18
+ }