@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
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HumanInputAnswer,
|
|
3
|
+
HumanInputQuestion,
|
|
4
|
+
SessionHumanInputRequest,
|
|
5
|
+
SubmitHumanInputResponseRequest,
|
|
6
|
+
} from "@opengeni/sdk";
|
|
7
|
+
import { useEffect, useId, useRef, useState, type FormEvent, type ReactNode } from "react";
|
|
8
|
+
import { cn } from "../lib/cn";
|
|
9
|
+
|
|
10
|
+
export type HumanInputAnswerDraft = {
|
|
11
|
+
values: string[];
|
|
12
|
+
other: string;
|
|
13
|
+
otherSelected: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type HumanInputFormMessages = {
|
|
17
|
+
title: string;
|
|
18
|
+
description: string;
|
|
19
|
+
submit: string;
|
|
20
|
+
skip: string;
|
|
21
|
+
submitting: string;
|
|
22
|
+
other: string;
|
|
23
|
+
deadlineLabel: string;
|
|
24
|
+
formatDeadline: (value: string) => string;
|
|
25
|
+
required: string;
|
|
26
|
+
minLength: (count: number) => string;
|
|
27
|
+
maxLength: (count: number) => string;
|
|
28
|
+
otherRequired: string;
|
|
29
|
+
minSelections: (count: number) => string;
|
|
30
|
+
maxSelections: (count: number) => string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const defaultHumanInputFormMessages: HumanInputFormMessages = {
|
|
34
|
+
title: "Your input is needed",
|
|
35
|
+
description: "The agent is paused until you answer these questions.",
|
|
36
|
+
submit: "Continue",
|
|
37
|
+
skip: "Skip",
|
|
38
|
+
submitting: "Submitting…",
|
|
39
|
+
other: "Other",
|
|
40
|
+
deadlineLabel: "Respond before",
|
|
41
|
+
formatDeadline,
|
|
42
|
+
required: "This question is required.",
|
|
43
|
+
minLength: (count) => `Enter at least ${count} characters.`,
|
|
44
|
+
maxLength: (count) => `Enter no more than ${count} characters.`,
|
|
45
|
+
otherRequired: "Enter a value for Other.",
|
|
46
|
+
minSelections: (count) => `Choose at least ${count} option${count === 1 ? "" : "s"}.`,
|
|
47
|
+
maxSelections: (count) => `Choose no more than ${count} option${count === 1 ? "" : "s"}.`,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type HumanInputFormProps = {
|
|
51
|
+
request: Pick<SessionHumanInputRequest, "id" | "questions" | "allowSkip" | "expiresAt">;
|
|
52
|
+
onSubmit: (response: SubmitHumanInputResponseRequest) => void | Promise<void>;
|
|
53
|
+
submitting?: boolean | undefined;
|
|
54
|
+
error?: string | null | undefined;
|
|
55
|
+
title?: ReactNode;
|
|
56
|
+
description?: ReactNode;
|
|
57
|
+
submitLabel?: string | undefined;
|
|
58
|
+
skipLabel?: string | undefined;
|
|
59
|
+
messages?: Partial<HumanInputFormMessages> | undefined;
|
|
60
|
+
autoFocus?: boolean | undefined;
|
|
61
|
+
className?: string | undefined;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Styled but host-neutral renderer for one structured request. Hosts can use
|
|
66
|
+
* the headless `useHumanInputRequests` hook instead, or replace the title and
|
|
67
|
+
* description while retaining accessible field semantics and validation.
|
|
68
|
+
*/
|
|
69
|
+
export function HumanInputForm({
|
|
70
|
+
request,
|
|
71
|
+
onSubmit,
|
|
72
|
+
submitting = false,
|
|
73
|
+
error,
|
|
74
|
+
title,
|
|
75
|
+
description,
|
|
76
|
+
submitLabel,
|
|
77
|
+
skipLabel,
|
|
78
|
+
messages: messageOverrides,
|
|
79
|
+
autoFocus = true,
|
|
80
|
+
className,
|
|
81
|
+
}: HumanInputFormProps) {
|
|
82
|
+
const messages = { ...defaultHumanInputFormMessages, ...messageOverrides };
|
|
83
|
+
const resolvedTitle = title === undefined ? messages.title : title;
|
|
84
|
+
const resolvedDescription = description === undefined ? messages.description : description;
|
|
85
|
+
const resolvedSubmitLabel = submitLabel ?? messages.submit;
|
|
86
|
+
const resolvedSkipLabel = skipLabel ?? messages.skip;
|
|
87
|
+
const formId = useId();
|
|
88
|
+
const [drafts, setDrafts] = useState<Record<string, HumanInputAnswerDraft>>(() =>
|
|
89
|
+
initialDrafts(request.questions),
|
|
90
|
+
);
|
|
91
|
+
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
|
|
92
|
+
const [submissionError, setSubmissionError] = useState<string | null>(null);
|
|
93
|
+
const [submittingInternally, setSubmittingInternally] = useState(false);
|
|
94
|
+
const submissionInFlight = useRef(false);
|
|
95
|
+
const submissionGeneration = useRef(0);
|
|
96
|
+
const busy = submitting || submittingInternally;
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
submissionGeneration.current += 1;
|
|
100
|
+
submissionInFlight.current = false;
|
|
101
|
+
setDrafts(initialDrafts(request.questions));
|
|
102
|
+
setValidationErrors({});
|
|
103
|
+
setSubmissionError(null);
|
|
104
|
+
setSubmittingInternally(false);
|
|
105
|
+
}, [request.id, request.questions]);
|
|
106
|
+
|
|
107
|
+
const update = (
|
|
108
|
+
questionId: string,
|
|
109
|
+
apply: (draft: HumanInputAnswerDraft) => HumanInputAnswerDraft,
|
|
110
|
+
): void => {
|
|
111
|
+
setDrafts((current) => ({
|
|
112
|
+
...current,
|
|
113
|
+
[questionId]: apply(current[questionId] ?? emptyDraft()),
|
|
114
|
+
}));
|
|
115
|
+
setValidationErrors((current) => {
|
|
116
|
+
if (!(questionId in current)) return current;
|
|
117
|
+
const next = { ...current };
|
|
118
|
+
delete next[questionId];
|
|
119
|
+
return next;
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const submitResponse = async (response: SubmitHumanInputResponseRequest): Promise<void> => {
|
|
124
|
+
if (busy || submissionInFlight.current) return;
|
|
125
|
+
const generation = submissionGeneration.current;
|
|
126
|
+
submissionInFlight.current = true;
|
|
127
|
+
setSubmissionError(null);
|
|
128
|
+
setSubmittingInternally(true);
|
|
129
|
+
try {
|
|
130
|
+
await onSubmit(response);
|
|
131
|
+
} catch (cause) {
|
|
132
|
+
if (generation === submissionGeneration.current) {
|
|
133
|
+
setSubmissionError(cause instanceof Error ? cause.message : String(cause));
|
|
134
|
+
}
|
|
135
|
+
} finally {
|
|
136
|
+
if (generation === submissionGeneration.current) {
|
|
137
|
+
submissionInFlight.current = false;
|
|
138
|
+
setSubmittingInternally(false);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const submit = async (event: FormEvent<HTMLFormElement>): Promise<void> => {
|
|
144
|
+
event.preventDefault();
|
|
145
|
+
const result = answersFromDrafts(request.questions, drafts, messages);
|
|
146
|
+
if (Object.keys(result.errors).length > 0) {
|
|
147
|
+
setValidationErrors(result.errors);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
await submitResponse({ outcome: "answered", answers: result.answers });
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<form
|
|
155
|
+
data-human-input-request={request.id}
|
|
156
|
+
onSubmit={(event) => void submit(event)}
|
|
157
|
+
className={cn(
|
|
158
|
+
"og-root flex w-full flex-col gap-5 rounded-og-lg border border-og-border bg-og-surface-1 p-5 shadow-og-sm",
|
|
159
|
+
className,
|
|
160
|
+
)}
|
|
161
|
+
>
|
|
162
|
+
<header>
|
|
163
|
+
<h2 className="text-og-md font-semibold text-og-fg">{resolvedTitle}</h2>
|
|
164
|
+
{resolvedDescription ? (
|
|
165
|
+
<div className="mt-1 text-og-sm text-og-fg-muted">{resolvedDescription}</div>
|
|
166
|
+
) : null}
|
|
167
|
+
{request.expiresAt ? (
|
|
168
|
+
<p className="mt-1 text-og-xs text-og-fg-subtle">
|
|
169
|
+
{messages.deadlineLabel}{" "}
|
|
170
|
+
<time dateTime={request.expiresAt}>{messages.formatDeadline(request.expiresAt)}</time>
|
|
171
|
+
</p>
|
|
172
|
+
) : null}
|
|
173
|
+
</header>
|
|
174
|
+
|
|
175
|
+
<fieldset disabled={busy} className="contents">
|
|
176
|
+
{request.questions.map((question, index) => {
|
|
177
|
+
const draft = drafts[question.id] ?? emptyDraft();
|
|
178
|
+
const fieldId = `${formId}-${index}`;
|
|
179
|
+
const errorId = `${fieldId}-error`;
|
|
180
|
+
const helpId = `${fieldId}-help`;
|
|
181
|
+
const describedBy =
|
|
182
|
+
[question.helpText ? helpId : null, validationErrors[question.id] ? errorId : null]
|
|
183
|
+
.filter(Boolean)
|
|
184
|
+
.join(" ") || undefined;
|
|
185
|
+
return (
|
|
186
|
+
<div key={question.id} className="flex flex-col gap-2">
|
|
187
|
+
<label
|
|
188
|
+
htmlFor={question.kind === "text" ? fieldId : undefined}
|
|
189
|
+
className="text-og-base font-medium text-og-fg"
|
|
190
|
+
>
|
|
191
|
+
{question.label ?? question.prompt}
|
|
192
|
+
{question.required ? (
|
|
193
|
+
<span aria-hidden className="ml-1 text-og-status-failed">
|
|
194
|
+
*
|
|
195
|
+
</span>
|
|
196
|
+
) : null}
|
|
197
|
+
</label>
|
|
198
|
+
{question.label ? (
|
|
199
|
+
<p className="text-og-sm text-og-fg-muted">{question.prompt}</p>
|
|
200
|
+
) : null}
|
|
201
|
+
{question.helpText ? (
|
|
202
|
+
<p id={helpId} className="text-og-xs text-og-fg-subtle">
|
|
203
|
+
{question.helpText}
|
|
204
|
+
</p>
|
|
205
|
+
) : null}
|
|
206
|
+
{question.kind === "text" ? (
|
|
207
|
+
<textarea
|
|
208
|
+
id={fieldId}
|
|
209
|
+
value={draft.values[0] ?? ""}
|
|
210
|
+
onChange={(event) =>
|
|
211
|
+
update(question.id, (current) => ({
|
|
212
|
+
...current,
|
|
213
|
+
values: event.target.value ? [event.target.value] : [],
|
|
214
|
+
}))
|
|
215
|
+
}
|
|
216
|
+
aria-invalid={Boolean(validationErrors[question.id])}
|
|
217
|
+
aria-describedby={describedBy}
|
|
218
|
+
autoFocus={autoFocus && index === 0}
|
|
219
|
+
rows={3}
|
|
220
|
+
className="min-h-20 w-full resize-y rounded-og-md border border-og-border bg-og-surface-2 px-3 py-2 text-og-base text-og-fg outline-none placeholder:text-og-fg-subtle focus:border-og-accent"
|
|
221
|
+
/>
|
|
222
|
+
) : (
|
|
223
|
+
<div
|
|
224
|
+
role={question.kind === "single_select" ? "radiogroup" : "group"}
|
|
225
|
+
aria-describedby={describedBy}
|
|
226
|
+
className="flex flex-col gap-2"
|
|
227
|
+
>
|
|
228
|
+
{question.options.map((option, optionIndex) => {
|
|
229
|
+
const checked = draft.values.includes(option.id);
|
|
230
|
+
return (
|
|
231
|
+
<label
|
|
232
|
+
key={option.id}
|
|
233
|
+
className="flex cursor-pointer items-start gap-2 rounded-og-md border border-og-border px-3 py-2 hover:bg-og-surface-2"
|
|
234
|
+
>
|
|
235
|
+
<input
|
|
236
|
+
type={question.kind === "single_select" ? "radio" : "checkbox"}
|
|
237
|
+
name={question.kind === "single_select" ? fieldId : undefined}
|
|
238
|
+
autoFocus={autoFocus && index === 0 && optionIndex === 0}
|
|
239
|
+
checked={checked}
|
|
240
|
+
onChange={(event) =>
|
|
241
|
+
update(question.id, (current) => ({
|
|
242
|
+
...current,
|
|
243
|
+
values:
|
|
244
|
+
question.kind === "single_select"
|
|
245
|
+
? event.target.checked
|
|
246
|
+
? [option.id]
|
|
247
|
+
: []
|
|
248
|
+
: event.target.checked
|
|
249
|
+
? [...current.values, option.id]
|
|
250
|
+
: current.values.filter((value) => value !== option.id),
|
|
251
|
+
...(question.kind === "single_select" && event.target.checked
|
|
252
|
+
? { otherSelected: false }
|
|
253
|
+
: {}),
|
|
254
|
+
}))
|
|
255
|
+
}
|
|
256
|
+
className="mt-0.5 accent-og-accent"
|
|
257
|
+
/>
|
|
258
|
+
<span className="min-w-0">
|
|
259
|
+
<span className="block text-og-sm font-medium text-og-fg">
|
|
260
|
+
{option.label}
|
|
261
|
+
</span>
|
|
262
|
+
{option.description ? (
|
|
263
|
+
<span className="block text-og-xs text-og-fg-muted">
|
|
264
|
+
{option.description}
|
|
265
|
+
</span>
|
|
266
|
+
) : null}
|
|
267
|
+
</span>
|
|
268
|
+
</label>
|
|
269
|
+
);
|
|
270
|
+
})}
|
|
271
|
+
{question.allowOther ? (
|
|
272
|
+
<label className="flex items-start gap-2 rounded-og-md border border-og-border px-3 py-2">
|
|
273
|
+
<input
|
|
274
|
+
type={question.kind === "single_select" ? "radio" : "checkbox"}
|
|
275
|
+
name={question.kind === "single_select" ? fieldId : undefined}
|
|
276
|
+
checked={draft.otherSelected}
|
|
277
|
+
autoFocus={autoFocus && index === 0 && question.options.length === 0}
|
|
278
|
+
onChange={(event) =>
|
|
279
|
+
update(question.id, (current) => ({
|
|
280
|
+
...current,
|
|
281
|
+
otherSelected: event.target.checked,
|
|
282
|
+
...(question.kind === "single_select" && event.target.checked
|
|
283
|
+
? { values: [] }
|
|
284
|
+
: {}),
|
|
285
|
+
}))
|
|
286
|
+
}
|
|
287
|
+
className="mt-2 accent-og-accent"
|
|
288
|
+
/>
|
|
289
|
+
<span className="min-w-0 flex-1">
|
|
290
|
+
<span className="block text-og-sm font-medium text-og-fg">
|
|
291
|
+
{messages.other}
|
|
292
|
+
</span>
|
|
293
|
+
<input
|
|
294
|
+
type="text"
|
|
295
|
+
value={draft.other}
|
|
296
|
+
disabled={!draft.otherSelected || busy}
|
|
297
|
+
onChange={(event) =>
|
|
298
|
+
update(question.id, (current) => ({
|
|
299
|
+
...current,
|
|
300
|
+
other: event.target.value,
|
|
301
|
+
}))
|
|
302
|
+
}
|
|
303
|
+
className="mt-1 w-full rounded-og-sm border border-og-border bg-og-surface-2 px-2 py-1.5 text-og-sm text-og-fg outline-none focus:border-og-accent disabled:opacity-50"
|
|
304
|
+
/>
|
|
305
|
+
</span>
|
|
306
|
+
</label>
|
|
307
|
+
) : null}
|
|
308
|
+
</div>
|
|
309
|
+
)}
|
|
310
|
+
{validationErrors[question.id] ? (
|
|
311
|
+
<p id={errorId} role="alert" className="text-og-xs text-og-status-failed">
|
|
312
|
+
{validationErrors[question.id]}
|
|
313
|
+
</p>
|
|
314
|
+
) : null}
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
317
|
+
})}
|
|
318
|
+
</fieldset>
|
|
319
|
+
|
|
320
|
+
{(error ?? submissionError) ? (
|
|
321
|
+
<p role="alert" className="text-og-sm text-og-status-failed">
|
|
322
|
+
{error ?? submissionError}
|
|
323
|
+
</p>
|
|
324
|
+
) : null}
|
|
325
|
+
<footer className="flex items-center justify-end gap-2">
|
|
326
|
+
{request.allowSkip ? (
|
|
327
|
+
<button
|
|
328
|
+
type="button"
|
|
329
|
+
disabled={busy}
|
|
330
|
+
onClick={() => void submitResponse({ outcome: "skipped" })}
|
|
331
|
+
className="rounded-og-md border border-og-border px-3 py-2 text-og-sm font-medium text-og-fg-muted hover:bg-og-surface-2 disabled:opacity-50"
|
|
332
|
+
>
|
|
333
|
+
{resolvedSkipLabel}
|
|
334
|
+
</button>
|
|
335
|
+
) : null}
|
|
336
|
+
<button
|
|
337
|
+
type="submit"
|
|
338
|
+
disabled={busy}
|
|
339
|
+
className="rounded-og-md bg-og-accent px-3 py-2 text-og-sm font-medium text-og-accent-fg hover:bg-og-accent-strong disabled:opacity-50"
|
|
340
|
+
>
|
|
341
|
+
{busy ? messages.submitting : resolvedSubmitLabel}
|
|
342
|
+
</button>
|
|
343
|
+
</footer>
|
|
344
|
+
</form>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function answersFromDrafts(
|
|
349
|
+
questions: HumanInputQuestion[],
|
|
350
|
+
drafts: Record<string, HumanInputAnswerDraft>,
|
|
351
|
+
messageOverrides: Partial<HumanInputFormMessages> = {},
|
|
352
|
+
): { answers: HumanInputAnswer[]; errors: Record<string, string> } {
|
|
353
|
+
const messages = { ...defaultHumanInputFormMessages, ...messageOverrides };
|
|
354
|
+
const answers: HumanInputAnswer[] = [];
|
|
355
|
+
const errors: Record<string, string> = {};
|
|
356
|
+
for (const question of questions) {
|
|
357
|
+
const draft = drafts[question.id] ?? emptyDraft();
|
|
358
|
+
const values = question.kind === "text" ? draft.values.filter(Boolean) : draft.values;
|
|
359
|
+
const other = draft.otherSelected ? draft.other.trim() : "";
|
|
360
|
+
const supplied = values.length + (other ? 1 : 0);
|
|
361
|
+
if (question.required && supplied === 0) {
|
|
362
|
+
errors[question.id] = messages.required;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (question.kind === "text") {
|
|
366
|
+
const value = values[0] ?? "";
|
|
367
|
+
if (
|
|
368
|
+
value &&
|
|
369
|
+
question.validation?.minLength != null &&
|
|
370
|
+
value.length < question.validation.minLength
|
|
371
|
+
) {
|
|
372
|
+
errors[question.id] = messages.minLength(question.validation.minLength);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (question.validation?.maxLength != null && value.length > question.validation.maxLength) {
|
|
376
|
+
errors[question.id] = messages.maxLength(question.validation.maxLength);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
} else {
|
|
380
|
+
if (draft.otherSelected && !other) {
|
|
381
|
+
errors[question.id] = messages.otherRequired;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
const min = question.validation?.minSelections;
|
|
385
|
+
const max = question.kind === "single_select" ? 1 : question.validation?.maxSelections;
|
|
386
|
+
if (min != null && supplied < min) {
|
|
387
|
+
errors[question.id] = messages.minSelections(min);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (max != null && supplied > max) {
|
|
391
|
+
errors[question.id] = messages.maxSelections(max);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (supplied > 0) {
|
|
396
|
+
answers.push({
|
|
397
|
+
questionId: question.id,
|
|
398
|
+
values,
|
|
399
|
+
...(other ? { other } : {}),
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return { answers, errors };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function initialDrafts(questions: HumanInputQuestion[]): Record<string, HumanInputAnswerDraft> {
|
|
407
|
+
return Object.fromEntries(questions.map((question) => [question.id, emptyDraft()]));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function emptyDraft(): HumanInputAnswerDraft {
|
|
411
|
+
return { values: [], other: "", otherSelected: false };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function formatDeadline(value: string): string {
|
|
415
|
+
const date = new Date(value);
|
|
416
|
+
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
|
|
417
|
+
}
|
|
@@ -194,7 +194,7 @@ export function MachineCard({
|
|
|
194
194
|
onAttach?.(machine);
|
|
195
195
|
}}
|
|
196
196
|
className={cn(
|
|
197
|
-
"rounded-og-sm border border-og-border px-2.5 py-1 text-og-xs font-medium text-og-fg-muted transition-colors pointer-coarse:min-h-
|
|
197
|
+
"rounded-og-sm border border-og-border px-2.5 py-1 text-og-xs font-medium text-og-fg-muted transition-colors pointer-coarse:min-h-11",
|
|
198
198
|
"hover:border-og-border-strong hover:text-og-fg disabled:cursor-not-allowed disabled:opacity-50",
|
|
199
199
|
)}
|
|
200
200
|
>
|
|
@@ -46,7 +46,7 @@ function EmptyState({ onEnroll }: { onEnroll?: (() => void) | undefined }) {
|
|
|
46
46
|
type="button"
|
|
47
47
|
data-enroll-cta
|
|
48
48
|
onClick={onEnroll}
|
|
49
|
-
className="inline-flex items-center gap-1.5 rounded-og-sm bg-og-accent px-3 py-1.5 text-og-sm font-medium text-og-accent-fg transition-colors hover:bg-og-accent-strong pointer-coarse:min-h-
|
|
49
|
+
className="inline-flex items-center gap-1.5 rounded-og-sm bg-og-accent px-3 py-1.5 text-og-sm font-medium text-og-accent-fg transition-colors hover:bg-og-accent-strong pointer-coarse:min-h-11"
|
|
50
50
|
>
|
|
51
51
|
<PlusIcon className="size-3.5" aria-hidden />
|
|
52
52
|
Enroll a machine
|
|
@@ -90,7 +90,7 @@ function Header({
|
|
|
90
90
|
type="button"
|
|
91
91
|
data-enroll-cta
|
|
92
92
|
onClick={onEnroll}
|
|
93
|
-
className="inline-flex items-center gap-1.5 rounded-og-sm border border-og-border px-2.5 py-1 text-og-sm font-medium text-og-fg-muted transition-colors hover:border-og-border-strong hover:text-og-fg pointer-coarse:min-h-
|
|
93
|
+
className="inline-flex items-center gap-1.5 rounded-og-sm border border-og-border px-2.5 py-1 text-og-sm font-medium text-og-fg-muted transition-colors hover:border-og-border-strong hover:text-og-fg pointer-coarse:min-h-11"
|
|
94
94
|
>
|
|
95
95
|
<PlusIcon className="size-3.5" aria-hidden />
|
|
96
96
|
Enroll machine
|
|
@@ -140,7 +140,7 @@ export function MachinesDashboard({
|
|
|
140
140
|
data-machines-retry
|
|
141
141
|
onClick={onRefresh}
|
|
142
142
|
disabled={loading}
|
|
143
|
-
className="inline-flex items-center gap-1.5 rounded-og-sm border border-og-status-failed/30 px-2 py-1 text-og-xs font-medium transition-colors hover:border-og-status-failed/50 disabled:cursor-not-allowed disabled:opacity-60 pointer-coarse:min-h-
|
|
143
|
+
className="inline-flex items-center gap-1.5 rounded-og-sm border border-og-status-failed/30 px-2 py-1 text-og-xs font-medium transition-colors hover:border-og-status-failed/50 disabled:cursor-not-allowed disabled:opacity-60 pointer-coarse:min-h-11"
|
|
144
144
|
>
|
|
145
145
|
<RefreshCwIcon className={cn("size-3.5", loading && "animate-og-spin")} aria-hidden />
|
|
146
146
|
{loading ? "Retrying…" : "Retry"}
|
|
@@ -998,10 +998,10 @@ function NoticeRow({ item }: { item: NoticeItem }) {
|
|
|
998
998
|
}
|
|
999
999
|
|
|
1000
1000
|
/**
|
|
1001
|
-
* The inline
|
|
1002
|
-
* affordance
|
|
1003
|
-
*
|
|
1004
|
-
*
|
|
1001
|
+
* The inline connection card: a lapsed connection surfaces as a calm, tappable
|
|
1002
|
+
* reconnect affordance, while an incompatible host-owned endpoint/resource
|
|
1003
|
+
* contract remains informative and non-actionable. The `reason` only shapes
|
|
1004
|
+
* the helper copy; no domain or enum code is shown as a label.
|
|
1005
1005
|
* `onReconnect` (from the app, which owns the SDK client) runs the flow; without
|
|
1006
1006
|
* it, a pre-minted authorization link is offered, or the card stays informative.
|
|
1007
1007
|
*/
|
|
@@ -1018,6 +1018,8 @@ function AuthNeededRow({
|
|
|
1018
1018
|
const [busy, setBusy] = useState(false);
|
|
1019
1019
|
const [failed, setFailed] = useState(false);
|
|
1020
1020
|
const provider = providerLabel(item.providerDomain);
|
|
1021
|
+
const unavailable =
|
|
1022
|
+
item.reason === "unsupported_auth" || item.reason === "resource_scope_unavailable";
|
|
1021
1023
|
|
|
1022
1024
|
const start = async () => {
|
|
1023
1025
|
if (!onReconnect || busy) {
|
|
@@ -1045,11 +1047,13 @@ function AuthNeededRow({
|
|
|
1045
1047
|
label={provider}
|
|
1046
1048
|
/>
|
|
1047
1049
|
<div className="min-w-0">
|
|
1048
|
-
<p className="truncate text-og-md font-medium text-og-fg">
|
|
1050
|
+
<p className="truncate text-og-md font-medium text-og-fg">
|
|
1051
|
+
{unavailable ? `${provider} tools unavailable` : `Reconnect ${provider}`}
|
|
1052
|
+
</p>
|
|
1049
1053
|
<p className="truncate text-og-sm text-og-fg-subtle">{authReasonLine(item.reason)}</p>
|
|
1050
1054
|
</div>
|
|
1051
1055
|
</div>
|
|
1052
|
-
{onReconnect ? (
|
|
1056
|
+
{!unavailable && onReconnect ? (
|
|
1053
1057
|
<button
|
|
1054
1058
|
type="button"
|
|
1055
1059
|
onClick={() => void start()}
|
|
@@ -1062,7 +1066,7 @@ function AuthNeededRow({
|
|
|
1062
1066
|
<RefreshCwIcon className={cn("size-3.5", busy && "animate-og-spin")} aria-hidden />
|
|
1063
1067
|
{busy ? "Reconnecting…" : "Reconnect"}
|
|
1064
1068
|
</button>
|
|
1065
|
-
) : item.authorizationUrl ? (
|
|
1069
|
+
) : !unavailable && item.authorizationUrl ? (
|
|
1066
1070
|
<a
|
|
1067
1071
|
href={item.authorizationUrl}
|
|
1068
1072
|
rel="noreferrer"
|
|
@@ -1160,6 +1164,10 @@ function authReasonLine(reason: AuthNeededItem["reason"]): string {
|
|
|
1160
1164
|
case "expired":
|
|
1161
1165
|
case "refresh_failed":
|
|
1162
1166
|
return "Its access expired.";
|
|
1167
|
+
case "unsupported_auth":
|
|
1168
|
+
return "This connection cannot authenticate the configured tool endpoint.";
|
|
1169
|
+
case "resource_scope_unavailable":
|
|
1170
|
+
return "This tool endpoint cannot enforce the selected repository access.";
|
|
1163
1171
|
default:
|
|
1164
1172
|
return "Its connection needs attention.";
|
|
1165
1173
|
}
|
|
@@ -12,6 +12,8 @@ export type ModelPickerProps = {
|
|
|
12
12
|
onChange: (modelId: string) => void;
|
|
13
13
|
disabled?: boolean | undefined;
|
|
14
14
|
className?: string | undefined;
|
|
15
|
+
/** Visible-to-assistive-technology label. Defaults to "Model". */
|
|
16
|
+
label?: string | undefined;
|
|
15
17
|
};
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -25,7 +27,14 @@ export type ModelPickerProps = {
|
|
|
25
27
|
* the send path. Renders nothing when no models are exposed, so a single-model
|
|
26
28
|
* deployment shows no chrome.
|
|
27
29
|
*/
|
|
28
|
-
export function ModelPicker({
|
|
30
|
+
export function ModelPicker({
|
|
31
|
+
models,
|
|
32
|
+
value,
|
|
33
|
+
onChange,
|
|
34
|
+
disabled,
|
|
35
|
+
className,
|
|
36
|
+
label = "Model",
|
|
37
|
+
}: ModelPickerProps) {
|
|
29
38
|
const selectId = useId();
|
|
30
39
|
// Group by provider, preserving first-seen order for both the providers and
|
|
31
40
|
// the models within each — the server already orders the list (default model
|
|
@@ -50,14 +59,14 @@ export function ModelPicker({ models, value, onChange, disabled, className }: Mo
|
|
|
50
59
|
return (
|
|
51
60
|
<span className={cn("relative inline-flex items-center", className)}>
|
|
52
61
|
<label htmlFor={selectId} className="sr-only">
|
|
53
|
-
|
|
62
|
+
{label}
|
|
54
63
|
</label>
|
|
55
64
|
<select
|
|
56
65
|
id={selectId}
|
|
57
66
|
value={value ?? ""}
|
|
58
67
|
onChange={(event) => onChange(event.target.value)}
|
|
59
68
|
disabled={disabled === true}
|
|
60
|
-
aria-label=
|
|
69
|
+
aria-label={label}
|
|
61
70
|
className={cn(
|
|
62
71
|
// Sized like the other footer controls; the chevron overlay needs the
|
|
63
72
|
// right padding so the value never collides with it.
|
|
@@ -78,6 +78,20 @@ export function PierreDiff({
|
|
|
78
78
|
className,
|
|
79
79
|
}: PierreDiffProps) {
|
|
80
80
|
const [failed, setFailed] = useState(false);
|
|
81
|
+
const [forcedColors, setForcedColors] = useState(false);
|
|
82
|
+
|
|
83
|
+
// Pierre's rich renderer distinguishes additions/deletions primarily through
|
|
84
|
+
// foreground and background colors inside a shadow root. In Windows forced-
|
|
85
|
+
// colors mode those paints intentionally collapse, so expose the literal
|
|
86
|
+
// unified patch (+/- markers) instead of leaving changed lines ambiguous.
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
|
|
89
|
+
const query = window.matchMedia("(forced-colors: active)");
|
|
90
|
+
const update = () => setForcedColors(query.matches);
|
|
91
|
+
update();
|
|
92
|
+
query.addEventListener?.("change", update);
|
|
93
|
+
return () => query.removeEventListener?.("change", update);
|
|
94
|
+
}, []);
|
|
81
95
|
|
|
82
96
|
// Probe the import once so a hard failure (peer missing) shows `fallback`
|
|
83
97
|
// rather than a Suspense boundary that never resolves. Skipped when `plain`.
|
|
@@ -92,11 +106,18 @@ export function PierreDiff({
|
|
|
92
106
|
};
|
|
93
107
|
}, [plain]);
|
|
94
108
|
|
|
95
|
-
if (plain || failed) {
|
|
109
|
+
if (plain || failed || forcedColors) {
|
|
96
110
|
// `plain` opts out of highlighting; `failed` = `@pierre/diffs` not installed.
|
|
97
111
|
// Render the caller's fallback, or a plain (unhighlighted) patch dump — NOT a
|
|
98
112
|
// second hunk renderer. One highlighted renderer (Pierre) + a text degrade.
|
|
99
|
-
return
|
|
113
|
+
return (
|
|
114
|
+
<div
|
|
115
|
+
className={className}
|
|
116
|
+
data-opengeni-plain-diff={forcedColors ? "forced-colors" : "fallback"}
|
|
117
|
+
>
|
|
118
|
+
{fallback ?? <PlainPatch diff={diff} />}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
100
121
|
}
|
|
101
122
|
|
|
102
123
|
// Default to the dark theme: the host UI is dark-first, and Pierre's own
|
|
@@ -131,13 +152,15 @@ export function PierreDiff({
|
|
|
131
152
|
"--diffs-fg-number-addition-override": "var(--og-color-status-idle)",
|
|
132
153
|
"--diffs-fg-number-deletion-override": "var(--og-color-status-failed)",
|
|
133
154
|
"--diffs-bg-addition-override":
|
|
134
|
-
"color-mix(in
|
|
155
|
+
"color-mix(in oklab, var(--og-color-status-idle) 12%, var(--og-color-bg))",
|
|
135
156
|
"--diffs-bg-deletion-override":
|
|
136
|
-
"color-mix(in
|
|
157
|
+
"color-mix(in oklab, var(--og-color-status-failed) 12%, var(--og-color-bg))",
|
|
137
158
|
"--diffs-bg-addition-emphasis-override":
|
|
138
|
-
"color-mix(in
|
|
159
|
+
"color-mix(in oklab, var(--og-color-status-idle) 22%, var(--og-color-bg))",
|
|
139
160
|
"--diffs-bg-deletion-emphasis-override":
|
|
140
|
-
"color-mix(in
|
|
161
|
+
"color-mix(in oklab, var(--og-color-status-failed) 22%, var(--og-color-bg))",
|
|
162
|
+
"--diffs-header-font-family": "var(--og-font-sans)",
|
|
163
|
+
"--diffs-font-family": "var(--og-font-mono)",
|
|
141
164
|
"--diffs-font-size": "var(--og-code-font-size)",
|
|
142
165
|
"--diffs-line-height": "var(--og-code-line-height)",
|
|
143
166
|
} as CSSProperties;
|
|
@@ -177,7 +200,10 @@ function PlainPatch({ diff }: { diff: GitFileDiff[] }) {
|
|
|
177
200
|
{diff.map((file) => (
|
|
178
201
|
<pre
|
|
179
202
|
key={file.path}
|
|
203
|
+
aria-label={`Diff for ${file.path}`}
|
|
180
204
|
className="mb-2 overflow-auto whitespace-pre rounded-og-sm border border-og-border bg-og-bg/60 p-2.5 font-og-mono text-og-xs leading-5"
|
|
205
|
+
role="region"
|
|
206
|
+
tabIndex={0}
|
|
181
207
|
>
|
|
182
208
|
{gitFileDiffToPatch(file)
|
|
183
209
|
.split("\n")
|