@opengeni/react 0.11.0 → 0.13.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 (85) hide show
  1. package/README.md +1 -1
  2. package/dist/{chunk-5C7RGAWA.js → chunk-NFYVQWIB.js} +170 -64
  3. package/dist/chunk-NFYVQWIB.js.map +1 -0
  4. package/dist/index.d.ts +791 -145
  5. package/dist/index.js +5447 -2213
  6. package/dist/index.js.map +1 -1
  7. package/dist/{machines-BeZ3bD3t.d.ts → machines-CnlMb7E-.d.ts} +5 -5
  8. package/dist/machines.d.ts +1 -1
  9. package/dist/machines.js +1 -1
  10. package/package.json +17 -12
  11. package/src/approvals.ts +16 -4
  12. package/src/client.ts +32 -5
  13. package/src/commands/index.ts +1 -7
  14. package/src/commands/registry.ts +46 -12
  15. package/src/components/chat-composer.tsx +135 -39
  16. package/src/components/code-editor.tsx +28 -36
  17. package/src/components/command-palette.tsx +26 -5
  18. package/src/components/desktop-viewer.tsx +29 -16
  19. package/src/components/diff-view.tsx +27 -189
  20. package/src/components/enrollment-consent.tsx +28 -10
  21. package/src/components/enrollment-device-flow.tsx +8 -5
  22. package/src/components/file-browser.tsx +190 -56
  23. package/src/components/fleet-tile.tsx +13 -4
  24. package/src/components/machine-card.tsx +16 -4
  25. package/src/components/machine-dock-bar.tsx +20 -8
  26. package/src/components/machine-metrics.tsx +31 -8
  27. package/src/components/machine-status-pill.tsx +26 -5
  28. package/src/components/machines-dashboard.tsx +8 -2
  29. package/src/components/markdown.tsx +39 -9
  30. package/src/components/message-timeline.tsx +633 -109
  31. package/src/components/pierre-diff.tsx +83 -15
  32. package/src/components/pierre-file.tsx +10 -9
  33. package/src/components/sandbox-files.tsx +110 -227
  34. package/src/components/sandbox-terminal.tsx +256 -88
  35. package/src/components/sandbox-workspace.tsx +823 -0
  36. package/src/components/session-status.tsx +28 -2
  37. package/src/components/workbench-changes.tsx +541 -0
  38. package/src/components/workspace-dock.tsx +85 -22
  39. package/src/hooks/internal.ts +5 -2
  40. package/src/hooks/use-available-models.ts +7 -2
  41. package/src/hooks/use-billing-usage.ts +4 -1
  42. package/src/hooks/use-codex-accounts.ts +74 -44
  43. package/src/hooks/use-composer.ts +57 -36
  44. package/src/hooks/use-environments.ts +92 -47
  45. package/src/hooks/use-file-attachments.ts +101 -55
  46. package/src/hooks/use-goal.ts +52 -16
  47. package/src/hooks/use-machine-chip.ts +134 -0
  48. package/src/hooks/use-machines.ts +34 -16
  49. package/src/hooks/use-packs.ts +24 -19
  50. package/src/hooks/use-relay-frame-stream.ts +5 -2
  51. package/src/hooks/use-rigs.ts +335 -0
  52. package/src/hooks/use-sandbox-files.ts +213 -39
  53. package/src/hooks/use-sandbox-git.ts +188 -11
  54. package/src/hooks/use-sandbox-terminal.ts +18 -14
  55. package/src/hooks/use-scheduled-tasks.ts +10 -2
  56. package/src/hooks/use-session-capabilities.ts +77 -20
  57. package/src/hooks/use-session-control.ts +49 -21
  58. package/src/hooks/use-session-events.ts +48 -20
  59. package/src/hooks/use-session-lineage.ts +119 -0
  60. package/src/hooks/use-session.ts +43 -28
  61. package/src/hooks/use-slash-commands.ts +6 -4
  62. package/src/hooks/use-turn-queue.ts +74 -147
  63. package/src/hooks/use-windowed-sections.ts +204 -0
  64. package/src/hooks/use-workspace-capture.ts +233 -0
  65. package/src/hooks/use-workspace-edit.ts +231 -0
  66. package/src/hooks/use-workspace-sessions.ts +48 -5
  67. package/src/hooks/use-workspaces.ts +18 -15
  68. package/src/index.ts +128 -23
  69. package/src/lib/format.ts +44 -6
  70. package/src/lib/xterm-renderer.ts +65 -0
  71. package/src/lib/xterm-theme.ts +224 -18
  72. package/src/machines.ts +13 -3
  73. package/src/provider.tsx +3 -1
  74. package/src/timeline/activity-rail.tsx +213 -54
  75. package/src/timeline/disclosure-context.tsx +12 -2
  76. package/src/timeline/index.ts +21 -2
  77. package/src/timeline/parsers.ts +44 -10
  78. package/src/timeline/projection.ts +435 -84
  79. package/src/timeline/shared.tsx +111 -24
  80. package/src/timeline/tool-diff.tsx +24 -20
  81. package/src/timeline/tool-renderers.tsx +98 -21
  82. package/src/timeline/turn-summary.tsx +76 -23
  83. package/src/timeline/types.ts +96 -12
  84. package/styles/tokens.css +2 -2
  85. package/dist/chunk-5C7RGAWA.js.map +0 -1
