@opengeni/react 0.29.0 → 0.32.1

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 (207) hide show
  1. package/dist/approvals.d.ts +14 -0
  2. package/dist/{chunk-M7X4JZOD.js → chunk-4QOQ2DSR.js} +8 -1
  3. package/dist/chunk-4QOQ2DSR.js.map +1 -0
  4. package/dist/{chunk-HQFPO7VX.js → chunk-7CJOAW3V.js} +24 -3
  5. package/dist/{chunk-HQFPO7VX.js.map → chunk-7CJOAW3V.js.map} +1 -1
  6. package/dist/{chunk-CSJBF3QJ.js → chunk-A2WITZTI.js} +377 -113
  7. package/dist/chunk-A2WITZTI.js.map +1 -0
  8. package/dist/{chunk-BC6RKIHS.js → chunk-GZ4KR67U.js} +11 -11
  9. package/dist/{chunk-AVLMIWYO.js → chunk-KZRUYCL4.js} +12 -6
  10. package/dist/chunk-KZRUYCL4.js.map +1 -0
  11. package/dist/{chunk-OKXCECLG.js → chunk-TCCE5ZU5.js} +171 -22
  12. package/dist/chunk-TCCE5ZU5.js.map +1 -0
  13. package/dist/chunk-U255M5EZ.js +178 -0
  14. package/dist/chunk-U255M5EZ.js.map +1 -0
  15. package/dist/{chunk-IPVOVOW6.js → chunk-WY2QN2CX.js} +2539 -2174
  16. package/dist/chunk-WY2QN2CX.js.map +1 -0
  17. package/dist/chunk-ZNNE2VES.js +6413 -0
  18. package/dist/chunk-ZNNE2VES.js.map +1 -0
  19. package/dist/client.d.ts +33 -0
  20. package/dist/commands/index.d.ts +3 -0
  21. package/dist/commands/registry.d.ts +41 -0
  22. package/dist/commands/types.d.ts +85 -0
  23. package/dist/components/approval-surface.d.ts +28 -0
  24. package/dist/components/chat-composer.d.ts +51 -0
  25. package/dist/components/code-editor.d.ts +43 -0
  26. package/dist/components/command-palette.d.ts +25 -0
  27. package/dist/components/composer-transcription-control.d.ts +36 -0
  28. package/dist/components/composer.d.ts +277 -0
  29. package/dist/components/copy-button.d.ts +26 -0
  30. package/dist/components/desktop-viewer.d.ts +85 -0
  31. package/dist/components/diff-view.d.ts +38 -0
  32. package/dist/components/enrollment-consent.d.ts +33 -0
  33. package/dist/components/enrollment-device-flow.d.ts +27 -0
  34. package/dist/components/file-browser.d.ts +47 -0
  35. package/dist/components/fleet-tile.d.ts +18 -0
  36. package/dist/components/human-input-form.d.ts +47 -0
  37. package/dist/components/machine-card.d.ts +21 -0
  38. package/dist/components/machine-dock-bar.d.ts +31 -0
  39. package/dist/components/machine-health-pill.d.ts +9 -0
  40. package/dist/components/machine-metrics.d.ts +15 -0
  41. package/dist/components/machine-status-pill.d.ts +54 -0
  42. package/dist/components/machines/health.d.ts +25 -0
  43. package/dist/components/machines/machine-detail.d.ts +14 -0
  44. package/dist/components/machines/metric-history-chart.d.ts +24 -0
  45. package/dist/components/machines/metric-sparkline.d.ts +9 -0
  46. package/dist/components/machines/series.d.ts +24 -0
  47. package/dist/components/machines-dashboard.d.ts +30 -0
  48. package/dist/components/markdown.d.ts +31 -0
  49. package/dist/components/message-timeline.d.ts +98 -0
  50. package/dist/components/model-picker.d.ts +19 -0
  51. package/dist/components/pierre-diff.d.ts +34 -0
  52. package/dist/components/pierre-file.d.ts +27 -0
  53. package/dist/components/queue-draft-policy.d.ts +7 -0
  54. package/dist/components/queue-surface-implementation.d.ts +20 -0
  55. package/dist/components/queue-surface-state.d.ts +12 -0
  56. package/dist/components/queue-surface.d.ts +12 -0
  57. package/dist/components/sandbox-files.d.ts +49 -0
  58. package/dist/components/sandbox-terminal.d.ts +123 -0
  59. package/dist/components/sandbox-workspace.d.ts +128 -0
  60. package/dist/components/session-chrome.d.ts +70 -0
  61. package/dist/components/session-status.d.ts +25 -0
  62. package/dist/components/soften-streaming-markdown.d.ts +13 -0
  63. package/dist/components/stream-reveal.d.ts +37 -0
  64. package/dist/components/tip-follow.d.ts +139 -0
  65. package/dist/components/tooltip.d.ts +16 -0
  66. package/dist/components/workbench-changes.d.ts +34 -0
  67. package/dist/components/workspace-dock.d.ts +35 -0
  68. package/dist/composer.d.ts +11 -7
  69. package/dist/composer.js +9 -6
  70. package/dist/hooks/internal.d.ts +46 -0
  71. package/dist/hooks/use-available-models.d.ts +37 -0
  72. package/dist/hooks/use-billing-usage.d.ts +26 -0
  73. package/dist/hooks/use-codex-accounts.d.ts +69 -0
  74. package/dist/{use-composer-ClHy1rkL.d.ts → hooks/use-composer.d.ts} +16 -24
  75. package/dist/hooks/use-desktop-stream.d.ts +43 -0
  76. package/dist/hooks/use-environments.d.ts +35 -0
  77. package/dist/{use-file-attachments-BmT8WtqO.d.ts → hooks/use-file-attachments.d.ts} +6 -9
  78. package/dist/hooks/use-goal.d.ts +46 -0
  79. package/dist/hooks/use-human-input.d.ts +26 -0
  80. package/dist/hooks/use-last-started-turn-policy.d.ts +27 -0
  81. package/dist/hooks/use-machine-chip.d.ts +54 -0
  82. package/dist/hooks/use-machines.d.ts +81 -0
  83. package/dist/hooks/use-packs.d.ts +27 -0
  84. package/dist/hooks/use-relay-frame-stream.d.ts +53 -0
  85. package/dist/hooks/use-rigs.d.ts +87 -0
  86. package/dist/hooks/use-sandbox-files.d.ts +108 -0
  87. package/dist/hooks/use-sandbox-git.d.ts +58 -0
  88. package/dist/hooks/use-sandbox-terminal.d.ts +62 -0
  89. package/dist/hooks/use-scheduled-tasks.d.ts +15 -0
  90. package/dist/hooks/use-session-capabilities.d.ts +83 -0
  91. package/dist/hooks/use-session-control.d.ts +22 -0
  92. package/dist/hooks/use-session-events.d.ts +92 -0
  93. package/dist/hooks/use-session-lineage.d.ts +17 -0
  94. package/dist/hooks/use-session-mcp-approval-policy.d.ts +23 -0
  95. package/dist/hooks/use-session.d.ts +23 -0
  96. package/dist/hooks/use-slash-commands.d.ts +66 -0
  97. package/dist/hooks/use-terminal-stream.d.ts +46 -0
  98. package/dist/hooks/use-transcription.d.ts +73 -0
  99. package/dist/hooks/use-turn-queue.d.ts +44 -0
  100. package/dist/hooks/use-voice-input.d.ts +25 -0
  101. package/dist/hooks/use-windowed-sections.d.ts +42 -0
  102. package/dist/hooks/use-workspace-capture.d.ts +49 -0
  103. package/dist/hooks/use-workspace-edit.d.ts +73 -0
  104. package/dist/hooks/use-workspace-sessions.d.ts +30 -0
  105. package/dist/hooks/use-workspaces.d.ts +22 -0
  106. package/dist/human-input.d.ts +18 -0
  107. package/dist/index.d.ts +144 -2119
  108. package/dist/index.js +421 -324
  109. package/dist/index.js.map +1 -1
  110. package/dist/lib/clipboard.d.ts +7 -0
  111. package/dist/lib/cn.d.ts +3 -0
  112. package/dist/lib/format.d.ts +48 -0
  113. package/dist/lib/git-patch.d.ts +8 -0
  114. package/dist/lib/motion-inspect.d.ts +5 -0
  115. package/dist/lib/motion.d.ts +14 -0
  116. package/dist/lib/noto-sans-arabic-loader.d.ts +1 -0
  117. package/dist/lib/noto-sans-jp-loader.generated.d.ts +1 -0
  118. package/dist/lib/relay-wire.d.ts +30 -0
  119. package/dist/lib/use-portal-token-style.d.ts +9 -0
  120. package/dist/lib/use-theme-type.d.ts +5 -0
  121. package/dist/lib/use-unicode-fonts.d.ts +12 -0
  122. package/dist/lib/xterm-renderer.d.ts +19 -0
  123. package/dist/lib/xterm-theme.d.ts +75 -0
  124. package/dist/machines.d.ts +30 -413
  125. package/dist/machines.js +3 -3
  126. package/dist/model-policy.d.ts +32 -0
  127. package/dist/model-policy.js +33 -0
  128. package/dist/model-policy.js.map +1 -0
  129. package/dist/provider.d.ts +16 -0
  130. package/dist/{queue-surface-implementation-UQ535OKQ.js → queue-surface-implementation-THPAKGMD.js} +2 -2
  131. package/dist/{queue-surface-implementation-UQ535OKQ.js.map → queue-surface-implementation-THPAKGMD.js.map} +1 -1
  132. package/dist/session-context.d.ts +82 -0
  133. package/dist/session-ui.d.ts +10 -7
  134. package/dist/session-ui.js +9 -5
  135. package/dist/session.d.ts +28 -6
  136. package/dist/session.js +11 -11
  137. package/dist/timeline/activity-rail.d.ts +19 -0
  138. package/dist/timeline/disclosure-context.d.ts +14 -0
  139. package/dist/timeline/entrance.d.ts +14 -0
  140. package/dist/timeline/fleet-decision-projection.d.ts +9 -0
  141. package/dist/timeline/fleet-decision-row.d.ts +11 -0
  142. package/dist/timeline/fold-memory.d.ts +15 -0
  143. package/dist/timeline/index.d.ts +18 -0
  144. package/dist/timeline/parsers.d.ts +96 -0
  145. package/dist/timeline/projection.d.ts +38 -0
  146. package/dist/timeline/registry.d.ts +40 -0
  147. package/dist/timeline/screenshot-lightbox.d.ts +26 -0
  148. package/dist/timeline/seen-activity-ids.d.ts +2 -0
  149. package/dist/timeline/shared.d.ts +125 -0
  150. package/dist/timeline/tool-diff.d.ts +8 -0
  151. package/dist/timeline/tool-display-name.d.ts +10 -0
  152. package/dist/timeline/tool-renderers.d.ts +5 -0
  153. package/dist/timeline/turn-summary.d.ts +116 -0
  154. package/dist/{use-turn-queue-D_1a8W_l.d.ts → timeline/types.d.ts} +41 -69
  155. package/dist/types/machines.d.ts +11 -0
  156. package/package.json +8 -4
  157. package/src/client.ts +1 -0
  158. package/src/components/composer-transcription-control.tsx +278 -130
  159. package/src/components/composer.tsx +20 -15
  160. package/src/components/copy-button.tsx +127 -0
  161. package/src/components/markdown.tsx +231 -32
  162. package/src/components/message-timeline.tsx +1304 -378
  163. package/src/components/model-picker.tsx +83 -43
  164. package/src/components/queue-surface-implementation.tsx +1 -1
  165. package/src/components/session-chrome.tsx +942 -0
  166. package/src/components/soften-streaming-markdown.ts +205 -0
  167. package/src/components/stream-reveal.ts +195 -0
  168. package/src/components/tip-follow.ts +459 -0
  169. package/src/components/tooltip.tsx +103 -0
  170. package/src/composer.ts +6 -0
  171. package/src/hooks/internal.ts +30 -4
  172. package/src/hooks/use-available-models.ts +51 -4
  173. package/src/hooks/use-composer.ts +23 -3
  174. package/src/hooks/use-last-started-turn-policy.ts +95 -0
  175. package/src/hooks/use-session-events.ts +359 -5
  176. package/src/hooks/use-voice-input.ts +232 -0
  177. package/src/index.ts +47 -1
  178. package/src/lib/clipboard.ts +58 -0
  179. package/src/lib/format.ts +18 -0
  180. package/src/lib/motion-inspect.ts +5 -0
  181. package/src/lib/motion.ts +80 -0
  182. package/src/model-policy.ts +210 -0
  183. package/src/session-ui.ts +7 -0
  184. package/src/timeline/activity-rail.tsx +46 -7
  185. package/src/timeline/entrance.tsx +10 -0
  186. package/src/timeline/fold-memory.ts +52 -0
  187. package/src/timeline/index.ts +9 -1
  188. package/src/timeline/projection.ts +226 -41
  189. package/src/timeline/seen-activity-ids.ts +19 -0
  190. package/src/timeline/shared.tsx +1 -1
  191. package/src/timeline/tool-renderers.tsx +75 -6
  192. package/src/timeline/turn-summary.tsx +387 -97
  193. package/src/timeline/types.ts +21 -0
  194. package/styles/index.css +99 -7
  195. package/styles/tokens.css +44 -3
  196. package/dist/chunk-AVLMIWYO.js.map +0 -1
  197. package/dist/chunk-CSJBF3QJ.js.map +0 -1
  198. package/dist/chunk-IPVOVOW6.js.map +0 -1
  199. package/dist/chunk-M7X4JZOD.js.map +0 -1
  200. package/dist/chunk-OKXCECLG.js.map +0 -1
  201. package/dist/chunk-ZMNPFZWL.js +0 -3918
  202. package/dist/chunk-ZMNPFZWL.js.map +0 -1
  203. package/dist/composer-BMzimGTO.d.ts +0 -528
  204. package/dist/session-DaONt-dY.d.ts +0 -264
  205. package/dist/session-context-CAjlaeZb.d.ts +0 -84
  206. package/dist/session-ui-B5_roA95.d.ts +0 -259
  207. /package/dist/{chunk-BC6RKIHS.js.map → chunk-GZ4KR67U.js.map} +0 -0
