@opengeni/react 5.0.5-canary.3 → 5.2.0-canary.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 (81) hide show
  1. package/README.md +13 -1
  2. package/dist/artifacts.d.ts +2 -0
  3. package/dist/artifacts.js +1391 -2
  4. package/dist/artifacts.js.map +1 -1
  5. package/dist/chunk-2TKI3WEG.js +73 -0
  6. package/dist/chunk-2TKI3WEG.js.map +1 -0
  7. package/dist/{chunk-V3WX3TQG.js → chunk-45XEUVTK.js} +2 -2
  8. package/dist/{chunk-M2HNWZYR.js → chunk-IHQII5WO.js} +69 -18
  9. package/dist/chunk-IHQII5WO.js.map +1 -0
  10. package/dist/{chunk-HUMIRRYI.js → chunk-IYCR4343.js} +19 -19
  11. package/dist/{chunk-HUMIRRYI.js.map → chunk-IYCR4343.js.map} +1 -1
  12. package/dist/{chunk-6J56T3MG.js → chunk-NYESIADM.js} +14 -5
  13. package/dist/chunk-NYESIADM.js.map +1 -0
  14. package/dist/{chunk-7S2KWUJD.js → chunk-QXJCNVSD.js} +2283 -2215
  15. package/dist/chunk-QXJCNVSD.js.map +1 -0
  16. package/dist/{chunk-BV5A4OTE.js → chunk-T5FRXMC3.js} +21 -2
  17. package/dist/{chunk-BV5A4OTE.js.map → chunk-T5FRXMC3.js.map} +1 -1
  18. package/dist/{chunk-BRFMGY4L.js → chunk-UGMKPUEM.js} +28 -3
  19. package/dist/chunk-UGMKPUEM.js.map +1 -0
  20. package/dist/components/artifacts/inline-html-document.d.ts +2 -0
  21. package/dist/components/artifacts/published-html-artifact-frame.d.ts +2 -0
  22. package/dist/components/artifacts/visualization-assets.gen.d.ts +2 -0
  23. package/dist/components/markdown.d.ts +13 -1
  24. package/dist/components/message-timeline.d.ts +4 -1
  25. package/dist/components/session-conversation.d.ts +3 -0
  26. package/dist/components/user-message-body.d.ts +10 -1
  27. package/dist/composer.js +2 -2
  28. package/dist/{computer-viewer-7TEZC6Y2.js → computer-viewer-AA4JH2AP.js} +2 -2
  29. package/dist/connect.js +1 -1
  30. package/dist/index.d.ts +3 -2
  31. package/dist/index.js +9 -6
  32. package/dist/index.js.map +1 -1
  33. package/dist/interaction.js +1 -1
  34. package/dist/{platform-activity-row-27LKAWZD.js → platform-activity-row-UW4UVVM4.js} +6 -3
  35. package/dist/platform-activity-row-UW4UVVM4.js.map +1 -0
  36. package/dist/session-ui.d.ts +1 -1
  37. package/dist/session-ui.js +6 -5
  38. package/dist/session-ui.js.map +1 -1
  39. package/dist/session.js +2 -2
  40. package/dist/sites-ui.d.ts +2 -1
  41. package/dist/sites.js +12 -4
  42. package/dist/sites.js.map +1 -1
  43. package/dist/timeline/activity-rail.d.ts +1 -0
  44. package/dist/timeline/platform-activity-row.d.ts +1 -1
  45. package/dist/timeline/retained-image.d.ts +16 -0
  46. package/dist/timeline/rolling-activity.d.ts +9 -0
  47. package/dist/timeline/shared.d.ts +4 -1
  48. package/dist/timeline/turn-summary.d.ts +4 -1
  49. package/package.json +4 -4
  50. package/src/artifacts.ts +2 -0
  51. package/src/components/artifacts/inline-html-document.ts +24 -0
  52. package/src/components/artifacts/published-html-artifact-frame.tsx +32 -2
  53. package/src/components/artifacts/visualization-assets/base.css +1015 -0
  54. package/src/components/artifacts/visualization-assets/helpers.html +349 -0
  55. package/src/components/artifacts/visualization-assets.gen.ts +3 -0
  56. package/src/components/copy-button.tsx +7 -2
  57. package/src/components/markdown.tsx +111 -34
  58. package/src/components/message-timeline.tsx +61 -4
  59. package/src/components/session-conversation.tsx +5 -0
  60. package/src/components/user-message-body.tsx +20 -2
  61. package/src/hooks/use-composer.ts +15 -2
  62. package/src/hooks/use-computer-session.ts +36 -1
  63. package/src/index.ts +10 -2
  64. package/src/session-ui.ts +4 -1
  65. package/src/sites-ui.tsx +23 -6
  66. package/src/timeline/activity-rail.tsx +20 -3
  67. package/src/timeline/platform-activity-row.tsx +5 -2
  68. package/src/timeline/retained-image.ts +91 -0
  69. package/src/timeline/rolling-activity.tsx +83 -0
  70. package/src/timeline/shared.tsx +33 -12
  71. package/src/timeline/tool-renderers.tsx +7 -91
  72. package/src/timeline/turn-summary.tsx +21 -15
  73. package/styles/compiled.css +122 -0
  74. package/styles/index.css +49 -0
  75. package/dist/chunk-6J56T3MG.js.map +0 -1
  76. package/dist/chunk-7S2KWUJD.js.map +0 -1
  77. package/dist/chunk-BRFMGY4L.js.map +0 -1
  78. package/dist/chunk-M2HNWZYR.js.map +0 -1
  79. package/dist/platform-activity-row-27LKAWZD.js.map +0 -1
  80. /package/dist/{chunk-V3WX3TQG.js.map → chunk-45XEUVTK.js.map} +0 -0
  81. /package/dist/{computer-viewer-7TEZC6Y2.js.map → computer-viewer-AA4JH2AP.js.map} +0 -0
