@opengeni/react 0.15.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -10
- package/dist/{chunk-TOJR776I.js → chunk-6BXBGJ3B.js} +45 -369
- package/dist/chunk-6BXBGJ3B.js.map +1 -0
- package/dist/chunk-6UT5OC3P.js +2133 -0
- package/dist/chunk-6UT5OC3P.js.map +1 -0
- package/dist/chunk-EBTT3MYN.js +92 -0
- package/dist/chunk-EBTT3MYN.js.map +1 -0
- package/dist/chunk-HHFA4AFX.js +425 -0
- package/dist/chunk-HHFA4AFX.js.map +1 -0
- package/dist/chunk-LAZ2A743.js +2393 -0
- package/dist/chunk-LAZ2A743.js.map +1 -0
- package/dist/chunk-MRSECXRP.js +563 -0
- package/dist/chunk-MRSECXRP.js.map +1 -0
- package/dist/chunk-TK7G6XLT.js +18 -0
- package/dist/chunk-TK7G6XLT.js.map +1 -0
- package/dist/composer-DvUaa5ki.d.ts +585 -0
- package/dist/composer.d.ts +6 -0
- package/dist/composer.js +56 -0
- package/dist/composer.js.map +1 -0
- package/dist/index.d.ts +1288 -1785
- package/dist/index.js +7015 -8806
- package/dist/index.js.map +1 -1
- package/dist/machines.d.ts +412 -3
- package/dist/machines.js +3 -1
- package/dist/noto-sans-arabic-loader-IA2T4X2A.js +21 -0
- package/dist/noto-sans-arabic-loader-IA2T4X2A.js.map +1 -0
- package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js +292 -0
- package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js.map +1 -0
- package/dist/queue-surface-implementation-NQMV2ML3.js +890 -0
- package/dist/queue-surface-implementation-NQMV2ML3.js.map +1 -0
- package/dist/session-Gd4iVPYw.d.ts +425 -0
- package/dist/session-context-DIFFlXKc.d.ts +52 -0
- package/dist/session.d.ts +4 -0
- package/dist/session.js +50 -0
- package/dist/session.js.map +1 -0
- package/dist/use-composer-BCfm4mzX.d.ts +112 -0
- package/package.json +20 -4
- package/src/approvals.ts +5 -3
- package/src/client.ts +40 -0
- package/src/components/approval-surface.tsx +198 -0
- package/src/components/chat-composer.tsx +94 -993
- package/src/components/code-editor.tsx +104 -26
- package/src/components/command-palette.tsx +8 -2
- package/src/components/composer-transcription-control.tsx +211 -0
- package/src/components/composer.tsx +1536 -0
- package/src/components/desktop-viewer.tsx +3 -3
- package/src/components/enrollment-consent.tsx +2 -2
- package/src/components/file-browser.tsx +367 -29
- package/src/components/human-input-form.tsx +417 -0
- package/src/components/machine-card.tsx +1 -1
- package/src/components/machines-dashboard.tsx +3 -3
- package/src/components/message-timeline.tsx +15 -7
- package/src/components/model-picker.tsx +12 -3
- package/src/components/pierre-diff.tsx +32 -6
- package/src/components/queue-surface-implementation.tsx +1041 -0
- package/src/components/queue-surface-state.tsx +94 -0
- package/src/components/queue-surface.tsx +41 -559
- package/src/components/sandbox-files.tsx +138 -14
- package/src/components/sandbox-terminal.tsx +8 -2
- package/src/components/sandbox-workspace.tsx +339 -119
- package/src/components/workbench-changes.tsx +84 -33
- package/src/components/workspace-dock.tsx +188 -18
- package/src/composer.ts +60 -0
- package/src/hooks/internal.ts +115 -34
- package/src/hooks/use-composer.ts +335 -104
- package/src/hooks/use-human-input.ts +131 -0
- package/src/hooks/use-machine-chip.ts +2 -2
- package/src/hooks/use-machines.ts +27 -11
- package/src/hooks/use-sandbox-files.ts +310 -62
- package/src/hooks/use-sandbox-git.ts +154 -40
- package/src/hooks/use-sandbox-terminal.ts +28 -6
- package/src/hooks/use-session-capabilities.ts +38 -9
- package/src/hooks/use-session-control.ts +37 -8
- package/src/hooks/use-session-events.ts +522 -48
- package/src/hooks/use-slash-commands.ts +38 -38
- package/src/hooks/use-transcription.ts +757 -0
- package/src/hooks/use-turn-queue.ts +134 -66
- package/src/hooks/use-windowed-sections.ts +2 -2
- package/src/hooks/use-workspace-capture.ts +146 -40
- package/src/hooks/use-workspace-edit.ts +50 -14
- package/src/hooks/use-workspace-sessions.ts +3 -0
- package/src/human-input.ts +72 -0
- package/src/index.ts +76 -6
- package/src/lib/noto-sans-arabic-loader.ts +21 -0
- package/src/lib/noto-sans-jp-loader.generated.ts +295 -0
- package/src/lib/use-portal-token-style.ts +51 -0
- package/src/lib/use-unicode-fonts.ts +57 -0
- package/src/provider.tsx +4 -69
- package/src/session-context.ts +135 -0
- package/src/session.ts +79 -0
- package/src/timeline/parsers.ts +35 -0
- package/src/timeline/projection.ts +12 -4
- package/src/timeline/tool-renderers.tsx +41 -0
- package/src/timeline/turn-summary.tsx +2 -2
- package/src/timeline/types.ts +16 -8
- package/styles/index.css +56 -0
- package/styles/index.d.ts +2 -0
- package/styles/tokens.css +11 -6
- package/styles/tokens.d.ts +2 -0
- package/dist/chunk-TOJR776I.js.map +0 -1
- package/dist/machines-BpdwuQcD.d.ts +0 -449
|
@@ -12,25 +12,28 @@ import {
|
|
|
12
12
|
} from "react";
|
|
13
13
|
import { VList } from "virtua";
|
|
14
14
|
import { cn } from "../lib/cn";
|
|
15
|
+
import { useUnicodeFallbackFonts } from "../lib/use-unicode-fonts";
|
|
15
16
|
import { useThemeType } from "../lib/use-theme-type";
|
|
16
17
|
import { formatAsOf } from "../hooks/use-machine-chip";
|
|
18
|
+
import type { SandboxGitFileDiff } from "../hooks/use-sandbox-git";
|
|
17
19
|
import { useWindowedSections } from "../hooks/use-windowed-sections";
|
|
18
20
|
import { PierreDiff } from "./pierre-diff";
|
|
19
21
|
|
|
20
22
|
/* ----------------------------------------------------------------------------
|
|
21
23
|
Changes tab — a PR-review surface.
|
|
22
24
|
|
|
23
|
-
A file rail (status glyph, ±counts, grouped by
|
|
24
|
-
|
|
25
|
+
A file rail (status glyph, ±counts, grouped by repository when metadata is
|
|
26
|
+
available, otherwise by top-level directory for a large legacy input) on the
|
|
27
|
+
left; a stacked, WINDOWED diff pane on the right. Only
|
|
25
28
|
the file sections inside the visible window ± overscan mount a Pierre/Shiki
|
|
26
29
|
highlighter (the one renderer), so a 40- or 400-file change set never mounts N
|
|
27
|
-
highlighters at once (
|
|
30
|
+
highlighters at once (D2). A per-file size guard (binary / diff too
|
|
28
31
|
large) degrades to an "open live" affordance instead of a diff body.
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
threshold
|
|
33
|
+
Workspace-wide Git results carry an explicit `repoRoot`, so grouping never
|
|
34
|
+
guesses repository ownership from a directory name. Multi-repo results group
|
|
35
|
+
at every size; legacy unscoped inputs group by top-level directory only past
|
|
36
|
+
the threshold.
|
|
34
37
|
-------------------------------------------------------------------------- */
|
|
35
38
|
|
|
36
39
|
/** Group the rail (and reorder the pane) once the change set is larger than this. */
|
|
@@ -67,9 +70,16 @@ const STATUS_LETTER: Record<GitFileDiff["status"], string> = {
|
|
|
67
70
|
typechange: "T",
|
|
68
71
|
};
|
|
69
72
|
|
|
73
|
+
function compactFileLabel(file: SandboxGitFileDiff): string {
|
|
74
|
+
const slash = file.path.lastIndexOf("/");
|
|
75
|
+
const basename = slash >= 0 ? file.path.slice(slash + 1) : file.path;
|
|
76
|
+
const parent = slash >= 0 ? file.path.slice(0, slash + 1) : "";
|
|
77
|
+
return `${STATUS_LETTER[file.status]} · ${basename}${parent ? ` — ${parent}` : ""}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
70
80
|
export type WorkbenchChangesProps = {
|
|
71
81
|
/** The changed files (`useSandboxGit().diff`). Assumed non-empty by the caller. */
|
|
72
|
-
diff:
|
|
82
|
+
diff: SandboxGitFileDiff[];
|
|
73
83
|
/** Which source the diff came from — drives the "live" vs "as of turn" badge. */
|
|
74
84
|
source: "live" | "capture" | null;
|
|
75
85
|
/** When the served capture was taken (ISO), when `source === "capture"`. */
|
|
@@ -84,28 +94,41 @@ export type WorkbenchChangesProps = {
|
|
|
84
94
|
|
|
85
95
|
type RailRow =
|
|
86
96
|
| { kind: "group"; label: string; count: number }
|
|
87
|
-
| { kind: "file"; file:
|
|
97
|
+
| { kind: "file"; file: SandboxGitFileDiff; index: number };
|
|
88
98
|
|
|
89
99
|
/** Order the files and build the rail rows (grouped past the threshold). The
|
|
90
100
|
* returned `orderedFiles` drives BOTH the rail and the diff pane so a rail row's
|
|
91
101
|
* `index` addresses the matching pane section. Exported for tests. */
|
|
92
|
-
export function buildRail(files:
|
|
93
|
-
|
|
102
|
+
export function buildRail(files: SandboxGitFileDiff[]): {
|
|
103
|
+
orderedFiles: SandboxGitFileDiff[];
|
|
104
|
+
rows: RailRow[];
|
|
105
|
+
grouped: boolean;
|
|
106
|
+
} {
|
|
107
|
+
const roots = new Set(
|
|
108
|
+
files.flatMap((file) => (file.repoRoot === undefined ? [] : [file.repoRoot])),
|
|
109
|
+
);
|
|
110
|
+
const groupByRepo = roots.size > 1;
|
|
111
|
+
if (!groupByRepo && files.length <= GROUP_THRESHOLD) {
|
|
94
112
|
return {
|
|
95
113
|
orderedFiles: files,
|
|
96
114
|
rows: files.map((file, index) => ({ kind: "file", file, index })),
|
|
115
|
+
grouped: false,
|
|
97
116
|
};
|
|
98
117
|
}
|
|
99
|
-
const groups = new Map<string,
|
|
118
|
+
const groups = new Map<string, SandboxGitFileDiff[]>();
|
|
100
119
|
for (const file of files) {
|
|
101
120
|
const slash = file.path.indexOf("/");
|
|
102
|
-
const key =
|
|
121
|
+
const key = groupByRepo
|
|
122
|
+
? file.repoRoot || "(workspace)"
|
|
123
|
+
: slash > 0
|
|
124
|
+
? file.path.slice(0, slash)
|
|
125
|
+
: "(root)";
|
|
103
126
|
const bucket = groups.get(key);
|
|
104
127
|
if (bucket) bucket.push(file);
|
|
105
128
|
else groups.set(key, [file]);
|
|
106
129
|
}
|
|
107
130
|
const labels = [...groups.keys()].sort((a, b) => a.localeCompare(b));
|
|
108
|
-
const orderedFiles:
|
|
131
|
+
const orderedFiles: SandboxGitFileDiff[] = [];
|
|
109
132
|
const rows: RailRow[] = [];
|
|
110
133
|
for (const label of labels) {
|
|
111
134
|
const bucket = (groups.get(label) ?? []).slice().sort((a, b) => a.path.localeCompare(b.path));
|
|
@@ -115,7 +138,7 @@ export function buildRail(files: GitFileDiff[]): { orderedFiles: GitFileDiff[];
|
|
|
115
138
|
orderedFiles.push(file);
|
|
116
139
|
}
|
|
117
140
|
}
|
|
118
|
-
return { orderedFiles, rows };
|
|
141
|
+
return { orderedFiles, rows, grouped: true };
|
|
119
142
|
}
|
|
120
143
|
|
|
121
144
|
/** A file's diff body is "too large to inline" when it's binary or the diff guard
|
|
@@ -150,8 +173,13 @@ export function WorkbenchChanges({
|
|
|
150
173
|
const [activePath, setActivePath] = useState<string | null>(null);
|
|
151
174
|
const pickerId = useId();
|
|
152
175
|
|
|
153
|
-
const
|
|
154
|
-
|
|
176
|
+
const unicodeFontPaths = useMemo(
|
|
177
|
+
() => diff.flatMap((file) => (file.oldPath ? [file.path, file.oldPath] : [file.path])),
|
|
178
|
+
[diff],
|
|
179
|
+
);
|
|
180
|
+
useUnicodeFallbackFonts(unicodeFontPaths);
|
|
181
|
+
|
|
182
|
+
const { orderedFiles, rows, grouped } = useMemo(() => buildRail(diff), [diff]);
|
|
155
183
|
const selectedIndex = orderedFiles.findIndex((file) => file.path === activePath);
|
|
156
184
|
const activeIndex = selectedIndex >= 0 ? selectedIndex : 0;
|
|
157
185
|
|
|
@@ -251,10 +279,20 @@ export function WorkbenchChanges({
|
|
|
251
279
|
compact && source === "capture" && "flex-col items-stretch gap-1.5 py-2",
|
|
252
280
|
)}
|
|
253
281
|
>
|
|
254
|
-
<span
|
|
282
|
+
<span
|
|
283
|
+
role="status"
|
|
284
|
+
aria-atomic="true"
|
|
285
|
+
aria-live="polite"
|
|
286
|
+
data-contrast-audited
|
|
287
|
+
className="min-w-0 text-og-xs text-og-fg-muted"
|
|
288
|
+
>
|
|
255
289
|
{diff.length} {diff.length === 1 ? "file" : "files"} changed
|
|
256
|
-
<span className="ml-2 text-og-status-idle"
|
|
257
|
-
|
|
290
|
+
<span data-contrast-audited className="ml-2 text-og-status-idle">
|
|
291
|
+
+{additions}
|
|
292
|
+
</span>
|
|
293
|
+
<span data-contrast-audited className="ml-1 text-og-status-failed">
|
|
294
|
+
−{deletions}
|
|
295
|
+
</span>
|
|
258
296
|
</span>
|
|
259
297
|
<div
|
|
260
298
|
className={cn(
|
|
@@ -281,12 +319,13 @@ export function WorkbenchChanges({
|
|
|
281
319
|
const index = Number(event.currentTarget.value);
|
|
282
320
|
setActivePath(orderedFiles[index]?.path ?? null);
|
|
283
321
|
}}
|
|
322
|
+
title={activeFile?.path}
|
|
284
323
|
data-compact-file-picker
|
|
285
324
|
className="h-11 w-full rounded-og-md border border-og-border bg-og-bg px-3 font-og-mono text-og-sm text-og-fg outline-none transition-colors focus:border-og-accent focus:ring-2 focus:ring-og-accent-soft"
|
|
286
325
|
>
|
|
287
326
|
{orderedFiles.map((file, index) => (
|
|
288
327
|
<option key={file.path} value={index}>
|
|
289
|
-
{
|
|
328
|
+
{compactFileLabel(file)}
|
|
290
329
|
</option>
|
|
291
330
|
))}
|
|
292
331
|
</select>
|
|
@@ -316,7 +355,7 @@ export function WorkbenchChanges({
|
|
|
316
355
|
<div
|
|
317
356
|
key={`g:${row.label}`}
|
|
318
357
|
data-rail-group
|
|
319
|
-
className="flex items-center gap-1.5 px-2 pb-0.5 pt-2 text-
|
|
358
|
+
className="flex items-center gap-1.5 px-2 pb-0.5 pt-2 text-og-xs font-medium uppercase tracking-wide text-og-fg-subtle"
|
|
320
359
|
>
|
|
321
360
|
<span className="min-w-0 truncate">{row.label}</span>
|
|
322
361
|
<span className="shrink-0">{row.count}</span>
|
|
@@ -387,7 +426,7 @@ function SourceBadge({
|
|
|
387
426
|
if (source === "capture" && capturedAt) {
|
|
388
427
|
return (
|
|
389
428
|
<span
|
|
390
|
-
className="inline-flex items-center gap-1 rounded-og-xs border border-og-border px-1.5 py-px text-
|
|
429
|
+
className="inline-flex items-center gap-1 rounded-og-xs border border-og-border px-1.5 py-px text-og-xs text-og-fg-muted"
|
|
391
430
|
title={new Date(capturedAt).toLocaleString()}
|
|
392
431
|
>
|
|
393
432
|
<HistoryIcon className="size-3 shrink-0 text-og-status-running" aria-hidden />
|
|
@@ -396,7 +435,7 @@ function SourceBadge({
|
|
|
396
435
|
);
|
|
397
436
|
}
|
|
398
437
|
return (
|
|
399
|
-
<span className="rounded-og-xs bg-og-surface-2 px-1.5 py-px text-
|
|
438
|
+
<span className="rounded-og-xs bg-og-surface-2 px-1.5 py-px text-og-xs text-og-fg-subtle">
|
|
400
439
|
{label}
|
|
401
440
|
</span>
|
|
402
441
|
);
|
|
@@ -419,14 +458,21 @@ function RailFileRow({
|
|
|
419
458
|
active,
|
|
420
459
|
onClick,
|
|
421
460
|
}: {
|
|
422
|
-
file:
|
|
461
|
+
file: SandboxGitFileDiff;
|
|
423
462
|
grouped: boolean;
|
|
424
463
|
active: boolean;
|
|
425
464
|
onClick: () => void;
|
|
426
465
|
}) {
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
const
|
|
466
|
+
// A real repository group strips the exact repo root. Legacy top-directory
|
|
467
|
+
// grouping strips only that first segment.
|
|
468
|
+
const repoPrefix = file.repoRoot ? `${file.repoRoot}/` : null;
|
|
469
|
+
const shown = grouped
|
|
470
|
+
? file.repoRoot !== undefined
|
|
471
|
+
? repoPrefix && file.path.startsWith(repoPrefix)
|
|
472
|
+
? file.path.slice(repoPrefix.length)
|
|
473
|
+
: file.path
|
|
474
|
+
: file.path.slice(file.path.indexOf("/") + 1) || file.path
|
|
475
|
+
: file.path;
|
|
430
476
|
return (
|
|
431
477
|
<button
|
|
432
478
|
type="button"
|
|
@@ -441,14 +487,19 @@ function RailFileRow({
|
|
|
441
487
|
)}
|
|
442
488
|
>
|
|
443
489
|
<span
|
|
490
|
+
data-contrast-audited
|
|
444
491
|
className={cn("w-3 shrink-0 text-center font-og-mono text-og-xs", STATUS_TINT[file.status])}
|
|
445
492
|
>
|
|
446
493
|
{STATUS_LETTER[file.status]}
|
|
447
494
|
</span>
|
|
448
495
|
<span className="min-w-0 flex-1 truncate">{shown}</span>
|
|
449
|
-
<span className="ml-auto flex shrink-0 items-center gap-1 pl-1 font-og-mono text-
|
|
450
|
-
<span className="text-og-status-idle"
|
|
451
|
-
|
|
496
|
+
<span className="ml-auto flex shrink-0 items-center gap-1 pl-1 font-og-mono text-og-xs">
|
|
497
|
+
<span data-contrast-audited className="text-og-status-idle">
|
|
498
|
+
+{file.additions}
|
|
499
|
+
</span>
|
|
500
|
+
<span data-contrast-audited className="text-og-status-failed">
|
|
501
|
+
−{file.deletions}
|
|
502
|
+
</span>
|
|
452
503
|
</span>
|
|
453
504
|
</button>
|
|
454
505
|
);
|
|
@@ -512,7 +563,7 @@ function DiffSection({
|
|
|
512
563
|
<span className="min-w-0 truncate font-og-mono text-og-xs">
|
|
513
564
|
{renamed ? `${file.oldPath} → ${file.path}` : file.path}
|
|
514
565
|
</span>
|
|
515
|
-
<span className="flex shrink-0 items-center gap-2 font-og-mono text-
|
|
566
|
+
<span className="flex shrink-0 items-center gap-2 font-og-mono text-og-xs">
|
|
516
567
|
<span className="text-og-status-idle">+{file.additions}</span>
|
|
517
568
|
<span className="text-og-status-failed">−{file.deletions}</span>
|
|
518
569
|
</span>
|
|
@@ -571,7 +622,7 @@ function LayoutToggle({
|
|
|
571
622
|
type="button"
|
|
572
623
|
onClick={() => onChange(value)}
|
|
573
624
|
className={cn(
|
|
574
|
-
"min-h-7 rounded-og-xs px-1.5 py-0.5 text-
|
|
625
|
+
"min-h-7 rounded-og-xs px-1.5 py-0.5 text-og-xs capitalize focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent max-[1023px]:min-w-11 pointer-coarse:min-h-11 pointer-coarse:min-w-11",
|
|
575
626
|
layout === value
|
|
576
627
|
? "bg-og-accent-soft text-og-fg"
|
|
577
628
|
: "text-og-fg-subtle hover:text-og-fg",
|
|
@@ -15,7 +15,14 @@ import {
|
|
|
15
15
|
PanelRightCloseIcon,
|
|
16
16
|
XIcon,
|
|
17
17
|
} from "lucide-react";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
Group,
|
|
20
|
+
Panel,
|
|
21
|
+
Separator,
|
|
22
|
+
useDefaultLayout,
|
|
23
|
+
usePanelRef,
|
|
24
|
+
type LayoutStorage,
|
|
25
|
+
} from "react-resizable-panels";
|
|
19
26
|
import { cn } from "../lib/cn";
|
|
20
27
|
|
|
21
28
|
export type WorkspaceTab = {
|
|
@@ -53,6 +60,24 @@ export type WorkspaceDockProps = {
|
|
|
53
60
|
className?: string | undefined;
|
|
54
61
|
};
|
|
55
62
|
|
|
63
|
+
const SERVER_LAYOUT_STORAGE: LayoutStorage = {
|
|
64
|
+
getItem: () => null,
|
|
65
|
+
setItem: () => {},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** `useDefaultLayout` defaults to the ambient `localStorage` identifier when
|
|
69
|
+
* storage is undefined, which throws during SSR. Access can also throw in a
|
|
70
|
+
* browser that blocks storage (sandboxed frames/private policy), so both cases
|
|
71
|
+
* use a stable, non-persisting implementation instead. */
|
|
72
|
+
function getLayoutStorage(): LayoutStorage {
|
|
73
|
+
if (typeof window === "undefined") return SERVER_LAYOUT_STORAGE;
|
|
74
|
+
try {
|
|
75
|
+
return window.localStorage;
|
|
76
|
+
} catch {
|
|
77
|
+
return SERVER_LAYOUT_STORAGE;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
56
81
|
/**
|
|
57
82
|
* The resizable / collapsible / maximizable right-hand Workspace dock. Replaces
|
|
58
83
|
* a fixed grid column: drag the separator to set width, collapse to a thin rail
|
|
@@ -116,6 +141,7 @@ export function WorkspaceDock({
|
|
|
116
141
|
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
117
142
|
const reopenRef = useRef<HTMLButtonElement | null>(null);
|
|
118
143
|
const returnFocusRef = useRef<HTMLElement | null>(null);
|
|
144
|
+
const initialNarrowFocusRef = useRef(false);
|
|
119
145
|
const dockPanelRef = usePanelRef();
|
|
120
146
|
const [internalCollapsed, setInternalCollapsed] = useState(false);
|
|
121
147
|
const [maximized, setMaximized] = useState(false);
|
|
@@ -132,9 +158,9 @@ export function WorkspaceDock({
|
|
|
132
158
|
// Persisted layout (width split) keyed by autoSaveId.
|
|
133
159
|
const { defaultLayout, onLayoutChanged } = useDefaultLayout({
|
|
134
160
|
panelIds: ["primary", "dock"],
|
|
135
|
-
storage:
|
|
161
|
+
storage: getLayoutStorage(),
|
|
136
162
|
id: autoSaveId,
|
|
137
|
-
}
|
|
163
|
+
});
|
|
138
164
|
|
|
139
165
|
const requestedTab = activeTab ?? internalTab;
|
|
140
166
|
const tabIds = tabs.map((tab) => tab.id);
|
|
@@ -206,6 +232,23 @@ export function WorkspaceDock({
|
|
|
206
232
|
return () => cancelAnimationFrame(frame);
|
|
207
233
|
}, [collapsed, hostControlled, narrow]);
|
|
208
234
|
|
|
235
|
+
// On the first narrow render there is no collapsed-state transition to drive
|
|
236
|
+
// focus into the modal: the overlay can already be open at mount. Place focus
|
|
237
|
+
// on its selected tab once, just as we do when an existing overlay is opened.
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
if (!narrow || collapsed || initialNarrowFocusRef.current) return;
|
|
240
|
+
initialNarrowFocusRef.current = true;
|
|
241
|
+
const active = document.activeElement;
|
|
242
|
+
const workspaceSurface = rootRef.current?.querySelector<HTMLElement>('[role="dialog"]');
|
|
243
|
+
if (active instanceof HTMLElement && !workspaceSurface?.contains(active)) {
|
|
244
|
+
returnFocusRef.current = active;
|
|
245
|
+
}
|
|
246
|
+
const frame = requestAnimationFrame(() => {
|
|
247
|
+
workspaceSurface?.querySelector<HTMLElement>('[role="tab"][aria-selected="true"]')?.focus();
|
|
248
|
+
});
|
|
249
|
+
return () => cancelAnimationFrame(frame);
|
|
250
|
+
}, [collapsed, narrow]);
|
|
251
|
+
|
|
209
252
|
// Keep the active tab valid if the available tabs change.
|
|
210
253
|
useEffect(() => {
|
|
211
254
|
if (firstTabId && !requestedTabIsValid) {
|
|
@@ -224,14 +267,55 @@ export function WorkspaceDock({
|
|
|
224
267
|
setCollapsed(false);
|
|
225
268
|
}, [dockPanelRef, setCollapsed]);
|
|
226
269
|
|
|
227
|
-
// Esc restores from maximize (desktop) and closes the mobile overlay.
|
|
270
|
+
// Esc restores from maximize (desktop) and closes the mobile overlay. Tab is
|
|
271
|
+
// fenced inside either full-screen surface: both visually cover the host, so
|
|
272
|
+
// keyboard focus must never move into invisible content behind them.
|
|
228
273
|
useEffect(() => {
|
|
229
274
|
const overlayOpen = maximized || (narrow && !collapsed);
|
|
230
275
|
if (!overlayOpen) return;
|
|
231
276
|
const onKey = (event: KeyboardEvent) => {
|
|
232
|
-
if (event.key
|
|
233
|
-
|
|
234
|
-
|
|
277
|
+
if (event.key === "Escape") {
|
|
278
|
+
if (maximized) setMaximized(false);
|
|
279
|
+
else collapse();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (event.key !== "Tab") return;
|
|
283
|
+
const surface = rootRef.current?.querySelector<HTMLElement>(
|
|
284
|
+
narrow ? '[role="dialog"]' : "[data-workspace-surface]",
|
|
285
|
+
);
|
|
286
|
+
if (!surface) return;
|
|
287
|
+
const focusable = Array.from(surface.querySelectorAll<HTMLElement>("*")).filter(
|
|
288
|
+
(element) =>
|
|
289
|
+
element.tabIndex >= 0 &&
|
|
290
|
+
!element.hasAttribute("disabled") &&
|
|
291
|
+
element.getClientRects().length > 0 &&
|
|
292
|
+
getComputedStyle(element).visibility !== "hidden",
|
|
293
|
+
);
|
|
294
|
+
const first = focusable[0];
|
|
295
|
+
const last = focusable.at(-1);
|
|
296
|
+
const active = document.activeElement;
|
|
297
|
+
if (!first || !last) {
|
|
298
|
+
event.preventDefault();
|
|
299
|
+
surface.focus();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (!surface.contains(active)) {
|
|
303
|
+
event.preventDefault();
|
|
304
|
+
(event.shiftKey ? last : first).focus();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if ((event.shiftKey && active === first) || (!event.shiftKey && active === last)) {
|
|
308
|
+
event.preventDefault();
|
|
309
|
+
(event.shiftKey ? last : first).focus();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
// Focus can emerge from a shadow-DOM editor/highlighter host at a point
|
|
313
|
+
// the light-DOM order cannot predict. Repair any escape after the browser
|
|
314
|
+
// performs its default Tab move.
|
|
315
|
+
requestAnimationFrame(() => {
|
|
316
|
+
const next = document.activeElement;
|
|
317
|
+
if (!surface.contains(next)) (event.shiftKey ? last : first).focus();
|
|
318
|
+
});
|
|
235
319
|
};
|
|
236
320
|
window.addEventListener("keydown", onKey);
|
|
237
321
|
return () => window.removeEventListener("keydown", onKey);
|
|
@@ -243,8 +327,16 @@ export function WorkspaceDock({
|
|
|
243
327
|
if (narrow) {
|
|
244
328
|
return (
|
|
245
329
|
<div ref={rootRef} className={cn("relative flex h-full min-h-0 w-full min-w-0", className)}>
|
|
246
|
-
<div className="min-h-0 min-w-0 flex-1">{primary}</div>
|
|
247
330
|
<div
|
|
331
|
+
data-workspace-primary
|
|
332
|
+
aria-hidden={!collapsed ? true : undefined}
|
|
333
|
+
className="min-h-0 min-w-0 flex-1"
|
|
334
|
+
inert={!collapsed ? true : undefined}
|
|
335
|
+
>
|
|
336
|
+
{primary}
|
|
337
|
+
</div>
|
|
338
|
+
<div
|
|
339
|
+
data-workspace-surface
|
|
248
340
|
role="dialog"
|
|
249
341
|
aria-modal="true"
|
|
250
342
|
aria-label="Workspace"
|
|
@@ -323,12 +415,19 @@ export function WorkspaceDock({
|
|
|
323
415
|
onLayoutChanged={onLayoutChanged}
|
|
324
416
|
>
|
|
325
417
|
<Panel id="primary" minSize="30%" className="min-h-0 min-w-0">
|
|
326
|
-
|
|
418
|
+
<div
|
|
419
|
+
data-workspace-primary
|
|
420
|
+
aria-hidden={maximized ? true : undefined}
|
|
421
|
+
className="h-full min-h-0 min-w-0"
|
|
422
|
+
inert={maximized ? true : undefined}
|
|
423
|
+
>
|
|
424
|
+
{primary}
|
|
425
|
+
</div>
|
|
327
426
|
</Panel>
|
|
328
427
|
|
|
329
428
|
{!collapsed && (
|
|
330
|
-
<Separator className="group relative w-1.5 shrink-0 outline-none">
|
|
331
|
-
<span className="absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-og-border transition-colors group-hover:bg-og-accent group-data-[separator-state=dragging]:bg-og-accent" />
|
|
429
|
+
<Separator className="group relative z-10 w-1.5 shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent max-[1023px]:-mx-[19px] max-[1023px]:w-11 pointer-coarse:-mx-[19px] pointer-coarse:w-11">
|
|
430
|
+
<span className="absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-og-border transition-colors group-hover:bg-og-accent group-focus-visible:bg-og-accent group-data-[separator-state=dragging]:bg-og-accent" />
|
|
332
431
|
</Separator>
|
|
333
432
|
)}
|
|
334
433
|
|
|
@@ -354,6 +453,9 @@ export function WorkspaceDock({
|
|
|
354
453
|
layout changes must never destroy an editor buffer or terminal view. */}
|
|
355
454
|
<div
|
|
356
455
|
data-workspace-surface
|
|
456
|
+
role={maximized ? "dialog" : undefined}
|
|
457
|
+
aria-modal={maximized ? true : undefined}
|
|
458
|
+
aria-label={maximized ? "Workspace" : undefined}
|
|
357
459
|
aria-hidden={collapsed ? true : undefined}
|
|
358
460
|
className={cn(
|
|
359
461
|
"flex h-full min-h-0 min-w-0 flex-col bg-og-bg",
|
|
@@ -429,8 +531,15 @@ function DockChrome({
|
|
|
429
531
|
const active = tabs.find((t) => t.id === current) ?? tabs[0];
|
|
430
532
|
const tabsetId = useId();
|
|
431
533
|
const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);
|
|
534
|
+
const tabListRef = useRef<HTMLDivElement | null>(null);
|
|
432
535
|
const [visitedTabs, setVisitedTabs] = useState<Set<string>>(() => new Set());
|
|
536
|
+
const [tabOverflow, setTabOverflow] = useState({ start: false, end: false });
|
|
433
537
|
const activeId = active?.id ?? "";
|
|
538
|
+
const activeTabIndex = tabs.findIndex((tab) => tab.id === activeId);
|
|
539
|
+
const keepActiveTabVisible = useCallback(() => {
|
|
540
|
+
const selected = activeTabIndex >= 0 ? tabRefs.current[activeTabIndex] : null;
|
|
541
|
+
selected?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
542
|
+
}, [activeTabIndex]);
|
|
434
543
|
|
|
435
544
|
useEffect(() => {
|
|
436
545
|
if (!activeId) return;
|
|
@@ -442,6 +551,55 @@ function DockChrome({
|
|
|
442
551
|
});
|
|
443
552
|
}, [activeId]);
|
|
444
553
|
|
|
554
|
+
// A controlled/initial tab can become active without receiving focus (for
|
|
555
|
+
// example, a host routing a guarded diff into Files). Native scrolling only
|
|
556
|
+
// follows focus, so keep the selected tab inside the horizontally scrollable
|
|
557
|
+
// strip even when state — rather than a click or arrow key — selected it.
|
|
558
|
+
useDockLayoutEffect(() => {
|
|
559
|
+
keepActiveTabVisible();
|
|
560
|
+
}, [activeId, keepActiveTabVisible]);
|
|
561
|
+
|
|
562
|
+
// Hidden scrollbars keep the chrome quiet, but without an edge cue a clipped
|
|
563
|
+
// host label reads like corrupted copy. Fade only the edge(s) with more tabs,
|
|
564
|
+
// and recompute after scrolling, resizing, or late font metrics.
|
|
565
|
+
useDockLayoutEffect(() => {
|
|
566
|
+
const list = tabListRef.current;
|
|
567
|
+
if (!list) return;
|
|
568
|
+
const update = () => {
|
|
569
|
+
const max = Math.max(0, list.scrollWidth - list.clientWidth);
|
|
570
|
+
const next = { start: list.scrollLeft > 1, end: list.scrollLeft < max - 1 };
|
|
571
|
+
setTabOverflow((previous) =>
|
|
572
|
+
previous.start === next.start && previous.end === next.end ? previous : next,
|
|
573
|
+
);
|
|
574
|
+
};
|
|
575
|
+
update();
|
|
576
|
+
list.addEventListener("scroll", update, { passive: true });
|
|
577
|
+
const observer =
|
|
578
|
+
typeof ResizeObserver === "undefined"
|
|
579
|
+
? null
|
|
580
|
+
: new ResizeObserver(() => {
|
|
581
|
+
keepActiveTabVisible();
|
|
582
|
+
update();
|
|
583
|
+
});
|
|
584
|
+
observer?.observe(list);
|
|
585
|
+
for (const tab of tabRefs.current) {
|
|
586
|
+
if (tab) observer?.observe(tab);
|
|
587
|
+
}
|
|
588
|
+
return () => {
|
|
589
|
+
list.removeEventListener("scroll", update);
|
|
590
|
+
observer?.disconnect();
|
|
591
|
+
};
|
|
592
|
+
}, [activeId, keepActiveTabVisible, tabs.length]);
|
|
593
|
+
|
|
594
|
+
const tabMask =
|
|
595
|
+
tabOverflow.start && tabOverflow.end
|
|
596
|
+
? "linear-gradient(to right, transparent 0, black 14px, black calc(100% - 14px), transparent 100%)"
|
|
597
|
+
: tabOverflow.start
|
|
598
|
+
? "linear-gradient(to right, transparent 0, black 14px, black 100%)"
|
|
599
|
+
: tabOverflow.end
|
|
600
|
+
? "linear-gradient(to right, black 0, black calc(100% - 14px), transparent 100%)"
|
|
601
|
+
: undefined;
|
|
602
|
+
|
|
445
603
|
const activateTab = (index: number) => {
|
|
446
604
|
const tab = tabs[index];
|
|
447
605
|
if (!tab) {
|
|
@@ -465,8 +623,8 @@ function DockChrome({
|
|
|
465
623
|
|
|
466
624
|
return (
|
|
467
625
|
<>
|
|
468
|
-
<div className="grid shrink-0 grid-cols-[auto_minmax(0,1fr)_auto_auto] items-center gap-x-2 border-b border-og-border px-1.5 py-1 max-[1023px]:grid-cols-[auto_minmax(0,1fr)_auto] max-[1023px]:gap-y-0 max-[1023px]:px-2 max-[1023px]:pb-1 max-[1023px]:pt-0">
|
|
469
|
-
<div className="flex min-w-0 shrink-0 items-center max-[1023px]:min-h-11">
|
|
626
|
+
<div className="grid shrink-0 grid-cols-[auto_minmax(0,1fr)_auto_auto] items-center gap-x-2 border-b border-og-border px-1.5 py-1 max-[1023px]:grid-cols-[auto_minmax(0,1fr)_auto] max-[1023px]:gap-y-0 max-[1023px]:px-2 max-[1023px]:pb-1 max-[1023px]:pt-0 min-[640px]:max-[1023px]:grid-cols-[auto_minmax(0,1fr)_auto_auto]">
|
|
627
|
+
<div className="flex min-w-0 shrink-0 items-center max-[1023px]:min-h-11 min-[640px]:max-[1023px]:col-start-1 min-[640px]:max-[1023px]:row-start-1">
|
|
470
628
|
{leading ?? (
|
|
471
629
|
<span className="hidden truncate px-1 text-og-sm font-semibold text-og-fg max-[1023px]:inline">
|
|
472
630
|
Workspace
|
|
@@ -477,11 +635,19 @@ function DockChrome({
|
|
|
477
635
|
grow into or overlap the chrome controls (they stay shrink-0). The
|
|
478
636
|
scrollbar is hidden to keep the strip calm. On the narrow overlay it
|
|
479
637
|
owns a full second row, so status/close chrome can never squeeze the
|
|
480
|
-
canonical workspace navigation out of view.
|
|
638
|
+
canonical workspace navigation out of view. Tablets have enough
|
|
639
|
+
width to retain the denser single-row chrome, with this same strip
|
|
640
|
+
absorbing any unusually long host-injected labels. */}
|
|
481
641
|
<div
|
|
482
|
-
|
|
642
|
+
ref={tabListRef}
|
|
643
|
+
className="flex min-w-0 items-center gap-0.5 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden max-[1023px]:col-span-3 max-[1023px]:row-start-2 max-[1023px]:w-full min-[640px]:max-[1023px]:col-span-1 min-[640px]:max-[1023px]:col-start-2 min-[640px]:max-[1023px]:row-start-1"
|
|
483
644
|
role="tablist"
|
|
484
645
|
aria-orientation="horizontal"
|
|
646
|
+
style={{
|
|
647
|
+
maskImage: tabMask,
|
|
648
|
+
WebkitMaskImage: tabMask,
|
|
649
|
+
scrollPaddingInline: "16px",
|
|
650
|
+
}}
|
|
485
651
|
>
|
|
486
652
|
{tabs.map((tab, index) => (
|
|
487
653
|
<button
|
|
@@ -504,13 +670,17 @@ function DockChrome({
|
|
|
504
670
|
: "text-og-fg-subtle hover:text-og-fg",
|
|
505
671
|
)}
|
|
506
672
|
>
|
|
507
|
-
<span>{tab.label}</span>
|
|
673
|
+
<span data-contrast-audited>{tab.label}</span>
|
|
508
674
|
{tab.badge}
|
|
509
675
|
</button>
|
|
510
676
|
))}
|
|
511
677
|
</div>
|
|
512
|
-
<div className="flex min-w-0 shrink-0 items-center justify-self-end">
|
|
513
|
-
|
|
678
|
+
<div className="flex min-w-0 shrink-0 items-center justify-self-end min-[640px]:max-[1023px]:col-start-3 min-[640px]:max-[1023px]:row-start-1">
|
|
679
|
+
{accessory}
|
|
680
|
+
</div>
|
|
681
|
+
<div className="flex shrink-0 items-center gap-0.5 text-og-fg-subtle min-[640px]:max-[1023px]:col-start-4 min-[640px]:max-[1023px]:row-start-1">
|
|
682
|
+
{controls}
|
|
683
|
+
</div>
|
|
514
684
|
</div>
|
|
515
685
|
<div className="min-h-0 min-w-0 flex-1 overflow-hidden">
|
|
516
686
|
{tabs.length > 0 ? (
|
package/src/composer.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced chat-composer framework. Import this subpath as a namespace:
|
|
3
|
+
*
|
|
4
|
+
* `import * as Composer from "@opengeni/react/composer"`.
|
|
5
|
+
*/
|
|
6
|
+
export {
|
|
7
|
+
Actions,
|
|
8
|
+
AttachButton,
|
|
9
|
+
Attachments,
|
|
10
|
+
CommandPalette,
|
|
11
|
+
Confirmation,
|
|
12
|
+
Controls,
|
|
13
|
+
Footer,
|
|
14
|
+
Frame,
|
|
15
|
+
Help,
|
|
16
|
+
Hint,
|
|
17
|
+
Input,
|
|
18
|
+
ModelPicker,
|
|
19
|
+
OPEN_WORKSTREAM_CONTROL_EVENT,
|
|
20
|
+
PauseButton,
|
|
21
|
+
PausedState,
|
|
22
|
+
RestoredResources,
|
|
23
|
+
Root,
|
|
24
|
+
SendButton,
|
|
25
|
+
Status,
|
|
26
|
+
Surface,
|
|
27
|
+
defaultChatComposerMessages,
|
|
28
|
+
useChatComposer,
|
|
29
|
+
useChatComposerController,
|
|
30
|
+
} from "./components/composer";
|
|
31
|
+
export type {
|
|
32
|
+
ChatComposerController,
|
|
33
|
+
ChatComposerContextValue,
|
|
34
|
+
ChatComposerMessages,
|
|
35
|
+
ComposerActionsProps,
|
|
36
|
+
ComposerAttachButtonProps,
|
|
37
|
+
ComposerCommandPaletteProps,
|
|
38
|
+
ComposerControlLinks,
|
|
39
|
+
ComposerControlState,
|
|
40
|
+
ComposerControlsProps,
|
|
41
|
+
ComposerDelivery,
|
|
42
|
+
ComposerDraftState,
|
|
43
|
+
ComposerFooterProps,
|
|
44
|
+
ComposerFrameProps,
|
|
45
|
+
ComposerHintProps,
|
|
46
|
+
ComposerInputProps,
|
|
47
|
+
ComposerModelPickerProps,
|
|
48
|
+
ComposerPauseButtonProps,
|
|
49
|
+
ComposerRootProps,
|
|
50
|
+
ComposerSendButtonProps,
|
|
51
|
+
ComposerSubmitBlocker,
|
|
52
|
+
ComposerSubmitMode,
|
|
53
|
+
ComposerSurfaceProps,
|
|
54
|
+
UseChatComposerControllerOptions,
|
|
55
|
+
} from "./components/composer";
|
|
56
|
+
export { ComposerTranscriptionControl } from "./components/composer-transcription-control";
|
|
57
|
+
export type {
|
|
58
|
+
ComposerTranscriptionControlProps,
|
|
59
|
+
ComposerTranscriptionMessages,
|
|
60
|
+
} from "./components/composer-transcription-control";
|