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