@opengeni/react 0.36.1 → 0.40.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 (85) hide show
  1. package/README.md +92 -2
  2. package/dist/{chunk-J2RVJ6JX.js → chunk-4IJCL7YO.js} +13 -2
  3. package/dist/chunk-4IJCL7YO.js.map +1 -0
  4. package/dist/{chunk-7CJOAW3V.js → chunk-EB67J347.js} +1 -135
  5. package/dist/chunk-EB67J347.js.map +1 -0
  6. package/dist/chunk-EKZH6IDG.js +141 -0
  7. package/dist/chunk-EKZH6IDG.js.map +1 -0
  8. package/dist/{chunk-OKKMZDQF.js → chunk-JALF5FI3.js} +9 -7
  9. package/dist/{chunk-OKKMZDQF.js.map → chunk-JALF5FI3.js.map} +1 -1
  10. package/dist/{chunk-Z5FV355Z.js → chunk-KR2SK5GJ.js} +1382 -724
  11. package/dist/chunk-KR2SK5GJ.js.map +1 -0
  12. package/dist/{chunk-N4XEBE23.js → chunk-OMCFRWHL.js} +22 -13
  13. package/dist/{chunk-N4XEBE23.js.map → chunk-OMCFRWHL.js.map} +1 -1
  14. package/dist/{chunk-FT2TXNGZ.js → chunk-Q2NCKWTK.js} +183 -18
  15. package/dist/chunk-Q2NCKWTK.js.map +1 -0
  16. package/dist/{chunk-22RM4GMO.js → chunk-SJKT4TKW.js} +26 -2
  17. package/dist/chunk-SJKT4TKW.js.map +1 -0
  18. package/dist/{chunk-ALA3UGWB.js → chunk-SXIQK54Z.js} +8 -6
  19. package/dist/{chunk-ALA3UGWB.js.map → chunk-SXIQK54Z.js.map} +1 -1
  20. package/dist/chunk-WZT5G5OR.js +433 -0
  21. package/dist/chunk-WZT5G5OR.js.map +1 -0
  22. package/dist/client.d.ts +2 -0
  23. package/dist/components/chat-composer.d.ts +7 -1
  24. package/dist/components/human-input-form.d.ts +16 -6
  25. package/dist/components/human-input-surface.d.ts +19 -0
  26. package/dist/components/machine-input-display.d.ts +12 -0
  27. package/dist/components/message-timeline.d.ts +6 -1
  28. package/dist/components/model-policy-picker.d.ts +78 -0
  29. package/dist/components/session-chrome.d.ts +2 -1
  30. package/dist/components/tip-follow.d.ts +30 -0
  31. package/dist/composer.d.ts +2 -0
  32. package/dist/composer.js +22 -5
  33. package/dist/hooks/use-composer.d.ts +10 -1
  34. package/dist/index.d.ts +7 -1
  35. package/dist/index.js +2261 -2230
  36. package/dist/index.js.map +1 -1
  37. package/dist/machines.js +4 -3
  38. package/dist/model-policy.d.ts +12 -4
  39. package/dist/model-policy.js +5 -1
  40. package/dist/realtime/dropdown-menu.d.ts +16 -0
  41. package/dist/realtime/realtime-control.d.ts +126 -0
  42. package/dist/realtime.d.ts +11 -0
  43. package/dist/realtime.js +1023 -0
  44. package/dist/realtime.js.map +1 -0
  45. package/dist/session-context.d.ts +7 -1
  46. package/dist/session-ui.d.ts +2 -0
  47. package/dist/session-ui.js +6 -4
  48. package/dist/session.js +5 -4
  49. package/dist/timeline/compute-label.d.ts +2 -0
  50. package/dist/timeline/index.d.ts +1 -0
  51. package/dist/timeline/types.d.ts +5 -0
  52. package/package.json +7 -2
  53. package/src/client.ts +13 -0
  54. package/src/components/chat-composer.tsx +19 -5
  55. package/src/components/composer-transcription-control.tsx +2 -2
  56. package/src/components/composer.tsx +14 -5
  57. package/src/components/human-input-form.tsx +506 -186
  58. package/src/components/human-input-surface.tsx +80 -0
  59. package/src/components/machine-input-display.ts +83 -0
  60. package/src/components/message-timeline.tsx +348 -308
  61. package/src/components/model-policy-picker.tsx +587 -0
  62. package/src/components/sandbox-files.tsx +1 -1
  63. package/src/components/session-chrome.tsx +141 -15
  64. package/src/components/tip-follow.ts +127 -6
  65. package/src/composer.ts +13 -0
  66. package/src/hooks/use-composer.ts +229 -29
  67. package/src/index.ts +19 -0
  68. package/src/model-policy.ts +51 -7
  69. package/src/realtime/dropdown-menu.tsx +123 -0
  70. package/src/realtime/realtime-control.tsx +1178 -0
  71. package/src/realtime.ts +27 -0
  72. package/src/session-context.ts +16 -0
  73. package/src/session-ui.ts +2 -0
  74. package/src/timeline/compute-label.tsx +18 -0
  75. package/src/timeline/index.ts +3 -0
  76. package/src/timeline/projection.ts +21 -1
  77. package/src/timeline/tool-renderers.tsx +232 -43
  78. package/src/timeline/types.ts +5 -0
  79. package/styles/index.css +61 -0
  80. package/styles/tokens.css +2 -0
  81. package/dist/chunk-22RM4GMO.js.map +0 -1
  82. package/dist/chunk-7CJOAW3V.js.map +0 -1
  83. package/dist/chunk-FT2TXNGZ.js.map +0 -1
  84. package/dist/chunk-J2RVJ6JX.js.map +0 -1
  85. package/dist/chunk-Z5FV355Z.js.map +0 -1
