@opengeni/react 3.8.0-canary.1 → 4.0.1-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 (63) hide show
  1. package/README.md +10 -24
  2. package/dist/artifacts.js +7 -7
  3. package/dist/{browser-viewer-SS46LUAC.js → browser-viewer-NTD6UIPR.js} +3 -3
  4. package/dist/{chunk-VEPNNDNG.js → chunk-24M4YBCL.js} +4 -4
  5. package/dist/{chunk-ELIYWBZR.js → chunk-BV5A4OTE.js} +4 -4
  6. package/dist/{chunk-7W6PVVTK.js → chunk-CQG7YUJB.js} +2582 -1037
  7. package/dist/chunk-CQG7YUJB.js.map +1 -0
  8. package/dist/{chunk-BXJOQFCD.js → chunk-HUMIRRYI.js} +84 -221
  9. package/dist/chunk-HUMIRRYI.js.map +1 -0
  10. package/dist/{chunk-7BWFU6H4.js → chunk-Z7WBDB4E.js} +395 -287
  11. package/dist/chunk-Z7WBDB4E.js.map +1 -0
  12. package/dist/components/human-input-form.d.ts +3 -1
  13. package/dist/components/human-input-surface.d.ts +3 -2
  14. package/dist/components/markdown-table-layout.d.ts +9 -0
  15. package/dist/components/session-conversation.d.ts +2 -0
  16. package/dist/composer.js +2 -3
  17. package/dist/{computer-viewer-FWEZISW4.js → computer-viewer-7TEZC6Y2.js} +3 -3
  18. package/dist/hooks/use-codex-accounts.d.ts +11 -1
  19. package/dist/hooks/use-session-events.d.ts +2 -2
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +44 -34
  22. package/dist/index.js.map +1 -1
  23. package/dist/interaction.js +3 -3
  24. package/dist/markdown-table-layout-3Z3AWBY7.js +71 -0
  25. package/dist/markdown-table-layout-3Z3AWBY7.js.map +1 -0
  26. package/dist/older-history.d.ts +3 -2
  27. package/dist/{platform-activity-row-BJHUHXAT.js → platform-activity-row-27LKAWZD.js} +2 -2
  28. package/dist/platform-activity-row-27LKAWZD.js.map +1 -0
  29. package/dist/realtime.js +3 -3
  30. package/dist/session-ui.js +6 -9
  31. package/dist/session.js +1 -1
  32. package/dist/timeline/types.d.ts +3 -0
  33. package/package.json +2 -6
  34. package/src/components/human-input-form.tsx +185 -53
  35. package/src/components/human-input-surface.tsx +3 -0
  36. package/src/components/markdown-table-layout.ts +85 -0
  37. package/src/components/markdown.tsx +20 -2
  38. package/src/components/message-timeline.tsx +47 -7
  39. package/src/components/session-commands-panel.tsx +7 -1
  40. package/src/components/session-conversation.tsx +4 -1
  41. package/src/hooks/use-codex-accounts.ts +32 -5
  42. package/src/hooks/use-session-events.ts +16 -277
  43. package/src/index.ts +0 -1
  44. package/src/older-history.ts +22 -4
  45. package/src/timeline/platform-activity-row.tsx +4 -1
  46. package/src/timeline/projection.ts +99 -9
  47. package/src/timeline/types.ts +3 -0
  48. package/dist/chat.d.ts +0 -37
  49. package/dist/chat.js +0 -315
  50. package/dist/chat.js.map +0 -1
  51. package/dist/chunk-7BWFU6H4.js.map +0 -1
  52. package/dist/chunk-7W6PVVTK.js.map +0 -1
  53. package/dist/chunk-BXJOQFCD.js.map +0 -1
  54. package/dist/chunk-HQV2I5HV.js +0 -124
  55. package/dist/chunk-HQV2I5HV.js.map +0 -1
  56. package/dist/chunk-YAIPFO5D.js +0 -1416
  57. package/dist/chunk-YAIPFO5D.js.map +0 -1
  58. package/dist/platform-activity-row-BJHUHXAT.js.map +0 -1
  59. package/src/chat.tsx +0 -393
  60. /package/dist/{browser-viewer-SS46LUAC.js.map → browser-viewer-NTD6UIPR.js.map} +0 -0
  61. /package/dist/{chunk-VEPNNDNG.js.map → chunk-24M4YBCL.js.map} +0 -0
  62. /package/dist/{chunk-ELIYWBZR.js.map → chunk-BV5A4OTE.js.map} +0 -0
  63. /package/dist/{computer-viewer-FWEZISW4.js.map → computer-viewer-7TEZC6Y2.js.map} +0 -0
@@ -7,19 +7,20 @@ import {
7
7
  useBrowserSession,
8
8
  useBrowserSessions,
9
9
  useSiteAuthConnections
10
- } from "./chunk-VEPNNDNG.js";
10
+ } from "./chunk-24M4YBCL.js";
11
11
  import {
12
12
  ComputerViewer,
13
13
  useComputerFrameStream,
14
14
  useComputerSession,
15
15
  useComputerSessions
16
- } from "./chunk-ELIYWBZR.js";
16
+ } from "./chunk-BV5A4OTE.js";
17
17
  import {
18
18
  useInteractionInterventions,
19
19
  useInteractionInvalidation
20
20
  } from "./chunk-H5YGYNXS.js";
21
21
  import "./chunk-4MDLEHXL.js";
22
22
  import "./chunk-5YK2ZDIG.js";
23
+ import "./chunk-JVYQRRC4.js";
23
24
  import "./chunk-PLYRL5ER.js";
