@opengeni/react 0.15.0 → 0.23.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 (103) hide show
  1. package/README.md +110 -10
  2. package/dist/{chunk-TOJR776I.js → chunk-6XUS5VFM.js} +45 -369
  3. package/dist/chunk-6XUS5VFM.js.map +1 -0
  4. package/dist/chunk-CATATY77.js +2393 -0
  5. package/dist/chunk-CATATY77.js.map +1 -0
  6. package/dist/chunk-I3BJZIG5.js +439 -0
  7. package/dist/chunk-I3BJZIG5.js.map +1 -0
  8. package/dist/chunk-TK7G6XLT.js +18 -0
  9. package/dist/chunk-TK7G6XLT.js.map +1 -0
  10. package/dist/chunk-UAHPKLB6.js +2237 -0
  11. package/dist/chunk-UAHPKLB6.js.map +1 -0
  12. package/dist/chunk-UQS7OQDE.js +591 -0
  13. package/dist/chunk-UQS7OQDE.js.map +1 -0
  14. package/dist/chunk-YDNWMKXO.js +102 -0
  15. package/dist/chunk-YDNWMKXO.js.map +1 -0
  16. package/dist/composer-CVm5uoUI.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 +7140 -8817
  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-3UW3MIIR.js +890 -0
  30. package/dist/queue-surface-implementation-3UW3MIIR.js.map +1 -0
  31. package/dist/session-Db00TRwl.d.ts +446 -0
  32. package/dist/session-context-rgrfElMl.d.ts +58 -0
  33. package/dist/session.d.ts +4 -0
  34. package/dist/session.js +54 -0
  35. package/dist/session.js.map +1 -0
  36. package/dist/use-composer-By1wn3MM.d.ts +99 -0
  37. package/package.json +20 -4
  38. package/src/approvals.ts +5 -3
  39. package/src/client.ts +44 -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 +205 -25
  53. package/src/components/model-picker.tsx +12 -3
  54. package/src/components/pierre-diff.tsx +32 -6
  55. package/src/components/queue-draft-policy.ts +18 -0
  56. package/src/components/queue-surface-implementation.tsx +1037 -0
  57. package/src/components/queue-surface-state.tsx +94 -0
  58. package/src/components/queue-surface.tsx +42 -559
  59. package/src/components/sandbox-files.tsx +138 -14
  60. package/src/components/sandbox-terminal.tsx +8 -2
  61. package/src/components/sandbox-workspace.tsx +339 -119
  62. package/src/components/workbench-changes.tsx +84 -33
  63. package/src/components/workspace-dock.tsx +188 -18
  64. package/src/composer.ts +60 -0
  65. package/src/hooks/internal.ts +115 -34
  66. package/src/hooks/use-composer.ts +385 -104
  67. package/src/hooks/use-human-input.ts +131 -0
  68. package/src/hooks/use-machine-chip.ts +2 -2
  69. package/src/hooks/use-machines.ts +27 -11
  70. package/src/hooks/use-sandbox-files.ts +310 -62
  71. package/src/hooks/use-sandbox-git.ts +154 -40
  72. package/src/hooks/use-sandbox-terminal.ts +28 -6
  73. package/src/hooks/use-session-capabilities.ts +38 -9
  74. package/src/hooks/use-session-control.ts +37 -8
  75. package/src/hooks/use-session-events.ts +522 -48
  76. package/src/hooks/use-session-mcp-approval-policy.ts +161 -0
  77. package/src/hooks/use-slash-commands.ts +38 -38
  78. package/src/hooks/use-transcription.ts +757 -0
  79. package/src/hooks/use-turn-queue.ts +134 -66
  80. package/src/hooks/use-windowed-sections.ts +2 -2
  81. package/src/hooks/use-workspace-capture.ts +146 -40
  82. package/src/hooks/use-workspace-edit.ts +50 -14
  83. package/src/hooks/use-workspace-sessions.ts +3 -0
  84. package/src/human-input.ts +72 -0
  85. package/src/index.ts +85 -6
  86. package/src/lib/noto-sans-arabic-loader.ts +21 -0
  87. package/src/lib/noto-sans-jp-loader.generated.ts +295 -0
  88. package/src/lib/use-portal-token-style.ts +51 -0
  89. package/src/lib/use-unicode-fonts.ts +57 -0
  90. package/src/provider.tsx +4 -69
  91. package/src/session-context.ts +160 -0
  92. package/src/session.ts +89 -0
  93. package/src/timeline/parsers.ts +35 -0
  94. package/src/timeline/projection.ts +12 -4
  95. package/src/timeline/tool-renderers.tsx +41 -0
  96. package/src/timeline/turn-summary.tsx +2 -2
  97. package/src/timeline/types.ts +10 -9
  98. package/styles/index.css +56 -0
  99. package/styles/index.d.ts +2 -0
  100. package/styles/tokens.css +11 -6
  101. package/styles/tokens.d.ts +2 -0
  102. package/dist/chunk-TOJR776I.js.map +0 -1
  103. package/dist/machines-BpdwuQcD.d.ts +0 -449
