@opengeni/react 5.2.0-canary.0 → 6.0.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 (62) hide show
  1. package/README.md +12 -0
  2. package/dist/artifacts.d.ts +1 -1
  3. package/dist/artifacts.js +3 -1
  4. package/dist/artifacts.js.map +1 -1
  5. package/dist/{chunk-QXJCNVSD.js → chunk-CC7AVRIJ.js} +1769 -1493
  6. package/dist/chunk-CC7AVRIJ.js.map +1 -0
  7. package/dist/{chunk-5PQJOCX5.js → chunk-KANFU34B.js} +6 -4
  8. package/dist/chunk-KANFU34B.js.map +1 -0
  9. package/dist/{chunk-IYCR4343.js → chunk-PZ27MCZP.js} +36 -1
  10. package/dist/chunk-PZ27MCZP.js.map +1 -0
  11. package/dist/{chunk-2TKI3WEG.js → chunk-WYWRYUNQ.js} +12 -1
  12. package/dist/chunk-WYWRYUNQ.js.map +1 -0
  13. package/dist/{chunk-T5FRXMC3.js → chunk-YS24CFDI.js} +24 -9
  14. package/dist/chunk-YS24CFDI.js.map +1 -0
  15. package/dist/components/session-chrome.d.ts +6 -1
  16. package/dist/{computer-viewer-AA4JH2AP.js → computer-viewer-CTFCJVS7.js} +2 -2
  17. package/dist/connect.js +11 -19
  18. package/dist/connect.js.map +1 -1
  19. package/dist/hooks/use-file-attachments.d.ts +2 -0
  20. package/dist/index.d.ts +4 -0
  21. package/dist/index.js +13 -5
  22. package/dist/index.js.map +1 -1
  23. package/dist/interaction.js +1 -1
  24. package/dist/{platform-activity-row-UW4UVVM4.js → platform-activity-row-DUE2W74J.js} +1 -1
  25. package/dist/platform-activity-row-DUE2W74J.js.map +1 -0
  26. package/dist/session-ui.d.ts +2 -0
  27. package/dist/session-ui.js +6 -107
  28. package/dist/session-ui.js.map +1 -1
  29. package/dist/session.js +2 -2
  30. package/dist/timeline/genie-loading.d.ts +9 -1
  31. package/dist/timeline/index.d.ts +3 -1
  32. package/dist/timeline/knowledge-receipt.d.ts +17 -0
  33. package/dist/timeline/platform-activity-row.d.ts +0 -1
  34. package/dist/timeline/retained-image.d.ts +2 -0
  35. package/dist/timeline/types.d.ts +13 -2
  36. package/package.json +3 -3
  37. package/src/artifacts.ts +1 -1
  38. package/src/components/computer-viewer.tsx +11 -3
  39. package/src/components/message-timeline.tsx +42 -12
  40. package/src/components/session-chrome.tsx +14 -2
  41. package/src/connection-catalog.tsx +8 -21
  42. package/src/hooks/use-computer-frame-stream.ts +17 -5
  43. package/src/hooks/use-file-attachments.ts +4 -1
  44. package/src/hooks/use-goal.ts +3 -1
  45. package/src/index.ts +6 -0
  46. package/src/session-ui.ts +3 -0
  47. package/src/timeline/activity-rail.tsx +11 -1
  48. package/src/timeline/genie-loading.tsx +18 -4
  49. package/src/timeline/index.ts +4 -0
  50. package/src/timeline/knowledge-receipt.tsx +100 -0
  51. package/src/timeline/platform-activity-row.tsx +0 -1
  52. package/src/timeline/projection.ts +40 -0
  53. package/src/timeline/retained-image.ts +12 -0
  54. package/src/timeline/tool-renderers.tsx +115 -16
  55. package/src/timeline/types.ts +14 -0
  56. package/dist/chunk-2TKI3WEG.js.map +0 -1
  57. package/dist/chunk-5PQJOCX5.js.map +0 -1
  58. package/dist/chunk-IYCR4343.js.map +0 -1
  59. package/dist/chunk-QXJCNVSD.js.map +0 -1
  60. package/dist/chunk-T5FRXMC3.js.map +0 -1
  61. package/dist/platform-activity-row-UW4UVVM4.js.map +0 -1
  62. /package/dist/{computer-viewer-AA4JH2AP.js.map → computer-viewer-CTFCJVS7.js.map} +0 -0
