@opengeni/react 0.23.0 → 0.25.5

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/dist/{chunk-UQS7OQDE.js → chunk-F4CFWKVI.js} +334 -64
  2. package/dist/chunk-F4CFWKVI.js.map +1 -0
  3. package/dist/chunk-GQN2QIR2.js +3767 -0
  4. package/dist/chunk-GQN2QIR2.js.map +1 -0
  5. package/dist/{chunk-I3BJZIG5.js → chunk-HIWPQYWI.js} +2 -120
  6. package/dist/chunk-HIWPQYWI.js.map +1 -0
  7. package/dist/chunk-M7X4JZOD.js +121 -0
  8. package/dist/chunk-M7X4JZOD.js.map +1 -0
  9. package/dist/{chunk-UAHPKLB6.js → chunk-MP6237JB.js} +571 -975
  10. package/dist/chunk-MP6237JB.js.map +1 -0
  11. package/dist/chunk-OZDLELJQ.js +937 -0
  12. package/dist/chunk-OZDLELJQ.js.map +1 -0
  13. package/dist/{chunk-CATATY77.js → chunk-TMH6HZWF.js} +8 -7
  14. package/dist/chunk-TMH6HZWF.js.map +1 -0
  15. package/dist/{chunk-6XUS5VFM.js → chunk-YU5PGUK7.js} +6 -4
  16. package/dist/{chunk-6XUS5VFM.js.map → chunk-YU5PGUK7.js.map} +1 -1
  17. package/dist/{composer-CVm5uoUI.d.ts → composer-DoC4veX1.d.ts} +6 -63
  18. package/dist/composer.d.ts +3 -2
  19. package/dist/composer.js +4 -3
  20. package/dist/fleet-decision-projection-SLNZMS42.js +241 -0
  21. package/dist/fleet-decision-projection-SLNZMS42.js.map +1 -0
  22. package/dist/fleet-decision-row-GGCAT4E4.js +351 -0
  23. package/dist/fleet-decision-row-GGCAT4E4.js.map +1 -0
  24. package/dist/index.d.ts +17 -259
  25. package/dist/index.js +1042 -5058
  26. package/dist/index.js.map +1 -1
  27. package/dist/machines.js +3 -2
  28. package/dist/session-Du_FsrZ1.d.ts +264 -0
  29. package/dist/session-ui-BqQDH7YV.d.ts +183 -0
  30. package/dist/session-ui.d.ts +7 -0
  31. package/dist/session-ui.js +17 -0
  32. package/dist/session-ui.js.map +1 -0
  33. package/dist/session.d.ts +4 -2
  34. package/dist/session.js +26 -9
  35. package/dist/{use-composer-By1wn3MM.d.ts → use-composer-BnEzheSk.d.ts} +8 -2
  36. package/dist/use-file-attachments-C0kpHrs9.d.ts +76 -0
  37. package/dist/use-turn-queue-3rkJwjFv.d.ts +290 -0
  38. package/package.json +6 -2
  39. package/src/components/composer.tsx +7 -6
  40. package/src/components/message-timeline.tsx +94 -54
  41. package/src/hooks/use-composer.ts +470 -73
  42. package/src/hooks/use-file-attachments.ts +96 -3
  43. package/src/hooks/use-goal.ts +23 -5
  44. package/src/hooks/use-session-events.ts +5 -0
  45. package/src/hooks/use-session-lineage.ts +1 -1
  46. package/src/hooks/use-session.ts +9 -6
  47. package/src/hooks/use-workspace-sessions.ts +30 -5
  48. package/src/session-ui.ts +13 -0
  49. package/src/session.ts +15 -0
  50. package/src/timeline/activity-rail.tsx +18 -2
  51. package/src/timeline/fleet-decision-projection.ts +366 -0
  52. package/src/timeline/fleet-decision-row.tsx +412 -0
  53. package/src/timeline/index.ts +2 -0
  54. package/src/timeline/projection.ts +19 -8
  55. package/src/timeline/tool-display-name.ts +16 -0
  56. package/src/timeline/tool-renderers.tsx +1 -1
  57. package/src/timeline/types.ts +68 -1
  58. package/dist/chunk-CATATY77.js.map +0 -1
  59. package/dist/chunk-I3BJZIG5.js.map +0 -1
  60. package/dist/chunk-UAHPKLB6.js.map +0 -1
  61. package/dist/chunk-UQS7OQDE.js.map +0 -1
  62. package/dist/session-Db00TRwl.d.ts +0 -446
