@opengeni/react 0.29.0 → 0.33.1

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