@opengeni/react 0.15.0 → 0.20.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 (101) hide show
  1. package/README.md +110 -10
  2. package/dist/{chunk-TOJR776I.js → chunk-6BXBGJ3B.js} +45 -369
  3. package/dist/chunk-6BXBGJ3B.js.map +1 -0
  4. package/dist/chunk-6UT5OC3P.js +2133 -0
  5. package/dist/chunk-6UT5OC3P.js.map +1 -0
  6. package/dist/chunk-EBTT3MYN.js +92 -0
  7. package/dist/chunk-EBTT3MYN.js.map +1 -0
  8. package/dist/chunk-HHFA4AFX.js +425 -0
  9. package/dist/chunk-HHFA4AFX.js.map +1 -0
  10. package/dist/chunk-LAZ2A743.js +2393 -0
  11. package/dist/chunk-LAZ2A743.js.map +1 -0
  12. package/dist/chunk-MRSECXRP.js +563 -0
  13. package/dist/chunk-MRSECXRP.js.map +1 -0
  14. package/dist/chunk-TK7G6XLT.js +18 -0
  15. package/dist/chunk-TK7G6XLT.js.map +1 -0
  16. package/dist/composer-DvUaa5ki.d.ts +585 -0
  17. package/dist/composer.d.ts +6 -0
  18. package/dist/composer.js +56 -0
  19. package/dist/composer.js.map +1 -0
  20. package/dist/index.d.ts +1288 -1785
  21. package/dist/index.js +7015 -8806
  22. package/dist/index.js.map +1 -1
  23. package/dist/machines.d.ts +412 -3
  24. package/dist/machines.js +3 -1
  25. package/dist/noto-sans-arabic-loader-IA2T4X2A.js +21 -0
  26. package/dist/noto-sans-arabic-loader-IA2T4X2A.js.map +1 -0
  27. package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js +292 -0
  28. package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js.map +1 -0
  29. package/dist/queue-surface-implementation-NQMV2ML3.js +890 -0
  30. package/dist/queue-surface-implementation-NQMV2ML3.js.map +1 -0
  31. package/dist/session-Gd4iVPYw.d.ts +425 -0
  32. package/dist/session-context-DIFFlXKc.d.ts +52 -0
  33. package/dist/session.d.ts +4 -0
  34. package/dist/session.js +50 -0
  35. package/dist/session.js.map +1 -0
  36. package/dist/use-composer-BCfm4mzX.d.ts +112 -0
  37. package/package.json +20 -4
  38. package/src/approvals.ts +5 -3
  39. package/src/client.ts +40 -0
  40. package/src/components/approval-surface.tsx +198 -0
  41. package/src/components/chat-composer.tsx +94 -993
  42. package/src/components/code-editor.tsx +104 -26
  43. package/src/components/command-palette.tsx +8 -2
  44. package/src/components/composer-transcription-control.tsx +211 -0
  45. package/src/components/composer.tsx +1536 -0
  46. package/src/components/desktop-viewer.tsx +3 -3
  47. package/src/components/enrollment-consent.tsx +2 -2
  48. package/src/components/file-browser.tsx +367 -29
  49. package/src/components/human-input-form.tsx +417 -0
  50. package/src/components/machine-card.tsx +1 -1
  51. package/src/components/machines-dashboard.tsx +3 -3
  52. package/src/components/message-timeline.tsx +15 -7
  53. package/src/components/model-picker.tsx +12 -3
  54. package/src/components/pierre-diff.tsx +32 -6
  55. package/src/components/queue-surface-implementation.tsx +1041 -0
  56. package/src/components/queue-surface-state.tsx +94 -0
  57. package/src/components/queue-surface.tsx +41 -559
  58. package/src/components/sandbox-files.tsx +138 -14
  59. package/src/components/sandbox-terminal.tsx +8 -2
  60. package/src/components/sandbox-workspace.tsx +339 -119
  61. package/src/components/workbench-changes.tsx +84 -33
  62. package/src/components/workspace-dock.tsx +188 -18
  63. package/src/composer.ts +60 -0
  64. package/src/hooks/internal.ts +115 -34
  65. package/src/hooks/use-composer.ts +335 -104
  66. package/src/hooks/use-human-input.ts +131 -0
  67. package/src/hooks/use-machine-chip.ts +2 -2
  68. package/src/hooks/use-machines.ts +27 -11
  69. package/src/hooks/use-sandbox-files.ts +310 -62
  70. package/src/hooks/use-sandbox-git.ts +154 -40
  71. package/src/hooks/use-sandbox-terminal.ts +28 -6
  72. package/src/hooks/use-session-capabilities.ts +38 -9
  73. package/src/hooks/use-session-control.ts +37 -8
  74. package/src/hooks/use-session-events.ts +522 -48
  75. package/src/hooks/use-slash-commands.ts +38 -38
  76. package/src/hooks/use-transcription.ts +757 -0
  77. package/src/hooks/use-turn-queue.ts +134 -66
  78. package/src/hooks/use-windowed-sections.ts +2 -2
  79. package/src/hooks/use-workspace-capture.ts +146 -40
  80. package/src/hooks/use-workspace-edit.ts +50 -14
  81. package/src/hooks/use-workspace-sessions.ts +3 -0
  82. package/src/human-input.ts +72 -0
  83. package/src/index.ts +76 -6
  84. package/src/lib/noto-sans-arabic-loader.ts +21 -0
  85. package/src/lib/noto-sans-jp-loader.generated.ts +295 -0
  86. package/src/lib/use-portal-token-style.ts +51 -0
  87. package/src/lib/use-unicode-fonts.ts +57 -0
  88. package/src/provider.tsx +4 -69
  89. package/src/session-context.ts +135 -0
  90. package/src/session.ts +79 -0
  91. package/src/timeline/parsers.ts +35 -0
  92. package/src/timeline/projection.ts +12 -4
  93. package/src/timeline/tool-renderers.tsx +41 -0
  94. package/src/timeline/turn-summary.tsx +2 -2
  95. package/src/timeline/types.ts +16 -8
  96. package/styles/index.css +56 -0
  97. package/styles/index.d.ts +2 -0
  98. package/styles/tokens.css +11 -6
  99. package/styles/tokens.d.ts +2 -0
  100. package/dist/chunk-TOJR776I.js.map +0 -1
  101. package/dist/machines-BpdwuQcD.d.ts +0 -449