@@ -1,6 +1,6 @@
1
1
  import type { FileAsset, FileResourceRef } from "@opengeni/sdk";
2
2
  import { useCallback, useRef, useState } from "react";
3
- import { useOpenGeni, type ClientOverride } from "../provider";
3
+ import { useOpenGeni, type ClientOverride } from "../session-context";
4
4
 
5
5
  export type UseFileAttachmentsOptions = ClientOverride & {
6
6
  /**
@@ -33,12 +33,20 @@ export type UseFileAttachmentsResult = {
33
33
  * straight into `useComposer`'s `sendExtras.resources`.
34
34
  */
35
35
  readyResources: FileResourceRef[];
36
- /** True while any attachment is still uploading (drives the send-gate). */
36
+ /** True while any attachment is still uploading (drives progress UI). */
37
37
  uploading: boolean;
38
+ /**
39
+ * True while any attachment still needs an explicit outcome: wait for an
40
+ * upload, retry a failure, or remove it. This is the loss-prevention send
41
+ * gate; failed cards must never be silently omitted from a message.
42
+ */
43
+ hasUnresolved: boolean;
38
44
  /** Explicit picker / drop path — uploads every file, no filter. */
39
45
  addFiles: (files: Iterable<File>) => void;
40
46
  /** Clipboard path — applies `pasteFilter` (default `image/*`) then uploads. */
41
47
  addFromPaste: (event: { clipboardData: DataTransfer | null }) => void;
48
+ /** Restore already-ready server assets without recreating browser-local bytes. */
49
+ restoreReadyFiles: (files: Iterable<FileAsset>) => void;
42
50
  /**
43
51
  * Re-run the upload for a `failed` attachment, in place (same id, same
44
52
  * source file). No-op for an id that isn't a known failed upload.
@@ -46,7 +54,13 @@ export type UseFileAttachmentsResult = {
46
54
  retry: (id: string) => void;
47
55
  /** Remove one attachment; revokes its object-URL. */
48
56
  remove: (id: string) => void;
49
- /** Remove all; revokes every object-URL. Call from `useComposer`'s `onSent`. */
57
+ /**
58
+ * Remove only finalized files whose durable ids were accepted by a send.
59
+ * Attachments added while that request was in flight remain queued for the
60
+ * next message.
61
+ */
62
+ removeReadyFiles: (fileIds: Iterable<string>) => void;
63
+ /** Remove all attachments and revoke every object-URL. */
50
64
  clear: () => void;
51
65
  };
52
66
 
@@ -83,6 +97,10 @@ export function useFileAttachments(
83
97
  data: file,
84
98
  })