@@ -1,14 +1,41 @@
1
1
  import type { ClientModel, SessionStatus } from "@opengeni/sdk";
2
- import { ArrowUpIcon, FileIcon, ImageIcon, LoaderCircleIcon, PaperclipIcon, RotateCwIcon, SquareIcon, XIcon } from "lucide-react";
2
+ import {
3
+ ArrowUpIcon,
4
+ FileIcon,
5
+ ImageIcon,
6
+ LoaderCircleIcon,
7
+ PaperclipIcon,
8
+ PlayIcon,
9
+ RotateCwIcon,
10
+ SquareIcon,
11
+ XIcon,
12
+ } from "lucide-react";
3
13
  import { AnimatePresence, motion } from "motion/react";
4
- import { useCallback, useEffect, useId, useMemo, useRef, useState, type ChangeEvent, type ClipboardEvent, type DragEvent, type KeyboardEvent, type ReactNode, type RefObject } from "react";
14
+ import {
15
+ useCallback,
16
+ useEffect,
17
+ useId,
18
+ useMemo,
19
+ useRef,
20
+ useState,
21
+ type ChangeEvent,
22
+ type ClipboardEvent,
23
+ type DragEvent,
24
+ type KeyboardEvent,
25
+ type ReactNode,
26
+ type RefObject,
27
+ } from "react";
5
28
  import { argHint } from "../commands/registry";
6
29
  import type { Notice, SlashCommand } from "../commands/types";
7
30
  import type { ComposerState } from "../hooks/use-composer";
8
- import { shouldSubmitOnKey } from "../hooks/use-composer";
31
+ import { composerModeForKey, shouldSubmitOnKey } from "../hooks/use-composer";
9
32
  import type { UseFileAttachmentsResult } from "../hooks/use-file-attachments";
10
33
  import { defaultCommands } from "../commands/registry";
11
- import { useSlashCommands, type ConfirmState, type SlashCommandContext } from "../hooks/use-slash-commands";
34
+ import {
35
+ useSlashCommands,
36
+ type ConfirmState,
37
+ type SlashCommandContext,
38
+ } from "../hooks/use-slash-commands";
12
39
  import { cn } from "../lib/cn";
13
40
  import { formatBytes } from "../lib/format";
14
41
  import { CommandPalette } from "./command-palette";
@@ -68,7 +95,13 @@ export type ChatComposerProps = {
68
95
  onClearView?: (() => void) | undefined;
69
96
  };
70
97
 
71
- const ACTIVE_STATUSES: ReadonlySet<SessionStatus> = new Set(["queued", "running"]);
98
+ const ACTIVE_STATUSES: ReadonlySet<SessionStatus> = new Set([
99
+ "queued",
100
+ "running",
101
+ "requires_action",
102
+ "recovering",
103
+ "waiting_capacity",
104
+ ]);
72
105
 