@@ -1,9 +1,11 @@
1
- import type { FsReadResponse } from "@opengeni/sdk";
2
1
  import { FileCode2Icon, FileWarningIcon, LoaderCircleIcon } from "lucide-react";
3
2
  import { type ReactNode, useCallback, useEffect, useRef, useState } from "react";
4
3
  import { cn } from "../lib/cn";
5
4
  import { useThemeType } from "../lib/use-theme-type";
6
- import type { UseSandboxFilesResult } from "../hooks/use-sandbox-files";
5
+ import {
6
+ CapturedFileUnavailableError,
7
+ type UseSandboxFilesResult,
8
+ } from "../hooks/use-sandbox-files";
7
9
  import type { UseSandboxGitResult } from "../hooks/use-sandbox-git";
8
10
  import { CodeEditor } from "./code-editor";
9
11
  import { FileBrowser } from "./file-browser";
@@ -36,6 +38,15 @@ export type SandboxFilesProps = {
36
38
  requestedPathRequestId?: string | number | undefined;
37
39
  /** False while the parent is waking a cold sandbox for `requestedPath`. */
38
40
  requestedPathReady?: boolean | undefined;
41
+ /** The machine is cold and no durable capture exists yet. Render an explicit
42
+ * wake gate instead of an empty-tree lie or an implicit Channel-A read. */
43
+ workspaceResting?: boolean | undefined;
44
+ /** A deliberate wake has started but the live file surface is not ready yet. */
45
+ workspaceWaking?: boolean | undefined;
46
+ /** Whether live reads are currently authoritative. */
47
+ liveWorkspaceReady?: boolean | undefined;
48
+ /** Deliberately wake the machine to read content absent from the capture. */
49
+ onWakeWorkspace?: (() => void) | undefined;
39
50
  themeType?: "dark" | "light" | undefined;
40
51
  className?: string | undefined;
41
52
  };
