@opengeni/react 0.42.1 → 0.44.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 (53) hide show
  1. package/dist/{chunk-SJKT4TKW.js → chunk-23EJ676W.js} +3 -1
  2. package/dist/chunk-23EJ676W.js.map +1 -0
  3. package/dist/{chunk-Q2NCKWTK.js → chunk-HFO4ERGQ.js} +51 -4
  4. package/dist/chunk-HFO4ERGQ.js.map +1 -0
  5. package/dist/{chunk-4IJCL7YO.js → chunk-LWR4MXSS.js} +4 -1
  6. package/dist/chunk-LWR4MXSS.js.map +1 -0
  7. package/dist/{chunk-UWYTCQWW.js → chunk-QQRM3DO3.js} +93 -22
  8. package/dist/{chunk-UWYTCQWW.js.map → chunk-QQRM3DO3.js.map} +1 -1
  9. package/dist/{chunk-JALF5FI3.js → chunk-SRFUT2ZU.js} +2 -2
  10. package/dist/{chunk-WZT5G5OR.js → chunk-U6K24XQD.js} +5 -4
  11. package/dist/{chunk-WZT5G5OR.js.map → chunk-U6K24XQD.js.map} +1 -1
  12. package/dist/{chunk-KR2SK5GJ.js → chunk-YNYIAYXQ.js} +427 -94
  13. package/dist/chunk-YNYIAYXQ.js.map +1 -0
  14. package/dist/components/chat-composer.d.ts +5 -1
  15. package/dist/components/composer-transcription-control.d.ts +3 -1
  16. package/dist/components/composer.d.ts +6 -4
  17. package/dist/components/session-chrome.d.ts +1 -1
  18. package/dist/composer.js +4 -4
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +70 -21
  21. package/dist/index.js.map +1 -1
  22. package/dist/model-policy.d.ts +2 -0
  23. package/dist/model-policy.js +1 -1
  24. package/dist/realtime/realtime-control.d.ts +29 -1
  25. package/dist/realtime.d.ts +1 -1
  26. package/dist/realtime.js +269 -151
  27. package/dist/realtime.js.map +1 -1
  28. package/dist/session-ui.js +2 -2
  29. package/dist/session.js +3 -3
  30. package/dist/timeline/index.d.ts +1 -1
  31. package/dist/timeline/parsers.d.ts +13 -8
  32. package/package.json +2 -2
  33. package/src/components/chat-composer.tsx +58 -19
  34. package/src/components/composer-transcription-control.tsx +194 -165
  35. package/src/components/composer.tsx +78 -14
  36. package/src/components/model-policy-picker.tsx +7 -2
  37. package/src/components/session-chrome.tsx +89 -78
  38. package/src/hooks/use-composer.ts +79 -9
  39. package/src/index.ts +2 -0
  40. package/src/model-policy.ts +4 -0
  41. package/src/realtime/realtime-control.tsx +307 -137
  42. package/src/realtime.ts +1 -0
  43. package/src/timeline/index.ts +2 -0
  44. package/src/timeline/parsers.ts +201 -19
  45. package/src/timeline/projection.ts +11 -0
  46. package/src/timeline/tool-renderers.tsx +213 -8
  47. package/src/timeline/turn-summary.tsx +7 -2
  48. package/styles/tokens.css +8 -5
  49. package/dist/chunk-4IJCL7YO.js.map +0 -1
  50. package/dist/chunk-KR2SK5GJ.js.map +0 -1
  51. package/dist/chunk-Q2NCKWTK.js.map +0 -1
  52. package/dist/chunk-SJKT4TKW.js.map +0 -1
  53. /package/dist/{chunk-JALF5FI3.js.map → chunk-SRFUT2ZU.js.map} +0 -0
@@ -11,7 +11,7 @@
11
11
  * | --- | --- |
12
12
  * | `--og-session-chrome-surface` / `-open` | Dock fill (collapsed / expanded) |
13
13
  * | `--og-session-chrome-border` / `-open` | Dock edge |
14
- * | `--og-session-chrome-highlight` | Sliding chip selection pill |
14
+ * | `--og-session-chrome-highlight` / `-ring` | Sliding chip selection fill + edge |
15
15
  * | `--og-session-chrome-shadow` / `-open` | Elevation |