73
106
  /**
74
107
  * The chat composer — the only human-to-agent input surface. Plain chat in,
@@ -255,7 +288,11 @@ export function ChatComposer({
255
288
  if (commandDraftBlocked) {
256
289
  // Dismissed palette + a "/command" draft: don't send it as chat. Nudge
257
290
  // the operator to re-open the palette (any edit re-opens it) or clear it.
258
- setNotice({ tone: "error", message: "That's a slash command — press Enter in the command list to run it, or edit the line to send a message." });
291
+ setNotice({
292
+ tone: "error",
293
+ message:
294
+ "That's a slash command — press Enter in the command list to run it, or edit the line to send a message.",
295
+ });
259
296
  return;
260
297
  }
261
298
  if (blockedByUpload) {
@@ -263,7 +300,7 @@ export function ChatComposer({
263
300
  // depart without them. The send button is disabled in the same state.
264
301
  return;
265
302
  }
266
- void composer.send();
303
+ void composer.send(undefined, composerModeForKey(event));
267
304
  }
268
305
  };
269
306
 
@@ -300,12 +337,23 @@ export function ChatComposer({
300
337
  [commands, commandContext],
301
338
  );
302
339
 
303
- const activeNotice = notice ?? (composer.error ? { tone: "error" as const, message: composer.error.message || "Sending failed — your draft is still here. Try again." } : null);
340
+ const activeNotice =
341
+ notice ??
342
+ (composer.error
343
+ ? {
344
+ tone: "error" as const,
345
+ message:
346
+ composer.error.message || "Sending failed — your draft is still here. Try again.",
347
+ }
348
+ : null);
304
349
 
305
350
  return (
306
351
  // Respect the iOS home-indicator inset so the sticky composer never sits
307
352
  // under it (0 on non-notch devices and desktop, so it's inert there).
308
- <div className={cn("og-root", className)} style={{ paddingBottom: "env(safe-area-inset-bottom)" }}>
353
+ <div
354
+ className={cn("og-root", className)}
355
+ style={{ paddingBottom: "env(safe-area-inset-bottom)" }}
356
+ >
309
357
  <div className="relative">
310
358
  {paletteEnabled ? (
311
359
  <CommandPalette
@@ -374,10 +422,18 @@ export function ChatComposer({
374
422
  disabled={disabled}
375
423
  autoFocus={autoFocus}
376
424
  aria-label="Message the agent"
377
- role={paletteEnabled && palette.open ? "combobox" : undefined}
378
- aria-expanded={paletteEnabled ? palette.open : undefined}
425
+ aria-keyshortcuts="Enter Meta+Enter Control+Enter Shift+Enter"
426
+ // A multiline textarea cannot legally take the combobox role or
427
+ // aria-expanded. It remains a native textbox and advertises the
428
+ // popup as list autocomplete; controls/active-descendant exist
429
+ // only while the suggestion list itself exists.
430
+ aria-autocomplete={paletteEnabled ? "list" : undefined}
379
431
  aria-controls={paletteEnabled && palette.open ? listboxId : undefined}
380
- aria-activedescendant={paletteEnabled && palette.open ? `${listboxId}-option-${palette.highlight}` : undefined}
432
+ aria-activedescendant={
433
+ paletteEnabled && palette.open
434
+ ? `${listboxId}-option-${palette.highlight}`
435
+ : undefined
436
+ }
381
437
  className={cn(
382
438
  // Font size steps up to 16px below `md` so iOS never zooms the
383
439
  // viewport on focus; desktop keeps the 15px og-md rhythm.
@@ -426,7 +482,7 @@ export function ChatComposer({
426
482
  className={cn(
427
483
  "inline-flex size-8 items-center justify-center rounded-og-md",
428
484
  "text-og-fg-muted transition-colors duration-150 hover:bg-og-surface-2 hover:text-og-fg",
429
- "disabled:cursor-not-allowed disabled:opacity-50",
485
+ "disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:size-11",
430
486
  )}
431
487
  >
432
488
  <PaperclipIcon className="size-4" />
@@ -445,12 +501,12 @@ export function ChatComposer({
445
501
  </span>
446
502
  ) : (
447
503
  <span className="min-w-0 flex-1 px-1.5 text-og-xs text-og-fg-subtle max-sm:hidden">
448
- {hint ?? "Enter to send · Shift+Enter for a new line · / for commands"}
504
+ {hint ?? "Enter to queue · Cmd/Ctrl+Enter to steer · Shift+Enter for a new line"}
449
505
  </span>
450
506
  )}
451
507
  <span className="ml-auto flex shrink-0 items-center gap-1.5">
452
508
  <AnimatePresence initial={false}>
453
- {active ? (
509
+ {active || status === "paused" ? (
454
510
  <motion.button
455
511
  key="stop"
456
512
  type="button"
@@ -458,10 +514,16 @@ export function ChatComposer({
458
514
  animate={{ opacity: 1, scale: 1 }}
459
515
  exit={{ opacity: 0, scale: 0.8 }}
460
516
  transition={{ duration: 0.15, ease: "easeOut" }}
461
- onClick={() => void composer.interrupt()}
462
- disabled={composer.interrupting}
463
- aria-label="Stop the current turn"
464
- title="Stop the current turn"
517
+ onClick={() =>
518
+ void (status === "paused" ? composer.resume() : composer.pause())
519
+ }
520
+ disabled={composer.pausing || composer.resuming}
521
+ aria-label={status === "paused" ? "Resume the session" : "Pause the session"}
522
+ title={
523
+ status === "paused"
524
+ ? "Resume the current inference before queued prompts"
525
+ : "Pause the session; queued prompts are preserved"
526
+ }
465
527
  className={cn(
466
528
  "inline-flex size-8 items-center justify-center rounded-og-md border border-og-border pointer-coarse:size-11",
467
529
  "bg-og-surface-2 text-og-fg-muted transition-colors duration-150",
@@ -469,8 +531,10 @@ export function ChatComposer({
469
531
  "disabled:opacity-50",
470
532
  )}
471
533
  >
472
- {composer.interrupting ? (
534
+ {composer.pausing || composer.resuming ? (
473
535
  <LoaderCircleIcon className="size-3.5 animate-og-spin" />
536
+ ) : status === "paused" ? (
537
+ <PlayIcon className="size-3.5 fill-current" />
474
538
  ) : (
475
539
  <SquareIcon className="size-3 fill-current" />
476
540
  )}
@@ -487,6 +551,7 @@ export function ChatComposer({
487
551
  }}
488
552
  disabled={!canSend || disabled === true || commandDraftBlocked}
489
553
  aria-label="Send message"
554
+ title="Queue message (Enter); steer with Cmd/Ctrl+Enter"
490
555
  className={cn(
491
556
  "inline-flex size-8 items-center justify-center rounded-og-md pointer-coarse:size-11",
492
557
  "bg-og-accent text-og-accent-fg shadow-og-sm",
@@ -495,7 +560,11 @@ export function ChatComposer({
495
560
  "disabled:cursor-not-allowed disabled:bg-og-surface-3 disabled:text-og-fg-subtle disabled:shadow-none",
496
561
  )}
497
562
  >
498
- {composer.sending ? <LoaderCircleIcon className="size-4 animate-og-spin" /> : <ArrowUpIcon className="size-4" />}
563
+ {composer.sending ? (
564
+ <LoaderCircleIcon className="size-4 animate-og-spin" />
565
+ ) : (
566
+ <ArrowUpIcon className="size-4" />
567
+ )}
499
568
  </button>
500
569
  </span>
501
570
  </div>
@@ -503,9 +572,7 @@ export function ChatComposer({
503
572
  </div>
504
573
  </div>
505
574
  <AnimatePresence>
506
- {helpOpen ? (
507
- <HelpPanel commands={helpCommands} onClose={() => setHelpOpen(false)} />
508
- ) : null}
575
+ {helpOpen ? <HelpPanel commands={helpCommands} onClose={() => setHelpOpen(false)} /> : null}
509
576
  </AnimatePresence>
510
577
  <AnimatePresence>
511
578
  {activeNotice ? (
@@ -521,7 +588,10 @@ export function ChatComposer({
521
588
  onAnimationComplete={() => {
522
589
  if (activeNotice.tone === "ok") {
523
590
  // Auto-dismiss success notices after a beat.
524
- window.setTimeout(() => setNotice((current) => (current === activeNotice ? null : current)), 2400);
591
+ window.setTimeout(
592
+ () => setNotice((current) => (current === activeNotice ? null : current)),
593
+ 2400,
594
+ );
525
595
  }
526
596
  }}
527
597
  >
@@ -608,7 +678,11 @@ function ConfirmBar({
608
678
  * in-progress upload does), and removing it clears the way. Styled with the
609
679
  * package's og-* tokens so it themes in any consumer.
610
680
  */