85
99
  .then((asset) => {
100
+ // Retry bytes are useful only until durable finalization succeeds.
101
+ // Drop the source File immediately; restored/ready attachments must
102
+ // never retain browser-local byte authority.
103
+ sources.current.delete(id);
86
104
  setAttachments((current) =>
87
105
  current.map((attachment) =>
88
106
  attachment.id === id
@@ -171,6 +189,60 @@ export function useFileAttachments(
171
189
  [addFiles, pasteFilter],
172
190
  );
173
191
 
192
+ const restoreReadyFiles = useCallback(
193
+ (files: Iterable<FileAsset>) => {
194
+ const incoming = new Map<string, FileAsset>();
195
+ for (const file of files) {
196
+ if (file.status === "ready" && file.workspaceId === workspaceId) {
197
+ incoming.set(file.id, file);
198
+ }
199
+ }
200
+ setAttachments((current) => {
201
+ const unresolved = current.filter((attachment) => attachment.status !== "ready");
202
+ const existingReady = new Map(
203
+ current.flatMap((attachment) =>
204
+ attachment.status === "ready" && attachment.file
205
+ ? ([[attachment.file.id, attachment]] as const)
206
+ : [],
207
+ ),
208
+ );
209
+ const restored = [...incoming.values()].map((file): FileAttachment => {
210
+ const existing = existingReady.get(file.id);
211
+ return existing
212
+ ? {
213
+ ...existing,
214
+ name: file.filename,
215
+ contentType: file.contentType,
216
+ sizeBytes: file.sizeBytes,
217
+ status: "ready",
218
+ file,
219
+ error: undefined,
220
+ }
221
+ : {
222
+ id: `restored:${file.id}`,
223
+ name: file.filename,
224
+ contentType: file.contentType,
225
+ sizeBytes: file.sizeBytes,
226
+ status: "ready",
227
+ file,
228
+ // No source File and no object URL: server metadata is the
229
+ // only authority restored across page/device boundaries.
230
+ };
231
+ });
232
+ for (const [fileId, attachment] of existingReady) {
233
+ if (!incoming.has(fileId) && attachment.previewUrl) {
234
+ URL.revokeObjectURL(attachment.previewUrl);
235
+ }
236
+ }
237
+ // A server restoration is authoritative for finalized assets, but an
238
+ // upload that has not finalized still belongs to the local actor. Keep
239
+ // those unresolved entries while replacing the ready set exactly.
240
+ return [...unresolved, ...restored];
241
+ });
242
+ },
243
+ [workspaceId],
244
+ );
245
+
174
246
  const remove = useCallback((id: string) => {
175
247
  sources.current.delete(id);
176
248
  setAttachments((current) => {
@@ -182,6 +254,24 @@ export function useFileAttachments(
182
254
  });
183
255
  }, []);
184
256
 
257
+ const removeReadyFiles = useCallback((fileIds: Iterable<string>) => {
258
+ const accepted = new Set(fileIds);
259
+ if (accepted.size === 0) return;
260
+ setAttachments((current) =>
261
+ current.filter((attachment) => {
262
+ const removeAccepted =
263
+ attachment.status === "ready" &&
264
+ attachment.file !== undefined &&
265
+ accepted.has(attachment.file.id);
266
+ if (removeAccepted) {
267
+ sources.current.delete(attachment.id);
268
+ if (attachment.previewUrl) URL.revokeObjectURL(attachment.previewUrl);
269
+ }
270
+ return !removeAccepted;
271
+ }),
272
+ );
273
+ }, []);
274
+
185
275
  const clear = useCallback(() => {
186
276
  sources.current.clear();
187
277
  setAttachments((current) => {
@@ -202,10 +292,13 @@ export function useFileAttachments(
202
292
  : [],
203
293
  ),
204
294
  uploading: attachments.some((attachment) => attachment.status === "uploading"),
295
+ hasUnresolved: attachments.some((attachment) => attachment.status !== "ready"),
205
296
  addFiles,
206
297
  addFromPaste,
298
+ restoreReadyFiles,
207
299
  retry,
208
300
  remove,
301
+ removeReadyFiles,
209
302
  clear,
210
303
  };
211
304
  }
@@ -1,6 +1,6 @@
1
1
  import { OpenGeniApiError, type SessionEvent, type SessionGoal } from "@opengeni/sdk";
2
2
  import { useCallback, useEffect, useRef, useState } from "react";
3
- import { useOpenGeni, type ClientOverride } from "../provider";
3
+ import { useOpenGeni, type ClientOverride } from "../session-context";
4
4
  import {
5
5
  useDebouncedCallback,
6
6
  useMutationRunner,
@@ -13,9 +13,26 @@ export function isGoalEvent(event: Pick<SessionEvent, "type">): boolean {
13
13
  return event.type.startsWith("goal.");
14
14
  }
15
15
 
16
+ /**
17
+ * Events that can change the server-authoritative continuation projection.
18
+ * The projection is derived from durable turn, session, control, and system
19
+ * update state, so those events refresh the goal without model polling.
20
+ */
21
+ export function isGoalRefreshEvent(event: Pick<SessionEvent, "type">): boolean {
22
+ const type = event.type;
23
+ return (
24
+ isGoalEvent(event) ||
25
+ type.startsWith("turn.") ||
26
+ type.startsWith("session.") ||
27
+ type.startsWith("system.update.") ||
28
+ type.startsWith("workspace.inference.") ||
29
+ type.startsWith("user.")
30
+ );
31
+ }
32
+
16
33
  export type UseGoalOptions = ClientOverride &
17
34
  SessionEventFeedOptions & {
18
- /** Optional safety-net polling (ms). Off by default — goal.* events drive updates. */
35
+ /** Optional safety-net polling (ms). Off by default — event refreshes drive updates. */
19
36
  pollIntervalMs?: number | undefined;
20
37
  };
21
38
 
@@ -46,8 +63,9 @@ export type UseGoalResult = {
46
63
  /**
47
64
  * The session's goal: state, the autonomy counters (`autoContinuations`,
48
65
  * `noProgressStreak`), and pause/resume control. A goal-less session yields
49
- * `goal: null` (the 404 is absorbed). Live-updates on `goal.*` events
50
- * pass `options.events` from `useSessionEvents` to reuse its stream.
66
+ * `goal: null` (the 404 is absorbed). Live-updates on goal, turn, session,
67
+ * control, and system-update events — pass `options.events` from
68
+ * `useSessionEvents` to reuse its stream.
51
69
  */
52
70
  export function useGoal(
53
71
  sessionId: string | null | undefined,
@@ -124,7 +142,7 @@ export function useGoal(
124
142
  }, [load, enabled, workspaceId, sessionId, options.pollIntervalMs, sharedFeed]);
125
143
 
126
144
  const scheduleRefresh = useDebouncedCallback(() => void load());
127
- useSessionEventTrigger(client, workspaceId, sessionId, isGoalEvent, scheduleRefresh, {
145
+ useSessionEventTrigger(client, workspaceId, sessionId, isGoalRefreshEvent, scheduleRefresh, {
128
146
  enabled,
129
147
  ...(sharedEvents !== undefined ? { events: sharedEvents } : {}),
130
148
  });
@@ -858,6 +858,11 @@ async function loadPreviousPage(
858
858
  before,
859
859
  limit: requested,
860
860
  compact: true,
861
+ // Monitoring summaries omit correlation fields once a bounded durable
862
+ // payload exceeds their preview threshold. The browser timeline needs the
863
+ // sanitized stored payload so persisted tool outputs still match their
864
+ // calls and expose truthful truncation telemetry after a reload.
865
+ payloadMode: "full",
861
866
  });
862
867
  if (options.signal?.aborted) {
863
868
  throw abortError();
@@ -1,6 +1,6 @@
1
1
  import type { SessionEvent, SessionLineageResponse } from "@opengeni/sdk";
2
2
  import { useCallback, useEffect, useRef } from "react";
3
- import { useOpenGeni, type ClientOverride } from "../provider";
3
+ import { useOpenGeni, type ClientOverride } from "../session-context";
4
4
  import { useDebouncedCallback, usePolledValue, useSessionEventTrigger } from "./internal";
5
5
 
6
6
  export type UseSessionLineageOptions = ClientOverride & {
@@ -1,6 +1,6 @@
1
1
  import type { Session, SessionEvent } from "@opengeni/sdk";
2
- import { useCallback, useEffect, useState } from "react";
3
- import { useOpenGeni, type ClientOverride } from "../provider";
2
+ import { useCallback, useEffect, useMemo, useState } from "react";
3
+ import { useOpenGeni, type ClientOverride } from "../session-context";
4
4
  import {
5
5
  useMutationRunner,
6
6
  usePolledValue,
@@ -66,10 +66,13 @@ export function useSession(
66
66
  const base = data ?? null;
67
67
  // The override only ever carries title/titleSource patches; it is reset on
68
68
  // every fresh load so it can never go stale against the server snapshot.
69
- const session =
70
- base && override && override.id === base.id
71
- ? { ...base, title: override.title, titleSource: override.titleSource }
72
- : base;
69
+ const session = useMemo(
70
+ () =>
71
+ base && override && override.id === base.id
72
+ ? { ...base, title: override.title, titleSource: override.titleSource }
73
+ : base,
74
+ [base, override],
75
+ );
73
76
 
74
77
  // Live-patch the title on auto (agent) + cross-client (user/agent) renames so
75
78
  // the UI reflects the new title without polling or a full re-fetch.
@@ -1,5 +1,5 @@
1
1
  import type { Session } from "@opengeni/sdk";
2
- import { useCallback } from "react";
2
+ import { useCallback, useEffect, useRef } from "react";
3
3
  import { useOpenGeni, type ClientOverride } from "../provider";
4
4
  import { usePolledValue } from "./internal";
5
5
 
@@ -8,6 +8,8 @@ export type UseWorkspaceSessionsOptions = ClientOverride & {
8
8
  parentSessionId?: string | null | undefined;
9
9
  cursor?: string | undefined;
10
10
  search?: string | undefined;
11
+ /** Return only the complete personal pinned projection. */
12
+ pinsOnly?: boolean | undefined;
11
13
  /** Refresh interval (ms) for fleet/manager views. Off by default. */
12
14
  pollIntervalMs?: number | undefined;
13
15
  enabled?: boolean | undefined;
@@ -38,7 +40,21 @@ export function useWorkspaceSessions(
38
40
  const parentSessionId = options.parentSessionId;
39
41
  const cursor = options.cursor;
40
42
  const search = options.search;
41
- const queryKey = JSON.stringify({ workspaceId, limit, parentSessionId, cursor, search });
43
+ const pinsOnly = options.pinsOnly;
44
+ const enabled = options.enabled ?? true;
45
+ const queryKey = [
46
+ workspaceId,
47
+ limit ?? "",
48
+ parentSessionId === null ? "null" : (parentSessionId ?? ""),
49
+ cursor ?? "",
50
+ search ?? "",
51
+ pinsOnly ? "1" : "",
52
+ ].join("\u0000");
53
+ const previousQueryKey = useRef(queryKey);
54
+ const queryKeyTransition = previousQueryKey.current !== queryKey;
55
+ useEffect(() => {
56
+ previousQueryKey.current = queryKey;
57
+ }, [queryKey]);
42
58
  const load = useCallback(
43
59
  async () => ({
44
60
  queryKey,
@@ -47,13 +63,14 @@ export function useWorkspaceSessions(
47
63
  ...(parentSessionId !== undefined ? { parentSessionId } : {}),
48
64
  ...(cursor !== undefined ? { cursor } : {}),
49
65
  ...(search !== undefined ? { search } : {}),
66
+ ...(pinsOnly ? { pinsOnly: true } : {}),
50
67
  }),
51
68
  }),
52
- [client, workspaceId, limit, parentSessionId, cursor, search, queryKey],
69
+ [client, workspaceId, limit, parentSessionId, cursor, search, pinsOnly, queryKey],
53
70
  );
54
71
  const state = usePolledValue(load, {
55
72
  pollIntervalMs: options.pollIntervalMs,
56
- enabled: options.enabled,
73
+ enabled,
57
74
  });
58
75
  // usePolledValue drops stale async completions, while the explicit query key
59
76
  // also prevents the previous query's cached value from painting for the one
@@ -69,7 +86,15 @@ export function useWorkspaceSessions(
69
86
  pinned,
70
87
  pinnedTruncated: page?.pinnedTruncated ?? false,
71
88
  nextCursor: page?.nextCursor ?? null,
72
- loading: state.loading,
89
+ // `usePolledValue` clears the old data and starts the new request in an
90
+ // effect. During that query-key transition render, its old loading flag
91
+ // can still be false; expose loading immediately so consumers do not
92
+ // announce a transient false zero-match result.
93
+ loading:
94
+ enabled &&
95
+ (state.loading ||
96
+ queryKeyTransition ||
97
+ (state.data !== null && state.data.queryKey !== queryKey)),
73
98
  error: state.error,
74
99
  refresh: state.refresh,
75
100
  };
@@ -0,0 +1,13 @@
1
+ // @opengeni/react/session-ui — styled surfaces used by the session route.
2
+ // Keep these separate from the hook-only session entry and the broad root barrel
3
+ // so session hosts do not pay for unrelated React surfaces.
4
+ export { HumanInputForm } from "./components/human-input-form";
5
+ export type {
6
+ HumanInputAnswerDraft,
7
+ HumanInputFormMessages,
8
+ HumanInputFormProps,
9
+ } from "./components/human-input-form";
10
+ export { MessageTimeline, TimelineRow } from "./components/message-timeline";
11
+ export type { MessageTimelineProps } from "./components/message-timeline";
12
+ export { QueueSurface } from "./components/queue-surface";
13
+ export type { QueueSurfaceProps } from "./components/queue-surface";
package/src/session.ts CHANGED
@@ -15,6 +15,8 @@ export type {
15
15
  EmbeddedSessionClientOverride as ClientOverride,
16
16
  } from "./session-context";
17
17
 
18
+ export { useSession, isTitleEvent } from "./hooks/use-session";
19
+ export type { UseSessionOptions, UseSessionResult } from "./hooks/use-session";
18
20
  export { useSessionEvents } from "./hooks/use-session-events";
19
21
  export type {
20
22
  SessionEventsConnectionState,
@@ -29,12 +31,25 @@ export {
29
31
  FILE_ONLY_MESSAGE_TEXT,
30
32
  } from "./hooks/use-composer";
31
33
  export type { ComposerSendExtras, ComposerState, UseComposerOptions } from "./hooks/use-composer";
34
+ export { useFileAttachments } from "./hooks/use-file-attachments";
35
+ export type {
36
+ FileAttachment,
37
+ UseFileAttachmentsOptions,
38
+ UseFileAttachmentsResult,
39
+ } from "./hooks/use-file-attachments";
32
40
  export { useTurnQueue, isTurnQueueEvent } from "./hooks/use-turn-queue";
33
41
  export type {
34
42
  QueueMutationKind,
35
43
  UseTurnQueueOptions,
36
44
  UseTurnQueueResult,
37
45
  } from "./hooks/use-turn-queue";
46
+ export { useGoal, isGoalEvent } from "./hooks/use-goal";
47
+ export type { UseGoalOptions, UseGoalResult } from "./hooks/use-goal";
48
+ export { useSessionLineage, isLineageRefreshEvent } from "./hooks/use-session-lineage";
49
+ export type {
50
+ UseSessionLineageOptions,
51
+ UseSessionLineageResult,
52
+ } from "./hooks/use-session-lineage";
38
53
  export { useSessionControl } from "./hooks/use-session-control";
39
54
  export type {
40
55
  UseSessionControlOptions,
@@ -5,15 +5,19 @@ import {
5
5
  BrainIcon,
6
6
  SquareTerminalIcon,
7
7
  } from "lucide-react";
8
+ import { lazy, Suspense } from "react";
9
+ import { jsx as rowJsx, jsxs as rowJsxs } from "react/jsx-runtime";
8
10
  import { cn } from "../lib/cn";
9
11
  import { truncate } from "../lib/format";
10
12
  import { defaultToolRegistry } from "./tool-renderers";
11
13
  import { useEntranceAnimation } from "./entrance";
12
14
  import type { ToolRegistry } from "./registry";
13
15
  import { BodyNote, PayloadBlock, ActivityDisclosure } from "./shared";
14
- import { toolDisplayName } from "./projection";
16
+ import { toolDisplayName } from "./tool-display-name";
15
17
  import type { ActivityItem, MemoryItem, ReasoningItem, SandboxItem, WorkerItem } from "./types";
16
18
 
19
+ const LazyFleetDecisionRow = lazy(() => import("./fleet-decision-row"));
20
+
17
21
  /* ----------------------------------------------------------------------------
18
22
  Activity rail
19
23
 
@@ -79,7 +83,7 @@ export function ActivityRail({
79
83
  const newFamily = index > 0 && familyOf(item) !== familyOf(items[index - 1]!);
80
84
  const row = renderActivity(item, toolRegistry, onOpenSession, onMemoryClick);
81
85
  return (
82
- <div key={item.id} className={cn(newFamily && "mt-3")}>
86
+ <div key={item.id} data-og-timeline-row-anchor="" className={cn(newFamily && "mt-3")}>
83
87
  {row}
84
88
  </div>
85
89
  );
@@ -112,6 +116,18 @@ function renderActivity(
112
116
  return <SandboxRow item={item} />;
113
117
  case "memory":
114
118
  return <MemoryRow item={item} onMemoryClick={onMemoryClick} />;
119
+ case "fleet-decision":
120
+ return (
121
+ <Suspense fallback={null}>
122
+ <LazyFleetDecisionRow
123
+ item={item}
124
+ d={ActivityDisclosure}
125
+ b={BodyNote}
126
+ j={rowJsx}
127
+ s={rowJsxs}
128
+ />
129
+ </Suspense>
130
+ );
115
131
  default:
116
132
  return assertNever(item);
117
133
  }