@opengeni/react 0.45.0 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/README.md +68 -12
  2. package/dist/{chunk-U3CSCMWA.js → chunk-FK6VG4LL.js} +9 -13
  3. package/dist/chunk-FK6VG4LL.js.map +1 -0
  4. package/dist/{chunk-HFO4ERGQ.js → chunk-FWXGSNXT.js} +2 -2
  5. package/dist/{chunk-SRFUT2ZU.js → chunk-I4MPFDRP.js} +119 -63
  6. package/dist/chunk-I4MPFDRP.js.map +1 -0
  7. package/dist/{chunk-EB67J347.js → chunk-KZ73RL76.js} +111 -31
  8. package/dist/chunk-KZ73RL76.js.map +1 -0
  9. package/dist/{chunk-LWR4MXSS.js → chunk-LHLYWG5R.js} +10 -1
  10. package/dist/chunk-LHLYWG5R.js.map +1 -0
  11. package/dist/{chunk-L3EWO3UK.js → chunk-NJ7M2B6Z.js} +445 -84
  12. package/dist/chunk-NJ7M2B6Z.js.map +1 -0
  13. package/dist/{chunk-VCBFUSUZ.js → chunk-O4VXOBGF.js} +3 -3
  14. package/dist/{chunk-VCBFUSUZ.js.map → chunk-O4VXOBGF.js.map} +1 -1
  15. package/dist/{chunk-6EDV6YFE.js → chunk-OL6QVXGF.js} +121 -29
  16. package/dist/chunk-OL6QVXGF.js.map +1 -0
  17. package/dist/chunk-VZTQ73XT.js +86 -0
  18. package/dist/chunk-VZTQ73XT.js.map +1 -0
  19. package/dist/{chunk-EHSYZ4KP.js → chunk-WYA65Y3F.js} +16 -16
  20. package/dist/chunk-WYA65Y3F.js.map +1 -0
  21. package/dist/{chunk-FD72GJMK.js → chunk-XCNTXWBB.js} +274 -125
  22. package/dist/chunk-XCNTXWBB.js.map +1 -0
  23. package/dist/client.d.ts +1 -1
  24. package/dist/components/chat-composer.d.ts +7 -2
  25. package/dist/components/composer-transcription-control.d.ts +2 -0
  26. package/dist/components/composer.d.ts +14 -0
  27. package/dist/components/machines/machine-detail.d.ts +3 -1
  28. package/dist/components/machines-dashboard.d.ts +3 -3
  29. package/dist/components/message-timeline.d.ts +12 -2
  30. package/dist/components/sandbox-files.d.ts +2 -2
  31. package/dist/components/sandbox-terminal.d.ts +29 -0
  32. package/dist/components/sandbox-workspace.d.ts +5 -1
  33. package/dist/components/workspace-dock.d.ts +3 -1
  34. package/dist/composer.d.ts +2 -2
  35. package/dist/composer.js +6 -6
  36. package/dist/hooks/internal.d.ts +6 -0
  37. package/dist/hooks/use-codex-accounts.d.ts +1 -1
  38. package/dist/hooks/use-environments.d.ts +10 -4
  39. package/dist/hooks/use-machines.d.ts +7 -0
  40. package/dist/hooks/use-sandbox-files.d.ts +19 -0
  41. package/dist/hooks/use-sandbox-git.d.ts +7 -0
  42. package/dist/hooks/use-transcription.d.ts +2 -2
  43. package/dist/hooks/use-voice-input.d.ts +13 -4
  44. package/dist/index.d.ts +6 -5
  45. package/dist/index.js +904 -538
  46. package/dist/index.js.map +1 -1
  47. package/dist/lib/composer-responsive-context.d.ts +9 -0
  48. package/dist/lib/use-portal-token-style.d.ts +14 -3
  49. package/dist/machines.js +2 -2
  50. package/dist/{queue-surface-implementation-J4XJFKRW.js → queue-surface-implementation-U5JFG7HT.js} +19 -18
  51. package/dist/queue-surface-implementation-U5JFG7HT.js.map +1 -0
  52. package/dist/realtime/realtime-control.d.ts +9 -0
  53. package/dist/realtime.js +29 -32
  54. package/dist/realtime.js.map +1 -1
  55. package/dist/session-ui.js +5 -5
  56. package/dist/session.js +4 -4
  57. package/dist/timeline/activity-rail.d.ts +3 -2
  58. package/dist/timeline/index.d.ts +2 -2
  59. package/dist/timeline/parsers.d.ts +3 -3
  60. package/dist/timeline/registry.d.ts +3 -0
  61. package/dist/timeline/screenshot-lightbox.d.ts +1 -1
  62. package/dist/timeline/types.d.ts +4 -0
  63. package/dist/voice-recording-store.d.ts +8 -2
  64. package/package.json +20 -4
  65. package/src/client.ts +3 -0
  66. package/src/components/chat-composer.tsx +12 -2
  67. package/src/components/code-editor.tsx +1 -1
  68. package/src/components/command-palette.tsx +1 -1
  69. package/src/components/composer-transcription-control.tsx +41 -16
  70. package/src/components/composer.tsx +52 -26
  71. package/src/components/copy-button.tsx +2 -2
  72. package/src/components/desktop-viewer.tsx +2 -2
  73. package/src/components/file-browser.tsx +39 -17
  74. package/src/components/human-input-form.tsx +2 -2
  75. package/src/components/machine-card.tsx +1 -1
  76. package/src/components/machines/machine-detail.tsx +39 -15
  77. package/src/components/machines-dashboard.tsx +7 -6
  78. package/src/components/message-timeline.tsx +44 -11
  79. package/src/components/model-picker.tsx +2 -2
  80. package/src/components/model-policy-picker.tsx +16 -15
  81. package/src/components/queue-surface-implementation.tsx +20 -16
  82. package/src/components/queue-surface-state.tsx +2 -2
  83. package/src/components/sandbox-files.tsx +140 -60
  84. package/src/components/sandbox-terminal.tsx +24 -5
  85. package/src/components/sandbox-workspace.tsx +180 -142
  86. package/src/components/session-chrome.tsx +14 -14
  87. package/src/components/tooltip.tsx +13 -16
  88. package/src/components/user-message-body.tsx +1 -1
  89. package/src/components/workbench-changes.tsx +4 -4
  90. package/src/components/workspace-dock.tsx +13 -5
  91. package/src/composer.ts +3 -0
  92. package/src/hooks/internal.ts +132 -38
  93. package/src/hooks/use-codex-accounts.ts +4 -4
  94. package/src/hooks/use-environments.ts +24 -5
  95. package/src/hooks/use-file-attachments.ts +70 -32
  96. package/src/hooks/use-goal.ts +17 -5
  97. package/src/hooks/use-machines.ts +47 -1
  98. package/src/hooks/use-sandbox-files.ts +310 -59
  99. package/src/hooks/use-sandbox-git.ts +148 -41
  100. package/src/hooks/use-session-capabilities.ts +35 -24
  101. package/src/hooks/use-session-events.ts +18 -4
  102. package/src/hooks/use-transcription.ts +4 -14
  103. package/src/hooks/use-turn-queue.ts +17 -5
  104. package/src/hooks/use-voice-input.ts +481 -36
  105. package/src/hooks/use-windowed-sections.ts +9 -7
  106. package/src/index.ts +11 -6
  107. package/src/lib/composer-responsive-context.ts +15 -0
  108. package/src/lib/use-portal-token-style.ts +60 -15
  109. package/src/provider.tsx +52 -7
  110. package/src/realtime/dropdown-menu.tsx +15 -19
  111. package/src/realtime/realtime-control.tsx +18 -7
  112. package/src/timeline/activity-rail.tsx +15 -6
  113. package/src/timeline/index.ts +1 -1
  114. package/src/timeline/parsers.ts +33 -20
  115. package/src/timeline/projection.ts +23 -0
  116. package/src/timeline/registry.ts +7 -0
  117. package/src/timeline/screenshot-lightbox.tsx +15 -4
  118. package/src/timeline/shared.tsx +8 -8
  119. package/src/timeline/tool-renderers.tsx +166 -22
  120. package/src/timeline/turn-summary.tsx +6 -6
  121. package/src/timeline/types.ts +4 -0
  122. package/src/voice-recording-store.ts +15 -0
  123. package/styles/compiled.css +5065 -0
  124. package/styles/compiled.d.ts +2 -0
  125. package/styles/effective-tokens.css +27 -0
  126. package/styles/index.css +3 -0
  127. package/styles/responsive.css +196 -0
  128. package/styles/responsive.d.ts +2 -0
  129. package/styles/tokens.css +50 -17
  130. package/dist/chunk-6EDV6YFE.js.map +0 -1
  131. package/dist/chunk-EB67J347.js.map +0 -1
  132. package/dist/chunk-EHSYZ4KP.js.map +0 -1
  133. package/dist/chunk-FD72GJMK.js.map +0 -1
  134. package/dist/chunk-JB5725BI.js +0 -48
  135. package/dist/chunk-JB5725BI.js.map +0 -1
  136. package/dist/chunk-L3EWO3UK.js.map +0 -1
  137. package/dist/chunk-LWR4MXSS.js.map +0 -1
  138. package/dist/chunk-SRFUT2ZU.js.map +0 -1
  139. package/dist/chunk-U3CSCMWA.js.map +0 -1
  140. package/dist/queue-surface-implementation-J4XJFKRW.js.map +0 -1
  141. /package/dist/{chunk-HFO4ERGQ.js.map → chunk-FWXGSNXT.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/react",
3
- "version": "0.45.0",
3
+ "version": "0.48.0",
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": {
@@ -14,7 +14,9 @@
14
14
  "styles"
15
15
  ],