24
25
  import "./chunk-KZ73RL76.js";
25
26
  import {
@@ -28,7 +29,6 @@ import {
28
29
  useEmbeddedInteraction,
29
30
  useEmbeddedInterventions
30
31
  } from "./chunk-NEXLLTP4.js";
31
- import "./chunk-JVYQRRC4.js";
32
32
  import "./chunk-22KP6LR5.js";
33
33
 
34
34
  // src/hooks/use-network-routes.ts
@@ -0,0 +1,71 @@
1
+ // src/components/markdown-table-layout.ts
2
+ function markdownTableWidth({
3
+ columnLeft,
4
+ columnWidth,
5
+ contentLeft,
6
+ contentRight,
7
+ preferredWidth
8
+ }) {
9
+ const center = columnLeft + columnWidth / 2;
10
+ const available = 2 * Math.min(center - contentLeft, contentRight - center);
11
+ return Math.max(columnWidth, Math.min(preferredWidth, available));
12
+ }
13
+ function observeMarkdownTableLayout(wrapper, table) {
14
+ const body = wrapper.parentElement;
15
+ const scroller = body?.closest("[data-og-wide-table-message]")?.closest("[data-og-timeline-scroller]");
16
+ if (!body?.classList.contains("og-markdown-body") || !scroller) return;
17
+ let width = null;
18
+ const reset = () => {
19
+ width = null;
20
+ wrapper.style.width = "";
21
+ wrapper.style.maxWidth = "";
22
+ wrapper.style.marginInline = "";
23
+ };
24
+ const measure = () => {
25
+ for (let ancestor = body; ancestor && ancestor !== scroller; ancestor = ancestor.parentElement) {
26
+ if (getComputedStyle(ancestor).overflowX !== "visible") {
27
+ reset();
28
+ return;
29
+ }
30
+ }
31
+ const column = body.getBoundingClientRect();
32
+ const panel = scroller.getBoundingClientRect();
33
+ const panelStyle = getComputedStyle(scroller);
34
+ const left = panel.left + scroller.clientLeft + parseFloat(panelStyle.paddingLeft);
35
+ const right = panel.left + scroller.clientLeft + scroller.clientWidth - parseFloat(panelStyle.paddingRight);
36
+ const previousWidth = table.style.width;
37
+ let preferred;
38
+ try {
39
+ table.style.width = "max-content";
40
+ preferred = table.getBoundingClientRect().width;
41
+ } finally {
42
+ table.style.width = previousWidth;
43
+ }
44
+ const next = markdownTableWidth({
45
+ columnLeft: column.left,
46
+ columnWidth: column.width,
47
+ contentLeft: left,
48
+ contentRight: right,
49
+ preferredWidth: preferred
50
+ });
51
+ if (width !== null && Math.abs(width - next) < 0.5) return;
52
+ width = next;
53
+ wrapper.style.width = `${width}px`;
54
+ wrapper.style.maxWidth = "none";
55
+ wrapper.style.marginInline = `calc((100% - ${width}px) / 2)`;
56
+ };
57
+ measure();
58
+ const observer = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(measure);
59
+ observer?.observe(scroller);
60
+ observer?.observe(body);
61
+ observer?.observe(table);
62
+ return () => {
63
+ observer?.disconnect();
64
+ reset();
65
+ };
66
+ }
67
+ export {
68
+ markdownTableWidth,
69
+ observeMarkdownTableLayout
70
+ };
71
+ //# sourceMappingURL=markdown-table-layout-3Z3AWBY7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/markdown-table-layout.ts"],"sourcesContent":["export function markdownTableWidth({\n columnLeft,\n columnWidth,\n contentLeft,\n contentRight,\n preferredWidth,\n}: {\n columnLeft: number;\n columnWidth: number;\n contentLeft: number;\n contentRight: number;\n preferredWidth: number;\n}): number {\n const center = columnLeft + columnWidth / 2;\n const available = 2 * Math.min(center - contentLeft, contentRight - center);\n return Math.max(columnWidth, Math.min(preferredWidth, available));\n}\n\n/** Loaded only for assistant tables. Keep prose and nested scroll owners intact. */\nexport function observeMarkdownTableLayout(wrapper: HTMLDivElement, table: HTMLTableElement) {\n const body = wrapper.parentElement;\n const scroller = body\n ?.closest(\"[data-og-wide-table-message]\")\n ?.closest<HTMLElement>(\"[data-og-timeline-scroller]\");\n if (!body?.classList.contains(\"og-markdown-body\") || !scroller) return;\n\n let width: number | null = null;\n const reset = () => {\n width = null;\n wrapper.style.width = \"\";\n wrapper.style.maxWidth = \"\";\n wrapper.style.marginInline = \"\";\n };\n const measure = () => {\n // Intermediate clipped/scrollable surfaces own their own content bounds.\n for (\n let ancestor: HTMLElement | null = body;\n ancestor && ancestor !== scroller;\n ancestor = ancestor.parentElement\n ) {\n if (getComputedStyle(ancestor).overflowX !== \"visible\") {\n reset();\n return;\n }\n }\n const column = body.getBoundingClientRect();\n const panel = scroller.getBoundingClientRect();\n const panelStyle = getComputedStyle(scroller);\n const left = panel.left + scroller.clientLeft + parseFloat(panelStyle.paddingLeft);\n const right =\n panel.left + scroller.clientLeft + scroller.clientWidth - parseFloat(panelStyle.paddingRight);\n\n // Measure the original table, preserving its wrapping and TSV copy DOM.\n const previousWidth = table.style.width;\n let preferred: number;\n try {\n table.style.width = \"max-content\";\n preferred = table.getBoundingClientRect().width;\n } finally {\n table.style.width = previousWidth;\n }\n const next = markdownTableWidth({\n columnLeft: column.left,\n columnWidth: column.width,\n contentLeft: left,\n contentRight: right,\n preferredWidth: preferred,\n });\n if (width !== null && Math.abs(width - next) < 0.5) return;\n width = next;\n wrapper.style.width = `${width}px`;\n wrapper.style.maxWidth = \"none\";\n wrapper.style.marginInline = `calc((100% - ${width}px) / 2)`;\n };\n\n measure();\n const observer = typeof ResizeObserver === \"undefined\" ? undefined : new ResizeObserver(measure);\n observer?.observe(scroller);\n observer?.observe(body);\n observer?.observe(table);\n return () => {\n observer?.disconnect();\n reset();\n };\n}\n"],"mappings":";AAAO,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMW;AACT,QAAM,SAAS,aAAa,cAAc;AAC1C,QAAM,YAAY,IAAI,KAAK,IAAI,SAAS,aAAa,eAAe,MAAM;AAC1E,SAAO,KAAK,IAAI,aAAa,KAAK,IAAI,gBAAgB,SAAS,CAAC;AAClE;AAGO,SAAS,2BAA2B,SAAyB,OAAyB;AAC3F,QAAM,OAAO,QAAQ;AACrB,QAAM,WAAW,MACb,QAAQ,8BAA8B,GACtC,QAAqB,6BAA6B;AACtD,MAAI,CAAC,MAAM,UAAU,SAAS,kBAAkB,KAAK,CAAC,SAAU;AAEhE,MAAI,QAAuB;AAC3B,QAAM,QAAQ,MAAM;AAClB,YAAQ;AACR,YAAQ,MAAM,QAAQ;AACtB,YAAQ,MAAM,WAAW;AACzB,YAAQ,MAAM,eAAe;AAAA,EAC/B;AACA,QAAM,UAAU,MAAM;AAEpB,aACM,WAA+B,MACnC,YAAY,aAAa,UACzB,WAAW,SAAS,eACpB;AACA,UAAI,iBAAiB,QAAQ,EAAE,cAAc,WAAW;AACtD,cAAM;AACN;AAAA,MACF;AAAA,IACF;AACA,UAAM,SAAS,KAAK,sBAAsB;AAC1C,UAAM,QAAQ,SAAS,sBAAsB;AAC7C,UAAM,aAAa,iBAAiB,QAAQ;AAC5C,UAAM,OAAO,MAAM,OAAO,SAAS,aAAa,WAAW,WAAW,WAAW;AACjF,UAAM,QACJ,MAAM,OAAO,SAAS,aAAa,SAAS,cAAc,WAAW,WAAW,YAAY;AAG9F,UAAM,gBAAgB,MAAM,MAAM;AAClC,QAAI;AACJ,QAAI;AACF,YAAM,MAAM,QAAQ;AACpB,kBAAY,MAAM,sBAAsB,EAAE;AAAA,IAC5C,UAAE;AACA,YAAM,MAAM,QAAQ;AAAA,IACtB;AACA,UAAM,OAAO,mBAAmB;AAAA,MAC9B,YAAY,OAAO;AAAA,MACnB,aAAa,OAAO;AAAA,MACpB,aAAa;AAAA,MACb,cAAc;AAAA,MACd,gBAAgB;AAAA,IAClB,CAAC;AACD,QAAI,UAAU,QAAQ,KAAK,IAAI,QAAQ,IAAI,IAAI,IAAK;AACpD,YAAQ;AACR,YAAQ,MAAM,QAAQ,GAAG,KAAK;AAC9B,YAAQ,MAAM,WAAW;AACzB,YAAQ,MAAM,eAAe,gBAAgB,KAAK;AAAA,EACpD;AAEA,UAAQ;AACR,QAAM,WAAW,OAAO,mBAAmB,cAAc,SAAY,IAAI,eAAe,OAAO;AAC/F,YAAU,QAAQ,QAAQ;AAC1B,YAAU,QAAQ,IAAI;AACtB,YAAU,QAAQ,KAAK;AACvB,SAAO,MAAM;AACX,cAAU,WAAW;AACrB,UAAM;AAAA,EACR;AACF;","names":[]}
@@ -8,6 +8,7 @@
8
8
  */
9
9
  export type OlderHistoryLoadReceipt = Promise<boolean> & {
10
10
  readonly committed: boolean;
11
+ readonly tailPreserved?: boolean;
11
12
  };
12
13
  /**
13
14
  * Source-compatible public callback accepted by MessageTimeline.
@@ -20,10 +21,10 @@ export type OlderHistoryLoadReceipt = Promise<boolean> & {
20
21
  export type OlderHistoryLoader = () => unknown;
21
22
  type OlderHistoryReceiptCapture = (receipt: OlderHistoryLoadReceipt) => void;
22
23
  /** Bind a receipt before a synchronous forwarding stack can publish state. */
23
- export declare function invokeOlderHistoryLoaderWithReceiptCapture(load: OlderHistoryLoader, capture: OlderHistoryReceiptCapture): unknown;
24
+ export declare function invokeOlderHistoryLoaderWithReceiptCapture(load: OlderHistoryLoader, capture: OlderHistoryReceiptCapture, preserveTail?: boolean): unknown;
24
25
  /**
25
26
  * Build an await-compatible older-history receipt for custom timeline hosts.
26
27
  * Call `markCommitted` immediately before publishing an accepted older window.
27
28
  */
28
- export declare function createOlderHistoryLoadReceipt(load: (markCommitted: () => void) => boolean | Promise<boolean>): OlderHistoryLoadReceipt;
29
+ export declare function createOlderHistoryLoadReceipt(load: (markCommitted: () => void, preserveTail: boolean, markTailPreserved: () => void) => boolean | Promise<boolean>): OlderHistoryLoadReceipt;
29
30
  export {};
@@ -47,7 +47,7 @@ function PlatformActivityRow({
47
47
  return j(ActivityDisclosure, {
48
48
  icon: j(BotIcon, { className: "size-3.5" }),
49
49
  iconTone: failed ? "failed" : running ? "running" : "muted",
50
- title: startupPhaseTitle(item.phase, item.status, item.outcome),
50
+ title: item.blockedReason === "rotation_in_progress" ? "Waiting for sandbox rotation" : startupPhaseTitle(item.phase, item.status, item.outcome),
51
51
  preview: item.phase === "model_preparation" ? "Includes overlapping sandbox, rig, repository, and runtime setup shown below." : void 0,
52
52
  running,
53
53
  failed,
@@ -166,4 +166,4 @@ function startupDuration(durationMs) {
166
166
  export {
167
167
  PlatformActivityRow as default
168
168
  };
169
- //# sourceMappingURL=platform-activity-row-BJHUHXAT.js.map
169
+ //# sourceMappingURL=platform-activity-row-27LKAWZD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/timeline/platform-activity-row.tsx"],"sourcesContent":["import type { ReasoningItem, SandboxItem, StartupPhaseItem } from \"./types\";\n\ntype RowJsx = typeof import(\"react/jsx-runtime\").jsx;\ntype RowJsxs = typeof import(\"react/jsx-runtime\").jsxs;\n\nexport default function PlatformActivityRow({\n item,\n d: ActivityDisclosure,\n p: PayloadBlock,\n t: displayName,\n b: BotIcon,\n m: Markdown,\n r: truncate,\n j,\n s,\n}: {\n item: ReasoningItem | SandboxItem | StartupPhaseItem;\n d: typeof import(\"./shared\").ActivityDisclosure;\n p: typeof import(\"./shared\").PayloadBlock;\n t: (name: string) => string;\n b: typeof import(\"lucide-react\").BotIcon;\n m: typeof import(\"../components/markdown\").Markdown;\n r: typeof import(\"../lib/format\").truncate;\n j: RowJsx;\n s: RowJsxs;\n}) {\n if (item.kind === \"reasoning\") {\n return j(ActivityDisclosure, {\n icon: brainIcon(j, s),\n iconTone: \"muted\",\n title: item.streaming\n ? \"Thinking\"\n : j(\"span\", {\n className: \"font-normal italic text-og-fg-subtle\",\n children: \"Thought\",\n }),\n running: item.streaming,\n preview: truncate(item.text, 110),\n children: j(\"div\", {\n className: \"text-og-base leading-6 text-og-fg-muted [&_strong]:text-og-fg-muted\",\n children: j(Markdown, { streaming: item.streaming, children: item.text }),\n }),\n });\n }\n if (item.kind === \"sandbox\") {\n return s(ActivityDisclosure, {\n icon: terminalIcon(j, s),\n iconTone:\n item.status === \"failed\" ? \"failed\" : item.status === \"running\" ? \"running\" : \"muted\",\n title: sandboxRowTitle(item, displayName),\n running: item.status === \"running\",\n failed: item.status === \"failed\",\n cancelled: item.status === \"cancelled\",\n preview: item.command ?? undefined,\n children: [\n item.command ? j(PayloadBlock, { label: \"Command\", value: item.command }, \"command\") : null,\n item.output ? j(PayloadBlock, { label: \"Output\", value: item.output }, \"output\") : null,\n ],\n });\n }\n\n const failed = item.status === \"failed\";\n const running = item.status === \"running\";\n const duration = startupDuration(item.durationMs);\n return j(ActivityDisclosure, {\n icon: j(BotIcon, { className: \"size-3.5\" }),\n iconTone: failed ? \"failed\" : running ? \"running\" : \"muted\",\n title:\n item.blockedReason === \"rotation_in_progress\"\n ? \"Waiting for sandbox rotation\"\n : startupPhaseTitle(item.phase, item.status, item.outcome),\n preview:\n item.phase === \"model_preparation\"\n ? \"Includes overlapping sandbox, rig, repository, and runtime setup shown below.\"\n : undefined,\n running,\n failed,\n cancelled: item.status === \"cancelled\",\n chip: duration ? { tone: failed ? \"bad\" : \"muted\", text: duration } : undefined,\n expandable: false,\n });\n}\n\nfunction brainIcon(j: RowJsx, s: RowJsxs) {\n return lucideIcon(j, s, \"brain\", [\n j(\"path\", { d: \"M 12 18V5\" }, \"spine\"),\n j(\"path\", { d: \"M15 13a4.17 4.17 0 0 1-3-4 4.17 4.17 0 0 1-3 4\" }, \"center\"),\n j(\"path\", { d: \"M17.598 6.5A3 3 0 1 0 12 5a3 3 0 1 0-5.598 1.5\" }, \"top\"),\n j(\"path\", { d: \"M17.997 5.125a4 4 0 0 1 2.526 5.77\" }, \"upper-right\"),\n j(\"path\", { d: \"M18 18a4 4 0 0 0 2-7.464\" }, \"right\"),\n j(\"path\", { d: \"M19.967 17.483A4 4 0 1 1 12 18a4 4 0 1 1-7.967-.517\" }, \"base\"),\n j(\"path\", { d: \"M6 18a4 4 0 0 1-2-7.464\" }, \"left\"),\n j(\"path\", { d: \"M6.003 5.125a4 4 0 0 0-2.526 5.77\" }, \"upper-left\"),\n ]);\n}\n\nfunction terminalIcon(j: RowJsx, s: RowJsxs) {\n return lucideIcon(j, s, \"square-terminal\", [\n j(\"path\", { d: \"m7 11 2-2-2-2\" }, \"prompt\"),\n j(\"path\", { d: \"M11 13h4\" }, \"cursor\"),\n j(\"rect\", { width: 18, height: 18, x: 3, y: 3, rx: 2, ry: 2 }, \"frame\"),\n ]);\n}\n\nfunction lucideIcon(j: RowJsx, s: RowJsxs, name: string, children: ReturnType<RowJsx>[]) {\n return s(\"svg\", {\n \"aria-hidden\": \"true\",\n className: `lucide lucide-${name} size-3.5`,\n fill: \"none\",\n height: 24,\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: 2,\n viewBox: \"0 0 24 24\",\n width: 24,\n children,\n });\n}\n\nfunction sandboxRowTitle(item: SandboxItem, displayName: (name: string) => string): string {\n if (item.name === \"sandbox.provision\") {\n return startupPhaseTitle(\n \"sandbox\",\n item.status === \"cancelled\" ? \"complete\" : item.status,\n item.origin ?? null,\n );\n }\n return displayName(item.name);\n}\n\nfunction startupPhaseTitle(\n phase: StartupPhaseItem[\"phase\"],\n status: StartupPhaseItem[\"status\"],\n outcome: StartupPhaseItem[\"outcome\"],\n): string {\n if (status === \"complete\" && phase === \"sandbox\" && outcome && outcome !== \"skipped\") {\n return `Sandbox ${outcome === \"resumed\" ? \"reattached\" : outcome}`;\n }\n if (status === \"complete\" && phase === \"rig\" && outcome === \"skipped\") {\n return \"Rig already ready\";\n }\n const statusIndex =\n status === \"running\" ? 0 : status === \"failed\" ? 1 : status === \"cancelled\" ? 2 : 3;\n return STARTUP_PHASE_TITLES[phase][statusIndex];\n}\n\nconst STARTUP_PHASE_TITLES: Record<\n StartupPhaseItem[\"phase\"],\n readonly [string, string, string, string]\n> = {\n queue: [\n \"Waiting for a worker\",\n \"Worker startup failed\",\n \"Worker wait interrupted\",\n \"Worker started\",\n ],\n sandbox: [\n \"Starting sandbox\",\n \"Sandbox didn’t start\",\n \"Sandbox startup interrupted\",\n \"Sandbox ready\",\n ],\n rig: [\"Setting up rig\", \"Rig setup failed\", \"Rig setup interrupted\", \"Rig ready\"],\n repository: [\n \"Preparing repository\",\n \"Repository preparation failed\",\n \"Repository preparation interrupted\",\n \"Repository ready\",\n ],\n files: [\n \"Preparing files\",\n \"File preparation failed\",\n \"File preparation interrupted\",\n \"Files ready\",\n ],\n tools: [\n \"Connecting tools\",\n \"Tool connection failed\",\n \"Tool connection interrupted\",\n \"Tools ready\",\n ],\n model_preparation: [\n \"Preparing runtime and model request\",\n \"Runtime/model preparation failed\",\n \"Runtime/model preparation interrupted\",\n \"Model request dispatched\",\n ],\n provider_first_byte: [\n \"Waiting for model\",\n \"Model didn’t respond\",\n \"Model wait interrupted\",\n \"Model started responding\",\n ],\n};\n\nfunction startupDuration(durationMs: number | null): string | null {\n if (durationMs === null || !Number.isFinite(durationMs) || durationMs < 0) return null;\n if (durationMs < 1_000) return `${Math.round(durationMs)}ms`;\n if (durationMs < 60_000) return `${(durationMs / 1_000).toFixed(1)}s`;\n const minutes = Math.floor(durationMs / 60_000);\n const seconds = Math.round((durationMs % 60_000) / 1_000);\n return `${minutes}m ${String(seconds).padStart(2, \"0\")}s`;\n}\n"],"mappings":"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","names":[]}
package/dist/realtime.js CHANGED
@@ -4,13 +4,13 @@ import {
4
4
  PickerBackHeader,
5
5
  PickerNavRow
6
6
  } from "./chunk-R4KRSFGA.js";
7
- import {
8
- useEmbeddedRealtimeSession
9
- } from "./chunk-NEXLLTP4.js";
10
7
  import {
11
8
  usePortalTokenSource,
12
9
  usePortalTokenStyle
13
10
  } from "./chunk-VZTQ73XT.js";
11
+ import {
12
+ useEmbeddedRealtimeSession
13
+ } from "./chunk-NEXLLTP4.js";
14
14
  import "./chunk-VRBL3F5E.js";
15
15
  import {
16
16
  cn
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  BUILT_IN_TURN_SUMMARY_FACET_IDS,
3
+ HumanInputForm,
3
4
  HumanInputSurface,
4
5
  MessageTimeline,
5
6
  QueueSurface,
@@ -12,27 +13,23 @@ import {
12
13
  sessionChromeGoalPillLabel,
13
14
  sessionChromeGoalPillState,
14
15
  userMessageLikelyNeedsDisclosure
15
- } from "./chunk-7W6PVVTK.js";
16
+ } from "./chunk-CQG7YUJB.js";
16
17
  import "./chunk-YBM6CQRU.js";
17
18
  import "./chunk-5M6VTFJ5.js";
18
19
  import "./chunk-ZYRGEKWS.js";
19
20
  import "./chunk-NOSYBS2D.js";
20
21
  import "./chunk-PAJU5KKL.js";
21
- import "./chunk-7BWFU6H4.js";
22
+ import "./chunk-JVYQRRC4.js";
23
+ import "./chunk-Z7WBDB4E.js";
22
24
  import "./chunk-R4KRSFGA.js";
25
+ import "./chunk-VZTQ73XT.js";
23
26
  import {
24
27
  createOlderHistoryLoadReceipt
25
- } from "./chunk-BXJOQFCD.js";
28
+ } from "./chunk-HUMIRRYI.js";
26
29
  import "./chunk-6J56T3MG.js";
27
30
  import "./chunk-PLYRL5ER.js";
28
31
  import "./chunk-KZ73RL76.js";
29
32
  import "./chunk-NEXLLTP4.js";
30
- import {
31
- HumanInputForm
32
- } from "./chunk-YAIPFO5D.js";
33
- import "./chunk-JVYQRRC4.js";
34
- import "./chunk-HQV2I5HV.js";
35
- import "./chunk-VZTQ73XT.js";
36
33
  import "./chunk-VRBL3F5E.js";
37
34
  import "./chunk-22KP6LR5.js";
38
35
  export {
package/dist/session.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  useSession,
29
29
  useSessionEvents,
30
30
  useTurnQueue
31
- } from "./chunk-BXJOQFCD.js";
31
+ } from "./chunk-HUMIRRYI.js";
32
32
  import {
33
33
  FILE_ONLY_MESSAGE_TEXT,
34
34
  composeSendInput,
@@ -173,6 +173,7 @@ export type StartupPhaseItem = {
173
173
  durationMs: number | null;
174
174
  /** Sandbox origin and rig marker outcomes refine the settled label only. */
175
175
  outcome: "created" | "restored" | "resumed" | "skipped" | null;
176
+ blockedReason?: "rotation_in_progress" | undefined;
176
177
  occurredAt: string;
177
178
  };
178
179
  /**
@@ -352,6 +353,8 @@ export type AuthNeededItem = {
352
353
  export type TurnOutcome = "complete" | "failed" | "cancelled";
353
354
  export type TurnEndItem = {
354
355
  kind: "turn-end";
356
+ /** Keep an existing answer visible when a recorded input wait ends without final output. */
357
+ preserveWaitResponse?: true;
355
358
  id: string;
356
359
  turnId: string | null;
357
360
  outcome: TurnOutcome;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/react",
3
- "version": "3.8.0-canary.1",
3
+ "version": "4.0.1-canary.1",
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": {
@@ -78,10 +78,6 @@
78
78
  "types": "./dist/clipboard.d.ts",
79
79
  "import": "./dist/clipboard.js"
80
80
  },
81
- "./chat": {
82
- "types": "./dist/chat.tsx",
83
- "import": "./dist/chat.tsx"
84
- },
85
81
  "./styles.css": {
86
82
  "types": "./styles/index.d.ts",
87
83
  "style": "./styles/index.css",
@@ -124,7 +120,7 @@
124
120
  "@dnd-kit/utilities": "3.2.2",
125
121
  "@fontsource-variable/noto-sans-arabic": "5.2.10",
126
122
  "@fontsource-variable/noto-sans-jp": "5.2.10",
127
- "@opengeni/sdk": "^3.8.0-canary.1",
123
+ "@opengeni/sdk": "^4.0.1-canary.1",
128
124
  "clsx": "^2.1.1",
129
125
  "dequal": "2.0.3",
130
126
  "lucide-react": "^1.8.0",
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  HumanInputAnswer,
3
3
  HumanInputQuestion,
4
+ SkillRecord,
4
5
  SessionHumanInputRequest,
5
6
  SubmitHumanInputResponseRequest,
6
7
  } from "@opengeni/sdk";
@@ -64,6 +65,10 @@ export const defaultHumanInputFormMessages: HumanInputFormMessages = {
64
65
  };
65
66
 
66
67
  export type HumanInputFormProps = {
68
+ /** Scoped immutable revision reader. Saving a Skill fails closed without its full preview. */
69
+ loadSkillReview?:
70
+ | ((reference: NonNullable<HumanInputQuestion["skillReview"]>) => Promise<SkillRecord>)
71
+ | undefined;
67
72
  request: Pick<SessionHumanInputRequest, "id" | "questions" | "allowSkip" | "expiresAt">;
68
73
  onSubmit: (response: SubmitHumanInputResponseRequest) => void | Promise<void>;
69
74
  submitting?: boolean | undefined;
@@ -98,6 +103,7 @@ export function HumanInputForm(props: HumanInputFormProps) {
98
103
  function HumanInputRequestForm({
99
104
  request,
100
105
  onSubmit,
106
+ loadSkillReview,
101
107
  submitting = false,
102
108
  error,
103
109
  title,
@@ -142,6 +148,116 @@ function HumanInputRequestForm({
142
148
  const submissionInFlight = useRef(false);
143
149
  const submissionGeneration = useRef(0);
144
150
  const busy = submitting || submittingInternally;
151
+ const reviewIdentity = JSON.stringify(
152
+ request.questions
153
+ .filter((question) => question.skillReview)
154
+ .map((question) => ({
155
+ id: question.id,
156
+ reference: {
157
+ sourceOperationId: question.skillReview!.sourceOperationId,
158
+ skillId: question.skillReview!.skillId,
159
+ revisionId: question.skillReview!.revisionId,
160
+ expectedRevisionId: question.skillReview!.expectedRevisionId,
161
+ expectedScopeVersion: question.skillReview!.expectedScopeVersion,
162
+ },
163
+ })),
164
+ );
165
+ const [reviewReload, setReviewReload] = useState(0);
166
+ const [reviews, setReviews] = useState<{
167
+ loader: typeof loadSkillReview;
168
+ identity: string;
169
+ records: Record<string, SkillRecord>;
170
+ error: string | null;
171
+ } | null>(null);
172
+ useEffect(() => {
173
+ let current = true;
174
+ const questions = JSON.parse(reviewIdentity) as Array<{
175
+ id: string;
176
+ reference: NonNullable<HumanInputQuestion["skillReview"]>;
177
+ }>;
178
+ if (!questions.length) return;
179
+ setReviews(null);
180
+ void Promise.all(
181
+ questions.map(async (question) => {
182
+ if (!loadSkillReview)
183
+ throw new Error(
184
+ "This client cannot preview Skill files. Open this request in OpenGeni to review it.",
185
+ );
186
+ const reference = question.reference;
187
+ const record = await loadSkillReview(reference);
188
+ if (
189
+ record.id !== reference.skillId ||
190
+ record.revisionId !== reference.revisionId ||
191
+ !record.files.some((file) => file.path === "SKILL.md")
192
+ ) {
193
+ throw new Error("The requested Skill revision could not be verified.");
194
+ }
195
+ return [question.id, record] as const;
196
+ }),
197
+ )
198
+ .then((records) => {
199
+ if (current)
200
+ setReviews({
201
+ loader: loadSkillReview,
202
+ identity: reviewIdentity,
203
+ records: Object.fromEntries(records),
204
+ error: null,
205
+ });
206
+ })
207
+ .catch((cause) => {
208
+ if (current)
209
+ setReviews({
210
+ loader: loadSkillReview,
211
+ identity: reviewIdentity,
212
+ records: {},
213
+ error: cause instanceof Error ? cause.message : "Could not load the Skill files.",
214
+ });
215
+ });
216
+ return () => {
217
+ current = false;
218
+ };
219
+ }, [loadSkillReview, reviewIdentity, reviewReload]);
220
+ const visibleReviews =
221
+ reviews?.loader === loadSkillReview && reviews?.identity === reviewIdentity ? reviews : null;
222
+ const preview = (question: HumanInputQuestion) => {
223
+ if (!question.skillReview) return null;
224
+ const record = visibleReviews?.records[question.id];
225
+ return (
226
+ <div className="mb-3 min-w-0 space-y-2" data-skill-review="">
227
+ {record ? (
228
+ <>
229
+ <p className="text-og-sm">
230
+ {record.title ?? "Skill"} · {record.scope} · {record.files.length} files
231
+ </p>
232
+ <p className="text-og-xs text-og-fg-muted">
233
+ Saving activates these exact files. No additional review is required.
234
+ </p>
235
+ {record.files.map((file) => (
236
+ <details key={file.path} open={file.path === "SKILL.md"}>
237
+ <summary className="cursor-pointer break-all text-og-sm">{file.path}</summary>
238
+ <pre className="max-h-64 overflow-auto whitespace-pre-wrap break-words rounded-og-md bg-og-surface-1 p-2 text-og-xs">
239
+ {file.content}
240
+ </pre>
241
+ </details>
242
+ ))}
243
+ </>
244
+ ) : visibleReviews?.error ? (
245
+ <>
246
+ <p role="alert" className="text-og-sm">
247
+ {visibleReviews.error}
248
+ </p>
249
+ <button type="button" onClick={() => setReviewReload((value) => value + 1)}>
250
+ Retry preview
251
+ </button>
252
+ </>
253
+ ) : (
254
+ <p role="status" className="text-og-sm">
255
+ Loading the exact Skill files…
256
+ </p>
257
+ )}
258
+ </div>
259
+ );
260
+ };
145
261
 
146
262
  useEffect(() => {
147
263
  if (collapsed) {
@@ -232,6 +348,18 @@ function HumanInputRequestForm({
232
348
  }
233
349
  return;
234
350
  }
351
+ const unreviewedSave = request.questions.find(
352
+ (question) =>
353
+ question.skillReview &&
354
+ !visibleReviews?.records[question.id] &&
355
+ result.answers.some(
356
+ (answer) => answer.questionId === question.id && answer.values.includes("save"),
357
+ ),
358
+ );
359
+ if (unreviewedSave) {
360
+ setValidationErrors({ [unreviewedSave.id]: "Load the exact Skill files before saving." });
361
+ return;
362
+ }
235
363
  await submitResponse({ outcome: "answered", answers: result.answers });
236
364
  };
237
365
 
@@ -365,6 +493,7 @@ function HumanInputRequestForm({
365
493
  // Title already carries the question; only render the control + help extras.
366
494
  return (
367
495
  <div key={question.id} data-human-input-question={question.id}>
496
+ {preview(question)}
368
497
  <QuestionControls
369
498
  question={question}
370
499
  questionNumber={null}
@@ -389,6 +518,7 @@ function HumanInputRequestForm({
389
518
  data-human-input-question={question.id}
390
519
  className="flex flex-col gap-1.5"
391
520
  >
521
+ {preview(question)}
392
522
  <QuestionControls
393
523
  question={question}
394
524
  questionNumber={index + 1}
@@ -643,62 +773,64 @@ function QuestionControls({
643
773
  </label>
644
774
  );
645
775
  })}
646
- <div
647
- className={cn(
648
- "flex items-start gap-2.5 rounded-og-md px-2.5 py-2 transition-colors",
649
- draft.otherSelected
650
- ? "bg-og-status-waiting/12 text-og-fg"
651
- : "text-og-fg hover:bg-og-surface-1/80",
652
- )}
653
- >
654
- <input
655
- id={otherChoiceId}
656
- type={question.kind === "single_select" ? "radio" : "checkbox"}
657
- name={question.kind === "single_select" ? fieldId : undefined}
658
- aria-labelledby={otherLabelId}
659
- checked={draft.otherSelected}
660
- autoFocus={autoFocus && firstOption && question.options.length === 0}
661
- onChange={(event) =>
662
- onUpdate((current) => ({
663
- ...current,
664
- otherSelected: event.target.checked,
665
- ...(question.kind === "single_select" && event.target.checked
666
- ? { values: [] }
667
- : {}),
668
- }))
669
- }
670
- className="mt-2 accent-og-accent"
671
- />
672
- <span className="min-w-0 flex-1">
673
- <label
674
- id={otherLabelId}
675
- htmlFor={otherChoiceId}
676
- className="block text-og-sm font-medium"
677
- >
678
- {messages.other}
679
- </label>
680
- <label htmlFor={otherTextId} className="sr-only">
681
- {messages.other} answer for {visibleLabel}
682
- </label>
776
+ {!question.skillReview ? (
777
+ <div
778
+ className={cn(
779
+ "flex items-start gap-2.5 rounded-og-md px-2.5 py-2 transition-colors",
780
+ draft.otherSelected
781
+ ? "bg-og-status-waiting/12 text-og-fg"
782
+ : "text-og-fg hover:bg-og-surface-1/80",
783
+ )}
784
+ >
683
785
  <input
684
- id={otherTextId}
685
- type="text"
686
- value={draft.other}
687
- disabled={busy}
688
- placeholder="Type a value…"
689
- onClick={() => onUpdate(selectOtherDraft)}
690
- onFocus={() => onUpdate(selectOtherDraft)}
691
- onChange={(event) => {
692
- const other = event.target.value;
786
+ id={otherChoiceId}
787
+ type={question.kind === "single_select" ? "radio" : "checkbox"}
788
+ name={question.kind === "single_select" ? fieldId : undefined}
789
+ aria-labelledby={otherLabelId}
790
+ checked={draft.otherSelected}
791
+ autoFocus={autoFocus && firstOption && question.options.length === 0}
792
+ onChange={(event) =>
693
793
  onUpdate((current) => ({
694
- ...selectOtherDraft(current),
695
- other,
696
- }));
697
- }}
698
- className="mt-1.5 w-full rounded-og-sm border border-og-border bg-og-surface-1 px-2 py-1.5 text-og-sm text-og-fg outline-hidden focus:border-og-accent disabled:opacity-50"
794
+ ...current,
795
+ otherSelected: event.target.checked,
796
+ ...(question.kind === "single_select" && event.target.checked
797
+ ? { values: [] }
798
+ : {}),
799
+ }))
800
+ }
801
+ className="mt-2 accent-og-accent"
699
802
  />
700
- </span>
701
- </div>
803
+ <span className="min-w-0 flex-1">
804
+ <label
805
+ id={otherLabelId}
806
+ htmlFor={otherChoiceId}
807
+ className="block text-og-sm font-medium"
808
+ >
809
+ {messages.other}
810
+ </label>
811
+ <label htmlFor={otherTextId} className="sr-only">
812
+ {messages.other} answer for {visibleLabel}
813
+ </label>
814
+ <input
815
+ id={otherTextId}
816
+ type="text"
817
+ value={draft.other}
818
+ disabled={busy}
819
+ placeholder="Type a value…"
820
+ onClick={() => onUpdate(selectOtherDraft)}
821
+ onFocus={() => onUpdate(selectOtherDraft)}
822
+ onChange={(event) => {
823
+ const other = event.target.value;
824
+ onUpdate((current) => ({
825
+ ...selectOtherDraft(current),
826
+ other,
827
+ }));
828
+ }}
829
+ className="mt-1.5 w-full rounded-og-sm border border-og-border bg-og-surface-1 px-2 py-1.5 text-og-sm text-og-fg outline-hidden focus:border-og-accent disabled:opacity-50"
830
+ />
831
+ </span>
832
+ </div>
833
+ ) : null}
702
834
  </div>
703
835
  )}
704
836
  {error ? (
@@ -9,6 +9,7 @@ import {
9
9
  } from "./human-input-form";
10
10
 
11
11
  export type HumanInputSurfaceProps = {
12
+ loadSkillReview?: HumanInputFormProps["loadSkillReview"];
12
13
  requests: SessionHumanInputRequest[];
13
14
  onSubmit: (requestId: string, response: SubmitHumanInputResponseRequest) => void | Promise<void>;
14
15
  respondingRequestId?: string | null | undefined;
@@ -28,6 +29,7 @@ export type HumanInputSurfaceProps = {
28
29
  export function HumanInputSurface({
29
30
  requests,
30
31
  onSubmit,
32
+ loadSkillReview,
31
33
  respondingRequestId = null,
32
34
  error,
33
35
  messages,
@@ -68,6 +70,7 @@ export function HumanInputSurface({
68
70
 
69
71
  const formProps: HumanInputFormProps = {
70
72
  request: active,
73
+ loadSkillReview,
71
74
  submitting: respondingRequestId !== null,
72
75
  error: error ?? null,
73
76
  progressLabel,