@opengeni/react 0.44.1 → 0.44.3

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.
package/dist/composer.js CHANGED
@@ -37,8 +37,8 @@ import {
37
37
  useChatComposer,
38
38
  useChatComposerController,
39
39
  useVoiceInput
40
- } from "./chunk-KC27K42G.js";
41
- import "./chunk-Q2NCKWTK.js";
40
+ } from "./chunk-QQRM3DO3.js";
41
+ import "./chunk-HFO4ERGQ.js";
42
42
  import "./chunk-EB67J347.js";
43
43
  import {
44
44
  BillingClassMark,
package/dist/index.js CHANGED
@@ -109,7 +109,7 @@ import {
109
109
  useLightboxOptional,
110
110
  useThemeType,
111
111
  v4aToGitFileDiff
112
- } from "./chunk-KG5F2OKE.js";
112
+ } from "./chunk-YNYIAYXQ.js";
113
113
  import "./chunk-YDNWMKXO.js";
114
114
  import {
115
115
  buildTimeline,
@@ -171,14 +171,14 @@ import {
171
171
  useSlashCommands,
172
172
  useTranscription,
173
173
  useVoiceInput
174
- } from "./chunk-KC27K42G.js";
174
+ } from "./chunk-QQRM3DO3.js";
175
175
  import {
176
176
  FILE_ONLY_MESSAGE_TEXT,
177
177
  composeSendInput,
178
178
  shouldSteerOnKey,
179
179
  shouldSubmitOnKey,
180
180
  useComposer
181
- } from "./chunk-Q2NCKWTK.js";
181
+ } from "./chunk-HFO4ERGQ.js";
182
182
  import {
183
183
  useMutationRunner,
184
184
  usePolledValue,
@@ -7,7 +7,7 @@ import {
7
7
  SessionChrome,
8
8
  TimelineRow,
9
9
  sessionChromeGoalPillState
10
- } from "./chunk-KG5F2OKE.js";
10
+ } from "./chunk-YNYIAYXQ.js";
11
11
  import "./chunk-YDNWMKXO.js";
12
12
  import "./chunk-LWR4MXSS.js";
13
13
  import "./chunk-ATSTR5P3.js";
package/dist/session.js CHANGED
@@ -33,7 +33,7 @@ import {
33
33
  shouldSteerOnKey,
34
34
  shouldSubmitOnKey,
35
35
  useComposer
36
- } from "./chunk-Q2NCKWTK.js";
36
+ } from "./chunk-HFO4ERGQ.js";
37
37
  import "./chunk-EB67J347.js";
38
38
  import "./chunk-EKZH6IDG.js";
39
39
  import "./chunk-LAKLF4PA.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/react",
3
- "version": "0.44.1",
3
+ "version": "0.44.3",
4
4
  "description": "React hooks and styled components for OpenGeni: live session streaming, chat composer, message timeline, session status, and fleet views — token-themed (CSS variables), dark-first, built on Tailwind v4 + Radix + Motion.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -75,7 +75,7 @@
75
75
  "@dnd-kit/utilities": "3.2.2",
76
76
  "@fontsource-variable/noto-sans-arabic": "5.2.10",
77
77
  "@fontsource-variable/noto-sans-jp": "5.2.10",
78
- "@opengeni/sdk": "^0.44.0",
78
+ "@opengeni/sdk": "^0.44.3",
79
79
  "clsx": "^2.1.1",
80
80
  "lucide-react": "^1.8.0",
81
81
  "motion": "^12.0.0",
@@ -585,6 +585,22 @@ export function useComposer(
585
585
  }
586
586
  void loadDraft(false);
587
587
  }, [durableDrafts, loadDraft, sessionId]);