16
16
  "type": "module",
17
- "sideEffects": false,
17
+ "sideEffects": [
18
+ "**/*.css"
19
+ ],
18
20
  "main": "./dist/index.js",
19
21
  "module": "./dist/index.js",
20
22
  "types": "./dist/index.d.ts",
@@ -49,10 +51,22 @@
49
51
  },
50
52
  "./styles.css": {
51
53
  "types": "./styles/index.d.ts",
54
+ "style": "./styles/index.css",
52
55
  "import": "./styles/index.css"
53
56
  },
57
+ "./compiled.css": {
58
+ "types": "./styles/compiled.d.ts",
59
+ "style": "./styles/compiled.css",
60
+ "import": "./styles/compiled.css"
61
+ },
62
+ "./responsive.css": {
63
+ "types": "./styles/responsive.d.ts",
64
+ "style": "./styles/responsive.css",
65
+ "import": "./styles/responsive.css"
66
+ },
54
67
  "./tokens.css": {
55
68
  "types": "./styles/tokens.d.ts",
69
+ "style": "./styles/tokens.css",
56
70
  "import": "./styles/tokens.css"
57
71
  }
58
72
  },
@@ -62,7 +76,9 @@
62
76
  },
63
77
  "scripts": {
64
78
  "typecheck": "tsc --noEmit",
65
- "build": "bun ../../scripts/build-typescript-package.ts",
79
+ "build": "bun run build:css && bun ../../scripts/build-typescript-package.ts",
80
+ "build:css": "bun scripts/build-compiled-css.ts",
81
+ "check:css": "bun scripts/build-compiled-css.ts --check",
66
82
  "demo": "vite dev demo --port 3100",
67
83
  "demo:build": "vite build demo",
68
84
  "demo:build:deployed": "OPENGENI_REACT_DEMO_BASE=/react-demo/ OPENGENI_REACT_DEMO_OUT_DIR=../../../apps/web/dist/react-demo vite build demo",
@@ -75,7 +91,7 @@
75
91
  "@dnd-kit/utilities": "3.2.2",
76
92
  "@fontsource-variable/noto-sans-arabic": "5.2.10",
77
93
  "@fontsource-variable/noto-sans-jp": "5.2.10",
78
- "@opengeni/sdk": "^0.44.6",
94
+ "@opengeni/sdk": "^0.48.0",
79
95
  "clsx": "^2.1.1",
80
96
  "lucide-react": "^1.8.0",
81
97
  "motion": "^12.0.0",
package/src/client.ts CHANGED
@@ -50,6 +50,7 @@ export type SessionClientLike = Pick<
50
50
  | "createFileDownloadUrl"
51
51
  // VariableSets
52
52
  | "listVariableSets"
53
+ | "getVariableSetVariable"
53
54
  | "createVariableSet"
54
55
  | "updateVariableSet"
55
56
  | "deleteVariableSet"
@@ -99,6 +100,7 @@ export type SessionClientLike = Pick<
99
100
  | "detachViewer"
100
101
  // Channel-A structured services (terminal-as-events feed via fs/git/terminal)
101
102
  | "fsList"
103
+ | "fsListBatch"
102
104
  | "fsRead"
103
105
  | "fsWrite"
104
106
  | "fsDelete"
@@ -106,6 +108,7 @@ export type SessionClientLike = Pick<
106
108
  | "fsMkdir"
107
109
  | "gitStatus"
108
110
  | "gitDiff"
111
+ | "gitReadBatch"
109
112
  // Workbench v2 turn-end capture reads (the cold-paint source; M3 consumes these)
110
113
  | "getWorkspaceCapture"
111
114
  | "getWorkspaceCaptureFile"
@@ -29,6 +29,7 @@ import {
29
29
  useChatComposerController,
30
30
  type ChatComposerMessages,
31
31
  type ComposerControlLinks,
32
+ type ResponsiveBasis,
32
33
  } from "./composer";
33
34
  import {
34
35
  ComposerTranscriptionControl,
@@ -39,6 +40,11 @@ export { OPEN_WORKSTREAM_CONTROL_EVENT };
39
40
 
40
41
  export type ChatComposerProps = {
41
42
  composer: ComposerState;
43
+ /**
44
+ * Measurement surface for responsive chrome. Defaults to the historical
45
+ * viewport breakpoints; opt into `container` for narrow embedded panels.
46
+ */
47
+ responsiveBasis?: ResponsiveBasis | undefined;
42
48
  /** Canonical workstream control, separate from lifecycle status. */
43
49
  effectiveControl?: EffectiveSessionControl | null | undefined;
44
50
  /** Waiting prompts already ahead of a normal Send. */
@@ -91,6 +97,7 @@ export type ChatComposerProps = {
91
97
  */
92
98
  export function ChatComposer({
93
99
  composer,
100
+ responsiveBasis,
94
101
  effectiveControl,
95
102
  queuedAheadCount,
96
103
  canControlWorkspace,
@@ -140,7 +147,7 @@ export function ChatComposer({
140
147
  const stackActions = hasControls && Boolean(actionsStart);
141
148
 
142
149
  return (
143
- <Root controller={controller} className={className}>
150
+ <Root controller={controller} responsiveBasis={responsiveBasis} className={className}>
144
151
  <Frame>
145
152
  <CommandPalette />
146
153
  <Surface>
@@ -152,7 +159,10 @@ export function ChatComposer({
152
159
  {controller.confirmState ? (
153
160
  <Confirmation />
154
161
  ) : (
155
- <Footer className={stackActions ? "max-sm:flex-nowrap sm:flex-wrap" : undefined}>
162
+ <Footer
163
+ data-og-stack-actions={stackActions ? "" : undefined}
164
+ className={stackActions ? "max-sm:flex-nowrap sm:flex-wrap" : undefined}
165
+ >
156
166
  <LayoutGroup id="og-composer-footer">
157
167
  {hasControls ? (
158
168
  <Controls
@@ -458,7 +458,7 @@ function PlainTextarea({
458
458
  }
459
459
  }}
460
460
  aria-label="Plain text file editor"
461
- className="h-full w-full resize-none bg-transparent p-2 font-og-mono text-og-sm text-og-fg outline-none"
461
+ className="h-full w-full resize-none bg-transparent p-2 font-og-mono text-og-sm text-og-fg outline-hidden"
462
462
  />
463
463
  );
464
464
  }
@@ -101,7 +101,7 @@ export function CommandPalette({
101
101
  {dangerLabel}
102
102
  </span>
103
103
  ) : null}
104
- <span className="truncate text-[12px] text-og-fg-subtle max-sm:hidden">
104
+ <span className="og-command-description truncate text-[12px] text-og-fg-subtle max-sm:hidden">
105
105
  {command.description}
106
106
  </span>
107
107
  </span>
@@ -35,10 +35,12 @@ export type ComposerTranscriptionMessages = {
35
35
  stop: string;
36
36
  cancel: string;
37
37
  retry: string;
38
+ pauseRetrying: string;
38
39
  requestingPermission: string;
39
40
  recording: string;
40
41
  saving: string;
41
42
  transcribing: string;
43
+ retrying: string;
42
44
  recovered: string;
43
45
  recoveredTranscript: string;
44
46
  insertRecoveredTranscript: string;
@@ -61,10 +63,12 @@ const defaultMessages: ComposerTranscriptionMessages = {
61
63
  stop: "Stop and transcribe",
62
64
  cancel: "Cancel recording",
63
65
  retry: "Retry voice input",
66
+ pauseRetrying: "Pause automatic retry",
64
67
  requestingPermission: "Requesting microphone…",
65
68
  recording: "Recording. Press Escape to cancel.",
66
69
  saving: "Saving audio locally…",
67
70
  transcribing: "Transcribing…",
71
+ retrying: "Recording saved. Retrying automatically…",
68
72
  recovered: "Recording recovered and saved locally.",
69
73
  recoveredTranscript: "Transcript saved locally. Check your draft before inserting.",
70
74
  insertRecoveredTranscript: "Insert saved transcript",
@@ -138,6 +142,7 @@ export function ComposerTranscriptionControl({
138
142
  createOwnerId,
139
143
  });
140
144
  const { status } = transcription;
145
+ const retrying = status === "retrying";
141
146
 
142
147
  const cancelTranscription = transcription.cancel;
143
148
  useEffect(() => {
@@ -153,7 +158,7 @@ export function ComposerTranscriptionControl({
153
158
  status === "transcribing";
154
159
  const recoverable =
155
160
  transcription.hasRecoverableRecording &&
156
- (status === "recovered" || status === "transcript-ready" || status === "error");
161
+ (retrying || status === "recovered" || status === "transcript-ready" || status === "error");
157
162
  const savedTranscript = status === "transcript-ready" && transcription.savedTranscript !== null;
158
163
  const unavailableMessage = composer.disabled
159
164
  ? messages.unavailableDisabled
@@ -175,13 +180,15 @@ export function ComposerTranscriptionControl({
175
180
  ? messages.saving
176
181
  : status === "transcribing"
177
182
  ? messages.transcribing
178
- : status === "recovered"
179
- ? messages.recovered
180
- : status === "transcript-ready"
181
- ? messages.recoveredTranscript
182
- : status === "error"
183
- ? (errorMessage ?? messages.errorUnknown)
184
- : unavailableMessage;
183
+ : retrying
184
+ ? messages.retrying
185
+ : status === "recovered"
186
+ ? messages.recovered
187
+ : status === "transcript-ready"
188
+ ? messages.recoveredTranscript
189
+ : status === "error"
190
+ ? (errorMessage ?? messages.errorUnknown)
191
+ : unavailableMessage;
185
192
 
186
193
  function start(event: MouseEvent<HTMLButtonElement>) {
187
194
  if (unavailableMessage) {
@@ -217,23 +224,39 @@ export function ComposerTranscriptionControl({
217
224
  "bg-og-surface-2/70 pl-2 pr-1 pointer-coarse:h-11",
218
225
  )}
219
226
  >
220
- <span className="max-w-44 truncate text-og-xs text-og-fg-muted max-sm:max-w-28">
227
+ <span className="og-transcription-recovered-label max-w-44 truncate text-og-xs text-og-fg-muted max-sm:max-w-28">
221
228
  {savedTranscript
222
229
  ? (errorMessage ?? messages.recoveredTranscript)
223
- : status === "error"
224
- ? (errorMessage ?? messages.errorRetryable)
225
- : messages.recovered}
230
+ : retrying
231
+ ? messages.retrying
232
+ : status === "error"
233
+ ? (errorMessage ?? messages.errorRetryable)
234
+ : messages.recovered}
226
235
  </span>
227
- <Tip tip={savedTranscript ? messages.insertRecoveredTranscript : messages.retry}>
236
+ <Tip
237
+ tip={
238
+ savedTranscript
239
+ ? messages.insertRecoveredTranscript
240
+ : retrying
241
+ ? messages.pauseRetrying
242
+ : messages.retry
243
+ }
244
+ >
228
245
  <button
229
246
  type="button"
230
247
  onClick={() =>
231
248
  savedTranscript
232
249
  ? void transcription.insertSavedTranscript()
233
- : transcription.retry()
250
+ : retrying
251
+ ? transcription.cancel()
252
+ : transcription.retry()
234
253
  }
235
254
  aria-label={
236
- savedTranscript ? messages.insertRecoveredTranscript : messages.retry
255
+ savedTranscript
256
+ ? messages.insertRecoveredTranscript
257
+ : retrying
258
+ ? messages.pauseRetrying
259
+ : messages.retry
237
260
  }
238
261
  className={cn(
239
262
  "inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm",
@@ -243,6 +266,8 @@ export function ComposerTranscriptionControl({
243
266
  >
244
267
  {savedTranscript ? (
245
268
  <ClipboardPasteIcon className="size-3.5" />
269
+ ) : retrying ? (
270
+ <SquareIcon className="size-3.5" />
246
271
  ) : (
247
272
  <RefreshCwIcon className="size-3.5" />
248
273
  )}
@@ -366,7 +391,7 @@ export function ComposerTranscriptionControl({
366
391
  <Tip tip={errorMessage}>
367
392
  <span
368
393
  aria-hidden="true"
369
- className="max-w-40 truncate text-og-xs text-og-status-failed max-sm:max-w-24"
394
+ className="og-transcription-error-label max-w-40 truncate text-og-xs text-og-status-failed max-sm:max-w-24"
370
395
  >
371
396
  {errorMessage}
372
397
  </span>
@@ -45,6 +45,10 @@ import {
45
45
  type SlashCommandContext,
46
46
  } from "../hooks/use-slash-commands";
47
47
  import { cn } from "../lib/cn";
48
+ import {
49
+ ComposerResponsiveContext,
50
+ type ResponsiveBasis,
51
+ } from "../lib/composer-responsive-context";
48
52
  import { composerSubmissionErrorMessage, formatBytes, formatRelativeTime } from "../lib/format";
49
53
  import type { PickerModelRow } from "../model-policy";
50
54
  import { OPEN_WORKSTREAM_CONTROL_EVENT } from "../workstream-control-event";
@@ -53,6 +57,7 @@ import { ModelPicker as ModelPickerView } from "./model-picker";
53
57
  import { TooltipProvider } from "./tooltip";
54
58
 
55
59
  export { OPEN_WORKSTREAM_CONTROL_EVENT };
60
+ export type { ResponsiveBasis } from "../lib/composer-responsive-context";
56
61
 
57
62
  function ComposerTip({
58
63
  tip,
@@ -743,29 +748,43 @@ function isRecord(value: unknown): value is Record<string, unknown> {
743
748
  export type ComposerRootProps = Omit<ComponentPropsWithoutRef<"div">, "children"> & {
744
749
  controller: ChatComposerController;
745
750
  children: ReactNode;
751
+ /**
752
+ * Measurement surface for responsive composer chrome. `viewport` preserves
753
+ * the existing breakpoint behavior; `container` responds to this root's
754
+ * inline size and also bounds composer-owned portalled menus to it.
755
+ */
756
+ responsiveBasis?: ResponsiveBasis | undefined;
746
757
  };
747
758
 
748
759
  export const Root = forwardRef<HTMLDivElement, ComposerRootProps>(function ComposerRoot(
749
- { controller, children, className, style, ...props },
760
+ { controller, children, className, style, responsiveBasis = "viewport", ...props },
750
761
  forwardedRef,
751
762
  ) {
763
+ const responsiveRootRef = useRef<HTMLDivElement | null>(null);
764
+ const responsiveContext = useMemo(
765
+ () => ({ responsiveBasis, rootRef: responsiveRootRef }),
766
+ [responsiveBasis],
767
+ );
752
768
  // Provider stays on Root so optional Radix tip surfaces (voice input) work.
753
769
  // Ordinary composer actions use native `title` via ComposerTip to avoid
754
770
  // pulling Popper into every tip hotspot.
755
771
  return (
756
772
  <ComposerContext.Provider value={controller}>
757
- <TooltipProvider delayDuration={300}>
758
- <div
759
- {...props}
760
- ref={mergeRefs(controller.rootRef, forwardedRef)}
761
- data-og-composer-id={controller.id}
762
- className={cn("og-root", className)}
763
- style={{ paddingBottom: "env(safe-area-inset-bottom)", ...style }}
764
- >
765
- {children}
766
- <ComposerAnnouncements />
767
- </div>
768
- </TooltipProvider>
773
+ <ComposerResponsiveContext.Provider value={responsiveContext}>
774
+ <TooltipProvider delayDuration={300}>
775
+ <div
776
+ {...props}
777
+ ref={mergeRefs(controller.rootRef, responsiveRootRef, forwardedRef)}
778
+ data-og-composer-id={controller.id}
779
+ data-og-responsive-basis={responsiveBasis}
780
+ className={cn("og-root og-composer", className)}
781
+ style={{ paddingBottom: "env(safe-area-inset-bottom)", ...style }}
782
+ >
783
+ {children}
784
+ <ComposerAnnouncements />
785
+ </div>
786
+ </TooltipProvider>
787
+ </ComposerResponsiveContext.Provider>
769
788
  </ComposerContext.Provider>
770
789
  );
771
790
  });
@@ -957,8 +976,8 @@ export const Input = forwardRef<HTMLTextAreaElement, ComposerInputProps>(functio
957
976
  paletteOpen ? `${controller.listboxId}-option-${controller.palette.highlight}` : undefined
958
977
  }
959
978
  className={cn(
960
- "block w-full resize-none bg-transparent px-3.5 pt-3 pb-1 text-og-composer md:px-4 md:text-og-composer-wide",
961
- "text-og-fg placeholder:text-og-fg-subtle focus:outline-none focus-visible:outline-none",
979
+ "og-composer-input block w-full resize-none bg-transparent px-3.5 pt-3 pb-1 text-og-composer md:px-4 md:text-og-composer-wide pointer-coarse:min-h-11",
980
+ "text-og-fg placeholder:text-og-fg-subtle focus:outline-hidden focus-visible:outline-hidden",
962
981
  "disabled:cursor-not-allowed disabled:opacity-60",
963
982
  className,
964
983
  )}
@@ -992,7 +1011,7 @@ export const Footer = forwardRef<HTMLDivElement, ComposerFooterProps>(function C
992
1011
  {...props}
993
1012
  ref={ref}
994
1013
  className={cn(
995
- "flex items-end gap-1.5 px-2 pb-2 pt-0.5 sm:px-2.5 sm:pb-2.5",
1014
+ "og-composer-footer flex items-end gap-1.5 px-2 pb-2 pt-0.5 sm:px-2.5 sm:pb-2.5",
996
1015
  // Mobile: one control row — never wrap into a second toolbar line.
997
1016
  "max-sm:flex-nowrap max-sm:items-center max-sm:gap-1",
998
1017
  className,
@@ -1010,7 +1029,7 @@ export const Controls = forwardRef<HTMLSpanElement, ComposerControlsProps>(
1010
1029
  {...props}
1011
1030
  ref={ref}
1012
1031
  className={cn(
1013
- "flex min-w-0 flex-1 flex-wrap items-center gap-1.5",
1032
+ "og-composer-controls flex min-w-0 flex-1 flex-wrap items-center gap-1.5",
1014
1033
  "max-sm:flex-nowrap max-sm:gap-1",
1015
1034
  className,
1016
1035
  )}
@@ -1030,7 +1049,10 @@ export const Hint = forwardRef<HTMLSpanElement, ComposerHintProps>(function Comp
1030
1049
  <span
1031
1050
  {...props}
1032
1051
  ref={ref}
1033
- className={cn("min-w-0 flex-1 px-1.5 text-og-xs text-og-fg-subtle max-sm:hidden", className)}
1052
+ className={cn(
1053
+ "og-composer-hint min-w-0 flex-1 px-1.5 text-og-xs text-og-fg-subtle max-sm:hidden",
1054
+ className,
1055
+ )}
1034
1056
  >
1035
1057
  {children ?? controller.messages.keyboardHint}
1036
1058
  </span>
@@ -1048,7 +1070,7 @@ export const Actions = forwardRef<HTMLSpanElement, ComposerActionsProps>(functio
1048
1070
  {...props}
1049
1071
  ref={ref}
1050
1072
  className={cn(
1051
- "ml-auto flex shrink-0 items-center gap-1.5",
1073
+ "og-composer-actions ml-auto flex shrink-0 items-center gap-1.5",
1052
1074
  "max-sm:flex-nowrap max-sm:gap-1",
1053
1075
  className,
1054
1076
  )}
@@ -1095,7 +1117,7 @@ export const AttachButton = forwardRef<HTMLButtonElement, ComposerAttachButtonPr
1095
1117
  className={cn(
1096
1118
  "inline-flex size-8 items-center justify-center rounded-og-md",
1097
1119
  "text-og-fg-muted transition-colors duration-150 hover:bg-og-surface-2 hover:text-og-fg",
1098
- "disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:size-9",
1120
+ "disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:size-11",
1099
1121
  className,
1100
1122
  )}
1101
1123
  >
@@ -1167,7 +1189,7 @@ export const PauseButton = forwardRef<HTMLButtonElement, ComposerPauseButtonProp
1167
1189
  disabled={busy}
1168
1190
  aria-label={ariaLabel ?? controller.messages.pauseAriaLabel}
1169
1191
  className={cn(
1170
- "inline-flex size-8 items-center justify-center rounded-og-md border border-og-border pointer-coarse:size-9",
1192
+ "inline-flex size-8 items-center justify-center rounded-og-md border border-og-border pointer-coarse:size-11",
1171
1193
  "bg-og-surface-2 text-og-fg-muted transition-colors duration-150",
1172
1194
  "hover:border-og-status-waiting/50 hover:text-og-status-waiting",
1173
1195
  "disabled:opacity-50",
@@ -1216,7 +1238,7 @@ export const SendButton = forwardRef<HTMLButtonElement, ComposerSendButtonProps>
1216
1238
  : controller.messages.sendMessageAriaLabel)
1217
1239
  }
1218
1240
  className={cn(
1219
- "inline-flex size-8 items-center justify-center rounded-og-md pointer-coarse:size-9",
1241
+ "inline-flex size-8 items-center justify-center rounded-og-md pointer-coarse:size-11",
1220
1242
  "bg-og-accent text-og-accent-fg shadow-og-sm",
1221
1243
  "transition-[background-color,transform,opacity] duration-150 ease-og-spring",
1222
1244
  "hover:bg-og-accent-strong active:scale-95",
@@ -1386,8 +1408,12 @@ function WorkstreamPausedStrip({
1386
1408
  ) : (
1387
1409
  <PlayIcon className="size-3.5 fill-current" />
1388
1410
  )}
1389
- <span className="hidden min-[400px]:inline">{messages.resumeThisWorkstream}</span>
1390
- <span className="min-[400px]:hidden">{messages.resumeShort}</span>
1411
+ <span className="og-composer-resume-label-long hidden min-[400px]:inline">
1412
+ {messages.resumeThisWorkstream}
1413
+ </span>
1414
+ <span className="og-composer-resume-label-short min-[400px]:hidden">
1415
+ {messages.resumeShort}
1416
+ </span>
1391
1417
  </button>
1392
1418
  <button
1393
1419
  type="button"
@@ -1412,7 +1438,7 @@ function WorkstreamPausedStrip({
1412
1438
  {blockerHref(entry, controlLinks) ? (
1413
1439
  <a
1414
1440
  href={blockerHref(entry, controlLinks)}
1415
- className="font-medium text-og-fg underline decoration-og-border-strong underline-offset-2 hover:text-og-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-ring/40"
1441
+ className="font-medium text-og-fg underline decoration-og-border-strong underline-offset-2 hover:text-og-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-ring/40"
1416
1442
  >
1417
1443
  {entry.displayName}
1418
1444
  </a>
@@ -1582,7 +1608,7 @@ function AttachmentChips({
1582
1608
  onRetry?: ((id: string) => void) | undefined;
1583
1609
  }) {
1584
1610
  return (
1585
- <div className="flex flex-wrap gap-2 border-b border-og-border px-3 py-2">
1611
+ <div className="flex flex-wrap gap-2 px-3 py-2">
1586
1612
  {attachments.map((attachment) => {
1587
1613
  const failed = attachment.status === "failed";
1588
1614
  const statusText =
@@ -93,10 +93,10 @@ export function CopyButton({
93
93
  aria-label={tip}
94
94
  onClick={onClick}
95
95
  className={cn(
96
- "inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm",
96
+ "inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm pointer-coarse:size-11",
97
97
  "text-og-fg-subtle transition-[opacity,color,background-color] duration-150",
98
98
  "hover:bg-og-surface-2/80 hover:text-og-fg",
99
- "focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent/40",
99
+ "focus-visible:opacity-100 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40",
100
100
  reveal === "group-hover" &&
101
101
  "opacity-0 group-hover/copy:opacity-100 group-focus-within/copy:opacity-100 pointer-coarse:opacity-70",
102
102
  copied && "opacity-100 text-og-accent",
@@ -507,7 +507,7 @@ function TakeControlCallToAction({
507
507
  "border-og-border",
508
508
  "bg-og-bg/85 backdrop-blur-md",
509
509
  "shadow-og-lg",
510
- "outline-none transition-all duration-150 ease-out",
510
+ "outline-hidden transition-all duration-150 ease-out",
511
511
  "focus-visible:ring-2 focus-visible:ring-og-accent focus-visible:ring-offset-2 focus-visible:ring-offset-og-bg",
512
512
  disabled
513
513
  ? "cursor-not-allowed opacity-60"
@@ -568,7 +568,7 @@ function InControlBar({ shared, onRelease }: { shared: boolean; onRelease: () =>
568
568
  className={cn(
569
569
  "inline-flex items-center gap-1.5 rounded-og-sm border px-2.5 py-1 text-og-xs font-semibold shadow-og-md transition-colors",
570
570
  "border-og-accent bg-og-bg/90 text-og-fg backdrop-blur-sm",
571
- "outline-none hover:bg-og-accent hover:text-og-accent-fg focus-visible:ring-2 focus-visible:ring-og-accent",
571
+ "outline-hidden hover:bg-og-accent hover:text-og-accent-fg focus-visible:ring-2 focus-visible:ring-og-accent",
572
572
  )}
573
573
  >
574
574
  <MonitorIcon className="size-3.5" strokeWidth={2} aria-hidden />
@@ -24,7 +24,11 @@ import {
24
24
  import { AlertDialog } from "radix-ui";
25
25
  import { VList, type VListHandle } from "virtua";
26
26
  import { cn } from "../lib/cn";
27
- import { type PortalTokenStyle, usePortalTokenStyle } from "../lib/use-portal-token-style";
27
+ import {
28
+ type PortalTokenStyle,
29
+ usePortalTokenSource,
30
+ usePortalTokenStyle,
31
+ } from "../lib/use-portal-token-style";
28
32
  import { useUnicodeFallbackFonts } from "../lib/use-unicode-fonts";
29
33
  import type { FileTreeNode, UseSandboxFilesResult } from "../hooks/use-sandbox-files";
30
34
 
@@ -155,10 +159,11 @@ export function FileBrowser({
155
159
  const [busy, setBusy] = useState(false);
156
160
  const [coarsePointer, setCoarsePointer] = useState(false);
157
161
  const treeId = useId();
158
- const containerRef = useRef<HTMLDivElement | null>(null);
162
+ const container = usePortalTokenSource<HTMLDivElement>();
163
+ const containerRef = container.currentRef;
159
164
  const deleteReturnFocusRef = useRef<HTMLElement | null>(null);
160
165
  const vlistRef = useRef<VListHandle>(null);
161
- const portalTokenStyle = usePortalTokenStyle(containerRef);
166
+ const portalTokenStyle = usePortalTokenStyle(container.source);
162
167
  const getTreeElement = useCallback(
163
168
  () => (typeof document === "undefined" ? null : document.getElementById(treeId)),
164
169
  [treeId],
@@ -569,6 +574,7 @@ export function FileBrowser({
569
574
  runDelete,
570
575
  supportsMutation,
571
576
  coarsePointer,
577
+ containerRef,
572
578
  rowIndexByPath,
573
579
  treeId,
574
580
  ],
@@ -594,7 +600,13 @@ export function FileBrowser({
594
600
  // Retry lives next to the state it explains (the degraded/empty notice), so do
595
601
  // not render a second toolbar retry for the same failure.
596
602
  const showToolbar = supportsMutation || result.loading;
597
- const usesVirtualTree = !showErrorEmpty && !showEmpty;
603
+ // Virtua caches the previous visible range when a lazy-expanded subtree is
604
+ // inserted in the middle of a short list. The spacer grows but later root
605
+ // siblings can remain unmounted until a scroll/resize (for example,
606
+ // `.bash_history` vanished after expanding `repositories/<repo>`). Small
607
+ // trees do not benefit from virtualization, so render them directly and keep
608
+ // Virtua only for genuinely large workspaces.
609
+ const usesVirtualTree = !showErrorEmpty && !showEmpty && renderItems.length > 500;
598
610
 
599
611
  // Render a SINGLE node row (no recursion — children are separate flat items the
600
612
  // virtualizer renders). Returns the row element; the caller assigns the key.
@@ -854,7 +866,7 @@ export function FileBrowser({
854
866
  type="button"
855
867
  onClick={() => void result.refresh()}
856
868
  disabled={result.loading}
857
- className="inline-flex min-h-7 shrink-0 items-center gap-1 rounded-og-sm border border-og-border bg-og-surface-1 px-2 font-medium text-og-fg transition-colors hover:border-og-border-strong focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:min-h-11"
869
+ className="inline-flex min-h-7 shrink-0 items-center gap-1 rounded-og-sm border border-og-border bg-og-surface-1 px-2 font-medium text-og-fg transition-colors hover:border-og-border-strong focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:min-h-11"
858
870
  >
859
871
  <RefreshCwIcon
860
872
  className={cn("size-3", result.loading && "animate-spin motion-reduce:animate-none")}
@@ -868,13 +880,7 @@ export function FileBrowser({
868
880
  {/* The tree itself. The root is a drop target so a node can be moved to "". */}
869
881
  {/* biome-ignore lint/a11y/noNoninteractiveTabindex: the tree owns keyboard nav */}
870
882
  <div
871
- ref={containerRef}
872
- id={usesVirtualTree ? undefined : treeId}
873
- role={usesVirtualTree ? undefined : "tree"}
874
- aria-label={usesVirtualTree ? undefined : "Workspace files"}
875
- aria-activedescendant={usesVirtualTree ? undefined : activeDescendantId}
876
- tabIndex={usesVirtualTree ? undefined : 0}
877
- aria-multiselectable={usesVirtualTree ? undefined : false}
883
+ ref={container.ref}
878
884
  onFocus={(event) => {
879
885
  if ((event.target as HTMLElement).id === treeId && cursorIndex < 0) {
880
886
  const first = flatRows[0];
@@ -901,7 +907,7 @@ export function FileBrowser({
901
907
  : undefined
902
908
  }
903
909
  className={cn(
904
- "flex min-h-0 min-w-0 flex-1 flex-col p-1 outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent",
910
+ "flex min-h-0 min-w-0 flex-1 flex-col p-1",
905
911
  dragOver === "" && "ring-1 ring-inset ring-og-accent",
906
912
  )}
907
913
  data-opengeni-file-tree
@@ -929,6 +935,22 @@ export function FileBrowser({
929
935
  <div className="p-2 text-og-sm text-og-fg-subtle">
930
936
  {emptyState ?? "This directory is empty"}
931
937
  </div>
938
+ ) : !usesVirtualTree ? (
939
+ <div
940
+ id={treeId}
941
+ role="tree"
942
+ aria-label="Workspace files"
943
+ aria-activedescendant={activeDescendantId}
944
+ aria-multiselectable={false}
945
+ tabIndex={0}
946
+ className="min-h-0 min-w-0 flex-1 overflow-auto outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent"
947
+ >
948
+ {renderItems.map((item) => (
949
+ <div key={itemKey(item)} className="min-w-0">
950
+ {renderItem(item)}
951
+ </div>
952
+ ))}
953
+ </div>
932
954
  ) : (
933
955
  // Virtualized: only the rows in/near the viewport mount, so a 3k-node
934
956
  // tree stays responsive. The flat item list already carries the
@@ -941,7 +963,7 @@ export function FileBrowser({
941
963
  aria-activedescendant={activeDescendantId}
942
964
  aria-multiselectable={false}
943
965
  tabIndex={0}
944
- className="min-h-0 flex-1 outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent"
966
+ className="min-h-0 flex-1 outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent"
945
967
  itemSize={coarsePointer ? 44 : 28}
946
968
  ssrCount={Math.min(32, renderItems.length)}
947
969
  >
@@ -1041,9 +1063,9 @@ function DeleteDialog({
1041
1063
  }}
1042
1064
  >
1043
1065
  <AlertDialog.Portal container={typeof document === "undefined" ? null : document.body}>
1044
- <AlertDialog.Overlay className="fixed inset-0 z-[100] bg-black/45 backdrop-blur-[2px]" />
1066
+ <AlertDialog.Overlay className="og-root fixed inset-0 z-[100] bg-black/45 backdrop-blur-[2px]" />
1045
1067
  <AlertDialog.Content
1046
- className="og-root fixed left-1/2 top-1/2 z-[101] max-h-[calc(100dvh-2rem)] w-[calc(100%-2rem)] max-w-sm -translate-x-1/2 -translate-y-1/2 overflow-y-auto rounded-og-lg border border-og-border-strong bg-og-surface-1 p-4 text-og-fg shadow-2xl outline-none"
1068
+ className="og-root fixed left-1/2 top-1/2 z-[101] max-h-[calc(100dvh-2rem)] w-[calc(100%-2rem)] max-w-sm -translate-x-1/2 -translate-y-1/2 overflow-y-auto rounded-og-lg border border-og-border-strong bg-og-surface-1 p-4 text-og-fg shadow-2xl outline-hidden"
1047
1069
  style={portalTokenStyle}
1048
1070
  onOpenAutoFocus={(event) => {
1049
1071
  event.preventDefault();
@@ -1208,7 +1230,7 @@ function InlineInput({
1208
1230
  className={cn(
1209
1231
  "min-w-0 flex-1 rounded-og-sm border bg-og-bg px-1 py-0 text-og-sm",
1210
1232
  "border-og-accent text-og-fg",
1211
- "outline-none",
1233
+ "outline-hidden",
1212
1234
  )}
1213
1235
  />
1214
1236
  </div>
@@ -559,7 +559,7 @@ function QuestionControls({
559
559
  aria-describedby={describedBy}
560
560
  autoFocus={autoFocus}
561
561
  rows={2}
562
- className="min-h-14 w-full resize-y rounded-og-md border border-og-border bg-og-surface-1 px-3 py-2 text-og-sm text-og-fg outline-none placeholder:text-og-fg-subtle focus:border-og-accent"
562
+ className="min-h-14 w-full resize-y rounded-og-md border border-og-border bg-og-surface-1 px-3 py-2 text-og-sm text-og-fg outline-hidden placeholder:text-og-fg-subtle focus:border-og-accent"
563
563
  />
564
564
  ) : (
565
565
  <div
@@ -651,7 +651,7 @@ function QuestionControls({
651
651
  other: event.target.value,
652
652
  }))
653
653
  }
654
- 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-none focus:border-og-accent disabled:opacity-50"
654
+ 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"
655
655
  />
656
656
  </span>
657
657
  </label>