@@ -1,136 +1,125 @@
1
1
  import type {
2
+ ClientVoiceInputConfig,
3
+ OpenGeniClient,
2
4
  TranscriptionAdapter,
3
- TranscriptionDiagnostic,
4
- TranscriptionErrorCode,
5
- TranscriptionTargetSelection,
6
5
  WorkspaceTranscriptionPolicy,
7
6
  } from "@opengeni/sdk";
8
- import { LoaderCircleIcon, MicIcon, RotateCwIcon, SquareIcon } from "lucide-react";
9
- import { type MouseEvent } from "react";
7
+ import { LoaderCircleIcon, MicIcon, SquareIcon, XIcon } from "lucide-react";
8
+ import { AnimatePresence, motion } from "motion/react";
9
+ import { useEffect, useState, type MouseEvent } from "react";
10
10
  import { cn } from "../lib/cn";
11
- import { useTranscription, type TranscriptionLifecycleTimeouts } from "../hooks/use-transcription";
11
+ import { useVoiceInput } from "../hooks/use-voice-input";
12
12
  import { useChatComposer } from "./composer";
13
13
 
14
14
  export type ComposerTranscriptionMessages = {
15
15
  start: string;
16
16
  stop: string;
17
+ cancel: string;
17
18
  retry: string;
18
19
  requestingPermission: string;
19
- listening: string;
20
- reconnecting: string;
21
- cancelling: string;
20
+ recording: string;
21
+ transcribing: string;
22
22
  unavailableDisabled: string;
23
- unavailableAdapter: string;
24
- unavailablePolicy: string;
23
+ unavailable: string;
25
24
  errorPermissionDenied: string;
26
25
  errorNotSupported: string;
27
- errorNetwork: string;
28
- errorProvider: string;
29
- errorPolicyBlocked: string;
30
- errorTimeout: string;
31
- errorCancelled: string;
26
+ errorUnavailable: string;
27
+ errorTooLarge: string;
28
+ errorInvalidAudio: string;
32
29
  errorUnknown: string;
33
30
  };
