@opengeni/react 5.1.0-canary.0 → 5.2.0-canary.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +12 -0
  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-M2HNWZYR.js → chunk-IHQII5WO.js} +69 -18
  8. package/dist/chunk-IHQII5WO.js.map +1 -0
  9. package/dist/{chunk-5PQJOCX5.js → chunk-KANFU34B.js} +6 -4
  10. package/dist/chunk-KANFU34B.js.map +1 -0
  11. package/dist/{chunk-WS6D5BHH.js → chunk-O4NQNMPG.js} +2393 -2123
  12. package/dist/chunk-O4NQNMPG.js.map +1 -0
  13. package/dist/{chunk-HUMIRRYI.js → chunk-PZ27MCZP.js} +54 -19
  14. package/dist/chunk-PZ27MCZP.js.map +1 -0
  15. package/dist/{chunk-BRFMGY4L.js → chunk-UGMKPUEM.js} +28 -3
  16. package/dist/chunk-UGMKPUEM.js.map +1 -0
  17. package/dist/{chunk-BV5A4OTE.js → chunk-YS24CFDI.js} +44 -10
  18. package/dist/chunk-YS24CFDI.js.map +1 -0
  19. package/dist/components/artifacts/inline-html-document.d.ts +2 -0
  20. package/dist/components/artifacts/published-html-artifact-frame.d.ts +2 -0
  21. package/dist/components/artifacts/visualization-assets.gen.d.ts +2 -0
  22. package/dist/components/markdown.d.ts +13 -1
  23. package/dist/components/session-chrome.d.ts +6 -1
  24. package/dist/{computer-viewer-7TEZC6Y2.js → computer-viewer-CTFCJVS7.js} +2 -2
  25. package/dist/connect.js +1 -1
  26. package/dist/hooks/use-file-attachments.d.ts +2 -0
  27. package/dist/index.d.ts +6 -1
  28. package/dist/index.js +16 -5
  29. package/dist/index.js.map +1 -1
  30. package/dist/interaction.js +1 -1
  31. package/dist/{platform-activity-row-27LKAWZD.js → platform-activity-row-DUE2W74J.js} +6 -3
  32. package/dist/platform-activity-row-DUE2W74J.js.map +1 -0
  33. package/dist/session-ui.d.ts +2 -0
  34. package/dist/session-ui.js +7 -107
  35. package/dist/session-ui.js.map +1 -1
  36. package/dist/session.js +2 -2
  37. package/dist/sites-ui.d.ts +2 -1
  38. package/dist/sites.js +12 -4
  39. package/dist/sites.js.map +1 -1
  40. package/dist/timeline/activity-rail.d.ts +1 -0
  41. package/dist/timeline/genie-loading.d.ts +9 -1
  42. package/dist/timeline/index.d.ts +3 -1
  43. package/dist/timeline/knowledge-receipt.d.ts +17 -0
  44. package/dist/timeline/platform-activity-row.d.ts +1 -2
  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/dist/timeline/types.d.ts +13 -2
  50. package/package.json +4 -4
  51. package/src/artifacts.ts +2 -0
  52. package/src/components/artifacts/inline-html-document.ts +24 -0
  53. package/src/components/artifacts/published-html-artifact-frame.tsx +32 -2
  54. package/src/components/artifacts/visualization-assets/base.css +1015 -0
  55. package/src/components/artifacts/visualization-assets/helpers.html +349 -0
  56. package/src/components/artifacts/visualization-assets.gen.ts +3 -0
  57. package/src/components/computer-viewer.tsx +11 -3
  58. package/src/components/copy-button.tsx +7 -2
  59. package/src/components/markdown.tsx +111 -34
  60. package/src/components/message-timeline.tsx +48 -3
  61. package/src/components/session-chrome.tsx +14 -2
  62. package/src/hooks/use-computer-frame-stream.ts +17 -5
  63. package/src/hooks/use-computer-session.ts +36 -1
  64. package/src/hooks/use-file-attachments.ts +4 -1
  65. package/src/hooks/use-goal.ts +3 -1
  66. package/src/index.ts +12 -1
  67. package/src/session-ui.ts +3 -0
  68. package/src/sites-ui.tsx +23 -6
  69. package/src/timeline/activity-rail.tsx +31 -4
  70. package/src/timeline/genie-loading.tsx +18 -4
  71. package/src/timeline/index.ts +4 -0
  72. package/src/timeline/knowledge-receipt.tsx +100 -0
  73. package/src/timeline/platform-activity-row.tsx +5 -3
  74. package/src/timeline/projection.ts +40 -0
  75. package/src/timeline/retained-image.ts +91 -0
  76. package/src/timeline/rolling-activity.tsx +83 -0
  77. package/src/timeline/shared.tsx +33 -12
  78. package/src/timeline/tool-renderers.tsx +55 -91
  79. package/src/timeline/turn-summary.tsx +21 -15
  80. package/src/timeline/types.ts +14 -0
  81. package/styles/compiled.css +122 -0
  82. package/styles/index.css +49 -0
  83. package/dist/chunk-5PQJOCX5.js.map +0 -1
  84. package/dist/chunk-BRFMGY4L.js.map +0 -1
  85. package/dist/chunk-BV5A4OTE.js.map +0 -1
  86. package/dist/chunk-HUMIRRYI.js.map +0 -1
  87. package/dist/chunk-M2HNWZYR.js.map +0 -1
  88. package/dist/chunk-WS6D5BHH.js.map +0 -1
  89. package/dist/platform-activity-row-27LKAWZD.js.map +0 -1
  90. /package/dist/{computer-viewer-7TEZC6Y2.js.map → computer-viewer-CTFCJVS7.js.map} +0 -0