@@ -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]);
@@ -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
@@ -617,3 +617,9 @@ export {
617
617
  tryParseJson,
618
618
  } from "./lib/format";
619
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";
@@ -1,3 +1,4 @@
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";
@@ -241,7 +242,6 @@ export function renderActivity(
241
242
  t={toolDisplayName}
242
243
  b={BotIcon}
243
244
  m={Markdown}
244
- r={truncate}
245
245
  j={rowJsx}
246
246
  s={rowJsxs}
247
247
  />
@@ -261,6 +261,16 @@ export function renderActivity(
261
261
  }
262
262
  case "worker":
263
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
+ );
264
274
  case "memory":
265
275
  return <MemoryRow item={item} onMemoryClick={onMemoryClick} />;
266
276
  case "fleet-decision":
@@ -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
+ }
@@ -19,7 +19,6 @@ export default function PlatformActivityRow({
19
19
  t: (name: string) => string;
20
20
  b: typeof import("lucide-react").BotIcon;
21
21
  m: typeof import("../components/markdown").Markdown;
22
- r: typeof import("../lib/format").truncate;
23
22
  j: RowJsx;
24
23
  s: RowJsxs;
25
24
  }) {
@@ -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:
@@ -7,6 +7,18 @@ type RetainedImageState =
7
7
  | { kind: "unavailable"; label: string }
8
8
  | { kind: "error"; message: string };
9
9
 
10
+ /** Shared browser preview allowlist; other published files remain downloads. */
11
+ export function isRetainedImageContentType(contentType: string): boolean {
12
+ return [
13
+ "image/png",
14
+ "image/jpeg",
15
+ "image/gif",
16
+ "image/webp",
17
+ "image/avif",
18
+ "image/svg+xml",
19
+ ].includes(contentType);
20
+ }
21
+
10
22
  export function useRetainedImageObjectUrl(
11
23
  artifact: RetainedArtifactReference,
12
24
  load: ToolRendererProps["loadRetainedArtifact"],
@@ -1,4 +1,5 @@
1
- import { useRetainedImageObjectUrl } from "./retained-image";
1
+ import { KnowledgeReceiptRow } from "./knowledge-receipt";
2
+ import { isRetainedImageContentType, useRetainedImageObjectUrl } from "./retained-image";
2
3
  import {
3
4
  parseSandboxFileArtifactReceipt,
4
5
  type GitFileDiff,
@@ -782,9 +783,12 @@ function RetainedSessionImageDisclosure({
782
783
  batched,
783
784
  failed,
784
785
  cancelled,
786
+ filename,
787
+ defaultOpen,
788
+ children,
785
789
  }: {
786
790
  artifact: RetainedArtifactReference;
787
- load: ToolRendererProps["loadRetainedScreenshot"];
791
+ load: ToolRendererProps["loadRetainedArtifact"];
788
792
  title: string;
789
793
  caption: string;
790
794
  noun: "image" | "screenshot";
@@ -793,15 +797,19 @@ function RetainedSessionImageDisclosure({
793
797
  batched: string | null;
794
798
  failed: boolean;
795
799
  cancelled: boolean;
800
+ filename?: string;
801
+ defaultOpen?: boolean;
802
+ children?: ReactNode;
796
803
  }) {
797
804
  const state = useRetainedImageObjectUrl(artifact, load);
798
- const downloadFilename = retainedImageFilename(artifact);
805
+ const downloadFilename = filename ?? retainedImageFilename(artifact);
799
806
 
800
807
  return (
801
808
  <ActivityDisclosure
802
809
  icon={icon}
803
810
  iconTone={failed ? "failed" : state.kind === "ready" ? "accent" : "muted"}
804
811
  title={title}
812
+ defaultOpen={defaultOpen}
805
813
  failed={failed}
806
814
  cancelled={cancelled}
807
815
  preview={
@@ -851,6 +859,7 @@ function RetainedSessionImageDisclosure({
851
859
  </BodyNote>
852
860
  )}
853
861
  {batched ? <BodyNote>batched: {batched}</BodyNote> : null}
862
+ {children}
854
863
  </ActivityDisclosure>
855
864
  );
856
865
  }
@@ -944,6 +953,22 @@ function SandboxFilePublishRenderer({ item, loadRetainedArtifact }: ToolRenderer
944
953
  return <GenericRenderer item={item} />;
945
954
  }
946
955
 
956
+ // The closed SDK receipt has already checked the workspace-qualified route.
957
+ const workspaceId = /^\/v1\/workspaces\/([0-9a-f-]+)\/artifacts\//.exec(
958
+ receipt.artifact.retrieval.path,
959
+ )?.[1];
960
+ const openLink = workspaceId ? (
961
+ <a
962
+ href={`/workspaces/${workspaceId}/artifacts/files/${receipt.artifact.artifactId}`}
963
+ aria-label={`Open ${receipt.filename} in Artifacts`}
964
+ className="inline-flex min-h-7 items-center rounded-og-sm px-2 text-og-sm font-medium text-og-accent-strong hover:bg-og-surface-2 hover:underline focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent pointer-coarse:min-h-10"
965
+ onClick={(event) => event.stopPropagation()}
966
+ onKeyDown={(event) => event.stopPropagation()}
967
+ >
968
+ Open in Artifacts
969
+ </a>
970
+ ) : null;
971
+
947
972
  const download = async () => {
948
973
  if (!loadRetainedArtifact) {
949
974
  setDownloadState("error");
@@ -976,6 +1001,44 @@ function SandboxFilePublishRenderer({ item, loadRetainedArtifact }: ToolRenderer
976
1001
  }
977
1002
  };
978
1003
 
1004
+ const downloadButton = (
1005
+ <button
1006
+ type="button"
1007
+ onClick={() => void download()}
1008
+ disabled={downloadState === "loading"}
1009
+ className="inline-flex items-center gap-1.5 rounded-og-sm border border-og-border px-2.5 py-1.5 text-og-sm font-medium text-og-fg transition-colors hover:border-og-border-strong hover:bg-og-surface-2 disabled:cursor-wait disabled:opacity-60"
1010
+ >
1011
+ <DownloadIcon className="size-3.5" />
1012
+ {downloadState === "loading"
1013
+ ? "Preparing…"
1014
+ : downloadState === "error"
1015
+ ? "Retry download"
1016
+ : "Download"}
1017
+ </button>
1018
+ );
1019
+
1020
+ if (isRetainedImageContentType(receipt.artifact.contentType)) {
1021
+ return (
1022
+ <RetainedSessionImageDisclosure
1023
+ artifact={receipt.artifact}
1024
+ load={loadRetainedArtifact}
1025
+ title={`Published ${receipt.filename}`}
1026
+ caption={receipt.filename}
1027
+ noun="image"
1028
+ icon={<ImageIcon className={ICON_SIZE} />}
1029
+ lightboxLabel="Image"
1030
+ batched={null}
1031
+ failed={false}
1032
+ cancelled={false}
1033
+ filename={receipt.filename}
1034
+ defaultOpen
1035
+ >
1036
+ {downloadButton}
1037
+ {openLink}
1038
+ </RetainedSessionImageDisclosure>
1039
+ );
1040
+ }
1041
+
979
1042
  return (
980
1043
  <ActivityDisclosure
981
1044
  icon={<DownloadIcon className={ICON_SIZE} />}
@@ -984,19 +1047,8 @@ function SandboxFilePublishRenderer({ item, loadRetainedArtifact }: ToolRenderer
984
1047
  defaultOpen
985
1048
  preview={formatBytes(receipt.artifact.originalBytes)}
986
1049
  >
987
- <button
988
- type="button"
989
- onClick={() => void download()}
990
- disabled={downloadState === "loading"}
991
- className="inline-flex items-center gap-1.5 rounded-og-sm border border-og-border px-2.5 py-1.5 text-og-sm font-medium text-og-fg transition-colors hover:border-og-border-strong hover:bg-og-surface-2 disabled:cursor-wait disabled:opacity-60"
992
- >
993
- <DownloadIcon className="size-3.5" />
994
- {downloadState === "loading"
995
- ? "Preparing…"
996
- : downloadState === "error"
997
- ? "Retry download"
998
- : "Download"}
999
- </button>
1050
+ {downloadButton}
1051
+ {openLink}
1000
1052
  </ActivityDisclosure>
1001
1053
  );
1002
1054
  }
@@ -2286,7 +2338,54 @@ function GenericToolIcon({ name }: { name: string }) {
2286
2338
 
2287
2339
  /* ---- the default registry -------------------------------------------------- */
2288
2340
 
2341
+ function KnowledgeSaveRenderer({ item }: ToolRendererProps) {
2342
+ const output = unwrapMcpOutput(item.output);
2343
+ const parsed = tryParseJson(output.text);
2344
+ if (item.status === "running" || output.isError || !parsed || typeof parsed !== "object")
2345
+ return <GenericRenderer item={item} />;
2346
+ const value = parsed as Record<string, unknown>;
2347
+ const receipt = (value.status === "retained" ? value.receipt : value) as
2348
+ | Record<string, unknown>
2349
+ | undefined;
2350
+ if (
2351
+ !receipt ||
2352
+ typeof receipt.entryId !== "string" ||
2353
+ !["published", "pending", "rejected", "archived"].includes(String(receipt.outcome))
2354
+ )
2355
+ return <GenericRenderer item={item} />;
2356
+ const args = parseToolArgs(item.arguments);
2357
+ const entry = args.entry as Record<string, unknown> | undefined;
2358
+ return (
2359
+ <KnowledgeReceiptRow
2360
+ outcome={receipt.outcome as "published" | "pending" | "rejected" | "archived"}
2361
+ entryId={receipt.entryId}
2362
+ title={
2363
+ typeof entry?.title === "string"
2364
+ ? entry.title
2365
+ : typeof value.filename === "string"
2366
+ ? value.filename
2367
+ : undefined
2368
+ }
2369
+ source={value.status === "retained" || value.retained === true}
2370
+ />
2371
+ );
2372
+ }
2373
+
2289
2374
  const BASE_ENTRIES: ToolRegistryEntry[] = [
2375
+ ...[
2376
+ "knowledge_save",
2377
+ "knowledge_archive",
2378
+ "knowledge_retain_file",
2379
+ "knowledge_retain_message",
2380
+ "task_note_promote_knowledge",
2381
+ ].flatMap((name) =>
2382
+ [name, `opengeni__${name}`, `mcp__opengeni__${name}`].map((trustedName) => ({
2383
+ match: "name" as const,
2384
+ name: trustedName,
2385
+ matchPrefixedLeaf: false,
2386
+ render: KnowledgeSaveRenderer,
2387
+ })),
2388
+ ),
2290
2389
  // Provider-native items carry `raw.type` on the wire — this is their source of
2291
2390
  // truth and is consulted first by the registry.
2292
2391
  { match: "rawType", type: "apply_patch_call", render: ApplyPatchRenderer },
@@ -228,6 +228,18 @@ export type StartupPhaseItem = {
228
228
  * `onMemoryClick` handler the row also deep-links to the record in its memory
229
229
  * pane; without one it is non-interactive rich content.
230
230
  */
231
+ export type KnowledgeItem = {
232
+ kind: "knowledge";
233
+ id: string;
234
+ turnId: string | null;
235
+ status: "complete" | "failed";
236
+ outcome: "published" | "pending" | "rejected" | "archived" | "failed";
237
+ fileId?: string | undefined;
238
+ filename?: string | undefined;
239
+ entryId?: string | undefined;
240
+ occurredAt: string;
241
+ };
242
+
231
243
  export type MemoryItem = {
232
244
  kind: "memory";
233
245
  id: string;
@@ -478,6 +490,7 @@ export type TimelineItem =
478
490
  | MachineInputBatchItem
479
491
  | AuthNeededItem
480
492
  | MemoryItem
493
+ | KnowledgeItem
481
494
  | FleetDecisionItem
482
495
  | TurnEndItem;
483
496
 
@@ -489,6 +502,7 @@ export type ActivityItem =
489
502
  | SandboxItem
490
503
  | StartupPhaseItem
491
504
  | MemoryItem
505
+ | KnowledgeItem
492
506
  | FleetDecisionItem;
493
507
 
494
508
  export type TimelineGroup =
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/timeline/retained-image.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { RetainedArtifactReference } from \"@opengeni/sdk\";\nimport type { ToolRendererProps } from \"./registry\";\ntype RetainedImageState =\n | { kind: \"loading\" }\n | { kind: \"ready\"; url: string }\n | { kind: \"unavailable\"; label: string }\n | { kind: \"error\"; message: string };\n\nexport function useRetainedImageObjectUrl(\n artifact: RetainedArtifactReference,\n load: ToolRendererProps[\"loadRetainedArtifact\"],\n): RetainedImageState {\n const [state, setState] = useState<RetainedImageState>({ kind: \"loading\" });\n // Function outputs are commonly serialized JSON. Parsing them creates a new\n // object on every render, so depend on the immutable wire value rather than\n // object identity; otherwise the loader can refetch after its own setState.\n const artifactValue = retainedArtifactValue(artifact);\n const stableArtifactRef = useRef({ value: artifactValue, artifact });\n if (stableArtifactRef.current.value !== artifactValue) {\n stableArtifactRef.current = { value: artifactValue, artifact };\n }\n const stableArtifact = stableArtifactRef.current.artifact;\n useEffect(() => {\n if (!load) {\n setState({ kind: \"unavailable\", label: \"retrieval is not configured\" });\n return;\n }\n const controller = new AbortController();\n let objectUrl: string | null = null;\n setState({ kind: \"loading\" });\n void load(stableArtifact, controller.signal)\n .then((source) => {\n if (controller.signal.aborted) return;\n if (!source) {\n setState({ kind: \"unavailable\", label: \"bytes are unavailable\" });\n return;\n }\n if (!(source instanceof Uint8Array)) {\n if (!source.url.trim()) {\n setState({ kind: \"unavailable\", label: \"URL is unavailable\" });\n return;\n }\n setState({ kind: \"ready\", url: source.url });\n return;\n }\n objectUrl = URL.createObjectURL(\n new Blob([source as unknown as BlobPart], { type: stableArtifact.contentType }),\n );\n setState({ kind: \"ready\", url: objectUrl });\n })\n .catch((error: unknown) => {\n if (controller.signal.aborted) return;\n const status =\n error && typeof error === \"object\" && \"status\" in error\n ? Number((error as { status?: unknown }).status)\n : null;\n setState(\n status === 404\n ? { kind: \"unavailable\", label: \"deleted\" }\n : status === 410\n ? { kind: \"unavailable\", label: \"expired or unavailable\" }\n : { kind: \"error\", message: \"retrieval failed\" },\n );\n });\n return () => {\n controller.abort();\n if (objectUrl) URL.revokeObjectURL(objectUrl);\n };\n }, [stableArtifact, load]);\n return state;\n}\n\nfunction retainedArtifactValue(artifact: RetainedArtifactReference): string {\n return [\n artifact.artifactId,\n artifact.kind,\n artifact.contentType,\n artifact.originalBytes,\n artifact.sha256,\n artifact.retainedAt,\n artifact.dimensions?.width ?? \"\",\n artifact.dimensions?.height ?? \"\",\n artifact.retention.policy,\n artifact.retention.expiresAt ?? \"\",\n artifact.retrieval.method,\n artifact.retrieval.path,\n artifact.retrieval.acceptRanges,\n artifact.retrieval.maxRangeBytes,\n ].join(\"\\0\");\n}\n"],"mappings":";AAAA,SAAS,WAAW,QAAQ,gBAAgB;AASrC,SAAS,0BACd,UACA,MACoB;AACpB,QAAM,CAAC,OAAO,QAAQ,IAAI,SAA6B,EAAE,MAAM,UAAU,CAAC;AAI1E,QAAM,gBAAgB,sBAAsB,QAAQ;AACpD,QAAM,oBAAoB,OAAO,EAAE,OAAO,eAAe,SAAS,CAAC;AACnE,MAAI,kBAAkB,QAAQ,UAAU,eAAe;AACrD,sBAAkB,UAAU,EAAE,OAAO,eAAe,SAAS;AAAA,EAC/D;AACA,QAAM,iBAAiB,kBAAkB,QAAQ;AACjD,YAAU,MAAM;AACd,QAAI,CAAC,MAAM;AACT,eAAS,EAAE,MAAM,eAAe,OAAO,8BAA8B,CAAC;AACtE;AAAA,IACF;AACA,UAAM,aAAa,IAAI,gBAAgB;AACvC,QAAI,YAA2B;AAC/B,aAAS,EAAE,MAAM,UAAU,CAAC;AAC5B,SAAK,KAAK,gBAAgB,WAAW,MAAM,EACxC,KAAK,CAAC,WAAW;AAChB,UAAI,WAAW,OAAO,QAAS;AAC/B,UAAI,CAAC,QAAQ;AACX,iBAAS,EAAE,MAAM,eAAe,OAAO,wBAAwB,CAAC;AAChE;AAAA,MACF;AACA,UAAI,EAAE,kBAAkB,aAAa;AACnC,YAAI,CAAC,OAAO,IAAI,KAAK,GAAG;AACtB,mBAAS,EAAE,MAAM,eAAe,OAAO,qBAAqB,CAAC;AAC7D;AAAA,QACF;AACA,iBAAS,EAAE,MAAM,SAAS,KAAK,OAAO,IAAI,CAAC;AAC3C;AAAA,MACF;AACA,kBAAY,IAAI;AAAA,QACd,IAAI,KAAK,CAAC,MAA6B,GAAG,EAAE,MAAM,eAAe,YAAY,CAAC;AAAA,MAChF;AACA,eAAS,EAAE,MAAM,SAAS,KAAK,UAAU,CAAC;AAAA,IAC5C,CAAC,EACA,MAAM,CAAC,UAAmB;AACzB,UAAI,WAAW,OAAO,QAAS;AAC/B,YAAM,SACJ,SAAS,OAAO,UAAU,YAAY,YAAY,QAC9C,OAAQ,MAA+B,MAAM,IAC7C;AACN;AAAA,QACE,WAAW,MACP,EAAE,MAAM,eAAe,OAAO,UAAU,IACxC,WAAW,MACT,EAAE,MAAM,eAAe,OAAO,yBAAyB,IACvD,EAAE,MAAM,SAAS,SAAS,mBAAmB;AAAA,MACrD;AAAA,IACF,CAAC;AACH,WAAO,MAAM;AACX,iBAAW,MAAM;AACjB,UAAI,UAAW,KAAI,gBAAgB,SAAS;AAAA,IAC9C;AAAA,EACF,GAAG,CAAC,gBAAgB,IAAI,CAAC;AACzB,SAAO;AACT;AAEA,SAAS,sBAAsB,UAA6C;AAC1E,SAAO;AAAA,IACL,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS,YAAY,SAAS;AAAA,IAC9B,SAAS,YAAY,UAAU;AAAA,IAC/B,SAAS,UAAU;AAAA,IACnB,SAAS,UAAU,aAAa;AAAA,IAChC,SAAS,UAAU;AAAA,IACnB,SAAS,UAAU;AAAA,IACnB,SAAS,UAAU;AAAA,IACnB,SAAS,UAAU;AAAA,EACrB,EAAE,KAAK,IAAI;AACb;","names":[]}