@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.
- package/README.md +1 -1
- package/dist/{chunk-5C7RGAWA.js → chunk-NFYVQWIB.js} +170 -64
- package/dist/chunk-NFYVQWIB.js.map +1 -0
- package/dist/index.d.ts +791 -145
- package/dist/index.js +5447 -2213
- package/dist/index.js.map +1 -1
- package/dist/{machines-BeZ3bD3t.d.ts → machines-CnlMb7E-.d.ts} +5 -5
- package/dist/machines.d.ts +1 -1
- package/dist/machines.js +1 -1
- package/package.json +17 -12
- package/src/approvals.ts +16 -4
- package/src/client.ts +32 -5
- package/src/commands/index.ts +1 -7
- package/src/commands/registry.ts +46 -12
- package/src/components/chat-composer.tsx +135 -39
- package/src/components/code-editor.tsx +28 -36
- package/src/components/command-palette.tsx +26 -5
- package/src/components/desktop-viewer.tsx +29 -16
- package/src/components/diff-view.tsx +27 -189
- package/src/components/enrollment-consent.tsx +28 -10
- package/src/components/enrollment-device-flow.tsx +8 -5
- package/src/components/file-browser.tsx +190 -56
- package/src/components/fleet-tile.tsx +13 -4
- package/src/components/machine-card.tsx +16 -4
- package/src/components/machine-dock-bar.tsx +20 -8
- package/src/components/machine-metrics.tsx +31 -8
- package/src/components/machine-status-pill.tsx +26 -5
- package/src/components/machines-dashboard.tsx +8 -2
- package/src/components/markdown.tsx +39 -9
- package/src/components/message-timeline.tsx +633 -109
- package/src/components/pierre-diff.tsx +83 -15
- package/src/components/pierre-file.tsx +10 -9
- package/src/components/sandbox-files.tsx +110 -227
- package/src/components/sandbox-terminal.tsx +256 -88
- package/src/components/sandbox-workspace.tsx +823 -0
- package/src/components/session-status.tsx +28 -2
- package/src/components/workbench-changes.tsx +541 -0
- package/src/components/workspace-dock.tsx +85 -22
- package/src/hooks/internal.ts +5 -2
- package/src/hooks/use-available-models.ts +7 -2
- package/src/hooks/use-billing-usage.ts +4 -1
- package/src/hooks/use-codex-accounts.ts +74 -44
- package/src/hooks/use-composer.ts +57 -36
- package/src/hooks/use-environments.ts +92 -47
- package/src/hooks/use-file-attachments.ts +101 -55
- package/src/hooks/use-goal.ts +52 -16
- package/src/hooks/use-machine-chip.ts +134 -0
- package/src/hooks/use-machines.ts +34 -16
- package/src/hooks/use-packs.ts +24 -19
- package/src/hooks/use-relay-frame-stream.ts +5 -2
- package/src/hooks/use-rigs.ts +335 -0
- package/src/hooks/use-sandbox-files.ts +213 -39
- package/src/hooks/use-sandbox-git.ts +188 -11
- package/src/hooks/use-sandbox-terminal.ts +18 -14
- package/src/hooks/use-scheduled-tasks.ts +10 -2
- package/src/hooks/use-session-capabilities.ts +77 -20
- package/src/hooks/use-session-control.ts +49 -21
- package/src/hooks/use-session-events.ts +48 -20
- package/src/hooks/use-session-lineage.ts +119 -0
- package/src/hooks/use-session.ts +43 -28
- package/src/hooks/use-slash-commands.ts +6 -4
- package/src/hooks/use-turn-queue.ts +74 -147
- package/src/hooks/use-windowed-sections.ts +204 -0
- package/src/hooks/use-workspace-capture.ts +233 -0
- package/src/hooks/use-workspace-edit.ts +231 -0
- package/src/hooks/use-workspace-sessions.ts +48 -5
- package/src/hooks/use-workspaces.ts +18 -15
- package/src/index.ts +128 -23
- package/src/lib/format.ts +44 -6
- package/src/lib/xterm-renderer.ts +65 -0
- package/src/lib/xterm-theme.ts +224 -18
- package/src/machines.ts +13 -3
- package/src/provider.tsx +3 -1
- package/src/timeline/activity-rail.tsx +213 -54
- package/src/timeline/disclosure-context.tsx +12 -2
- package/src/timeline/index.ts +21 -2
- package/src/timeline/parsers.ts +44 -10
- package/src/timeline/projection.ts +435 -84
- package/src/timeline/shared.tsx +111 -24
- package/src/timeline/tool-diff.tsx +24 -20
- package/src/timeline/tool-renderers.tsx +98 -21
- package/src/timeline/turn-summary.tsx +76 -23
- package/src/timeline/types.ts +96 -12
- package/styles/tokens.css +2 -2
- package/dist/chunk-5C7RGAWA.js.map +0 -1
|
@@ -1,14 +1,41 @@
|
|
|
1
1
|
import type { ClientModel, SessionStatus } from "@opengeni/sdk";
|
|
2
|
-
import {
|
|
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 {
|
|
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 {
|
|
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([
|
|
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({
|
|
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 =
|
|
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
|
|
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
|
-
|
|
378
|
-
|
|
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={
|
|
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
|
|
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={() =>
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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.
|
|
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 ?
|
|
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(
|
|
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({
|
|
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 =
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
|
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
|
|
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(
|
|
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" ?
|
|
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-
|
|
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-
|
|
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({
|
|
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
|
|
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({
|
|
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
|
|
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
|
|
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 ?
|
|
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">
|
|
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 {
|
|
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
|
-
}, [
|
|
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(
|
|
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) ??
|
|
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(
|
|
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
|
|
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
|
: ""}
|