588
+ // After long background / sleep the in-memory revision is often stale while
589
+ // a prior autosave already advanced the server. Soft-reload on wake so the
590
+ // next keystroke does not OCC against a dead revision.
591
+ useEffect(() => {
592
+ if (!sessionId || !durableDrafts) return;
593
+ const onWake = () => {
594
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
595
+ void loadDraft(false);
596
+ };
597
+ document.addEventListener("visibilitychange", onWake);
598
+ window.addEventListener("pageshow", onWake);
599
+ return () => {
600
+ document.removeEventListener("visibilitychange", onWake);
601
+ window.removeEventListener("pageshow", onWake);
602
+ };
603
+ }, [durableDrafts, loadDraft, sessionId]);
588
604
  useEffect(() => {
589
605
  if (!sessionId || !durableDrafts) return;
590
606
  return registerSessionReconciler(sessionId, "composer", async () => await loadDraft(false));
@@ -692,7 +708,16 @@ export function useComposer(
692
708
  }
693
709
  setDraftSaving(true);
694
710
  try {
695
- const saved = await client.saveComposerDraft(workspaceId, sessionId, request);
711
+ const saved = await saveComposerDraftWithStaleRetry({
712
+ client,
713
+ workspaceId,
714
+ sessionId,
715
+ request,
716
+ onAdoptRemote: (remote) => {
717
+ draftRef.current = remote;
718
+ setDraft(remote);
719
+ },
720
+ });
696
721
  if (
697
722
  targetKeyRef.current !== ownedTargetKey ||
698
723
  targetGeneration.current !== ownedGeneration
@@ -701,8 +726,12 @@ export function useComposer(
701
726
  }
702
727
  draftRef.current = saved;
703
728
  setDraft(saved);
704
- lastSavedSignature.current = signature;
729
+ lastSavedSignature.current = draftSignature({
730
+ ...request,
731
+ expectedRevision: saved.revision,
732
+ });
705
733
  setDraftConflict(null);
734
+ setError(null);
706
735
  success = true;
707
736
  } catch (cause) {
708
737
  if (
@@ -1260,11 +1289,13 @@ export function useComposer(
1260
1289
  }
1261
1290
  if (choice === "use_remote") {
1262
1291
  applyDraft(remote);
1292
+ setError(null);
1263
1293
  return;
1264
1294
  }
1265
1295
  draftRef.current = remote;
1266
1296
  setDraft(remote);
1267
1297
  setDraftConflict(null);
1298
+ setError(null);
1268
1299
  const payload = currentDraftPayload();
1269
1300
  if (payload) await persistPayload({ ...payload, expectedRevision: remote.revision });
1270
1301
  },
@@ -1393,13 +1424,52 @@ function asError(cause: unknown): Error {
1393
1424
 
1394
1425
  function isDraftConflictError(error: Error): boolean {
1395
1426
  const apiError = error as Partial<OpenGeniApiError>;
1396
- return (
1397
- apiError.status === 409 &&
1398
- apiError.outcomeUnknown === false &&
1399
- (apiError.code === undefined ||
1400
- apiError.code === "conflict" ||
1401
- apiError.code === "idempotency_conflict")
1402
- );
1427
+ if (apiError.status !== 409 || apiError.outcomeUnknown === true) return false;
1428
+ // Production queue OCC returns `DRAFT_CHANGED`. Older/SDK-shaped 409s may
1429
+ // omit code or use the generic conflict labels — all are recoverable OCC.
1430
+ const code = apiError.code;
1431
+ if (
1432
+ code === undefined ||
1433
+ code === "DRAFT_CHANGED" ||
1434
+ code === "conflict" ||
1435
+ code === "idempotency_conflict"
1436
+ ) {
1437
+ return true;
1438
+ }
1439
+ return /draft changed/i.test(error.message);
1440
+ }
1441
+
1442
+ /**
1443
+ * One OCC retry: adopt the server revision and rewrite the same local content.
1444
+ * Covers the common "tab slept through a successful autosave" case without
1445
+ * stranding the operator on a raw 409 toast.
1446
+ */
1447
+ async function saveComposerDraftWithStaleRetry(input: {
1448
+ client: {
1449
+ getComposerDraft: (workspaceId: string, sessionId: string) => Promise<ComposerDraft>;
1450
+ saveComposerDraft: (
1451
+ workspaceId: string,
1452
+ sessionId: string,
1453
+ request: SaveComposerDraftRequest,
1454
+ ) => Promise<ComposerDraft>;
1455
+ };
1456
+ workspaceId: string;
1457
+ sessionId: string;
1458
+ request: SaveComposerDraftRequest;
1459
+ onAdoptRemote: (remote: ComposerDraft) => void;
1460
+ }): Promise<ComposerDraft> {
1461
+ try {
1462
+ return await input.client.saveComposerDraft(input.workspaceId, input.sessionId, input.request);
1463
+ } catch (cause) {
1464
+ const problem = asError(cause);
1465
+ if (!isDraftConflictError(problem)) throw problem;
1466
+ const remote = await input.client.getComposerDraft(input.workspaceId, input.sessionId);
1467
+ input.onAdoptRemote(remote);
1468
+ return await input.client.saveComposerDraft(input.workspaceId, input.sessionId, {
1469
+ ...input.request,
1470
+ expectedRevision: remote.revision,
1471
+ });
1472
+ }
1403
1473
  }
1404
1474
 
1405
1475
  function draftPayload(draft: ComposerDraft): SaveComposerDraftRequest {
@@ -17,6 +17,7 @@ import {
17
17
  MessagesSquareIcon,
18
18
  MessageSquareIcon,
19
19
  MousePointer2Icon,
20
+ PackageSearchIcon,
20
21
  PanelsTopLeftIcon,
21
22
  PlugIcon,
22
23
  SearchIcon,
@@ -1033,6 +1034,204 @@ function SecretSetRenderer({ item }: ToolRendererProps) {
1033
1034
  );
1034
1035
  }
1035
1036
 
1037
+ /* ---- tool_search (progressive MCP disclosure) ------------------------------ */
1038
+
1039
+ type DisclosedTool = {
1040
+ /** Full wire name (`server__leaf` or bare). */
1041
+ name: string;
1042
+ /** Server / namespace prefix before `__`, when present. */
1043
+ source: string | null;
1044
+ /** Leaf tool name after `__`. */
1045
+ leaf: string;
1046
+ };
1047
+
1048
+ function splitToolWireName(name: string): DisclosedTool {
1049
+ const boundary = name.indexOf("__");
1050
+ if (boundary <= 0) {
1051
+ return { name, source: null, leaf: name };
1052
+ }
1053
+ return {
1054
+ name,
1055
+ source: name.slice(0, boundary),
1056
+ leaf: name.slice(boundary + 2),
1057
+ };
1058
+ }
1059
+
1060
+ /** Capability query from live tool_search args (object or JSON string). */
1061
+ function toolSearchQuery(item: ToolRendererProps["item"]): string {
1062
+ const fromArgs = parseToolArgs(item.arguments);
1063
+ if (typeof fromArgs.query === "string" && fromArgs.query.trim()) {
1064
+ return fromArgs.query.trim();
1065
+ }
1066
+ const raw = item.raw;
1067
+ if (raw && typeof raw === "object") {
1068
+ const rawArgs = (raw as { arguments?: unknown }).arguments;
1069
+ if (typeof rawArgs === "string" && rawArgs.trim()) {
1070
+ const parsed = tryParseJson(rawArgs);
1071
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1072
+ const query = (parsed as { query?: unknown }).query;
1073
+ if (typeof query === "string" && query.trim()) {
1074
+ return query.trim();
1075
+ }
1076
+ }
1077
+ } else if (rawArgs && typeof rawArgs === "object" && !Array.isArray(rawArgs)) {
1078
+ const query = (rawArgs as { query?: unknown }).query;
1079
+ if (typeof query === "string" && query.trim()) {
1080
+ return query.trim();
1081
+ }
1082
+ }
1083
+ }
1084
+ return "";
1085
+ }
1086
+
1087
+ /**
1088
+ * Parse disclosed tools from the runtime event shape.
1089
+ * `normalizeSdkEvent` collapses `tool_search_output.tools[]` into text:
1090
+ * "Disclosed tools: a, b" | "No matching tools found."
1091
+ * Also accept a structured `tools` array when a host/enricher preserves it.
1092
+ */
1093
+ function parseDisclosedTools(output: unknown): DisclosedTool[] | null {
1094
+ if (output && typeof output === "object" && !Array.isArray(output)) {
1095
+ const tools = (output as { tools?: unknown }).tools;
1096
+ if (Array.isArray(tools)) {
1097
+ return tools
1098
+ .map((tool) => {
1099
+ if (typeof tool === "string" && tool.trim()) {
1100
+ return splitToolWireName(tool.trim());
1101
+ }
1102
+ if (
1103
+ tool &&
1104
+ typeof tool === "object" &&
1105
+ typeof (tool as { name?: unknown }).name === "string"
1106
+ ) {
1107
+ const name = (tool as { name: string }).name.trim();
1108
+ return name ? splitToolWireName(name) : null;
1109
+ }
1110
+ return null;
1111
+ })
1112
+ .filter((tool): tool is DisclosedTool => tool != null);
1113
+ }
1114
+ }
1115
+
1116
+ const { text } = unwrapMcpOutput(output);
1117
+ const trimmed = text.trim();
1118
+ if (!trimmed) {
1119
+ return null;
1120
+ }
1121
+ if (/^no matching tools found\.?$/i.test(trimmed)) {
1122
+ return [];
1123
+ }
1124
+ const disclosed = trimmed.match(/^disclosed tools:\s*(.+)$/i);
1125
+ if (disclosed?.[1]) {
1126
+ return disclosed[1]
1127
+ .split(",")
1128
+ .map((part) => part.trim())
1129
+ .filter(Boolean)
1130
+ .map(splitToolWireName);
1131
+ }
1132
+ const parsed = tryParseJson(trimmed);
1133
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1134
+ return parseDisclosedTools(parsed);
1135
+ }
1136
+ return null;
1137
+ }
1138
+
1139
+ function toolSearchPreview(tools: DisclosedTool[] | null, cancelled: boolean): string | undefined {
1140
+ if (cancelled) {
1141
+ return undefined;
1142
+ }
1143
+ if (!tools) {
1144
+ return "Done";
1145
+ }
1146
+ if (tools.length === 0) {
1147
+ return "No matches";
1148
+ }
1149
+ if (tools.length === 1) {
1150
+ return tools[0]!.leaf;
1151
+ }
1152
+ const head = tools[0]!.leaf;
1153
+ return `${tools.length} tools · ${truncatePreview(head, 28)}`;
1154
+ }
1155
+
1156
+ function ToolSearchRenderer({ item }: ToolRendererProps) {
1157
+ const query = toolSearchQuery(item);
1158
+ const icon = <PackageSearchIcon className={ICON_SIZE} />;
1159
+ const running = item.status === "running";
1160
+ const queryPreview = query ? truncatePreview(query, 64) : "";
1161
+
1162
+ if (running) {
1163
+ return (
1164
+ <ActivityDisclosure
1165
+ icon={icon}
1166
+ iconTone="running"
1167
+ title="Looking up tools"
1168
+ running
1169
+ preview={
1170
+ queryPreview ? (
1171
+ <RunningPreview>{queryPreview}</RunningPreview>
1172
+ ) : (
1173
+ <RunningPreview>Matching capabilities…</RunningPreview>
1174
+ )
1175
+ }
1176
+ >
1177
+ {query ? <BodyNote>capability query: {query}</BodyNote> : null}
1178
+ <PayloadBlock label="Arguments" value={redactSecrets(parseToolArgs(item.arguments))} />
1179
+ </ActivityDisclosure>
1180
+ );
1181
+ }
1182
+
1183
+ const { text: outText, isError } = unwrapMcpOutput(item.output);
1184
+ if ((isError || item.status === "failed") && item.status !== "cancelled") {
1185
+ return (
1186
+ <ActivityDisclosure
1187
+ icon={icon}
1188
+ iconTone="failed"
1189
+ title="Tool lookup failed"
1190
+ failed
1191
+ preview={truncatePreview(outText, 80) || queryPreview || "Lookup failed"}
1192
+ >
1193
+ {query ? <BodyNote>capability query: {query}</BodyNote> : null}
1194
+ <PayloadBlock label="Arguments" value={redactSecrets(parseToolArgs(item.arguments))} />
1195
+ <PayloadBlock label="Error" value={outText} failed />
1196
+ </ActivityDisclosure>
1197
+ );
1198
+ }
1199
+
1200
+ const tools = parseDisclosedTools(item.output);
1201
+ const preview = toolSearchPreview(tools, item.status === "cancelled");
1202
+
1203
+ return (
1204
+ <ActivityDisclosure
1205
+ icon={icon}
1206
+ iconTone="muted"
1207
+ title="Looked up tools"
1208
+ cancelled={item.status === "cancelled"}
1209
+ preview={preview}
1210
+ >
1211
+ {query ? <BodyNote>capability query: {query}</BodyNote> : null}
1212
+ {tools && tools.length > 0 ? (
1213
+ <ul className="grid gap-1.5">
1214
+ {tools.slice(0, 12).map((tool) => (
1215
+ <li key={tool.name} className="flex min-w-0 items-baseline gap-2">
1216
+ {tool.source ? (
1217
+ <span className="shrink-0 text-og-xs text-og-fg-subtle">{tool.source}</span>
1218
+ ) : null}
1219
+ <span className="truncate font-mono text-og-sm text-og-fg">{tool.leaf}</span>
1220
+ </li>
1221
+ ))}
1222
+ {tools.length > 12 ? (
1223
+ <li className="text-og-xs text-og-fg-muted">+{tools.length - 12} more</li>
1224
+ ) : null}
1225
+ </ul>
1226
+ ) : tools && tools.length === 0 ? (
1227
+ <BodyNote>no deferred tools matched this capability query.</BodyNote>
1228
+ ) : null}
1229
+ <PayloadBlock label="Arguments" value={redactSecrets(parseToolArgs(item.arguments))} />
1230
+ {tools == null && outText ? <PayloadBlock label="Result" value={outText} /> : null}
1231
+ </ActivityDisclosure>
1232
+ );
1233
+ }
1234
+
1036
1235
  /* ---- docs / knowledge search ----------------------------------------------- */
1037
1236
 
1038
1237
  function DocsSearchRenderer({ item }: ToolRendererProps) {
@@ -1577,9 +1776,11 @@ function GenericToolIcon({ name }: { name: string }) {
1577
1776
  leaf.includes("knowledge") ||
1578
1777
  leaf === "list_document_bases"
1579
1778
  ? FileSearchIcon
1580
- : leaf === "tool_search" || leaf === "load_skill"
1581
- ? PlugIcon
1582
- : WrenchIcon;
1779
+ : leaf === "tool_search"
1780
+ ? PackageSearchIcon
1781
+ : leaf === "load_skill"
1782
+ ? PlugIcon
1783
+ : WrenchIcon;
1583
1784
  return <Icon className={ICON_SIZE} />;
1584
1785
  }
1585
1786
 
@@ -1591,6 +1792,7 @@ const BASE_ENTRIES: ToolRegistryEntry[] = [
1591
1792
  { match: "rawType", type: "apply_patch_call", render: ApplyPatchRenderer },
1592
1793
  { match: "rawType", type: "computer_call", render: ComputerCallRenderer },
1593
1794
  { match: "rawType", type: "hosted_tool_call", render: WebSearchRenderer },
1795
+ { match: "rawType", type: "tool_search_call", render: ToolSearchRenderer },
1594
1796
  // First-party sandbox + MCP tools resolve by name (exact or MCP leaf).
1595
1797
  { match: "name", name: "exec_command", render: ExecRenderer },
1596
1798
  { match: "name", name: "request_human_input", render: AskRenderer },
@@ -1609,6 +1811,7 @@ const BASE_ENTRIES: ToolRegistryEntry[] = [
1609
1811
  { match: "name", name: "computer_keypress", render: ComputerCallRenderer },
1610
1812
  { match: "name", name: "computer_drag", render: ComputerCallRenderer },
1611
1813
  { match: "name", name: "web_search_call", render: WebSearchRenderer },
1814
+ { match: "name", name: "tool_search", render: ToolSearchRenderer },
1612
1815
  { match: "name", name: "view_image", render: ViewImageRenderer },
1613
1816
  { match: "name", name: "environment_set_variable", render: SecretSetRenderer },
1614
1817
  { match: "name", name: "variable_set_set_variable", render: SecretSetRenderer },