16
16
  * | `--og-session-chrome-radius` | Dock corner radius |
17
17
  * | `--og-session-chrome-chip-min-height` | Signal chip height |
@@ -394,7 +394,7 @@ export function SessionChrome({
394
394
  const chipRefs = useRef<Partial<Record<SessionChromeSignalId, HTMLButtonElement | null>>>({});
395
395
  const railRef = useRef<HTMLDivElement | null>(null);
396
396
  const panelBodyRef = useRef<HTMLDivElement | null>(null);
397
- const [pill, setPill] = useState({ left: 0, width: 0, opacity: 0 });
397
+ const [pill, setPill] = useState({ left: 0, top: 0, width: 0, height: 0, opacity: 0 });
398
398
  const [panelHeight, setPanelHeight] = useState(0);
399
399
 
400
400
  const signalIds = signals.map((signal) => signal.id).join(",");
@@ -421,11 +421,15 @@ export function SessionChrome({
421
421
  }
422
422
  const chip = chipRefs.current[active];
423
423
  if (!chip) return;
424
+ // Measure against the chip's own box so a wrapped multi-row rail never
425
+ // stretches the highlight into a tall stripe across every signal.
424
426
  const railBox = rail.getBoundingClientRect();
425
427
  const chipBox = chip.getBoundingClientRect();
426
428
  setPill({
427
429
  left: chipBox.left - railBox.left,
430
+ top: chipBox.top - railBox.top,
428
431
  width: chipBox.width,
432
+ height: chipBox.height,
429
433
  opacity: 1,
430
434
  });
431
435
  };
@@ -433,6 +437,9 @@ export function SessionChrome({
433
437
  if (!rail) return;
434
438
  const observer = new ResizeObserver(measure);
435
439
  observer.observe(rail);
440
+ for (const chip of Object.values(chipRefs.current)) {
441
+ if (chip) observer.observe(chip);
442
+ }
436
443
  window.addEventListener("resize", measure);
437
444
  return () => {
438
445
  observer.disconnect();
@@ -568,88 +575,92 @@ export function SessionChrome({
568
575
  }}
569
576
  >
570
577
  <div
571
- ref={railRef}
572
- className="relative flex flex-wrap items-stretch"
578
+ className="relative"
573
579
  style={{
574
- gap: "var(--og-session-chrome-chip-gap)",
575
- padding: "var(--og-session-chrome-rail-pad)",
580
+ paddingTop: "var(--og-session-chrome-rail-pad)",
581
+ paddingBottom: "var(--og-session-chrome-rail-pad)",
582
+ paddingLeft: "var(--og-session-chrome-rail-pad)",
583
+ paddingRight: "var(--og-session-chrome-rail-pad)",
576
584
  }}
577
585
  >
578
- <motion.div
579
- aria-hidden
580
- className="pointer-events-none absolute rounded-og-md ring-1 ring-og-border/50"
586
+ <div
587
+ ref={railRef}
588
+ className="relative flex flex-wrap items-center"
581
589
  style={{
582
- top: "var(--og-session-chrome-rail-pad)",
583
- bottom: "var(--og-session-chrome-rail-pad)",
584
- background: "var(--og-session-chrome-highlight)",
590
+ gap: "var(--og-session-chrome-chip-gap)",
585
591
  }}
586
- initial={false}
587
- animate={{
588
- x: pill.left,
589
- width: pill.width,
590
- opacity: pill.opacity,
591
- }}
592
- transition={{ duration: shellDuration, ease }}
593
- />
594
- {signals.map((signal) => {
595
- const selected = active === signal.id;
596
- return (
597
- <button
598
- key={signal.id}
599
- type="button"
600
- ref={(node) => {
601
- chipRefs.current[signal.id] = node;
602
- }}
603
- aria-expanded={selected}
604
- aria-controls={panelId}
605
- data-testid={`session-chrome-${signal.id}`}
606
- data-og-session-chrome-signal={signal.id}
607
- onClick={() => setActive(selected ? null : signal.id)}
608
- className={cn(
609
- "group relative z-[1] inline-flex min-h-[var(--og-session-chrome-chip-min-height)] max-w-full items-center gap-1 rounded-og-md text-left text-og-xs outline-none",
610
- "transition-colors duration-150 motion-reduce:transition-none",
611
- "hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent/40",
612
- "pointer-coarse:min-h-11",
613
- selected ? "text-og-fg" : "text-og-fg-muted",
614
- )}
615
- style={{
616
- paddingInline: "var(--og-session-chrome-chip-pad-x)",
617
- }}
618
- >
619
- <span className={cn("shrink-0", toneClass(signal.tone, selected))}>
620
- {signal.icon}
621
- </span>
622
- <span className="shrink-0 font-medium text-og-fg">{signal.label}</span>
623
- {signal.detail ? (
624
- <>
625
- <span aria-hidden className="shrink-0 text-og-fg-subtle/60">
626
- ·
627
- </span>
628
- <span className="min-w-0 max-w-[8.5rem] truncate text-og-fg sm:max-w-[12rem]">
629
- {signal.detail}
592
+ >
593
+ <motion.div
594
+ aria-hidden
595
+ className="pointer-events-none absolute left-0 top-0 rounded-og-md"
596
+ style={{
597
+ background: "var(--og-session-chrome-highlight)",
598
+ boxShadow: "inset 0 0 0 1px var(--og-session-chrome-highlight-ring)",
599
+ }}
600
+ initial={false}
601
+ animate={{
602
+ x: pill.left,
603
+ y: pill.top,
604
+ width: pill.width,
605
+ height: pill.height,
606
+ opacity: pill.opacity,
607
+ }}
608
+ transition={{ duration: shellDuration, ease }}
609
+ />
610
+ {signals.map((signal) => {
611
+ const selected = active === signal.id;
612
+ return (
613
+ <button
614
+ key={signal.id}
615
+ type="button"
616
+ ref={(node) => {
617
+ chipRefs.current[signal.id] = node;
618
+ }}
619
+ aria-expanded={selected}
620
+ aria-controls={panelId}
621
+ aria-label={selected ? `Close ${signal.label}` : undefined}
622
+ data-testid={`session-chrome-${signal.id}`}
623
+ data-og-session-chrome-signal={signal.id}
624
+ onClick={() => setActive(selected ? null : signal.id)}
625
+ className={cn(
626
+ "group relative z-[1] inline-flex min-h-[var(--og-session-chrome-chip-min-height)] max-w-full items-center gap-1 rounded-og-md py-1 text-left text-og-xs outline-none",
627
+ // Coarse pointers keep a 44px target (session-pins acceptance).
628
+ "pointer-coarse:min-h-11",
629
+ "transition-colors duration-150 motion-reduce:transition-none",
630
+ "hover:text-og-fg focus-visible:bg-og-surface-3/50",
631
+ selected ? "text-og-fg" : "text-og-fg-muted",
632
+ )}
633
+ style={{
634
+ paddingInline: "var(--og-session-chrome-chip-pad-x)",
635
+ }}
636
+ >
637
+ <span className={cn("shrink-0", toneClass(signal.tone, selected))}>
638
+ {signal.icon}
639
+ </span>
640
+ <span className="shrink-0 font-medium text-og-fg">{signal.label}</span>
641
+ {signal.detail ? (
642
+ <>
643
+ <span aria-hidden className="shrink-0 text-og-fg-subtle/60">
644
+ ·
645
+ </span>
646
+ <span className="min-w-0 max-w-[8.5rem] truncate text-og-fg sm:max-w-[12rem]">
647
+ {signal.detail}
648
+ </span>
649
+ </>
650
+ ) : null}
651
+ {selected ? (
652
+ <span
653
+ data-testid="session-chrome-close"
654
+ className="ml-0.5 inline-flex size-4 shrink-0 items-center justify-center rounded-og-sm text-og-fg-subtle transition-colors group-hover:text-og-fg pointer-coarse:size-5"
655
+ aria-hidden
656
+ >
657
+ <XIcon className="size-3" />
630
658
  </span>
631
- </>
632
- ) : null}
633
- </button>
634
- );
635
- })}
636
- <AnimatePresence initial={false}>
637
- {open ? (
638
- <motion.button
639
- key="close"
640
- type="button"
641
- aria-label="Close session chrome panel"
642
- onClick={() => setActive(null)}
643
- initial={reduceMotion ? false : { opacity: 0 }}
644
- animate={{ opacity: 1 }}
645
- exit={reduceMotion ? { opacity: 1 } : { opacity: 0 }}
646
- transition={{ duration: crossfadeDuration, ease }}
647
- className="relative z-[1] ml-auto inline-flex size-7 shrink-0 items-center justify-center rounded-og-md text-og-fg-subtle outline-none transition-colors hover:bg-og-surface-3/60 hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent/40 pointer-coarse:size-11"
648
- >
649
- <XIcon className="size-3" />
650
- </motion.button>
651
- ) : null}
652
- </AnimatePresence>
659
+ ) : null}
660
+ </button>
661
+ );
662
+ })}
663
+ </div>
653
664
  </div>
654
665
 
655
666
  <motion.div
@@ -585,6 +585,22 @@ export function useComposer(
585
585
  }
586
586
  void loadDraft(false);
587
587
  }, [durableDrafts, loadDraft, sessionId]);
588
+ // After long background / sleep the in-memory revision is often stale while
589
+ // a prior autosave already advanced the server. Soft-reload on wake so the
590
+ // next keystroke does not OCC against a dead revision.
591
+ useEffect(() => {
592
+ if (!sessionId || !durableDrafts) return;
593
+ const onWake = () => {
594
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
595
+ void loadDraft(false);
596
+ };
597
+ document.addEventListener("visibilitychange", onWake);
598
+ window.addEventListener("pageshow", onWake);
599
+ return () => {
600
+ document.removeEventListener("visibilitychange", onWake);
601
+ window.removeEventListener("pageshow", onWake);
602
+ };
603
+ }, [durableDrafts, loadDraft, sessionId]);
588
604
  useEffect(() => {
589
605
  if (!sessionId || !durableDrafts) return;
590
606
  return registerSessionReconciler(sessionId, "composer", async () => await loadDraft(false));
@@ -692,7 +708,16 @@ export function useComposer(
692
708
  }
693
709
  setDraftSaving(true);
694
710
  try {
695
- const saved = await client.saveComposerDraft(workspaceId, sessionId, request);
711
+ const saved = await saveComposerDraftWithStaleRetry({
712
+ client,
713
+ workspaceId,
714
+ sessionId,
715
+ request,
716
+ onAdoptRemote: (remote) => {
717
+ draftRef.current = remote;
718
+ setDraft(remote);
719
+ },
720
+ });
696
721
  if (
697
722
  targetKeyRef.current !== ownedTargetKey ||
698
723
  targetGeneration.current !== ownedGeneration
@@ -701,8 +726,12 @@ export function useComposer(
701
726
  }
702
727
  draftRef.current = saved;
703
728
  setDraft(saved);
704
- lastSavedSignature.current = signature;
729
+ lastSavedSignature.current = draftSignature({
730
+ ...request,
731
+ expectedRevision: saved.revision,
732
+ });
705
733
  setDraftConflict(null);
734
+ setError(null);
706
735
  success = true;
707
736
  } catch (cause) {
708
737
  if (
@@ -1260,11 +1289,13 @@ export function useComposer(
1260
1289
  }
1261
1290
  if (choice === "use_remote") {
1262
1291
  applyDraft(remote);
1292
+ setError(null);
1263
1293
  return;
1264
1294
  }
1265
1295
  draftRef.current = remote;
1266
1296
  setDraft(remote);
1267
1297
  setDraftConflict(null);
1298
+ setError(null);
1268
1299
  const payload = currentDraftPayload();
1269
1300
  if (payload) await persistPayload({ ...payload, expectedRevision: remote.revision });
1270
1301
  },
@@ -1393,13 +1424,52 @@ function asError(cause: unknown): Error {
1393
1424
 
1394
1425
  function isDraftConflictError(error: Error): boolean {
1395
1426
  const apiError = error as Partial<OpenGeniApiError>;
1396
- return (
1397
- apiError.status === 409 &&
1398
- apiError.outcomeUnknown === false &&
1399
- (apiError.code === undefined ||
1400
- apiError.code === "conflict" ||
1401
- apiError.code === "idempotency_conflict")
1402
- );
1427
+ if (apiError.status !== 409 || apiError.outcomeUnknown === true) return false;
1428
+ // Production queue OCC returns `DRAFT_CHANGED`. Older/SDK-shaped 409s may
1429
+ // omit code or use the generic conflict labels — all are recoverable OCC.
1430
+ const code = apiError.code;
1431
+ if (
1432
+ code === undefined ||
1433
+ code === "DRAFT_CHANGED" ||
1434
+ code === "conflict" ||
1435
+ code === "idempotency_conflict"
1436
+ ) {
1437
+ return true;
1438
+ }
1439
+ return /draft changed/i.test(error.message);
1440
+ }
1441
+
1442
+ /**
1443
+ * One OCC retry: adopt the server revision and rewrite the same local content.
1444
+ * Covers the common "tab slept through a successful autosave" case without
1445
+ * stranding the operator on a raw 409 toast.
1446
+ */
1447
+ async function saveComposerDraftWithStaleRetry(input: {
1448
+ client: {
1449
+ getComposerDraft: (workspaceId: string, sessionId: string) => Promise<ComposerDraft>;
1450
+ saveComposerDraft: (
1451
+ workspaceId: string,
1452
+ sessionId: string,
1453
+ request: SaveComposerDraftRequest,
1454
+ ) => Promise<ComposerDraft>;
1455
+ };
1456
+ workspaceId: string;
1457
+ sessionId: string;
1458
+ request: SaveComposerDraftRequest;
1459
+ onAdoptRemote: (remote: ComposerDraft) => void;
1460
+ }): Promise<ComposerDraft> {
1461
+ try {
1462
+ return await input.client.saveComposerDraft(input.workspaceId, input.sessionId, input.request);
1463
+ } catch (cause) {
1464
+ const problem = asError(cause);
1465
+ if (!isDraftConflictError(problem)) throw problem;
1466
+ const remote = await input.client.getComposerDraft(input.workspaceId, input.sessionId);
1467
+ input.onAdoptRemote(remote);
1468
+ return await input.client.saveComposerDraft(input.workspaceId, input.sessionId, {
1469
+ ...input.request,
1470
+ expectedRevision: remote.revision,
1471
+ });
1472
+ }
1403
1473
  }
1404
1474
 
1405
1475
  function draftPayload(draft: ComposerDraft): SaveComposerDraftRequest {
package/src/index.ts CHANGED
@@ -367,12 +367,14 @@ export type {
367
367
  // Pure provider-shape parsers (exec banner, V4A diff, secret redaction, …)
368
368
  export {
369
369
  applyPatchOps,
370
+ applyPatchOpsFromToolItem,
370
371
  controlCaret,
371
372
  execTruncated,
372
373
  isApplyPatch,
373
374
  isExecSessionLostBanner,
374
375
  looksBinary,
375
376
  parseExecBannerSessionId,
377
+ parseFreeformApplyPatch,
376
378
  parseToolArgs,
377
379
  redactSecrets,
378
380
  sandboxCommandExitCode,
@@ -5,6 +5,8 @@ export type PickerBillingClass = "opengeni_credits" | "codex_subscription" | "by
5
5
  export type PickerModelRow<TCatalog extends ClientModel = WorkspaceModelCatalogModel> = {
6
6
  id: string;
7
7
  label: string;
8
+ /** Catalog-curated compact label for dense UI; fall back to `label` when absent. */
9
+ shortLabel?: string | undefined;
8
10
  billingClass: PickerBillingClass;
9
11
  billingClassLabel: string;
10
12
  selectable: boolean;
@@ -171,6 +173,7 @@ export function projectPickerRows(models: WorkspaceModelCatalogModel[]): PickerM
171
173
  return {
172
174
  id: catalog.id,
173
175
  label: catalog.label,
176
+ ...(catalog.shortLabel ? { shortLabel: catalog.shortLabel } : {}),
174
177
  billingClass,
175
178
  billingClassLabel: billingClassLabel(billingClass),
176
179
  selectable: catalog.availability.selectable,
@@ -191,6 +194,7 @@ export function projectClientModelRows(models: ClientModel[]): PickerModelRow<Cl
191
194
  return {
192
195
  id: catalog.id,
193
196
  label: catalog.label,
197
+ ...(catalog.shortLabel ? { shortLabel: catalog.shortLabel } : {}),
194
198
  billingClass,
195
199
  billingClassLabel: billingClassLabel(billingClass),
196
200
  selectable: true,