@opengeni/react 0.13.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 (113) hide show
  1. package/README.md +129 -23
  2. package/dist/chunk-6BXBGJ3B.js +1956 -0
  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 +642 -1080
  21. package/dist/index.js +7672 -8005
  22. package/dist/index.js.map +1 -1
  23. package/dist/machines.d.ts +412 -3
  24. package/dist/machines.js +25 -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 +23 -4
  38. package/src/approvals.ts +5 -3
  39. package/src/client.ts +50 -1
  40. package/src/components/approval-surface.tsx +198 -0
  41. package/src/components/chat-composer.tsx +104 -751
  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 +82 -16
  51. package/src/components/machine-health-pill.tsx +68 -0
  52. package/src/components/machine-metrics.tsx +10 -24
  53. package/src/components/machines/health.ts +146 -0
  54. package/src/components/machines/machine-detail.tsx +220 -0
  55. package/src/components/machines/metric-history-chart.tsx +298 -0
  56. package/src/components/machines/metric-sparkline.tsx +76 -0
  57. package/src/components/machines/series.ts +113 -0
  58. package/src/components/machines-dashboard.tsx +16 -4
  59. package/src/components/message-timeline.tsx +15 -7
  60. package/src/components/model-picker.tsx +12 -3
  61. package/src/components/pierre-diff.tsx +32 -6
  62. package/src/components/queue-surface-implementation.tsx +1041 -0
  63. package/src/components/queue-surface-state.tsx +94 -0
  64. package/src/components/queue-surface.tsx +60 -0
  65. package/src/components/sandbox-files.tsx +230 -21
  66. package/src/components/sandbox-terminal.tsx +8 -2
  67. package/src/components/sandbox-workspace.tsx +489 -135
  68. package/src/components/session-status.tsx +0 -6
  69. package/src/components/workbench-changes.tsx +145 -50
  70. package/src/components/workspace-dock.tsx +329 -68
  71. package/src/composer.ts +60 -0
  72. package/src/hooks/internal.ts +115 -34
  73. package/src/hooks/use-composer.ts +635 -74
  74. package/src/hooks/use-human-input.ts +131 -0
  75. package/src/hooks/use-machine-chip.ts +2 -2
  76. package/src/hooks/use-machines.ts +27 -11
  77. package/src/hooks/use-sandbox-files.ts +310 -62
  78. package/src/hooks/use-sandbox-git.ts +154 -40
  79. package/src/hooks/use-sandbox-terminal.ts +28 -6
  80. package/src/hooks/use-session-capabilities.ts +38 -9
  81. package/src/hooks/use-session-control.ts +43 -15
  82. package/src/hooks/use-session-events.ts +523 -48
  83. package/src/hooks/use-session-lineage.ts +15 -6
  84. package/src/hooks/use-session.ts +10 -2
  85. package/src/hooks/use-slash-commands.ts +38 -38
  86. package/src/hooks/use-transcription.ts +757 -0
  87. package/src/hooks/use-turn-queue.ts +273 -77
  88. package/src/hooks/use-windowed-sections.ts +2 -2
  89. package/src/hooks/use-workspace-capture.ts +146 -40
  90. package/src/hooks/use-workspace-edit.ts +50 -14
  91. package/src/hooks/use-workspace-sessions.ts +3 -0
  92. package/src/human-input.ts +72 -0
  93. package/src/index.ts +88 -12
  94. package/src/lib/noto-sans-arabic-loader.ts +21 -0
  95. package/src/lib/noto-sans-jp-loader.generated.ts +295 -0
  96. package/src/lib/use-portal-token-style.ts +51 -0
  97. package/src/lib/use-unicode-fonts.ts +57 -0
  98. package/src/machines.ts +16 -0
  99. package/src/provider.tsx +163 -41
  100. package/src/session-context.ts +135 -0
  101. package/src/session.ts +79 -0
  102. package/src/timeline/parsers.ts +78 -6
  103. package/src/timeline/projection.ts +36 -6
  104. package/src/timeline/tool-renderers.tsx +41 -0
  105. package/src/timeline/turn-summary.tsx +2 -2
  106. package/src/timeline/types.ts +16 -8
  107. package/styles/index.css +78 -0
  108. package/styles/index.d.ts +2 -0
  109. package/styles/tokens.css +11 -6
  110. package/styles/tokens.d.ts +2 -0
  111. package/dist/chunk-NFYVQWIB.js +0 -1377
  112. package/dist/chunk-NFYVQWIB.js.map +0 -1
  113. package/dist/machines-CnlMb7E-.d.ts +0 -329
