@opengeni/react 0.12.0 → 0.15.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 +20 -14
- package/dist/chunk-TOJR776I.js +2280 -0
- package/dist/chunk-TOJR776I.js.map +1 -0
- package/dist/index.d.ts +1040 -363
- package/dist/index.js +10938 -6281
- package/dist/index.js.map +1 -1
- package/dist/{machines-BeZ3bD3t.d.ts → machines-BpdwuQcD.d.ts} +134 -14
- package/dist/machines.d.ts +1 -1
- package/dist/machines.js +23 -1
- package/package.json +20 -12
- package/src/approvals.ts +16 -4
- package/src/client.ts +41 -5
- package/src/commands/index.ts +1 -7
- package/src/commands/registry.ts +46 -12
- package/src/components/chat-composer.tsx +417 -69
- 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 +93 -15
- package/src/components/machine-dock-bar.tsx +20 -8
- package/src/components/machine-health-pill.tsx +68 -0
- package/src/components/machine-metrics.tsx +28 -19
- package/src/components/machine-status-pill.tsx +26 -5
- package/src/components/machines/health.ts +146 -0
- package/src/components/machines/machine-detail.tsx +220 -0
- package/src/components/machines/metric-history-chart.tsx +298 -0
- package/src/components/machines/metric-sparkline.tsx +76 -0
- package/src/components/machines/series.ts +113 -0
- package/src/components/machines-dashboard.tsx +21 -3
- 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/queue-surface.tsx +578 -0
- package/src/components/sandbox-files.tsx +193 -225
- package/src/components/sandbox-terminal.tsx +256 -88
- package/src/components/sandbox-workspace.tsx +957 -0
- package/src/components/session-status.tsx +22 -2
- package/src/components/workbench-changes.tsx +585 -0
- package/src/components/workspace-dock.tsx +213 -59
- 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 +414 -63
- 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 +50 -23
- package/src/hooks/use-session-events.ts +50 -21
- package/src/hooks/use-session-lineage.ts +128 -0
- package/src/hooks/use-session.ts +53 -30
- package/src/hooks/use-slash-commands.ts +6 -4
- package/src/hooks/use-turn-queue.ts +202 -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 +133 -23
- package/src/lib/format.ts +3 -3
- package/src/lib/xterm-renderer.ts +65 -0
- package/src/lib/xterm-theme.ts +224 -18
- package/src/machines.ts +29 -3
- package/src/provider.tsx +195 -6
- 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 +87 -16
- package/src/timeline/projection.ts +412 -87
- 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/index.css +22 -0
- package/styles/tokens.css +2 -2
- package/dist/chunk-5TIXRCSI.js +0 -1291
- package/dist/chunk-5TIXRCSI.js.map +0 -1
|
@@ -1,23 +1,62 @@
|
|
|
1
|
-
import type { ClientModel,
|
|
2
|
-
import {
|
|
1
|
+
import type { ClientModel, EffectiveSessionControl } from "@opengeni/sdk";
|
|
2
|
+
import {
|
|
3
|
+
ArrowUpIcon,
|
|
4
|
+
ChevronDownIcon,
|
|
5
|
+
FileIcon,
|
|
6
|
+
ImageIcon,
|
|
7
|
+
LoaderCircleIcon,
|
|
8
|
+
PaperclipIcon,
|
|
9
|
+
PauseIcon,
|
|
10
|
+
PlayIcon,
|
|
11
|
+
RotateCwIcon,
|
|
12
|
+
XIcon,
|
|
13
|
+
} from "lucide-react";
|
|
3
14
|
import { AnimatePresence, motion } from "motion/react";
|
|
4
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
useCallback,
|
|
17
|
+
useEffect,
|
|
18
|
+
useId,
|
|
19
|
+
useMemo,
|
|
20
|
+
useRef,
|
|
21
|
+
useState,
|
|
22
|
+
type ChangeEvent,
|
|
23
|
+
type ClipboardEvent,
|
|
24
|
+
type DragEvent,
|
|
25
|
+
type KeyboardEvent,
|
|
26
|
+
type ReactNode,
|
|
27
|
+
type RefObject,
|
|
28
|
+
} from "react";
|
|
5
29
|
import { argHint } from "../commands/registry";
|
|
6
30
|
import type { Notice, SlashCommand } from "../commands/types";
|
|
7
31
|
import type { ComposerState } from "../hooks/use-composer";
|
|
8
|
-
import { shouldSubmitOnKey } from "../hooks/use-composer";
|
|
32
|
+
import { shouldSteerOnKey, shouldSubmitOnKey } from "../hooks/use-composer";
|
|
9
33
|
import type { UseFileAttachmentsResult } from "../hooks/use-file-attachments";
|
|
10
34
|
import { defaultCommands } from "../commands/registry";
|
|
11
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
useSlashCommands,
|
|
37
|
+
type ConfirmState,
|
|
38
|
+
type SlashCommandContext,
|
|
39
|
+
} from "../hooks/use-slash-commands";
|
|
12
40
|
import { cn } from "../lib/cn";
|
|
13
|
-
import { formatBytes } from "../lib/format";
|
|
41
|
+
import { formatBytes, formatRelativeTime } from "../lib/format";
|
|
14
42
|
import { CommandPalette } from "./command-palette";
|
|
15
43
|
import { ModelPicker } from "./model-picker";
|
|
16
44
|
|
|
17
45
|
export type ChatComposerProps = {
|
|
18
46
|
composer: ComposerState;
|
|
19
|
-
/**
|
|
20
|
-
|
|
47
|
+
/** Canonical workstream control, separate from lifecycle status. */
|
|
48
|
+
effectiveControl?: EffectiveSessionControl | null | undefined;
|
|
49
|
+
/** Waiting prompts already ahead of a normal Send. */
|
|
50
|
+
queuedAheadCount?: number | undefined;
|
|
51
|
+
/** Whether broader Workspace Resume is authorized for this viewer. */
|
|
52
|
+
canControlWorkspace?: boolean | undefined;
|
|
53
|
+
/** Optional host routes used to navigate from effective Pause blockers. */
|
|
54
|
+
controlLinks?:
|
|
55
|
+
| {
|
|
56
|
+
workspaceHref?: string | undefined;
|
|
57
|
+
sessionHref?: ((sessionId: string) => string) | undefined;
|
|
58
|
+
}
|
|
59
|
+
| undefined;
|
|
21
60
|
placeholder?: string | undefined;
|
|
22
61
|
disabled?: boolean | undefined;
|
|
23
62
|
autoFocus?: boolean | undefined;
|
|
@@ -68,7 +107,7 @@ export type ChatComposerProps = {
|
|
|
68
107
|
onClearView?: (() => void) | undefined;
|
|
69
108
|
};
|
|
70
109
|
|
|
71
|
-
const
|
|
110
|
+
export const OPEN_WORKSTREAM_CONTROL_EVENT = "opengeni:open-workstream-control";
|
|
72
111
|
|
|
73
112
|
/**
|
|
74
113
|
* The chat composer — the only human-to-agent input surface. Plain chat in,
|
|
@@ -83,7 +122,10 @@ const ACTIVE_STATUSES: ReadonlySet<SessionStatus> = new Set(["queued", "running"
|
|
|
83
122
|
*/
|
|
84
123
|
export function ChatComposer({
|
|
85
124
|
composer,
|
|
86
|
-
|
|
125
|
+
effectiveControl,
|
|
126
|
+
queuedAheadCount = 0,
|
|
127
|
+
canControlWorkspace = false,
|
|
128
|
+
controlLinks,
|
|
87
129
|
placeholder,
|
|
88
130
|
disabled,
|
|
89
131
|
autoFocus,
|
|
@@ -102,7 +144,24 @@ export function ChatComposer({
|
|
|
102
144
|
}: ChatComposerProps) {
|
|
103
145
|
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
|
104
146
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
105
|
-
const
|
|
147
|
+
const paused = effectiveControl?.state === "paused";
|
|
148
|
+
const [controlDetailsOpen, setControlDetailsOpen] = useState(false);
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
const openControl = () => {
|
|
151
|
+
textareaRef.current?.scrollIntoView({ block: "end", behavior: "smooth" });
|
|
152
|
+
if (paused) {
|
|
153
|
+
setControlDetailsOpen(true);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
window.requestAnimationFrame(() => {
|
|
157
|
+
document
|
|
158
|
+
.querySelector<HTMLButtonElement>('button[aria-label="Pause this workstream"]')
|
|
159
|
+
?.focus();
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
document.addEventListener(OPEN_WORKSTREAM_CONTROL_EVENT, openControl);
|
|
163
|
+
return () => document.removeEventListener(OPEN_WORKSTREAM_CONTROL_EVENT, openControl);
|
|
164
|
+
}, [paused]);
|
|
106
165
|
|
|
107
166
|
// Block sends while attachments are still uploading so a message never
|
|
108
167
|
// departs without the files the user attached to it. This gates BOTH the
|
|
@@ -255,7 +314,11 @@ export function ChatComposer({
|
|
|
255
314
|
if (commandDraftBlocked) {
|
|
256
315
|
// Dismissed palette + a "/command" draft: don't send it as chat. Nudge
|
|
257
316
|
// the operator to re-open the palette (any edit re-opens it) or clear it.
|
|
258
|
-
setNotice({
|
|
317
|
+
setNotice({
|
|
318
|
+
tone: "error",
|
|
319
|
+
message:
|
|
320
|
+
"That's a slash command — press Enter in the command list to run it, or edit the line to send a message.",
|
|
321
|
+
});
|
|
259
322
|
return;
|
|
260
323
|
}
|
|
261
324
|
if (blockedByUpload) {
|
|
@@ -263,7 +326,7 @@ export function ChatComposer({
|
|
|
263
326
|
// depart without them. The send button is disabled in the same state.
|
|
264
327
|
return;
|
|
265
328
|
}
|
|
266
|
-
void composer.send();
|
|
329
|
+
void (shouldSteerOnKey(event) ? composer.steer() : composer.send());
|
|
267
330
|
}
|
|
268
331
|
};
|
|
269
332
|
|
|
@@ -300,12 +363,24 @@ export function ChatComposer({
|
|
|
300
363
|
[commands, commandContext],
|
|
301
364
|
);
|
|
302
365
|
|
|
303
|
-
const activeNotice =
|
|
366
|
+
const activeNotice =
|
|
367
|
+
notice ??
|
|
368
|
+
(composer.error
|
|
369
|
+
? {
|
|
370
|
+
tone: "error" as const,
|
|
371
|
+
message: /control changed|paused while/i.test(composer.error.message)
|
|
372
|
+
? "This workstream was paused while you were sending. Nothing was sent, and your draft is still here."
|
|
373
|
+
: composer.error.message || "Sending failed — your draft is still here. Try again.",
|
|
374
|
+
}
|
|
375
|
+
: null);
|
|
304
376
|
|
|
305
377
|
return (
|
|
306
378
|
// Respect the iOS home-indicator inset so the sticky composer never sits
|
|
307
379
|
// under it (0 on non-notch devices and desktop, so it's inert there).
|
|
308
|
-
<div
|
|
380
|
+
<div
|
|
381
|
+
className={cn("og-root", className)}
|
|
382
|
+
style={{ paddingBottom: "env(safe-area-inset-bottom)" }}
|
|
383
|
+
>
|
|
309
384
|
<div className="relative">
|
|
310
385
|
{paletteEnabled ? (
|
|
311
386
|
<CommandPalette
|
|
@@ -355,6 +430,26 @@ export function ChatComposer({
|
|
|
355
430
|
</span>
|
|
356
431
|
</div>
|
|
357
432
|
) : null}
|
|
433
|
+
{paused && effectiveControl ? (
|
|
434
|
+
<WorkstreamPausedStrip
|
|
435
|
+
control={effectiveControl}
|
|
436
|
+
queuedAheadCount={queuedAheadCount}
|
|
437
|
+
open={controlDetailsOpen}
|
|
438
|
+
busy={composer.resuming}
|
|
439
|
+
sending={composer.sending}
|
|
440
|
+
canControlWorkspace={canControlWorkspace}
|
|
441
|
+
controlLinks={controlLinks}
|
|
442
|
+
onOpenChange={setControlDetailsOpen}
|
|
443
|
+
onResume={() => void composer.resume()}
|
|
444
|
+
onResumeOption={(option) => void composer.resumeScope(option)}
|
|
445
|
+
/>
|
|
446
|
+
) : null}
|
|
447
|
+
{composer.restoredResources.length > 0 ? (
|
|
448
|
+
<RestoredResourceChips
|
|
449
|
+
resources={composer.restoredResources}
|
|
450
|
+
onRemove={composer.removeRestoredResource}
|
|
451
|
+
/>
|
|
452
|
+
) : null}
|
|
358
453
|
{attachments && attachments.attachments.length > 0 ? (
|
|
359
454
|
<AttachmentChips
|
|
360
455
|
attachments={attachments.attachments}
|
|
@@ -370,14 +465,26 @@ export function ChatComposer({
|
|
|
370
465
|
onChange={(event) => composer.setValue(event.target.value)}
|
|
371
466
|
onKeyDown={onKeyDown}
|
|
372
467
|
onPaste={handlePaste}
|
|
373
|
-
placeholder={
|
|
468
|
+
placeholder={
|
|
469
|
+
paused && disabled !== true
|
|
470
|
+
? "Sending will resume this workstream…"
|
|
471
|
+
: (placeholder ?? "Message the agent…")
|
|
472
|
+
}
|
|
374
473
|
disabled={disabled}
|
|
375
474
|
autoFocus={autoFocus}
|
|
376
475
|
aria-label="Message the agent"
|
|
377
|
-
|
|
378
|
-
|
|
476
|
+
aria-keyshortcuts="Enter Meta+Enter Control+Enter Shift+Enter"
|
|
477
|
+
// A multiline textarea cannot legally take the combobox role or
|
|
478
|
+
// aria-expanded. It remains a native textbox and advertises the
|
|
479
|
+
// popup as list autocomplete; controls/active-descendant exist
|
|
480
|
+
// only while the suggestion list itself exists.
|
|
481
|
+
aria-autocomplete={paletteEnabled ? "list" : undefined}
|
|
379
482
|
aria-controls={paletteEnabled && palette.open ? listboxId : undefined}
|
|
380
|
-
aria-activedescendant={
|
|
483
|
+
aria-activedescendant={
|
|
484
|
+
paletteEnabled && palette.open
|
|
485
|
+
? `${listboxId}-option-${palette.highlight}`
|
|
486
|
+
: undefined
|
|
487
|
+
}
|
|
381
488
|
className={cn(
|
|
382
489
|
// Font size steps up to 16px below `md` so iOS never zooms the
|
|
383
490
|
// viewport on focus; desktop keeps the 15px og-md rhythm.
|
|
@@ -426,7 +533,7 @@ export function ChatComposer({
|
|
|
426
533
|
className={cn(
|
|
427
534
|
"inline-flex size-8 items-center justify-center rounded-og-md",
|
|
428
535
|
"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",
|
|
536
|
+
"disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:size-11",
|
|
430
537
|
)}
|
|
431
538
|
>
|
|
432
539
|
<PaperclipIcon className="size-4" />
|
|
@@ -445,38 +552,31 @@ export function ChatComposer({
|
|
|
445
552
|
</span>
|
|
446
553
|
) : (
|
|
447
554
|
<span className="min-w-0 flex-1 px-1.5 text-og-xs text-og-fg-subtle max-sm:hidden">
|
|
448
|
-
{hint ?? "Enter to
|
|
555
|
+
{hint ?? "Enter to queue · Cmd/Ctrl+Enter to steer · Shift+Enter for a new line"}
|
|
449
556
|
</span>
|
|
450
557
|
)}
|
|
451
558
|
<span className="ml-auto flex shrink-0 items-center gap-1.5">
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
<LoaderCircleIcon className="size-3.5 animate-og-spin" />
|
|
474
|
-
) : (
|
|
475
|
-
<SquareIcon className="size-3 fill-current" />
|
|
476
|
-
)}
|
|
477
|
-
</motion.button>
|
|
478
|
-
) : null}
|
|
479
|
-
</AnimatePresence>
|
|
559
|
+
{effectiveControl && !paused ? (
|
|
560
|
+
<button
|
|
561
|
+
type="button"
|
|
562
|
+
onClick={() => void composer.pause()}
|
|
563
|
+
disabled={composer.pausing || composer.resuming}
|
|
564
|
+
aria-label="Pause this workstream"
|
|
565
|
+
title="Pause this workstream; queued prompts and approvals are preserved"
|
|
566
|
+
className={cn(
|
|
567
|
+
"inline-flex size-8 items-center justify-center rounded-og-md border border-og-border pointer-coarse:size-11",
|
|
568
|
+
"bg-og-surface-2 text-og-fg-muted transition-colors duration-150",
|
|
569
|
+
"hover:border-og-status-waiting/50 hover:text-og-status-waiting",
|
|
570
|
+
"disabled:opacity-50",
|
|
571
|
+
)}
|
|
572
|
+
>
|
|
573
|
+
{composer.pausing || composer.resuming ? (
|
|
574
|
+
<LoaderCircleIcon className="size-3.5 animate-og-spin" />
|
|
575
|
+
) : (
|
|
576
|
+
<PauseIcon className="size-3.5 fill-current" />
|
|
577
|
+
)}
|
|
578
|
+
</button>
|
|
579
|
+
) : null}
|
|
480
580
|
<button
|
|
481
581
|
type="button"
|
|
482
582
|
onClick={() => {
|
|
@@ -486,7 +586,12 @@ export function ChatComposer({
|
|
|
486
586
|
void composer.send();
|
|
487
587
|
}}
|
|
488
588
|
disabled={!canSend || disabled === true || commandDraftBlocked}
|
|
489
|
-
aria-label="Send message"
|
|
589
|
+
aria-label={paused ? "Send and resume" : "Send message"}
|
|
590
|
+
title={
|
|
591
|
+
paused
|
|
592
|
+
? "Send & resume (Enter); Steer & resume with Cmd/Ctrl+Enter"
|
|
593
|
+
: "Queue message (Enter); steer with Cmd/Ctrl+Enter"
|
|
594
|
+
}
|
|
490
595
|
className={cn(
|
|
491
596
|
"inline-flex size-8 items-center justify-center rounded-og-md pointer-coarse:size-11",
|
|
492
597
|
"bg-og-accent text-og-accent-fg shadow-og-sm",
|
|
@@ -495,7 +600,11 @@ export function ChatComposer({
|
|
|
495
600
|
"disabled:cursor-not-allowed disabled:bg-og-surface-3 disabled:text-og-fg-subtle disabled:shadow-none",
|
|
496
601
|
)}
|
|
497
602
|
>
|
|
498
|
-
{composer.sending ?
|
|
603
|
+
{composer.sending ? (
|
|
604
|
+
<LoaderCircleIcon className="size-4 animate-og-spin" />
|
|
605
|
+
) : (
|
|
606
|
+
<ArrowUpIcon className="size-4" />
|
|
607
|
+
)}
|
|
499
608
|
</button>
|
|
500
609
|
</span>
|
|
501
610
|
</div>
|
|
@@ -503,9 +612,7 @@ export function ChatComposer({
|
|
|
503
612
|
</div>
|
|
504
613
|
</div>
|
|
505
614
|
<AnimatePresence>
|
|
506
|
-
{helpOpen ? (
|
|
507
|
-
<HelpPanel commands={helpCommands} onClose={() => setHelpOpen(false)} />
|
|
508
|
-
) : null}
|
|
615
|
+
{helpOpen ? <HelpPanel commands={helpCommands} onClose={() => setHelpOpen(false)} /> : null}
|
|
509
616
|
</AnimatePresence>
|
|
510
617
|
<AnimatePresence>
|
|
511
618
|
{activeNotice ? (
|
|
@@ -521,7 +628,10 @@ export function ChatComposer({
|
|
|
521
628
|
onAnimationComplete={() => {
|
|
522
629
|
if (activeNotice.tone === "ok") {
|
|
523
630
|
// Auto-dismiss success notices after a beat.
|
|
524
|
-
window.setTimeout(
|
|
631
|
+
window.setTimeout(
|
|
632
|
+
() => setNotice((current) => (current === activeNotice ? null : current)),
|
|
633
|
+
2400,
|
|
634
|
+
);
|
|
525
635
|
}
|
|
526
636
|
}}
|
|
527
637
|
>
|
|
@@ -529,6 +639,218 @@ export function ChatComposer({
|
|
|
529
639
|
</motion.p>
|
|
530
640
|
) : null}
|
|
531
641
|
</AnimatePresence>
|
|
642
|
+
{composer.draftConflict ? (
|
|
643
|
+
<div
|
|
644
|
+
className="mt-1.5 flex flex-wrap items-center gap-2 px-1 text-og-xs text-og-status-failed"
|
|
645
|
+
role="alert"
|
|
646
|
+
>
|
|
647
|
+
<span className="min-w-0 flex-1">
|
|
648
|
+
This draft changed in another tab. Your local draft is still here.
|
|
649
|
+
</span>
|
|
650
|
+
<button
|
|
651
|
+
type="button"
|
|
652
|
+
className="underline underline-offset-2"
|
|
653
|
+
onClick={() => void composer.resolveDraftConflict("use_remote")}
|
|
654
|
+
>
|
|
655
|
+
Use other draft
|
|
656
|
+
</button>
|
|
657
|
+
<button
|
|
658
|
+
type="button"
|
|
659
|
+
className="font-medium underline underline-offset-2"
|
|
660
|
+
onClick={() => void composer.resolveDraftConflict("keep_mine")}
|
|
661
|
+
>
|
|
662
|
+
Keep mine
|
|
663
|
+
</button>
|
|
664
|
+
</div>
|
|
665
|
+
) : composer.draftSaving ? (
|
|
666
|
+
<p className="px-1 pt-1 text-og-xs text-og-fg-subtle" role="status">
|
|
667
|
+
Saving draft…
|
|
668
|
+
</p>
|
|
669
|
+
) : null}
|
|
670
|
+
</div>
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function WorkstreamPausedStrip({
|
|
675
|
+
control,
|
|
676
|
+
queuedAheadCount,
|
|
677
|
+
open,
|
|
678
|
+
busy,
|
|
679
|
+
sending,
|
|
680
|
+
canControlWorkspace,
|
|
681
|
+
controlLinks,
|
|
682
|
+
onOpenChange,
|
|
683
|
+
onResume,
|
|
684
|
+
onResumeOption,
|
|
685
|
+
}: {
|
|
686
|
+
control: EffectiveSessionControl;
|
|
687
|
+
queuedAheadCount: number;
|
|
688
|
+
open: boolean;
|
|
689
|
+
busy: boolean;
|
|
690
|
+
sending: boolean;
|
|
691
|
+
canControlWorkspace: boolean;
|
|
692
|
+
controlLinks: ChatComposerProps["controlLinks"];
|
|
693
|
+
onOpenChange: (open: boolean) => void;
|
|
694
|
+
onResume: () => void;
|
|
695
|
+
onResumeOption: (option: EffectiveSessionControl["resumeOptions"][number]) => void;
|
|
696
|
+
}) {
|
|
697
|
+
const blocker = control.primaryBlocker;
|
|
698
|
+
const cause =
|
|
699
|
+
blocker?.kind === "workspace"
|
|
700
|
+
? "Workspace paused"
|
|
701
|
+
: control.directState === "paused"
|
|
702
|
+
? "Paused here"
|
|
703
|
+
: `Paused by ${blocker?.displayName ?? "parent"}`;
|
|
704
|
+
const primary = control.resumeOptions.find((option) => option.scope === "selected");
|
|
705
|
+
const broaderOptions = control.resumeOptions.filter(
|
|
706
|
+
(option) =>
|
|
707
|
+
option !== primary &&
|
|
708
|
+
option.scope !== "selected" &&
|
|
709
|
+
(option.scope !== "workspace" || canControlWorkspace),
|
|
710
|
+
);
|
|
711
|
+
return (
|
|
712
|
+
<div className="border-b border-og-status-waiting/25 bg-og-status-waiting/[0.07]">
|
|
713
|
+
<div className="flex min-w-0 items-center gap-2 px-3 py-2">
|
|
714
|
+
<PauseIcon className="size-3.5 shrink-0 text-og-status-waiting" />
|
|
715
|
+
<button
|
|
716
|
+
type="button"
|
|
717
|
+
className="min-w-0 flex-1 text-left"
|
|
718
|
+
onClick={() => onOpenChange(!open)}
|
|
719
|
+
aria-expanded={open}
|
|
720
|
+
>
|
|
721
|
+
<span className="block truncate text-og-xs font-medium text-og-fg">{cause}</span>
|
|
722
|
+
<span className="block truncate text-[11px] text-og-fg-muted">
|
|
723
|
+
{queuedAheadCount > 0
|
|
724
|
+
? `Send & resume queues behind ${queuedAheadCount} waiting prompt${queuedAheadCount === 1 ? "" : "s"}.`
|
|
725
|
+
: sending
|
|
726
|
+
? "Resuming and sending…"
|
|
727
|
+
: "Your next message resumes this workstream automatically."}
|
|
728
|
+
</span>
|
|
729
|
+
</button>
|
|
730
|
+
<button
|
|
731
|
+
type="button"
|
|
732
|
+
aria-label="Resume this workstream"
|
|
733
|
+
className="inline-flex min-h-8 shrink-0 items-center gap-1.5 rounded-og-md border border-og-status-waiting/35 bg-og-surface-1 px-2.5 text-og-xs font-medium text-og-fg hover:bg-og-surface-2 pointer-coarse:min-h-11"
|
|
734
|
+
disabled={busy}
|
|
735
|
+
onClick={onResume}
|
|
736
|
+
>
|
|
737
|
+
{busy ? (
|
|
738
|
+
<LoaderCircleIcon className="size-3.5 animate-og-spin" />
|
|
739
|
+
) : (
|
|
740
|
+
<PlayIcon className="size-3.5 fill-current" />
|
|
741
|
+
)}
|
|
742
|
+
<span className="hidden min-[400px]:inline">Resume this workstream</span>
|
|
743
|
+
<span className="min-[400px]:hidden">Resume</span>
|
|
744
|
+
</button>
|
|
745
|
+
<button
|
|
746
|
+
type="button"
|
|
747
|
+
className="inline-flex size-8 shrink-0 items-center justify-center rounded-og-md text-og-fg-muted hover:bg-og-surface-2 pointer-coarse:size-11"
|
|
748
|
+
onClick={() => onOpenChange(!open)}
|
|
749
|
+
aria-label={open ? "Hide pause details" : "Show pause details"}
|
|
750
|
+
>
|
|
751
|
+
<ChevronDownIcon className={cn("size-3.5 transition-transform", open && "rotate-180")} />
|
|
752
|
+
</button>
|
|
753
|
+
</div>
|
|
754
|
+
{open ? (
|
|
755
|
+
<div className="border-t border-og-status-waiting/20 px-3 pb-3 pt-2">
|
|
756
|
+
<ul className="grid gap-2" aria-label="Reasons this workstream is paused">
|
|
757
|
+
{control.blockers.map((entry, index) => (
|
|
758
|
+
<li
|
|
759
|
+
key={`${entry.kind}-${entry.sessionId ?? "workspace"}-${entry.revision}`}
|
|
760
|
+
className="text-og-xs"
|
|
761
|
+
>
|
|
762
|
+
<span className="font-medium text-og-fg">
|
|
763
|
+
{index === 0 ? "Paused by " : "Also paused by "}
|
|
764
|
+
</span>
|
|
765
|
+
{blockerHref(entry, controlLinks) ? (
|
|
766
|
+
<a
|
|
767
|
+
href={blockerHref(entry, controlLinks)}
|
|
768
|
+
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"
|
|
769
|
+
>
|
|
770
|
+
{entry.displayName}
|
|
771
|
+
</a>
|
|
772
|
+
) : (
|
|
773
|
+
<span className="font-medium text-og-fg">{entry.displayName}</span>
|
|
774
|
+
)}
|
|
775
|
+
{entry.reason ? <span className="text-og-fg-muted"> · {entry.reason}</span> : null}
|
|
776
|
+
{entry.actor ? <span className="text-og-fg-subtle"> · {entry.actor}</span> : null}
|
|
777
|
+
{entry.changedAt ? (
|
|
778
|
+
<span className="text-og-fg-subtle">
|
|
779
|
+
{" "}
|
|
780
|
+
· {formatRelativeTime(entry.changedAt)}
|
|
781
|
+
</span>
|
|
782
|
+
) : null}
|
|
783
|
+
</li>
|
|
784
|
+
))}
|
|
785
|
+
</ul>
|
|
786
|
+
{broaderOptions.length > 0 ? (
|
|
787
|
+
<div className="mt-2 flex flex-wrap gap-1.5">
|
|
788
|
+
{broaderOptions.map((option) => (
|
|
789
|
+
<button
|
|
790
|
+
key={`${option.scope}-${option.targetId ?? "selected"}`}
|
|
791
|
+
type="button"
|
|
792
|
+
disabled={busy}
|
|
793
|
+
title={option.impactCopy}
|
|
794
|
+
onClick={() => onResumeOption(option)}
|
|
795
|
+
className="rounded-og-md border border-og-border bg-og-surface-1 px-2 py-1 text-og-xs text-og-fg-muted hover:bg-og-surface-2 hover:text-og-fg pointer-coarse:min-h-10"
|
|
796
|
+
>
|
|
797
|
+
<span className="block font-medium">
|
|
798
|
+
{option.scope === "workspace" ? "Resume workspace" : "Resume from this session"}
|
|
799
|
+
</span>
|
|
800
|
+
<span className="block text-[10px] text-og-fg-subtle">
|
|
801
|
+
{option.selectedStateAfter === "active"
|
|
802
|
+
? "This session will be able to run"
|
|
803
|
+
: `Still paused by ${option.remainingPrimaryBlocker?.displayName ?? "a narrower pause"}`}
|
|
804
|
+
</span>
|
|
805
|
+
</button>
|
|
806
|
+
))}
|
|
807
|
+
</div>
|
|
808
|
+
) : null}
|
|
809
|
+
</div>
|
|
810
|
+
) : null}
|
|
811
|
+
</div>
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function blockerHref(
|
|
816
|
+
blocker: EffectiveSessionControl["blockers"][number],
|
|
817
|
+
links: ChatComposerProps["controlLinks"],
|
|
818
|
+
): string | undefined {
|
|
819
|
+
if (blocker.kind === "workspace") return links?.workspaceHref;
|
|
820
|
+
return blocker.sessionId ? links?.sessionHref?.(blocker.sessionId) : undefined;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function RestoredResourceChips({
|
|
824
|
+
resources,
|
|
825
|
+
onRemove,
|
|
826
|
+
}: {
|
|
827
|
+
resources: ComposerState["restoredResources"];
|
|
828
|
+
onRemove: (index: number) => void;
|
|
829
|
+
}) {
|
|
830
|
+
return (
|
|
831
|
+
<div
|
|
832
|
+
className="flex flex-wrap gap-1.5 border-b border-og-border px-3 py-2"
|
|
833
|
+
aria-label="Restored prompt resources"
|
|
834
|
+
>
|
|
835
|
+
{resources.map((resource, index) => (
|
|
836
|
+
<span
|
|
837
|
+
key={`${resource.kind}-${resource.kind === "file" ? resource.fileId : resource.uri}`}
|
|
838
|
+
className="inline-flex min-w-0 max-w-full items-center gap-1.5 rounded-og-md border border-og-border bg-og-surface-2 px-2 py-1 text-og-xs text-og-fg-muted"
|
|
839
|
+
>
|
|
840
|
+
<FileIcon className="size-3 shrink-0" />
|
|
841
|
+
<span className="truncate">
|
|
842
|
+
{resource.kind === "file" ? `File ${resource.fileId.slice(0, 8)}` : resource.uri}
|
|
843
|
+
</span>
|
|
844
|
+
<button
|
|
845
|
+
type="button"
|
|
846
|
+
className="shrink-0 hover:text-og-fg"
|
|
847
|
+
onClick={() => onRemove(index)}
|
|
848
|
+
aria-label={`Remove restored resource ${index + 1}`}
|
|
849
|
+
>
|
|
850
|
+
<XIcon className="size-3" />
|
|
851
|
+
</button>
|
|
852
|
+
</span>
|
|
853
|
+
))}
|
|
532
854
|
</div>
|
|
533
855
|
);
|
|
534
856
|
}
|
|
@@ -608,7 +930,11 @@ function ConfirmBar({
|
|
|
608
930
|
* in-progress upload does), and removing it clears the way. Styled with the
|
|
609
931
|
* package's og-* tokens so it themes in any consumer.
|
|
610
932
|
*/
|
|
611
|
-
function AttachmentChips({
|
|
933
|
+
function AttachmentChips({
|
|
934
|
+
attachments,
|
|
935
|
+
onRemove,
|
|
936
|
+
onRetry,
|
|
937
|
+
}: {
|
|
612
938
|
attachments: UseFileAttachmentsResult["attachments"];
|
|
613
939
|
onRemove: (id: string) => void;
|
|
614
940
|
onRetry?: ((id: string) => void) | undefined;
|
|
@@ -617,21 +943,28 @@ function AttachmentChips({ attachments, onRemove, onRetry }: {
|
|
|
617
943
|
<div className="flex flex-wrap gap-2 border-b border-og-border px-3 py-2">
|
|
618
944
|
{attachments.map((attachment) => {
|
|
619
945
|
const failed = attachment.status === "failed";
|
|
620
|
-
const statusText =
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
946
|
+
const statusText =
|
|
947
|
+
attachment.status === "uploading"
|
|
948
|
+
? "Uploading"
|
|
949
|
+
: failed
|
|
950
|
+
? attachment.error || "Upload failed"
|
|
951
|
+
: formatBytes(attachment.sizeBytes);
|
|
625
952
|
return (
|
|
626
953
|
<div
|
|
627
954
|
key={attachment.id}
|
|
628
955
|
className={cn(
|
|
629
956
|
"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
|
|
957
|
+
failed
|
|
958
|
+
? "border-og-status-failed/40 bg-og-status-failed/10"
|
|
959
|
+
: "border-og-border bg-og-surface-2",
|
|
631
960
|
)}
|
|
632
961
|
>
|
|
633
962
|
{attachment.previewUrl ? (
|
|
634
|
-
<img
|
|
963
|
+
<img
|
|
964
|
+
src={attachment.previewUrl}
|
|
965
|
+
alt=""
|
|
966
|
+
className="size-8 shrink-0 rounded object-cover"
|
|
967
|
+
/>
|
|
635
968
|
) : attachment.contentType.startsWith("image/") ? (
|
|
636
969
|
<ImageIcon className="size-4 shrink-0 text-og-fg-muted" />
|
|
637
970
|
) : (
|
|
@@ -640,18 +973,23 @@ function AttachmentChips({ attachments, onRemove, onRetry }: {
|
|
|
640
973
|
<div className="min-w-0 flex-1">
|
|
641
974
|
<div className="truncate font-medium text-og-fg">{attachment.name}</div>
|
|
642
975
|
<div
|
|
643
|
-
className={cn(
|
|
976
|
+
className={cn(
|
|
977
|
+
"truncate text-og-xs",
|
|
978
|
+
failed ? "text-og-status-failed" : "text-og-fg-subtle",
|
|
979
|
+
)}
|
|
644
980
|
title={failed ? statusText : undefined}
|
|
645
981
|
>
|
|
646
982
|
{statusText}
|
|
647
983
|
</div>
|
|
648
984
|
</div>
|
|
649
|
-
{attachment.status === "uploading" ?
|
|
985
|
+
{attachment.status === "uploading" ? (
|
|
986
|
+
<LoaderCircleIcon className="size-3.5 shrink-0 animate-og-spin" />
|
|
987
|
+
) : null}
|
|
650
988
|
{failed && onRetry ? (
|
|
651
989
|
<button
|
|
652
990
|
type="button"
|
|
653
991
|
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-
|
|
992
|
+
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
993
|
aria-label={`Retry ${attachment.name}`}
|
|
656
994
|
title="Retry upload"
|
|
657
995
|
>
|
|
@@ -661,7 +999,7 @@ function AttachmentChips({ attachments, onRemove, onRetry }: {
|
|
|
661
999
|
<button
|
|
662
1000
|
type="button"
|
|
663
1001
|
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-
|
|
1002
|
+
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
1003
|
aria-label={`Remove ${attachment.name}`}
|
|
666
1004
|
>
|
|
667
1005
|
<XIcon className="size-3.5" />
|
|
@@ -674,7 +1012,13 @@ function AttachmentChips({ attachments, onRemove, onRetry }: {
|
|
|
674
1012
|
}
|
|
675
1013
|
|
|
676
1014
|
/** The in-composer /help panel, rendered entirely from the registry. */
|
|
677
|
-
function HelpPanel({
|
|
1015
|
+
function HelpPanel({
|
|
1016
|
+
commands,
|
|
1017
|
+
onClose,
|
|
1018
|
+
}: {
|
|
1019
|
+
commands: readonly SlashCommand[];
|
|
1020
|
+
onClose: () => void;
|
|
1021
|
+
}) {
|
|
678
1022
|
return (
|
|
679
1023
|
<motion.div
|
|
680
1024
|
initial={{ opacity: 0, height: 0 }}
|
|
@@ -684,7 +1028,11 @@ function HelpPanel({ commands, onClose }: { commands: readonly SlashCommand[]; o
|
|
|
684
1028
|
>
|
|
685
1029
|
<div className="flex items-center justify-between border-b border-og-border px-3 py-1.5">
|
|
686
1030
|
<span className="text-og-sm font-medium text-og-fg">Commands</span>
|
|
687
|
-
<button
|
|
1031
|
+
<button
|
|
1032
|
+
type="button"
|
|
1033
|
+
onClick={onClose}
|
|
1034
|
+
className="text-og-xs text-og-fg-subtle hover:text-og-fg"
|
|
1035
|
+
>
|
|
688
1036
|
Close
|
|
689
1037
|
</button>
|
|
690
1038
|
</div>
|