@@ -3,8 +3,6 @@ import {
3
3
  type ComponentType,
4
4
  type CSSProperties,
5
5
  type ReactNode,
6
- lazy,
7
- Suspense,
8
6
  useCallback,
9
7
  useEffect,
10
8
  useMemo,
@@ -92,13 +90,6 @@ export function languageForPath(path: string): keyof typeof LANGUAGE_LOADERS | n
92
90
  }
93
91
  }
94
92
 
95
- const LazyCodeMirror = lazy(async () => {
96
- const mod = (await import("@uiw/react-codemirror")) as unknown as {
97
- default: ReactCodeMirrorComponent;
98
- };
99
- return { default: mod.default };
100
- });
101
-
102
93
  export type CodeEditorProps = {
103
94
  /** Workspace-relative path — drives language inference (and the save target upstream). */
104
95
  path: string;
@@ -106,6 +97,10 @@ export type CodeEditorProps = {
106
97
  initialContents: string;
107
98
  /** Persist the current buffer. Resolves when the write lands; rejects to surface an error. */
108
99
  onSave: (contents: string) => Promise<unknown>;
100
+ /** Explicitly overwrite after a visible expected-content conflict. */
101
+ onOverwrite?: ((contents: string) => Promise<unknown>) | undefined;
102
+ /** Re-read the selected file after a visible save conflict. */
103
+ onReload?: (() => void) | undefined;
109
104
  /** Fired once when the buffer FIRST diverges from its baseline (the first real
110
105
  * keystroke) — the wake-on-edit intent. The host warms the box on this so the
111
106
  * eventual save lands fast; merely opening the file for reading never fires it. */
@@ -136,6 +131,8 @@ export function CodeEditor({
136
131
  path,
137
132
  initialContents,
138
133
  onSave,
134
+ onOverwrite,
135
+ onReload,
139
136
  onEditIntent,
140
137
  readOnly = false,
141
138
  themeType = "dark",
@@ -156,6 +153,8 @@ export function CodeEditor({
156
153
  const [savedTick, setSavedTick] = useState(false);
157
154
 
158
155
  const dirty = value !== baseline;
156
+ const saveConflict =
157
+ saveError !== null && (saveError as Error & { code?: unknown }).code === "file_write_conflict";
159
158
 
160
159
  // Reload the buffer when the file identity changes. Guard on initialContents
161
160
  // too so an external refresh of the SAME path (e.g. fs.changed re-read) reseeds
@@ -201,7 +200,16 @@ export function CodeEditor({
201
200
  keymap: { of: (binds: unknown[]) => unknown };
202
201
  Prec: { highest: (ext: unknown) => unknown };
203
202
  };
204
- const languageExtension = langKey ? await LANGUAGE_LOADERS[langKey]?.() : null;
203
+ // Language peers are optional. A missing grammar must degrade to a plain
204
+ // CodeMirror document, not throw the entire editor down to textarea.
205
+ let languageExtension: unknown | null = null;
206
+ if (langKey) {
207
+ try {
208
+ languageExtension = (await LANGUAGE_LOADERS[langKey]?.()) ?? null;
209
+ } catch {
210
+ languageExtension = null;
211
+ }
212
+ }
205
213
  if (cancelled) return;
206
214
  // A high-precedence Cmd/Ctrl-S keymap that calls back into the latest
207
215
  // save handler (kept fresh via a ref) and swallows the browser's
@@ -222,8 +230,9 @@ export function CodeEditor({
222
230
  setBundle({
223
231
  Editor: cmMod.default,
224
232
  saveKeymapExtension,
225
- languageExtension: languageExtension ?? null,
233
+ languageExtension,
226
234
  });
235
+ setFailed(false);
227
236
  } catch {
228
237
  if (!cancelled) setFailed(true);
229
238
  }
@@ -256,6 +265,25 @@ export function CodeEditor({
256
265
  }
257
266
  }, [readOnly, value, baseline, onSave, path]);
258
267
 
268
+ const overwrite = useCallback(async () => {
269
+ if (readOnly || !onOverwrite) return;
270
+ const snapshot = value;
271
+ if (snapshot === baseline) return;
272
+ setSaving(true);
273
+ setSaveError(null);
274
+ setSavedTick(false);
275
+ try {
276
+ await onOverwrite(snapshot);
277
+ setBaseline(snapshot);
278
+ lastSeed.current = { path, contents: snapshot };
279
+ setSavedTick(true);
280
+ } catch (cause) {
281
+ setSaveError(cause instanceof Error ? cause : new Error(String(cause)));
282
+ } finally {
283
+ setSaving(false);
284
+ }
285
+ }, [readOnly, onOverwrite, value, baseline, path]);
286
+
259
287
  saveRef.current = () => {
260
288
  void save();
261
289
  };
@@ -276,13 +304,7 @@ export function CodeEditor({
276
304
  return exts;
277
305
  }, [bundle]);
278
306
 
279
- if (failed) {
280
- return (
281
- <div className={cn("flex h-full min-h-0 flex-col", className)}>
282
- {fallback ?? <PlainTextarea value={value} readOnly={readOnly} onChange={noteEdit} />}
283
- </div>
284
- );
285
- }
307
+ const Editor = bundle?.Editor;
286
308
 
287
309
  return (
288
310
  <div
@@ -291,7 +313,7 @@ export function CodeEditor({
291
313
  style={editorVars}
292
314
  >
293
315
  {/* Save bar: dirty indicator + status + the explicit Save button. */}
294
- <div className="flex shrink-0 items-center gap-2 border-b border-og-border bg-og-surface-1 px-2 py-1">
316
+ <div className="flex min-h-9 shrink-0 items-center gap-2 border-b border-og-border bg-og-surface-1 px-2 py-1">
295
317
  <span
296
318
  className={cn(
297
319
  "size-1.5 shrink-0 rounded-full transition-colors",
@@ -304,7 +326,7 @@ export function CodeEditor({
304
326
  {dirty && !readOnly ? " •" : ""}
305
327
  </span>
306
328
  <div className="ml-auto flex shrink-0 items-center gap-2">
307
- {saveError && (
329
+ {saveError && !saveConflict && (
308
330
  <span
309
331
  className="max-w-[220px] truncate text-og-xs text-og-status-failed"
310
332
  title={saveError.message}
@@ -315,13 +337,13 @@ export function CodeEditor({
315
337
  {!saveError && savedTick && <span className="text-og-xs text-og-status-idle">Saved</span>}
316
338
  {readOnly ? (
317
339
  <span className="text-og-xs uppercase tracking-wide text-og-fg-subtle">Read-only</span>
318
- ) : (
340
+ ) : !saveConflict ? (
319
341
  <button
320
342
  type="button"
321
343
  onClick={() => void save()}
322
344
  disabled={saving || !dirty}
323
345
  className={cn(
324
- "flex items-center gap-1 rounded-og-sm border border-og-border px-1.5 py-0.5 text-og-xs pointer-coarse:min-h-10",
346
+ "flex items-center gap-1 rounded-og-sm border border-og-border px-1.5 py-0.5 text-og-xs pointer-coarse:min-h-11",
325
347
  saving || !dirty
326
348
  ? "cursor-default text-og-fg-subtle opacity-60"
327
349
  : "text-og-fg hover:bg-og-accent-soft",
@@ -335,14 +357,59 @@ export function CodeEditor({
335
357
  )}
336
358
  Save
337
359
  </button>
338
- )}
360
+ ) : null}
339
361
  </div>
340
362
  </div>
341
363
 
364
+ {saveConflict ? (
365
+ <div
366
+ className="flex shrink-0 flex-wrap items-center gap-x-3 gap-y-2 border-b border-og-status-failed/25 bg-og-status-failed/8 px-2 py-2"
367
+ role="alert"
368
+ >
369
+ <div className="min-w-[12rem] flex-1 text-og-xs text-og-fg-muted">
370
+ <span className="font-medium text-og-status-failed">File changed on machine.</span>{" "}
371
+ Reloading discards your edits; overwriting replaces the live version.
372
+ </div>
373
+ <div className="ml-auto flex shrink-0 items-center gap-1.5">
374
+ {onReload ? (
375
+ <button
376
+ type="button"
377
+ onClick={() => {
378
+ setSaveError(null);
379
+ onReload();
380
+ }}
381
+ className="min-h-8 rounded-og-sm border border-og-border bg-og-surface-1 px-2 text-og-xs font-medium text-og-fg hover:bg-og-surface-2 pointer-coarse:min-h-11"
382
+ >
383
+ Reload live
384
+ </button>
385
+ ) : null}
386
+ {onOverwrite ? (
387
+ <button
388
+ type="button"
389
+ onClick={() => void overwrite()}
390
+ disabled={saving}
391
+ className="min-h-8 rounded-og-sm border border-og-status-failed/40 px-2 text-og-xs font-medium text-og-status-failed hover:bg-og-status-failed/10 disabled:opacity-50 pointer-coarse:min-h-11"
392
+ >
393
+ Overwrite
394
+ </button>
395
+ ) : null}
396
+ </div>
397
+ </div>
398
+ ) : null}
399
+
342
400
  {/* The editor surface. */}
343
401
  <div className="min-h-0 flex-1 overflow-auto">
344
- <Suspense fallback={loading ?? <EditorSkeleton />}>
345
- <LazyCodeMirror
402
+ {failed ? (
403
+ (fallback ?? (
404
+ <PlainTextarea
405
+ value={value}
406
+ readOnly={readOnly}
407
+ onChange={noteEdit}
408
+ onSave={() => void save()}
409
+ />
410
+ ))
411
+ ) : Editor ? (
412
+ <Editor
346
413
  value={value}
347
414
  theme={themeType === "light" ? "light" : "dark"}
348
415
  editable={!readOnly}
@@ -353,7 +420,9 @@ export function CodeEditor({
353
420
  className="og-cm-editor min-h-full text-og-sm"
354
421
  onChange={readOnly ? undefined : noteEdit}
355
422
  />
356
- </Suspense>
423
+ ) : (
424
+ (loading ?? <EditorSkeleton />)
425
+ )}
357
426
  </div>
358
427
  </div>
359
428
  );
@@ -369,10 +438,12 @@ function PlainTextarea({
369
438
  value,
370
439
  readOnly,
371
440
  onChange,
441
+ onSave,
372
442
  }: {
373
443
  value: string;
374
444
  readOnly: boolean;
375
445
  onChange: (value: string) => void;
446
+ onSave: () => void;
376
447
  }) {
377
448
  return (
378
449
  <textarea
@@ -380,6 +451,13 @@ function PlainTextarea({
380
451
  readOnly={readOnly}
381
452
  spellCheck={false}
382
453
  onChange={(e) => onChange(e.target.value)}
454
+ onKeyDown={(event) => {
455
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "s") {
456
+ event.preventDefault();
457
+ onSave();
458
+ }
459
+ }}
460
+ aria-label="Plain text file editor"
383
461
  className="h-full w-full resize-none bg-transparent p-2 font-og-mono text-og-sm text-og-fg outline-none"
384
462
  />
385
463
  );
@@ -15,6 +15,10 @@ export type CommandPaletteProps = {
15
15
  argHintText: string;
16
16
  /** id used for aria-activedescendant wiring from the textarea. */
17
17
  listboxId: string;
18
+ /** Accessible list label. Defaults to "Slash commands". */
19
+ label?: string | undefined;
20
+ /** Badge shown for destructive commands. Defaults to "danger". */
21
+ dangerLabel?: string | undefined;
18
22
  };
19
23
 
20
24
  /**
@@ -31,6 +35,8 @@ export function CommandPalette({
31
35
  onRun,
32
36
  argHintText,
33
37
  listboxId,
38
+ label = "Slash commands",
39
+ dangerLabel = "danger",
34
40
  }: CommandPaletteProps) {
35
41
  return (
36
42
  <AnimatePresence>
@@ -48,7 +54,7 @@ export function CommandPalette({
48
54
  <ul
49
55
  id={listboxId}
50
56
  role="listbox"
51
- aria-label="Slash commands"
57
+ aria-label={label}
52
58
  className="max-h-72 overflow-y-auto py-1"
53
59
  >
54
60
  {items.map((command, index) => {
@@ -92,7 +98,7 @@ export function CommandPalette({
92
98
  <span className="ml-auto flex items-center gap-1.5">
93
99
  {command.danger ? (
94
100
  <span className="rounded-og-xs bg-og-status-failed/15 px-1 text-[10px] uppercase tracking-wide text-og-status-failed">
95
- danger
101
+ {dangerLabel}
96
102
  </span>
97
103
  ) : null}
98
104
  <span className="truncate text-[12px] text-og-fg-subtle max-sm:hidden">
@@ -0,0 +1,211 @@
1
+ import type {
2
+ TranscriptionAdapter,
3
+ TranscriptionDiagnostic,
4
+ TranscriptionErrorCode,
5
+ TranscriptionTargetSelection,
6
+ WorkspaceTranscriptionPolicy,
7
+ } from "@opengeni/sdk";
8
+ import { LoaderCircleIcon, MicIcon, RotateCwIcon, SquareIcon } from "lucide-react";
9
+ import { type MouseEvent } from "react";
10
+ import { cn } from "../lib/cn";
11
+ import { useTranscription, type TranscriptionLifecycleTimeouts } from "../hooks/use-transcription";
12
+ import { useChatComposer } from "./composer";
13
+
14
+ export type ComposerTranscriptionMessages = {
15
+ start: string;
16
+ stop: string;
17
+ retry: string;
18
+ requestingPermission: string;
19
+ listening: string;
20
+ reconnecting: string;
21
+ cancelling: string;
22
+ unavailableDisabled: string;
23
+ unavailableAdapter: string;
24
+ unavailablePolicy: string;
25
+ errorPermissionDenied: string;
26
+ errorNotSupported: string;
27
+ errorNetwork: string;
28
+ errorProvider: string;
29
+ errorPolicyBlocked: string;
30
+ errorTimeout: string;
31
+ errorCancelled: string;
32
+ errorUnknown: string;
33
+ };
34
+
35
+ const defaultMessages: ComposerTranscriptionMessages = {
36
+ start: "Start voice input",
37
+ stop: "Stop voice input",
38
+ retry: "Retry voice input",
39
+ requestingPermission: "Requesting microphone permission…",
40
+ listening: "Listening… Press Escape to cancel.",
41
+ reconnecting: "Reconnecting voice input…",
42
+ cancelling: "Cancelling voice input…",
43
+ unavailableDisabled: "Voice input is unavailable while the composer is disabled.",
44
+ unavailableAdapter: "Voice input needs an approved transcription adapter.",
45
+ unavailablePolicy: "Enable and accept transcription in Workspace settings.",
46
+ errorPermissionDenied: "Microphone permission was denied. Your draft was not changed.",
47
+ errorNotSupported: "Voice input is not supported on this device.",
48
+ errorNetwork: "Voice input lost its network connection.",
49
+ errorProvider: "The transcription service could not continue.",
50
+ errorPolicyBlocked: "Workspace policy does not authorize voice input.",
51
+ errorTimeout: "Voice input took too long to start. Try again.",
52
+ errorCancelled: "Voice input was cancelled.",
53
+ errorUnknown: "Voice input could not start. Try again.",
54
+ };
55
+
56
+ export type ComposerTranscriptionControlProps = {
57
+ adapter: TranscriptionAdapter | null;
58
+ policy: WorkspaceTranscriptionPolicy;
59
+ selection?: TranscriptionTargetSelection | undefined;
60
+ messages?: Partial<ComposerTranscriptionMessages> | undefined;
61
+ className?: string | undefined;
62
+ lifecycleTimeouts?: Partial<TranscriptionLifecycleTimeouts> | undefined;
63
+ onDiagnostic?: ((diagnostic: TranscriptionDiagnostic) => void) | undefined;
64
+ };
65
+
66
+ /** One provider-neutral microphone control for the nearest editable composer. */
67
+ export function ComposerTranscriptionControl({
68
+ adapter,
69
+ policy,
70
+ selection,
71
+ messages: overrides,
72
+ className,
73
+ lifecycleTimeouts,
74
+ onDiagnostic,
75
+ }: ComposerTranscriptionControlProps) {
76
+ const composer = useChatComposer();
77
+ const messages = { ...defaultMessages, ...overrides };
78
+ const transcription = useTranscription({
79
+ adapter,
80
+ policy,
81
+ ...(selection ? { selection } : {}),
82
+ value: composer.value,
83
+ setValue: composer.setValue,
84
+ focusInput: composer.focusInput,
85
+ disabled: composer.disabled,
86
+ lifecycleTimeouts,
87
+ onDiagnostic,
88
+ });
89
+ const status = transcription.state.status;
90
+ const active =
91
+ status === "requesting-permission" ||
92
+ status === "listening" ||
93
+ status === "reconnecting" ||
94
+ status === "cancelling";
95
+ const unavailableMessage =
96
+ transcription.unavailableReason === "composer_disabled"
97
+ ? messages.unavailableDisabled
98
+ : transcription.unavailableReason === "adapter_missing"
99
+ ? messages.unavailableAdapter
100
+ : transcription.unavailableReason
101
+ ? messages.unavailablePolicy
102
+ : null;
103
+ const label = unavailableMessage
104
+ ? unavailableMessage
105
+ : status === "error"
106
+ ? messages.retry
107
+ : active
108
+ ? messages.stop
109
+ : messages.start;
110
+ const errorMessage = transcription.state.error
111
+ ? transcriptionErrorMessage(transcription.state.error.code, messages)
112
+ : null;
113
+ const announcement =
114
+ transcription.state.partial ||
115
+ (status === "requesting-permission"
116
+ ? messages.requestingPermission
117
+ : status === "listening"
118
+ ? messages.listening
119
+ : status === "reconnecting"
120
+ ? (errorMessage ?? messages.reconnecting)
121
+ : status === "cancelling"
122
+ ? messages.cancelling
123
+ : status === "error"
124
+ ? (errorMessage ?? messages.errorUnknown)
125
+ : unavailableMessage);
126
+
127
+ function activate(event: MouseEvent<HTMLButtonElement>) {
128
+ if (unavailableMessage || status === "cancelling") {
129
+ event.preventDefault();
130
+ return;
131
+ }
132
+ if (active) void transcription.cancel();
133
+ else void transcription.start();
134
+ }
135
+
136
+ return (
137
+ <span
138
+ className={cn("inline-flex min-w-0 items-center gap-1.5", className)}
139
+ data-transcription-status={status}
140
+ >
141
+ <button
142
+ type="button"
143
+ onClick={activate}
144
+ aria-label={label}
145
+ aria-pressed={active}
146
+ aria-disabled={unavailableMessage !== null || status === "cancelling"}
147
+ aria-keyshortcuts={active ? "Escape" : undefined}
148
+ title={label}
149
+ className={cn(
150
+ "inline-flex size-8 shrink-0 items-center justify-center rounded-og-md pointer-coarse:size-11",
151
+ "text-og-fg-muted transition-colors duration-150 motion-reduce:transition-none",
152
+ unavailableMessage
153
+ ? "cursor-not-allowed opacity-45"
154
+ : status === "cancelling"
155
+ ? "cursor-not-allowed"
156
+ : "hover:bg-og-surface-2 hover:text-og-fg",
157
+ status === "listening" && "bg-og-status-failed/10 text-og-status-failed",
158
+ )}
159
+ >
160
+ {status === "requesting-permission" || status === "cancelling" ? (
161
+ <LoaderCircleIcon className="size-4 animate-og-spin motion-reduce:animate-none" />
162
+ ) : status === "reconnecting" ? (
163
+ <RotateCwIcon className="size-4 animate-og-spin motion-reduce:animate-none" />
164
+ ) : active ? (
165
+ <SquareIcon className="size-3.5 fill-current" />
166
+ ) : (
167
+ <MicIcon className="size-4" />
168
+ )}
169
+ </button>
170
+ {announcement && (active || status === "error") ? (
171
+ <span
172
+ aria-hidden="true"
173
+ title={announcement}
174
+ className={cn(
175
+ "max-w-48 truncate text-og-xs text-og-fg-muted max-sm:max-w-28",
176
+ status === "error" && "text-og-status-failed",
177
+ )}
178
+ >
179
+ {announcement}
180
+ </span>
181
+ ) : null}
182
+ <span className="sr-only" role={status === "error" ? "alert" : "status"} aria-live="polite">
183
+ {announcement}
184
+ </span>
185
+ </span>
186
+ );
187
+ }
188
+
189
+ function transcriptionErrorMessage(
190
+ code: TranscriptionErrorCode,
191
+ messages: ComposerTranscriptionMessages,
192
+ ): string {
193
+ switch (code) {
194
+ case "permission_denied":
195
+ return messages.errorPermissionDenied;
196
+ case "not_supported":
197
+ return messages.errorNotSupported;
198
+ case "network":
199
+ return messages.errorNetwork;
200
+ case "provider":
201
+ return messages.errorProvider;
202
+ case "policy_blocked":
203
+ return messages.errorPolicyBlocked;
204
+ case "timeout":
205
+ return messages.errorTimeout;
206
+ case "cancelled":
207
+ return messages.errorCancelled;
208
+ case "unknown":
209
+ return messages.errorUnknown;
210
+ }
211
+ }