@opengeni/react 6.1.0 → 7.0.0-canary.35625901699001

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 (214) hide show
  1. package/README.md +83 -2
  2. package/dist/approvals.d.ts +2 -1
  3. package/dist/artifacts-document.js +1 -1
  4. package/dist/artifacts-presentation.js +1 -1
  5. package/dist/artifacts-spreadsheet.js +1 -1
  6. package/dist/artifacts.js +3 -3
  7. package/dist/{browser-viewer-NTD6UIPR.js → browser-viewer-LGH4IP47.js} +3 -3
  8. package/dist/capability-catalog-row.d.ts +20 -0
  9. package/dist/capability-logo-fallback.d.ts +3 -0
  10. package/dist/{chunk-NOSYBS2D.js → chunk-2VHARTBL.js} +2 -1
  11. package/dist/{chunk-VRBL3F5E.js → chunk-5GAL23FF.js} +10 -2
  12. package/dist/{chunk-VRBL3F5E.js.map → chunk-5GAL23FF.js.map} +1 -1
  13. package/dist/chunk-5UONOQ3Y.js +635 -0
  14. package/dist/chunk-5UONOQ3Y.js.map +1 -0
  15. package/dist/{chunk-NYESIADM.js → chunk-AKOMC3VO.js} +3 -3
  16. package/dist/chunk-AKOMC3VO.js.map +1 -0
  17. package/dist/chunk-BRM6FVZA.js +302 -0
  18. package/dist/chunk-BRM6FVZA.js.map +1 -0
  19. package/dist/{chunk-KANFU34B.js → chunk-DBPVUGQT.js} +11 -2
  20. package/dist/chunk-DBPVUGQT.js.map +1 -0
  21. package/dist/chunk-FHIGTPME.js +1589 -0
  22. package/dist/chunk-FHIGTPME.js.map +1 -0
  23. package/dist/{chunk-5M6VTFJ5.js → chunk-GBPCZH3Y.js} +2 -2
  24. package/dist/{chunk-YBM6CQRU.js → chunk-GEN54DU5.js} +4 -4
  25. package/dist/{chunk-UMECRJRV.js → chunk-HJ5VJ4JB.js} +21 -12
  26. package/dist/chunk-HJ5VJ4JB.js.map +1 -0
  27. package/dist/{chunk-R4KRSFGA.js → chunk-IP4BUBKH.js} +3 -3
  28. package/dist/{chunk-NLTS7JHI.js → chunk-JPHDWKZE.js} +5 -5
  29. package/dist/{chunk-TDSEILUH.js → chunk-MI6ALLLD.js} +1297 -1535
  30. package/dist/chunk-MI6ALLLD.js.map +1 -0
  31. package/dist/{chunk-ICA6R6TW.js → chunk-OWHPKAOV.js} +28 -20
  32. package/dist/chunk-OWHPKAOV.js.map +1 -0
  33. package/dist/{chunk-24M4YBCL.js → chunk-PNAVEK2U.js} +4 -4
  34. package/dist/{chunk-L2NBTKRX.js → chunk-TXCBE32L.js} +203 -19
  35. package/dist/chunk-TXCBE32L.js.map +1 -0
  36. package/dist/{chunk-45XEUVTK.js → chunk-UXE5SSNN.js} +293 -463
  37. package/dist/chunk-UXE5SSNN.js.map +1 -0
  38. package/dist/{chunk-PEI6RANI.js → chunk-VR4Z73EU.js} +203 -9
  39. package/dist/chunk-VR4Z73EU.js.map +1 -0
  40. package/dist/{chunk-HUIKIDM5.js → chunk-XMXRI5JK.js} +65 -18
  41. package/dist/chunk-XMXRI5JK.js.map +1 -0
  42. package/dist/client.d.ts +1 -1
  43. package/dist/components/artifacts/spreadsheet-general-display.d.ts +24 -0
  44. package/dist/components/chat-composer.d.ts +13 -3
  45. package/dist/components/markdown.d.ts +6 -3
  46. package/dist/components/message-timeline.d.ts +7 -2
  47. package/dist/components/preview-loading.d.ts +2 -0
  48. package/dist/components/session-capability-frame.d.ts +24 -0
  49. package/dist/components/session-chrome.d.ts +1 -2
  50. package/dist/components/session-commands.d.ts +6 -0
  51. package/dist/components/session-connection-request.d.ts +8 -0
  52. package/dist/components/session-conversation.d.ts +3 -0
  53. package/dist/components/session-mcp-capability-card.d.ts +22 -0
  54. package/dist/components/timeline-search.d.ts +34 -0
  55. package/dist/composer.js +7 -7
  56. package/dist/connect-account-identity.d.ts +8 -0
  57. package/dist/connect-accounts.d.ts +5 -0
  58. package/dist/connect-chooser.d.ts +6 -0
  59. package/dist/connect-panel.d.ts +9 -2
  60. package/dist/connect-setup.d.ts +5 -1
  61. package/dist/connect.d.ts +5 -21
  62. package/dist/connect.js +1268 -584
  63. package/dist/connect.js.map +1 -1
  64. package/dist/connection-catalog.d.ts +6 -1
  65. package/dist/connection-discovery.d.ts +12 -0
  66. package/dist/connection-installed.d.ts +1 -0
  67. package/dist/connection-logo.d.ts +5 -2
  68. package/dist/connection-service-logo.d.ts +8 -0
  69. package/dist/connection-service-presentation.d.ts +13 -0
  70. package/dist/connection-skeleton.d.ts +5 -0
  71. package/dist/discovery-cache.d.ts +5 -0
  72. package/dist/hooks/use-codex-accounts.d.ts +11 -5
  73. package/dist/hooks/use-connect.d.ts +22 -0
  74. package/dist/hooks/use-session-events.d.ts +5 -0
  75. package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
  76. package/dist/hooks/use-workspace-sessions.d.ts +2 -0
  77. package/dist/index.d.ts +8 -2
  78. package/dist/index.js +771 -793
  79. package/dist/index.js.map +1 -1
  80. package/dist/interaction.js +2 -2
  81. package/dist/machines.js +2 -2
  82. package/dist/mcp-connection-status.d.ts +14 -0
  83. package/dist/{model-policy-picker-menu-3R336DP2.js → model-policy-picker-menu-RVOQ4ZZG.js} +4 -4
  84. package/dist/model-policy.d.ts +6 -2
  85. package/dist/model-policy.js +1 -1
  86. package/dist/{platform-activity-row-DUE2W74J.js → platform-activity-row-HLOH7FW4.js} +9 -4
  87. package/dist/platform-activity-row-HLOH7FW4.js.map +1 -0
  88. package/dist/plugin-discovery.d.ts +6 -0
  89. package/dist/{queue-surface-implementation-LK3S4BIW.js → queue-surface-implementation-3RIFK2BS.js} +4 -4
  90. package/dist/realtime.js +22 -22
  91. package/dist/realtime.js.map +1 -1
  92. package/dist/session-auth-recommendation.d.ts +5 -0
  93. package/dist/session-capability-policy.d.ts +12 -0
  94. package/dist/session-ui.d.ts +2 -0
  95. package/dist/session-ui.js +14 -12
  96. package/dist/session.d.ts +1 -0
  97. package/dist/session.js +6 -4
  98. package/dist/skill-discovery.d.ts +11 -0
  99. package/dist/timeline/compaction-copy.d.ts +10 -0
  100. package/dist/timeline/projection.d.ts +3 -1
  101. package/dist/timeline/tool-display-name.d.ts +2 -1
  102. package/dist/timeline/turn-summary.d.ts +2 -2
  103. package/dist/timeline/types.d.ts +24 -2
  104. package/dist/{timeline-annotation-markers-DRSTLEM6.js → timeline-annotation-markers-OY6OWR3V.js} +2 -2
  105. package/dist/{timeline-annotation-selection-IHLUGVND.js → timeline-annotation-selection-BOXA2TIN.js} +2 -2
  106. package/dist/{timeline-annotations-dialog-6Q53N5FG.js → timeline-annotations-dialog-RGY3MHL3.js} +3 -3
  107. package/package.json +3 -3
  108. package/src/approvals.ts +8 -1
  109. package/src/assets/README.md +8 -0
  110. package/src/assets/gmail.ico +0 -0
  111. package/src/assets/outlook.ico +0 -0
  112. package/src/capability-catalog-row.tsx +94 -0
  113. package/src/capability-logo-fallback.ts +22 -0
  114. package/src/client.ts +0 -9
  115. package/src/components/approval-surface.tsx +12 -2
  116. package/src/components/artifacts/document-editor.tsx +63 -6
  117. package/src/components/artifacts/presentation-editor.tsx +214 -9
  118. package/src/components/artifacts/spreadsheet-canvas.ts +3 -8
  119. package/src/components/artifacts/spreadsheet-general-display.ts +43 -0
  120. package/src/components/artifacts/spreadsheet-grid.tsx +10 -11
  121. package/src/components/chat-composer.tsx +26 -3
  122. package/src/components/composer.tsx +10 -4
  123. package/src/components/human-input-form.tsx +12 -2
  124. package/src/components/markdown.tsx +133 -9
  125. package/src/components/message-timeline.tsx +104 -36
  126. package/src/components/model-picker.tsx +11 -3
  127. package/src/components/model-policy-picker-menu.tsx +10 -3
  128. package/src/components/preview-loading.tsx +154 -0
  129. package/src/components/sandbox-workspace.tsx +20 -36
  130. package/src/components/session-capability-frame.tsx +163 -0
  131. package/src/components/session-chrome.tsx +46 -77
  132. package/src/components/session-commands.tsx +18 -0
  133. package/src/components/session-connection-request.tsx +97 -0
  134. package/src/components/session-conversation.tsx +10 -5
  135. package/src/components/session-mcp-capability-card.tsx +532 -0
  136. package/src/components/session-status.tsx +2 -2
  137. package/src/components/timeline-anchor.tsx +9 -8
  138. package/src/components/timeline-search.tsx +168 -0
  139. package/src/components/user-message-body.tsx +7 -0
  140. package/src/connect-account-identity.tsx +34 -0
  141. package/src/connect-accounts.tsx +114 -52
  142. package/src/connect-chooser.tsx +171 -39
  143. package/src/connect-panel.tsx +73 -9
  144. package/src/connect-setup.tsx +72 -25
  145. package/src/connect.ts +13 -22
  146. package/src/connection-assets.d.ts +4 -0
  147. package/src/connection-catalog.tsx +61 -39
  148. package/src/connection-discovery.tsx +235 -0
  149. package/src/connection-installed.tsx +2 -0
  150. package/src/connection-logo.tsx +18 -10
  151. package/src/connection-service-logo.tsx +55 -0
  152. package/src/connection-service-presentation.ts +46 -0
  153. package/src/connection-skeleton.tsx +16 -0
  154. package/src/conversation-timeline.ts +1 -0
  155. package/src/discovery-cache.ts +42 -0
  156. package/src/hooks/use-codex-accounts.ts +33 -14
  157. package/src/hooks/use-composer.ts +4 -5
  158. package/src/hooks/use-connect.ts +23 -0
  159. package/src/hooks/use-session-events.ts +114 -1
  160. package/src/hooks/use-session-mcp-approval-policy.ts +13 -5
  161. package/src/hooks/use-workspace-sessions.ts +10 -0
  162. package/src/index.ts +22 -2
  163. package/src/mcp-connection-status.ts +35 -0
  164. package/src/model-policy.ts +21 -0
  165. package/src/plugin-details.tsx +8 -5
  166. package/src/plugin-discovery.tsx +92 -72
  167. package/src/realtime/realtime-control.tsx +6 -8
  168. package/src/session-auth-recommendation.ts +48 -0
  169. package/src/session-capability-policy.ts +83 -0
  170. package/src/session-ui.ts +2 -0
  171. package/src/session.ts +1 -0
  172. package/src/skill-discovery.tsx +101 -56
  173. package/src/timeline/compaction-copy.ts +40 -0
  174. package/src/timeline/platform-activity-row.tsx +8 -3
  175. package/src/timeline/projection.ts +78 -2
  176. package/src/timeline/rolling-activity.tsx +4 -2
  177. package/src/timeline/shared.tsx +6 -1
  178. package/src/timeline/startup-timings.tsx +1 -1
  179. package/src/timeline/tool-display-name.ts +8 -6
  180. package/src/timeline/tool-renderers.tsx +5 -3
  181. package/src/timeline/turn-summary.tsx +17 -3
  182. package/src/timeline/types.ts +24 -2
  183. package/styles/compiled.css +325 -27
  184. package/styles/connect.css +90 -15
  185. package/styles/index.css +46 -0
  186. package/styles/session-capability.css +42 -0
  187. package/dist/chunk-45XEUVTK.js.map +0 -1
  188. package/dist/chunk-HQV2I5HV.js +0 -124
  189. package/dist/chunk-HQV2I5HV.js.map +0 -1
  190. package/dist/chunk-HUIKIDM5.js.map +0 -1
  191. package/dist/chunk-ICA6R6TW.js.map +0 -1
  192. package/dist/chunk-IHQII5WO.js +0 -886
  193. package/dist/chunk-IHQII5WO.js.map +0 -1
  194. package/dist/chunk-KANFU34B.js.map +0 -1
  195. package/dist/chunk-L2NBTKRX.js.map +0 -1
  196. package/dist/chunk-NYESIADM.js.map +0 -1
  197. package/dist/chunk-PEI6RANI.js.map +0 -1
  198. package/dist/chunk-TDSEILUH.js.map +0 -1
  199. package/dist/chunk-UMECRJRV.js.map +0 -1
  200. package/dist/hooks/use-packs.d.ts +0 -32
  201. package/dist/platform-activity-row-DUE2W74J.js.map +0 -1
  202. package/src/hooks/use-packs.ts +0 -157
  203. /package/dist/{browser-viewer-NTD6UIPR.js.map → browser-viewer-LGH4IP47.js.map} +0 -0
  204. /package/dist/{chunk-NOSYBS2D.js.map → chunk-2VHARTBL.js.map} +0 -0
  205. /package/dist/{chunk-5M6VTFJ5.js.map → chunk-GBPCZH3Y.js.map} +0 -0
  206. /package/dist/{chunk-YBM6CQRU.js.map → chunk-GEN54DU5.js.map} +0 -0
  207. /package/dist/{chunk-R4KRSFGA.js.map → chunk-IP4BUBKH.js.map} +0 -0
  208. /package/dist/{chunk-NLTS7JHI.js.map → chunk-JPHDWKZE.js.map} +0 -0
  209. /package/dist/{chunk-24M4YBCL.js.map → chunk-PNAVEK2U.js.map} +0 -0
  210. /package/dist/{model-policy-picker-menu-3R336DP2.js.map → model-policy-picker-menu-RVOQ4ZZG.js.map} +0 -0
  211. /package/dist/{queue-surface-implementation-LK3S4BIW.js.map → queue-surface-implementation-3RIFK2BS.js.map} +0 -0
  212. /package/dist/{timeline-annotation-markers-DRSTLEM6.js.map → timeline-annotation-markers-OY6OWR3V.js.map} +0 -0
  213. /package/dist/{timeline-annotation-selection-IHLUGVND.js.map → timeline-annotation-selection-BOXA2TIN.js.map} +0 -0
  214. /package/dist/{timeline-annotations-dialog-6Q53N5FG.js.map → timeline-annotations-dialog-RGY3MHL3.js.map} +0 -0
