@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
@@ -12,7 +12,15 @@ export type GenieLoadingOptions = {
12
12
  /** Replace the visual while preserving SDK loading visibility and transitions. */
13
13
  render?: (props: GenieLoadingRenderProps) => ReactNode;
14
14
  phrases?: readonly string[];
15
- /** Public options stay independent of the renderer's declaration layout. */
15
+ /** Native copy overrides. Omitted messages retain the default English copy. */
16
+ messages?: {
17
+ status?: string;
18
+ slowStatus?: string;
19
+ slowText?: string;
20
+ showDetails?: string;
21
+ hideDetails?: string;
22
+ };
23
+ /** Public adapter options; the renderer dependency's declarations stay private. */
16
24
  orb?: {
17
25
  state?:
18
26
  | "working"
@@ -90,10 +98,14 @@ export function GenieLoading({
90
98
  </div>
91
99
  <div className="og-genie-copy">
92
100
  <span className="sr-only" role="status">
93
- {slow ? "Preparing your task. Taking longer than usual." : "Preparing your task."}
101
+ {slow
102
+ ? (options?.messages?.slowStatus ?? "Preparing your task. Taking longer than usual.")
103
+ : (options?.messages?.status ?? "Preparing your task.")}
94
104
  </span>
95
105
  <span key={slow ? "slow" : phrase} className="og-genie-phrase" aria-hidden="true">
96
- {slow ? "A little longer than usual…" : phrases[phrase % phrases.length]}
106
+ {slow
107
+ ? (options?.messages?.slowText ?? "A little longer than usual…")
108
+ : phrases[phrase % phrases.length]}
97
109
  </span>
98
110
  {showDetails || detailsOpen ? (
99
111
  <button
@@ -102,7 +114,9 @@ export function GenieLoading({
102
114
  aria-expanded={detailsOpen}
103
115
  onClick={onShowDetails}
104
116
  >
105
- {detailsOpen ? "Hide details" : "Behind the magic"}
117
+ {detailsOpen
118
+ ? (options?.messages?.hideDetails ?? "Hide details")
119
+ : (options?.messages?.showDetails ?? "Behind the magic")}
106
120
  <span aria-hidden="true"> ↗</span>
107
121
  </button>
108
122
  ) : null}
@@ -41,6 +41,7 @@ export type {
41
41
  MachineInputBatchItem,
42
42
  MachineInputMember,
43
43
  MemoryItem,
44
+ KnowledgeItem,
44
45
  NoticeItem,
45
46
  ReasoningItem,
46
47
  SandboxItem,
@@ -139,3 +140,6 @@ export {
139
140
  v4aToGitFileDiff,
140
141
  } from "./parsers";
141
142
  export type { ApplyPatchOperation } from "./parsers";
143
+
144
+ export { KnowledgeActivityProvider } from "./knowledge-receipt";
145
+ export type { KnowledgeActivityActions } from "./knowledge-receipt";
@@ -0,0 +1,100 @@
1
+ import { createContext, useContext, useState, type ReactNode } from "react";
2
+ import { BrainCircuitIcon } from "lucide-react";
3
+ import { ActivityDisclosure, BodyNote } from "./shared";
4
+
5
+ export type KnowledgeActivityActions = {
6
+ onInspect?: ((entryId: string) => void) | undefined;
7
+ onRetryFile?: ((fileId: string) => Promise<boolean | void>) | undefined;
8
+ retryDisabled?: boolean | undefined;
9
+ };
10
+ const KnowledgeActions = createContext<KnowledgeActivityActions>({});
11
+ export function KnowledgeActivityProvider({
12
+ children,
13
+ ...actions
14
+ }: KnowledgeActivityActions & { children: ReactNode }) {
15
+ return <KnowledgeActions.Provider value={actions}>{children}</KnowledgeActions.Provider>;
16
+ }
17
+
18
+ /** A saved/pending result never opens or blocks the human-input surface. */
19
+ export function KnowledgeReceiptRow(props: {
20
+ outcome: "published" | "pending" | "rejected" | "archived" | "failed";
21
+ entryId?: string | undefined;
22
+ fileId?: string | undefined;
23
+ title?: string | undefined;
24
+ source?: boolean | undefined;
25
+ }) {
26
+ const actions = useContext(KnowledgeActions);
27
+ const [retrying, setRetrying] = useState(false);
28
+ const [retrySent, setRetrySent] = useState(false);
29
+ const [error, setError] = useState<string | null>(null);
30
+ const label =
31
+ props.outcome === "pending"
32
+ ? "Knowledge saved for review"
33
+ : props.outcome === "failed"
34
+ ? "Source preparation failed"
35
+ : props.outcome === "rejected"
36
+ ? "Source remains rejected"
37
+ : props.outcome === "archived"
38
+ ? "Knowledge archived"
39
+ : props.source
40
+ ? "Source saved to Knowledge"
41
+ : "Saved to Knowledge";
42
+ const actionClass =
43
+ "rounded-og-sm px-1 py-0.5 text-og-sm text-og-fg-muted hover:text-og-fg focus-visible:outline-2 focus-visible:outline-og-accent disabled:opacity-50";
44
+ return (
45
+ <ActivityDisclosure
46
+ icon={<BrainCircuitIcon className="size-3.5" />}
47
+ iconTone="muted"
48
+ title={label}
49
+ preview={props.title}
50
+ >
51
+ {props.outcome === "pending" ? (
52
+ <BodyNote>
53
+ Saved in Needs review. This task continues; future tasks can use it after approval.
54
+ </BodyNote>
55
+ ) : null}
56
+ {props.outcome === "failed" ? (
57
+ <BodyNote>
58
+ The original file remains attached. Searchable text has not been retained.
59
+ </BodyNote>
60
+ ) : null}
61
+ {props.outcome === "rejected" ? (
62
+ <BodyNote>Processing the file again leaves the review decision unchanged.</BodyNote>
63
+ ) : null}
64
+ {props.entryId && actions.onInspect ? (
65
+ <button
66
+ className={actionClass}
67
+ type="button"
68
+ onClick={() => actions.onInspect?.(props.entryId!)}
69
+ >
70
+ {props.outcome === "pending" ? "Review in Knowledge" : "View in Knowledge"}
71
+ </button>
72
+ ) : null}
73
+ {props.outcome === "failed" && props.fileId && actions.onRetryFile ? (
74
+ <button
75
+ className={actionClass}
76
+ type="button"
77
+ disabled={retrying || retrySent || actions.retryDisabled}
78
+ onClick={() => {
79
+ setRetrying(true);
80
+ setError(null);
81
+ void actions.onRetryFile!(props.fileId!)
82
+ .then((sent) => {
83
+ if (sent !== false) setRetrySent(true);
84
+ else setError("Could not start the retry. Try again when the chat is ready.");
85
+ })
86
+ .catch(() => setError("Could not start the retry. Please try again."))
87
+ .finally(() => setRetrying(false));
88
+ }}
89
+ >
90
+ {retrying ? "Starting retry…" : retrySent ? "Retry requested" : "Retry preparation"}
91
+ </button>
92
+ ) : null}
93
+ {error ? (
94
+ <p role="alert" className="text-og-sm">
95
+ {error}
96
+ </p>
97
+ ) : null}
98
+ </ActivityDisclosure>
99
+ );
100
+ }
@@ -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
  }: {
@@ -20,7 +19,6 @@ export default function PlatformActivityRow({
20
19
  t: (name: string) => string;
21
20
  b: typeof import("lucide-react").BotIcon;
22
21
  m: typeof import("../components/markdown").Markdown;
23
- r: typeof import("../lib/format").truncate;
24
22
  j: RowJsx;
25
23
  s: RowJsxs;
26
24
  }) {
@@ -35,7 +33,11 @@ export default function PlatformActivityRow({
35
33
  children: "Thought",
36
34
  }),
37
35
  running: item.streaming,
38
- preview: truncate(item.text, 110),
36
+ compactPreview: item.text.replace(/\*\*|__|`/g, "").replace(/\s+/g, " "),
37
+ preview: j("span", {
38
+ className: "og-reasoning-preview",
39
+ children: j(Markdown, { streaming: item.streaming, children: item.text }),
40
+ }),
39
41
  children: j("div", {
40
42
  className: "text-og-base leading-6 text-og-fg-muted [&_strong]:text-og-fg-muted",
41
43
  children: j(Markdown, { streaming: item.streaming, children: item.text }),
@@ -1175,6 +1175,45 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
1175
1175
  break;
1176
1176
  }
1177
1177
 
1178
+ case "instruction.confirmation.recovered": {
1179
+ const receipt = asRecord(payload.receipt);
1180
+ if (receipt.outcome !== "published") break;
1181
+ closeStreamingTail();
1182
+ items.push({
1183
+ kind: "notice",
1184
+ id: event.id,
1185
+ tone: "input",
1186
+ text: "Your previously approved workspace instruction was saved.",
1187
+ occurredAt: event.occurredAt,
1188
+ });
1189
+ break;
1190
+ }
1191
+ case "knowledge.confirmation.recovered":
1192
+ case "knowledge.source.prepared":
1193
+ case "knowledge.source.failed": {
1194
+ const receipt = asRecord(payload.receipt);
1195
+ const outcome = event.type === "knowledge.source.failed" ? "failed" : receipt.outcome;
1196
+ if (
1197
+ (event.type !== "knowledge.confirmation.recovered" &&
1198
+ typeof payload.fileId !== "string") ||
1199
+ !["published", "pending", "rejected", "archived", "failed"].includes(String(outcome))
1200
+ )
1201
+ break;
1202
+ closeStreamingTail();
1203
+ items.push({
1204
+ kind: "knowledge",
1205
+ id: event.id,
1206
+ turnId,
1207
+ fileId: typeof payload.fileId === "string" ? payload.fileId : undefined,
1208
+ filename: typeof payload.filename === "string" ? payload.filename : undefined,
1209
+ entryId: typeof receipt.entryId === "string" ? receipt.entryId : undefined,
1210
+ status: outcome === "failed" ? "failed" : "complete",
1211
+ outcome: outcome as "published" | "pending" | "rejected" | "archived" | "failed",
1212
+ occurredAt: event.occurredAt,
1213
+ });
1214
+ break;
1215
+ }
1216
+
1178
1217
  case "memory.saved":
1179
1218
  case "memory.corrected": {
1180
1219
  // A first-party memory write is a discrete step, not streamed text, so it
@@ -1517,6 +1556,7 @@ function isActivityItem(item: TimelineItem): item is ActivityItem {
1517
1556
  case "sandbox":
1518
1557
  case "startup-phase":
1519
1558
  case "memory":
1559
+ case "knowledge":
1520
1560
  case "fleet-decision":
1521
1561
  return true;
1522
1562
  default:
@@ -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
+ }
@@ -0,0 +1,83 @@
1
+ import { renderActivity } from "./activity-rail";
2
+ import { AnimatePresence, motion, useReducedMotion } from "motion/react";
3
+ import { WrenchIcon } from "lucide-react";
4
+ import { Suspense, useEffect, useState } from "react";
5
+ import { CompactActivityContext } from "./shared";
6
+ import { defaultToolRegistry } from "./tool-renderers";
7
+ import type { ToolRegistry } from "./registry";
8
+ import { toolDisplayName } from "./tool-display-name";
9
+ import type { ActivityItem } from "./types";
10
+
11
+ /** One stable viewport; updates replace its content without moving the conversation. */
12
+ export function RollingActivity({
13
+ items,
14
+ toolRegistry = defaultToolRegistry,
15
+ previousItem,
16
+ }: {
17
+ items: ActivityItem[];
18
+ /** The standalone item visible immediately before this reel mounted. */
19
+ previousItem?: ActivityItem | undefined;
20
+ toolRegistry?: ToolRegistry;
21
+ }) {
22
+ const reduced = useReducedMotion();
23
+ const [mounted, setMounted] = useState(false);
24
+ useEffect(() => setMounted(true), []);
25
+ const work = items.filter((item) => item.kind !== "startup-phase");
26
+ const active = work.filter((item) =>
27
+ item.kind === "reasoning" ? item.streaming : "status" in item && item.status === "running",
28
+ );
29
+ // Advance with the event order; finishing a parallel tool must not replay an older one.
30
+ const item = !mounted && previousItem ? previousItem : work.at(-1);
31
+ if (!item) return null;
32
+ const earlierCount = Math.max(
33
+ 0,
34
+ work.findIndex((entry) => entry.id === item.id),
35
+ );
36
+ const fallback = (
37
+ <span className="og-rolling-label">
38
+ <WrenchIcon className="size-3.5" />
39
+ <span>{item.kind === "tool-call" ? toolDisplayName(item.name) : "Working"}</span>
40
+ </span>
41
+ );
42
+ return (
43
+ <span
44
+ className="og-rolling-status"
45
+ data-running={active.some((entry) => entry.id === item.id) ? "true" : undefined}
46
+ >
47
+ <span className="sr-only">
48
+ {item.kind === "tool-call"
49
+ ? toolDisplayName(item.name)
50
+ : item.kind === "reasoning"
51
+ ? "Thinking"
52
+ : "Working"}
53
+ </span>
54
+ <span className="og-rolling-window" aria-hidden="true">
55
+ <AnimatePresence initial={false} mode="sync">
56
+ <motion.span
57
+ key={item.id}
58
+ className="og-rolling-face"
59
+ initial={{
60
+ opacity: 0,
61
+ y: reduced ? 0 : 24,
62
+ }}
63
+ animate={{ opacity: 1, y: 0 }}
64
+ exit={{
65
+ opacity: 0,
66
+ y: reduced ? 0 : -24,
67
+ }}
68
+ transition={{ duration: reduced ? 0 : 0.4, ease: [0.22, 1, 0.36, 1] }}
69
+ >
70
+ <CompactActivityContext.Provider value={true}>
71
+ <Suspense fallback={fallback}>
72
+ {renderActivity(item, toolRegistry, undefined, undefined, undefined, undefined)}
73
+ </Suspense>
74
+ </CompactActivityContext.Provider>
75
+ </motion.span>
76
+ </AnimatePresence>
77
+ </span>
78
+ {earlierCount > 0 ? (
79
+ <span className="og-rolling-count">{`+${earlierCount} earlier`}</span>
80
+ ) : null}
81
+ </span>
82
+ );
83
+ }
@@ -55,6 +55,8 @@ export function ToolCallTruncationProvider({
55
55
  }
56
56
 
57
57
  export type ActivityDisclosureProps = {
58
+ /** Override the rolling preview; null keeps rapidly streaming details out of the header. */
59
+ compactPreview?: ReactNode;
58
60
  icon: ReactNode;
59
61
  /** Icon tint. Defaults to the muted foreground; renderers pass accent/failed. */
60
62
  iconTone?: "accent" | "failed" | "running" | "muted" | undefined;
@@ -116,6 +118,8 @@ const ICON_TONE: Record<NonNullable<ActivityDisclosureProps["iconTone"]>, string
116
118
  * inline media, and at most one right-gutter settle chip. Compact by default;
117
119
  * the body mounts only when expanded.
118
120
  */
121
+ export const CompactActivityContext = createContext(false);
122
+
119
123
  export function ActivityDisclosure({
120
124
  icon,
121
125
  iconTone: iconToneProp = "muted",
@@ -123,6 +127,7 @@ export function ActivityDisclosure({
123
127
  titleMono,
124
128
  running,
125
129
  preview,
130
+ compactPreview,
126
131
  media,
127
132
  chip: chipProp,
128
133
  failed,
@@ -150,6 +155,21 @@ export function ActivityDisclosure({
150
155
  const forcedDefaultOpen = useForcedDefaultOpen();
151
156
  const [open, setOpen] = useState(defaultOpen ?? forcedDefaultOpen ?? false);
152
157
  const truncation = useContext(ToolCallTruncationContext);
158
+ const compact = useContext(CompactActivityContext);
159
+ if (compact)
160
+ return (
161
+ <span className="og-rolling-label">
162
+ <span className={cn("shrink-0", ICON_TONE[iconTone])}>{icon}</span>
163
+ <span className="og-command-reel">
164
+ <span className="og-reel-title">{title}</span>
165
+ {(compactPreview === undefined ? preview : compactPreview) ? (
166
+ <span className="og-reel-preview text-og-fg-subtle">
167
+ {compactPreview === undefined ? preview : compactPreview}
168
+ </span>
169
+ ) : null}
170
+ </span>
171
+ </span>
172
+ );
153
173
  const hasBody = expandable && (children != null || truncation != null);
154
174
 
155
175
  // The preview is detail-on-demand: it is suppressed once the row is open so a
@@ -189,20 +209,21 @@ export function ActivityDisclosure({
189
209
  <span className="size-3.5 shrink-0" />
190
210
  )}
191
211
  <span className={cn("shrink-0", ICON_TONE[iconTone])}>{icon}</span>
192
- <span
193
- className={cn(
194
- "min-w-0 shrink truncate text-og-base font-medium",
195
- titleMono && "font-og-mono text-og-sm font-normal",
196
- running && "og-shimmer-text",
212
+ <span className={cn("og-command-reel", running && "og-command-reel-running")}>
213
+ <span
214
+ className={cn(
215
+ "min-w-0 shrink truncate text-og-base font-medium",
216
+ titleMono && "font-og-mono text-og-sm font-normal",
217
+ )}
218
+ >
219
+ {title}
220
+ </span>
221
+ {previewVisible && !media ? (
222
+ <span className="min-w-0 flex-1 truncate text-og-sm text-og-fg-subtle">{preview}</span>
223
+ ) : (
224
+ <span className="flex-1" />
197
225
  )}
198
- >
199
- {title}
200
226
  </span>
201
- {previewVisible && !media ? (
202
- <span className="min-w-0 flex-1 truncate text-og-sm text-og-fg-subtle">{preview}</span>
203
- ) : (
204
- <span className="flex-1" />
205
- )}
206
227
  {/* The right gutter carries at most ONE signal: the media thumbnail, else a
207
228
  terse settle chip (hidden once expanded — the body owns the detail). */}
208
229
  {media && !open ? (
@@ -1,3 +1,5 @@
1
+ import { KnowledgeReceiptRow } from "./knowledge-receipt";
2
+ import { useRetainedImageObjectUrl } from "./retained-image";
1
3
  import {
2
4
  parseSandboxFileArtifactReceipt,
3
5
  type GitFileDiff,
@@ -34,7 +36,7 @@ import {
34
36
  VideoIcon,
35
37
  WrenchIcon,
36
38
  } from "lucide-react";
37
- import { useEffect, useRef, useState, type ReactNode } from "react";
39
+ import { useContext, useState, type ReactNode } from "react";
38
40
  import { formatBytes, stringifyPayload, tryParseJson } from "../lib/format";
39
41
  import { useTimelineComputeLabel } from "./compute-label";
40
42
  import {
@@ -71,6 +73,7 @@ import {
71
73
  TermBlock,
72
74
  Thumbnail,
73
75
  ActivityDisclosure,
76
+ CompactActivityContext,
74
77
  type DisclosureChip,
75
78
  } from "./shared";
76
79
  import { RawPatch, ToolDiff } from "./tool-diff";
@@ -96,9 +99,12 @@ const ICON_SIZE = "size-3.5";
96
99
  * left-to-right.
97
100
  */
98
101
  function RunningPreview({ children }: { children: ReactNode }) {
102
+ const compact = useContext(CompactActivityContext);
99
103
  return (
100
104
  <span className="inline-flex items-center gap-1.5">
101
- <span className="size-1.5 shrink-0 animate-og-pulse rounded-full bg-og-status-running" />
105
+ {!compact && (
106
+ <span className="size-1.5 shrink-0 animate-og-pulse rounded-full bg-og-status-running" />
107
+ )}
102
108
  <span className="min-w-0 truncate">{children}</span>
103
109
  </span>
104
110
  );
@@ -766,95 +772,6 @@ function ComputerCallRenderer({ item, loadRetainedScreenshot }: ToolRendererProp
766
772
  );
767
773
  }
768
774
 
769
- type RetainedImageState =
770
- | { kind: "loading" }
771
- | { kind: "ready"; url: string }
772
- | { kind: "unavailable"; label: string }
773
- | { kind: "error"; message: string };
774
-
775
- function useRetainedImageObjectUrl(
776
- artifact: RetainedArtifactReference,
777
- load: ToolRendererProps["loadRetainedArtifact"],
778
- ): RetainedImageState {
779
- const [state, setState] = useState<RetainedImageState>({ kind: "loading" });
780
- // Function outputs are commonly serialized JSON. Parsing them creates a new
781
- // object on every render, so depend on the immutable wire value rather than
782
- // object identity; otherwise the loader can refetch after its own setState.
783
- const artifactValue = retainedArtifactValue(artifact);
784
- const stableArtifactRef = useRef({ value: artifactValue, artifact });
785
- if (stableArtifactRef.current.value !== artifactValue) {
786
- stableArtifactRef.current = { value: artifactValue, artifact };
787
- }
788
- const stableArtifact = stableArtifactRef.current.artifact;
789
- useEffect(() => {
790
- if (!load) {
791
- setState({ kind: "unavailable", label: "retrieval is not configured" });
792
- return;
793
- }
794
- const controller = new AbortController();
795
- let objectUrl: string | null = null;
796
- setState({ kind: "loading" });
797
- void load(stableArtifact, controller.signal)
798
- .then((source) => {
799
- if (controller.signal.aborted) return;
800
- if (!source) {
801
- setState({ kind: "unavailable", label: "bytes are unavailable" });
802
- return;
803
- }
804
- if (!(source instanceof Uint8Array)) {
805
- if (!source.url.trim()) {
806
- setState({ kind: "unavailable", label: "URL is unavailable" });
807
- return;
808
- }
809
- setState({ kind: "ready", url: source.url });
810
- return;
811
- }
812
- objectUrl = URL.createObjectURL(
813
- new Blob([source as unknown as BlobPart], { type: stableArtifact.contentType }),
814
- );
815
- setState({ kind: "ready", url: objectUrl });
816
- })
817
- .catch((error: unknown) => {
818
- if (controller.signal.aborted) return;
819
- const status =
820
- error && typeof error === "object" && "status" in error
821
- ? Number((error as { status?: unknown }).status)
822
- : null;
823
- setState(
824
- status === 404
825
- ? { kind: "unavailable", label: "deleted" }
826
- : status === 410
827
- ? { kind: "unavailable", label: "expired or unavailable" }
828
- : { kind: "error", message: "retrieval failed" },
829
- );
830
- });
831
- return () => {
832
- controller.abort();
833
- if (objectUrl) URL.revokeObjectURL(objectUrl);
834
- };
835
- }, [stableArtifact, load]);
836
- return state;
837
- }
838
-
839
- function retainedArtifactValue(artifact: RetainedArtifactReference): string {
840
- return [
841
- artifact.artifactId,
842
- artifact.kind,
843
- artifact.contentType,
844
- artifact.originalBytes,
845
- artifact.sha256,
846
- artifact.retainedAt,
847
- artifact.dimensions?.width ?? "",
848
- artifact.dimensions?.height ?? "",
849
- artifact.retention.policy,
850
- artifact.retention.expiresAt ?? "",
851
- artifact.retrieval.method,
852
- artifact.retrieval.path,
853
- artifact.retrieval.acceptRanges,
854
- artifact.retrieval.maxRangeBytes,
855
- ].join("\0");
856
- }
857
-
858
775
  function RetainedSessionImageDisclosure({
859
776
  artifact,
860
777
  load,
@@ -2370,7 +2287,54 @@ function GenericToolIcon({ name }: { name: string }) {
2370
2287
 
2371
2288
  /* ---- the default registry -------------------------------------------------- */
2372
2289
 
2290
+ function KnowledgeSaveRenderer({ item }: ToolRendererProps) {
2291
+ const output = unwrapMcpOutput(item.output);
2292
+ const parsed = tryParseJson(output.text);
2293
+ if (item.status === "running" || output.isError || !parsed || typeof parsed !== "object")
2294
+ return <GenericRenderer item={item} />;
2295
+ const value = parsed as Record<string, unknown>;
2296
+ const receipt = (value.status === "retained" ? value.receipt : value) as
2297
+ | Record<string, unknown>
2298
+ | undefined;
2299
+ if (
2300
+ !receipt ||
2301
+ typeof receipt.entryId !== "string" ||
2302
+ !["published", "pending", "rejected", "archived"].includes(String(receipt.outcome))
2303
+ )
2304
+ return <GenericRenderer item={item} />;
2305
+ const args = parseToolArgs(item.arguments);
2306
+ const entry = args.entry as Record<string, unknown> | undefined;
2307
+ return (
2308
+ <KnowledgeReceiptRow
2309
+ outcome={receipt.outcome as "published" | "pending" | "rejected" | "archived"}
2310
+ entryId={receipt.entryId}
2311
+ title={
2312
+ typeof entry?.title === "string"
2313
+ ? entry.title
2314
+ : typeof value.filename === "string"
2315
+ ? value.filename
2316
+ : undefined
2317
+ }
2318
+ source={value.status === "retained" || value.retained === true}
2319
+ />
2320
+ );
2321
+ }
2322
+
2373
2323
  const BASE_ENTRIES: ToolRegistryEntry[] = [
2324
+ ...[
2325
+ "knowledge_save",
2326
+ "knowledge_archive",
2327
+ "knowledge_retain_file",
2328
+ "knowledge_retain_message",
2329
+ "task_note_promote_knowledge",
2330
+ ].flatMap((name) =>
2331
+ [name, `opengeni__${name}`, `mcp__opengeni__${name}`].map((trustedName) => ({
2332
+ match: "name" as const,
2333
+ name: trustedName,
2334
+ matchPrefixedLeaf: false,
2335
+ render: KnowledgeSaveRenderer,
2336
+ })),
2337
+ ),
2374
2338
  // Provider-native items carry `raw.type` on the wire — this is their source of
2375
2339
  // truth and is consulted first by the registry.
2376
2340
  { match: "rawType", type: "apply_patch_call", render: ApplyPatchRenderer },