@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.
Files changed (101) hide show
  1. package/README.md +110 -10
  2. package/dist/{chunk-TOJR776I.js → chunk-6BXBGJ3B.js} +45 -369
  3. package/dist/chunk-6BXBGJ3B.js.map +1 -0
  4. package/dist/chunk-6UT5OC3P.js +2133 -0
  5. package/dist/chunk-6UT5OC3P.js.map +1 -0
  6. package/dist/chunk-EBTT3MYN.js +92 -0
  7. package/dist/chunk-EBTT3MYN.js.map +1 -0
  8. package/dist/chunk-HHFA4AFX.js +425 -0
  9. package/dist/chunk-HHFA4AFX.js.map +1 -0
  10. package/dist/chunk-LAZ2A743.js +2393 -0
  11. package/dist/chunk-LAZ2A743.js.map +1 -0
  12. package/dist/chunk-MRSECXRP.js +563 -0
  13. package/dist/chunk-MRSECXRP.js.map +1 -0
  14. package/dist/chunk-TK7G6XLT.js +18 -0
  15. package/dist/chunk-TK7G6XLT.js.map +1 -0
  16. package/dist/composer-DvUaa5ki.d.ts +585 -0
  17. package/dist/composer.d.ts +6 -0
  18. package/dist/composer.js +56 -0
  19. package/dist/composer.js.map +1 -0
  20. package/dist/index.d.ts +1288 -1785
  21. package/dist/index.js +7015 -8806
  22. package/dist/index.js.map +1 -1
  23. package/dist/machines.d.ts +412 -3
  24. package/dist/machines.js +3 -1
  25. package/dist/noto-sans-arabic-loader-IA2T4X2A.js +21 -0
  26. package/dist/noto-sans-arabic-loader-IA2T4X2A.js.map +1 -0
  27. package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js +292 -0
  28. package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js.map +1 -0
  29. package/dist/queue-surface-implementation-NQMV2ML3.js +890 -0
  30. package/dist/queue-surface-implementation-NQMV2ML3.js.map +1 -0
  31. package/dist/session-Gd4iVPYw.d.ts +425 -0
  32. package/dist/session-context-DIFFlXKc.d.ts +52 -0
  33. package/dist/session.d.ts +4 -0
  34. package/dist/session.js +50 -0
  35. package/dist/session.js.map +1 -0
  36. package/dist/use-composer-BCfm4mzX.d.ts +112 -0
  37. package/package.json +20 -4
  38. package/src/approvals.ts +5 -3
  39. package/src/client.ts +40 -0
  40. package/src/components/approval-surface.tsx +198 -0
  41. package/src/components/chat-composer.tsx +94 -993
  42. package/src/components/code-editor.tsx +104 -26
  43. package/src/components/command-palette.tsx +8 -2
  44. package/src/components/composer-transcription-control.tsx +211 -0
  45. package/src/components/composer.tsx +1536 -0
  46. package/src/components/desktop-viewer.tsx +3 -3
  47. package/src/components/enrollment-consent.tsx +2 -2
  48. package/src/components/file-browser.tsx +367 -29
  49. package/src/components/human-input-form.tsx +417 -0
  50. package/src/components/machine-card.tsx +1 -1
  51. package/src/components/machines-dashboard.tsx +3 -3
  52. package/src/components/message-timeline.tsx +15 -7
  53. package/src/components/model-picker.tsx +12 -3
  54. package/src/components/pierre-diff.tsx +32 -6
  55. package/src/components/queue-surface-implementation.tsx +1041 -0
  56. package/src/components/queue-surface-state.tsx +94 -0
  57. package/src/components/queue-surface.tsx +41 -559
  58. package/src/components/sandbox-files.tsx +138 -14
  59. package/src/components/sandbox-terminal.tsx +8 -2
  60. package/src/components/sandbox-workspace.tsx +339 -119
  61. package/src/components/workbench-changes.tsx +84 -33
  62. package/src/components/workspace-dock.tsx +188 -18
  63. package/src/composer.ts +60 -0
  64. package/src/hooks/internal.ts +115 -34
  65. package/src/hooks/use-composer.ts +335 -104
  66. package/src/hooks/use-human-input.ts +131 -0
  67. package/src/hooks/use-machine-chip.ts +2 -2
  68. package/src/hooks/use-machines.ts +27 -11
  69. package/src/hooks/use-sandbox-files.ts +310 -62
  70. package/src/hooks/use-sandbox-git.ts +154 -40
  71. package/src/hooks/use-sandbox-terminal.ts +28 -6
  72. package/src/hooks/use-session-capabilities.ts +38 -9
  73. package/src/hooks/use-session-control.ts +37 -8
  74. package/src/hooks/use-session-events.ts +522 -48
  75. package/src/hooks/use-slash-commands.ts +38 -38
  76. package/src/hooks/use-transcription.ts +757 -0
  77. package/src/hooks/use-turn-queue.ts +134 -66
  78. package/src/hooks/use-windowed-sections.ts +2 -2
  79. package/src/hooks/use-workspace-capture.ts +146 -40
  80. package/src/hooks/use-workspace-edit.ts +50 -14
  81. package/src/hooks/use-workspace-sessions.ts +3 -0
  82. package/src/human-input.ts +72 -0
  83. package/src/index.ts +76 -6
  84. package/src/lib/noto-sans-arabic-loader.ts +21 -0
  85. package/src/lib/noto-sans-jp-loader.generated.ts +295 -0
  86. package/src/lib/use-portal-token-style.ts +51 -0
  87. package/src/lib/use-unicode-fonts.ts +57 -0
  88. package/src/provider.tsx +4 -69
  89. package/src/session-context.ts +135 -0
  90. package/src/session.ts +79 -0
  91. package/src/timeline/parsers.ts +35 -0
  92. package/src/timeline/projection.ts +12 -4
  93. package/src/timeline/tool-renderers.tsx +41 -0
  94. package/src/timeline/turn-summary.tsx +2 -2
  95. package/src/timeline/types.ts +16 -8
  96. package/styles/index.css +56 -0
  97. package/styles/index.d.ts +2 -0
  98. package/styles/tokens.css +11 -6
  99. package/styles/tokens.d.ts +2 -0
  100. package/dist/chunk-TOJR776I.js.map +0 -1
  101. 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-10",
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-10"
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-10"
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-10"
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 reconnect card: a lapsed connection surfaces as a calm, tappable
1002
- * affordance — provider logo, one human line, one primary Reconnect button —
1003
- * instead of a raw "linear.app needs to be reconnected." string. The `reason`
1004
- * only shapes the helper copy; no domain or enum code is shown as a label.
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">Reconnect {provider}</p>
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({ models, value, onChange, disabled, className }: ModelPickerProps) {
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
- Model
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="Model"
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 <div className={className}>{fallback ?? <PlainPatch diff={diff} />}</div>;
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 oklch, var(--og-color-status-idle) 12%, var(--og-color-bg))",
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 oklch, var(--og-color-status-failed) 12%, var(--og-color-bg))",
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 oklch, var(--og-color-status-idle) 22%, var(--og-color-bg))",
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 oklch, var(--og-color-status-failed) 22%, var(--og-color-bg))",
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")