@@ -57,6 +68,10 @@ export function SandboxFiles({
57
68
  requestedPath,
58
69
  requestedPathRequestId,
59
70
  requestedPathReady = true,
71
+ workspaceResting = false,
72
+ workspaceWaking = false,
73
+ liveWorkspaceReady = true,
74
+ onWakeWorkspace,
60
75
  themeType,
61
76
  className,
62
77
  }: SandboxFilesProps) {
@@ -64,6 +79,8 @@ export function SandboxFiles({
64
79
  // View vs Edit for the selected file. Resets to View on every new selection so
65
80
  // opening a file never lands you in a stale dirty editor for a different path.
66
81
  const [editMode, setEditMode] = useState(false);
82
+ const [liveRequestedPath, setLiveRequestedPath] = useState<string | null>(null);
83
+ const [viewReloadRevision, setViewReloadRevision] = useState(0);
67
84
  const pendingRequestRef = useRef<string | number | null>(null);
68
85
  const handledRequestRef = useRef<string | number | null>(null);
69
86
  const requestKey = requestedPath ? (requestedPathRequestId ?? requestedPath) : null;
@@ -111,7 +128,7 @@ export function SandboxFiles({
111
128
  // writable). Nothing is auto-selected — the pane waits for a tree click, so the
112
129
  // Files tab opens as a calm browser, not a diff.
113
130
  const viewPath = selected;
114
- const fileView = useFileView(viewPath, files.readFile);
131
+ const fileView = useFileView(viewPath, files.readFile, viewReloadRevision);
115
132
 
116
133
  // Selecting a (different) file always returns to View — never drop the user into
117
134
  // an editor whose buffer belongs to the previously-selected path. Manual
@@ -124,6 +141,8 @@ export function SandboxFiles({
124
141
  }
125
142
  setSelected(path);
126
143
  setEditMode(false);
144
+ setLiveRequestedPath(null);
145
+ setViewReloadRevision(0);
127
146
  }, []);
128
147
 
129
148
  // A tree file is editable only when it is a real, fully-loaded text file: not
@@ -139,6 +158,12 @@ export function SandboxFiles({
139
158
  !fileView.truncated &&
140
159
  fileView.content !== null;
141
160
  const showEditor = canEdit && editMode;
161
+ const captureFileUnavailable =
162
+ fileView.error instanceof CapturedFileUnavailableError ? fileView.error : null;
163
+ const waitingForSelectedFile =
164
+ liveRequestedPath === viewPath &&
165
+ (!liveWorkspaceReady || files.loading) &&
166
+ captureFileUnavailable !== null;
142
167
 
143
168
  if (!fileSystemAvailable) {
144
169
  return (
@@ -146,12 +171,43 @@ export function SandboxFiles({
146
171
  className={className}
147
172
  icon={<FileWarningIcon className="size-5" aria-hidden />}
148
173
  title="Files unavailable"
174
+ announce="alert"
149
175
  >
150
176
  This sandbox does not expose a file system.
151
177
  </Notice>
152
178
  );
153
179
  }
154
180
 
181
+ if (workspaceResting || workspaceWaking) {
182
+ return (
183
+ <div className={cn("h-full", className)} data-opengeni-workspace-resting>
184
+ <Notice
185
+ icon={
186
+ workspaceWaking ? (
187
+ <LoaderCircleIcon
188
+ className="size-5 animate-spin motion-reduce:animate-none"
189
+ aria-hidden
190
+ />
191
+ ) : (
192
+ <FileCode2Icon className="size-5" aria-hidden />
193
+ )
194
+ }
195
+ title={workspaceWaking ? "Waking workspace" : "Workspace is resting"}
196
+ announce="status"
197
+ >
198
+ <p>
199
+ {workspaceWaking
200
+ ? "Connecting to the live file system…"
201
+ : "No captured revision is available yet. Wake the sandbox to browse its current files."}
202
+ </p>
203
+ {!workspaceWaking && onWakeWorkspace ? (
204
+ <WakeButton onClick={onWakeWorkspace}>Open live workspace</WakeButton>
205
+ ) : null}
206
+ </Notice>
207
+ </div>
208
+ );
209
+ }
210
+
155
211
  return (
156
212
  <div ref={rootRef} className={cn("flex h-full min-h-0 min-w-0 flex-col", className)}>
157
213
  {/* Branch + dirty header (context; NOT the changed-files list). */}
@@ -214,16 +270,54 @@ export function SandboxFiles({
214
270
  path={viewPath}
215
271
  initialContents={fileView.content}
216
272
  themeType={resolvedTheme}
217
- onSave={(contents) => files.writeFile(viewPath, contents)}
273
+ onSave={(contents) =>
274
+ files.writeFile(viewPath, contents, { expectedContent: fileView.content! })
275
+ }
276
+ onOverwrite={(contents) => files.writeFile(viewPath, contents, { force: true })}
277
+ onReload={() => setViewReloadRevision((revision) => revision + 1)}
218
278
  {...(onEditIntent ? { onEditIntent } : {})}
219
279
  className="h-full"
220
280
  />
281
+ ) : waitingForSelectedFile ? (
282
+ <Notice
283
+ icon={
284
+ <LoaderCircleIcon
285
+ className="size-5 animate-spin motion-reduce:animate-none"
286
+ aria-hidden
287
+ />
288
+ }
289
+ title="Waking workspace"
290
+ announce="status"
291
+ >
292
+ Opening {viewPath} when the live file system is ready…
293
+ </Notice>
294
+ ) : captureFileUnavailable ? (
295
+ <Notice icon={<FileCode2Icon className="size-5" aria-hidden />} title="On machine">
296
+ <p>
297
+ {captureFileUnavailable.reason === "too-large"
298
+ ? "This file is larger than the captured preview limit."
299
+ : captureFileUnavailable.reason === "content-missing"
300
+ ? "The captured copy is no longer available."
301
+ : "This file was indexed, but it was not changed in the captured turn."}
302
+ </p>
303
+ {onWakeWorkspace ? (
304
+ <WakeButton
305
+ onClick={() => {
306
+ setLiveRequestedPath(viewPath);
307
+ if (liveWorkspaceReady) void files.refresh();
308
+ else onWakeWorkspace();
309
+ }}
310
+ >
311
+ {liveWorkspaceReady ? "Retry live file" : "Open live file"}
312
+ </WakeButton>
313
+ ) : null}
314
+ </Notice>
221
315
  ) : fileView.error ? (
222
- <Notice>
316
+ <Notice announce="alert">
223
317
  Could not open {viewPath}: {fileView.error.message}
224
318
  </Notice>
225
319
  ) : fileView.loading ? (
226
- <Notice>Loading {viewPath}…</Notice>
320
+ <Notice announce="status">Loading {viewPath}…</Notice>
227
321
  ) : fileView.isBinary ? (
228
322
  <Notice>
229
323
  {viewPath} is a binary file ({fileView.sizeBytes ?? 0} bytes).
@@ -255,7 +349,7 @@ export function SandboxFiles({
255
349
  )}
256
350
  </>
257
351
  ) : (
258
- <Notice>Loading {viewPath}…</Notice>
352
+ <Notice announce="status">Loading {viewPath}…</Notice>
259
353
  )
260
354
  ) : // Nothing selected — the tree shows the whole workspace; pick a file.
261
355
  requestedPath && !requestedPathReady ? (
@@ -267,6 +361,7 @@ export function SandboxFiles({
267
361
  />
268
362
  }
269
363
  title="Waking sandbox"
364
+ announce="status"
270
365
  >
271
366
  Opening {requestedPath} when the live workspace is ready…
272
367
  </Notice>
@@ -282,19 +377,36 @@ export function SandboxFiles({
282
377
  );
283
378
  }
284
379
 
380
+ function WakeButton({ children, onClick }: { children: ReactNode; onClick: () => void }) {
381
+ return (
382
+ <button
383
+ type="button"
384
+ onClick={onClick}
385
+ className="mt-1 inline-flex min-h-11 items-center justify-center rounded-og-md bg-og-accent px-3 py-2 text-og-sm font-medium text-og-on-accent shadow-sm transition-colors hover:bg-og-accent-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent focus-visible:ring-offset-2 focus-visible:ring-offset-og-bg"
386
+ >
387
+ {children}
388
+ </button>
389
+ );
390
+ }
391
+
285
392
  function GitHeader({ git, dirtyCount }: { git: UseSandboxGitResult; dirtyCount: number }) {
286
393
  const dirty = dirtyCount > 0;
287
394
  return (
288
395
  <div className="flex shrink-0 items-center gap-2 border-b border-og-border bg-og-surface-1 px-2 py-1 text-og-sm">
289
396
  <span
397
+ aria-hidden="true"
290
398
  className={cn(
291
399
  "size-2 shrink-0 rounded-full",
292
400
  dirty ? "bg-og-status-running" : "bg-og-status-idle",
293
401
  )}
294
- title={dirty ? `${dirtyCount} changed` : "clean"}
295
402
  />
403
+ <span className="sr-only">
404
+ {dirty ? `Working tree has ${dirtyCount} changed files` : "Working tree clean"}
405
+ </span>
296
406
  <span className="truncate font-og-mono text-og-fg">
297
- {git.branch ?? (git.isRepo ? "(detached)" : "no repo")}
407
+ {git.repoCount > 1
408
+ ? `${git.repoCount} repositories`
409
+ : (git.branch ?? (git.isRepo ? "(detached)" : "no repo"))}
298
410
  </span>
299
411
  {(git.ahead > 0 || git.behind > 0) && (
300
412
  <span className="flex shrink-0 items-center gap-1.5 text-og-xs text-og-fg-subtle">
@@ -303,7 +415,11 @@ function GitHeader({ git, dirtyCount }: { git: UseSandboxGitResult; dirtyCount:
303
415
  </span>
304
416
  )}
305
417
  {dirty && (
306
- <span data-contrast-audited className="ml-auto shrink-0 text-og-xs text-og-fg-subtle">
418
+ <span
419
+ aria-hidden="true"
420
+ data-contrast-audited
421
+ className="ml-auto shrink-0 text-og-xs text-og-fg-subtle"
422
+ >
307
423
  {dirtyCount} changed
308
424
  </span>
309
425
  )}
@@ -328,7 +444,7 @@ function Segmented({
328
444
  type="button"
329
445
  onClick={() => onChange(opt.value)}
330
446
  className={cn(
331
- "min-h-7 rounded-og-xs px-1.5 py-0.5 text-og-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent max-[1023px]:min-h-11 pointer-coarse:min-h-11",
447
+ "min-h-7 rounded-og-xs px-1.5 py-0.5 text-og-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent max-[1023px]:min-h-11 max-[1023px]:min-w-11 pointer-coarse:min-h-11 pointer-coarse:min-w-11",
332
448
  opt.value === value
333
449
  ? "bg-og-accent-soft text-og-fg"
334
450
  : "text-og-fg-subtle hover:text-og-fg",
@@ -361,7 +477,8 @@ type FileViewState = {
361
477
  */
362
478
  function useFileView(
363
479
  path: string | null,
364
- readFile: (path: string) => Promise<FsReadResponse>,
480
+ readFile: UseSandboxFilesResult["readFile"],
481
+ reloadRevision = 0,
365
482
  ): FileViewState {
366
483
  const [state, setState] = useState<FileViewState>({
367
484
  content: null,
@@ -384,6 +501,7 @@ function useFileView(
384
501
  return;
385
502
  }
386
503
  let cancelled = false;
504
+ const abort = new AbortController();
387
505
  setState({
388
506
  content: null,
389
507
  isBinary: false,
@@ -392,7 +510,7 @@ function useFileView(
392
510
  loading: true,
393
511
  error: null,
394
512
  });
395
- void readFile(path)
513
+ void readFile(path, { signal: abort.signal })
396
514
  .then((res) => {
397
515
  if (cancelled) return;
398
516
  const content = res.isBinary
@@ -422,8 +540,9 @@ function useFileView(
422
540
  });
423
541
  return () => {
424
542
  cancelled = true;
543
+ abort.abort();
425
544
  };
426
- }, [path, readFile]);
545
+ }, [path, readFile, reloadRevision]);
427
546
  return state;
428
547
  }
429
548
 
@@ -447,14 +566,19 @@ function Notice({
447
566
  className,
448
567
  icon,
449
568
  title,
569
+ announce,
450
570
  }: {
451
571
  children: ReactNode;
452
572
  className?: string | undefined;
453
573
  icon?: ReactNode | undefined;
454
574
  title?: string | undefined;
575
+ announce?: "status" | "alert" | undefined;
455
576
  }) {
456
577
  return (
457
578
  <div
579
+ role={announce}
580
+ aria-atomic={announce ? "true" : undefined}
581
+ aria-live={announce === "alert" ? "assertive" : announce === "status" ? "polite" : undefined}
458
582
  className={cn(
459
583
  "flex h-full items-center justify-center p-4 text-center text-og-sm text-og-fg-subtle",
460
584
  className,
@@ -221,7 +221,13 @@ export function SandboxTerminal({
221
221
  // status lines INSIDE xterm instead of an overlay — but only when there is no
222
222
  // firehose transcript to show yet (otherwise the projected output is shown).
223
223
  const warm = liveness === "warm" || liveness === "draining";
224
- const booting = activated && !ptyMode && !warm && liveness != null && result.chunks.length === 0;
224
+ const booting =
225
+ activated &&
226
+ !ptyMode &&
227
+ !warm &&
228
+ liveness !== null &&
229
+ liveness !== undefined &&
230
+ result.chunks.length === 0;
225
231
 
226
232
  function handleActivate() {
227
233
  if (!activated) setActivated(true);
@@ -502,7 +508,7 @@ export function SandboxTerminal({
502
508
  writtenRef.current = new Set();
503
509
  wroteFirehoseRef.current = false;
504
510
  }}
505
- className="rounded-og-sm px-1.5 py-0.5 text-og-xs hover:text-og-fg pointer-coarse:min-h-10"
511
+ className="rounded-og-sm px-1.5 py-0.5 text-og-xs hover:text-og-fg pointer-coarse:min-h-11"
506
512
  >
507
513
  Clear
508
514
  </button>