@@ -1,5 +1,7 @@
1
1
  import {
2
2
  Children,
3
+ createContext,
4
+ useContext,
3
5
  isValidElement,
4
6
  memo,
5
7
  useEffect,
@@ -13,6 +15,7 @@ import {
13
15
  import ReactMarkdown, {
14
16
  defaultUrlTransform,
15
17
  type Components,
18
+ type ExtraProps,
16
19
  type UrlTransform,
17
20
  } from "react-markdown";
18
21
  import remarkGfm from "remark-gfm";
@@ -40,7 +43,17 @@ import { TooltipProvider } from "./tooltip";
40
43
  * the tokens land. Consumers who want a different renderer can still pass
41
44
  * `renderMessageText` to `MessageTimeline` to override this entirely.
42
45
  */
46
+ export type MarkdownInteractiveBlock = { kind: "html" | "site"; content: string };
47
+ const InteractiveContext = createContext<{
48
+ source: string;
49
+ render?: (block: MarkdownInteractiveBlock) => ReactNode;
50
+ renderImage?: (image: { src: string; alt: string }) => ReactNode;
51
+ }>({ source: "" });
52
+
43
53
  export type MarkdownProps = {
54
+ renderImage?: ((image: { src: string; alt: string }) => ReactNode) | undefined;
55
+ /** Host opt-in for assistant-authored interactive fences. */
56
+ renderInteractiveBlock?: ((block: MarkdownInteractiveBlock) => ReactNode) | undefined;
44
57
  children: string;
45
58
  className?: string | undefined;
46
59
  /**
@@ -171,7 +184,7 @@ const baseComponents: Components = {
171
184
  </code>
172
185
  ),
173
186
  // Fenced code — quiet mono block (no card / no nested vertical scroll).
174
- pre: ({ children }) => <MarkdownCodeBlock>{children}</MarkdownCodeBlock>,
187
+ pre: InteractiveCodeBlock,
175
188
  // Tables stay unboxed (hairline rules); hover reveals a TSV copy control.
176
189
  table: ({ children, ...props }) => <MarkdownTable {...props}>{children}</MarkdownTable>,
177
190
  thead: ({ children, ...props }) => <thead {...props}>{children}</thead>,
@@ -191,19 +204,7 @@ const baseComponents: Components = {
191
204
  {children}
192
205
  </td>
193
206
  ),
194
- img: ({ alt, src, ...props }) =>
195
- src ? (
196
- <img
197
- alt={alt ?? ""}
198
- src={src}
199
- className="my-3 max-w-full rounded-og-md border border-og-border"
200
- {...props}
201
- />
202
- ) : alt ? (
203
- <span className="text-og-fg-subtle" aria-disabled="true">
204
- {alt}
205
- </span>
206
- ) : null,
207
+ img: MarkdownImage,
207
208
  };
208
209
 
209
210
  const MARKDOWN_LINK_CLASS =
@@ -343,8 +344,30 @@ export function sandboxFilePathFromHref(href: string | undefined): string | null
343
344
  return sandboxFileLocationFromHref(href)?.path ?? null;
344
345
  }
345
346
 
347
+ export function retainedImageId(src: string): string | null {
348
+ return (
349
+ /^artifact:([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i
350
+ .exec(src)?.[1]
351
+ ?.toLowerCase() ?? null
352
+ );
353
+ }
354
+ function MarkdownImage({ src, alt }: ComponentPropsWithoutRef<"img">) {
355
+ const { renderImage } = useContext(InteractiveContext);
356
+ if (!src) return <span>{alt ?? "Image unavailable"}</span>;
357
+ if (retainedImageId(src))
358
+ return renderImage ? (
359
+ <>{renderImage({ src, alt: alt ?? "" })}</>
360
+ ) : (
361
+ <span>{alt || "Image"} (preview unavailable)</span>
362
+ );
363
+ return <img src={src} alt={alt ?? ""} loading="lazy" className="my-3 max-w-full rounded-og-md" />;
364
+ }
365
+
346
366
  const markdownUrlTransform: UrlTransform = (url, key, node) =>
347
- key === "href" && node.tagName === "a" && isSandboxHref(url) ? url : defaultUrlTransform(url);
367
+ (key === "href" && node.tagName === "a" && isSandboxHref(url)) ||
368
+ (key === "src" && node.tagName === "img" && retainedImageId(url))
369
+ ? url
370
+ : defaultUrlTransform(url);
348
371
 
349
372
  /** How long after the stream ends the reveal pipeline stays for trailing animations. */
350
373
  /** Trailing ink window after stream end — keep ≥ {@link INK_FADE_MS}. */
@@ -382,6 +405,42 @@ function fenceLanguage(children: ReactNode): string | null {
382
405
  return found;
383
406
  }
384
407
 
408
+ function InteractiveCodeBlock({ children, node }: ComponentPropsWithoutRef<"pre"> & ExtraProps) {
409
+ const { source, render } = useContext(InteractiveContext);
410
+ const language = fenceLanguage(children);
411
+ if (render && (language === "opengeni-html" || language === "opengeni-site")) {
412
+ const start = node?.position?.start.offset;
413
+ const end = node?.position?.end.offset;
414
+ const original =
415
+ start !== undefined && end !== undefined && end <= source.length
416
+ ? source.slice(start, end)
417
+ : "";
418
+ const lines = original.trimEnd().split("\n");
419
+ const opening = lines[0]?.match(/^ {0,3}([\x60]{3,}|~{3,})/);
420
+
421
+ const closing = lines
422
+ .at(-1)
423
+ ?.replace(/^\s*(?:>\s*)+/, "")
424
+ .trim();
425
+ const complete =
426
+ opening &&
427
+ lines.length > 1 &&
428
+ closing &&
429
+ closing.length >= opening[1]!.length &&
430
+ [...closing].every((c) => c === opening[1]![0]);
431
+ if (!complete) return <p role="status">Preparing preview…</p>;
432
+ return (
433
+ <>
434
+ {render({
435
+ kind: language === "opengeni-html" ? "html" : "site",
436
+ content: nodeText(children).replace(/\n$/, ""),
437
+ })}
438
+ </>
439
+ );
440
+ }
441
+ return <MarkdownCodeBlock>{children}</MarkdownCodeBlock>;
442
+ }
443
+
385
444
  function MarkdownCodeBlock({ children }: { children?: ReactNode }) {
386
445
  const code = nodeText(children).replace(/\n$/, "");
387
446
  const language = fenceLanguage(children);
@@ -462,7 +521,14 @@ function MarkdownTable({ children, className, ...props }: ComponentPropsWithoutR
462
521
  );
463
522
  }
464
523
 
465
- function MarkdownImpl({ children, className, streaming = false, onSandboxFile }: MarkdownProps) {
524
+ function MarkdownImpl({
525
+ children,
526
+ className,
527
+ streaming = false,
528
+ onSandboxFile,
529
+ renderInteractiveBlock,
530
+ renderImage,
531
+ }: MarkdownProps) {
466
532
  // Tip-ink engine for THIS body: created on the first streaming render, kept
467
533
  // through a short linger after the stream ends (so the last age window can
468
534
  // finish), then dropped so settled bodies pay zero cost. Observing during
@@ -553,28 +619,39 @@ function MarkdownImpl({ children, className, streaming = false, onSandboxFile }:
553
619
  const parseText = streaming || revealActive ? softenStreamingMarkdown(children) : children;
554
620
  const components = useMemo(() => markdownComponents(onSandboxFile), [onSandboxFile]);
555
621
 
622
+ const interactiveContext = useMemo(
623
+ () => ({
624
+ source: children,
625
+ ...(renderInteractiveBlock ? { render: renderInteractiveBlock } : {}),
626
+ ...(renderImage ? { renderImage } : {}),
627
+ }),
628
+ [children, renderInteractiveBlock, renderImage],
629
+ );
630
+
556
631
  // `min-w-0` lets the prose shrink inside flex parents (message bubbles) so
557
632
  // long links and code blocks wrap/scroll instead of forcing overflow.
558
633
  return (
559
- <TooltipProvider delayDuration={400}>
560
- <div
561
- ref={bodyRef}
562
- className={cn(
563
- "og-markdown-body min-w-0 break-words",
564
- settling && "og-markdown-settle",
565
- className,
566
- )}
567
- >
568
- <ReactMarkdown
569
- remarkPlugins={[remarkGfm]}
570
- rehypePlugins={rehypePlugins}
571
- components={components}
572
- urlTransform={markdownUrlTransform}
634
+ <InteractiveContext.Provider value={interactiveContext}>
635
+ <TooltipProvider delayDuration={400}>
636
+ <div
637
+ ref={bodyRef}
638
+ className={cn(
639
+ "og-markdown-body min-w-0 break-words",
640
+ settling && "og-markdown-settle",
641
+ className,
642
+ )}
573
643
  >
574
- {parseText}
575
- </ReactMarkdown>
576
- </div>
577
- </TooltipProvider>
644
+ <ReactMarkdown
645
+ remarkPlugins={[remarkGfm]}
646
+ rehypePlugins={rehypePlugins}
647
+ components={components}
648
+ urlTransform={markdownUrlTransform}
649
+ >
650
+ {parseText}
651
+ </ReactMarkdown>
652
+ </div>
653
+ </TooltipProvider>
654
+ </InteractiveContext.Provider>
578
655
  );
579
656
  }
580
657
 
@@ -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";
@@ -2526,6 +2527,12 @@ const TimelineGroupView = memo(function TimelineGroupView({
2526
2527
  const startupDetails = useStartupDetails();
2527
2528
  const enter = useEntranceAnimation();
2528
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]);
2529
2536
  const foldMemory = useFoldMemory();
2530
2537
  // Settled (or live-fold) activity clusters get a chip. Inside an expanded
2531
2538
  // turn that is the second layer — quiet nested chips under the outer turn
@@ -2552,7 +2559,8 @@ const TimelineGroupView = memo(function TimelineGroupView({
2552
2559
  );
2553
2560
  }
2554
2561
  const settleFold =
2555
- group.kind === "turn" ? !!(enter && !insideTurn && !turnDefaultOpen) : liveActivitySettle;
2562
+ !turnSummary?.rolling &&
2563
+ (group.kind === "turn" ? !!(enter && !insideTurn && !turnDefaultOpen) : liveActivitySettle);
2556
2564
  switch (group.kind) {
2557
2565
  case "activity":
2558
2566
  // Preparation is one quiet surface, not a fold with eight technical steps.
@@ -2578,6 +2586,24 @@ const TimelineGroupView = memo(function TimelineGroupView({
2578
2586
  />
2579
2587
  );
2580
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
+ }
2581
2607
  if (insideTurn) {
2582
2608
  // Nested chips whenever the parent has ≥2 clusters. During outer settle
2583
2609
  // chrome they stay force-open so structure is visible and height stays
@@ -2589,7 +2615,13 @@ const TimelineGroupView = memo(function TimelineGroupView({
2589
2615
  // settled closed pre-wrap was showing as a CHIP, so mounting it closed
2590
2616
  // is both the stable height and the honest state — force-opening it
2591
2617
  // was the "already-collapsed cluster auto-expands at the end" reopen.
2592
- 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;
2593
2625
  if (!useNestedChip) {
2594
2626
  return (
2595
2627
  <ActivityRail
@@ -2634,7 +2666,20 @@ const TimelineGroupView = memo(function TimelineGroupView({
2634
2666
  items={group.items}
2635
2667
  outcome={group.outcome}
2636
2668
  failureText={group.failureText}
2637
- 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
+ }
2638
2683
  foldKey={group.id}
2639
2684
  facets={turnSummary?.facets}
2640
2685
  settleFold={settleFold}
@@ -100,6 +100,11 @@ export type SessionChromeProps = {
100
100
  queue: UseTurnQueueResult;
101
101
  /** Needed for queue edit → composer checkout. Omit with `readOnly`. */
102
102
  composer?: ComposerState | undefined;
103
+ /** Focus the composer after a successful queue checkout has applied its draft.
104
+ * Connect to the composer controller's `focusInput`, or a custom input ref.
105
+ * Never called for failed checkout or while replacement awaits confirmation.
106
+ */
107
+ onComposerFocus?: (() => void) | undefined;
103
108
  goal?: UseGoalResult | null | undefined;
104
109
  /** Expanded agents body (host supplies tree / list). */
105
110
  agentsPanel?: ReactNode;
@@ -431,6 +436,7 @@ export function SessionChrome({
431
436
  sessionStatus,
432
437
  queue,
433
438
  composer,
439
+ onComposerFocus,
434
440
  goal,
435
441
  agentsPanel,
436
442
  commandsPanel,
@@ -802,7 +808,10 @@ export function SessionChrome({
802
808
  expectedDraftRevision: composer.draftRevision,
803
809
  replaceDraft: true,
804
810
  });
805
- if (checkedOut) composer.applyDraft(checkedOut);
811
+ if (checkedOut) {
812
+ composer.applyDraft(checkedOut);
813
+ onComposerFocus?.();
814
+ }
806
815
  })();
807
816
  }
808
817
  : undefined
@@ -819,7 +828,10 @@ export function SessionChrome({
819
828
  expectedDraftRevision: composer.draftRevision,
820
829
  replaceDraft: false,
821
830
  });
822
- if (checkedOut) composer.applyDraft(checkedOut);
831
+ if (checkedOut) {
832
+ composer.applyDraft(checkedOut);
833
+ onComposerFocus?.();
834
+ }
823
835
  })();
824
836
  },
825
837
  );
@@ -159,9 +159,19 @@ export function useComputerFrameStream(
159
159
  pendingFrame = null;
160
160
  };
161
161
 
162
+ const clearRecoveryTimers = () => {
163
+ if (reconnectTimer) clearTimeout(reconnectTimer);
164
+ if (expiryTimer) clearTimeout(expiryTimer);
165
+ reconnectTimer = null;
166
+ expiryTimer = null;
167
+ };
168
+
162
169
  const scheduleReconnect = () => {
163
170
  if (disposed || reconnectTimer) return;
164
- if (failures >= MAX_AUTOMATIC_RECONNECTS_WITHOUT_A_FRAME) return;
171
+ if (failures >= MAX_AUTOMATIC_RECONNECTS_WITHOUT_A_FRAME) {
172
+ clearRecoveryTimers();
173
+ return;
174
+ }
165
175
  const delay = Math.min(5_000, 250 * 2 ** Math.min(failures, 5));
166
176
  failures += 1;
167
177
  setResult((current) => ({ ...current, state: "reconnecting" }));
@@ -177,6 +187,7 @@ export function useComputerFrameStream(
177
187
  setResult((current) => ({ ...current, state: "error", error }));
178
188
  clearSocket();
179
189
  if (reconnectAutomatically) scheduleReconnect();
190
+ else clearRecoveryTimers();
180
191
  };
181
192
 
182
193
  const onOpen = (source: ComputerFrameWebSocket) => {
@@ -295,8 +306,7 @@ export function useComputerFrameStream(
295
306
 
296
307
  const onClose = (source: ComputerFrameWebSocket) => {
297
308
  if (disposed || source !== socket) return;
298
- clearSocket();
299
- scheduleReconnect();
309
+ fail(new Error("Desktop view lost connection."));
300
310
  };
301
311
 
302
312
  const connectSocket = () => {
@@ -366,6 +376,9 @@ export function useComputerFrameStream(
366
376
  throw new Error("desktop attachment does not match the requested resource");
367
377
  }
368
378
  controllerGeneration = attachment.controllerGeneration;
379
+ // A fresh attachment owns a new producer whose sequence can restart.
380
+ // Transport reconnects reuse the attachment and retain their sequence fence.
381
+ latestRef.current = { key: "", sequence: -1 };
369
382
  activeAttachment = attachment;
370
383
  activeStream = attachment.stream;
371
384
  attachmentExpiresAt = Date.parse(attachment.expiresAt);
@@ -406,8 +419,7 @@ export function useComputerFrameStream(
406
419
  disposed = true;
407
420
  pendingFrame = null;
408
421
  attachmentAbort.abort();
409
- if (reconnectTimer) clearTimeout(reconnectTimer);
410
- if (expiryTimer) clearTimeout(expiryTimer);
422
+ clearRecoveryTimers();
411
423
  clearSocket(true);
412
424
  };
413
425
  }, [client, enabled, nonce, options.computerSessionId, options.targetId, workspaceId]);
@@ -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.");
@@ -13,6 +13,8 @@ export type UseFileAttachmentsOptions = EmbeddedFileAttachmentClientOverride & {
13
13
  * UseFileAttachmentsResult.addFiles} (the explicit picker / drop path)
14
14
  * bypasses it.
15
15
  */
16
+ /** Retain original uploads in the chat owner’s personal scope when private. */
17
+ scope?: "workspace" | "personal";
16
18
  pasteFilter?: ((file: File) => boolean) | undefined;
17
19
  };
18
20
 
@@ -212,6 +214,7 @@ export function useFileAttachments(
212
214
  filename: file.name || "file",
213
215
  contentType: file.type || "application/octet-stream",
214
216
  data: file,
217
+ ...(options.scope ? { scope: options.scope } : {}),
215
218
  })
216
219
  .then((asset) => {
217
220
  if (scopeGeneration.current !== generation) return;
@@ -252,7 +255,7 @@ export function useFileAttachments(
252
255
  );
253
256
  });
254
257
  },
255
- [client, workspaceId],
258
+ [client, workspaceId, options.scope],
256
259
  );
257
260
 
258
261
  const addFiles = useCallback(
@@ -138,7 +138,9 @@ export function useGoal(
138
138
  return;
139
139
  }
140
140
  if (sharedFeed) {
141
- setLoading(false);
141
+ // A shared log supplies invalidations, not the authoritative goal snapshot.
142
+ setLoading(true);
143
+ void load();
142
144
  return () => {
143
145
  retireLoads();
144
146
  };
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
 
@@ -520,7 +521,11 @@ export {
520
521
  sandboxFileLocationFromHref,
521
522
  sandboxFilePathFromHref,
522
523
  } from "./components/markdown";
523
- export type { MarkdownProps, SandboxFileLocation } from "./components/markdown";
524
+ export type {
525
+ MarkdownProps,
526
+ MarkdownInteractiveBlock,
527
+ SandboxFileLocation,
528
+ } from "./components/markdown";
524
529
  export { CopyButton, CopyHoverFrame } from "./components/copy-button";
525
530
  export { copyTextToClipboard, tableElementToTsv } from "./lib/clipboard";
526
531
  export { SessionStatus, StatusDot, SESSION_STATUS_META } from "./components/session-status";
@@ -612,3 +617,9 @@ export {
612
617
  tryParseJson,
613
618
  } from "./lib/format";
614
619
  export { SessionCommandsPanel } from "./components/session-commands-panel";
620
+
621
+ export { KnowledgeActivityProvider } from "./timeline/knowledge-receipt";
622
+ export type { KnowledgeActivityActions } from "./timeline/knowledge-receipt";
623
+
624
+ export { StartupTimings } from "./timeline/startup-timings";
625
+ export { setStartupDetails, useStartupDetails } from "./timeline/startup-preference";
package/src/session-ui.ts CHANGED
@@ -46,6 +46,9 @@ export type {
46
46
  SessionChromeSignalTone,
47
47
  } from "./components/session-chrome";
48
48
  export { SessionCommandsPanel } from "./components/session-commands-panel";
49
+
50
+ export { KnowledgeActivityProvider } from "./timeline/knowledge-receipt";
51
+ export type { KnowledgeActivityActions } from "./timeline/knowledge-receipt";
49
52
  export { StartupTimings } from "./timeline/startup-timings";
50
53
  export { useStartupDetails, setStartupDetails } from "./timeline/startup-preference";
51
54
  export type { GenieLoadingOptions } from "./timeline/genie-loading";
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");
@@ -1,8 +1,9 @@
1
+ import { KnowledgeReceiptRow } from "./knowledge-receipt";
1
2
  import { AnimatePresence, motion, useReducedMotion } from "motion/react";
2
3
  import { GenieLoading } from "./genie-loading";
3
4
  import { useStartupDetails } from "./startup-preference";
4
5
  import { ArrowRightIcon, BotIcon, BrainCircuitIcon } from "lucide-react";
5
- import { lazy, Suspense, useLayoutEffect, useRef, useState } from "react";
6
+ import { lazy, Suspense, useContext, useLayoutEffect, useRef, useState } from "react";
6
7
  import { jsx as rowJsx, jsxs as rowJsxs } from "react/jsx-runtime";
7
8
  import { Markdown } from "../components/markdown";
8
9
  import { cn } from "../lib/cn";
@@ -11,7 +12,13 @@ import { defaultToolRegistry } from "./tool-renderers";
11
12
  import { useEntranceAnimation, useEntranceAnimationLive } from "./entrance";
12
13
  import type { RetainedArtifactLoader, RetainedScreenshotLoader, ToolRegistry } from "./registry";
13
14
  import { useSeenActivityIds } from "./seen-activity-ids";
14
- import { BodyNote, PayloadBlock, ActivityDisclosure, ToolCallTruncationProvider } from "./shared";
15
+ import {
16
+ BodyNote,
17
+ PayloadBlock,
18
+ ActivityDisclosure,
19
+ CompactActivityContext,
20
+ ToolCallTruncationProvider,
21
+ } from "./shared";
15
22
  import { toolDisplayName } from "./tool-display-name";
16
23
  import type { ActivityItem, MemoryItem, WorkerItem } from "./types";
17
24
 
@@ -214,7 +221,7 @@ function assertNever(item: never): never {
214
221
  throw new Error(`ActivityRail: unhandled activity item ${JSON.stringify(item)}`);
215
222
  }
216
223
 
217
- function renderActivity(
224
+ export function renderActivity(
218
225
  item: ActivityItem,
219
226
  toolRegistry: ToolRegistry,
220
227
  onOpenSession: ((sessionId: string) => void) | undefined,
@@ -235,7 +242,6 @@ function renderActivity(
235
242
  t={toolDisplayName}
236
243
  b={BotIcon}
237
244
  m={Markdown}
238
- r={truncate}
239
245
  j={rowJsx}
240
246
  s={rowJsxs}
241
247
  />
@@ -255,6 +261,16 @@ function renderActivity(
255
261
  }
256
262
  case "worker":
257
263
  return <WorkerRow item={item} onOpenSession={onOpenSession} />;
264
+ case "knowledge":
265
+ return (
266
+ <KnowledgeReceiptRow
267
+ outcome={item.outcome}
268
+ entryId={item.entryId}
269
+ fileId={item.fileId}
270
+ title={item.filename}
271
+ source={Boolean(item.fileId)}
272
+ />
273
+ );
258
274
  case "memory":
259
275
  return <MemoryRow item={item} onMemoryClick={onMemoryClick} />;
260
276
  case "fleet-decision":
@@ -380,6 +396,7 @@ function WorkerRow({
380
396
  item: WorkerItem;
381
397
  onOpenSession?: ((sessionId: string) => void) | undefined;
382
398
  }) {
399
+ const compact = useContext(CompactActivityContext);
383
400
  const running = item.status === "running";
384
401
  const failed = item.status === "failed";
385
402
  const cancelled = item.status === "cancelled";
@@ -399,6 +416,16 @@ function WorkerRow({
399
416
  : cancelled
400
417
  ? "Worker interrupted"
401
418
  : "Worker messaged";
419
+ if (compact) {
420
+ return (
421
+ <ActivityDisclosure
422
+ icon={<BotIcon className="size-3.5" />}
423
+ title={title}
424
+ preview={item.prompt}
425
+ running={running}
426
+ />
427
+ );
428
+ }
402
429
  // A worker is a first-class actor but still a STEP on the rail — a borderless
403
430
  // row (no card), aligned to its sibling tool rows: the chevron column is an
404
431
  // empty spacer (a worker doesn't expand), then the bot glyph, then the title