34
31
 
35
32
  const defaultMessages: ComposerTranscriptionMessages = {
36
33
  start: "Start voice input",
37
- stop: "Stop voice input",
34
+ stop: "Stop and transcribe",
35
+ cancel: "Cancel recording",
38
36
  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…",
37
+ requestingPermission: "Requesting microphone…",
38
+ recording: "Recording. Press Escape to cancel.",
39
+ transcribing: "Transcribing…",
43
40
  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.",
41
+ unavailable: "Voice input is unavailable for this workspace.",
46
42
  errorPermissionDenied: "Microphone permission was denied. Your draft was not changed.",
47
43
  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.",
44
+ errorUnavailable: "Voice input is not configured.",
45
+ errorTooLarge: "Recording is too large. Try a shorter message.",
46
+ errorInvalidAudio: "The recording could not be read. Try again.",
53
47
  errorUnknown: "Voice input could not start. Try again.",
54
48
  };
55
49
 
56
50
  export type ComposerTranscriptionControlProps = {
57
- adapter: TranscriptionAdapter | null;
58
- policy: WorkspaceTranscriptionPolicy;
59
- selection?: TranscriptionTargetSelection | undefined;
51
+ client?: OpenGeniClient | null | undefined;
52
+ workspaceId?: string | undefined;
53
+ capability?: ClientVoiceInputConfig | null | undefined;
54
+ workspaceEnabled?: boolean | undefined;
55
+ /** @deprecated Native MediaRecorder input no longer uses host adapters. */
56
+ adapter?: TranscriptionAdapter | null | undefined;
57
+ /** @deprecated Native MediaRecorder input no longer uses host policies. */
58
+ policy?: WorkspaceTranscriptionPolicy | undefined;
59
+ /** @deprecated Native input uses the server capability duration limit. */
60
+ lifecycleTimeouts?: unknown;
61
+ /** @deprecated Native input does not expose provider diagnostics. */
62
+ onDiagnostic?: unknown;
60
63
  messages?: Partial<ComposerTranscriptionMessages> | undefined;
61
64
  className?: string | undefined;
62
- lifecycleTimeouts?: Partial<TranscriptionLifecycleTimeouts> | undefined;
63
- onDiagnostic?: ((diagnostic: TranscriptionDiagnostic) => void) | undefined;
64
65
  };