@@ -1,12 +1,13 @@
1
- import type {
2
- ComposerDraft,
3
- EffectiveControlResumeOption,
4
- EffectiveSessionControl,
5
- OpenGeniApiError,
6
- ResourceRef,
7
- SaveComposerDraftRequest,
8
- SendMessageInput,
9
- SessionEvent,
1
+ import {
2
+ DEFAULT_FILE_RESOURCE_MOUNT_ROOT,
3
+ type ComposerDraft,
4
+ type EffectiveControlResumeOption,
5
+ type EffectiveSessionControl,
6
+ type OpenGeniApiError,
7
+ type ResourceRef,
8
+ type SaveComposerDraftRequest,
9
+ type SendMessageInput,
10
+ type SessionEvent,
10
11
  } from "@opengeni/sdk";
11
12
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
12
13
  import { useEmbeddedSession, type EmbeddedSessionClientOverride } from "../session-context";
@@ -225,6 +226,8 @@ export type ComposerState = {
225
226
  send: (text?: string) => Promise<boolean>;
226
227
  /** Supersede current direction with the draft. */
227
228
  steer: (text?: string) => Promise<boolean>;
229
+ /** Optimistic-to-durable projection for a Steer that has not started yet. */
230
+ steering?: ComposerSteeringState | null | undefined;
228
231
  sending: boolean;
229
232
  canSend: boolean;
230
233
  /** Pause the session without deleting its prompt queue. */
@@ -250,6 +253,59 @@ export type ComposerState = {
250
253
  clearError: () => void;
251
254
  };
252
255
 
256
+ export type ComposerSteeringState = {
257
+ phase: "submitting" | "accepted";
258
+ text: string;
259
+ clientEventId: string | null;
260
+ triggerEventId: string | null;
261
+ turnId: string | null;
262
+ };
263
+
264
+ const STEERING_SETTLEMENT_EVENT_TYPES = new Set([
265
+ "turn.started",
266
+ "turn.completed",
267
+ "turn.failed",
268
+ "turn.cancelled",
269
+ "turn.superseded",
270
+ ]);
271
+
272
+ function isSteeringSettlementEvent(event: SessionEvent): boolean {
273
+ return STEERING_SETTLEMENT_EVENT_TYPES.has(event.type);
274
+ }
275
+
276
+ function steeringAcceptedEvent(
277
+ steering: ComposerSteeringState,
278
+ events: readonly SessionEvent[],
279
+ ): SessionEvent | undefined {
280
+ return events.find(
281
+ (event) =>
282
+ event.type === "user.message" &&
283
+ steering.clientEventId !== null &&
284
+ event.clientEventId === steering.clientEventId,
285
+ );
286
+ }
287
+
288
+ function steeringSettledByEvents(
289
+ steering: ComposerSteeringState,
290
+ events: readonly SessionEvent[],
291
+ ): boolean {
292
+ const acceptedEventId =
293
+ steering.triggerEventId ?? steeringAcceptedEvent(steering, events)?.id ?? null;
294
+ return events.some((event) => {
295
+ if (steering.turnId && event.turnId === steering.turnId && isSteeringSettlementEvent(event)) {
296
+ return true;
297
+ }
298
+ if (event.type !== "turn.started" || !acceptedEventId) return false;
299
+ const payload = event.payload;
300
+ return (
301
+ typeof payload === "object" &&
302
+ payload !== null &&
303
+ "triggerEventId" in payload &&
304
+ payload.triggerEventId === acceptedEventId
305
+ );
306
+ });
307
+ }
308
+
253
309
  /**
254
310
  * Draft + send + Pause/Resume state for the chat composer — the only
255
311
  * human-to-agent input surface. The draft survives a failed send (nothing is
@@ -271,6 +327,17 @@ export function useComposer(
271
327
  // a parent may switch sessionId without remounting this public hook.
272
328
  const [stateTargetKey, setStateTargetKey] = useState(targetKey);
273
329
  const [sending, setSending] = useState(false);
330
+ const [steering, setSteering] = useState<ComposerSteeringState | null>(() =>
331
+ initialPendingOperation?.delivery === "steer"
332
+ ? {
333
+ phase: "submitting",
334
+ text: initialPendingOperation.input.text,
335
+ clientEventId: initialPendingOperation.input.clientEventId ?? null,
336
+ triggerEventId: null,
337
+ turnId: null,
338
+ }
339
+ : null,
340
+ );
274
341
  const [pausing, setPausing] = useState(false);
275
342
  const [resuming, setResuming] = useState(false);
276
343
  const [error, setError] = useState<Error | null>(null);
@@ -282,6 +349,8 @@ export function useComposer(
282
349
  () => initialShadow?.resources ?? [],
283
350
  );
284
351
  const pendingOperationRef = useRef<PendingComposerOperation | null>(initialPendingOperation);
352
+ const steeringSettlementEventsRef = useRef<SessionEvent[]>([]);
353
+ const steeringRef = useRef(steering);
285
354
  const pendingClientEventId = useRef<string | null>(
286
355
  initialPendingOperation?.input.clientEventId ?? null,
287
356
  );
@@ -303,6 +372,9 @@ export function useComposer(
303
372
  useLayoutEffect(() => {
304
373
  onDraftAppliedRef.current = onDraftApplied;
305
374
  }, [onDraftApplied]);
375
+ useLayoutEffect(() => {
376
+ steeringRef.current = steering;
377
+ }, [steering]);
306
378
  // Read through a ref so a new extras closure (created every render by
307
379
  // callers passing inline functions) does not invalidate `send`.
308
380
  const sendExtrasRef = useRef(options.sendExtras);
@@ -320,6 +392,7 @@ export function useComposer(
320
392
  targetGeneration.current += 1;
321
393
  draftReadGeneration.current += 1;
322
394
  pendingOperationRef.current = restorePendingComposerOperation(pendingOperationKey);
395
+ steeringSettlementEventsRef.current = [];
323
396
  pendingClientEventId.current = pendingOperationRef.current?.input.clientEventId ?? null;
324
397
  const shadow = pendingOperationRef.current?.newerShadow;
325
398
  localEditRevision.current = shadow ? 1 : 0;
@@ -333,6 +406,17 @@ export function useComposer(
333
406
  setStateTargetKey(targetKey);
334
407
  setValue(shadow?.text ?? "");
335
408
  setSending(false);
409
+ setSteering(
410
+ pendingOperationRef.current?.delivery === "steer"
411
+ ? {
412
+ phase: "submitting",
413
+ text: pendingOperationRef.current.input.text,
414
+ clientEventId: pendingOperationRef.current.input.clientEventId ?? null,
415
+ triggerEventId: null,
416
+ turnId: null,
417
+ }
418
+ : null,
419
+ );
336
420
  setPausing(false);
337
421
  setResuming(false);
338
422
  setError(null);
@@ -454,13 +538,15 @@ export function useComposer(
454
538
  replaceLocal ||
455
539
  (!localWasDirtyAtStart && localAtStart === localEditRevision.current)
456
540
  ) {
457
- // Model/effort ride in sendExtras (outside localEditRevision). If
541
+ // Model/effort/latency ride in sendExtras (outside
542
+ // localEditRevision). If
458
543
  // the picker changed during this fetch, skip onDraftApplied so a
459
- // stale server model/effort cannot undo the operator's pick.
544
+ // stale server policy cannot undo the operator's pick.
460
545
  const extrasNow = resolveSendExtras(sendExtrasRef.current);
461
546
  const pickerChangedDuringFetch =
462
547
  extrasNow.model !== extrasAtStart.model ||
463
- extrasNow.reasoningEffort !== extrasAtStart.reasoningEffort;
548
+ extrasNow.reasoningEffort !== extrasAtStart.reasoningEffort ||
549
+ extrasNow.latencyMode !== extrasAtStart.latencyMode;
464
550
  valueRef.current = fetched.text;
465
551
  restoredResourcesRef.current = fetched.resources;
466
552
  lastSavedSignature.current = draftSignature(draftPayload(fetched));
@@ -503,18 +589,76 @@ export function useComposer(
503
589
  if (!sessionId || !durableDrafts) return;
504
590
  return registerSessionReconciler(sessionId, "composer", async () => await loadDraft(false));
505
591
  }, [durableDrafts, loadDraft, registerSessionReconciler, sessionId]);
592
+ const reconcileSteering = useCallback(async (): Promise<void> => {
593
+ if (!sessionId || !steeringRef.current) return;
594
+ const ownedTargetKey = targetKey;
595
+ let events: SessionEvent[];
596
+ try {
597
+ events = await client.listEvents(workspaceId, sessionId, {
598
+ includeTypes: [
599
+ "user.message",
600
+ "turn.started",
601
+ "turn.completed",
602
+ "turn.failed",
603
+ "turn.cancelled",
604
+ "turn.superseded",
605
+ ],
606
+ limit: 250,
607
+ payloadMode: "full",
608
+ });
609
+ } catch {
610
+ // Best effort: the live stream still settles steering when its event arrives.
611
+ return;
612
+ }
613
+ if (targetKeyRef.current !== ownedTargetKey) return;
614
+ setSteering((current) => {
615
+ if (!current) return current;
616
+ if (steeringSettledByEvents(current, events)) {
617
+ steeringSettlementEventsRef.current = [];
618
+ return null;
619
+ }
620
+ const accepted = steeringAcceptedEvent(current, events);
621
+ if (!accepted || current.triggerEventId) return current;
622
+ return {
623
+ ...current,
624
+ phase: "accepted",
625
+ triggerEventId: accepted.id,
626
+ };
627
+ });
628
+ }, [client, sessionId, targetKey, workspaceId]);
506
629
  useSessionEventTrigger(
507
630
  client,
508
631
  workspaceId,
509
632
  sessionId,
510
- isComposerDraftEvent,
511
- () => void loadDraft(false),
633
+ (event) => isComposerDraftEvent(event) || isSteeringSettlementEvent(event),
634
+ (event) => {
635
+ if (isComposerDraftEvent(event)) void loadDraft(false);
636
+ if (!isSteeringSettlementEvent(event)) return;
637
+ steeringSettlementEventsRef.current = [
638
+ ...steeringSettlementEventsRef.current.slice(-15),
639
+ event,
640
+ ];
641
+ setSteering((current) => {
642
+ if (!current || !steeringSettledByEvents(current, [event])) return current;
643
+ steeringSettlementEventsRef.current = [];
644
+ return null;
645
+ });
646
+ },
512
647
  {
513
- enabled: Boolean(sessionId) && durableDrafts,
648
+ enabled: Boolean(sessionId) && (durableDrafts || steering !== null),
514
649
  ...(options.events !== undefined ? { events: options.events } : {}),
515
650
  },
651
+ reconcileSteering,
516
652
  );
517
653
 
654
+ useEffect(() => {
655
+ if (!steering) return;
656
+ const observed = [...(options.events ?? []), ...steeringSettlementEventsRef.current];
657
+ if (!steeringSettledByEvents(steering, observed)) return;
658
+ steeringSettlementEventsRef.current = [];
659
+ setSteering(null);
660
+ }, [options.events, steering]);
661
+
518
662
  const currentDraftPayload = useCallback((): SaveComposerDraftRequest | null => {
519
663
  if (!durableDrafts || targetKeyRef.current !== targetKey) return null;
520
664
  const base = draftRef.current;
@@ -667,6 +811,8 @@ export function useComposer(
667
811
  forgetPendingComposerOperation(operationKey);
668
812
  };
669
813
 
814
+ let keepSteering = pending?.delivery === "steer";
815
+
670
816
  const settleAccepted = (operation: PendingComposerOperation): void => {
671
817
  clearPending();
672
818
  const draftWasUnchanged = valueRef.current === operation.draftAtSend;
@@ -699,30 +845,40 @@ export function useComposer(
699
845
  onSent?.(operation.input.text, operation.input);
700
846
  };
701
847
 
702
- const deliver = async (operation: PendingComposerOperation): Promise<void> => {
848
+ const deliver = async (operation: PendingComposerOperation) => {
703
849
  if (operation.delivery === "steer") {
704
- await client.steerMessage(workspaceId, sessionId, operation.input);
705
- } else {
706
- await client.sendMessage(workspaceId, sessionId, operation.input);
850
+ return await client.steerMessage(workspaceId, sessionId, operation.input);
707
851
  }
852
+ await client.sendMessage(workspaceId, sessionId, operation.input);
853
+ return null;
708
854
  };
709
855
 
856
+ if (delivery === "steer") {
857
+ setSteering({
858
+ phase: "submitting",
859
+ text: rawText,
860
+ clientEventId: pending?.input.clientEventId ?? pendingClientEventId.current,
861
+ triggerEventId: null,
862
+ turnId: null,
863
+ });
864
+ }
710
865
  setSending(true);
711
866
  setError(null);
712
867
  try {
713
868
  if (pending) {
714
- let accepted = false;
869
+ let acceptedEvent: SessionEvent | null = null;
715
870
  try {
716
871
  const events = await client.listEvents(workspaceId, sessionId, {
717
872
  includeTypes: ["user.message"],
718
873
  limit: 100,
719
874
  payloadMode: "none",
720
875
  });
721
- accepted = events.some(
722
- (event) =>
723
- event.type === "user.message" &&
724
- event.clientEventId === pending.input.clientEventId,
725
- );
876
+ acceptedEvent =
877
+ events.find(
878
+ (event) =>
879
+ event.type === "user.message" &&
880
+ event.clientEventId === pending.input.clientEventId,
881
+ ) ?? null;
726
882
  } catch (cause) {
727
883
  if (
728
884
  targetKeyRef.current === ownedTargetKey &&
@@ -738,7 +894,17 @@ export function useComposer(
738
894
  ) {
739
895
  return false;
740
896
  }
741
- if (accepted) {
897
+ if (acceptedEvent) {
898
+ if (pending.delivery === "steer") {
899
+ keepSteering = true;
900
+ setSteering({
901
+ phase: "accepted",
902
+ text: pending.input.text,
903
+ clientEventId: pending.input.clientEventId ?? null,
904
+ triggerEventId: acceptedEvent.id,
905
+ turnId: null,
906
+ });
907
+ }
742
908
  settleAccepted(pending);
743
909
  return true;
744
910
  }
@@ -751,8 +917,19 @@ export function useComposer(
751
917
  return false;
752
918
  }
753
919
  try {
754
- await deliver(pending);
920
+ const result = await deliver(pending);
921
+ if (pending.delivery === "steer" && result) {
922
+ keepSteering = true;
923
+ setSteering({
924
+ phase: "accepted",
925
+ text: pending.input.text,
926
+ clientEventId: pending.input.clientEventId ?? null,
927
+ triggerEventId: result.accepted.id,
928
+ turnId: result.turn.id,
929
+ });
930
+ }
755
931
  } catch (cause) {
932
+ if (pending.delivery === "steer") keepSteering = true;
756
933
  if (
757
934
  targetKeyRef.current === ownedTargetKey &&
758
935
  targetGeneration.current === ownedGeneration
@@ -812,11 +989,32 @@ export function useComposer(
812
989
  };
813
990
  pendingOperationRef.current = operation;
814
991
  rememberPendingComposerOperation(operationKey, operation);
992
+ if (delivery === "steer") {
993
+ setSteering({
994
+ phase: "submitting",
995
+ text: sendText,
996
+ clientEventId: input.clientEventId ?? null,
997
+ triggerEventId: null,
998
+ turnId: null,
999
+ });
1000
+ }
815
1001
  try {
816
- await deliver(operation);
1002
+ const result = await deliver(operation);
1003
+ if (delivery === "steer" && result) {
1004
+ keepSteering = true;
1005
+ setSteering({
1006
+ phase: "accepted",
1007
+ text: sendText,
1008
+ clientEventId: input.clientEventId ?? null,
1009
+ triggerEventId: result.accepted.id,
1010
+ turnId: result.turn.id,
1011
+ });
1012
+ }
817
1013
  } catch (cause) {
818
1014
  if (!isOutcomeUnknownError(cause)) {
819
1015
  clearPending();
1016
+ } else if (delivery === "steer") {
1017
+ keepSteering = true;
820
1018
  }
821
1019
  if (
822
1020
  targetKeyRef.current === ownedTargetKey &&
@@ -840,6 +1038,7 @@ export function useComposer(
840
1038
  targetGeneration.current === ownedGeneration
841
1039
  ) {
842
1040
  setSending(false);
1041
+ if (delivery === "steer" && !keepSteering) setSteering(null);
843
1042
  }
844
1043
  }
845
1044
  },
@@ -1095,6 +1294,7 @@ export function useComposer(
1095
1294
  hasDraftContent,
1096
1295
  send,
1097
1296
  steer,
1297
+ steering: identityMatches ? steering : null,
1098
1298
  sending: identityMatches ? sending : false,
1099
1299
  canSend:
1100
1300
  identityMatches &&
@@ -1245,7 +1445,7 @@ function mergeResources(base: ResourceRef[], additions: ResourceRef[]): Resource
1245
1445
  // different duplicate counts after server normalization.
1246
1446
  const key =
1247
1447
  resource.kind === "file"
1248
- ? `file:${resource.fileId}\u0000${resource.mountPath ?? `files/${resource.fileId}`}`
1448
+ ? `file:${resource.fileId}\u0000${resource.mountPath ?? `${DEFAULT_FILE_RESOURCE_MOUNT_ROOT}/${resource.fileId}`}`
1249
1449
  : JSON.stringify(resource);
1250
1450
  if (seen.has(key)) return false;
1251
1451
  seen.add(key);
package/src/index.ts CHANGED
@@ -256,6 +256,8 @@ export type {
256
256
  HumanInputFormMessages,
257
257
  HumanInputFormProps,
258
258
  } from "./components/human-input-form";
259
+ export { HumanInputSurface } from "./components/human-input-surface";
260
+ export type { HumanInputSurfaceProps } from "./components/human-input-surface";
259
261
 
260
262
  // Timeline projection
261
263
  export {
@@ -393,6 +395,19 @@ export { defaultChatComposerMessages } from "./components/composer";
393
395
  export type { ChatComposerMessages } from "./components/composer";
394
396
  export { ModelPicker } from "./components/model-picker";
395
397
  export type { ModelPickerProps } from "./components/model-picker";
398
+ export {
399
+ BillingClassMark,
400
+ ModelPolicyPicker,
401
+ ModelPolicyPickerMenu,
402
+ PickerAnimatedPage,
403
+ PickerBackHeader,
404
+ PickerNavRow,
405
+ defaultModelPolicyPickerMessages,
406
+ } from "./components/model-policy-picker";
407
+ export type {
408
+ ModelPolicyPickerMessages,
409
+ ModelPolicyPickerProps,
410
+ } from "./components/model-policy-picker";
396
411
  export {
397
412
  advancedSourceSummary,
398
413
  availabilityReasonLabel,
@@ -404,7 +419,9 @@ export {
404
419
  findPickerRow,
405
420
  groupPickerRowsByBillingClass,
406
421
  labelLatencyMode,
422
+ labelReasoningEffort,
407
423
  payerSummaryForModel,
424
+ projectClientModelRows,
408
425
  projectPickerRows,
409
426
  runnableLatencyModesForModel,
410
427
  sortPickerRows,
@@ -430,6 +447,8 @@ export { SandboxTerminal } from "./components/sandbox-terminal";
430
447
  export type { SandboxTerminalProps, XtermTheme } from "./components/sandbox-terminal";
431
448
  export { FileBrowser } from "./components/file-browser";
432
449
  export type { FileBrowserProps } from "./components/file-browser";
450
+ export { WorkbenchChanges } from "./components/workbench-changes";
451
+ export type { WorkbenchChangesProps } from "./components/workbench-changes";
433
452
  export { DiffView } from "./components/diff-view";
434
453
  export type { DiffViewProps, DiffTheme } from "./components/diff-view";
435
454
  export { PierreDiff } from "./components/pierre-diff";
@@ -2,7 +2,7 @@ import type { ClientModel, ReasoningEffort, WorkspaceModelCatalogModel } from "@
2
2
 
3
3
  export type PickerBillingClass = "opengeni_credits" | "codex_subscription" | "byok";
4
4
 
5
- export type PickerModelRow = {
5
+ export type PickerModelRow<TCatalog extends ClientModel = WorkspaceModelCatalogModel> = {
6
6
  id: string;
7
7
  label: string;
8
8
  billingClass: PickerBillingClass;
@@ -11,7 +11,7 @@ export type PickerModelRow = {
11
11
  unavailableReason: string | null;
12
12
  provider: string;
13
13
  providerLabel: string;
14
- catalog: WorkspaceModelCatalogModel;
14
+ catalog: TCatalog;
15
15
  };
16
16
 
17
17
  export type LatencyModeId = "standard" | "priority" | "fast";
@@ -78,7 +78,7 @@ export function effortOptionsForModel(model: ClientModel): ReasoningEffort[] {
78
78
  if (!efforts || efforts.length === 0) {
79
79
  return ["low"];
80
80
  }
81
- const order: ReasoningEffort[] = ["none", "minimal", "low", "medium", "high", "xhigh"];
81
+ const order: ReasoningEffort[] = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
82
82
  return order.filter((effort) => efforts.includes(effort));
83
83
  }
84
84
 
@@ -117,6 +117,13 @@ export function labelLatencyMode(mode: LatencyModeId): string {
117
117
  return "Standard";
118
118
  }
119
119
 
120
+ export function labelReasoningEffort(effort: ReasoningEffort): string {
121
+ if (effort === "xhigh") {
122
+ return "Extra high";
123
+ }
124
+ return effort.slice(0, 1).toUpperCase() + effort.slice(1);
125
+ }
126
+
120
127
  export function payerSummaryForModel(model: ClientModel): string {
121
128
  const billing = model.billing;
122
129
  if (!billing) {
@@ -177,7 +184,27 @@ export function projectPickerRows(models: WorkspaceModelCatalogModel[]): PickerM
177
184
  });
178
185
  }
179
186
 
180
- export function sortPickerRows(rows: PickerModelRow[]): PickerModelRow[] {
187
+ /** Project the lightweight client-config model list into the same picker contract. */
188
+ export function projectClientModelRows(models: ClientModel[]): PickerModelRow<ClientModel>[] {
189
+ return models.map((catalog) => {
190
+ const billingClass = billingClassForModel(catalog);
191
+ return {
192
+ id: catalog.id,
193
+ label: catalog.label,
194
+ billingClass,
195
+ billingClassLabel: billingClassLabel(billingClass),
196
+ selectable: true,
197
+ unavailableReason: null,
198
+ provider: catalog.provider,
199
+ providerLabel: catalog.providerLabel,
200
+ catalog,
201
+ };
202
+ });
203
+ }
204
+
205
+ export function sortPickerRows<TCatalog extends ClientModel>(
206
+ rows: PickerModelRow<TCatalog>[],
207
+ ): PickerModelRow<TCatalog>[] {
181
208
  return [...rows].sort((left, right) => {
182
209
  const classDelta =
183
210
  BILLING_CLASS_ORDER.indexOf(left.billingClass) -
@@ -192,18 +219,35 @@ export function sortPickerRows(rows: PickerModelRow[]): PickerModelRow[] {
192
219
  });
193
220
  }
194
221
 
195
- export function findPickerRow(rows: PickerModelRow[], modelId: string): PickerModelRow | null {
222
+ export function findPickerRow<TCatalog extends ClientModel>(
223
+ rows: PickerModelRow<TCatalog>[],
224
+ modelId: string,
225
+ ): PickerModelRow<TCatalog> | null {
196
226
  return rows.find((row) => row.id === modelId) ?? null;
197
227
  }
198
228
 
199
229
  export function groupPickerRowsByBillingClass(
200
230
  rows: PickerModelRow[],
201
- ): Array<{ billingClass: PickerBillingClass; label: string; rows: PickerModelRow[] }> {
231
+ ): Array<{ billingClass: PickerBillingClass; label: string; rows: PickerModelRow[] }>;
232
+ export function groupPickerRowsByBillingClass<TCatalog extends ClientModel>(
233
+ rows: PickerModelRow<TCatalog>[],
234
+ ): Array<{
235
+ billingClass: PickerBillingClass;
236
+ label: string;
237
+ rows: PickerModelRow<TCatalog>[];
238
+ }>;
239
+ export function groupPickerRowsByBillingClass<TCatalog extends ClientModel>(
240
+ rows: PickerModelRow<TCatalog>[],
241
+ ): Array<{
242
+ billingClass: PickerBillingClass;
243
+ label: string;
244
+ rows: PickerModelRow<TCatalog>[];
245
+ }> {
202
246
  const sorted = sortPickerRows(rows);
203
247
  const groups: Array<{
204
248
  billingClass: PickerBillingClass;
205
249
  label: string;
206
- rows: PickerModelRow[];
250
+ rows: PickerModelRow<TCatalog>[];
207
251
  }> = [];
208
252
  for (const row of sorted) {
209
253
  const existing = groups.find((group) => group.billingClass === row.billingClass);
@@ -0,0 +1,123 @@
1
+ "use client";
2
+
3
+ import { ChevronRightIcon } from "lucide-react";
4
+ import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
5
+ import type { ComponentProps } from "react";
6
+ import { cn } from "../lib/cn";
7
+
8
+ function DropdownMenu(props: ComponentProps<typeof DropdownMenuPrimitive.Root>) {
9
+ return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
10
+ }
11
+
12
+ function DropdownMenuTrigger(props: ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
13
+ return <DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
14
+ }
15
+
16
+ function DropdownMenuContent({
17
+ className,
18
+ sideOffset = 4,
19
+ ...props
20
+ }: ComponentProps<typeof DropdownMenuPrimitive.Content>) {
21
+ return (
22
+ <DropdownMenuPrimitive.Portal>
23
+ <DropdownMenuPrimitive.Content
24
+ data-slot="dropdown-menu-content"
25
+ sideOffset={sideOffset}
26
+ className={cn(
27
+ "z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
28
+ className,
29
+ )}
30
+ {...props}
31
+ />
32
+ </DropdownMenuPrimitive.Portal>
33
+ );
34
+ }
35
+
36
+ function DropdownMenuItem({
37
+ className,
38
+ inset,
39
+ variant = "default",
40
+ ...props
41
+ }: ComponentProps<typeof DropdownMenuPrimitive.Item> & {
42
+ inset?: boolean;
43
+ variant?: "default" | "destructive";
44
+ }) {
45
+ return (
46
+ <DropdownMenuPrimitive.Item
47
+ data-slot="dropdown-menu-item"
48
+ data-inset={inset}
49
+ data-variant={variant}
50
+ className={cn(
51
+ "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
52
+ className,
53
+ )}
54
+ {...props}
55
+ />
56
+ );
57
+ }
58
+
59
+ function DropdownMenuSeparator({
60
+ className,
61
+ ...props
62
+ }: ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
63
+ return (
64
+ <DropdownMenuPrimitive.Separator
65
+ data-slot="dropdown-menu-separator"
66
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
67
+ {...props}
68
+ />
69
+ );
70
+ }
71
+
72
+ function DropdownMenuSub(props: ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
73
+ return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
74
+ }
75
+
76
+ function DropdownMenuSubTrigger({
77
+ className,
78
+ inset,
79
+ children,
80
+ ...props
81
+ }: ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & { inset?: boolean }) {
82
+ return (
83
+ <DropdownMenuPrimitive.SubTrigger
84
+ data-slot="dropdown-menu-sub-trigger"
85
+ data-inset={inset}
86
+ className={cn(
87
+ "flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
88
+ className,
89
+ )}
90
+ {...props}
91
+ >
92
+ {children}
93
+ <ChevronRightIcon className="ml-auto size-4" />
94
+ </DropdownMenuPrimitive.SubTrigger>
95
+ );
96
+ }
97
+
98
+ function DropdownMenuSubContent({
99
+ className,
100
+ ...props
101
+ }: ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
102
+ return (
103
+ <DropdownMenuPrimitive.SubContent
104
+ data-slot="dropdown-menu-sub-content"
105
+ className={cn(
106
+ "z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
107
+ className,
108
+ )}
109
+ {...props}
110
+ />
111
+ );
112
+ }
113
+
114
+ export {
115
+ DropdownMenu,
116
+ DropdownMenuContent,
117
+ DropdownMenuItem,
118
+ DropdownMenuSeparator,
119
+ DropdownMenuSub,
120
+ DropdownMenuSubContent,
121
+ DropdownMenuSubTrigger,
122
+ DropdownMenuTrigger,
123
+ };