@@ -1,3 +1,4 @@
1
+ import { RollingActivity } from "../timeline/rolling-activity";
1
2
  import { GenieLoadingOptionsContext, type GenieLoadingOptions } from "../timeline/genie-loading";
2
3
  import { ChildSessionLink } from "./child-session-link";
3
4
  import { useStartupDetails } from "../timeline/startup-preference";
@@ -62,6 +63,7 @@ import {
62
63
  UserMessageBody,
63
64
  UserMessageDisclosureProvider,
64
65
  type UserMessageDisclosureContextValue,
66
+ type UserMessageDisclosureLabels,
65
67
  } from "./user-message-body";
66
68
  import {
67
69
  createTipFollowState,
@@ -128,6 +130,8 @@ const TimelineAnnotationSelection = lazy(() => import("./timeline-annotation-sel
128
130
  const TimelineAnnotationMarkers = lazy(() => import("./timeline-annotation-markers"));
129
131
 
130
132
  export type MessageTimelineProps = {
133
+ /** Localized user-message disclosure actions, including custom UserMessageBody renderers. */
134
+ userMessageDisclosureLabels?: UserMessageDisclosureLabels | undefined;
131
135
  /** Raw session events (projected internally) … */
132
136
  events?: SessionEvent[] | undefined;
133
137
  /** … or pre-projected items (e.g. from `useSessionEvents().timeline`). */
@@ -404,6 +408,7 @@ function cssEscapeAttribute(value: string): string {
404
408
  * with a "jump to latest" affordance when the reader scrolls back.
405
409
  */
406
410
  export function MessageTimeline({
411
+ userMessageDisclosureLabels,
407
412
  events,
408
413
  items,
409
414
  status: _status,
@@ -919,8 +924,16 @@ export function MessageTimeline({
919
924
  () => ({
920
925
  expandedByMessageId: userMessageDisclosureMemoryRef.current,
921
926
  beginChange: beginUserMessageDisclosureChange,
927
+ labels: {
928
+ showMore: userMessageDisclosureLabels?.showMore,
929
+ showLess: userMessageDisclosureLabels?.showLess,
930
+ },
922
931
  }),
923
- [beginUserMessageDisclosureChange],
932
+ [
933
+ beginUserMessageDisclosureChange,
934
+ userMessageDisclosureLabels?.showMore,
935
+ userMessageDisclosureLabels?.showLess,
936
+ ],
924
937
  );
925
938
  const timelineGroupEntryContext = useMemo<TimelineGroupEntryContext>(
926
939
  () => ({
@@ -2514,6 +2527,12 @@ const TimelineGroupView = memo(function TimelineGroupView({
2514
2527
  const startupDetails = useStartupDetails();
2515
2528
  const enter = useEntranceAnimation();
2516
2529
  const settleChrome = useTurnSettleOpen();
2530
+ const previousSingleActivity = useRef<ActivityItem | undefined>(undefined);
2531
+ useLayoutEffect(() => {
2532
+ const work =
2533
+ group.kind === "activity" ? group.items.filter((item) => item.kind !== "startup-phase") : [];
2534
+ previousSingleActivity.current = work.length === 1 ? work[0] : undefined;
2535
+ }, [group]);
2517
2536
  const foldMemory = useFoldMemory();
2518
2537
  // Settled (or live-fold) activity clusters get a chip. Inside an expanded
2519
2538
  // turn that is the second layer — quiet nested chips under the outer turn
@@ -2540,7 +2559,8 @@ const TimelineGroupView = memo(function TimelineGroupView({
2540
2559
  );
2541
2560
  }
2542
2561
  const settleFold =
2543
- group.kind === "turn" ? !!(enter && !insideTurn && !turnDefaultOpen) : liveActivitySettle;
2562
+ !turnSummary?.rolling &&
2563
+ (group.kind === "turn" ? !!(enter && !insideTurn && !turnDefaultOpen) : liveActivitySettle);
2544
2564
  switch (group.kind) {
2545
2565
  case "activity":
2546
2566
  // Preparation is one quiet surface, not a fold with eight technical steps.
@@ -2566,6 +2586,24 @@ const TimelineGroupView = memo(function TimelineGroupView({
2566
2586
  />
2567
2587
  );
2568
2588
  }
2589
+ if (
2590
+ turnSummary?.rolling &&
2591
+ !startupDetails &&
2592
+ group.items.filter((item) => item.kind !== "startup-phase").length === 1
2593
+ ) {
2594
+ return (
2595
+ <ActivityRail
2596
+ items={group.items}
2597
+ startupActive={false}
2598
+ bare
2599
+ toolRegistry={toolRegistry}
2600
+ onOpenSession={onOpenSession}
2601
+ onMemoryClick={onMemoryClick}
2602
+ loadRetainedScreenshot={loadRetainedScreenshot}
2603
+ loadRetainedArtifact={loadRetainedArtifact}
2604
+ />
2605
+ );
2606
+ }
2569
2607
  if (insideTurn) {
2570
2608
  // Nested chips whenever the parent has ≥2 clusters. During outer settle
2571
2609
  // chrome they stay force-open so structure is visible and height stays
@@ -2577,7 +2615,13 @@ const TimelineGroupView = memo(function TimelineGroupView({
2577
2615
  // settled closed pre-wrap was showing as a CHIP, so mounting it closed
2578
2616
  // is both the stable height and the honest state — force-opening it
2579
2617
  // was the "already-collapsed cluster auto-expands at the end" reopen.
2580
- const useNestedChip = nestClusterChips && activityShouldFold && !containsGeneratedImage;
2618
+ const visibleActivity = group.items.filter((item) => item.kind !== "startup-phase");
2619
+ const singleThought =
2620
+ !startupDetails &&
2621
+ visibleActivity.length === 1 &&
2622
+ visibleActivity[0]?.kind === "reasoning";
2623
+ const useNestedChip =
2624
+ nestClusterChips && activityShouldFold && !containsGeneratedImage && !singleThought;
2581
2625
  if (!useNestedChip) {
2582
2626
  return (
2583
2627
  <ActivityRail
@@ -2622,7 +2666,20 @@ const TimelineGroupView = memo(function TimelineGroupView({
2622
2666
  items={group.items}
2623
2667
  outcome={group.outcome}
2624
2668
  failureText={group.failureText}
2625
- defaultOpen={!activityShouldFold || group.outcome === "failed" ? true : undefined}
2669
+ defaultOpen={
2670
+ group.outcome === "failed" || (!turnSummary?.rolling && !activityShouldFold)
2671
+ ? true
2672
+ : undefined
2673
+ }
2674
+ liveHeader={
2675
+ turnSummary?.rolling && !group.outcome && !foldLiveCluster ? (
2676
+ <RollingActivity
2677
+ items={group.items}
2678
+ toolRegistry={toolRegistry}
2679
+ previousItem={previousSingleActivity.current}
2680
+ />
2681
+ ) : undefined
2682
+ }
2626
2683
  foldKey={group.id}
2627
2684
  facets={turnSummary?.facets}
2628
2685
  settleFold={settleFold}
@@ -11,11 +11,14 @@ import { ChatComposer, type ChatComposerProps } from "./chat-composer";
11
11
  import { QueueSurface } from "./queue-surface";
12
12
  import { HumanInputSurface, type HumanInputSurfaceProps } from "./human-input-surface";
13
13
  import { MessageTimeline } from "./message-timeline";
14
+ import type { UserMessageDisclosureLabels } from "./user-message-body";
14
15
  import { conversationTimeline } from "../conversation-timeline";
15
16
  import { cn } from "../lib/cn";
16
17
 
17
18
  export type SessionConversationProps = ClientOverride & {
18
19
  sessionId: string;
20
+ /** Localized actions for already-sent user-message disclosure. */
21
+ userMessageDisclosureLabels?: UserMessageDisclosureLabels | undefined;
19
22
  loadSkillReview?: HumanInputSurfaceProps["loadSkillReview"];
20
23
  className?: string;
21
24
  /** Defaults to filling the host. The host owns available height. */
@@ -32,6 +35,7 @@ export function SessionConversation(props: SessionConversationProps) {
32
35
 
33
36
  function Conversation({
34
37
  sessionId,
38
+ userMessageDisclosureLabels,
35
39
  loadSkillReview,
36
40
  client,
37
41
  workspaceId,
@@ -71,6 +75,7 @@ function Conversation({
71
75
  >
72
76
  {error && <p role="alert">{error.message}</p>}
73
77
  <MessageTimeline
78
+ userMessageDisclosureLabels={userMessageDisclosureLabels}
74
79
  className="min-h-0 flex-1"
75
80
  items={conversationTimeline(feed.timeline, queue, composer)}
76
81
  status={status}
@@ -190,7 +190,15 @@ function observeSharedResize(element: Element, callback: () => void): (() => voi
190
190
  };
191
191
  }
192
192
 
193
+ export type UserMessageDisclosureLabels = {
194
+ /** Collapsed disclosure action. Defaults to "Show more". */
195
+ showMore?: string | undefined;
196
+ /** Expanded disclosure action. Defaults to "Show less". */
197
+ showLess?: string | undefined;
198
+ };
199
+
193
200
  export type UserMessageDisclosureContextValue = {
201
+ labels?: UserMessageDisclosureLabels | undefined;
194
202
  expandedByMessageId: Map<string, boolean>;
195
203
  beginChange: (
196
204
  messageBody: HTMLElement,
@@ -235,6 +243,8 @@ export type UserMessageBodyProps = {
235
243
  text: string;
236
244
  children: ReactNode;
237
245
  className?: string | undefined;
246
+ /** Per-field overrides for timeline labels, then the English defaults. */
247
+ disclosureLabels?: UserMessageDisclosureLabels | undefined;
238
248
  };
239
249
 
240
250
  /**
@@ -247,7 +257,13 @@ export type UserMessageBodyProps = {
247
257
  * remembers expansion per durable message id and preserves the reader's scroll
248
258
  * anchor when height changes.
249
259
  */
250
- export function UserMessageBody({ messageId, text, children, className }: UserMessageBodyProps) {
260
+ export function UserMessageBody({
261
+ messageId,
262
+ text,
263
+ children,
264
+ className,
265
+ disclosureLabels,
266
+ }: UserMessageBodyProps) {
251
267
  const disclosure = useContext(UserMessageDisclosureContext);
252
268
  const [expanded, setExpanded] = useState(
253
269
  () => disclosure?.expandedByMessageId.get(messageId) ?? false,
@@ -454,7 +470,9 @@ export function UserMessageBody({ messageId, text, children, className }: UserMe
454
470
  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-hidden 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"
455
471
  onClick={(event) => toggle(event.currentTarget)}
456
472
  >
457
- {expanded ? "Show less" : "Show more"}
473
+ {expanded
474
+ ? (disclosureLabels?.showLess ?? disclosure?.labels?.showLess ?? "Show less")
475
+ : (disclosureLabels?.showMore ?? disclosure?.labels?.showMore ?? "Show more")}
458
476
  </button>
459
477
  </div>
460
478
  );
@@ -677,6 +677,8 @@ export function useComposer(
677
677
  const [acceptedControl, setAcceptedControl] = useState<EffectiveSessionControl | null>(null);
678
678
  const [resuming, setResuming] = useState(false);
679
679
  const [error, setError] = useState<Error | null>(null);
680
+ // Background draft reads recover independently of Send/Steer/control failures.
681
+ const [draftReadError, setDraftReadError] = useState<Error | null>(null);
680
682
  const [draft, setDraft] = useState<ComposerDraft | null>(null);
681
683
  const [draftLoading, setDraftLoading] = useState(Boolean(sessionId) && durableDrafts);
682
684
  const [draftSaving, setDraftSaving] = useState(false);
@@ -794,6 +796,7 @@ export function useComposer(
794
796
  setAcceptedControl(null);
795
797
  setResuming(false);
796
798
  setError(null);
799
+ setDraftReadError(null);
797
800
  setDraft(null);
798
801
  setDraftLoading(Boolean(sessionId) && durableDrafts);
799
802
  setDraftSaving(false);
@@ -920,6 +923,7 @@ export function useComposer(
920
923
  if (targetKeyRef.current !== targetKey) return;
921
924
  if (!sessionId || !durableDrafts) {
922
925
  setDraftLoading(false);
926
+ setDraftReadError(null);
923
927
  return;
924
928
  }
925
929
  let retry = draftReadRetryRef.current;
@@ -993,6 +997,8 @@ export function useComposer(
993
997
  if (retry.timer !== null) clearTimeout(retry.timer);
994
998
  retry.failures = 0;
995
999
  retry.timer = null;
1000
+ // Recovery also counts when the server returns the same draft revision.
1001
+ setDraftReadError(null);
996
1002
  const currentRevision = draftRef.current?.revision ?? -1;
997
1003
  // Reconnect and client-generation effects can legitimately ask for the
998
1004
  // draft again. A same-revision response is not new authority: publishing
@@ -1045,12 +1051,18 @@ export function useComposer(
1045
1051
  }
1046
1052
  } catch (cause) {
1047
1053
  if (
1054
+ !requestAbort.signal.aborted &&
1048
1055
  generation === targetGeneration.current &&
1049
1056
  targetKeyRef.current === targetKey &&
1050
1057
  readTicket === draftReadGeneration.current
1051
1058
  ) {
1052
- setError(asError(cause));
1059
+ const problem = asError(cause);
1060
+ const timedOut = problem.name === "TimeoutError";
1061
+ setDraftReadError(
1062
+ timedOut ? new Error("Draft sync timed out. Retrying…", { cause }) : problem,
1063
+ );
1053
1064
  if (
1065
+ timedOut ||
1054
1066
  cause instanceof TypeError ||
1055
1067
  (cause && typeof cause === "object" && "retryable" in cause && cause.retryable === true)
1056
1068
  ) {
@@ -2639,6 +2651,7 @@ export function useComposer(
2639
2651
  const clearError = useCallback(() => {
2640
2652
  if (targetKeyRef.current !== targetKey) return;
2641
2653
  setError(null);
2654
+ setDraftReadError(null);
2642
2655
  setDraftConflict(null);
2643
2656
  }, [targetKey]);
2644
2657
  // `valueRef` is the synchronous composer authority. React state exists to
@@ -2706,7 +2719,7 @@ export function useComposer(
2706
2719
  resolveDraftConflict,
2707
2720
  restoredResources: identityMatches ? restoredResources : [],
2708
2721
  removeRestoredResource,
2709
- error: identityMatches ? error : null,
2722
+ error: identityMatches ? (error ?? draftReadError) : null,
2710
2723
  clearError,
2711
2724
  };
2712
2725
  }
@@ -117,6 +117,33 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
117
117
  if (!mountedRef.current || requestRef.current.id !== id) return;
118
118
  const targets = sortComputerTargets(targetResponse.targets);
119
119
  const selected = chooseTarget(targets, selectedTargetIdRef.current, session.platform);
120
+ // Discovery is useful independently of semantic observation. Publish it
121
+ // now so a slow/unresponsive application cannot block the frame stream
122
+ // or prevent the person from choosing a different window or screen.
123
+ const previous = observationRef.current.observation;
124
+ const retainedObservation =
125
+ selected &&
126
+ previous?.target.id === selected.id &&
127
+ previous.target.controllerGeneration === selected.controllerGeneration &&
128
+ previous.target.targetGeneration === selected.targetGeneration
129
+ ? previous
130
+ : null;
131
+ selectedTargetIdRef.current = selected?.id ?? null;
132
+ targetsRef.current = { computerSessionId, targets };
133
+ observationRef.current = { computerSessionId, observation: retainedObservation };
134
+ setState((current) =>
135
+ current.computerSessionId === computerSessionId
136
+ ? {
137
+ ...current,
138
+ session,
139
+ targets,
140
+ selectedTargetId: selected?.id ?? null,
141
+ observation: retainedObservation,
142
+ loading: false,
143
+ error: null,
144
+ }
145
+ : current,
146
+ );
120
147
  const observation = selected
121
148
  ? await client.observeComputerTarget(workspaceId, computerSessionId, selected.id, {
122
149
  signal: controller.signal,
@@ -303,7 +330,15 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
303
330
  ? (targetsRef.current.targets.find((candidate) => candidate.id === frame.targetId) ??
304
331
  (currentObservation?.target.id === frame.targetId ? currentObservation.target : null))
305
332
  : null;
306
- const target = frameTarget ?? focusTarget ?? currentObservation?.target ?? null;
333
+ const inputTarget =
334
+ (action.type === "keyboard" || action.type === "clipboard") &&
335
+ targetsRef.current.computerSessionId === computerSessionId
336
+ ? targetsRef.current.targets.find(
337
+ (candidate) => candidate.id === selectedTargetIdRef.current,
338
+ )
339
+ : null;
340
+ const target =
341
+ frameTarget ?? focusTarget ?? currentObservation?.target ?? inputTarget ?? null;
307
342
  if (!target) throw new Error("The desktop target is not ready for input.");
308
343
  if (frame && frame.targetId !== selectedTargetIdRef.current) {
309
344
  throw new Error("The displayed desktop frame is no longer selected.");
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export { retainedImageId } from "./components/markdown";
1
2
  // oxlint-disable-next-line typescript/triple-slash-reference -- package consumers must load the ambient type for this optional untyped peer without emitting a runtime import.
2
3
  /// <reference path="./types/external.d.ts" />
3
4
 
@@ -511,13 +512,20 @@ export { GeneratedVideoPlayer } from "./components/generated-video-player";
511
512
  export type { GeneratedVideoPlayerProps } from "./components/generated-video-player";
512
513
  export type { MessageTimelineProps } from "./components/message-timeline";
513
514
  export { UserMessageBody, userMessageLikelyNeedsDisclosure } from "./components/user-message-body";
514
- export type { UserMessageBodyProps } from "./components/user-message-body";
515
+ export type {
516
+ UserMessageBodyProps,
517
+ UserMessageDisclosureLabels,
518
+ } from "./components/user-message-body";
515
519
  export {
516
520
  Markdown,
517
521
  sandboxFileLocationFromHref,
518
522
  sandboxFilePathFromHref,
519
523
  } from "./components/markdown";
520
- export type { MarkdownProps, SandboxFileLocation } from "./components/markdown";
524
+ export type {
525
+ MarkdownProps,
526
+ MarkdownInteractiveBlock,
527
+ SandboxFileLocation,
528
+ } from "./components/markdown";
521
529
  export { CopyButton, CopyHoverFrame } from "./components/copy-button";
522
530
  export { copyTextToClipboard, tableElementToTsv } from "./lib/clipboard";
523
531
  export { SessionStatus, StatusDot, SESSION_STATUS_META } from "./components/session-status";
package/src/session-ui.ts CHANGED
@@ -16,7 +16,10 @@ export type { MessageTimelineProps } from "./components/message-timeline";
16
16
  export { createOlderHistoryLoadReceipt } from "./older-history";
17
17
  export type { OlderHistoryLoader, OlderHistoryLoadReceipt } from "./older-history";
18
18
  export { UserMessageBody, userMessageLikelyNeedsDisclosure } from "./components/user-message-body";
19
- export type { UserMessageBodyProps } from "./components/user-message-body";
19
+ export type {
20
+ UserMessageBodyProps,
21
+ UserMessageDisclosureLabels,
22
+ } from "./components/user-message-body";
20
23
  export { BUILT_IN_TURN_SUMMARY_FACET_IDS } from "./timeline/turn-summary";
21
24
  export type {
22
25
  BuiltInTurnSummaryFacetId,
package/src/sites-ui.tsx CHANGED
@@ -29,10 +29,11 @@ type SiteScope = { client: SiteClient; workspaceId: string; className?: string }
29
29
  /** Shared native/embedded read boundary: pin content to the observed version,
30
30
  * and never accept a response belonging to another workspace or Site. */
31
31
  export async function loadSiteSnapshot(
32
- client: Pick<SiteClient, "getWorkspaceArtifact" | "getWorkspaceArtifactHtml">,
32
+ client: Pick<SiteClient, "getWorkspaceArtifact" | "getWorkspaceArtifactHtml"> &
33
+ Partial<Pick<OpenGeniClient, "getWorkspaceArtifactContent">>,
33
34
  workspaceId: string,
34
35
  siteId: string,
35
- options: { signal?: AbortSignal; includeArchivedContent?: boolean } = {},
36
+ options: { signal?: AbortSignal; includeArchivedContent?: boolean; versionId?: string } = {},
36
37
  ): Promise<{
37
38
  detail: WorkspaceArtifactDetailResponse;
38
39
  content: SiteDisplayContent | null;
@@ -42,19 +43,35 @@ export async function loadSiteSnapshot(
42
43
  options.signal?.throwIfAborted();
43
44
  if (detail.artifact.id !== siteId || detail.artifact.workspaceId !== workspaceId)
44
45
  throw new Error("Site scope mismatch");
45
- const versionId = detail.artifact.currentVersion?.id;
46
- const content =
47
- versionId && (detail.artifact.status === "active" || options.includeArchivedContent)
46
+ const selectedVersion = options.versionId
47
+ ? detail.versions.find((version) => version.id === options.versionId)
48
+ : detail.artifact.currentVersion;
49
+ const versionId = options.versionId ?? selectedVersion?.id;
50
+ let content: SiteDisplayContent | null =
51
+ selectedVersion &&
52
+ versionId &&
53
+ (detail.artifact.status === "active" || options.includeArchivedContent)
48
54
  ? {
49
55
  artifactId: siteId,
50
56
  versionId,
51
- requestedTools: detail.artifact.currentVersion!.requestedTools,
57
+ requestedTools: selectedVersion!.requestedTools,
52
58
  html: await client.getWorkspaceArtifactHtml(workspaceId, siteId, {
53
59
  ...requestOptions,
54
60
  versionId,
55
61
  }),
56
62
  }
57
63
  : null;
64
+ if (
65
+ versionId &&
66
+ !selectedVersion &&
67
+ (detail.artifact.status === "active" || options.includeArchivedContent)
68
+ ) {
69
+ if (!client.getWorkspaceArtifactContent) throw new Error("Site version unavailable");
70
+ content = await client.getWorkspaceArtifactContent(workspaceId, siteId, {
71
+ ...requestOptions,
72
+ versionId,
73
+ });
74
+ }
58
75
  options.signal?.throwIfAborted();
59
76
  if (content && (content.artifactId !== siteId || content.versionId !== versionId))
60
77
  throw new Error("Site content mismatch");
@@ -2,7 +2,7 @@ import { AnimatePresence, motion, useReducedMotion } from "motion/react";
2
2
  import { GenieLoading } from "./genie-loading";
3
3
  import { useStartupDetails } from "./startup-preference";
4
4
  import { ArrowRightIcon, BotIcon, BrainCircuitIcon } from "lucide-react";
5
- import { lazy, Suspense, useLayoutEffect, useRef, useState } from "react";
5
+ import { lazy, Suspense, useContext, useLayoutEffect, useRef, useState } from "react";
6
6
  import { jsx as rowJsx, jsxs as rowJsxs } from "react/jsx-runtime";
7
7
  import { Markdown } from "../components/markdown";
8
8
  import { cn } from "../lib/cn";
@@ -11,7 +11,13 @@ import { defaultToolRegistry } from "./tool-renderers";
11
11
  import { useEntranceAnimation, useEntranceAnimationLive } from "./entrance";
12
12
  import type { RetainedArtifactLoader, RetainedScreenshotLoader, ToolRegistry } from "./registry";
13
13
  import { useSeenActivityIds } from "./seen-activity-ids";
14
- import { BodyNote, PayloadBlock, ActivityDisclosure, ToolCallTruncationProvider } from "./shared";
14
+ import {
15
+ BodyNote,
16
+ PayloadBlock,
17
+ ActivityDisclosure,
18
+ CompactActivityContext,
19
+ ToolCallTruncationProvider,
20
+ } from "./shared";
15
21
  import { toolDisplayName } from "./tool-display-name";
16
22
  import type { ActivityItem, MemoryItem, WorkerItem } from "./types";
17
23
 
@@ -214,7 +220,7 @@ function assertNever(item: never): never {
214
220
  throw new Error(`ActivityRail: unhandled activity item ${JSON.stringify(item)}`);
215
221
  }
216
222
 
217
- function renderActivity(
223
+ export function renderActivity(
218
224
  item: ActivityItem,
219
225
  toolRegistry: ToolRegistry,
220
226
  onOpenSession: ((sessionId: string) => void) | undefined,
@@ -380,6 +386,7 @@ function WorkerRow({
380
386
  item: WorkerItem;
381
387
  onOpenSession?: ((sessionId: string) => void) | undefined;
382
388
  }) {
389
+ const compact = useContext(CompactActivityContext);
383
390
  const running = item.status === "running";
384
391
  const failed = item.status === "failed";
385
392
  const cancelled = item.status === "cancelled";
@@ -399,6 +406,16 @@ function WorkerRow({
399
406
  : cancelled
400
407
  ? "Worker interrupted"
401
408
  : "Worker messaged";
409
+ if (compact) {
410
+ return (
411
+ <ActivityDisclosure
412
+ icon={<BotIcon className="size-3.5" />}
413
+ title={title}
414
+ preview={item.prompt}
415
+ running={running}
416
+ />
417
+ );
418
+ }
402
419
  // A worker is a first-class actor but still a STEP on the rail — a borderless
403
420
  // row (no card), aligned to its sibling tool rows: the chevron column is an
404
421
  // empty spacer (a worker doesn't expand), then the bot glyph, then the title
@@ -10,7 +10,6 @@ export default function PlatformActivityRow({
10
10
  t: displayName,
11
11
  b: BotIcon,
12
12
  m: Markdown,
13
- r: truncate,
14
13
  j,
15
14
  s,
16
15
  }: {
@@ -35,7 +34,11 @@ export default function PlatformActivityRow({
35
34
  children: "Thought",
36
35
  }),
37
36
  running: item.streaming,
38
- preview: truncate(item.text, 110),
37
+ compactPreview: item.text.replace(/\*\*|__|`/g, "").replace(/\s+/g, " "),
38
+ preview: j("span", {
39
+ className: "og-reasoning-preview",
40
+ children: j(Markdown, { streaming: item.streaming, children: item.text }),
41
+ }),
39
42
  children: j("div", {
40
43
  className: "text-og-base leading-6 text-og-fg-muted [&_strong]:text-og-fg-muted",
41
44
  children: j(Markdown, { streaming: item.streaming, children: item.text }),
@@ -0,0 +1,91 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import type { RetainedArtifactReference } from "@opengeni/sdk";
3
+ import type { ToolRendererProps } from "./registry";
4
+ type RetainedImageState =
5
+ | { kind: "loading" }
6
+ | { kind: "ready"; url: string }
7
+ | { kind: "unavailable"; label: string }
8
+ | { kind: "error"; message: string };
9
+
10
+ export function useRetainedImageObjectUrl(
11
+ artifact: RetainedArtifactReference,
12
+ load: ToolRendererProps["loadRetainedArtifact"],
13
+ ): RetainedImageState {
14
+ const [state, setState] = useState<RetainedImageState>({ kind: "loading" });
15
+ // Function outputs are commonly serialized JSON. Parsing them creates a new
16
+ // object on every render, so depend on the immutable wire value rather than
17
+ // object identity; otherwise the loader can refetch after its own setState.
18
+ const artifactValue = retainedArtifactValue(artifact);
19
+ const stableArtifactRef = useRef({ value: artifactValue, artifact });
20
+ if (stableArtifactRef.current.value !== artifactValue) {
21
+ stableArtifactRef.current = { value: artifactValue, artifact };
22
+ }
23
+ const stableArtifact = stableArtifactRef.current.artifact;
24
+ useEffect(() => {
25
+ if (!load) {
26
+ setState({ kind: "unavailable", label: "retrieval is not configured" });
27
+ return;
28
+ }
29
+ const controller = new AbortController();
30
+ let objectUrl: string | null = null;
31
+ setState({ kind: "loading" });
32
+ void load(stableArtifact, controller.signal)
33
+ .then((source) => {
34
+ if (controller.signal.aborted) return;
35
+ if (!source) {
36
+ setState({ kind: "unavailable", label: "bytes are unavailable" });
37
+ return;
38
+ }
39
+ if (!(source instanceof Uint8Array)) {
40
+ if (!source.url.trim()) {
41
+ setState({ kind: "unavailable", label: "URL is unavailable" });
42
+ return;
43
+ }
44
+ setState({ kind: "ready", url: source.url });
45
+ return;
46
+ }
47
+ objectUrl = URL.createObjectURL(
48
+ new Blob([source as unknown as BlobPart], { type: stableArtifact.contentType }),
49
+ );
50
+ setState({ kind: "ready", url: objectUrl });
51
+ })
52
+ .catch((error: unknown) => {
53
+ if (controller.signal.aborted) return;
54
+ const status =
55
+ error && typeof error === "object" && "status" in error
56
+ ? Number((error as { status?: unknown }).status)
57
+ : null;
58
+ setState(
59
+ status === 404
60
+ ? { kind: "unavailable", label: "deleted" }
61
+ : status === 410
62
+ ? { kind: "unavailable", label: "expired or unavailable" }
63
+ : { kind: "error", message: "retrieval failed" },
64
+ );
65
+ });
66
+ return () => {
67
+ controller.abort();
68
+ if (objectUrl) URL.revokeObjectURL(objectUrl);
69
+ };
70
+ }, [stableArtifact, load]);
71
+ return state;
72
+ }
73
+
74
+ function retainedArtifactValue(artifact: RetainedArtifactReference): string {
75
+ return [
76
+ artifact.artifactId,
77
+ artifact.kind,
78
+ artifact.contentType,
79
+ artifact.originalBytes,
80
+ artifact.sha256,
81
+ artifact.retainedAt,
82
+ artifact.dimensions?.width ?? "",
83
+ artifact.dimensions?.height ?? "",
84
+ artifact.retention.policy,
85
+ artifact.retention.expiresAt ?? "",
86
+ artifact.retrieval.method,
87
+ artifact.retrieval.path,
88
+ artifact.retrieval.acceptRanges,
89
+ artifact.retrieval.maxRangeBytes,
90
+ ].join("\0");
91
+ }