@opengeni/react 0.57.0 → 2.2.0-canary.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 +36 -11
- package/dist/artifacts-spreadsheet.js +1 -1
- package/dist/artifacts.js +1 -1
- package/dist/browser-viewer-GXUICIAG.js +14 -0
- package/dist/{chunk-R3AZOAB5.js → chunk-4MDLEHXL.js} +9 -115
- package/dist/chunk-4MDLEHXL.js.map +1 -0
- package/dist/{chunk-Y7OEDUJ5.js → chunk-5RZN3DD5.js} +1014 -420
- package/dist/chunk-5RZN3DD5.js.map +1 -0
- package/dist/chunk-5YK2ZDIG.js +116 -0
- package/dist/chunk-5YK2ZDIG.js.map +1 -0
- package/dist/chunk-H5YGYNXS.js +329 -0
- package/dist/chunk-H5YGYNXS.js.map +1 -0
- package/dist/{chunk-7WCLAEFV.js → chunk-ICA6R6TW.js} +2 -2
- package/dist/{chunk-7WCLAEFV.js.map → chunk-ICA6R6TW.js.map} +1 -1
- package/dist/{chunk-IGC6A2ZI.js → chunk-JOAVA2TW.js} +589 -217
- package/dist/chunk-JOAVA2TW.js.map +1 -0
- package/dist/{chunk-5UQPNLIO.js → chunk-LTCHJ4MS.js} +9 -9
- package/dist/chunk-LTCHJ4MS.js.map +1 -0
- package/dist/{chunk-C5ZPSSS5.js → chunk-NEXLLTP4.js} +2 -1
- package/dist/{chunk-C5ZPSSS5.js.map → chunk-NEXLLTP4.js.map} +1 -1
- package/dist/chunk-QGBJ6GHG.js +2300 -0
- package/dist/chunk-QGBJ6GHG.js.map +1 -0
- package/dist/{chunk-B6WPZSTT.js → chunk-UOLJWT5Y.js} +78 -16
- package/dist/chunk-UOLJWT5Y.js.map +1 -0
- package/dist/{chunk-MKPIAGVL.js → chunk-W6NFJXSA.js} +8 -19
- package/dist/chunk-W6NFJXSA.js.map +1 -0
- package/dist/{chunk-XBZWOM2K.js → chunk-X4A66EWZ.js} +8 -3
- package/dist/chunk-X4A66EWZ.js.map +1 -0
- package/dist/chunk-X4JAFD5R.js +3907 -0
- package/dist/chunk-X4JAFD5R.js.map +1 -0
- package/dist/{chunk-6KLX2RH5.js → chunk-XSA2QAKP.js} +334 -19
- package/dist/chunk-XSA2QAKP.js.map +1 -0
- package/dist/{chunk-HA46CYQK.js → chunk-XXWH2JLC.js} +792 -206
- package/dist/chunk-XXWH2JLC.js.map +1 -0
- package/dist/client.d.ts +2 -2
- package/dist/components/browser-viewer.d.ts +7 -1
- package/dist/components/computer-viewer.d.ts +4 -1
- package/dist/components/enrollment-consent.d.ts +6 -3
- package/dist/components/file-browser.d.ts +7 -1
- package/dist/components/human-input-form.d.ts +1 -1
- package/dist/components/human-input-surface.d.ts +1 -1
- package/dist/components/machine-card.d.ts +4 -1
- package/dist/components/machines/machine-detail.d.ts +4 -1
- package/dist/components/machines/machine-operation-policy.d.ts +11 -0
- package/dist/components/machines-dashboard.d.ts +3 -1
- package/dist/components/markdown.d.ts +20 -1
- package/dist/components/message-timeline.d.ts +1 -1
- package/dist/components/model-policy-picker.d.ts +2 -0
- package/dist/components/pierre-file.d.ts +1 -1
- package/dist/components/sandbox-files.d.ts +8 -2
- package/dist/components/sandbox-workspace.d.ts +33 -3
- package/dist/components/session-chrome.d.ts +15 -3
- package/dist/components/workspace-dock.d.ts +6 -2
- package/dist/composer.js +5 -5
- package/dist/computer-viewer-ZOP4Q7A7.js +16 -0
- package/dist/computer-viewer-ZOP4Q7A7.js.map +1 -0
- package/dist/hooks/channel-cache.d.ts +23 -0
- package/dist/hooks/shared-machines-list.d.ts +14 -0
- package/dist/hooks/use-attached-browsers.d.ts +2 -1
- package/dist/hooks/use-channels.d.ts +1 -0
- package/dist/hooks/use-composer.d.ts +37 -9
- package/dist/hooks/use-machine-chip.d.ts +3 -3
- package/dist/hooks/use-machines.d.ts +19 -1
- package/dist/hooks/use-sandbox-files.d.ts +1 -0
- package/dist/hooks/use-session-background-commands.d.ts +14 -0
- package/dist/hooks/use-turn-queue.d.ts +12 -0
- package/dist/hooks/use-workspace-sessions.d.ts +1 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +1096 -623
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +29 -181
- package/dist/interaction.js.map +1 -1
- package/dist/lib/interaction-errors.d.ts +3 -0
- package/dist/machines.d.ts +4 -2
- package/dist/machines.js +10 -2
- package/dist/model-policy.d.ts +1 -1
- package/dist/model-policy.js +1 -1
- package/dist/platform-activity-row-BJHUHXAT.js +169 -0
- package/dist/platform-activity-row-BJHUHXAT.js.map +1 -0
- package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-BSKUWHRI.js} +76 -16
- package/dist/queue-surface-implementation-BSKUWHRI.js.map +1 -0
- package/dist/realtime/realtime-control.d.ts +10 -3
- package/dist/realtime.js +138 -29
- package/dist/realtime.js.map +1 -1
- package/dist/session-ui.d.ts +1 -1
- package/dist/session-ui.js +6 -2
- package/dist/session.d.ts +2 -2
- package/dist/session.js +4 -4
- package/dist/timeline/index.d.ts +1 -1
- package/dist/timeline/platform-activity-row.d.ts +15 -0
- package/dist/timeline/screenshot-lightbox.d.ts +1 -1
- package/dist/timeline/shared.d.ts +4 -2
- package/dist/timeline/types.d.ts +48 -5
- package/dist/types/machines.d.ts +2 -2
- package/package.json +3 -3
- package/src/client.ts +12 -1
- package/src/components/artifacts/editable-spreadsheet.tsx +1 -1
- package/src/components/browser-viewer.tsx +361 -58
- package/src/components/chat-composer.tsx +1 -1
- package/src/components/composer.tsx +6 -6
- package/src/components/computer-viewer.tsx +368 -115
- package/src/components/enrollment-consent.tsx +64 -6
- package/src/components/file-browser.tsx +117 -2
- package/src/components/human-input-form.tsx +91 -55
- package/src/components/human-input-surface.tsx +1 -1
- package/src/components/machine-card.tsx +111 -0
- package/src/components/machine-input-display.ts +15 -0
- package/src/components/machines/machine-detail.tsx +47 -0
- package/src/components/machines/machine-operation-policy.tsx +179 -0
- package/src/components/machines-dashboard.tsx +6 -0
- package/src/components/markdown.tsx +174 -20
- package/src/components/message-timeline.tsx +162 -17
- package/src/components/model-policy-picker.tsx +9 -17
- package/src/components/pierre-file.tsx +1 -1
- package/src/components/queue-surface-implementation.tsx +82 -16
- package/src/components/sandbox-files.tsx +135 -19
- package/src/components/sandbox-workspace.tsx +204 -92
- package/src/components/session-chrome.tsx +415 -208
- package/src/components/session-status.tsx +1 -1
- package/src/components/user-message-body.tsx +168 -48
- package/src/components/workspace-dock.tsx +141 -175
- package/src/hooks/channel-cache.ts +104 -0
- package/src/hooks/shared-machines-list.ts +239 -0
- package/src/hooks/use-attached-browsers.ts +5 -1
- package/src/hooks/use-browser-frame-stream.ts +6 -1
- package/src/hooks/use-browser-session.ts +6 -4
- package/src/hooks/use-browser-sessions.ts +7 -1
- package/src/hooks/use-channels.ts +32 -10
- package/src/hooks/use-composer.ts +760 -291
- package/src/hooks/use-computer-frame-stream.ts +14 -9
- package/src/hooks/use-computer-session.ts +16 -14
- package/src/hooks/use-computer-sessions.ts +8 -1
- package/src/hooks/use-last-started-turn-policy.ts +1 -1
- package/src/hooks/use-machine-chip.ts +23 -6
- package/src/hooks/use-machines.ts +104 -6
- package/src/hooks/use-sandbox-files.ts +195 -140
- package/src/hooks/use-sandbox-git.ts +58 -29
- package/src/hooks/use-session-background-commands.ts +56 -0
- package/src/hooks/use-session-capabilities.ts +18 -0
- package/src/hooks/use-turn-queue.ts +111 -12
- package/src/hooks/use-workspace-sessions.ts +5 -1
- package/src/index.ts +26 -4
- package/src/lib/interaction-errors.ts +18 -0
- package/src/machines.ts +9 -1
- package/src/model-policy.ts +10 -2
- package/src/realtime/realtime-control.tsx +200 -37
- package/src/session-context.ts +1 -0
- package/src/session-ui.ts +6 -1
- package/src/session.ts +9 -1
- package/src/timeline/activity-rail.tsx +20 -75
- package/src/timeline/index.ts +4 -0
- package/src/timeline/platform-activity-row.tsx +201 -0
- package/src/timeline/projection.ts +476 -23
- package/src/timeline/screenshot-lightbox.tsx +48 -14
- package/src/timeline/shared.tsx +8 -2
- package/src/timeline/tool-renderers.tsx +104 -6
- package/src/timeline/turn-summary.tsx +6 -1
- package/src/timeline/types.ts +75 -2
- package/src/types/machines.ts +10 -0
- package/styles/compiled.css +115 -104
- package/dist/chunk-5UQPNLIO.js.map +0 -1
- package/dist/chunk-6KLX2RH5.js.map +0 -1
- package/dist/chunk-B6WPZSTT.js.map +0 -1
- package/dist/chunk-HA46CYQK.js.map +0 -1
- package/dist/chunk-IGC6A2ZI.js.map +0 -1
- package/dist/chunk-LLOLUHVG.js +0 -5937
- package/dist/chunk-LLOLUHVG.js.map +0 -1
- package/dist/chunk-MKPIAGVL.js.map +0 -1
- package/dist/chunk-R3AZOAB5.js.map +0 -1
- package/dist/chunk-XBZWOM2K.js.map +0 -1
- package/dist/chunk-Y7OEDUJ5.js.map +0 -1
- package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
- package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
- /package/dist/{interactive-workbench-viewers-TOHBXJEE.js.map → browser-viewer-GXUICIAG.js.map} +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ReactNode, useState } from "react";
|
|
2
|
+
import type { ResourceAuthorityScope } from "@opengeni/sdk";
|
|
2
3
|
import {
|
|
3
4
|
CircleAlertIcon,
|
|
4
5
|
LaptopIcon,
|
|
@@ -30,8 +31,10 @@ export type EnrollmentConsentProps = {
|
|
|
30
31
|
userCode: string;
|
|
31
32
|
machine: EnrollmentConsentMachine;
|
|
32
33
|
phase?: EnrollmentConsentPhase | undefined;
|
|
33
|
-
/**
|
|
34
|
-
|
|
34
|
+
/** Whether this caller may publish the machine to the whole organization. */
|
|
35
|
+
allowOrganizationScope?: boolean | undefined;
|
|
36
|
+
/** Approve with the chosen screen-control consent and explicit access scope. */
|
|
37
|
+
onApprove?: ((allowScreenControl: boolean, scope: ResourceAuthorityScope) => void) | undefined;
|
|
35
38
|
onDeny?: (() => void) | undefined;
|
|
36
39
|
/** Error message when phase === "error". */
|
|
37
40
|
errorMessage?: string | undefined;
|
|
@@ -61,13 +64,29 @@ export function EnrollmentConsent({
|
|
|
61
64
|
userCode,
|
|
62
65
|
machine,
|
|
63
66
|
phase = "review",
|
|
67
|
+
allowOrganizationScope = false,
|
|
64
68
|
onApprove,
|
|
65
69
|
onDeny,
|
|
66
70
|
errorMessage,
|
|
67
71
|
className,
|
|
68
72
|
}: EnrollmentConsentProps) {
|
|
69
73
|
const [allowScreenControl, setAllowScreenControl] = useState(machine.requestsScreenControl);
|
|
74
|
+
const [scope, setScope] = useState<ResourceAuthorityScope>("user");
|
|
70
75
|
const busy = phase === "approving";
|
|
76
|
+
const scopeCopy = {
|
|
77
|
+
user: {
|
|
78
|
+
label: "Personal",
|
|
79
|
+
body: "Only you can use this machine, from any workspace you can access in this organization.",
|
|
80
|
+
},
|
|
81
|
+
workspace: {
|
|
82
|
+
label: "Workspace",
|
|
83
|
+
body: "Everyone with access to this workspace can use this machine.",
|
|
84
|
+
},
|
|
85
|
+
organization: {
|
|
86
|
+
label: "Organization",
|
|
87
|
+
body: "Members of this organization can use this machine from their workspaces.",
|
|
88
|
+
},
|
|
89
|
+
} satisfies Record<ResourceAuthorityScope, { label: string; body: string }>;
|
|
71
90
|
|
|
72
91
|
if (phase === "approved") {
|
|
73
92
|
return (
|
|
@@ -75,7 +94,7 @@ export function EnrollmentConsent({
|
|
|
75
94
|
className={className}
|
|
76
95
|
tone="ok"
|
|
77
96
|
title="Machine connected"
|
|
78
|
-
body={`${machine.machineName} is now
|
|
97
|
+
body={`${machine.machineName} is now connected with ${scopeCopy[scope].label.toLowerCase()} access. You can close this page — it will appear in your Machines dashboard.`}
|
|
79
98
|
/>
|
|
80
99
|
);
|
|
81
100
|
}
|
|
@@ -156,11 +175,50 @@ export function EnrollmentConsent({
|
|
|
156
175
|
) : null}
|
|
157
176
|
<Capability
|
|
158
177
|
icon={<UsersIcon className="size-4" aria-hidden />}
|
|
159
|
-
title=
|
|
160
|
-
body=
|
|
178
|
+
title={`${scopeCopy[scope].label} access while connected`}
|
|
179
|
+
body={`${scopeCopy[scope].body} Disconnect the agent to revoke access instantly.`}
|
|
161
180
|
/>
|
|
162
181
|
</ul>
|
|
163
182
|
|
|
183
|
+
<fieldset className="flex flex-col gap-2" disabled={busy}>
|
|
184
|
+
<legend className="mb-1 text-og-sm font-medium text-og-fg">
|
|
185
|
+
Who can use this machine?
|
|
186
|
+
</legend>
|
|
187
|
+
{(["user", "workspace", "organization"] as const).map((candidate) => {
|
|
188
|
+
const disabled = candidate === "organization" && !allowOrganizationScope;
|
|
189
|
+
return (
|
|
190
|
+
<label
|
|
191
|
+
key={candidate}
|
|
192
|
+
data-enrollment-scope={candidate}
|
|
193
|
+
data-disabled={disabled ? "true" : "false"}
|
|
194
|
+
className={cn(
|
|
195
|
+
"flex items-start gap-2.5 rounded-og-md border border-og-border bg-og-bg px-3 py-2.5",
|
|
196
|
+
disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
|
|
197
|
+
)}
|
|
198
|
+
>
|
|
199
|
+
<input
|
|
200
|
+
type="radio"
|
|
201
|
+
name="enrollment-scope"
|
|
202
|
+
value={candidate}
|
|
203
|
+
checked={scope === candidate}
|
|
204
|
+
disabled={disabled || busy}
|
|
205
|
+
onChange={() => setScope(candidate)}
|
|
206
|
+
className="mt-0.5 size-4 accent-og-accent"
|
|
207
|
+
/>
|
|
208
|
+
<span className="min-w-0">
|
|
209
|
+
<span className="block text-og-base font-medium text-og-fg">
|
|
210
|
+
{scopeCopy[candidate].label}
|
|
211
|
+
{disabled ? " · organization admin required" : ""}
|
|
212
|
+
</span>
|
|
213
|
+
<span className="block text-og-sm text-og-fg-muted">
|
|
214
|
+
{scopeCopy[candidate].body}
|
|
215
|
+
</span>
|
|
216
|
+
</span>
|
|
217
|
+
</label>
|
|
218
|
+
);
|
|
219
|
+
})}
|
|
220
|
+
</fieldset>
|
|
221
|
+
|
|
164
222
|
{machine.canOfferDisplay ? (
|
|
165
223
|
<label
|
|
166
224
|
data-screen-control-toggle
|
|
@@ -206,7 +264,7 @@ export function EnrollmentConsent({
|
|
|
206
264
|
type="button"
|
|
207
265
|
data-approve
|
|
208
266
|
disabled={busy}
|
|
209
|
-
onClick={() => onApprove?.(allowScreenControl)}
|
|
267
|
+
onClick={() => onApprove?.(allowScreenControl, scope)}
|
|
210
268
|
className="flex-1 rounded-og-sm bg-og-status-failed px-3 py-2 text-og-sm font-semibold text-og-accent-fg transition-colors hover:opacity-90 disabled:opacity-50 pointer-coarse:min-h-11"
|
|
211
269
|
>
|
|
212
270
|
{busy ? "Connecting…" : "Grant full access"}
|
|
@@ -43,6 +43,12 @@ export type FileBrowserProps = {
|
|
|
43
43
|
/** Selection callback for the preview pane. */
|
|
44
44
|
onSelectFile?: ((path: string) => void) | undefined;
|
|
45
45
|
selectedPath?: string | undefined;
|
|
46
|
+
/** Deliberately reveal this selected path by lazily opening every loaded
|
|
47
|
+
* ancestor and scrolling its row into view. The path is opaque and is never
|
|
48
|
+
* normalized or re-rooted here. */
|
|
49
|
+
revealPath?: string | undefined;
|
|
50
|
+
/** Increment/change to reveal the same path again after a user collapses it. */
|
|
51
|
+
revealPathRequestId?: string | number | undefined;
|
|
46
52
|
/** Render-prop to theme/replace a row entirely (the Pierre-swap escape hatch). */
|
|
47
53
|
renderNode?: ((node: FileTreeNode, depth: number, expanded: boolean) => ReactNode) | undefined;
|
|
48
54
|
/** Shown when the tree is empty (no files / not loaded yet). */
|
|
@@ -96,11 +102,18 @@ function itemKey(item: RenderItem): string {
|
|
|
96
102
|
}
|
|
97
103
|
}
|
|
98
104
|
|
|
99
|
-
|
|
105
|
+
function pathIsInsideDirectory(path: string, directory: string): boolean {
|
|
106
|
+
if (!path.startsWith(directory) || path === directory) return false;
|
|
107
|
+
if (directory.endsWith("/") || directory.endsWith("\\")) return true;
|
|
108
|
+
const boundary = path[directory.length];
|
|
109
|
+
return boundary === "/" || boundary === "\\";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Walk the authoritative tree without assuming its target-owned separator. */
|
|
100
113
|
function findNode(nodes: FileTreeNode[], path: string): FileTreeNode | undefined {
|
|
101
114
|
for (const node of nodes) {
|
|
102
115
|
if (node.path === path) return node;
|
|
103
|
-
if (node.kind === "dir" && node.children && path
|
|
116
|
+
if (node.kind === "dir" && node.children && pathIsInsideDirectory(path, node.path)) {
|
|
104
117
|
const hit = findNode(node.children, path);
|
|
105
118
|
if (hit) return hit;
|
|
106
119
|
}
|
|
@@ -108,6 +121,34 @@ function findNode(nodes: FileTreeNode[], path: string): FileTreeNode | undefined
|
|
|
108
121
|
return undefined;
|
|
109
122
|
}
|
|
110
123
|
|
|
124
|
+
type RevealTrace = {
|
|
125
|
+
directories: string[];
|
|
126
|
+
frontier: string | null;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/** Follow only paths already named by the authoritative tree. This deliberately
|
|
130
|
+
* avoids manufacturing ancestors from a platform-specific path string. */
|
|
131
|
+
function traceRevealPath(nodes: FileTreeNode[], targetPath: string): RevealTrace {
|
|
132
|
+
for (const node of nodes) {
|
|
133
|
+
if (node.path === targetPath) {
|
|
134
|
+
return {
|
|
135
|
+
directories: node.kind === "dir" ? [node.path] : [],
|
|
136
|
+
frontier: node.kind === "dir" && node.children === undefined ? node.path : null,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (node.kind !== "dir" || !pathIsInsideDirectory(targetPath, node.path)) continue;
|
|
140
|
+
if (node.children === undefined) {
|
|
141
|
+
return { directories: [node.path], frontier: node.path };
|
|
142
|
+
}
|
|
143
|
+
const child = traceRevealPath(node.children, targetPath);
|
|
144
|
+
return {
|
|
145
|
+
directories: [node.path, ...child.directories],
|
|
146
|
+
frontier: child.frontier,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return { directories: [], frontier: null };
|
|
150
|
+
}
|
|
151
|
+
|
|
111
152
|
/** A pending inline-create: a phantom input row under `parent`. */
|
|
112
153
|
type DraftCreate = { parent: string; kind: "file" | "dir" };
|
|
113
154
|
/** A pending inline-rename of an existing node. */
|
|
@@ -142,6 +183,8 @@ export function FileBrowser({
|
|
|
142
183
|
fallback,
|
|
143
184
|
onSelectFile,
|
|
144
185
|
selectedPath,
|
|
186
|
+
revealPath,
|
|
187
|
+
revealPathRequestId,
|
|
145
188
|
renderNode,
|
|
146
189
|
emptyState,
|
|
147
190
|
editable = true,
|
|
@@ -158,6 +201,15 @@ export function FileBrowser({
|
|
|
158
201
|
const [pendingDelete, setPendingDelete] = useState<FileTreeNode | null>(null);
|
|
159
202
|
const [busy, setBusy] = useState(false);
|
|
160
203
|
const [coarsePointer, setCoarsePointer] = useState(false);
|
|
204
|
+
const [revealRequest, setRevealRequest] = useState<{
|
|
205
|
+
path: string;
|
|
206
|
+
requestId: string | number;
|
|
207
|
+
} | null>(null);
|
|
208
|
+
const handledRevealRequestRef = useRef<{
|
|
209
|
+
path: string;
|
|
210
|
+
requestId: string | number;
|
|
211
|
+
} | null>(null);
|
|
212
|
+
const attemptedRevealPathsRef = useRef<Set<string>>(new Set());
|
|
161
213
|
const treeId = useId();
|
|
162
214
|
const container = usePortalTokenSource<HTMLDivElement>();
|
|
163
215
|
const containerRef = container.currentRef;
|
|
@@ -184,6 +236,23 @@ export function FileBrowser({
|
|
|
184
236
|
}
|
|
185
237
|
}, []);
|
|
186
238
|
|
|
239
|
+
const effectiveRevealRequestId = revealPath ? (revealPathRequestId ?? revealPath) : null;
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
if (!revealPath || effectiveRevealRequestId === null) {
|
|
242
|
+
setRevealRequest(null);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const handled = handledRevealRequestRef.current;
|
|
246
|
+
if (handled?.path === revealPath && handled.requestId === effectiveRevealRequestId) return;
|
|
247
|
+
handledRevealRequestRef.current = {
|
|
248
|
+
path: revealPath,
|
|
249
|
+
requestId: effectiveRevealRequestId,
|
|
250
|
+
};
|
|
251
|
+
attemptedRevealPathsRef.current = new Set();
|
|
252
|
+
setActive(null);
|
|
253
|
+
setRevealRequest({ path: revealPath, requestId: effectiveRevealRequestId });
|
|
254
|
+
}, [effectiveRevealRequestId, revealPath]);
|
|
255
|
+
|
|
187
256
|
// The keyboard cursor: an explicitly-navigated node falls back to the
|
|
188
257
|
// externally-selected file so arrow keys pick up where the preview pane is.
|
|
189
258
|
const cursor = active ?? selectedPath ?? null;
|
|
@@ -215,6 +284,34 @@ export function FileBrowser({
|
|
|
215
284
|
[expand],
|
|
216
285
|
);
|
|
217
286
|
|
|
287
|
+
useEffect(() => {
|
|
288
|
+
if (!revealRequest) return;
|
|
289
|
+
const trace = traceRevealPath(result.tree, revealRequest.path);
|
|
290
|
+
if (trace.directories.length > 0) {
|
|
291
|
+
setExpanded((previous) => {
|
|
292
|
+
const next = new Set(previous);
|
|
293
|
+
let changed = false;
|
|
294
|
+
for (const path of trace.directories) {
|
|
295
|
+
if (next.has(path)) continue;
|
|
296
|
+
next.add(path);
|
|
297
|
+
changed = true;
|
|
298
|
+
}
|
|
299
|
+
return changed ? next : previous;
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
const frontier = trace.frontier;
|
|
303
|
+
if (
|
|
304
|
+
!frontier ||
|
|
305
|
+
attemptedRevealPathsRef.current.has(frontier) ||
|
|
306
|
+
loadingPaths.has(frontier) ||
|
|
307
|
+
result.expandingPaths.has(frontier)
|
|
308
|
+
) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
attemptedRevealPathsRef.current.add(frontier);
|
|
312
|
+
void expand(frontier);
|
|
313
|
+
}, [expand, loadingPaths, result.expandingPaths, result.tree, revealRequest]);
|
|
314
|
+
|
|
218
315
|
const toggle = useCallback(
|
|
219
316
|
async (node: FileTreeNode) => {
|
|
220
317
|
if (node.kind !== "dir") return;
|
|
@@ -800,6 +897,24 @@ export function FileBrowser({
|
|
|
800
897
|
if (index >= 0) vlistRef.current?.scrollToIndex(index);
|
|
801
898
|
}, [cursor, renderItems]);
|
|
802
899
|
|
|
900
|
+
useEffect(() => {
|
|
901
|
+
if (!revealRequest) return;
|
|
902
|
+
const index = renderItems.findIndex(
|
|
903
|
+
(item) => item.type === "node" && item.node.path === revealRequest.path,
|
|
904
|
+
);
|
|
905
|
+
if (index < 0) return;
|
|
906
|
+
if (usesVirtualTree) {
|
|
907
|
+
vlistRef.current?.scrollToIndex(index);
|
|
908
|
+
} else {
|
|
909
|
+
document.getElementById(`${treeId}-item-${index}`)?.scrollIntoView?.({ block: "nearest" });
|
|
910
|
+
}
|
|
911
|
+
setRevealRequest((current) =>
|
|
912
|
+
current?.path === revealRequest.path && current.requestId === revealRequest.requestId
|
|
913
|
+
? null
|
|
914
|
+
: current,
|
|
915
|
+
);
|
|
916
|
+
}, [renderItems, revealRequest, treeId, usesVirtualTree]);
|
|
917
|
+
|
|
803
918
|
return (
|
|
804
919
|
<div className={cn("flex min-h-0 min-w-0 flex-col", className)}>
|
|
805
920
|
{showToolbar && (
|
|
@@ -40,7 +40,7 @@ export const defaultHumanInputFormMessages: HumanInputFormMessages = {
|
|
|
40
40
|
title: "Input required",
|
|
41
41
|
/** Multi-question chrome has no default subtitle; hosts may still override. */
|
|
42
42
|
description: "",
|
|
43
|
-
submit: "
|
|
43
|
+
submit: "Send answers",
|
|
44
44
|
skip: "Skip",
|
|
45
45
|
submitting: "Submitting…",
|
|
46
46
|
other: "Other",
|
|
@@ -87,7 +87,15 @@ export type HumanInputFormProps = {
|
|
|
87
87
|
* options, sticky ask/submit chrome. Hosts can replace title/description or
|
|
88
88
|
* use `useHumanInputRequests` headlessly.
|
|
89
89
|
*/
|
|
90
|
-
export function HumanInputForm({
|
|
90
|
+
export function HumanInputForm(props: HumanInputFormProps) {
|
|
91
|
+
// The pending-request read model is refreshed after session events and
|
|
92
|
+
// returns newly allocated question arrays for the same durable request.
|
|
93
|
+
// Key the state owner by that request's lifecycle identity so reconciliation
|
|
94
|
+
// cannot erase an answer that the operator is still typing.
|
|
95
|
+
return <HumanInputRequestForm key={props.request.id} {...props} />;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function HumanInputRequestForm({
|
|
91
99
|
request,
|
|
92
100
|
onSubmit,
|
|
93
101
|
submitting = false,
|
|
@@ -135,16 +143,6 @@ export function HumanInputForm({
|
|
|
135
143
|
const submissionGeneration = useRef(0);
|
|
136
144
|
const busy = submitting || submittingInternally;
|
|
137
145
|
|
|
138
|
-
useEffect(() => {
|
|
139
|
-
submissionGeneration.current += 1;
|
|
140
|
-
submissionInFlight.current = false;
|
|
141
|
-
setDrafts(initialDrafts(request.questions));
|
|
142
|
-
setValidationErrors({});
|
|
143
|
-
setSubmissionError(null);
|
|
144
|
-
setSubmittingInternally(false);
|
|
145
|
-
setCollapsed(defaultCollapsed);
|
|
146
|
-
}, [request.id, request.questions, defaultCollapsed]);
|
|
147
|
-
|
|
148
146
|
useEffect(() => {
|
|
149
147
|
if (collapsed) {
|
|
150
148
|
setOverflowBelow(false);
|
|
@@ -369,6 +367,8 @@ export function HumanInputForm({
|
|
|
369
367
|
<div key={question.id} data-human-input-question={question.id}>
|
|
370
368
|
<QuestionControls
|
|
371
369
|
question={question}
|
|
370
|
+
questionNumber={null}
|
|
371
|
+
labelledBy={titleId}
|
|
372
372
|
draft={drafts[question.id] ?? emptyDraft()}
|
|
373
373
|
fieldId={`${formId}-${index}`}
|
|
374
374
|
error={validationErrors[question.id]}
|
|
@@ -391,6 +391,8 @@ export function HumanInputForm({
|
|
|
391
391
|
>
|
|
392
392
|
<QuestionControls
|
|
393
393
|
question={question}
|
|
394
|
+
questionNumber={index + 1}
|
|
395
|
+
labelledBy={undefined}
|
|
394
396
|
draft={drafts[question.id] ?? emptyDraft()}
|
|
395
397
|
fieldId={`${formId}-${index}`}
|
|
396
398
|
error={validationErrors[question.id]}
|
|
@@ -443,7 +445,7 @@ export function HumanInputForm({
|
|
|
443
445
|
<button
|
|
444
446
|
type="submit"
|
|
445
447
|
disabled={busy}
|
|
446
|
-
className="inline-flex min-h-9 items-center rounded-og-md bg-og-accent px-3 py-1.5 text-og-sm font-medium text-og-accent-fg transition
|
|
448
|
+
className="inline-flex min-h-9 items-center rounded-og-md bg-og-accent-deep px-3 py-1.5 text-og-sm font-medium text-og-accent-fg transition hover:brightness-110 disabled:opacity-50"
|
|
447
449
|
>
|
|
448
450
|
{busy ? messages.submitting : resolvedSubmitLabel}
|
|
449
451
|
</button>
|
|
@@ -454,6 +456,8 @@ export function HumanInputForm({
|
|
|
454
456
|
|
|
455
457
|
function QuestionControls({
|
|
456
458
|
question,
|
|
459
|
+
questionNumber,
|
|
460
|
+
labelledBy,
|
|
457
461
|
draft,
|
|
458
462
|
fieldId,
|
|
459
463
|
error,
|
|
@@ -466,6 +470,8 @@ function QuestionControls({
|
|
|
466
470
|
onUpdate,
|
|
467
471
|
}: {
|
|
468
472
|
question: HumanInputQuestion;
|
|
473
|
+
questionNumber: number | null;
|
|
474
|
+
labelledBy: string | undefined;
|
|
469
475
|
draft: HumanInputAnswerDraft;
|
|
470
476
|
fieldId: string;
|
|
471
477
|
error: string | undefined;
|
|
@@ -479,6 +485,13 @@ function QuestionControls({
|
|
|
479
485
|
}) {
|
|
480
486
|
const errorId = `${fieldId}-error`;
|
|
481
487
|
const helpId = `${fieldId}-help`;
|
|
488
|
+
const labelId = `${fieldId}-label`;
|
|
489
|
+
const promptId = `${fieldId}-prompt`;
|
|
490
|
+
const otherChoiceId = `${fieldId}-other-choice`;
|
|
491
|
+
const otherLabelId = `${fieldId}-other-label`;
|
|
492
|
+
const otherTextId = `${fieldId}-other-text`;
|
|
493
|
+
const visibleLabel = question.label ?? question.prompt;
|
|
494
|
+
const controlLabelId = showPromptChrome ? labelId : labelledBy;
|
|
482
495
|
const hint =
|
|
483
496
|
question.kind === "multi_select"
|
|
484
497
|
? messages.selectionHint(
|
|
@@ -488,6 +501,7 @@ function QuestionControls({
|
|
|
488
501
|
: null;
|
|
489
502
|
const describedBy =
|
|
490
503
|
[
|
|
504
|
+
question.label && showPromptChrome ? promptId : null,
|
|
491
505
|
question.helpText && showPromptChrome ? helpId : null,
|
|
492
506
|
hint ? `${fieldId}-hint` : null,
|
|
493
507
|
error ? errorId : null,
|
|
@@ -500,10 +514,15 @@ function QuestionControls({
|
|
|
500
514
|
<>
|
|
501
515
|
<div className="flex flex-wrap items-baseline gap-x-2">
|
|
502
516
|
<label
|
|
517
|
+
id={labelId}
|
|
503
518
|
htmlFor={question.kind === "text" ? fieldId : undefined}
|
|
504
519
|
className="text-og-sm font-medium text-og-fg"
|
|
505
520
|
>
|
|
506
|
-
{
|
|
521
|
+
{questionNumber === null ? null : (
|
|
522
|
+
<span className="mr-1.5 tabular-nums text-og-fg-muted">{questionNumber}.</span>
|
|
523
|
+
)}
|
|
524
|
+
{questionNumber === null ? null : " "}
|
|
525
|
+
{visibleLabel}
|
|
507
526
|
{question.required && !allowSkip ? (
|
|
508
527
|
<span aria-hidden className="ml-1 text-og-status-failed">
|
|
509
528
|
*
|
|
@@ -515,7 +534,11 @@ function QuestionControls({
|
|
|
515
534
|
) : null}
|
|
516
535
|
</label>
|
|
517
536
|
</div>
|
|
518
|
-
{question.label ?
|
|
537
|
+
{question.label ? (
|
|
538
|
+
<p id={promptId} className="text-og-sm text-og-fg-muted">
|
|
539
|
+
{question.prompt}
|
|
540
|
+
</p>
|
|
541
|
+
) : null}
|
|
519
542
|
{question.helpText ? (
|
|
520
543
|
<p id={helpId} className="text-og-xs text-og-fg-subtle">
|
|
521
544
|
{question.helpText}
|
|
@@ -556,6 +579,7 @@ function QuestionControls({
|
|
|
556
579
|
}))
|
|
557
580
|
}
|
|
558
581
|
aria-invalid={Boolean(error)}
|
|
582
|
+
aria-labelledby={controlLabelId}
|
|
559
583
|
aria-describedby={describedBy}
|
|
560
584
|
autoFocus={autoFocus}
|
|
561
585
|
rows={2}
|
|
@@ -564,6 +588,7 @@ function QuestionControls({
|
|
|
564
588
|
) : (
|
|
565
589
|
<div
|
|
566
590
|
role={question.kind === "single_select" ? "radiogroup" : "group"}
|
|
591
|
+
aria-labelledby={controlLabelId}
|
|
567
592
|
aria-describedby={describedBy}
|
|
568
593
|
className="flex flex-col gap-0.5"
|
|
569
594
|
>
|
|
@@ -613,49 +638,59 @@ function QuestionControls({
|
|
|
613
638
|
</label>
|
|
614
639
|
);
|
|
615
640
|
})}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
641
|
+
<div
|
|
642
|
+
className={cn(
|
|
643
|
+
"flex items-start gap-2.5 rounded-og-md px-2.5 py-2 transition-colors",
|
|
644
|
+
draft.otherSelected
|
|
645
|
+
? "bg-og-status-waiting/12 text-og-fg"
|
|
646
|
+
: "text-og-fg hover:bg-og-surface-1/80",
|
|
647
|
+
)}
|
|
648
|
+
>
|
|
649
|
+
<input
|
|
650
|
+
id={otherChoiceId}
|
|
651
|
+
type={question.kind === "single_select" ? "radio" : "checkbox"}
|
|
652
|
+
name={question.kind === "single_select" ? fieldId : undefined}
|
|
653
|
+
aria-labelledby={otherLabelId}
|
|
654
|
+
checked={draft.otherSelected}
|
|
655
|
+
autoFocus={autoFocus && firstOption && question.options.length === 0}
|
|
656
|
+
onChange={(event) =>
|
|
657
|
+
onUpdate((current) => ({
|
|
658
|
+
...current,
|
|
659
|
+
otherSelected: event.target.checked,
|
|
660
|
+
...(question.kind === "single_select" && event.target.checked
|
|
661
|
+
? { values: [] }
|
|
662
|
+
: {}),
|
|
663
|
+
}))
|
|
664
|
+
}
|
|
665
|
+
className="mt-2 accent-og-accent"
|
|
666
|
+
/>
|
|
667
|
+
<span className="min-w-0 flex-1">
|
|
668
|
+
<label
|
|
669
|
+
id={otherLabelId}
|
|
670
|
+
htmlFor={otherChoiceId}
|
|
671
|
+
className="block text-og-sm font-medium"
|
|
672
|
+
>
|
|
673
|
+
{messages.other}
|
|
674
|
+
</label>
|
|
675
|
+
<label htmlFor={otherTextId} className="sr-only">
|
|
676
|
+
{messages.other} answer for {visibleLabel}
|
|
677
|
+
</label>
|
|
625
678
|
<input
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
679
|
+
id={otherTextId}
|
|
680
|
+
type="text"
|
|
681
|
+
value={draft.other}
|
|
682
|
+
disabled={!draft.otherSelected || busy}
|
|
683
|
+
placeholder="Type a value…"
|
|
630
684
|
onChange={(event) =>
|
|
631
685
|
onUpdate((current) => ({
|
|
632
686
|
...current,
|
|
633
|
-
|
|
634
|
-
...(question.kind === "single_select" && event.target.checked
|
|
635
|
-
? { values: [] }
|
|
636
|
-
: {}),
|
|
687
|
+
other: event.target.value,
|
|
637
688
|
}))
|
|
638
689
|
}
|
|
639
|
-
className="mt-2
|
|
690
|
+
className="mt-1.5 w-full rounded-og-sm border border-og-border bg-og-surface-1 px-2 py-1.5 text-og-sm text-og-fg outline-hidden focus:border-og-accent disabled:opacity-50"
|
|
640
691
|
/>
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
<input
|
|
644
|
-
type="text"
|
|
645
|
-
value={draft.other}
|
|
646
|
-
disabled={!draft.otherSelected || busy}
|
|
647
|
-
placeholder="Type a value…"
|
|
648
|
-
onChange={(event) =>
|
|
649
|
-
onUpdate((current) => ({
|
|
650
|
-
...current,
|
|
651
|
-
other: event.target.value,
|
|
652
|
-
}))
|
|
653
|
-
}
|
|
654
|
-
className="mt-1.5 w-full rounded-og-sm border border-og-border bg-og-surface-1 px-2 py-1.5 text-og-sm text-og-fg outline-hidden focus:border-og-accent disabled:opacity-50"
|
|
655
|
-
/>
|
|
656
|
-
</span>
|
|
657
|
-
</label>
|
|
658
|
-
) : null}
|
|
692
|
+
</span>
|
|
693
|
+
</div>
|
|
659
694
|
</div>
|
|
660
695
|
)}
|
|
661
696
|
{error ? (
|
|
@@ -678,12 +713,13 @@ export function answersFromDrafts(
|
|
|
678
713
|
for (const question of questions) {
|
|
679
714
|
const draft = drafts[question.id] ?? emptyDraft();
|
|
680
715
|
const values = question.kind === "text" ? draft.values.filter(Boolean) : draft.values;
|
|
681
|
-
const other = draft.otherSelected ? draft.other
|
|
682
|
-
const
|
|
716
|
+
const other = draft.otherSelected ? draft.other : "";
|
|
717
|
+
const hasOther = Boolean(other.trim());
|
|
718
|
+
const supplied = values.length + (hasOther ? 1 : 0);
|
|
683
719
|
|
|
684
720
|
// Other-selected-but-empty must win over generic "required" — otherwise the
|
|
685
721
|
// user sees the wrong diagnosis next to a clearly selected control.
|
|
686
|
-
if (question.kind !== "text" && draft.otherSelected && !
|
|
722
|
+
if (question.kind !== "text" && draft.otherSelected && !hasOther) {
|
|
687
723
|
errors[question.id] = messages.otherRequired;
|
|
688
724
|
continue;
|
|
689
725
|
}
|
|
@@ -708,7 +744,7 @@ export function answersFromDrafts(
|
|
|
708
744
|
answers.push({
|
|
709
745
|
questionId: question.id,
|
|
710
746
|
values,
|
|
711
|
-
...(
|
|
747
|
+
...(hasOther ? { other } : {}),
|
|
712
748
|
});
|
|
713
749
|
}
|
|
714
750
|
}
|
|
@@ -20,7 +20,7 @@ export type HumanInputSurfaceProps = {
|
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* One decision shell for pending structured human-input requests. Parallel
|
|
23
|
-
* freezes are shown one at a time (oldest first) as “N of M”;
|
|
23
|
+
* freezes are shown one at a time (oldest first) as “N of M”; Send answers/Skip
|
|
24
24
|
* settles the active request and the next remaining set advances automatically
|
|
25
25
|
* when the authoritative pending list updates.
|
|
26
26
|
*/
|