@@ -44,6 +44,9 @@ const MAX_ZOOM = 4;
44
44
  const RAIL_ITEM_HEIGHT = 116;
45
45
  const RAIL_OVERSCAN = 3;
46
46
  const DEFAULT_RAIL_HEIGHT = 640;
47
+ const MOBILE_RAIL_ITEM_HEIGHT = 44;
48
+ const MOBILE_RAIL_OVERSCAN = 3;
49
+ const DEFAULT_MOBILE_LIST_HEIGHT = 264;
47
50
  const DEFAULT_VIEWPORT = { width: 960, height: 640 };
48
51
  const SPATIAL_TILE = 256;
49
52
  const MAX_SPATIAL_BUCKETS_PER_OBJECT = 1_024;
@@ -450,6 +453,15 @@ export function PresentationProjectionEditor({
450
453
  );
451
454
  }
452
455
 
456
+ function nextSlideIndexFromKey(key: string, activeIndex: number, length: number): number | null {
457
+ if (length === 0) return null;
458
+ if (key === "ArrowDown") return Math.min(length - 1, activeIndex + 1);
459
+ if (key === "ArrowUp") return Math.max(0, activeIndex - 1);
460
+ if (key === "Home") return 0;
461
+ if (key === "End") return length - 1;
462
+ return null;
463
+ }
464
+
453
465
  function PresentationEditorCore({
454
466
  presentation,
455
467
  revision,
@@ -492,6 +504,11 @@ function PresentationEditorCore({
492
504
  scrollTop: 0,
493
505
  height: DEFAULT_RAIL_HEIGHT,
494
506
  });
507
+ const [mobileSelectorOpen, setMobileSelectorOpen] = useState(false);
508
+ const [mobileRail, setMobileRail] = useState({
509
+ scrollTop: 0,
510
+ height: DEFAULT_MOBILE_LIST_HEIGHT,
511
+ });
495
512
  const [viewport, setViewport] = useState<ViewportMetrics>({
496
513
  scrollLeft: 0,
497
514
  scrollTop: 0,
@@ -504,6 +521,9 @@ function PresentationEditorCore({
504
521
  const [textDraft, setTextDraft] = useState("");
505
522
  const [paintEpoch, setPaintEpoch] = useState(0);
506
523
  const railRef = useRef<HTMLDivElement>(null);
524
+ const mobileSelectorTriggerRef = useRef<HTMLButtonElement>(null);
525
+ const mobileSelectorPanelRef = useRef<HTMLDivElement>(null);
526
+ const mobileRailRef = useRef<HTMLDivElement>(null);
507
527
  const viewportRef = useRef<HTMLDivElement>(null);
508
528
  const stageRef = useRef<HTMLDivElement>(null);
509
529
  const canvasRef = useRef<HTMLCanvasElement>(null);
@@ -517,6 +537,7 @@ function PresentationEditorCore({
517
537
  const commandVersionsRef = useRef(new Map<string, number>());
518
538
  const pendingSlideIdRef = useRef<string | null>(null);
519
539
  const pendingObjectIdRef = useRef<string | null>(null);
540
+ const mobileRailScrollOnOpenRef = useRef(0);
520
541
  const domId = useId().replaceAll(":", "");
521
542
  const selectionProxyId = `${domId}-presentation-selection`;
522
543
  const instructionsId = `${domId}-presentation-instructions`;
@@ -596,6 +617,47 @@ function PresentationEditorCore({
596
617
  return () => observer.disconnect();
597
618
  }, []);
598
619
 
620
+ useLayoutEffect(() => {
621
+ if (!mobileSelectorOpen) return;
622
+ const element = mobileRailRef.current;
623
+ if (!element) return;
624
+ element.scrollTop = mobileRailScrollOnOpenRef.current;
625
+ element.focus();
626
+ const measure = () =>
627
+ setMobileRail((current) => ({
628
+ scrollTop: element.scrollTop,
629
+ height: element.clientHeight || current.height,
630
+ }));
631
+ measure();
632
+ if (typeof ResizeObserver === "undefined") return;
633
+ const observer = new ResizeObserver(measure);
634
+ observer.observe(element);
635
+ return () => observer.disconnect();
636
+ }, [mobileSelectorOpen]);
637
+
638
+ useEffect(() => {
639
+ if (!mobileSelectorOpen) return;
640
+ const onPointerDown = (event: PointerEvent) => {
641
+ const target = event.target;
642
+ if (!(target instanceof Node)) return;
643
+ if (mobileSelectorTriggerRef.current?.contains(target)) return;
644
+ if (mobileSelectorPanelRef.current?.contains(target)) return;
645
+ setMobileSelectorOpen(false);
646
+ };
647
+ const onKeyDown = (event: KeyboardEvent) => {
648
+ if (event.key !== "Escape") return;
649
+ event.preventDefault();
650
+ setMobileSelectorOpen(false);
651
+ mobileSelectorTriggerRef.current?.focus();
652
+ };
653
+ document.addEventListener("pointerdown", onPointerDown);
654
+ document.addEventListener("keydown", onKeyDown);
655
+ return () => {
656
+ document.removeEventListener("pointerdown", onPointerDown);
657
+ document.removeEventListener("keydown", onKeyDown);
658
+ };
659
+ }, [mobileSelectorOpen]);
660
+
599
661
  useLayoutEffect(() => {
600
662
  const element = viewportRef.current;
601
663
  if (!element) return;
@@ -1058,6 +1120,34 @@ function PresentationEditorCore({
1058
1120
  [activeIndex, activeSlide, selectSlide, slides],
1059
1121
  );
1060
1122
 
1123
+ const toggleMobileSlideSelector = useCallback(() => {
1124
+ setMobileSelectorOpen((open) => {
1125
+ if (open) return false;
1126
+ const index = Math.max(0, activeIndex);
1127
+ const height = DEFAULT_MOBILE_LIST_HEIGHT;
1128
+ const maxScroll = Math.max(0, slides.length * MOBILE_RAIL_ITEM_HEIGHT - height);
1129
+ const scrollTop = Math.max(
1130
+ 0,
1131
+ Math.min(
1132
+ maxScroll,
1133
+ index * MOBILE_RAIL_ITEM_HEIGHT - height / 2 + MOBILE_RAIL_ITEM_HEIGHT / 2,
1134
+ ),
1135
+ );
1136
+ mobileRailScrollOnOpenRef.current = scrollTop;
1137
+ setMobileRail({ scrollTop, height });
1138
+ return true;
1139
+ });
1140
+ }, [activeIndex, slides.length]);
1141
+
1142
+ const chooseMobileSlide = useCallback(
1143
+ (slide: Slide, index: number) => {
1144
+ if (slide !== activeSlide) selectSlide(slide, index);
1145
+ setMobileSelectorOpen(false);
1146
+ mobileSelectorTriggerRef.current?.focus();
1147
+ },
1148
+ [activeSlide, selectSlide],
1149
+ );
1150
+
1061
1151
  const handleCanvasKeyDown = useCallback(
1062
1152
  (event: ReactKeyboardEvent<SVGSVGElement>) => {
1063
1153
  if (event.metaKey || event.ctrlKey) {
@@ -1229,6 +1319,17 @@ function PresentationEditorCore({
1229
1319
  slides.length,
1230
1320
  Math.ceil((rail.scrollTop + rail.height) / RAIL_ITEM_HEIGHT) + RAIL_OVERSCAN,
1231
1321
  );
1322
+ const mobileRailStart = Math.max(
1323
+ 0,
1324
+ Math.floor(mobileRail.scrollTop / MOBILE_RAIL_ITEM_HEIGHT) - MOBILE_RAIL_OVERSCAN,
1325
+ );
1326
+ const mobileRailEnd = Math.min(
1327
+ slides.length,
1328
+ Math.ceil((mobileRail.scrollTop + mobileRail.height) / MOBILE_RAIL_ITEM_HEIGHT) +
1329
+ MOBILE_RAIL_OVERSCAN,
1330
+ );
1331
+ const slideCountLabel =
1332
+ activeIndex >= 0 ? `${activeIndex + 1} / ${slides.length}` : `0 / ${slides.length}`;
1232
1333
  const stageWidth = presentation.slideSize.width * zoom;
1233
1334
  const stageHeight = presentation.slideSize.height * zoom;
1234
1335
 
@@ -1256,8 +1357,22 @@ function PresentationEditorCore({
1256
1357
  >
1257
1358
  <ChevronLeftIcon />
1258
1359
  </button>
1259
- <span className="min-w-16 shrink-0 text-center text-og-xs tabular-nums text-og-fg-muted">
1260
- {activeIndex >= 0 ? `${activeIndex + 1} / ${slides.length}` : `0 / ${slides.length}`}
1360
+ <button
1361
+ ref={mobileSelectorTriggerRef}
1362
+ type="button"
1363
+ aria-label="Choose slide"
1364
+ aria-haspopup="listbox"
1365
+ aria-expanded={mobileSelectorOpen}
1366
+ aria-controls={mobileSelectorOpen ? `${domId}-mobile-slides` : undefined}
1367
+ disabled={slides.length === 0}
1368
+ data-og-mobile-slide-selector
1369
+ onClick={toggleMobileSlideSelector}
1370
+ className="inline-flex min-h-11 min-w-16 shrink-0 items-center justify-center rounded-og-sm px-2 text-og-xs tabular-nums text-og-fg-muted hover:bg-og-surface-3 hover:text-og-fg disabled:opacity-35 sm:hidden"
1371
+ >
1372
+ <span>{slideCountLabel}</span>
1373
+ </button>
1374
+ <span className="hidden min-w-16 shrink-0 text-center text-og-xs tabular-nums text-og-fg-muted sm:inline">
1375
+ {slideCountLabel}
1261
1376
  </span>
1262
1377
  <button
1263
1378
  type="button"
@@ -1364,7 +1479,7 @@ function PresentationEditorCore({
1364
1479
  {commandFailure?.message ?? (pendingCommands > 0 ? "Saving presentation changes" : "")}
1365
1480
  </div>
1366
1481
 
1367
- <div className="flex min-h-0 min-w-0 flex-1">
1482
+ <div className="relative flex min-h-0 min-w-0 flex-1">
1368
1483
  <div
1369
1484
  ref={railRef}
1370
1485
  role="listbox"
@@ -1389,12 +1504,7 @@ function PresentationEditorCore({
1389
1504
  }));
1390
1505
  }}
1391
1506
  onKeyDown={(event) => {
1392
- if (slides.length === 0) return;
1393
- let nextIndex: number | null = null;
1394
- if (event.key === "ArrowDown") nextIndex = Math.min(slides.length - 1, activeIndex + 1);
1395
- else if (event.key === "ArrowUp") nextIndex = Math.max(0, activeIndex - 1);
1396
- else if (event.key === "Home") nextIndex = 0;
1397
- else if (event.key === "End") nextIndex = slides.length - 1;
1507
+ const nextIndex = nextSlideIndexFromKey(event.key, activeIndex, slides.length);
1398
1508
  if (nextIndex === null) return;
1399
1509
  event.preventDefault();
1400
1510
  const next = slides[nextIndex];
@@ -1447,6 +1557,101 @@ function PresentationEditorCore({
1447
1557
  </div>
1448
1558
  </div>
1449
1559
 
1560
+ {mobileSelectorOpen ? (
1561
+ <div
1562
+ ref={mobileSelectorPanelRef}
1563
+ className="absolute left-2 top-2 z-40 max-w-56 overflow-hidden rounded-og-md border border-og-border bg-og-surface-1 shadow-og-md sm:hidden"
1564
+ style={{ width: "calc(100% - 1rem)" }}
1565
+ >
1566
+ <div
1567
+ ref={mobileRailRef}
1568
+ id={`${domId}-mobile-slides`}
1569
+ role="listbox"
1570
+ tabIndex={0}
1571
+ aria-label="Slides"
1572
+ aria-orientation="vertical"
1573
+ aria-activedescendant={
1574
+ activeSlide && activeIndex >= mobileRailStart && activeIndex < mobileRailEnd
1575
+ ? `${domId}-mobile-slide-${safeDomId(activeSlide.id)}`
1576
+ : undefined
1577
+ }
1578
+ className="max-h-72 overflow-y-auto outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent"
1579
+ data-og-mobile-slide-list
1580
+ data-og-window-start={mobileRailStart}
1581
+ data-og-window-end={mobileRailEnd}
1582
+ onScroll={(event) => {
1583
+ const scrollTop = event.currentTarget.scrollTop;
1584
+ const height = event.currentTarget.clientHeight;
1585
+ setMobileRail((current) => ({
1586
+ scrollTop,
1587
+ height: height || current.height,
1588
+ }));
1589
+ }}
1590
+ onKeyDown={(event) => {
1591
+ if (event.key === "Escape") {
1592
+ event.preventDefault();
1593
+ setMobileSelectorOpen(false);
1594
+ mobileSelectorTriggerRef.current?.focus();
1595
+ return;
1596
+ }
1597
+ const nextIndex = nextSlideIndexFromKey(event.key, activeIndex, slides.length);
1598
+ if (nextIndex === null) return;
1599
+ event.preventDefault();
1600
+ const next = slides[nextIndex];
1601
+ if (!next) return;
1602
+ if (next !== activeSlide) selectSlide(next, nextIndex);
1603
+ const list = event.currentTarget;
1604
+ const top = nextIndex * MOBILE_RAIL_ITEM_HEIGHT;
1605
+ const bottom = top + MOBILE_RAIL_ITEM_HEIGHT;
1606
+ if (top < list.scrollTop) list.scrollTop = top;
1607
+ else if (bottom > list.scrollTop + list.clientHeight) {
1608
+ list.scrollTop = bottom - list.clientHeight;
1609
+ }
1610
+ }}
1611
+ >
1612
+ <div
1613
+ className="relative w-full"
1614
+ style={{ height: slides.length * MOBILE_RAIL_ITEM_HEIGHT }}
1615
+ >
1616
+ {slides.slice(mobileRailStart, mobileRailEnd).map((slide, offset) => {
1617
+ const index = mobileRailStart + offset;
1618
+ const active = slide === activeSlide;
1619
+ return (
1620
+ <button
1621
+ key={slide.id}
1622
+ id={`${domId}-mobile-slide-${safeDomId(slide.id)}`}
1623
+ type="button"
1624
+ role="option"
1625
+ tabIndex={-1}
1626
+ aria-selected={active}
1627
+ aria-label={`Slide ${index + 1}${slide.title ? `: ${slide.title}` : ""}`}
1628
+ onClick={() => chooseMobileSlide(slide, index)}
1629
+ className={cn(
1630
+ "absolute left-0 flex min-h-11 w-full items-center gap-2 px-2 text-left",
1631
+ active
1632
+ ? "bg-og-surface-3 text-og-fg"
1633
+ : "text-og-fg-muted hover:bg-og-surface-3/70",
1634
+ )}
1635
+ style={{
1636
+ top: index * MOBILE_RAIL_ITEM_HEIGHT,
1637
+ height: MOBILE_RAIL_ITEM_HEIGHT,
1638
+ }}
1639
+ data-og-mobile-slide-index={index}
1640
+ >
1641
+ <span className="w-7 shrink-0 text-right text-og-xs tabular-nums text-og-fg-subtle">
1642
+ {index + 1}
1643
+ </span>
1644
+ <span className="min-w-0 flex-1 truncate text-og-sm">
1645
+ {slide.title || `Slide ${index + 1}`}
1646
+ </span>
1647
+ </button>
1648
+ );
1649
+ })}
1650
+ </div>
1651
+ </div>
1652
+ </div>
1653
+ ) : null}
1654
+
1450
1655
  <div
1451
1656
  ref={viewportRef}
1452
1657
  className="relative min-h-0 min-w-0 flex-1 overflow-auto bg-og-surface-3/60 p-6"
@@ -1,3 +1,5 @@
1
+ import { formatSpreadsheetGeneralDisplay } from "./spreadsheet-general-display";
2
+
1
3
  const TILE_SIZE = 256;
2
4
  const MAX_CACHE_PHYSICAL_PIXELS = 32_000_000;
3
5
  const MAX_CACHE_TILES = 96;
@@ -430,7 +432,7 @@ export class SpreadsheetCanvasRenderer {
430
432
 
431
433
  for (const cell of cells) {
432
434
  const value = input.projection.valueAt(cell);
433
- const text = displayValue(value);
435
+ const text = formatSpreadsheetGeneralDisplay(value);
434
436
  if (!text) continue;
435
437
  paintCellText(context, input, cell, text, tileLeft, tileTop);
436
438
  }
@@ -776,13 +778,6 @@ function finitePositive(value: number | undefined, fallback: number): number {
776
778
  return value !== undefined && Number.isFinite(value) && value > 0 ? value : fallback;
777
779
  }
778
780
 
779
- function displayValue(value: unknown): string {
780
- if (value === null || value === undefined) return "";
781
- if (value instanceof Date) return Number.isNaN(value.getTime()) ? "" : value.toLocaleDateString();
782
- if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
783
- return String(value);
784
- }
785
-
786
781
  function columnName(index: number): string {
787
782
  let value = index + 1;
788
783
  let result = "";
@@ -0,0 +1,43 @@
1
+ /**
2
+ * DISPLAY-ONLY General number formatting for spreadsheet canvas and a11y
3
+ * labels. This is not numeric arithmetic and must never feed formula-bar /
4
+ * cell-edit `input`, stored cells, formula results, or serialized exports.
5
+ *
6
+ * Policy for independent review:
7
+ * IEEE-754 cannot represent many decimal fractions exactly, so values such as
8
+ * `110.00000000000001` and `0.1 + 0.2` leak binary residue through `String(n)`.
9
+ * General display therefore collapses a finite number to 15 significant decimal
10
+ * digits — the same bound spreadsheet General uses — via
11
+ * `String(Number(value.toPrecision(15)))`.
12
+ *
13
+ * That mapping yields `110` / `220` / `0.3` for the noisy cases while keeping
14
+ * exponent-scale values (`1e-20`, `1e21`) and ~15-digit legitimate precision.
15
+ * Integers stringify without a decimal; `-0` displays as `0`; non-finite
16
+ * numbers, dates, booleans, errors, and strings keep their existing labels.
17
+ *
18
+ * Do not reuse `Intl.NumberFormat({ maximumFractionDigits: 12 })` from the
19
+ * artifact-tool renderer: 12 fraction digits rounds `1e-20` to `0` and truncates
20
+ * high-precision values that General should still show.
21
+ */
22
+ export function formatSpreadsheetGeneralDisplay(value: unknown): string {
23
+ if (value === null || value === undefined) return "";
24
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? "" : value.toLocaleDateString();
25
+ if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
26
+ if (typeof value === "number") {
27
+ if (Object.is(value, -0) || value === 0) return "0";
28
+ if (!Number.isFinite(value)) return String(value);
29
+ const rounded = Number(value.toPrecision(15));
30
+ // Rounding at the finite IEEE-754 boundary can overflow. A display policy
31
+ // must not turn a finite source into an infinity label.
32
+ return String(Number.isFinite(rounded) ? rounded : value);
33
+ }
34
+ return String(value);
35
+ }
36
+
37
+ /** Exact edit/formula-bar source. Numbers stay `String(value)` with no General collapse. */
38
+ export function spreadsheetCellEditSource(value: unknown): string {
39
+ if (value === null || value === undefined) return "";
40
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? "" : value.toLocaleDateString();
41
+ if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
42
+ return String(value);
43
+ }
@@ -26,6 +26,10 @@ import {
26
26
  type SpreadsheetCanvasProjection,
27
27
  type SpreadsheetCanvasTheme,
28
28
  } from "./spreadsheet-canvas";
29
+ import {
30
+ formatSpreadsheetGeneralDisplay,
31
+ spreadsheetCellEditSource,
32
+ } from "./spreadsheet-general-display";
29
33
 
30
34
  const MAX_ROWS = 1_048_576;
31
35
  const MAX_COLUMNS = 16_384;
@@ -494,13 +498,6 @@ function isCellSelected(selection: SpreadsheetSelection, row: number, col: numbe
494
498
  );
495
499
  }
496
500
 
497
- function displayValue(value: unknown): string {
498
- if (value === null || value === undefined) return "";
499
- if (value instanceof Date) return Number.isNaN(value.getTime()) ? "" : value.toLocaleDateString();
500
- if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
501
- return String(value);
502
- }
503
-
504
501
  function parseCellInput(input: string): string | number | boolean | null {
505
502
  if (input === "") return null;
506
503
  const normalized = input.trim();
@@ -906,7 +903,7 @@ export function SpreadsheetGrid({
906
903
  const data = worksheet.cellData(row, col);
907
904
  return {
908
905
  value: data.formula ? workbook.valueAt(worksheet, { row, col }) : data.value,
909
- input: data.formula ?? displayValue(data.value),
906
+ input: data.formula ?? spreadsheetCellEditSource(data.value),
910
907
  format: data.format,
911
908
  };
912
909
  },
@@ -2059,7 +2056,7 @@ export function SpreadsheetProjectionGrid({
2059
2056
  role="gridcell"
2060
2057
  aria-colindex={col + 1}
2061
2058
  aria-selected={selected}
2062
- aria-label={`${address}${value === null ? "" : `, ${displayValue(value)}`}`}
2059
+ aria-label={`${address}${value === null ? "" : `, ${formatSpreadsheetGeneralDisplay(value)}`}`}
2063
2060
  data-og-cell={address}
2064
2061
  className={cn(
2065
2062
  "pointer-events-none absolute top-0 flex h-full min-w-0 overflow-hidden border-b border-r border-og-border px-1.5 text-og-fg",
@@ -2099,7 +2096,7 @@ export function SpreadsheetProjectionGrid({
2099
2096
  />
2100
2097
  ) : (
2101
2098
  <span className="block min-w-0 overflow-hidden text-ellipsis">
2102
- {displayValue(value)}
2099
+ {formatSpreadsheetGeneralDisplay(value)}
2103
2100
  </span>
2104
2101
  )}
2105
2102
  </div>
@@ -2117,7 +2114,9 @@ export function SpreadsheetProjectionGrid({
2117
2114
  aria-colindex={selection.focus.col + 1}
2118
2115
  aria-selected="true"
2119
2116
  aria-label={`${cellName(selection.focus)}${
2120
- hiddenActiveValue == null ? "" : `, ${displayValue(hiddenActiveValue)}`
2117
+ hiddenActiveValue == null
2118
+ ? ""
2119
+ : `, ${formatSpreadsheetGeneralDisplay(hiddenActiveValue)}`
2121
2120
  }`}
2122
2121
  />
2123
2122
  </div>
@@ -1,6 +1,12 @@
1
1
  import type { ClientModel, EffectiveSessionControl } from "@opengeni/sdk";
2
2
  import { LayoutGroup, motion } from "motion/react";
3
- import type { ClipboardEvent, ReactNode } from "react";
3
+ import {
4
+ useImperativeHandle,
5
+ type ClipboardEvent,
6
+ type ComponentProps,
7
+ type ReactNode,
8
+ type Ref,
9
+ } from "react";
4
10
  import type { SlashCommand } from "../commands/types";
5
11
  import type { ComposerState } from "../hooks/use-composer";
6
12
  import type { UseFileAttachmentsResult } from "../hooks/use-file-attachments";
@@ -41,6 +47,14 @@ export { OPEN_WORKSTREAM_CONTROL_EVENT };
41
47
 
42
48
  export type ChatComposerProps = {
43
49
  composer: ComposerState;
50
+ /** Replace only the normal footer, inside the shared composer context.
51
+ * Confirmation, drafts, annotations and attachments remain native. */
52
+ footer?: ReactNode;
53
+ focusRef?: Ref<{ focusInput: () => void }>;
54
+ inputProps?: ComponentProps<typeof Input> & {
55
+ [attribute: `data-${string}`]: string | number | boolean | undefined;
56
+ };
57
+ surfaceClassName?: string;
44
58
  /**
45
59
  * Measurement surface for responsive chrome. Defaults to the historical
46
60
  * viewport breakpoints; opt into `container` for narrow embedded panels.
@@ -125,6 +139,10 @@ export function ChatComposer({
125
139
  commandContext,
126
140
  onClearView,
127
141
  messages,
142
+ footer,
143
+ focusRef,
144
+ inputProps,
145
+ surfaceClassName,
128
146
  }: ChatComposerProps) {
129
147
  const controller = useChatComposerController({
130
148
  delivery: composer,
@@ -146,13 +164,16 @@ export function ChatComposer({
146
164
  attachments || models || controlsLeading || controlsStart || transcription,
147
165
  );
148
166
  const stackActions = hasControls && Boolean(actionsStart);
167
+ useImperativeHandle(focusRef, () => ({ focusInput: controller.focusInput }), [
168
+ controller.focusInput,
169
+ ]);
149
170
 
150
171
  return (
151
172
  <>
152
173
  <Root controller={controller} responsiveBasis={responsiveBasis} className={className}>
153
174
  <Frame>
154
175
  <CommandPalette />
155
- <Surface>
176
+ <Surface className={surfaceClassName}>
156
177
  <PausedState />
157
178
  <RestoredResources />
158
179
  <Attachments />
@@ -172,9 +193,11 @@ export function ChatComposer({
172
193
  />
173
194
  </div>
174
195
  ) : null}
175
- <Input placeholder={placeholder} autoFocus={autoFocus} />
196
+ <Input {...inputProps} placeholder={placeholder} autoFocus={autoFocus} />
176
197
  {controller.confirmState ? (
177
198
  <Confirmation />
199
+ ) : footer !== undefined ? (
200
+ footer
178
201
  ) : (
179
202
  <Footer
180
203
  data-og-stack-actions={stackActions ? "" : undefined}
@@ -449,7 +449,6 @@ export function useChatComposerController({
449
449
  }, [id, paused]);
450
450
 
451
451
  const blockedByAttachment = attachments?.hasUnresolved === true;
452
- const hasReadyAttachment = (attachments?.readyResources.length ?? 0) > 0;
453
452
 
454
453
  const [dragging, setDragging] = useState(false);
455
454
  const dragCarriesFiles = (event: { dataTransfer: DataTransfer | null }): boolean =>
@@ -573,7 +572,7 @@ export function useChatComposerController({
573
572
  ? "command"
574
573
  : annotationsIncomplete
575
574
  ? "annotations"
576
- : delivery.canSend || hasReadyAttachment
575
+ : delivery.canSend
577
576
  ? null
578
577
  : "empty";
579
578
  const canSubmit = submitBlocker === null;
@@ -593,7 +592,7 @@ export function useChatComposerController({
593
592
  delivery.requestAnnotationReview?.();
594
593
  return false;
595
594
  }
596
- if (!delivery.canSend && !hasReadyAttachment) return false;
595
+ if (!delivery.canSend) return false;
597
596
  submittingRef.current = true;
598
597
  setSubmitting(true);
599
598
  try {
@@ -609,7 +608,6 @@ export function useChatComposerController({
609
608
  commandDraftBlocked,
610
609
  delivery,
611
610
  disabled,
612
- hasReadyAttachment,
613
611
  messages.annotationNotesRequired,
614
612
  messages.slashCommandBlocked,
615
613
  ],
@@ -1015,6 +1013,14 @@ export const Input = forwardRef<HTMLTextAreaElement, ComposerInputProps>(functio
1015
1013
  const textarea = controller.textareaRef.current;
1016
1014
  if (!textarea || textarea.disabled) return;
1017
1015
  autoFocusedRef.current = true;
1016
+ // Draft hydration may finish after the user has started editing another
1017
+ // field. Initial focus is a convenience, not authority to take their caret.
1018
+ const active = textarea.ownerDocument.activeElement;
1019
+ if (
1020
+ active !== textarea &&
1021
+ active?.closest('input, textarea, select, [contenteditable="true"], [role="textbox"]')
1022
+ )
1023
+ return;
1018
1024
  textarea.focus();
1019
1025
  });
1020
1026
  return () => window.cancelAnimationFrame(frame);
@@ -159,6 +159,9 @@ function HumanInputRequestForm({
159
159
  revisionId: question.skillReview!.revisionId,
160
160
  expectedRevisionId: question.skillReview!.expectedRevisionId,
161
161
  expectedScopeVersion: question.skillReview!.expectedScopeVersion,
162
+ ...(question.skillReview!.removalOperationId
163
+ ? { removalOperationId: question.skillReview!.removalOperationId }
164
+ : {}),
162
165
  },
163
166
  })),
164
167
  );
@@ -188,6 +191,7 @@ function HumanInputRequestForm({
188
191
  if (
189
192
  record.id !== reference.skillId ||
190
193
  record.revisionId !== reference.revisionId ||
194
+ (record.removalOperationId ?? undefined) !== reference.removalOperationId ||
191
195
  !record.files.some((file) => file.path === "SKILL.md")
192
196
  ) {
193
197
  throw new Error("The requested Skill revision could not be verified.");
@@ -230,7 +234,9 @@ function HumanInputRequestForm({
230
234
  {record.title ?? "Skill"} · {record.scope} · {record.files.length} files
231
235
  </p>
232
236
  <p className="text-og-xs text-og-fg-muted">
233
- Saving activates these exact files. No additional review is required.
237
+ {question.skillReview.removalOperationId
238
+ ? "Permanently deletes this Skill and all stored revisions. This cannot be undone; conversations remain unchanged."
239
+ : "Saving activates these exact files. No additional review is required."}
234
240
  </p>
235
241
  {record.files.map((file) => (
236
242
  <details key={file.path} open={file.path === "SKILL.md"}>
@@ -357,7 +363,11 @@ function HumanInputRequestForm({
357
363
  ),
358
364
  );
359
365
  if (unreviewedSave) {
360
- setValidationErrors({ [unreviewedSave.id]: "Load the exact Skill files before saving." });
366
+ setValidationErrors({
367
+ [unreviewedSave.id]: unreviewedSave.skillReview?.removalOperationId
368
+ ? "Load the exact Skill proposal before approving permanent deletion."
369
+ : "Load the exact Skill files before saving.",
370
+ });
361
371
  return;
362
372
  }
363
373
  await submitResponse({ outcome: "answered", answers: result.answers });