611
- function AttachmentChips({ attachments, onRemove, onRetry }: {
681
+ function AttachmentChips({
682
+ attachments,
683
+ onRemove,
684
+ onRetry,
685
+ }: {
612
686
  attachments: UseFileAttachmentsResult["attachments"];
613
687
  onRemove: (id: string) => void;
614
688
  onRetry?: ((id: string) => void) | undefined;
@@ -617,21 +691,28 @@ function AttachmentChips({ attachments, onRemove, onRetry }: {
617
691
  <div className="flex flex-wrap gap-2 border-b border-og-border px-3 py-2">
618
692
  {attachments.map((attachment) => {
619
693
  const failed = attachment.status === "failed";
620
- const statusText = attachment.status === "uploading"
621
- ? "Uploading"
622
- : failed
623
- ? attachment.error || "Upload failed"
624
- : formatBytes(attachment.sizeBytes);
694
+ const statusText =
695
+ attachment.status === "uploading"
696
+ ? "Uploading"
697
+ : failed
698
+ ? attachment.error || "Upload failed"
699
+ : formatBytes(attachment.sizeBytes);
625
700
  return (
626
701
  <div
627
702
  key={attachment.id}
628
703
  className={cn(
629
704
  "flex min-w-0 max-w-[240px] items-center gap-2 rounded-og-md border px-2 py-1.5 text-og-sm",
630
- failed ? "border-og-status-failed/40 bg-og-status-failed/10" : "border-og-border bg-og-surface-2",
705
+ failed
706
+ ? "border-og-status-failed/40 bg-og-status-failed/10"
707
+ : "border-og-border bg-og-surface-2",
631
708
  )}
632
709
  >
633
710
  {attachment.previewUrl ? (
634
- <img src={attachment.previewUrl} alt="" className="size-8 shrink-0 rounded object-cover" />
711
+ <img
712
+ src={attachment.previewUrl}
713
+ alt=""
714
+ className="size-8 shrink-0 rounded object-cover"
715
+ />
635
716
  ) : attachment.contentType.startsWith("image/") ? (
636
717
  <ImageIcon className="size-4 shrink-0 text-og-fg-muted" />
637
718
  ) : (
@@ -640,18 +721,23 @@ function AttachmentChips({ attachments, onRemove, onRetry }: {
640
721
  <div className="min-w-0 flex-1">
641
722
  <div className="truncate font-medium text-og-fg">{attachment.name}</div>
642
723
  <div
643
- className={cn("truncate text-og-xs", failed ? "text-og-status-failed" : "text-og-fg-subtle")}
724
+ className={cn(
725
+ "truncate text-og-xs",
726
+ failed ? "text-og-status-failed" : "text-og-fg-subtle",
727
+ )}
644
728
  title={failed ? statusText : undefined}
645
729
  >
646
730
  {statusText}
647
731
  </div>
648
732
  </div>
649
- {attachment.status === "uploading" ? <LoaderCircleIcon className="size-3.5 shrink-0 animate-og-spin" /> : null}
733
+ {attachment.status === "uploading" ? (
734
+ <LoaderCircleIcon className="size-3.5 shrink-0 animate-og-spin" />
735
+ ) : null}
650
736
  {failed && onRetry ? (
651
737
  <button
652
738
  type="button"
653
739
  onClick={() => onRetry(attachment.id)}
654
- className="shrink-0 rounded-og-xs p-1 text-og-fg-muted hover:bg-og-surface-1 hover:text-og-fg pointer-coarse:size-8"
740
+ className="shrink-0 rounded-og-xs p-1 text-og-fg-muted hover:bg-og-surface-1 hover:text-og-fg pointer-coarse:size-10"
655
741
  aria-label={`Retry ${attachment.name}`}
656
742
  title="Retry upload"
657
743
  >
@@ -661,7 +747,7 @@ function AttachmentChips({ attachments, onRemove, onRetry }: {
661
747
  <button
662
748
  type="button"
663
749
  onClick={() => onRemove(attachment.id)}
664
- className="shrink-0 rounded-og-xs p-1 text-og-fg-muted hover:bg-og-surface-1 hover:text-og-fg pointer-coarse:size-8"
750
+ className="shrink-0 rounded-og-xs p-1 text-og-fg-muted hover:bg-og-surface-1 hover:text-og-fg pointer-coarse:size-10"
665
751
  aria-label={`Remove ${attachment.name}`}
666
752
  >
667
753
  <XIcon className="size-3.5" />
@@ -674,7 +760,13 @@ function AttachmentChips({ attachments, onRemove, onRetry }: {
674
760
  }
675
761
 
676
762
  /** The in-composer /help panel, rendered entirely from the registry. */
677
- function HelpPanel({ commands, onClose }: { commands: readonly SlashCommand[]; onClose: () => void }) {
763
+ function HelpPanel({
764
+ commands,
765
+ onClose,
766
+ }: {
767
+ commands: readonly SlashCommand[];
768
+ onClose: () => void;
769
+ }) {
678
770
  return (
679
771
  <motion.div
680
772
  initial={{ opacity: 0, height: 0 }}
@@ -684,7 +776,11 @@ function HelpPanel({ commands, onClose }: { commands: readonly SlashCommand[]; o
684
776
  >
685
777
  <div className="flex items-center justify-between border-b border-og-border px-3 py-1.5">
686
778
  <span className="text-og-sm font-medium text-og-fg">Commands</span>
687
- <button type="button" onClick={onClose} className="text-og-xs text-og-fg-subtle hover:text-og-fg">
779
+ <button
780
+ type="button"
781
+ onClick={onClose}
782
+ className="text-og-xs text-og-fg-subtle hover:text-og-fg"
783
+ >
688
784
  Close
689
785
  </button>
690
786
  </div>
@@ -106,6 +106,10 @@ export type CodeEditorProps = {
106
106
  initialContents: string;
107
107
  /** Persist the current buffer. Resolves when the write lands; rejects to surface an error. */
108
108
  onSave: (contents: string) => Promise<unknown>;
109
+ /** Fired once when the buffer FIRST diverges from its baseline (the first real
110
+ * keystroke) — the wake-on-edit intent. The host warms the box on this so the
111
+ * eventual save lands fast; merely opening the file for reading never fires it. */
112
+ onEditIntent?: (() => void) | undefined;
109
113
  /** Read-only mode (e.g. a truncated/too-large file shown for reference only). */
110
114
  readOnly?: boolean | undefined;
111
115
  themeType?: "dark" | "light" | undefined;
@@ -132,6 +136,7 @@ export function CodeEditor({
132
136
  path,
133
137
  initialContents,
134
138
  onSave,
139
+ onEditIntent,
135
140
  readOnly = false,
136
141
  themeType = "dark",
137
142
  loading,
@@ -155,18 +160,35 @@ export function CodeEditor({
155
160
  // Reload the buffer when the file identity changes. Guard on initialContents
156
161
  // too so an external refresh of the SAME path (e.g. fs.changed re-read) reseeds
157
162
  // a clean buffer — but only when the user hasn't got unsaved edits in flight.
163
+ // Fire the wake-on-edit intent at most once per opened file — reset on re-seed.
164
+ const editIntentFiredRef = useRef(false);
158
165
  const lastSeed = useRef<{ path: string; contents: string }>({ path, contents: initialContents });
159
166
  useEffect(() => {
160
167
  const seedChanged =
161
168
  lastSeed.current.path !== path || lastSeed.current.contents !== initialContents;
162
169
  if (!seedChanged) return;
163
170
  lastSeed.current = { path, contents: initialContents };
171
+ editIntentFiredRef.current = false;
164
172
  setValue(initialContents);
165
173
  setBaseline(initialContents);
166
174
  setSaveError(null);
167
175
  setSavedTick(false);
168
176
  }, [path, initialContents]);
169
177
 
178
+ // Record a buffer change: the FIRST divergence from the baseline is the edit
179
+ // intent that warms the box (idempotent — latched per opened file).
180
+ const noteEdit = useCallback(
181
+ (next: string) => {
182
+ setValue(next);
183
+ setSavedTick(false);
184
+ if (!editIntentFiredRef.current && next !== baseline) {
185
+ editIntentFiredRef.current = true;
186
+ onEditIntent?.();
187
+ }
188
+ },
189
+ [baseline, onEditIntent],
190
+ );
191
+
170
192
  // Resolve the lazy bundle (editor + keymap/Prec helpers + language grammar)
171
193
  // once, re-resolving the grammar when the file's language class changes.
172
194
  const langKey = useMemo(() => languageForPath(path), [path]);
@@ -257,16 +279,7 @@ export function CodeEditor({
257
279
  if (failed) {
258
280
  return (
259
281
  <div className={cn("flex h-full min-h-0 flex-col", className)}>
260
- {fallback ?? (
261
- <PlainTextarea
262
- value={value}
263
- readOnly={readOnly}
264
- onChange={(next) => {
265
- setValue(next);
266
- setSavedTick(false);
267
- }}
268
- />
269
- )}
282
+ {fallback ?? <PlainTextarea value={value} readOnly={readOnly} onChange={noteEdit} />}
270
283
  </div>
271
284
  );
272
285
  }
@@ -282,11 +295,7 @@ export function CodeEditor({
282
295
  <span
283
296
  className={cn(
284
297
  "size-1.5 shrink-0 rounded-full transition-colors",
285
- readOnly
286
- ? "bg-transparent"
287
- : dirty
288
- ? "bg-og-status-running"
289
- : "bg-og-status-idle",
298
+ readOnly ? "bg-transparent" : dirty ? "bg-og-status-running" : "bg-og-status-idle",
290
299
  )}
291
300
  title={readOnly ? "read-only" : dirty ? "unsaved changes" : "saved"}
292
301
  />
@@ -303,15 +312,9 @@ export function CodeEditor({
303
312
  {saveError.message}
304
313
  </span>
305
314
  )}
306
- {!saveError && savedTick && (
307
- <span className="text-og-xs text-og-status-idle">
308
- Saved
309
- </span>
310
- )}
315
+ {!saveError && savedTick && <span className="text-og-xs text-og-status-idle">Saved</span>}
311
316
  {readOnly ? (
312
- <span className="text-og-xs uppercase tracking-wide text-og-fg-subtle">
313
- Read-only
314
- </span>
317
+ <span className="text-og-xs uppercase tracking-wide text-og-fg-subtle">Read-only</span>
315
318
  ) : (
316
319
  <button
317
320
  type="button"
@@ -348,14 +351,7 @@ export function CodeEditor({
348
351
  extensions={cmExtensions}
349
352
  height="100%"
350
353
  className="og-cm-editor min-h-full text-og-sm"
351
- onChange={
352
- readOnly
353
- ? undefined
354
- : (next: string) => {
355
- setValue(next);
356
- setSavedTick(false);
357
- }
358
- }
354
+ onChange={readOnly ? undefined : noteEdit}
359
355
  />
360
356
  </Suspense>
361
357
  </div>
@@ -390,9 +386,5 @@ function PlainTextarea({
390
386
  }
391
387
 
392
388
  function EditorSkeleton() {
393
- return (
394
- <div className="p-3 text-og-sm text-og-fg-subtle">
395
- Loading editor…
396
- </div>
397
- );
389
+ return <div className="p-3 text-og-sm text-og-fg-subtle">Loading editor…</div>;
398
390
  }
@@ -23,7 +23,15 @@ export type CommandPaletteProps = {
23
23
  * the opengeni#46 design tokens. Full keyboard nav lives in useSlashCommands;
24
24
  * this component is presentational + aria.
25
25
  */
26
- export function CommandPalette({ open, items, highlight, onHighlight, onRun, argHintText, listboxId }: CommandPaletteProps) {
26
+ export function CommandPalette({
27
+ open,
28
+ items,
29
+ highlight,
30
+ onHighlight,
31
+ onRun,
32
+ argHintText,
33
+ listboxId,
34
+ }: CommandPaletteProps) {
27
35
  return (
28
36
  <AnimatePresence>
29
37
  {open ? (
@@ -61,14 +69,25 @@ export function CommandPalette({ open, items, highlight, onHighlight, onRun, arg
61
69
  className={cn(
62
70
  "mx-1 flex cursor-pointer items-center gap-2 rounded-og-md px-2.5 py-1.5",
63
71
  "transition-colors duration-100",
64
- selected ? "bg-og-accent/15 text-og-fg" : "text-og-fg-muted hover:bg-og-surface-3",
72
+ selected
73
+ ? "bg-og-accent/15 text-og-fg"
74
+ : "text-og-fg-muted hover:bg-og-surface-3",
65
75
  )}
66
76
  >
67
77
  <span className="flex min-w-0 items-baseline gap-1.5">
68
- <span className={cn("font-mono text-[13px]", selected ? "text-og-accent" : "text-og-fg")}>
78
+ <span
79
+ className={cn(
80
+ "font-mono text-[13px]",
81
+ selected ? "text-og-accent" : "text-og-fg",
82
+ )}
83
+ >
69
84
  /{command.name}
70
85
  </span>
71
- {hint ? <span className="truncate font-mono text-[11px] text-og-fg-subtle">{hint}</span> : null}
86
+ {hint ? (
87
+ <span className="truncate font-mono text-[11px] text-og-fg-subtle">
88
+ {hint}
89
+ </span>
90
+ ) : null}
72
91
  </span>
73
92
  <span className="ml-auto flex items-center gap-1.5">
74
93
  {command.danger ? (
@@ -76,7 +95,9 @@ export function CommandPalette({ open, items, highlight, onHighlight, onRun, arg
76
95
  danger
77
96
  </span>
78
97
  ) : null}
79
- <span className="truncate text-[12px] text-og-fg-subtle max-sm:hidden">{command.description}</span>
98
+ <span className="truncate text-[12px] text-og-fg-subtle max-sm:hidden">
99
+ {command.description}
100
+ </span>
80
101
  </span>
81
102
  </li>
82
103
  );
@@ -1,4 +1,8 @@
1
- import type { CapabilityUnavailableReason, DesktopRfbFactory, DesktopStreamCapability } from "@opengeni/sdk";
1
+ import type {
2
+ CapabilityUnavailableReason,
3
+ DesktopRfbFactory,
4
+ DesktopStreamCapability,
5
+ } from "@opengeni/sdk";
2
6
  import { LoaderCircleIcon, MonitorIcon, MousePointerClickIcon, WifiOffIcon } from "lucide-react";
3
7
  import { type ReactNode, useEffect, useRef, useState } from "react";
4
8
  import { cn } from "../lib/cn";
@@ -214,8 +218,7 @@ export function DesktopViewer({
214
218
  // Do NOT open a socket while the viewer-cap (429) notice is showing — the slot
215
219
  // is already exhausted, so connecting would only burn a doomed attempt (and in
216
220
  // tests leak an unhandled ws error from the never-resolving tunnel URL).
217
- const connectCapability =
218
- !transportNull && !needsAck && !viewerCapReached ? capability : null;
221
+ const connectCapability = !transportNull && !needsAck && !viewerCapReached ? capability : null;
219
222
  const stream = useDesktopStream({
220
223
  capability: connectCapability,
221
224
  containerRef,
@@ -309,7 +312,14 @@ export function DesktopViewer({
309
312
  if (!hasLiveUrl || connected || stream.error || connectTimeoutMs <= 0) return;
310
313
  const timer = setTimeout(() => setConnectTimedOut(true), connectTimeoutMs);
311
314
  return () => clearTimeout(timer);
312
- }, [hasLiveUrl, connected, connectTimeoutMs, connectCapability?.url, stream.error, reconnectNonce]);
315
+ }, [
316
+ hasLiveUrl,
317
+ connected,
318
+ connectTimeoutMs,
319
+ connectCapability?.url,
320
+ stream.error,
321
+ reconnectNonce,
322
+ ]);
313
323
 
314
324
  const reconnect = () => {
315
325
  setConnectTimedOut(false);
@@ -338,11 +348,15 @@ export function DesktopViewer({
338
348
  case "viewer_cap":
339
349
  overlay =
340
350
  renderViewerCap?.() ??
341
- defaultNotice("Too many viewers", "This session has reached its live-viewer limit. Try again shortly.");
351
+ defaultNotice(
352
+ "Too many viewers",
353
+ "This session has reached its live-viewer limit. Try again shortly.",
354
+ );
342
355
  break;
343
356
  case "unavailable":
344
357
  overlay =
345
- renderUnavailable?.(reason) ?? defaultNotice("Desktop unavailable", unavailableCopy(reason));
358
+ renderUnavailable?.(reason) ??
359
+ defaultNotice("Desktop unavailable", unavailableCopy(reason));
346
360
  break;
347
361
  case "consent":
348
362
  overlay = renderConsentGate ? (
@@ -362,7 +376,11 @@ export function DesktopViewer({
362
376
  );
363
377
  break;
364
378
  case "error":
365
- overlay = defaultNotice("Desktop disconnected", stream.error?.message ?? "The stream dropped.", reconnect);
379
+ overlay = defaultNotice(
380
+ "Desktop disconnected",
381
+ stream.error?.message ?? "The stream dropped.",
382
+ reconnect,
383
+ );
366
384
  break;
367
385
  case "connecting":
368
386
  case "connected":
@@ -511,9 +529,7 @@ function TakeControlCallToAction({
511
529
  <MousePointerClickIcon className="size-5" strokeWidth={2} />
512
530
  </span>
513
531
  <span className="flex flex-col items-start leading-tight">
514
- <span className="text-og-base font-semibold text-og-fg">
515
- Take control
516
- </span>
532
+ <span className="text-og-base font-semibold text-og-fg">Take control</span>
517
533
  <span className="text-og-xs text-og-fg-subtle">
518
534
  {disabled && disabledReason ? disabledReason : "Drive the mouse & keyboard"}
519
535
  </span>
@@ -594,10 +610,7 @@ function unavailableCopy(reason: CapabilityUnavailableReason | null): string {
594
610
  function WarmingNotice({ onRetry }: { onRetry?: (() => void) | undefined }) {
595
611
  return (
596
612
  <div className="flex max-w-sm flex-col items-center gap-3 rounded-og-lg border border-og-border bg-og-bg/90 p-5 text-center text-og-base text-og-fg backdrop-blur-sm">
597
- <LoaderCircleIcon
598
- className="size-7 animate-og-spin text-og-accent"
599
- strokeWidth={1.5}
600
- />
613
+ <LoaderCircleIcon className="size-7 animate-og-spin text-og-accent" strokeWidth={1.5} />
601
614
  <div className="space-y-1">
602
615
  <div className="font-medium">Warming the sandbox…</div>
603
616
  <p className="text-og-sm text-og-fg-subtle">
@@ -622,8 +635,8 @@ function DefaultConsentGate({ shared, onAccept }: { shared: boolean; onAccept: (
622
635
  <div className="max-w-sm rounded-og-lg border border-og-border bg-og-bg p-4 text-center text-og-base text-og-fg">
623
636
  <div className="mb-1 font-medium">Watch the live desktop?</div>
624
637
  <p className="mb-3 text-og-sm text-og-fg-subtle">
625
- The desktop pixel stream is <strong>un-redacted</strong> — it can show secrets the agent prints
626
- on screen.
638
+ The desktop pixel stream is <strong>un-redacted</strong> — it can show secrets the agent
639
+ prints on screen.
627
640
  {shared
628
641
  ? " This sandbox is shared: you'll also see other sessions' agents on the same screen."
629
642
  : ""}