65
66
 
67
+ const WAVEFORM_BARS = 18;
68
+ const WAVEFORM_BAR_KEYS = Array.from(
69
+ { length: WAVEFORM_BARS },
70
+ (_, index) => `voice-waveform-bar-${index + 1}`,
71
+ );
72
+
66
73
  /** One provider-neutral microphone control for the nearest editable composer. */
67
74
  export function ComposerTranscriptionControl({
68
- adapter,
69
- policy,
70
- selection,
75
+ client = null,
76
+ workspaceId = "",
77
+ capability = null,
78
+ workspaceEnabled = false,
71
79
  messages: overrides,
72
80
  className,
73
- lifecycleTimeouts,
74
- onDiagnostic,
75
81
  }: ComposerTranscriptionControlProps) {
76
82
  const composer = useChatComposer();
77
83
  const messages = { ...defaultMessages, ...overrides };
78
- const transcription = useTranscription({
79
- adapter,
80
- policy,
81
- ...(selection ? { selection } : {}),
84
+ const transcription = useVoiceInput({
85
+ client,
86
+ workspaceId,
87
+ capability,
88
+ enabled: workspaceEnabled,
82
89
  value: composer.value,
83
90
  setValue: composer.setValue,
84
91
  focusInput: composer.focusInput,
85
92
  disabled: composer.disabled,
86
- lifecycleTimeouts,
87
- onDiagnostic,
88
93
  });
89
- const status = transcription.state.status;
94
+ const { status } = transcription;
90
95
  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)
96
+ status === "requesting-permission" || status === "recording" || status === "transcribing";
97
+ const unavailableMessage = composer.disabled
98
+ ? messages.unavailableDisabled
99
+ : !capability?.available || !workspaceEnabled
100
+ ? messages.unavailable
101
+ : null;
102
+ const idleLabel = unavailableMessage ?? (status === "error" ? messages.retry : messages.start);
103
+ const errorMessage = transcription.error
104
+ ? transcriptionErrorMessage(transcription.error, messages)
112
105
  : null;
113
106
  const announcement =
114
- transcription.state.partial ||
115
- (status === "requesting-permission"
107
+ status === "requesting-permission"
116
108
  ? 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") {
109
+ : status === "recording"
110
+ ? messages.recording
111
+ : status === "transcribing"
112
+ ? messages.transcribing
113
+ : status === "error"
114
+ ? (errorMessage ?? messages.errorUnknown)
115
+ : unavailableMessage;
116
+
117
+ function start(event: MouseEvent<HTMLButtonElement>) {
118
+ if (unavailableMessage) {
129
119
  event.preventDefault();
130
120
  return;
131
121
  }
132
- if (active) void transcription.cancel();
133
- else void transcription.start();
122
+ void transcription.start();
134
123
  }
135
124
 
136
125
  return (
@@ -138,45 +127,110 @@ export function ComposerTranscriptionControl({
138
127
  className={cn("inline-flex min-w-0 items-center gap-1.5", className)}
139
128
  data-transcription-status={status}
140
129
  >
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" />
130
+ <AnimatePresence mode="popLayout" initial={false}>
131
+ {active ? (
132
+ <motion.span
133
+ key={status === "transcribing" ? "transcribing" : "capture"}
134
+ initial={{ opacity: 0, scale: 0.96 }}
135
+ animate={{ opacity: 1, scale: 1 }}
136
+ exit={{ opacity: 0, scale: 0.96 }}
137
+ transition={{ duration: 0.16, ease: [0.22, 1, 0.36, 1] }}
138
+ className={cn(
139
+ "inline-flex h-8 items-center gap-1 rounded-og-md border border-og-border/80",
140
+ "bg-og-surface-2/70 pl-2 pr-1 pointer-coarse:h-11",
141
+ )}
142
+ >
143
+ {status === "requesting-permission" ? (
144
+ <LoaderCircleIcon
145
+ className="size-3.5 shrink-0 text-og-fg-muted animate-og-spin motion-reduce:animate-none"
146
+ aria-hidden
147
+ />
148
+ ) : (
149
+ <span className="flex items-center gap-1.5">
150
+ {status === "recording" ? (
151
+ <span
152
+ aria-hidden
153
+ className="size-1.5 shrink-0 rounded-full bg-og-status-failed animate-og-pulse motion-reduce:animate-none"
154
+ />
155
+ ) : null}
156
+ <VoiceWaveform
157
+ stream={status === "recording" ? transcription.stream : null}
158
+ mode={status === "transcribing" ? "transcribing" : "recording"}
159
+ />
160
+ </span>
161
+ )}
162
+ {status === "recording" ? (
163
+ <>
164
+ <button
165
+ type="button"
166
+ onClick={() => transcription.cancel()}
167
+ aria-label={messages.cancel}
168
+ aria-keyshortcuts="Escape"
169
+ title={messages.cancel}
170
+ className={cn(
171
+ "inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm",
172
+ "text-og-fg-muted transition-colors duration-150 motion-reduce:transition-none",
173
+ "hover:bg-og-surface-3 hover:text-og-fg pointer-coarse:size-9",
174
+ )}
175
+ >
176
+ <XIcon className="size-3.5" />
177
+ </button>
178
+ <button
179
+ type="button"
180
+ onClick={() => transcription.stop()}
181
+ aria-label={messages.stop}
182
+ title={messages.stop}
183
+ className={cn(
184
+ "inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm",
185
+ "bg-og-fg text-og-bg transition-colors duration-150 motion-reduce:transition-none",
186
+ "hover:bg-og-fg-muted pointer-coarse:size-9",
187
+ )}
188
+ >
189
+ <SquareIcon className="size-2.5 fill-current" />
190
+ </button>
191
+ </>
192
+ ) : status === "transcribing" ? (
193
+ <span className="og-shimmer-text px-1.5 text-og-xs font-medium whitespace-nowrap">
194
+ {messages.transcribing}
195
+ </span>
196
+ ) : (
197
+ <span className="px-1.5 text-og-xs text-og-fg-muted whitespace-nowrap">
198
+ {messages.requestingPermission}
199
+ </span>
200
+ )}
201
+ </motion.span>
166
202
  ) : (
167
- <MicIcon className="size-4" />
203
+ <motion.button
204
+ key="idle"
205
+ type="button"
206
+ initial={{ opacity: 0, scale: 0.96 }}
207
+ animate={{ opacity: 1, scale: 1 }}
208
+ exit={{ opacity: 0, scale: 0.96 }}
209
+ transition={{ duration: 0.14, ease: [0.22, 1, 0.36, 1] }}
210
+ onClick={start}
211
+ aria-label={idleLabel}
212
+ aria-pressed={false}
213
+ aria-disabled={unavailableMessage !== null}
214
+ title={idleLabel}
215
+ className={cn(
216
+ "inline-flex size-8 shrink-0 items-center justify-center rounded-og-md pointer-coarse:size-11",
217
+ "text-og-fg-muted transition-colors duration-150 motion-reduce:transition-none",
218
+ unavailableMessage
219
+ ? "cursor-not-allowed opacity-45"
220
+ : "hover:bg-og-surface-2 hover:text-og-fg",
221
+ )}
222
+ >
223
+ <MicIcon className="size-4" />
224
+ </motion.button>
168
225
  )}
169
- </button>
170
- {announcement && (active || status === "error") ? (
226
+ </AnimatePresence>
227
+ {status === "error" && errorMessage ? (
171
228
  <span
172
229
  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
- )}
230
+ title={errorMessage}
231
+ className="max-w-40 truncate text-og-xs text-og-status-failed max-sm:max-w-24"
178
232
  >
179
- {announcement}
233
+ {errorMessage}
180
234
  </span>
181
235
  ) : null}
182
236
  <span className="sr-only" role={status === "error" ? "alert" : "status"} aria-live="polite">
@@ -186,26 +240,120 @@ export function ComposerTranscriptionControl({
186
240
  );
187
241
  }
188
242
 
189
- function transcriptionErrorMessage(
190
- code: TranscriptionErrorCode,
191
- messages: ComposerTranscriptionMessages,
192
- ): string {
243
+ function VoiceWaveform({
244
+ stream,
245
+ mode,
246
+ }: {
247
+ stream: MediaStream | null;
248
+ mode: "recording" | "transcribing";
249
+ }) {
250
+ const [levels, setLevels] = useState<number[]>(() => restingLevels(WAVEFORM_BARS));
251
+ const [live, setLive] = useState(false);
252
+
253
+ useEffect(() => {
254
+ if (!stream || mode !== "recording") {
255
+ setLive(false);
256
+ setLevels(restingLevels(WAVEFORM_BARS));
257
+ return;
258
+ }
259
+
260
+ const AudioCtx =
261
+ window.AudioContext ??
262
+ (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
263
+ if (!AudioCtx) return;
264
+
265
+ let cancelled = false;
266
+ let frame = 0;
267
+ let context: AudioContext | null = null;
268
+
269
+ try {
270
+ context = new AudioCtx();
271
+ const source = context.createMediaStreamSource(stream);
272
+ const analyser = context.createAnalyser();
273
+ analyser.fftSize = 64;
274
+ analyser.smoothingTimeConstant = 0.8;
275
+ source.connect(analyser);
276
+ const bins = new Uint8Array(analyser.frequencyBinCount);
277
+
278
+ const tick = () => {
279
+ if (cancelled) return;
280
+ analyser.getByteFrequencyData(bins);
281
+ const next = new Array<number>(WAVEFORM_BARS);
282
+ const usable = Math.max(8, Math.floor(bins.length * 0.65));
283
+ for (let i = 0; i < WAVEFORM_BARS; i++) {
284
+ const start = Math.floor((i / WAVEFORM_BARS) * usable);
285
+ const end = Math.max(start + 1, Math.floor(((i + 1) / WAVEFORM_BARS) * usable));
286
+ let sum = 0;
287
+ for (let j = start; j < end; j++) sum += bins[j] ?? 0;
288
+ const avg = sum / (end - start) / 255;
289
+ next[i] = Math.min(1, 0.14 + avg * 1.65);
290
+ }
291
+ setLevels(next);
292
+ setLive(true);
293
+ frame = requestAnimationFrame(tick);
294
+ };
295
+
296
+ void context.resume().then(() => {
297
+ if (!cancelled) frame = requestAnimationFrame(tick);
298
+ });
299
+ } catch {
300
+ setLive(false);
301
+ }
302
+
303
+ return () => {
304
+ cancelled = true;
305
+ cancelAnimationFrame(frame);
306
+ void context?.close();
307
+ };
308
+ }, [stream, mode]);
309
+
310
+ return (
311
+ <span
312
+ aria-hidden
313
+ data-voice-waveform={live ? "live" : "fallback"}
314
+ className="flex h-4 w-[3.75rem] items-center justify-center gap-px"
315
+ >
316
+ {WAVEFORM_BAR_KEYS.map((key, index) => {
317
+ const level = levels[index] ?? 0;
318
+ return (
319
+ <span
320
+ key={key}
321
+ className={cn(
322
+ "w-0.5 rounded-full bg-og-fg-muted/90 origin-center",
323
+ !live && "animate-og-waveform motion-reduce:animate-none",
324
+ mode === "transcribing" && "opacity-45",
325
+ )}
326
+ style={{
327
+ height: live ? `${Math.max(3, Math.round(level * 14))}px` : "14px",
328
+ animationDelay: live ? undefined : `${(index % 9) * 0.07}s`,
329
+ opacity: live ? 0.55 + level * 0.45 : undefined,
330
+ }}
331
+ />
332
+ );
333
+ })}
334
+ </span>
335
+ );
336
+ }
337
+
338
+ function restingLevels(count: number): number[] {
339
+ return Array.from({ length: count }, (_, index) => 0.22 + ((index * 17) % 5) * 0.06);
340
+ }
341
+
342
+ function transcriptionErrorMessage(code: string, messages: ComposerTranscriptionMessages): string {
193
343
  switch (code) {
194
344
  case "permission_denied":
195
345
  return messages.errorPermissionDenied;
196
346
  case "not_supported":
197
347
  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;
348
+ case "unavailable":
349
+ return messages.errorUnavailable;
350
+ case "too_large":
351
+ return messages.errorTooLarge;
352
+ case "invalid_audio":
353
+ return messages.errorInvalidAudio;
208
354
  case "unknown":
209
355
  return messages.errorUnknown;
356
+ default:
357
+ return messages.errorUnknown;
210
358
  }
211
359
  }
@@ -43,7 +43,8 @@ import {
43
43
  type SlashCommandContext,
44
44
  } from "../hooks/use-slash-commands";
45
45
  import { cn } from "../lib/cn";
46
- import { formatBytes, formatRelativeTime } from "../lib/format";
46
+ import { composerSubmissionErrorMessage, formatBytes, formatRelativeTime } from "../lib/format";
47
+ import type { PickerModelRow } from "../model-policy";
47
48
  import { CommandPalette as CommandPaletteView } from "./command-palette";
48
49
  import { ModelPicker as ModelPickerView } from "./model-picker";
49
50
 
@@ -191,6 +192,8 @@ export const defaultChatComposerMessages: ChatComposerMessages = {
191
192
  draftConflict: "This draft changed in another tab. Your local draft is still here.",
192
193
  useOtherDraft: "Use other draft",
193
194
  keepMine: "Keep mine",
195
+ // Kept for embedder message overrides / back-compat. Routine autosave is
196
+ // silent — only draft conflicts surface under the composer.
194
197
  savingDraft: "Saving draft…",
195
198
  formatBytes,
196
199
  formatRelativeTime,
@@ -470,7 +473,7 @@ export function useChatComposerController({
470
473
  tone: "error" as const,
471
474
  message: /control changed|paused while/i.test(delivery.error.message)
472
475
  ? messages.controlChangedError
473
- : delivery.error.message || messages.sendFailedError,
476
+ : composerSubmissionErrorMessage(delivery.error) || messages.sendFailedError,
474
477
  }
475
478
  : null);
476
479
  useEffect(() => {
@@ -698,9 +701,9 @@ export const Surface = forwardRef<HTMLDivElement, ComposerSurfaceProps>(function
698
701
  onDragLeave={controller.attachments ? controller.handleDragLeave : undefined}
699
702
  onDrop={controller.attachments ? controller.handleDrop : undefined}
700
703
  className={cn(
701
- "relative rounded-og-lg border border-og-border bg-og-surface-1 shadow-og-sm",
702
- "transition-[border-color,box-shadow] duration-200",
703
- "focus-within:border-og-accent/60 focus-within:shadow-og-glow",
704
+ "relative rounded-og-lg border border-og-border/90 bg-og-surface-1 shadow-og-sm",
705
+ "transition-[border-color,box-shadow] duration-200 ease-og-out",
706
+ "focus-within:border-og-accent/50 focus-within:shadow-og-glow",
704
707
  controller.dragging && "border-dashed border-og-accent",
705
708
  className,
706
709
  )}
@@ -814,7 +817,7 @@ export const Input = forwardRef<HTMLTextAreaElement, ComposerInputProps>(functio
814
817
  paletteOpen ? `${controller.listboxId}-option-${controller.palette.highlight}` : undefined
815
818
  }
816
819
  className={cn(
817
- "block w-full resize-none bg-transparent px-4 pt-3.5 pb-1 text-base leading-6 md:text-og-md",
820
+ "block w-full resize-none bg-transparent px-3.5 pt-3 pb-1 text-base leading-6 md:px-4 md:text-og-md",
818
821
  "text-og-fg placeholder:text-og-fg-subtle focus:outline-none focus-visible:outline-none",
819
822
  "disabled:cursor-not-allowed disabled:opacity-60",
820
823
  className,
@@ -848,7 +851,7 @@ export const Footer = forwardRef<HTMLDivElement, ComposerFooterProps>(function C
848
851
  <div
849
852
  {...props}
850
853
  ref={ref}
851
- className={cn("flex items-end gap-2 px-2.5 pb-2.5 pt-1", className)}
854
+ className={cn("flex items-end gap-1.5 px-2 pb-2 pt-0.5 sm:px-2.5 sm:pb-2.5", className)}
852
855
  />
853
856
  );
854
857
  });
@@ -949,29 +952,37 @@ export const AttachButton = forwardRef<HTMLButtonElement, ComposerAttachButtonPr
949
952
  );
950
953
 
951
954
  export type ComposerModelPickerProps = {
952
- models: ClientModel[];
955
+ models?: ClientModel[] | undefined;
956
+ /** Catalog-backed rows preferred over legacy provider-grouped models. */
957
+ rows?: PickerModelRow[] | undefined;
953
958
  value?: string | undefined;
954
959
  onChange?: ((modelId: string) => void) | undefined;
955
960
  label?: string | undefined;
956
961
  className?: string | undefined;
962
+ /** Filter to Codex models for remote_v2-locked sessions. */
963
+ codexOnly?: boolean | undefined;
957
964
  };
958
965
 
959
966
  export function ModelPicker({
960
967
  models,
968
+ rows,
961
969
  value,
962
970
  onChange,
963
971
  label,
964
972
  className,
973
+ codexOnly,
965
974
  }: ComposerModelPickerProps) {
966
975
  const controller = useComposerController();
967
976
  return (
968
977
  <ModelPickerView
969
978
  models={models}
979
+ rows={rows}
970
980
  value={value}
971
981
  onChange={(modelId) => onChange?.(modelId)}
972
982
  disabled={controller.disabled}
973
983
  label={label ?? controller.messages.modelLabel}
974
984
  className={className}
985
+ codexOnly={codexOnly}
975
986
  />
976
987
  );
977
988
  }
@@ -1119,8 +1130,6 @@ export function Status() {
1119
1130
  {controller.messages.keepMine}
1120
1131
  </button>
1121
1132
  </div>
1122
- ) : controller.draftSaving ? (
1123
- <p className="px-1 pt-1 text-og-xs text-og-fg-subtle">{controller.messages.savingDraft}</p>
1124
1133
  ) : null}
1125
1134
  </>
1126
1135
  );
@@ -1135,11 +1144,7 @@ function ComposerAnnouncements() {
1135
1144
  {controller.activeNotice.message}
1136
1145
  </p>
1137
1146
  ) : null}
1138
- {controller.draftConflict ? (
1139
- <p role="alert">{controller.messages.draftConflict}</p>
1140
- ) : controller.draftSaving ? (
1141
- <p role="status">{controller.messages.savingDraft}</p>
1142
- ) : null}
1147
+ {controller.draftConflict ? <p role="alert">{controller.messages.draftConflict}</p> : null}
1143
1148
  </div>
1144
1149
  );
1145
1150
  }