@@ -0,0 +1,2393 @@
1
+ import {
2
+ cn
3
+ } from "./chunk-TK7G6XLT.js";
4
+ import {
5
+ shouldSteerOnKey,
6
+ shouldSubmitOnKey
7
+ } from "./chunk-MRSECXRP.js";
8
+ import {
9
+ formatBytes,
10
+ formatRelativeTime
11
+ } from "./chunk-HHFA4AFX.js";
12
+
13
+ // src/commands/registry.ts
14
+ function parseCommandLine(value) {
15
+ if (value[0] !== "/") {
16
+ return null;
17
+ }
18
+ const body = value.slice(1);
19
+ const firstSpace = body.indexOf(" ");
20
+ if (firstSpace === -1) {
21
+ return { name: body, rest: "", hasTrailingSpace: false, args: [] };
22
+ }
23
+ const name = body.slice(0, firstSpace);
24
+ const rest = body.slice(firstSpace + 1);
25
+ return {
26
+ name,
27
+ rest,
28
+ hasTrailingSpace: true,
29
+ args: rest.split(/\s+/).filter((token) => token.length > 0)
30
+ };
31
+ }
32
+ var PERMISSION_SUPERUSER = "workspace:admin";
33
+ function hasPermission(required, permissions) {
34
+ if (!required) {
35
+ return true;
36
+ }
37
+ return permissions.includes(required) || permissions.includes(PERMISSION_SUPERUSER);
38
+ }
39
+ function matchCommand(commands, value) {
40
+ const parsed = parseCommandLine(value);
41
+ if (!parsed) {
42
+ return null;
43
+ }
44
+ const token = parsed.name.toLowerCase();
45
+ return commands.find((command) => command.name === token || command.aliases?.includes(token)) ?? null;
46
+ }
47
+ function filterCommands(commands, token, ctx) {
48
+ const needle = token.toLowerCase();
49
+ return commands.filter((command) => {
50
+ if (!hasPermission(command.permission, ctx.permissions)) {
51
+ return false;
52
+ }
53
+ if (command.available && !command.available(ctx)) {
54
+ return false;
55
+ }
56
+ if (needle.length === 0) {
57
+ return true;
58
+ }
59
+ return command.name.startsWith(needle) || (command.aliases?.some((alias) => alias.startsWith(needle)) ?? false);
60
+ });
61
+ }
62
+ function argHint(args) {
63
+ if (!args || args.length === 0) {
64
+ return "";
65
+ }
66
+ return args.map((arg) => {
67
+ const label = arg.oneOf ? arg.oneOf.join("|") : arg.name;
68
+ return arg.required ? `<${label}>` : `[${label}]`;
69
+ }).join(" ");
70
+ }
71
+ function firstMissingRequiredArg(command, args) {
72
+ const required = (command.args ?? []).filter((arg) => arg.required);
73
+ for (let i = 0; i < required.length; i += 1) {
74
+ const value = args[i];
75
+ if (value === void 0 || value.length === 0) {
76
+ return required[i] ?? null;
77
+ }
78
+ }
79
+ return null;
80
+ }
81
+ function requireSession(ctx) {
82
+ if (!ctx.sessionId) {
83
+ throw new Error("No active session yet \u2014 start a session first.");
84
+ }
85
+ return ctx.sessionId;
86
+ }
87
+ var hasSession = (ctx) => ctx.sessionId !== null;
88
+ var defaultCommands = [
89
+ {
90
+ name: "help",
91
+ aliases: ["?"],
92
+ description: "Show available commands.",
93
+ run: (_args, ctx) => {
94
+ ctx.openHelp();
95
+ return { status: "ok" };
96
+ }
97
+ },
98
+ {
99
+ name: "clear-view",
100
+ description: "Clear the local timeline view (this device only; no server change).",
101
+ run: (_args, ctx) => {
102
+ const cleared = ctx.clearView();
103
+ if (!cleared) {
104
+ return {
105
+ status: "error",
106
+ message: "This view can't be cleared here (no local timeline to reset)."
107
+ };
108
+ }
109
+ return { status: "ok", message: "Local view cleared." };
110
+ }
111
+ },
112
+ {
113
+ name: "goal",
114
+ description: "Pause or resume the session's goal loop.",
115
+ permission: "sessions:control",
116
+ available: hasSession,
117
+ args: [
118
+ { name: "action", required: true, oneOf: ["pause", "resume"], description: "pause | resume" }
119
+ ],
120
+ run: async (args, ctx) => {
121
+ const sessionId = requireSession(ctx);
122
+ const action = args[0];
123
+ if (action !== "pause" && action !== "resume") {
124
+ return { status: "error", message: "Usage: /goal pause | /goal resume" };
125
+ }
126
+ try {
127
+ await ctx.client.updateGoal(ctx.workspaceId, sessionId, {
128
+ status: action === "pause" ? "paused" : "active"
129
+ });
130
+ return { status: "ok", message: action === "pause" ? "Goal paused." : "Goal resumed." };
131
+ } catch (cause) {
132
+ return { status: "error", message: goalErrorMessage(cause, action) };
133
+ }
134
+ }
135
+ },
136
+ {
137
+ name: "compact",
138
+ description: "Compact the conversation context now.",
139
+ permission: "sessions:control",
140
+ available: hasSession,
141
+ run: async (_args, ctx) => {
142
+ const sessionId = requireSession(ctx);
143
+ try {
144
+ const result = await ctx.client.compactSessionContext(ctx.workspaceId, sessionId);
145
+ return { status: "ok", message: result.message };
146
+ } catch (cause) {
147
+ return { status: "error", message: errorMessage(cause) ?? "Could not compact context." };
148
+ }
149
+ }
150
+ },
151
+ {
152
+ name: "clear",
153
+ description: "Clear the conversation context (destructive; audit-preserved).",
154
+ permission: "sessions:control",
155
+ danger: true,
156
+ available: hasSession,
157
+ run: async (_args, ctx) => {
158
+ const sessionId = requireSession(ctx);
159
+ const confirmed = await ctx.confirm();
160
+ if (!confirmed) {
161
+ return { status: "ok", keepDraft: true };
162
+ }
163
+ try {
164
+ await ctx.client.clearSessionContext(ctx.workspaceId, sessionId);
165
+ return { status: "ok", message: "Context cleared." };
166
+ } catch (cause) {
167
+ return { status: "error", message: clearErrorMessage(cause) };
168
+ }
169
+ }
170
+ }
171
+ ];
172
+ function errorMessage(cause) {
173
+ if (cause && typeof cause === "object" && "message" in cause && typeof cause.message === "string") {
174
+ return cause.message;
175
+ }
176
+ return void 0;
177
+ }
178
+ function statusCode(cause) {
179
+ if (cause && typeof cause === "object" && "status" in cause && typeof cause.status === "number") {
180
+ return cause.status;
181
+ }
182
+ return void 0;
183
+ }
184
+ function goalErrorMessage(cause, action) {
185
+ const code = statusCode(cause);
186
+ if (code === 404) {
187
+ return "This session has no goal to control.";
188
+ }
189
+ if (code === 409) {
190
+ return action === "resume" ? "Only a paused goal can be resumed." : "Goal is already in a terminal state.";
191
+ }
192
+ return errorMessage(cause) ?? `Could not ${action} the goal.`;
193
+ }
194
+ function clearErrorMessage(cause) {
195
+ if (statusCode(cause) === 409) {
196
+ return "Pause the session and wait for the current inference to settle, then clear context.";
197
+ }
198
+ return errorMessage(cause) ?? "Could not clear context.";
199
+ }
200
+
201
+ // src/hooks/use-slash-commands.ts
202
+ import { useCallback, useMemo, useRef, useState } from "react";
203
+ function useSlashCommands(options) {
204
+ const { commands, context, handlers, value, setValue } = options;
205
+ const [highlight, setHighlight] = useState(0);
206
+ const [dismissedValue, setDismissedValue] = useState(null);
207
+ const dismissed = dismissedValue !== null && dismissedValue === value;
208
+ const navigatedRef = useRef(false);
209
+ const navTokenRef = useRef(value);
210
+ if (navTokenRef.current !== value) {
211
+ navTokenRef.current = value;
212
+ navigatedRef.current = false;
213
+ }
214
+ const parsed = useMemo(() => parseCommandLine(value), [value]);
215
+ const filterCtx = useMemo(
216
+ () => ({
217
+ sessionId: context?.sessionId ?? null,
218
+ status: context?.status ?? null,
219
+ permissions: context?.permissions ?? []
220
+ }),
221
+ [context?.sessionId, context?.status, context?.permissions]
222
+ );
223
+ const activeCommand = useMemo(() => {
224
+ if (!parsed || !parsed.hasTrailingSpace) {
225
+ return null;
226
+ }
227
+ return matchCommand(commands, value);
228
+ }, [commands, value, parsed]);
229
+ const items = useMemo(() => {
230
+ if (!parsed) {
231
+ return [];
232
+ }
233
+ if (activeCommand) {
234
+ return [activeCommand];
235
+ }
236
+ return filterCommands(commands, parsed.name, filterCtx);
237
+ }, [commands, parsed, activeCommand, filterCtx]);
238
+ const open = parsed !== null && items.length > 0 && !dismissed;
239
+ const isCommandDraft = parsed !== null && items.length > 0;
240
+ const clampedHighlight = items.length === 0 ? 0 : Math.min(highlight, items.length - 1);
241
+ const activeArgHint = activeCommand ? argHint(activeCommand.args) : "";
242
+ const buildContext = useCallback(
243
+ (command) => {
244
+ if (!context) {
245
+ return null;
246
+ }
247
+ return { ...context, ...handlers, confirm: () => handlers.confirm(command) };
248
+ },
249
+ [context, handlers]
250
+ );
251
+ const execute = useCallback(
252
+ async (command, args) => {
253
+ const ctx = buildContext(command);
254
+ if (!ctx) {
255
+ return;
256
+ }
257
+ try {
258
+ const result = await command.run(args, ctx);
259
+ if (result.message) {
260
+ ctx.notice({ tone: result.status === "ok" ? "ok" : "error", message: result.message });
261
+ }
262
+ if (result.status === "ok" && !result.keepDraft) {
263
+ setValue("");
264
+ }
265
+ } catch (cause) {
266
+ ctx.notice({ tone: "error", message: errorMessage2(cause) });
267
+ }
268
+ },
269
+ [buildContext, setValue]
270
+ );
271
+ const autocomplete = useCallback(
272
+ (command) => {
273
+ setValue(`/${command.name} `);
274
+ setHighlight(0);
275
+ },
276
+ [setValue]
277
+ );
278
+ const autocompleteHighlighted = useCallback(() => {
279
+ const command = items[clampedHighlight];
280
+ if (command) {
281
+ autocomplete(command);
282
+ }
283
+ }, [items, clampedHighlight, autocomplete]);
284
+ const runningRef = useRef(false);
285
+ const runResolved = useCallback(
286
+ async (command, executionOptions) => {
287
+ if (!parsed || runningRef.current) {
288
+ return;
289
+ }
290
+ runningRef.current = true;
291
+ try {
292
+ const explicit = executionOptions?.explicit ?? false;
293
+ const nameMatchesToken = command.name === parsed.name.toLowerCase() || (command.aliases?.includes(parsed.name.toLowerCase()) ?? false);
294
+ if (!explicit && !activeCommand && !nameMatchesToken && !parsed.hasTrailingSpace) {
295
+ autocomplete(command);
296
+ return;
297
+ }
298
+ const args = nameMatchesToken || parsed.hasTrailingSpace ? parsed.args : [];
299
+ const missing = firstMissingRequiredArg(command, args);
300
+ if (missing) {
301
+ if (!parsed.hasTrailingSpace) {
302
+ autocomplete(command);
303
+ }
304
+ return;
305
+ }
306
+ await execute(command, args);
307
+ } finally {
308
+ runningRef.current = false;
309
+ }
310
+ },
311
+ [parsed, activeCommand, autocomplete, execute]
312
+ );
313
+ const runHighlighted = useCallback(async () => {
314
+ if (!parsed) {
315
+ return;
316
+ }
317
+ if (navigatedRef.current && !activeCommand) {
318
+ const highlighted = items[clampedHighlight];
319
+ if (highlighted) {
320
+ await runResolved(highlighted, { explicit: true });
321
+ }
322
+ return;
323
+ }
324
+ const token = parsed.name.toLowerCase();
325
+ const exact = items.find((item) => item.name === token || item.aliases?.includes(token));
326
+ const command = activeCommand ?? exact ?? items[clampedHighlight];
327
+ if (!command) {
328
+ return;
329
+ }
330
+ await runResolved(command);
331
+ }, [parsed, activeCommand, items, clampedHighlight, runResolved]);
332
+ const runAt = useCallback(
333
+ async (index) => {
334
+ const command = items[index];
335
+ if (!command) {
336
+ return;
337
+ }
338
+ await runResolved(command, { explicit: true });
339
+ },
340
+ [items, runResolved]
341
+ );
342
+ const onKeyDown = useCallback(
343
+ (event) => {
344
+ if (!open) {
345
+ return false;
346
+ }
347
+ switch (event.key) {
348
+ case "ArrowDown": {
349
+ event.preventDefault();
350
+ navigatedRef.current = true;
351
+ setHighlight(
352
+ (current) => items.length === 0 ? 0 : (Math.min(current, items.length - 1) + 1) % items.length
353
+ );
354
+ return true;
355
+ }
356
+ case "ArrowUp": {
357
+ event.preventDefault();
358
+ navigatedRef.current = true;
359
+ setHighlight((current) => {
360
+ const base = Math.min(current, items.length - 1);
361
+ return items.length === 0 ? 0 : (base - 1 + items.length) % items.length;
362
+ });
363
+ return true;
364
+ }
365
+ case "Tab": {
366
+ event.preventDefault();
367
+ autocompleteHighlighted();
368
+ return true;
369
+ }
370
+ case "Enter": {
371
+ if (event.shiftKey || event.nativeEvent?.isComposing) {
372
+ return false;
373
+ }
374
+ event.preventDefault();
375
+ void runHighlighted();
376
+ return true;
377
+ }
378
+ case "Escape": {
379
+ event.preventDefault();
380
+ setDismissedValue(value);
381
+ return true;
382
+ }
383
+ default:
384
+ return false;
385
+ }
386
+ },
387
+ [open, items, autocompleteHighlighted, runHighlighted, value]
388
+ );
389
+ return {
390
+ open,
391
+ isCommandDraft,
392
+ items,
393
+ highlight: clampedHighlight,
394
+ setHighlight,
395
+ activeCommand,
396
+ activeArgHint,
397
+ onKeyDown,
398
+ runHighlighted,
399
+ runAt,
400
+ autocompleteHighlighted
401
+ };
402
+ }
403
+ function errorMessage2(cause) {
404
+ if (cause instanceof Error) {
405
+ return cause.message;
406
+ }
407
+ return String(cause);
408
+ }
409
+
410
+ // src/components/command-palette.tsx
411
+ import { AnimatePresence, motion } from "motion/react";
412
+ import { jsx, jsxs } from "react/jsx-runtime";
413
+ function CommandPalette({
414
+ open,
415
+ items,
416
+ highlight,
417
+ onHighlight,
418
+ onRun,
419
+ argHintText,
420
+ listboxId,
421
+ label = "Slash commands",
422
+ dangerLabel = "danger"
423
+ }) {
424
+ return /* @__PURE__ */ jsx(AnimatePresence, { children: open ? /* @__PURE__ */ jsxs(
425
+ motion.div,
426
+ {
427
+ initial: { opacity: 0, y: 6, scale: 0.99 },
428
+ animate: { opacity: 1, y: 0, scale: 1 },
429
+ exit: { opacity: 0, y: 6, scale: 0.99 },
430
+ transition: { duration: 0.13, ease: "easeOut" },
431
+ className: cn(
432
+ "absolute bottom-full left-0 right-0 z-20 mb-2 overflow-hidden",
433
+ "rounded-og-lg border border-og-border bg-og-surface-2 shadow-og-sm"
434
+ ),
435
+ children: [
436
+ /* @__PURE__ */ jsx(
437
+ "ul",
438
+ {
439
+ id: listboxId,
440
+ role: "listbox",
441
+ "aria-label": label,
442
+ className: "max-h-72 overflow-y-auto py-1",
443
+ children: items.map((command, index) => {
444
+ const selected = index === highlight;
445
+ const hint = argHint(command.args);
446
+ return /* @__PURE__ */ jsxs(
447
+ "li",
448
+ {
449
+ id: `${listboxId}-option-${index}`,
450
+ role: "option",
451
+ "aria-selected": selected,
452
+ onMouseEnter: () => onHighlight(index),
453
+ onMouseDown: (event) => {
454
+ event.preventDefault();
455
+ onRun(index);
456
+ },
457
+ className: cn(
458
+ "mx-1 flex cursor-pointer items-center gap-2 rounded-og-md px-2.5 py-1.5",
459
+ "transition-colors duration-100",
460
+ selected ? "bg-og-accent/15 text-og-fg" : "text-og-fg-muted hover:bg-og-surface-3"
461
+ ),
462
+ children: [
463
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-baseline gap-1.5", children: [
464
+ /* @__PURE__ */ jsxs(
465
+ "span",
466
+ {
467
+ className: cn(
468
+ "font-mono text-[13px]",
469
+ selected ? "text-og-accent" : "text-og-fg"
470
+ ),
471
+ children: [
472
+ "/",
473
+ command.name
474
+ ]
475
+ }
476
+ ),
477
+ hint ? /* @__PURE__ */ jsx("span", { className: "truncate font-mono text-[11px] text-og-fg-subtle", children: hint }) : null
478
+ ] }),
479
+ /* @__PURE__ */ jsxs("span", { className: "ml-auto flex items-center gap-1.5", children: [
480
+ command.danger ? /* @__PURE__ */ jsx("span", { className: "rounded-og-xs bg-og-status-failed/15 px-1 text-[10px] uppercase tracking-wide text-og-status-failed", children: dangerLabel }) : null,
481
+ /* @__PURE__ */ jsx("span", { className: "truncate text-[12px] text-og-fg-subtle max-sm:hidden", children: command.description })
482
+ ] })
483
+ ]
484
+ },
485
+ command.name
486
+ );
487
+ })
488
+ }
489
+ ),
490
+ argHintText ? /* @__PURE__ */ jsx("div", { className: "border-t border-og-border px-3 py-1.5 font-mono text-[11px] text-og-fg-subtle", children: argHintText }) : null
491
+ ]
492
+ }
493
+ ) : null });
494
+ }
495
+
496
+ // src/components/model-picker.tsx
497
+ import { ChevronDownIcon } from "lucide-react";
498
+ import { useId, useMemo as useMemo2 } from "react";
499
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
500
+ function ModelPicker({
501
+ models,
502
+ value,
503
+ onChange,
504
+ disabled,
505
+ className,
506
+ label = "Model"
507
+ }) {
508
+ const selectId = useId();
509
+ const groups = useMemo2(() => {
510
+ const byProvider = /* @__PURE__ */ new Map();
511
+ for (const model of models) {
512
+ let group = byProvider.get(model.provider);
513
+ if (!group) {
514
+ group = { label: model.providerLabel, models: [] };
515
+ byProvider.set(model.provider, group);
516
+ }
517
+ group.models.push(model);
518
+ }
519
+ return [...byProvider.values()];
520
+ }, [models]);
521
+ if (models.length === 0) {
522
+ return null;
523
+ }
524
+ return /* @__PURE__ */ jsxs2("span", { className: cn("relative inline-flex items-center", className), children: [
525
+ /* @__PURE__ */ jsx2("label", { htmlFor: selectId, className: "sr-only", children: label }),
526
+ /* @__PURE__ */ jsx2(
527
+ "select",
528
+ {
529
+ id: selectId,
530
+ value: value ?? "",
531
+ onChange: (event) => onChange(event.target.value),
532
+ disabled: disabled === true,
533
+ "aria-label": label,
534
+ className: cn(
535
+ // Sized like the other footer controls; the chevron overlay needs the
536
+ // right padding so the value never collides with it.
537
+ "h-8 max-w-[180px] cursor-pointer appearance-none truncate rounded-og-md bg-transparent",
538
+ "py-0 pl-2 pr-6 text-[13px] text-og-fg-muted",
539
+ "transition-colors duration-150 hover:bg-og-surface-2 hover:text-og-fg",
540
+ "focus:outline-none focus-visible:outline-none",
541
+ "disabled:cursor-not-allowed disabled:opacity-50"
542
+ ),
543
+ children: groups.map((group) => /* @__PURE__ */ jsx2("optgroup", { label: group.label, children: group.models.map((model) => /* @__PURE__ */ jsx2("option", { value: model.id, children: model.label }, model.id)) }, group.label))
544
+ }
545
+ ),
546
+ /* @__PURE__ */ jsx2(
547
+ ChevronDownIcon,
548
+ {
549
+ "aria-hidden": true,
550
+ className: "pointer-events-none absolute right-1.5 size-3.5 text-og-fg-subtle"
551
+ }
552
+ )
553
+ ] });
554
+ }
555
+
556
+ // src/components/composer.tsx
557
+ import {
558
+ ArrowUpIcon,
559
+ ChevronDownIcon as ChevronDownIcon2,
560
+ FileIcon,
561
+ ImageIcon,
562
+ LoaderCircleIcon,
563
+ PaperclipIcon,
564
+ PauseIcon,
565
+ PlayIcon,
566
+ RotateCwIcon,
567
+ XIcon
568
+ } from "lucide-react";
569
+ import { AnimatePresence as AnimatePresence2, motion as motion2 } from "motion/react";
570
+ import {
571
+ createContext,
572
+ forwardRef,
573
+ useCallback as useCallback2,
574
+ useContext,
575
+ useEffect,
576
+ useId as useId2,
577
+ useMemo as useMemo3,
578
+ useRef as useRef2,
579
+ useState as useState2
580
+ } from "react";
581
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
582
+ var OPEN_WORKSTREAM_CONTROL_EVENT = "opengeni:open-workstream-control";
583
+ var defaultChatComposerMessages = {
584
+ messagePlaceholder: "Message the agent\u2026",
585
+ pausedPlaceholder: "Sending will resume this workstream\u2026",
586
+ inputLabel: "Message the agent",
587
+ keyboardHint: "Enter to queue \xB7 Cmd/Ctrl+Enter to steer \xB7 Shift+Enter for a new line",
588
+ slashCommandBlocked: "That's a slash command \u2014 press Enter in the command list to run it, or edit the line to send a message.",
589
+ controlChangedError: "This workstream was paused while you were sending. Nothing was sent, and your draft is still here.",
590
+ sendFailedError: "Sending failed \u2014 your draft is still here. Try again.",
591
+ dropFiles: "Drop files to attach",
592
+ attachFiles: "Attach files",
593
+ pauseAriaLabel: "Pause this workstream",
594
+ pauseTitle: "Pause this workstream; queued prompts and approvals are preserved",
595
+ sendMessageAriaLabel: "Send message",
596
+ sendAndResumeAriaLabel: "Send and resume",
597
+ sendTitle: "Queue message (Enter); steer with Cmd/Ctrl+Enter",
598
+ sendAndResumeTitle: "Send & resume (Enter); Steer & resume with Cmd/Ctrl+Enter",
599
+ workspacePaused: "Workspace paused",
600
+ pausedHere: "Paused here",
601
+ parentBlocker: "parent",
602
+ narrowerPause: "a narrower pause",
603
+ pausedBy: (displayName) => `Paused by ${displayName}`,
604
+ queuedAhead: (count) => `Send & resume queues behind ${count} waiting prompt${count === 1 ? "" : "s"}.`,
605
+ resumingAndSending: "Resuming and sending\u2026",
606
+ nextMessageResumes: "Your next message resumes this workstream automatically.",
607
+ resumeThisWorkstream: "Resume this workstream",
608
+ resumeShort: "Resume",
609
+ hidePauseDetails: "Hide pause details",
610
+ showPauseDetails: "Show pause details",
611
+ pauseReasonsLabel: "Reasons this workstream is paused",
612
+ pausedByLabel: "Paused by ",
613
+ alsoPausedByLabel: "Also paused by ",
614
+ resumeWorkspace: "Resume workspace",
615
+ resumeFromSession: "Resume from this session",
616
+ sessionCanRun: "This session will be able to run",
617
+ stillPausedBy: (displayName) => `Still paused by ${displayName}`,
618
+ restoredResourcesLabel: "Restored prompt resources",
619
+ restoredFile: (fileId) => `File ${fileId.slice(0, 8)}`,
620
+ removeRestoredResource: (index) => `Remove restored resource ${index + 1}`,
621
+ uploading: "Uploading",
622
+ uploadFailed: "Upload failed",
623
+ retryAttachment: (name) => `Retry ${name}`,
624
+ retryUpload: "Retry upload",
625
+ removeAttachment: (name) => `Remove ${name}`,
626
+ confirmCommand: (name) => `Confirm /${name}`,
627
+ confirmDescription: (command) => `Run /${command.name}? ${command.description}`,
628
+ cancel: "Cancel",
629
+ runCommand: (name) => `Run /${name}`,
630
+ commands: "Commands",
631
+ slashCommandsLabel: "Slash commands",
632
+ modelLabel: "Model",
633
+ close: "Close",
634
+ danger: "danger",
635
+ draftConflict: "This draft changed in another tab. Your local draft is still here.",
636
+ useOtherDraft: "Use other draft",
637
+ keepMine: "Keep mine",
638
+ savingDraft: "Saving draft\u2026",
639
+ formatBytes,
640
+ formatRelativeTime
641
+ };
642
+ function useChatComposerController({
643
+ delivery,
644
+ draft,
645
+ control,
646
+ effectiveControl,
647
+ queuedAheadCount = 0,
648
+ canControlWorkspace = false,
649
+ controlLinks,
650
+ disabled = false,
651
+ attachments,
652
+ commands = defaultCommands,
653
+ commandContext,
654
+ onClearView,
655
+ onPaste,
656
+ messages: messageOverrides
657
+ }) {
658
+ const messages = useMemo3(
659
+ () => ({ ...defaultChatComposerMessages, ...messageOverrides }),
660
+ [messageOverrides]
661
+ );
662
+ const id = useId2();
663
+ const rootRef = useRef2(null);
664
+ const textareaRef = useRef2(null);
665
+ const pauseButtonRef = useRef2(null);
666
+ const fileInputRef = useRef2(null);
667
+ const listboxId = useId2();
668
+ const paused = effectiveControl?.state === "paused";
669
+ const [controlDetailsOpen, setControlDetailsOpen] = useState2(false);
670
+ const [paletteMounted, setPaletteMounted] = useState2(false);
671
+ const [submitting, setSubmitting] = useState2(false);
672
+ const submittingRef = useRef2(false);
673
+ const controlOperationRef = useRef2(false);
674
+ const mountedRef = useRef2(true);
675
+ useEffect(() => {
676
+ mountedRef.current = true;
677
+ return () => {
678
+ mountedRef.current = false;
679
+ };
680
+ }, []);
681
+ useEffect(() => {
682
+ const openControl = (event) => {
683
+ const requestedId = event instanceof CustomEvent && isRecord(event.detail) && typeof event.detail.composerId === "string" ? event.detail.composerId : null;
684
+ if (requestedId && requestedId !== id) return;
685
+ if (!requestedId) {
686
+ const roots = document.querySelectorAll("[data-og-composer-id]");
687
+ if (roots.length > 1 && !rootRef.current?.contains(document.activeElement)) return;
688
+ }
689
+ textareaRef.current?.scrollIntoView({ block: "end", behavior: "smooth" });
690
+ if (paused) {
691
+ setControlDetailsOpen(true);
692
+ return;
693
+ }
694
+ window.requestAnimationFrame(() => pauseButtonRef.current?.focus());
695
+ };
696
+ document.addEventListener(OPEN_WORKSTREAM_CONTROL_EVENT, openControl);
697
+ return () => document.removeEventListener(OPEN_WORKSTREAM_CONTROL_EVENT, openControl);
698
+ }, [id, paused]);
699
+ const blockedByUpload = attachments?.uploading === true;
700
+ const hasReadyAttachment = (attachments?.readyResources.length ?? 0) > 0;
701
+ const [dragging, setDragging] = useState2(false);
702
+ const dragCarriesFiles = (event) => event.dataTransfer !== null && [...event.dataTransfer.types].includes("Files");
703
+ const handleDragOver = useCallback2(
704
+ (event) => {
705
+ if (!attachments || !dragCarriesFiles(event)) return;
706
+ event.preventDefault();
707
+ setDragging(true);
708
+ },
709
+ [attachments]
710
+ );
711
+ const handleDragLeave = useCallback2(
712
+ (event) => {
713
+ if (!attachments) return;
714
+ if (event.currentTarget.contains(event.relatedTarget)) return;
715
+ setDragging(false);
716
+ },
717
+ [attachments]
718
+ );
719
+ const handleDrop = useCallback2(
720
+ (event) => {
721
+ if (!attachments || !dragCarriesFiles(event)) return;
722
+ event.preventDefault();
723
+ setDragging(false);
724
+ if (event.dataTransfer.files.length > 0) attachments.addFiles(event.dataTransfer.files);
725
+ },
726
+ [attachments]
727
+ );
728
+ const [notice, setNotice] = useState2(null);
729
+ const [helpOpen, setHelpOpen] = useState2(false);
730
+ const [confirmState, setConfirmState] = useState2(null);
731
+ const pendingConfirm = useRef2(null);
732
+ const settleConfirmation = useCallback2((confirmed) => {
733
+ const resolve = pendingConfirm.current;
734
+ pendingConfirm.current = null;
735
+ setConfirmState(null);
736
+ resolve?.(confirmed);
737
+ }, []);
738
+ useEffect(
739
+ () => () => {
740
+ pendingConfirm.current?.(false);
741
+ pendingConfirm.current = null;
742
+ },
743
+ []
744
+ );
745
+ const resizeInput = useCallback2(() => {
746
+ const textarea = textareaRef.current;
747
+ if (!textarea) return;
748
+ textarea.style.height = "0px";
749
+ textarea.style.height = `${Math.min(textarea.scrollHeight, 220)}px`;
750
+ }, []);
751
+ useEffect(() => resizeInput(), [delivery.value, resizeInput]);
752
+ const handlers = useMemo3(
753
+ () => ({
754
+ notice: (next) => {
755
+ setNotice(next);
756
+ delivery.clearError();
757
+ },
758
+ openHelp: () => setHelpOpen(true),
759
+ clearView: () => {
760
+ if (!onClearView) return false;
761
+ onClearView();
762
+ return true;
763
+ },
764
+ confirm: (command) => new Promise((resolve) => {
765
+ pendingConfirm.current?.(false);
766
+ pendingConfirm.current = resolve;
767
+ setConfirmState({ command, resolve: settleConfirmation });
768
+ })
769
+ }),
770
+ [delivery, onClearView, settleConfirmation]
771
+ );
772
+ const palette = useSlashCommands({
773
+ commands,
774
+ context: commandContext,
775
+ handlers,
776
+ value: delivery.value,
777
+ setValue: delivery.setValue
778
+ });
779
+ const paletteEnabled = commandContext !== void 0;
780
+ const commandDraftBlocked = paletteEnabled && palette.isCommandDraft;
781
+ const submitBlocker = disabled ? "disabled" : blockedByUpload ? "uploading" : delivery.sending || submitting ? "sending" : commandDraftBlocked ? "command" : delivery.canSend || hasReadyAttachment ? null : "empty";
782
+ const canSubmit = submitBlocker === null;
783
+ const submit = useCallback2(
784
+ async (mode) => {
785
+ if (commandDraftBlocked) {
786
+ setNotice({ tone: "error", message: messages.slashCommandBlocked });
787
+ delivery.clearError();
788
+ return false;
789
+ }
790
+ if (disabled || blockedByUpload || delivery.sending || submittingRef.current) return false;
791
+ if (!delivery.canSend && !hasReadyAttachment) return false;
792
+ submittingRef.current = true;
793
+ setSubmitting(true);
794
+ try {
795
+ return mode === "steer" ? await delivery.steer() : await delivery.send();
796
+ } finally {
797
+ submittingRef.current = false;
798
+ if (mountedRef.current) setSubmitting(false);
799
+ }
800
+ },
801
+ [
802
+ blockedByUpload,
803
+ commandDraftBlocked,
804
+ delivery,
805
+ disabled,
806
+ hasReadyAttachment,
807
+ messages.slashCommandBlocked
808
+ ]
809
+ );
810
+ const handleKeyDown = useCallback2(
811
+ (event) => {
812
+ if (paletteEnabled && paletteMounted && palette.onKeyDown(event)) return;
813
+ if (!shouldSubmitOnKey(event)) return;
814
+ event.preventDefault();
815
+ void submit(shouldSteerOnKey(event) ? "steer" : "queue");
816
+ },
817
+ [palette, paletteEnabled, paletteMounted, submit]
818
+ );
819
+ const handlePaste = useCallback2(
820
+ (event) => {
821
+ onPaste?.(event);
822
+ attachments?.addFromPaste(event);
823
+ },
824
+ [attachments, onPaste]
825
+ );
826
+ const handleFileChange = useCallback2(
827
+ (event) => {
828
+ if (event.target.files) attachments?.addFiles(event.target.files);
829
+ event.target.value = "";
830
+ },
831
+ [attachments]
832
+ );
833
+ const helpCommands = useMemo3(
834
+ () => commands.filter((command) => {
835
+ if (command.permission && commandContext) {
836
+ const permissions = commandContext.permissions;
837
+ return permissions.includes(command.permission) || permissions.includes("workspace:admin");
838
+ }
839
+ return true;
840
+ }),
841
+ [commandContext, commands]
842
+ );
843
+ const activeNotice = notice ?? (delivery.error ? {
844
+ tone: "error",
845
+ message: /control changed|paused while/i.test(delivery.error.message) ? messages.controlChangedError : delivery.error.message || messages.sendFailedError
846
+ } : null);
847
+ useEffect(() => {
848
+ if (notice?.tone !== "ok") return;
849
+ const timer = window.setTimeout(
850
+ () => setNotice((current) => current === notice ? null : current),
851
+ 2400
852
+ );
853
+ return () => window.clearTimeout(timer);
854
+ }, [notice]);
855
+ const runControlOperation = useCallback2(async (operation) => {
856
+ if (controlOperationRef.current) return false;
857
+ controlOperationRef.current = true;
858
+ try {
859
+ await operation();
860
+ return true;
861
+ } finally {
862
+ controlOperationRef.current = false;
863
+ }
864
+ }, []);
865
+ const pause = useCallback2(
866
+ async (reason) => {
867
+ if (!control || control.pausing || control.resuming) return false;
868
+ return await runControlOperation(() => control.pause(reason));
869
+ },
870
+ [control, runControlOperation]
871
+ );
872
+ const resume = useCallback2(
873
+ async (reason) => {
874
+ if (!control || control.pausing || control.resuming) return false;
875
+ return await runControlOperation(() => control.resume(reason));
876
+ },
877
+ [control, runControlOperation]
878
+ );
879
+ const resumeScope = useCallback2(
880
+ async (option) => {
881
+ if (!control || control.pausing || control.resuming) return false;
882
+ return await runControlOperation(() => control.resumeScope(option));
883
+ },
884
+ [control, runControlOperation]
885
+ );
886
+ return {
887
+ id,
888
+ rootRef,
889
+ textareaRef,
890
+ pauseButtonRef,
891
+ fileInputRef,
892
+ listboxId,
893
+ effectiveControl,
894
+ queuedAheadCount,
895
+ canControlWorkspace,
896
+ controlLinks,
897
+ disabled,
898
+ attachments,
899
+ messages,
900
+ sending: delivery.sending || submitting,
901
+ error: delivery.error,
902
+ clearError: delivery.clearError,
903
+ hasDraftState: draft !== void 0,
904
+ draftConflict: draft?.draftConflict ?? null,
905
+ draftSaving: draft?.draftSaving ?? false,
906
+ restoredResources: draft?.restoredResources ?? [],
907
+ removeRestoredResource: draft?.removeRestoredResource,
908
+ resolveDraftConflict: draft?.resolveDraftConflict,
909
+ hasControl: control !== void 0,
910
+ pausing: control?.pausing ?? false,
911
+ resuming: control?.resuming ?? false,
912
+ pause,
913
+ resume,
914
+ resumeScope,
915
+ paused,
916
+ controlDetailsOpen,
917
+ setControlDetailsOpen,
918
+ paletteMounted,
919
+ setPaletteMounted,
920
+ dragging,
921
+ handleDragOver,
922
+ handleDragLeave,
923
+ handleDrop,
924
+ palette,
925
+ paletteEnabled,
926
+ commandDraftBlocked,
927
+ confirmState,
928
+ settleConfirmation,
929
+ helpOpen,
930
+ setHelpOpen,
931
+ helpCommands,
932
+ activeNotice,
933
+ canSubmit,
934
+ submitBlocker,
935
+ submit,
936
+ handleKeyDown,
937
+ handlePaste,
938
+ handleFileChange,
939
+ focusInput: () => textareaRef.current?.focus(),
940
+ setValue: delivery.setValue,
941
+ value: delivery.value
942
+ };
943
+ }
944
+ var ComposerContext = createContext(null);
945
+ function useComposerController() {
946
+ const controller = useContext(ComposerContext);
947
+ if (!controller) throw new Error("useChatComposer must be used inside <Composer.Root>");
948
+ return controller;
949
+ }
950
+ function useChatComposer() {
951
+ return useComposerController();
952
+ }
953
+ function isRecord(value) {
954
+ return typeof value === "object" && value !== null;
955
+ }
956
+ var Root = forwardRef(function ComposerRoot({ controller, children, className, style, ...props }, forwardedRef) {
957
+ return /* @__PURE__ */ jsx3(ComposerContext.Provider, { value: controller, children: /* @__PURE__ */ jsxs3(
958
+ "div",
959
+ {
960
+ ...props,
961
+ ref: mergeRefs(controller.rootRef, forwardedRef),
962
+ "data-og-composer-id": controller.id,
963
+ className: cn("og-root", className),
964
+ style: { paddingBottom: "env(safe-area-inset-bottom)", ...style },
965
+ children: [
966
+ children,
967
+ /* @__PURE__ */ jsx3(ComposerAnnouncements, {})
968
+ ]
969
+ }
970
+ ) });
971
+ });
972
+ var Frame = forwardRef(function ComposerFrame({ className, ...props }, ref) {
973
+ return /* @__PURE__ */ jsx3("div", { ...props, ref, className: cn("relative", className) });
974
+ });
975
+ function CommandPalette2({ className }) {
976
+ const controller = useComposerController();
977
+ const setPaletteMounted = controller.setPaletteMounted;
978
+ useEffect(() => {
979
+ setPaletteMounted(true);
980
+ return () => setPaletteMounted(false);
981
+ }, [setPaletteMounted]);
982
+ if (!controller.paletteEnabled) return null;
983
+ return /* @__PURE__ */ jsx3("div", { className, children: /* @__PURE__ */ jsx3(
984
+ CommandPalette,
985
+ {
986
+ open: controller.palette.open && controller.confirmState === null,
987
+ items: controller.palette.items,
988
+ highlight: controller.palette.highlight,
989
+ onHighlight: controller.palette.setHighlight,
990
+ onRun: (index) => {
991
+ controller.palette.setHighlight(index);
992
+ void controller.palette.runAt(index);
993
+ },
994
+ argHintText: controller.palette.activeArgHint,
995
+ listboxId: controller.listboxId,
996
+ label: controller.messages.slashCommandsLabel,
997
+ dangerLabel: controller.messages.danger
998
+ }
999
+ ) });
1000
+ }
1001
+ var Surface = forwardRef(function ComposerSurface({ className, children, ...props }, ref) {
1002
+ const controller = useComposerController();
1003
+ return /* @__PURE__ */ jsxs3(
1004
+ "div",
1005
+ {
1006
+ ...props,
1007
+ ref,
1008
+ onDragOver: controller.attachments ? controller.handleDragOver : void 0,
1009
+ onDragLeave: controller.attachments ? controller.handleDragLeave : void 0,
1010
+ onDrop: controller.attachments ? controller.handleDrop : void 0,
1011
+ className: cn(
1012
+ "relative rounded-og-lg border border-og-border bg-og-surface-1 shadow-og-sm",
1013
+ "transition-[border-color,box-shadow] duration-200",
1014
+ "focus-within:border-og-accent/60 focus-within:shadow-og-glow",
1015
+ controller.dragging && "border-dashed border-og-accent",
1016
+ className
1017
+ ),
1018
+ children: [
1019
+ controller.dragging ? /* @__PURE__ */ jsx3(
1020
+ "div",
1021
+ {
1022
+ "aria-hidden": true,
1023
+ className: cn(
1024
+ "pointer-events-none absolute inset-0 z-10 flex items-center justify-center",
1025
+ "rounded-og-lg bg-og-surface-1/85 text-sm font-medium text-og-accent backdrop-blur-[1px]"
1026
+ ),
1027
+ children: /* @__PURE__ */ jsxs3("span", { className: "inline-flex items-center gap-2", children: [
1028
+ /* @__PURE__ */ jsx3(PaperclipIcon, { className: "size-4" }),
1029
+ controller.messages.dropFiles
1030
+ ] })
1031
+ }
1032
+ ) : null,
1033
+ children
1034
+ ]
1035
+ }
1036
+ );
1037
+ });
1038
+ function PausedState() {
1039
+ const controller = useComposerController();
1040
+ if (!controller.paused || !controller.effectiveControl || !controller.hasControl) return null;
1041
+ return /* @__PURE__ */ jsx3(
1042
+ WorkstreamPausedStrip,
1043
+ {
1044
+ control: controller.effectiveControl,
1045
+ queuedAheadCount: controller.queuedAheadCount,
1046
+ open: controller.controlDetailsOpen,
1047
+ busy: controller.resuming,
1048
+ sending: controller.sending,
1049
+ canControlWorkspace: controller.canControlWorkspace,
1050
+ controlLinks: controller.controlLinks,
1051
+ messages: controller.messages,
1052
+ onOpenChange: controller.setControlDetailsOpen,
1053
+ onResume: () => void controller.resume(),
1054
+ onResumeOption: (option) => void controller.resumeScope(option)
1055
+ }
1056
+ );
1057
+ }
1058
+ function RestoredResources() {
1059
+ const controller = useComposerController();
1060
+ if (controller.restoredResources.length === 0 || !controller.removeRestoredResource) return null;
1061
+ return /* @__PURE__ */ jsx3(
1062
+ RestoredResourceChips,
1063
+ {
1064
+ resources: controller.restoredResources,
1065
+ messages: controller.messages,
1066
+ onRemove: controller.removeRestoredResource
1067
+ }
1068
+ );
1069
+ }
1070
+ function Attachments() {
1071
+ const controller = useComposerController();
1072
+ if (!controller.attachments || controller.attachments.attachments.length === 0) return null;
1073
+ return /* @__PURE__ */ jsx3(
1074
+ AttachmentChips,
1075
+ {
1076
+ attachments: controller.attachments.attachments,
1077
+ messages: controller.messages,
1078
+ onRemove: controller.attachments.remove,
1079
+ onRetry: controller.attachments.retry
1080
+ }
1081
+ );
1082
+ }
1083
+ var Input = forwardRef(function ComposerInput({ rows = 1, placeholder, className, "aria-label": ariaLabel, ...props }, forwardedRef) {
1084
+ const controller = useComposerController();
1085
+ const paletteOpen = controller.paletteEnabled && controller.paletteMounted && controller.palette.open;
1086
+ return /* @__PURE__ */ jsx3(
1087
+ "textarea",
1088
+ {
1089
+ ...props,
1090
+ ref: mergeRefs(controller.textareaRef, forwardedRef),
1091
+ rows,
1092
+ value: controller.value,
1093
+ onChange: (event) => controller.setValue(event.target.value),
1094
+ onKeyDown: controller.handleKeyDown,
1095
+ onPaste: controller.handlePaste,
1096
+ placeholder: controller.paused && !controller.disabled ? controller.messages.pausedPlaceholder : placeholder ?? controller.messages.messagePlaceholder,
1097
+ disabled: controller.disabled,
1098
+ "aria-label": ariaLabel ?? controller.messages.inputLabel,
1099
+ "aria-keyshortcuts": "Enter Meta+Enter Control+Enter Shift+Enter",
1100
+ "aria-autocomplete": controller.paletteEnabled && controller.paletteMounted ? "list" : void 0,
1101
+ "aria-controls": paletteOpen ? controller.listboxId : void 0,
1102
+ "aria-activedescendant": paletteOpen ? `${controller.listboxId}-option-${controller.palette.highlight}` : void 0,
1103
+ className: cn(
1104
+ "block w-full resize-none bg-transparent px-4 pt-3.5 pb-1 text-base leading-6 md:text-og-md",
1105
+ "text-og-fg placeholder:text-og-fg-subtle focus:outline-none focus-visible:outline-none",
1106
+ "disabled:cursor-not-allowed disabled:opacity-60",
1107
+ className
1108
+ )
1109
+ }
1110
+ );
1111
+ });
1112
+ function Confirmation() {
1113
+ const controller = useComposerController();
1114
+ const command = controller.confirmState?.command;
1115
+ if (!command || !controller.confirmState) return null;
1116
+ return /* @__PURE__ */ jsx3(
1117
+ ConfirmBar,
1118
+ {
1119
+ command,
1120
+ messages: controller.messages,
1121
+ onCancel: () => controller.settleConfirmation(false),
1122
+ onConfirm: () => controller.settleConfirmation(true),
1123
+ returnFocusRef: controller.textareaRef
1124
+ }
1125
+ );
1126
+ }
1127
+ var Footer = forwardRef(function ComposerFooter({ className, ...props }, ref) {
1128
+ return /* @__PURE__ */ jsx3(
1129
+ "div",
1130
+ {
1131
+ ...props,
1132
+ ref,
1133
+ className: cn("flex items-end gap-2 px-2.5 pb-2.5 pt-1", className)
1134
+ }
1135
+ );
1136
+ });
1137
+ var Controls = forwardRef(
1138
+ function ComposerControls({ className, ...props }, ref) {
1139
+ return /* @__PURE__ */ jsx3(
1140
+ "span",
1141
+ {
1142
+ ...props,
1143
+ ref,
1144
+ className: cn("flex min-w-0 flex-1 flex-wrap items-center gap-1.5", className)
1145
+ }
1146
+ );
1147
+ }
1148
+ );
1149
+ var Hint = forwardRef(function ComposerHint({ className, children, ...props }, ref) {
1150
+ const controller = useComposerController();
1151
+ return /* @__PURE__ */ jsx3(
1152
+ "span",
1153
+ {
1154
+ ...props,
1155
+ ref,
1156
+ className: cn("min-w-0 flex-1 px-1.5 text-og-xs text-og-fg-subtle max-sm:hidden", className),
1157
+ children: children ?? controller.messages.keyboardHint
1158
+ }
1159
+ );
1160
+ });
1161
+ var Actions = forwardRef(function ComposerActions({ className, ...props }, ref) {
1162
+ return /* @__PURE__ */ jsx3(
1163
+ "span",
1164
+ {
1165
+ ...props,
1166
+ ref,
1167
+ className: cn("ml-auto flex shrink-0 items-center gap-1.5", className)
1168
+ }
1169
+ );
1170
+ });
1171
+ var AttachButton = forwardRef(
1172
+ function ComposerAttachButton({ accept, multiple = true, className, "aria-label": ariaLabel, title, children, ...props }, ref) {
1173
+ const controller = useComposerController();
1174
+ if (!controller.attachments) return null;
1175
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
1176
+ /* @__PURE__ */ jsx3(
1177
+ "input",
1178
+ {
1179
+ ref: controller.fileInputRef,
1180
+ type: "file",
1181
+ accept,
1182
+ multiple,
1183
+ className: "hidden",
1184
+ onChange: controller.handleFileChange
1185
+ }
1186
+ ),
1187
+ /* @__PURE__ */ jsx3(
1188
+ "button",
1189
+ {
1190
+ ...props,
1191
+ ref,
1192
+ type: "button",
1193
+ disabled: controller.disabled,
1194
+ onClick: () => controller.fileInputRef.current?.click(),
1195
+ "aria-label": ariaLabel ?? controller.messages.attachFiles,
1196
+ title: title ?? controller.messages.attachFiles,
1197
+ className: cn(
1198
+ "inline-flex size-8 items-center justify-center rounded-og-md",
1199
+ "text-og-fg-muted transition-colors duration-150 hover:bg-og-surface-2 hover:text-og-fg",
1200
+ "disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:size-11",
1201
+ className
1202
+ ),
1203
+ children: children ?? /* @__PURE__ */ jsx3(PaperclipIcon, { className: "size-4" })
1204
+ }
1205
+ )
1206
+ ] });
1207
+ }
1208
+ );
1209
+ function ModelPicker2({
1210
+ models,
1211
+ value,
1212
+ onChange,
1213
+ label,
1214
+ className
1215
+ }) {
1216
+ const controller = useComposerController();
1217
+ return /* @__PURE__ */ jsx3(
1218
+ ModelPicker,
1219
+ {
1220
+ models,
1221
+ value,
1222
+ onChange: (modelId) => onChange?.(modelId),
1223
+ disabled: controller.disabled,
1224
+ label: label ?? controller.messages.modelLabel,
1225
+ className
1226
+ }
1227
+ );
1228
+ }
1229
+ var PauseButton = forwardRef(
1230
+ function ComposerPauseButton({ className, "aria-label": ariaLabel, title, children, ...props }, ref) {
1231
+ const controller = useComposerController();
1232
+ if (!controller.effectiveControl || controller.paused || !controller.hasControl) return null;
1233
+ const busy = controller.pausing || controller.resuming;
1234
+ return /* @__PURE__ */ jsx3(
1235
+ "button",
1236
+ {
1237
+ ...props,
1238
+ ref: mergeRefs(controller.pauseButtonRef, ref),
1239
+ type: "button",
1240
+ onClick: () => void controller.pause(),
1241
+ disabled: busy,
1242
+ "aria-label": ariaLabel ?? controller.messages.pauseAriaLabel,
1243
+ title: title ?? controller.messages.pauseTitle,
1244
+ className: cn(
1245
+ "inline-flex size-8 items-center justify-center rounded-og-md border border-og-border pointer-coarse:size-11",
1246
+ "bg-og-surface-2 text-og-fg-muted transition-colors duration-150",
1247
+ "hover:border-og-status-waiting/50 hover:text-og-status-waiting",
1248
+ "disabled:opacity-50",
1249
+ className
1250
+ ),
1251
+ children: children ?? (busy ? /* @__PURE__ */ jsx3(LoaderCircleIcon, { className: "size-3.5 animate-og-spin" }) : /* @__PURE__ */ jsx3(PauseIcon, { className: "size-3.5 fill-current" }))
1252
+ }
1253
+ );
1254
+ }
1255
+ );
1256
+ var SendButton = forwardRef(
1257
+ function ComposerSendButton({ className, "aria-label": ariaLabel, title, children, ...props }, ref) {
1258
+ const controller = useComposerController();
1259
+ return /* @__PURE__ */ jsx3(
1260
+ "button",
1261
+ {
1262
+ ...props,
1263
+ ref,
1264
+ type: "button",
1265
+ onClick: () => void controller.submit("queue"),
1266
+ disabled: !controller.canSubmit,
1267
+ "aria-label": ariaLabel ?? (controller.paused ? controller.messages.sendAndResumeAriaLabel : controller.messages.sendMessageAriaLabel),
1268
+ title: title ?? (controller.paused ? controller.messages.sendAndResumeTitle : controller.messages.sendTitle),
1269
+ className: cn(
1270
+ "inline-flex size-8 items-center justify-center rounded-og-md pointer-coarse:size-11",
1271
+ "bg-og-accent text-og-accent-fg shadow-og-sm",
1272
+ "transition-[background-color,transform,opacity] duration-150 ease-og-spring",
1273
+ "hover:bg-og-accent-strong active:scale-95",
1274
+ "disabled:cursor-not-allowed disabled:bg-og-surface-3 disabled:text-og-fg-subtle disabled:shadow-none",
1275
+ className
1276
+ ),
1277
+ children: children ?? (controller.sending ? /* @__PURE__ */ jsx3(LoaderCircleIcon, { className: "size-4 animate-og-spin" }) : /* @__PURE__ */ jsx3(ArrowUpIcon, { className: "size-4" }))
1278
+ }
1279
+ );
1280
+ }
1281
+ );
1282
+ function Help() {
1283
+ const controller = useComposerController();
1284
+ return /* @__PURE__ */ jsx3(AnimatePresence2, { children: controller.helpOpen ? /* @__PURE__ */ jsx3(
1285
+ HelpPanel,
1286
+ {
1287
+ commands: controller.helpCommands,
1288
+ messages: controller.messages,
1289
+ onClose: () => controller.setHelpOpen(false)
1290
+ }
1291
+ ) : null });
1292
+ }
1293
+ function Status() {
1294
+ const controller = useComposerController();
1295
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
1296
+ /* @__PURE__ */ jsx3(AnimatePresence2, { children: controller.activeNotice ? /* @__PURE__ */ jsx3(
1297
+ motion2.p,
1298
+ {
1299
+ initial: { opacity: 0, height: 0 },
1300
+ animate: { opacity: 1, height: "auto" },
1301
+ exit: { opacity: 0, height: 0 },
1302
+ className: cn(
1303
+ "overflow-hidden px-1 pt-1.5 text-xs",
1304
+ controller.activeNotice.tone === "ok" ? "text-og-fg-muted" : "text-og-status-failed"
1305
+ ),
1306
+ children: controller.activeNotice.message
1307
+ }
1308
+ ) : null }),
1309
+ controller.draftConflict ? /* @__PURE__ */ jsxs3("div", { className: "mt-1.5 flex flex-wrap items-center gap-2 px-1 text-og-xs text-og-status-failed", children: [
1310
+ /* @__PURE__ */ jsx3("span", { className: "min-w-0 flex-1", children: controller.messages.draftConflict }),
1311
+ /* @__PURE__ */ jsx3(
1312
+ "button",
1313
+ {
1314
+ type: "button",
1315
+ className: "underline underline-offset-2",
1316
+ onClick: () => void controller.resolveDraftConflict?.("use_remote"),
1317
+ children: controller.messages.useOtherDraft
1318
+ }
1319
+ ),
1320
+ /* @__PURE__ */ jsx3(
1321
+ "button",
1322
+ {
1323
+ type: "button",
1324
+ className: "font-medium underline underline-offset-2",
1325
+ onClick: () => void controller.resolveDraftConflict?.("keep_mine"),
1326
+ children: controller.messages.keepMine
1327
+ }
1328
+ )
1329
+ ] }) : controller.draftSaving ? /* @__PURE__ */ jsx3("p", { className: "px-1 pt-1 text-og-xs text-og-fg-subtle", children: controller.messages.savingDraft }) : null
1330
+ ] });
1331
+ }
1332
+ function ComposerAnnouncements() {
1333
+ const controller = useComposerController();
1334
+ return /* @__PURE__ */ jsxs3("div", { className: "sr-only", children: [
1335
+ controller.activeNotice ? /* @__PURE__ */ jsx3("p", { role: controller.activeNotice.tone === "error" ? "alert" : "status", children: controller.activeNotice.message }) : null,
1336
+ controller.draftConflict ? /* @__PURE__ */ jsx3("p", { role: "alert", children: controller.messages.draftConflict }) : controller.draftSaving ? /* @__PURE__ */ jsx3("p", { role: "status", children: controller.messages.savingDraft }) : null
1337
+ ] });
1338
+ }
1339
+ function mergeRefs(...refs) {
1340
+ return (node) => {
1341
+ for (const ref of refs) {
1342
+ if (typeof ref === "function") ref(node);
1343
+ else if (ref) ref.current = node;
1344
+ }
1345
+ };
1346
+ }
1347
+ function WorkstreamPausedStrip({
1348
+ control,
1349
+ queuedAheadCount,
1350
+ open,
1351
+ busy,
1352
+ sending,
1353
+ canControlWorkspace,
1354
+ controlLinks,
1355
+ messages,
1356
+ onOpenChange,
1357
+ onResume,
1358
+ onResumeOption
1359
+ }) {
1360
+ const blocker = control.primaryBlocker;
1361
+ const cause = blocker?.kind === "workspace" ? messages.workspacePaused : control.directState === "paused" ? messages.pausedHere : messages.pausedBy(blocker?.displayName ?? messages.parentBlocker);
1362
+ const primary = control.resumeOptions.find((option) => option.scope === "selected");
1363
+ const broaderOptions = control.resumeOptions.filter(
1364
+ (option) => option !== primary && option.scope !== "selected" && (option.scope !== "workspace" || canControlWorkspace)
1365
+ );
1366
+ return /* @__PURE__ */ jsxs3("div", { className: "border-b border-og-status-waiting/25 bg-og-status-waiting/[0.07]", children: [
1367
+ /* @__PURE__ */ jsxs3("div", { className: "flex min-w-0 items-center gap-2 px-3 py-2", children: [
1368
+ /* @__PURE__ */ jsx3(PauseIcon, { className: "size-3.5 shrink-0 text-og-status-waiting" }),
1369
+ /* @__PURE__ */ jsxs3(
1370
+ "button",
1371
+ {
1372
+ type: "button",
1373
+ className: "min-w-0 flex-1 text-left",
1374
+ onClick: () => onOpenChange(!open),
1375
+ "aria-expanded": open,
1376
+ children: [
1377
+ /* @__PURE__ */ jsx3("span", { className: "block truncate text-og-xs font-medium text-og-fg", children: cause }),
1378
+ /* @__PURE__ */ jsx3("span", { className: "block truncate text-[11px] text-og-fg-muted", children: queuedAheadCount > 0 ? messages.queuedAhead(queuedAheadCount) : sending ? messages.resumingAndSending : messages.nextMessageResumes })
1379
+ ]
1380
+ }
1381
+ ),
1382
+ /* @__PURE__ */ jsxs3(
1383
+ "button",
1384
+ {
1385
+ type: "button",
1386
+ "aria-label": messages.resumeThisWorkstream,
1387
+ className: "inline-flex min-h-8 shrink-0 items-center gap-1.5 rounded-og-md border border-og-status-waiting/35 bg-og-surface-1 px-2.5 text-og-xs font-medium text-og-fg hover:bg-og-surface-2 pointer-coarse:min-h-11",
1388
+ disabled: busy,
1389
+ onClick: onResume,
1390
+ children: [
1391
+ busy ? /* @__PURE__ */ jsx3(LoaderCircleIcon, { className: "size-3.5 animate-og-spin" }) : /* @__PURE__ */ jsx3(PlayIcon, { className: "size-3.5 fill-current" }),
1392
+ /* @__PURE__ */ jsx3("span", { className: "hidden min-[400px]:inline", children: messages.resumeThisWorkstream }),
1393
+ /* @__PURE__ */ jsx3("span", { className: "min-[400px]:hidden", children: messages.resumeShort })
1394
+ ]
1395
+ }
1396
+ ),
1397
+ /* @__PURE__ */ jsx3(
1398
+ "button",
1399
+ {
1400
+ type: "button",
1401
+ className: "inline-flex size-8 shrink-0 items-center justify-center rounded-og-md text-og-fg-muted hover:bg-og-surface-2 pointer-coarse:size-11",
1402
+ onClick: () => onOpenChange(!open),
1403
+ "aria-label": open ? messages.hidePauseDetails : messages.showPauseDetails,
1404
+ children: /* @__PURE__ */ jsx3(ChevronDownIcon2, { className: cn("size-3.5 transition-transform", open && "rotate-180") })
1405
+ }
1406
+ )
1407
+ ] }),
1408
+ open ? /* @__PURE__ */ jsxs3("div", { className: "border-t border-og-status-waiting/20 px-3 pb-3 pt-2", children: [
1409
+ /* @__PURE__ */ jsx3("ul", { className: "grid gap-2", "aria-label": messages.pauseReasonsLabel, children: control.blockers.map((entry, index) => /* @__PURE__ */ jsxs3(
1410
+ "li",
1411
+ {
1412
+ className: "text-og-xs",
1413
+ children: [
1414
+ /* @__PURE__ */ jsx3("span", { className: "font-medium text-og-fg", children: index === 0 ? messages.pausedByLabel : messages.alsoPausedByLabel }),
1415
+ blockerHref(entry, controlLinks) ? /* @__PURE__ */ jsx3(
1416
+ "a",
1417
+ {
1418
+ href: blockerHref(entry, controlLinks),
1419
+ className: "font-medium text-og-fg underline decoration-og-border-strong underline-offset-2 hover:text-og-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-ring/40",
1420
+ children: entry.displayName
1421
+ }
1422
+ ) : /* @__PURE__ */ jsx3("span", { className: "font-medium text-og-fg", children: entry.displayName }),
1423
+ entry.reason ? /* @__PURE__ */ jsxs3("span", { className: "text-og-fg-muted", children: [
1424
+ " \xB7 ",
1425
+ entry.reason
1426
+ ] }) : null,
1427
+ entry.actor ? /* @__PURE__ */ jsxs3("span", { className: "text-og-fg-subtle", children: [
1428
+ " \xB7 ",
1429
+ entry.actor
1430
+ ] }) : null,
1431
+ entry.changedAt ? /* @__PURE__ */ jsxs3("span", { className: "text-og-fg-subtle", children: [
1432
+ " ",
1433
+ "\xB7 ",
1434
+ messages.formatRelativeTime(entry.changedAt)
1435
+ ] }) : null
1436
+ ]
1437
+ },
1438
+ `${entry.kind}-${entry.sessionId ?? "workspace"}-${entry.revision}`
1439
+ )) }),
1440
+ broaderOptions.length > 0 ? /* @__PURE__ */ jsx3("div", { className: "mt-2 flex flex-wrap gap-1.5", children: broaderOptions.map((option) => /* @__PURE__ */ jsxs3(
1441
+ "button",
1442
+ {
1443
+ type: "button",
1444
+ disabled: busy,
1445
+ title: option.impactCopy,
1446
+ onClick: () => onResumeOption(option),
1447
+ className: "rounded-og-md border border-og-border bg-og-surface-1 px-2 py-1 text-og-xs text-og-fg-muted hover:bg-og-surface-2 hover:text-og-fg pointer-coarse:min-h-10",
1448
+ children: [
1449
+ /* @__PURE__ */ jsx3("span", { className: "block font-medium", children: option.scope === "workspace" ? messages.resumeWorkspace : messages.resumeFromSession }),
1450
+ /* @__PURE__ */ jsx3("span", { className: "block text-[10px] text-og-fg-subtle", children: option.selectedStateAfter === "active" ? messages.sessionCanRun : messages.stillPausedBy(
1451
+ option.remainingPrimaryBlocker?.displayName ?? messages.narrowerPause
1452
+ ) })
1453
+ ]
1454
+ },
1455
+ `${option.scope}-${option.targetId ?? "selected"}`
1456
+ )) }) : null
1457
+ ] }) : null
1458
+ ] });
1459
+ }
1460
+ function blockerHref(blocker, links) {
1461
+ if (blocker.kind === "workspace") return links?.workspaceHref;
1462
+ return blocker.sessionId ? links?.sessionHref?.(blocker.sessionId) : void 0;
1463
+ }
1464
+ function RestoredResourceChips({
1465
+ resources,
1466
+ messages,
1467
+ onRemove
1468
+ }) {
1469
+ return /* @__PURE__ */ jsx3(
1470
+ "div",
1471
+ {
1472
+ className: "flex flex-wrap gap-1.5 border-b border-og-border px-3 py-2",
1473
+ "aria-label": messages.restoredResourcesLabel,
1474
+ children: resources.map((resource, index) => /* @__PURE__ */ jsxs3(
1475
+ "span",
1476
+ {
1477
+ className: "inline-flex min-w-0 max-w-full items-center gap-1.5 rounded-og-md border border-og-border bg-og-surface-2 px-2 py-1 text-og-xs text-og-fg-muted",
1478
+ children: [
1479
+ /* @__PURE__ */ jsx3(FileIcon, { className: "size-3 shrink-0" }),
1480
+ /* @__PURE__ */ jsx3("span", { className: "truncate", children: resource.kind === "file" ? messages.restoredFile(resource.fileId) : resource.uri }),
1481
+ /* @__PURE__ */ jsx3(
1482
+ "button",
1483
+ {
1484
+ type: "button",
1485
+ className: "shrink-0 hover:text-og-fg",
1486
+ onClick: () => onRemove(index),
1487
+ "aria-label": messages.removeRestoredResource(index),
1488
+ children: /* @__PURE__ */ jsx3(XIcon, { className: "size-3" })
1489
+ }
1490
+ )
1491
+ ]
1492
+ },
1493
+ `${resource.kind}-${resource.kind === "file" ? resource.fileId : resource.uri}`
1494
+ ))
1495
+ }
1496
+ );
1497
+ }
1498
+ function ConfirmBar({
1499
+ command,
1500
+ messages,
1501
+ onCancel,
1502
+ onConfirm,
1503
+ returnFocusRef
1504
+ }) {
1505
+ const confirmRef = useRef2(null);
1506
+ const descriptionId = useId2();
1507
+ useEffect(() => {
1508
+ const returnTo = returnFocusRef?.current ?? null;
1509
+ confirmRef.current?.focus();
1510
+ return () => returnTo?.focus();
1511
+ }, [returnFocusRef]);
1512
+ return /* @__PURE__ */ jsxs3(
1513
+ "div",
1514
+ {
1515
+ role: "alertdialog",
1516
+ "aria-label": messages.confirmCommand(command.name),
1517
+ "aria-describedby": descriptionId,
1518
+ "data-testid": "danger-confirm",
1519
+ onKeyDown: (event) => {
1520
+ if (event.key === "Escape") {
1521
+ event.preventDefault();
1522
+ event.stopPropagation();
1523
+ onCancel();
1524
+ }
1525
+ },
1526
+ className: "flex items-end justify-between gap-2 px-2.5 pb-2.5 pt-1",
1527
+ children: [
1528
+ /* @__PURE__ */ jsx3("span", { id: descriptionId, className: "min-w-0 flex-1 px-1.5 text-og-sm text-og-status-failed", children: messages.confirmDescription(command) }),
1529
+ /* @__PURE__ */ jsxs3("span", { className: "flex shrink-0 items-center gap-1.5", children: [
1530
+ /* @__PURE__ */ jsx3(
1531
+ "button",
1532
+ {
1533
+ type: "button",
1534
+ onClick: onCancel,
1535
+ className: "rounded-og-md border border-og-border bg-og-surface-2 px-2.5 py-1 text-og-sm text-og-fg-muted hover:bg-og-surface-3 pointer-coarse:min-h-11",
1536
+ children: messages.cancel
1537
+ }
1538
+ ),
1539
+ /* @__PURE__ */ jsx3(
1540
+ "button",
1541
+ {
1542
+ ref: confirmRef,
1543
+ type: "button",
1544
+ onClick: onConfirm,
1545
+ className: "rounded-og-md border border-og-status-failed/50 bg-og-status-failed/15 px-2.5 py-1 text-og-sm text-og-status-failed hover:bg-og-status-failed/25 pointer-coarse:min-h-11",
1546
+ children: messages.runCommand(command.name)
1547
+ }
1548
+ )
1549
+ ] })
1550
+ ]
1551
+ }
1552
+ );
1553
+ }
1554
+ function AttachmentChips({
1555
+ attachments,
1556
+ messages,
1557
+ onRemove,
1558
+ onRetry
1559
+ }) {
1560
+ return /* @__PURE__ */ jsx3("div", { className: "flex flex-wrap gap-2 border-b border-og-border px-3 py-2", children: attachments.map((attachment) => {
1561
+ const failed = attachment.status === "failed";
1562
+ const statusText = attachment.status === "uploading" ? messages.uploading : failed ? attachment.error || messages.uploadFailed : messages.formatBytes(attachment.sizeBytes);
1563
+ return /* @__PURE__ */ jsxs3(
1564
+ "div",
1565
+ {
1566
+ className: cn(
1567
+ "flex min-w-0 max-w-[240px] items-center gap-2 rounded-og-md border px-2 py-1.5 text-og-sm",
1568
+ failed ? "border-og-status-failed/40 bg-og-status-failed/10" : "border-og-border bg-og-surface-2"
1569
+ ),
1570
+ children: [
1571
+ attachment.previewUrl ? /* @__PURE__ */ jsx3(
1572
+ "img",
1573
+ {
1574
+ src: attachment.previewUrl,
1575
+ alt: "",
1576
+ className: "size-8 shrink-0 rounded object-cover"
1577
+ }
1578
+ ) : attachment.contentType.startsWith("image/") ? /* @__PURE__ */ jsx3(ImageIcon, { className: "size-4 shrink-0 text-og-fg-muted" }) : /* @__PURE__ */ jsx3(FileIcon, { className: "size-4 shrink-0 text-og-fg-muted" }),
1579
+ /* @__PURE__ */ jsxs3("div", { className: "min-w-0 flex-1", children: [
1580
+ /* @__PURE__ */ jsx3("div", { className: "truncate font-medium text-og-fg", children: attachment.name }),
1581
+ /* @__PURE__ */ jsx3(
1582
+ "div",
1583
+ {
1584
+ className: cn(
1585
+ "truncate text-og-xs",
1586
+ failed ? "text-og-status-failed" : "text-og-fg-subtle"
1587
+ ),
1588
+ title: failed ? statusText : void 0,
1589
+ children: statusText
1590
+ }
1591
+ )
1592
+ ] }),
1593
+ attachment.status === "uploading" ? /* @__PURE__ */ jsx3(LoaderCircleIcon, { className: "size-3.5 shrink-0 animate-og-spin" }) : null,
1594
+ failed && onRetry ? /* @__PURE__ */ jsx3(
1595
+ "button",
1596
+ {
1597
+ type: "button",
1598
+ onClick: () => onRetry(attachment.id),
1599
+ className: "shrink-0 rounded-og-xs p-1 text-og-fg-muted hover:bg-og-surface-1 hover:text-og-fg pointer-coarse:size-10",
1600
+ "aria-label": messages.retryAttachment(attachment.name),
1601
+ title: messages.retryUpload,
1602
+ children: /* @__PURE__ */ jsx3(RotateCwIcon, { className: "size-3.5" })
1603
+ }
1604
+ ) : null,
1605
+ /* @__PURE__ */ jsx3(
1606
+ "button",
1607
+ {
1608
+ type: "button",
1609
+ onClick: () => onRemove(attachment.id),
1610
+ className: "shrink-0 rounded-og-xs p-1 text-og-fg-muted hover:bg-og-surface-1 hover:text-og-fg pointer-coarse:size-10",
1611
+ "aria-label": messages.removeAttachment(attachment.name),
1612
+ children: /* @__PURE__ */ jsx3(XIcon, { className: "size-3.5" })
1613
+ }
1614
+ )
1615
+ ]
1616
+ },
1617
+ attachment.id
1618
+ );
1619
+ }) });
1620
+ }
1621
+ function HelpPanel({
1622
+ commands,
1623
+ messages,
1624
+ onClose
1625
+ }) {
1626
+ return /* @__PURE__ */ jsxs3(
1627
+ motion2.div,
1628
+ {
1629
+ initial: { opacity: 0, height: 0 },
1630
+ animate: { opacity: 1, height: "auto" },
1631
+ exit: { opacity: 0, height: 0 },
1632
+ className: "mt-2 overflow-hidden rounded-og-lg border border-og-border bg-og-surface-2",
1633
+ children: [
1634
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between border-b border-og-border px-3 py-1.5", children: [
1635
+ /* @__PURE__ */ jsx3("span", { className: "text-og-sm font-medium text-og-fg", children: messages.commands }),
1636
+ /* @__PURE__ */ jsx3(
1637
+ "button",
1638
+ {
1639
+ type: "button",
1640
+ onClick: onClose,
1641
+ className: "text-og-xs text-og-fg-subtle hover:text-og-fg",
1642
+ children: messages.close
1643
+ }
1644
+ )
1645
+ ] }),
1646
+ /* @__PURE__ */ jsx3("ul", { className: "py-1", children: commands.map((command) => {
1647
+ const hint = argHint(command.args);
1648
+ return /* @__PURE__ */ jsxs3("li", { className: "flex items-baseline gap-2 px-3 py-1", children: [
1649
+ /* @__PURE__ */ jsxs3("span", { className: "font-mono text-og-sm text-og-accent", children: [
1650
+ "/",
1651
+ command.name,
1652
+ hint ? /* @__PURE__ */ jsx3("span", { className: "ml-1 text-og-fg-subtle", children: hint }) : null
1653
+ ] }),
1654
+ /* @__PURE__ */ jsx3("span", { className: "text-og-sm text-og-fg-muted", children: command.description }),
1655
+ command.danger ? /* @__PURE__ */ jsx3("span", { className: "ml-auto rounded-og-xs bg-og-status-failed/15 px-1 text-og-xs uppercase tracking-wide text-og-status-failed", children: messages.danger }) : null
1656
+ ] }, command.name);
1657
+ }) })
1658
+ ]
1659
+ }
1660
+ );
1661
+ }
1662
+
1663
+ // src/hooks/use-transcription.ts
1664
+ import {
1665
+ authorizeTranscriptionAdapter,
1666
+ createTranscriptionSessionRequest
1667
+ } from "@opengeni/sdk";
1668
+ import { useCallback as useCallback3, useEffect as useEffect2, useMemo as useMemo4, useRef as useRef3, useState as useState3 } from "react";
1669
+ var INITIAL_TRANSCRIPTION_CONTROL_STATE = {
1670
+ status: "idle",
1671
+ generation: 0,
1672
+ localSessionId: null,
1673
+ providerSessionId: null,
1674
+ lastSequence: 0,
1675
+ partial: "",
1676
+ error: null,
1677
+ acceptedFinalIds: [],
1678
+ audioMilliseconds: 0,
1679
+ costUsd: null
1680
+ };
1681
+ function transitionTranscriptionControl(state, action) {
1682
+ if (action.type === "reset") {
1683
+ return {
1684
+ state: { ...INITIAL_TRANSCRIPTION_CONTROL_STATE, generation: state.generation },
1685
+ commit: null
1686
+ };
1687
+ }
1688
+ if (action.type === "start") {
1689
+ if (action.generation <= state.generation) return { state, commit: null };
1690
+ return {
1691
+ state: {
1692
+ ...INITIAL_TRANSCRIPTION_CONTROL_STATE,
1693
+ status: "requesting-permission",
1694
+ generation: action.generation,
1695
+ localSessionId: action.localSessionId
1696
+ },
1697
+ commit: null
1698
+ };
1699
+ }
1700
+ if (action.generation !== state.generation) return { state, commit: null };
1701
+ if (action.type === "start.failed") {
1702
+ if (state.status === "closed" || state.status === "error") return { state, commit: null };
1703
+ return {
1704
+ state: {
1705
+ ...state,
1706
+ status: "error",
1707
+ partial: "",
1708
+ error: { code: normalizeTranscriptionErrorCode(action.code), recoverable: false }
1709
+ },
1710
+ commit: null
1711
+ };
1712
+ }
1713
+ if (action.type === "cancel") {
1714
+ if (!isActiveTranscriptionStatus(state.status)) return { state, commit: null };
1715
+ return {
1716
+ state: { ...state, status: "cancelling", partial: "", error: null },
1717
+ commit: null
1718
+ };
1719
+ }
1720
+ if (action.type === "cancel.settled") {
1721
+ if (state.status !== "cancelling") return { state, commit: null };
1722
+ return { state: { ...state, status: "closed", partial: "" }, commit: null };
1723
+ }
1724
+ const event = action.event;
1725
+ if (event.localSessionId !== state.localSessionId) return { state, commit: null };
1726
+ if (!Number.isSafeInteger(event.sequence) || event.sequence <= state.lastSequence) {
1727
+ return { state, commit: null };
1728
+ }
1729
+ if (state.status === "closed" || state.status === "error") return { state, commit: null };
1730
+ if (state.status === "cancelling" && event.type !== "session.closed") {
1731
+ return { state: { ...state, lastSequence: event.sequence }, commit: null };
1732
+ }
1733
+ const sequenced = { ...state, lastSequence: event.sequence };
1734
+ switch (event.type) {
1735
+ case "permission.requested":
1736
+ return {
1737
+ state: { ...sequenced, status: "requesting-permission", partial: "", error: null },
1738
+ commit: null
1739
+ };
1740
+ case "session.opened":
1741
+ return {
1742
+ state: {
1743
+ ...sequenced,
1744
+ status: "listening",
1745
+ providerSessionId: event.providerSessionId,
1746
+ error: null
1747
+ },
1748
+ commit: null
1749
+ };
1750
+ case "transcript.partial":
1751
+ return {
1752
+ state: { ...sequenced, status: "listening", partial: event.text, error: null },
1753
+ commit: null
1754
+ };
1755
+ case "transcript.final": {
1756
+ const text = event.text.trim();
1757
+ const duplicate = state.acceptedFinalIds.includes(event.providerAcceptanceId);
1758
+ const acceptedFinalIds = duplicate || text.length === 0 ? state.acceptedFinalIds : [...state.acceptedFinalIds, event.providerAcceptanceId];
1759
+ return {
1760
+ state: {
1761
+ ...sequenced,
1762
+ status: "listening",
1763
+ partial: "",
1764
+ error: null,
1765
+ acceptedFinalIds
1766
+ },
1767
+ commit: duplicate || text.length === 0 ? null : text
1768
+ };
1769
+ }
1770
+ case "usage":
1771
+ return {
1772
+ state: {
1773
+ ...sequenced,
1774
+ audioMilliseconds: Math.max(state.audioMilliseconds, event.audioMilliseconds),
1775
+ costUsd: event.costUsd
1776
+ },
1777
+ commit: null
1778
+ };
1779
+ case "session.reconnecting":
1780
+ return {
1781
+ state: { ...sequenced, status: "reconnecting", partial: "", error: null },
1782
+ commit: null
1783
+ };
1784
+ case "session.error":
1785
+ return {
1786
+ state: {
1787
+ ...sequenced,
1788
+ status: event.recoverable ? "reconnecting" : "error",
1789
+ partial: "",
1790
+ error: {
1791
+ code: normalizeTranscriptionErrorCode(event.code),
1792
+ recoverable: event.recoverable
1793
+ }
1794
+ },
1795
+ commit: null
1796
+ };
1797
+ case "session.closed":
1798
+ return {
1799
+ state: {
1800
+ ...sequenced,
1801
+ status: event.reason === "error" ? "error" : "closed",
1802
+ partial: ""
1803
+ },
1804
+ commit: null
1805
+ };
1806
+ }
1807
+ }
1808
+ function appendFinalTranscript(draft, transcript) {
1809
+ const final = transcript.trim();
1810
+ if (!final) return draft;
1811
+ if (!draft) return final;
1812
+ return /\s$/u.test(draft) ? `${draft}${final}` : `${draft} ${final}`;
1813
+ }
1814
+ var DEFAULT_TRANSCRIPTION_LIFECYCLE_TIMEOUTS = {
1815
+ startMs: 15e3,
1816
+ cleanupMs: 2e3
1817
+ };
1818
+ function useTranscription({
1819
+ adapter,
1820
+ policy,
1821
+ selection = { kind: "primary" },
1822
+ value,
1823
+ setValue,
1824
+ focusInput,
1825
+ disabled = false,
1826
+ createLocalSessionId = defaultLocalSessionId,
1827
+ lifecycleTimeouts,
1828
+ onDiagnostic
1829
+ }) {
1830
+ const [state, setState] = useState3(INITIAL_TRANSCRIPTION_CONTROL_STATE);
1831
+ const stateRef = useRef3(state);
1832
+ const valueRef = useRef3(value);
1833
+ const sessionRef = useRef3(null);
1834
+ const pendingStartRef = useRef3(null);
1835
+ const activePolicyRef = useRef3(null);
1836
+ valueRef.current = value;
1837
+ const startTimeoutMs = normalizeTimeout(
1838
+ lifecycleTimeouts?.startMs,
1839
+ DEFAULT_TRANSCRIPTION_LIFECYCLE_TIMEOUTS.startMs
1840
+ );
1841
+ const cleanupTimeoutMs = normalizeTimeout(
1842
+ lifecycleTimeouts?.cleanupMs,
1843
+ DEFAULT_TRANSCRIPTION_LIFECYCLE_TIMEOUTS.cleanupMs
1844
+ );
1845
+ const reportDiagnostic = useCallback3(
1846
+ (diagnostic) => {
1847
+ if (!onDiagnostic) return;
1848
+ try {
1849
+ onDiagnostic(sanitizeTranscriptionDiagnostic(diagnostic));
1850
+ } catch {
1851
+ }
1852
+ },
1853
+ [onDiagnostic]
1854
+ );
1855
+ const cleanupRuntimeRef = useRef3({ cleanupTimeoutMs, reportDiagnostic });
1856
+ useEffect2(() => {
1857
+ cleanupRuntimeRef.current = { cleanupTimeoutMs, reportDiagnostic };
1858
+ }, [cleanupTimeoutMs, reportDiagnostic]);
1859
+ const policyRevision = useMemo4(
1860
+ () => transcriptionPolicyRevision(policy, selection),
1861
+ [policy, selection]
1862
+ );
1863
+ const authorization = useMemo4(
1864
+ () => adapter ? authorizeTranscriptionAdapter(policy, adapter.descriptor, selection) : null,
1865
+ [adapter, policy, selection]
1866
+ );
1867
+ const unavailableReason = disabled ? "composer_disabled" : !policy.enabled ? "disabled" : !policy.acceptanceId ? "unaccepted" : !policy.primary ? "target_missing" : !adapter ? "adapter_missing" : authorization?.authorized ? null : authorization?.reason ?? "unaccepted";
1868
+ const apply = useCallback3(
1869
+ (action) => {
1870
+ const transition = transitionTranscriptionControl(stateRef.current, action);
1871
+ if (transition.state !== stateRef.current) {
1872
+ stateRef.current = transition.state;
1873
+ setState(transition.state);
1874
+ }
1875
+ if (transition.commit !== null) {
1876
+ const next = appendFinalTranscript(valueRef.current, transition.commit);
1877
+ valueRef.current = next;
1878
+ setValue(next);
1879
+ focusInput();
1880
+ }
1881
+ return transition.state;
1882
+ },
1883
+ [focusInput, setValue]
1884
+ );
1885
+ const releaseTerminalSession = useCallback3(
1886
+ (generation) => {
1887
+ const active = sessionRef.current;
1888
+ if (!active || active.generation !== generation) return;
1889
+ sessionRef.current = null;
1890
+ if (activePolicyRef.current?.generation === generation) activePolicyRef.current = null;
1891
+ detachSessionCleanup(active.session, {
1892
+ cleanupTimeoutMs,
1893
+ reportDiagnostic
1894
+ });
1895
+ },
1896
+ [cleanupTimeoutMs, reportDiagnostic]
1897
+ );
1898
+ const start = useCallback3(async () => {
1899
+ if (!adapter || unavailableReason !== null || isActiveTranscriptionStatus(stateRef.current.status)) {
1900
+ return false;
1901
+ }
1902
+ const generation = stateRef.current.generation + 1;
1903
+ const localSessionId = createLocalSessionId();
1904
+ apply({ type: "start", generation, localSessionId });
1905
+ const request = createTranscriptionSessionRequest({
1906
+ policy,
1907
+ adapter,
1908
+ localSessionId,
1909
+ selection,
1910
+ sequenceFloor: 0
1911
+ });
1912
+ if (!request) {
1913
+ apply({
1914
+ type: "start.failed",
1915
+ generation,
1916
+ code: "policy_blocked"
1917
+ });
1918
+ return false;
1919
+ }
1920
+ activePolicyRef.current = { generation, revision: policyRevision };
1921
+ const controller = new AbortController();
1922
+ pendingStartRef.current = { generation, controller };
1923
+ let boundarySettled = false;
1924
+ let accepted = false;
1925
+ const observedStart = Promise.resolve().then(
1926
+ () => adapter.start(
1927
+ request,
1928
+ (event) => {
1929
+ apply({ type: "event", generation, event });
1930
+ if (event.type === "session.error" && !event.recoverable) {
1931
+ releaseTerminalSession(generation);
1932
+ } else if (event.type === "session.closed") {
1933
+ releaseTerminalSession(generation);
1934
+ }
1935
+ },
1936
+ {
1937
+ signal: controller.signal,
1938
+ reportDiagnostic
1939
+ }
1940
+ )
1941
+ ).then(
1942
+ (session) => {
1943
+ if (boundarySettled && !accepted) {
1944
+ detachSessionCleanup(session, {
1945
+ cancelReason: "stale-generation",
1946
+ cleanupTimeoutMs,
1947
+ reportDiagnostic
1948
+ });
1949
+ }
1950
+ return { kind: "session", session };
1951
+ },
1952
+ (error) => {
1953
+ if (boundarySettled) {
1954
+ reportDiagnostic({
1955
+ operation: "start",
1956
+ code: classifyStartError(error),
1957
+ detail: diagnosticDetail(error)
1958
+ });
1959
+ }
1960
+ return { kind: "error", error };
1961
+ }
1962
+ );
1963
+ let abortListener = null;
1964
+ const aborted = new Promise((resolve) => {
1965
+ if (controller.signal.aborted) {
1966
+ resolve({ kind: "aborted" });
1967
+ return;
1968
+ }
1969
+ abortListener = () => resolve({ kind: "aborted" });
1970
+ controller.signal.addEventListener("abort", abortListener, { once: true });
1971
+ });
1972
+ let timeoutId = null;
1973
+ const timedOut = new Promise((resolve) => {
1974
+ timeoutId = setTimeout(() => resolve({ kind: "timeout" }), startTimeoutMs);
1975
+ });
1976
+ const outcome = await Promise.race([observedStart, aborted, timedOut]);
1977
+ boundarySettled = true;
1978
+ if (timeoutId !== null) clearTimeout(timeoutId);
1979
+ if (abortListener) controller.signal.removeEventListener("abort", abortListener);
1980
+ if (pendingStartRef.current?.generation === generation) pendingStartRef.current = null;
1981
+ if (outcome.kind === "aborted") return false;
1982
+ if (outcome.kind === "timeout") {
1983
+ controller.abort("transcription-start-timeout");
1984
+ if (activePolicyRef.current?.generation === generation) activePolicyRef.current = null;
1985
+ reportDiagnostic({
1986
+ operation: "start",
1987
+ code: "timeout",
1988
+ detail: `Transcription adapter start exceeded ${startTimeoutMs}ms.`
1989
+ });
1990
+ apply({ type: "start.failed", generation, code: "timeout" });
1991
+ return false;
1992
+ }
1993
+ if (outcome.kind === "error") {
1994
+ if (activePolicyRef.current?.generation === generation) activePolicyRef.current = null;
1995
+ const code = classifyStartError(outcome.error);
1996
+ reportDiagnostic({
1997
+ operation: "start",
1998
+ code,
1999
+ detail: diagnosticDetail(outcome.error)
2000
+ });
2001
+ if (!controller.signal.aborted) {
2002
+ apply({ type: "start.failed", generation, code });
2003
+ }
2004
+ return false;
2005
+ }
2006
+ const current = stateRef.current;
2007
+ if (current.generation !== generation || current.localSessionId !== localSessionId || current.status === "cancelling" || !isActiveTranscriptionStatus(current.status)) {
2008
+ if (activePolicyRef.current?.generation === generation) activePolicyRef.current = null;
2009
+ detachSessionCleanup(outcome.session, {
2010
+ ...current.status === "error" ? {} : { cancelReason: "stale-generation" },
2011
+ cleanupTimeoutMs,
2012
+ reportDiagnostic
2013
+ });
2014
+ return false;
2015
+ }
2016
+ accepted = true;
2017
+ sessionRef.current = { generation, session: outcome.session };
2018
+ return true;
2019
+ }, [
2020
+ adapter,
2021
+ apply,
2022
+ createLocalSessionId,
2023
+ cleanupTimeoutMs,
2024
+ policy,
2025
+ policyRevision,
2026
+ releaseTerminalSession,
2027
+ reportDiagnostic,
2028
+ selection,
2029
+ startTimeoutMs,
2030
+ unavailableReason
2031
+ ]);
2032
+ const cancel = useCallback3(async () => {
2033
+ const current = stateRef.current;
2034
+ if (!isActiveTranscriptionStatus(current.status)) return false;
2035
+ const generation = current.generation;
2036
+ apply({ type: "cancel", generation });
2037
+ const pending = pendingStartRef.current;
2038
+ if (pending?.generation === generation) {
2039
+ pendingStartRef.current = null;
2040
+ pending.controller.abort("transcription-locally-cancelled");
2041
+ }
2042
+ const active = sessionRef.current;
2043
+ sessionRef.current = null;
2044
+ if (activePolicyRef.current?.generation === generation) activePolicyRef.current = null;
2045
+ apply({ type: "cancel.settled", generation });
2046
+ focusInput();
2047
+ if (active?.generation === generation) {
2048
+ detachSessionCleanup(active.session, {
2049
+ cancelReason: "user-cancelled",
2050
+ cleanupTimeoutMs,
2051
+ reportDiagnostic
2052
+ });
2053
+ }
2054
+ return true;
2055
+ }, [apply, cleanupTimeoutMs, focusInput, reportDiagnostic]);
2056
+ const reset = useCallback3(() => apply({ type: "reset" }), [apply]);
2057
+ useEffect2(() => {
2058
+ if (!isActiveTranscriptionStatus(state.status)) return;
2059
+ const onKeyDown = (event) => {
2060
+ if (event.key !== "Escape") return;
2061
+ event.preventDefault();
2062
+ void cancel();
2063
+ };
2064
+ document.addEventListener("keydown", onKeyDown);
2065
+ return () => document.removeEventListener("keydown", onKeyDown);
2066
+ }, [cancel, state.status]);
2067
+ useEffect2(() => {
2068
+ if (!isActiveTranscriptionStatus(state.status)) return;
2069
+ const activePolicy = activePolicyRef.current;
2070
+ if (unavailableReason === null && activePolicy?.generation === state.generation && activePolicy.revision === policyRevision) {
2071
+ return;
2072
+ }
2073
+ void cancel();
2074
+ }, [cancel, policyRevision, state.generation, state.status, unavailableReason]);
2075
+ useEffect2(
2076
+ () => () => {
2077
+ const pending = pendingStartRef.current;
2078
+ pendingStartRef.current = null;
2079
+ pending?.controller.abort("transcription-component-unmounted");
2080
+ const active = sessionRef.current;
2081
+ sessionRef.current = null;
2082
+ activePolicyRef.current = null;
2083
+ stateRef.current = {
2084
+ ...stateRef.current,
2085
+ generation: stateRef.current.generation + 1,
2086
+ status: "closed",
2087
+ partial: ""
2088
+ };
2089
+ if (active) {
2090
+ const runtime = cleanupRuntimeRef.current;
2091
+ detachSessionCleanup(active.session, {
2092
+ cancelReason: "component-unmounted",
2093
+ cleanupTimeoutMs: runtime.cleanupTimeoutMs,
2094
+ reportDiagnostic: runtime.reportDiagnostic
2095
+ });
2096
+ }
2097
+ },
2098
+ []
2099
+ );
2100
+ return {
2101
+ state,
2102
+ available: unavailableReason === null,
2103
+ unavailableReason,
2104
+ start,
2105
+ cancel,
2106
+ reset
2107
+ };
2108
+ }
2109
+ function isActiveTranscriptionStatus(status) {
2110
+ return status === "requesting-permission" || status === "listening" || status === "reconnecting" || status === "cancelling";
2111
+ }
2112
+ function defaultLocalSessionId() {
2113
+ return globalThis.crypto.randomUUID();
2114
+ }
2115
+ function classifyStartError(error) {
2116
+ if (isRecord2(error)) {
2117
+ const normalized = normalizeTranscriptionErrorCode(error.code);
2118
+ if (normalized !== "unknown") return normalized;
2119
+ }
2120
+ if (typeof DOMException !== "undefined" && error instanceof DOMException) {
2121
+ if (error.name === "NotAllowedError") return "permission_denied";
2122
+ if (error.name === "NotSupportedError") return "not_supported";
2123
+ if (error.name === "TimeoutError") return "timeout";
2124
+ if (error.name === "AbortError") return "cancelled";
2125
+ }
2126
+ return "unknown";
2127
+ }
2128
+ function normalizeTranscriptionErrorCode(code) {
2129
+ switch (code) {
2130
+ case "permission_denied":
2131
+ case "not_supported":
2132
+ case "network":
2133
+ case "provider":
2134
+ case "policy_blocked":
2135
+ case "timeout":
2136
+ case "cancelled":
2137
+ case "unknown":
2138
+ return code;
2139
+ default:
2140
+ return "unknown";
2141
+ }
2142
+ }
2143
+ function sanitizeTranscriptionDiagnostic(diagnostic) {
2144
+ const source = isRecord2(diagnostic) ? diagnostic : {};
2145
+ const operation = source.operation === "start" || source.operation === "session" || source.operation === "cancel" || source.operation === "close" ? source.operation : "session";
2146
+ const rawDetail = typeof source.detail === "string" ? source.detail : "No diagnostic detail.";
2147
+ const detail = rawDetail.replace(/[\u0000-\u001f\u007f]+/gu, " ").replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/giu, "Bearer [REDACTED]").replace(/\bsk-[A-Za-z0-9_-]{4,}\b/gu, "[REDACTED]").replace(
2148
+ /\b(api[-_ ]?key|authorization|access[-_ ]?token|refresh[-_ ]?token|token|secret)\b\s*[:=]\s*[^\s,;]+/giu,
2149
+ "$1=[REDACTED]"
2150
+ ).trim().slice(0, 512);
2151
+ return {
2152
+ operation,
2153
+ code: normalizeTranscriptionErrorCode(source.code),
2154
+ detail: detail || "No diagnostic detail."
2155
+ };
2156
+ }
2157
+ function detachSessionCleanup(session, options) {
2158
+ if (options.cancelReason) {
2159
+ void runBoundedCleanup(
2160
+ "cancel",
2161
+ () => session.cancel(options.cancelReason),
2162
+ options.cleanupTimeoutMs,
2163
+ options.reportDiagnostic
2164
+ );
2165
+ }
2166
+ void runBoundedCleanup(
2167
+ "close",
2168
+ () => session.close(),
2169
+ options.cleanupTimeoutMs,
2170
+ options.reportDiagnostic
2171
+ );
2172
+ }
2173
+ async function runBoundedCleanup(operation, invoke, timeoutMs, reportDiagnostic) {
2174
+ let timeoutId = null;
2175
+ const result = Promise.resolve().then(invoke).then(
2176
+ () => ({ kind: "settled" }),
2177
+ (error) => ({ kind: "rejected", error })
2178
+ );
2179
+ const timeout = new Promise((resolve) => {
2180
+ timeoutId = setTimeout(() => resolve({ kind: "timeout" }), timeoutMs);
2181
+ });
2182
+ const outcome = await Promise.race([result, timeout]);
2183
+ if (timeoutId !== null) clearTimeout(timeoutId);
2184
+ if (outcome.kind === "settled") return;
2185
+ reportDiagnostic({
2186
+ operation,
2187
+ code: outcome.kind === "timeout" ? "timeout" : "provider",
2188
+ detail: outcome.kind === "timeout" ? `Transcription ${operation} exceeded ${timeoutMs}ms.` : diagnosticDetail(outcome.error)
2189
+ });
2190
+ }
2191
+ function diagnosticDetail(error) {
2192
+ if (error instanceof Error) return error.message;
2193
+ return typeof error === "string" ? error : "Unknown transcription adapter failure.";
2194
+ }
2195
+ function normalizeTimeout(value, fallback) {
2196
+ return typeof value === "number" && Number.isFinite(value) && value >= 1 ? Math.min(Math.floor(value), 6e4) : fallback;
2197
+ }
2198
+ function isRecord2(value) {
2199
+ return typeof value === "object" && value !== null;
2200
+ }
2201
+ function transcriptionPolicyRevision(policy, selection) {
2202
+ const targetKey = (target) => target ? [
2203
+ target.provider,
2204
+ target.model ?? "",
2205
+ target.credentialMode,
2206
+ target.credentialConnectionId ?? "",
2207
+ target.region ?? ""
2208
+ ].join("\0") : "";
2209
+ return [
2210
+ policy.enabled ? "1" : "0",
2211
+ policy.acceptanceId ?? "",
2212
+ targetKey(policy.primary),
2213
+ policy.language ?? "",
2214
+ policy.autoDetectLanguage ? "1" : "0",
2215
+ policy.diarization.enabled ? "1" : "0",
2216
+ policy.diarization.maxSpeakers?.toString() ?? "",
2217
+ policy.retention.mode,
2218
+ policy.retention.maxDays?.toString() ?? "",
2219
+ policy.privacy.allowProviderLogging ? "1" : "0",
2220
+ policy.privacy.allowProviderTraining ? "1" : "0",
2221
+ policy.fallback.mode,
2222
+ policy.fallback.targets.map(targetKey).join(""),
2223
+ policy.cost.currency,
2224
+ policy.cost.maxPerHour?.toString() ?? "",
2225
+ policy.cost.maxPerMonth?.toString() ?? "",
2226
+ selection.kind,
2227
+ selection.kind === "fallback" ? selection.index.toString() : ""
2228
+ ].join("");
2229
+ }
2230
+
2231
+ // src/components/composer-transcription-control.tsx
2232
+ import { LoaderCircleIcon as LoaderCircleIcon2, MicIcon, RotateCwIcon as RotateCwIcon2, SquareIcon } from "lucide-react";
2233
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
2234
+ var defaultMessages = {
2235
+ start: "Start voice input",
2236
+ stop: "Stop voice input",
2237
+ retry: "Retry voice input",
2238
+ requestingPermission: "Requesting microphone permission\u2026",
2239
+ listening: "Listening\u2026 Press Escape to cancel.",
2240
+ reconnecting: "Reconnecting voice input\u2026",
2241
+ cancelling: "Cancelling voice input\u2026",
2242
+ unavailableDisabled: "Voice input is unavailable while the composer is disabled.",
2243
+ unavailableAdapter: "Voice input needs an approved transcription adapter.",
2244
+ unavailablePolicy: "Enable and accept transcription in Workspace settings.",
2245
+ errorPermissionDenied: "Microphone permission was denied. Your draft was not changed.",
2246
+ errorNotSupported: "Voice input is not supported on this device.",
2247
+ errorNetwork: "Voice input lost its network connection.",
2248
+ errorProvider: "The transcription service could not continue.",
2249
+ errorPolicyBlocked: "Workspace policy does not authorize voice input.",
2250
+ errorTimeout: "Voice input took too long to start. Try again.",
2251
+ errorCancelled: "Voice input was cancelled.",
2252
+ errorUnknown: "Voice input could not start. Try again."
2253
+ };
2254
+ function ComposerTranscriptionControl({
2255
+ adapter,
2256
+ policy,
2257
+ selection,
2258
+ messages: overrides,
2259
+ className,
2260
+ lifecycleTimeouts,
2261
+ onDiagnostic
2262
+ }) {
2263
+ const composer = useChatComposer();
2264
+ const messages = { ...defaultMessages, ...overrides };
2265
+ const transcription = useTranscription({
2266
+ adapter,
2267
+ policy,
2268
+ ...selection ? { selection } : {},
2269
+ value: composer.value,
2270
+ setValue: composer.setValue,
2271
+ focusInput: composer.focusInput,
2272
+ disabled: composer.disabled,
2273
+ lifecycleTimeouts,
2274
+ onDiagnostic
2275
+ });
2276
+ const status = transcription.state.status;
2277
+ const active = status === "requesting-permission" || status === "listening" || status === "reconnecting" || status === "cancelling";
2278
+ const unavailableMessage = transcription.unavailableReason === "composer_disabled" ? messages.unavailableDisabled : transcription.unavailableReason === "adapter_missing" ? messages.unavailableAdapter : transcription.unavailableReason ? messages.unavailablePolicy : null;
2279
+ const label = unavailableMessage ? unavailableMessage : status === "error" ? messages.retry : active ? messages.stop : messages.start;
2280
+ const errorMessage3 = transcription.state.error ? transcriptionErrorMessage(transcription.state.error.code, messages) : null;
2281
+ const announcement = transcription.state.partial || (status === "requesting-permission" ? messages.requestingPermission : status === "listening" ? messages.listening : status === "reconnecting" ? errorMessage3 ?? messages.reconnecting : status === "cancelling" ? messages.cancelling : status === "error" ? errorMessage3 ?? messages.errorUnknown : unavailableMessage);
2282
+ function activate(event) {
2283
+ if (unavailableMessage || status === "cancelling") {
2284
+ event.preventDefault();
2285
+ return;
2286
+ }
2287
+ if (active) void transcription.cancel();
2288
+ else void transcription.start();
2289
+ }
2290
+ return /* @__PURE__ */ jsxs4(
2291
+ "span",
2292
+ {
2293
+ className: cn("inline-flex min-w-0 items-center gap-1.5", className),
2294
+ "data-transcription-status": status,
2295
+ children: [
2296
+ /* @__PURE__ */ jsx4(
2297
+ "button",
2298
+ {
2299
+ type: "button",
2300
+ onClick: activate,
2301
+ "aria-label": label,
2302
+ "aria-pressed": active,
2303
+ "aria-disabled": unavailableMessage !== null || status === "cancelling",
2304
+ "aria-keyshortcuts": active ? "Escape" : void 0,
2305
+ title: label,
2306
+ className: cn(
2307
+ "inline-flex size-8 shrink-0 items-center justify-center rounded-og-md pointer-coarse:size-11",
2308
+ "text-og-fg-muted transition-colors duration-150 motion-reduce:transition-none",
2309
+ unavailableMessage ? "cursor-not-allowed opacity-45" : status === "cancelling" ? "cursor-not-allowed" : "hover:bg-og-surface-2 hover:text-og-fg",
2310
+ status === "listening" && "bg-og-status-failed/10 text-og-status-failed"
2311
+ ),
2312
+ children: status === "requesting-permission" || status === "cancelling" ? /* @__PURE__ */ jsx4(LoaderCircleIcon2, { className: "size-4 animate-og-spin motion-reduce:animate-none" }) : status === "reconnecting" ? /* @__PURE__ */ jsx4(RotateCwIcon2, { className: "size-4 animate-og-spin motion-reduce:animate-none" }) : active ? /* @__PURE__ */ jsx4(SquareIcon, { className: "size-3.5 fill-current" }) : /* @__PURE__ */ jsx4(MicIcon, { className: "size-4" })
2313
+ }
2314
+ ),
2315
+ announcement && (active || status === "error") ? /* @__PURE__ */ jsx4(
2316
+ "span",
2317
+ {
2318
+ "aria-hidden": "true",
2319
+ title: announcement,
2320
+ className: cn(
2321
+ "max-w-48 truncate text-og-xs text-og-fg-muted max-sm:max-w-28",
2322
+ status === "error" && "text-og-status-failed"
2323
+ ),
2324
+ children: announcement
2325
+ }
2326
+ ) : null,
2327
+ /* @__PURE__ */ jsx4("span", { className: "sr-only", role: status === "error" ? "alert" : "status", "aria-live": "polite", children: announcement })
2328
+ ]
2329
+ }
2330
+ );
2331
+ }
2332
+ function transcriptionErrorMessage(code, messages) {
2333
+ switch (code) {
2334
+ case "permission_denied":
2335
+ return messages.errorPermissionDenied;
2336
+ case "not_supported":
2337
+ return messages.errorNotSupported;
2338
+ case "network":
2339
+ return messages.errorNetwork;
2340
+ case "provider":
2341
+ return messages.errorProvider;
2342
+ case "policy_blocked":
2343
+ return messages.errorPolicyBlocked;
2344
+ case "timeout":
2345
+ return messages.errorTimeout;
2346
+ case "cancelled":
2347
+ return messages.errorCancelled;
2348
+ case "unknown":
2349
+ return messages.errorUnknown;
2350
+ }
2351
+ }
2352
+
2353
+ export {
2354
+ parseCommandLine,
2355
+ hasPermission,
2356
+ matchCommand,
2357
+ filterCommands,
2358
+ argHint,
2359
+ firstMissingRequiredArg,
2360
+ defaultCommands,
2361
+ useSlashCommands,
2362
+ CommandPalette,
2363
+ ModelPicker,
2364
+ OPEN_WORKSTREAM_CONTROL_EVENT,
2365
+ defaultChatComposerMessages,
2366
+ useChatComposerController,
2367
+ useChatComposer,
2368
+ Root,
2369
+ Frame,
2370
+ CommandPalette2,
2371
+ Surface,
2372
+ PausedState,
2373
+ RestoredResources,
2374
+ Attachments,
2375
+ Input,
2376
+ Confirmation,
2377
+ Footer,
2378
+ Controls,
2379
+ Hint,
2380
+ Actions,
2381
+ AttachButton,
2382
+ ModelPicker2,
2383
+ PauseButton,
2384
+ SendButton,
2385
+ Help,
2386
+ Status,
2387
+ INITIAL_TRANSCRIPTION_CONTROL_STATE,
2388
+ transitionTranscriptionControl,
2389
+ appendFinalTranscript,
2390
+ useTranscription,
2391
+ ComposerTranscriptionControl
2392
+ };
2393
+ //# sourceMappingURL=chunk-LAZ2A743.js.map