@hachej/boring-agent 0.1.5 → 0.1.7
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/dist/front/index.d.ts +26 -3
- package/dist/front/index.js +290 -51
- package/dist/front/styles.css +24 -0
- package/dist/{harness-CMiJ4kok.d.ts → harness-DpMCKbzh.d.ts} +20 -0
- package/dist/{sandbox-handle-store-OPdvRwie.d.ts → sandbox-handle-store-DCNEJJ6b.d.ts} +9 -0
- package/dist/server/index.d.ts +6 -1
- package/dist/server/index.js +711 -149
- package/dist/shared/index.d.ts +3 -3
- package/package.json +2 -2
package/dist/front/index.d.ts
CHANGED
|
@@ -4,12 +4,24 @@ import { ReactNode, ComponentType, HTMLAttributes, ComponentProps, FormEvent, El
|
|
|
4
4
|
import * as ai from 'ai';
|
|
5
5
|
import { UIMessage, FileUIPart, ChatStatus } from 'ai';
|
|
6
6
|
import * as _ai_sdk_react from '@ai-sdk/react';
|
|
7
|
-
import { S as SendMessageInput, d as SessionSummary } from '../harness-
|
|
7
|
+
import { S as SendMessageInput, d as SessionSummary } from '../harness-DpMCKbzh.js';
|
|
8
8
|
import { Button, Collapsible, CollapsibleContent, CollapsibleTrigger, InputGroupButton, TooltipContent, InputGroupAddon, InputGroupTextarea } from '@hachej/boring-ui-kit';
|
|
9
9
|
import { Streamdown } from 'streamdown';
|
|
10
10
|
import { StickToBottom } from 'use-stick-to-bottom';
|
|
11
11
|
import { ClassValue } from 'clsx';
|
|
12
12
|
|
|
13
|
+
interface UploadFileOptions {
|
|
14
|
+
apiBaseUrl?: string;
|
|
15
|
+
workspaceRequestId?: string | null;
|
|
16
|
+
directory?: string;
|
|
17
|
+
sourcePath?: string;
|
|
18
|
+
}
|
|
19
|
+
interface UploadFileResult {
|
|
20
|
+
url: string;
|
|
21
|
+
path: string;
|
|
22
|
+
}
|
|
23
|
+
declare function uploadFile(file: File, opts?: UploadFileOptions): Promise<UploadFileResult>;
|
|
24
|
+
|
|
13
25
|
type SlashCommandHandler = (args: string, ctx: SlashCommandContext) => string | void;
|
|
14
26
|
interface SlashCommand {
|
|
15
27
|
name: string;
|
|
@@ -200,6 +212,12 @@ interface ChatPanelProps {
|
|
|
200
212
|
*/
|
|
201
213
|
debug?: boolean;
|
|
202
214
|
className?: string;
|
|
215
|
+
/** When provided, files are uploaded immediately on attach and sent as stable
|
|
216
|
+
* server URLs rather than base64 data URLs. Supply via useFileUpload() from
|
|
217
|
+
* @hachej/boring-workspace's DataProvider context. */
|
|
218
|
+
onUploadFile?: (file: File) => Promise<{
|
|
219
|
+
url: string;
|
|
220
|
+
}>;
|
|
203
221
|
}
|
|
204
222
|
declare function ChatPanel(props: ChatPanelProps): react_jsx_runtime.JSX.Element;
|
|
205
223
|
|
|
@@ -359,8 +377,13 @@ type PromptInputProps = Omit<HTMLAttributes<HTMLFormElement>, "onSubmit" | "onEr
|
|
|
359
377
|
message: string;
|
|
360
378
|
}) => void;
|
|
361
379
|
onSubmit: (message: PromptInputMessage, event: FormEvent<HTMLFormElement>) => void | Promise<void>;
|
|
380
|
+
/** When provided, files are uploaded to the server immediately on add and the
|
|
381
|
+
* attachment URL is replaced with the stable server path before submit. */
|
|
382
|
+
onUploadFile?: (file: File) => Promise<{
|
|
383
|
+
url: string;
|
|
384
|
+
}>;
|
|
362
385
|
};
|
|
363
|
-
declare const PromptInput: ({ className, accept, multiple, globalDrop, syncHiddenInput, maxFiles, maxFileSize, onError, onSubmit, children, ...props }: PromptInputProps) => react_jsx_runtime.JSX.Element;
|
|
386
|
+
declare const PromptInput: ({ className, accept, multiple, globalDrop, syncHiddenInput, maxFiles, maxFileSize, onError, onSubmit, onUploadFile, children, ...props }: PromptInputProps) => react_jsx_runtime.JSX.Element;
|
|
364
387
|
type PromptInputTextareaProps = ComponentProps<typeof InputGroupTextarea>;
|
|
365
388
|
declare const PromptInputTextarea: ({ onChange, onKeyDown, className, placeholder, ...props }: PromptInputTextareaProps) => react_jsx_runtime.JSX.Element;
|
|
366
389
|
type PromptInputFooterProps = Omit<ComponentProps<typeof InputGroupAddon>, "align">;
|
|
@@ -391,4 +414,4 @@ declare const Shimmer: react.MemoExoticComponent<({ children, as: Component, cla
|
|
|
391
414
|
|
|
392
415
|
declare function cn(...inputs: ClassValue[]): string;
|
|
393
416
|
|
|
394
|
-
export { type AgentCommandContribution, type AgentCommandOptions, ArtifactOpenProvider, ChatEmptyState, type ChatEmptyStateProps, ChatPanel, type ChatPanelProps, type ChatSuggestion, CodeBlock, CodeBlockContainer, CodeBlockContent, CodeBlockCopyButton, CodeBlockHeader, type CommandRegistry, Conversation, ConversationContent, ConversationEmptyState, ConversationScrollButton, DebugDrawer, DiffView, type GroupedToolEntry, Message, MessageAction, MessageActions, MessageContent, MessageResponse, MessageToolbar, type OpenArtifactHandler, type ParsedCommand, PromptInput, PromptInputButton, PromptInputFooter, PromptInputSubmit, PromptInputTextarea, Reasoning, ReasoningContent, ReasoningTrigger, Shimmer, type SlashCommand, type SlashCommandContext, ToolCallGroup, type ToolPart, type ToolRenderer, type ToolRendererOverrides, type UseAgentChatOptions, type UseSessionsOptions, type UseSessionsResult, builtinCommands, cn, createCommandRegistry, defaultChatSuggestions, defaultToolRenderers, getAgentCommands, mergeShadcnToolRenderers, mergeToolRenderers, parseSlashCommand, resolveToolRenderer, useAgentChat, useOpenArtifact, useSessions };
|
|
417
|
+
export { type AgentCommandContribution, type AgentCommandOptions, ArtifactOpenProvider, ChatEmptyState, type ChatEmptyStateProps, ChatPanel, type ChatPanelProps, type ChatSuggestion, CodeBlock, CodeBlockContainer, CodeBlockContent, CodeBlockCopyButton, CodeBlockHeader, type CommandRegistry, Conversation, ConversationContent, ConversationEmptyState, ConversationScrollButton, DebugDrawer, DiffView, type GroupedToolEntry, Message, MessageAction, MessageActions, MessageContent, MessageResponse, MessageToolbar, type OpenArtifactHandler, type ParsedCommand, PromptInput, PromptInputButton, PromptInputFooter, PromptInputSubmit, PromptInputTextarea, Reasoning, ReasoningContent, ReasoningTrigger, Shimmer, type SlashCommand, type SlashCommandContext, ToolCallGroup, type ToolPart, type ToolRenderer, type ToolRendererOverrides, type UploadFileOptions, type UploadFileResult, type UseAgentChatOptions, type UseSessionsOptions, type UseSessionsResult, builtinCommands, cn, createCommandRegistry, defaultChatSuggestions, defaultToolRenderers, getAgentCommands, mergeShadcnToolRenderers, mergeToolRenderers, parseSlashCommand, resolveToolRenderer, uploadFile, useAgentChat, useOpenArtifact, useSessions };
|
package/dist/front/index.js
CHANGED
|
@@ -1,3 +1,39 @@
|
|
|
1
|
+
// src/front/upload/uploadFile.ts
|
|
2
|
+
function readAsDataUrl(file) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
const reader = new FileReader();
|
|
5
|
+
reader.onload = () => resolve(reader.result);
|
|
6
|
+
reader.onerror = () => reject(reader.error ?? new Error("Read failed"));
|
|
7
|
+
reader.readAsDataURL(file);
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
async function uploadFile(file, opts = {}) {
|
|
11
|
+
const { apiBaseUrl = "", workspaceRequestId, directory, sourcePath } = opts;
|
|
12
|
+
const dataUrl = await readAsDataUrl(file);
|
|
13
|
+
const comma = dataUrl.indexOf(",");
|
|
14
|
+
const contentBase64 = comma >= 0 ? dataUrl.slice(comma + 1) : "";
|
|
15
|
+
const headers = { "Content-Type": "application/json" };
|
|
16
|
+
if (workspaceRequestId) headers["x-boring-workspace-id"] = workspaceRequestId;
|
|
17
|
+
const base = apiBaseUrl.replace(/\/$/, "");
|
|
18
|
+
const res = await fetch(`${base}/api/v1/files/upload`, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers,
|
|
21
|
+
credentials: "include",
|
|
22
|
+
body: JSON.stringify({
|
|
23
|
+
filename: file.name,
|
|
24
|
+
contentType: file.type,
|
|
25
|
+
contentBase64,
|
|
26
|
+
...directory ? { directory } : {},
|
|
27
|
+
...sourcePath ? { sourcePath } : {}
|
|
28
|
+
})
|
|
29
|
+
});
|
|
30
|
+
if (!res.ok) throw new Error(`Upload failed: ${res.status}`);
|
|
31
|
+
const body = await res.json();
|
|
32
|
+
const url = body.markdownUrl ?? body.path;
|
|
33
|
+
if (!url) throw new Error("Upload response missing url");
|
|
34
|
+
return { url, path: body.path ?? url };
|
|
35
|
+
}
|
|
36
|
+
|
|
1
37
|
// src/front/ChatPanel.tsx
|
|
2
38
|
import { isToolUIPart as isToolUIPart2 } from "ai";
|
|
3
39
|
import { motion as motion2 } from "motion/react";
|
|
@@ -235,6 +271,7 @@ function useAgentChat(opts) {
|
|
|
235
271
|
};
|
|
236
272
|
}, [sessionId, cacheKey, setMessages]);
|
|
237
273
|
const messages = chat.messages;
|
|
274
|
+
const status = chat.status;
|
|
238
275
|
useEffect4(() => {
|
|
239
276
|
if (!hydrated || !cacheKey || messages.length === 0) return;
|
|
240
277
|
try {
|
|
@@ -242,7 +279,38 @@ function useAgentChat(opts) {
|
|
|
242
279
|
} catch {
|
|
243
280
|
}
|
|
244
281
|
}, [hydrated, cacheKey, messages]);
|
|
245
|
-
const
|
|
282
|
+
const SETTLED_STATES = /* @__PURE__ */ new Set([
|
|
283
|
+
"output-available",
|
|
284
|
+
"output-error",
|
|
285
|
+
"output-denied",
|
|
286
|
+
"approval-responded"
|
|
287
|
+
]);
|
|
288
|
+
const prevSettleRef = useRef4(status);
|
|
289
|
+
useEffect4(() => {
|
|
290
|
+
const prev = prevSettleRef.current;
|
|
291
|
+
prevSettleRef.current = status;
|
|
292
|
+
if (status !== "ready") return;
|
|
293
|
+
if (prev !== "streaming" && prev !== "submitted") return;
|
|
294
|
+
const hasUnsettled = messages.some(
|
|
295
|
+
(msg) => msg.parts?.some((p) => {
|
|
296
|
+
const part = p;
|
|
297
|
+
return typeof part.type === "string" && part.type.startsWith("tool-") && !SETTLED_STATES.has(part.state);
|
|
298
|
+
})
|
|
299
|
+
);
|
|
300
|
+
if (!hasUnsettled) return;
|
|
301
|
+
setMessages(
|
|
302
|
+
(prev2) => prev2.map((msg) => ({
|
|
303
|
+
...msg,
|
|
304
|
+
parts: msg.parts?.map((p) => {
|
|
305
|
+
const part = p;
|
|
306
|
+
if (typeof part.type === "string" && part.type.startsWith("tool-") && !SETTLED_STATES.has(part.state)) {
|
|
307
|
+
return { ...part, state: "output-error" };
|
|
308
|
+
}
|
|
309
|
+
return p;
|
|
310
|
+
})
|
|
311
|
+
}))
|
|
312
|
+
);
|
|
313
|
+
}, [status, setMessages]);
|
|
246
314
|
const prevStatusRef = useRef4(status);
|
|
247
315
|
useEffect4(() => {
|
|
248
316
|
const prev = prevStatusRef.current;
|
|
@@ -251,19 +319,58 @@ function useAgentChat(opts) {
|
|
|
251
319
|
if (prev !== "streaming" && prev !== "submitted") return;
|
|
252
320
|
if (!sessionId || messages.length === 0) return;
|
|
253
321
|
const url = `/api/v1/agent/chat/${encodeURIComponent(sessionId)}/messages`;
|
|
322
|
+
const stripped = messages.map((msg) => ({
|
|
323
|
+
...msg,
|
|
324
|
+
parts: msg.parts?.map((part) => {
|
|
325
|
+
const p = part;
|
|
326
|
+
if (p.type === "file" && typeof p.url === "string" && p.url.startsWith("data:")) {
|
|
327
|
+
return { ...p, url: "" };
|
|
328
|
+
}
|
|
329
|
+
return part;
|
|
330
|
+
})
|
|
331
|
+
}));
|
|
254
332
|
fetch(url, {
|
|
255
333
|
method: "PUT",
|
|
256
334
|
headers: {
|
|
257
335
|
"Content-Type": "application/json",
|
|
258
336
|
...optsRef.current.requestHeaders
|
|
259
337
|
},
|
|
260
|
-
body: JSON.stringify({ messages })
|
|
338
|
+
body: JSON.stringify({ messages: stripped })
|
|
261
339
|
}).catch(() => {
|
|
262
340
|
});
|
|
263
341
|
}, [sessionId, status, messages]);
|
|
264
342
|
return chat;
|
|
265
343
|
}
|
|
266
344
|
|
|
345
|
+
// src/front/splitFollowUp.ts
|
|
346
|
+
function splitFollowUp(msgs, consumed, genId) {
|
|
347
|
+
const targetIdx = msgs.findIndex(
|
|
348
|
+
(m) => m.role === "assistant" && m.parts?.some((p) => {
|
|
349
|
+
const part = p;
|
|
350
|
+
return part.type === "data-followup-consumed" || part.id?.startsWith("turn-");
|
|
351
|
+
})
|
|
352
|
+
);
|
|
353
|
+
const userMsg = {
|
|
354
|
+
id: genId(),
|
|
355
|
+
role: "user",
|
|
356
|
+
parts: [...consumed.files, { type: "text", text: consumed.text }]
|
|
357
|
+
};
|
|
358
|
+
if (targetIdx < 0) {
|
|
359
|
+
return [...msgs, userMsg];
|
|
360
|
+
}
|
|
361
|
+
const target = msgs[targetIdx];
|
|
362
|
+
const markerIdx = target.parts?.findIndex((p) => {
|
|
363
|
+
const part = p;
|
|
364
|
+
return part.type === "data-followup-consumed" || part.id?.startsWith("turn-");
|
|
365
|
+
}) ?? -1;
|
|
366
|
+
const markerIsPart = markerIdx >= 0 && (target.parts?.[markerIdx]).type === "data-followup-consumed";
|
|
367
|
+
const turn1Parts = markerIdx >= 0 ? target.parts?.slice(0, markerIdx) ?? [] : target.parts ?? [];
|
|
368
|
+
const turn2Parts = markerIdx >= 0 ? target.parts?.slice(markerIsPart ? markerIdx + 1 : markerIdx) ?? [] : [];
|
|
369
|
+
const asst1 = { ...target, parts: turn1Parts };
|
|
370
|
+
const asst2 = { ...target, id: genId(), parts: turn2Parts };
|
|
371
|
+
return [...msgs.slice(0, targetIdx), asst1, userMsg, asst2, ...msgs.slice(targetIdx + 1)];
|
|
372
|
+
}
|
|
373
|
+
|
|
267
374
|
// src/front/DebugDrawer.tsx
|
|
268
375
|
import { useCallback as useCallback3, useEffect as useEffect5, useRef as useRef5, useState as useState4 } from "react";
|
|
269
376
|
import { Button, IconButton, Tabs, TabsContent, TabsList, TabsTrigger } from "@hachej/boring-ui-kit";
|
|
@@ -2731,6 +2838,7 @@ var PromptInput = ({
|
|
|
2731
2838
|
maxFileSize,
|
|
2732
2839
|
onError,
|
|
2733
2840
|
onSubmit,
|
|
2841
|
+
onUploadFile,
|
|
2734
2842
|
children,
|
|
2735
2843
|
...props
|
|
2736
2844
|
}) => {
|
|
@@ -2739,6 +2847,13 @@ var PromptInput = ({
|
|
|
2739
2847
|
const inputRef = useRef9(null);
|
|
2740
2848
|
const formRef = useRef9(null);
|
|
2741
2849
|
const [items, setItems] = useState12([]);
|
|
2850
|
+
const setFileUrlLocal = useCallback10((id, url, status) => {
|
|
2851
|
+
setItems((prev) => prev.map((f) => {
|
|
2852
|
+
if (f.id !== id) return f;
|
|
2853
|
+
if (f.url !== url && f.url.startsWith("blob:")) URL.revokeObjectURL(f.url);
|
|
2854
|
+
return { ...f, url, status };
|
|
2855
|
+
}));
|
|
2856
|
+
}, []);
|
|
2742
2857
|
const files = usingProvider ? controller.attachments.files : items;
|
|
2743
2858
|
const [referencedSources, setReferencedSources] = useState12([]);
|
|
2744
2859
|
const filesRef = useRef9(files);
|
|
@@ -2784,34 +2899,37 @@ var PromptInput = ({
|
|
|
2784
2899
|
});
|
|
2785
2900
|
return;
|
|
2786
2901
|
}
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2902
|
+
const capacity = typeof maxFiles === "number" ? Math.max(0, maxFiles - items.length) : void 0;
|
|
2903
|
+
const capped = typeof capacity === "number" ? sized.slice(0, capacity) : sized;
|
|
2904
|
+
if (typeof capacity === "number" && sized.length > capacity) {
|
|
2905
|
+
onError?.({
|
|
2906
|
+
code: "max_files",
|
|
2907
|
+
message: "Too many files. Some were not added."
|
|
2908
|
+
});
|
|
2909
|
+
}
|
|
2910
|
+
const entries = capped.map((file) => ({
|
|
2911
|
+
filename: file.name,
|
|
2912
|
+
id: nanoid(),
|
|
2913
|
+
mediaType: file.type,
|
|
2914
|
+
type: "file",
|
|
2915
|
+
url: URL.createObjectURL(file),
|
|
2916
|
+
status: onUploadFile ? "uploading" : "ready"
|
|
2917
|
+
}));
|
|
2918
|
+
setItems((prev) => [...prev, ...entries]);
|
|
2919
|
+
if (onUploadFile) {
|
|
2920
|
+
for (let i = 0; i < entries.length; i++) {
|
|
2921
|
+
const entry = entries[i];
|
|
2922
|
+
const file = capped[i];
|
|
2923
|
+
onUploadFile(file).then(({ url }) => setFileUrlLocal(entry.id, url, "ready")).catch(() => setFileUrlLocal(entry.id, entry.url, "error"));
|
|
2805
2924
|
}
|
|
2806
|
-
|
|
2807
|
-
});
|
|
2925
|
+
}
|
|
2808
2926
|
},
|
|
2809
|
-
[matchesAccept, maxFiles, maxFileSize, onError]
|
|
2927
|
+
[matchesAccept, maxFiles, maxFileSize, onError, items.length, onUploadFile, setFileUrlLocal]
|
|
2810
2928
|
);
|
|
2811
2929
|
const removeLocal = useCallback10(
|
|
2812
2930
|
(id) => setItems((prev) => {
|
|
2813
2931
|
const found = prev.find((file) => file.id === id);
|
|
2814
|
-
if (found?.url) {
|
|
2932
|
+
if (found?.url.startsWith("blob:")) {
|
|
2815
2933
|
URL.revokeObjectURL(found.url);
|
|
2816
2934
|
}
|
|
2817
2935
|
return prev.filter((file) => file.id !== id);
|
|
@@ -2856,7 +2974,7 @@ var PromptInput = ({
|
|
|
2856
2974
|
const clearAttachments = useCallback10(
|
|
2857
2975
|
() => usingProvider ? controller?.attachments.clear() : setItems((prev) => {
|
|
2858
2976
|
for (const file of prev) {
|
|
2859
|
-
if (file.url) {
|
|
2977
|
+
if (file.url.startsWith("blob:")) {
|
|
2860
2978
|
URL.revokeObjectURL(file.url);
|
|
2861
2979
|
}
|
|
2862
2980
|
}
|
|
@@ -2959,6 +3077,7 @@ var PromptInput = ({
|
|
|
2959
3077
|
},
|
|
2960
3078
|
[add]
|
|
2961
3079
|
);
|
|
3080
|
+
const setFileUrl = usingProvider ? controller.attachments.setFileUrl : setFileUrlLocal;
|
|
2962
3081
|
const attachmentsCtx = useMemo8(
|
|
2963
3082
|
() => ({
|
|
2964
3083
|
add,
|
|
@@ -2966,9 +3085,10 @@ var PromptInput = ({
|
|
|
2966
3085
|
fileInputRef: inputRef,
|
|
2967
3086
|
files: files.map((item) => ({ ...item, id: item.id })),
|
|
2968
3087
|
openFileDialog,
|
|
2969
|
-
remove
|
|
3088
|
+
remove,
|
|
3089
|
+
setFileUrl
|
|
2970
3090
|
}),
|
|
2971
|
-
[files, add, remove, clearAttachments, openFileDialog]
|
|
3091
|
+
[files, add, remove, clearAttachments, openFileDialog, setFileUrl]
|
|
2972
3092
|
);
|
|
2973
3093
|
const refsCtx = useMemo8(
|
|
2974
3094
|
() => ({
|
|
@@ -2995,18 +3115,16 @@ var PromptInput = ({
|
|
|
2995
3115
|
const formData = new FormData(form);
|
|
2996
3116
|
return formData.get("message") || "";
|
|
2997
3117
|
})();
|
|
3118
|
+
if (files.some((f) => f.status === "uploading")) return;
|
|
2998
3119
|
if (!usingProvider) {
|
|
2999
3120
|
form.reset();
|
|
3000
3121
|
}
|
|
3001
3122
|
try {
|
|
3002
3123
|
const convertedFiles = await Promise.all(
|
|
3003
|
-
files.map(async ({ id: _id, ...item }) => {
|
|
3124
|
+
files.map(async ({ id: _id, status: _status, ...item }) => {
|
|
3004
3125
|
if (item.url?.startsWith("blob:")) {
|
|
3005
3126
|
const dataUrl = await convertBlobUrlToDataUrl(item.url);
|
|
3006
|
-
return {
|
|
3007
|
-
...item,
|
|
3008
|
-
url: dataUrl ?? item.url
|
|
3009
|
-
};
|
|
3127
|
+
return { ...item, url: dataUrl ?? item.url };
|
|
3010
3128
|
}
|
|
3011
3129
|
return item;
|
|
3012
3130
|
})
|
|
@@ -3472,7 +3590,7 @@ var AttachmentRemove = ({
|
|
|
3472
3590
|
};
|
|
3473
3591
|
|
|
3474
3592
|
// src/front/ChatPanel.tsx
|
|
3475
|
-
import { PaperclipIcon as PaperclipIcon2, CopyIcon as CopyIcon4, CheckIcon as CheckIcon3, RefreshCwIcon as RefreshCwIcon2, BrainIcon as BrainIcon2, EyeIcon, EyeOffIcon, BotIcon } from "lucide-react";
|
|
3593
|
+
import { PaperclipIcon as PaperclipIcon2, CopyIcon as CopyIcon4, CheckIcon as CheckIcon3, RefreshCwIcon as RefreshCwIcon2, BrainIcon as BrainIcon2, EyeIcon, EyeOffIcon, BotIcon, Loader2, AlertCircleIcon } from "lucide-react";
|
|
3476
3594
|
import {
|
|
3477
3595
|
Button as Button11,
|
|
3478
3596
|
IconButton as IconButton2,
|
|
@@ -3492,6 +3610,14 @@ import {
|
|
|
3492
3610
|
} from "@hachej/boring-ui-kit";
|
|
3493
3611
|
import { Fragment as Fragment8, jsx as jsx23, jsxs as jsxs21 } from "react/jsx-runtime";
|
|
3494
3612
|
var INLINE_TEXT_MIME_PREFIXES = ["text/", "application/json", "application/xml", "application/yaml"];
|
|
3613
|
+
async function resolveAttachmentUrls(files) {
|
|
3614
|
+
if (!files) return [];
|
|
3615
|
+
return Promise.all(files.map(async (file) => ({
|
|
3616
|
+
filename: file.filename,
|
|
3617
|
+
mediaType: file.mediaType,
|
|
3618
|
+
url: file.url.startsWith("blob:") ? await convertBlobUrlToDataUrl(file.url) ?? file.url : file.url
|
|
3619
|
+
})));
|
|
3620
|
+
}
|
|
3495
3621
|
async function readFileAsText(file) {
|
|
3496
3622
|
const looksText = INLINE_TEXT_MIME_PREFIXES.some((p) => file.mediaType?.startsWith(p)) || /\.(md|txt|csv|json|yaml|yml|ts|tsx|js|jsx|py|rb|rs|go|sh|bash|css|html|sql|log)$/i.test(file.filename ?? "");
|
|
3497
3623
|
if (!looksText) return null;
|
|
@@ -3646,9 +3772,14 @@ function ChatPanel(props) {
|
|
|
3646
3772
|
onData,
|
|
3647
3773
|
requestHeaders,
|
|
3648
3774
|
onOpenArtifact,
|
|
3775
|
+
onUploadFile,
|
|
3649
3776
|
debug = false
|
|
3650
3777
|
} = props;
|
|
3651
3778
|
const [debugWidth, setDebugWidth] = useState13(440);
|
|
3779
|
+
const [pendingMessage, setPendingMessage] = useState13(null);
|
|
3780
|
+
const pendingMessageRef = useRef10(null);
|
|
3781
|
+
const consumedFollowUpRef = useRef10(null);
|
|
3782
|
+
const followUpSplitIdsRef = useRef10(null);
|
|
3652
3783
|
const {
|
|
3653
3784
|
messages,
|
|
3654
3785
|
sendMessage,
|
|
@@ -3657,7 +3788,22 @@ function ChatPanel(props) {
|
|
|
3657
3788
|
error,
|
|
3658
3789
|
stop,
|
|
3659
3790
|
clearError
|
|
3660
|
-
} = useAgentChat({
|
|
3791
|
+
} = useAgentChat({
|
|
3792
|
+
sessionId,
|
|
3793
|
+
onData: (part) => {
|
|
3794
|
+
if (part?.type === "data-followup-consumed") {
|
|
3795
|
+
const pending = pendingMessageRef.current;
|
|
3796
|
+
consumedFollowUpRef.current = pending ? { text: pending.text, files: pending.files } : null;
|
|
3797
|
+
followUpSplitIdsRef.current = {
|
|
3798
|
+
userId: globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-followup-user`,
|
|
3799
|
+
assistantId: globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-followup-assistant`
|
|
3800
|
+
};
|
|
3801
|
+
setPendingMessage(null);
|
|
3802
|
+
}
|
|
3803
|
+
onData?.(part);
|
|
3804
|
+
},
|
|
3805
|
+
requestHeaders
|
|
3806
|
+
});
|
|
3661
3807
|
const mergedToolRenderers = mergeShadcnToolRenderers(toolRenderers);
|
|
3662
3808
|
const registry = useMemo10(
|
|
3663
3809
|
() => createCommandRegistry([...builtinCommands, ...extraCommands ?? []]),
|
|
@@ -3777,6 +3923,70 @@ function ChatPanel(props) {
|
|
|
3777
3923
|
return () => globalThis.removeEventListener?.("boring:model-change", onChange);
|
|
3778
3924
|
}, []);
|
|
3779
3925
|
const isStreaming = status === "submitted" || status === "streaming";
|
|
3926
|
+
const displayMessages = useMemo10(() => {
|
|
3927
|
+
const consumed = consumedFollowUpRef.current ?? pendingMessageRef.current;
|
|
3928
|
+
const ids = followUpSplitIdsRef.current;
|
|
3929
|
+
if (!consumed || !ids) return messages;
|
|
3930
|
+
const hasFollowUpPart = messages.some(
|
|
3931
|
+
(m) => m.role === "assistant" && m.parts?.some((p) => p.id?.startsWith("turn-"))
|
|
3932
|
+
);
|
|
3933
|
+
if (!hasFollowUpPart) return messages;
|
|
3934
|
+
let n = 0;
|
|
3935
|
+
return splitFollowUp(messages, consumed, () => n++ === 0 ? ids.userId : ids.assistantId);
|
|
3936
|
+
}, [messages]);
|
|
3937
|
+
const prevStatusForQueue = useRef10(status);
|
|
3938
|
+
useEffect11(() => {
|
|
3939
|
+
const prev = prevStatusForQueue.current;
|
|
3940
|
+
prevStatusForQueue.current = status;
|
|
3941
|
+
if (status !== "ready") return;
|
|
3942
|
+
if (prev !== "streaming" && prev !== "submitted") return;
|
|
3943
|
+
const consumed = consumedFollowUpRef.current ?? pendingMessageRef.current;
|
|
3944
|
+
if (!consumed) return;
|
|
3945
|
+
const ids = followUpSplitIdsRef.current;
|
|
3946
|
+
consumedFollowUpRef.current = null;
|
|
3947
|
+
followUpSplitIdsRef.current = null;
|
|
3948
|
+
pendingMessageRef.current = null;
|
|
3949
|
+
setPendingMessage(null);
|
|
3950
|
+
let n = 0;
|
|
3951
|
+
const genId = () => ids ? n++ === 0 ? ids.userId : ids.assistantId : globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`;
|
|
3952
|
+
const nextMessages = splitFollowUp(messages, consumed, genId);
|
|
3953
|
+
setMessages(nextMessages);
|
|
3954
|
+
fetch(`/api/v1/agent/chat/${encodeURIComponent(sessionId)}/messages`, {
|
|
3955
|
+
method: "PUT",
|
|
3956
|
+
headers: {
|
|
3957
|
+
"Content-Type": "application/json",
|
|
3958
|
+
...requestHeaders
|
|
3959
|
+
},
|
|
3960
|
+
body: JSON.stringify({ messages: nextMessages })
|
|
3961
|
+
}).catch(() => {
|
|
3962
|
+
});
|
|
3963
|
+
}, [status, messages, sessionId, requestHeaders, setMessages]);
|
|
3964
|
+
const handleStop = useCallback12(() => {
|
|
3965
|
+
stop();
|
|
3966
|
+
consumedFollowUpRef.current = null;
|
|
3967
|
+
followUpSplitIdsRef.current = null;
|
|
3968
|
+
pendingMessageRef.current = null;
|
|
3969
|
+
setPendingMessage(null);
|
|
3970
|
+
fetch(`/api/v1/agent/chat/${encodeURIComponent(sessionId)}/followup`, {
|
|
3971
|
+
method: "DELETE",
|
|
3972
|
+
headers: requestHeaders
|
|
3973
|
+
}).catch(() => {
|
|
3974
|
+
});
|
|
3975
|
+
}, [stop, sessionId, requestHeaders]);
|
|
3976
|
+
const handleInterrupt = useCallback12(() => {
|
|
3977
|
+
stop();
|
|
3978
|
+
}, [stop]);
|
|
3979
|
+
useEffect11(() => {
|
|
3980
|
+
const onKeyDown = (e) => {
|
|
3981
|
+
if (e.key !== "Escape" || !isStreaming) return;
|
|
3982
|
+
const tag = e.target?.tagName;
|
|
3983
|
+
if (tag === "INPUT" || tag === "TEXTAREA") return;
|
|
3984
|
+
e.preventDefault();
|
|
3985
|
+
handleInterrupt();
|
|
3986
|
+
};
|
|
3987
|
+
window.addEventListener("keydown", onKeyDown);
|
|
3988
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
3989
|
+
}, [isStreaming, handleInterrupt]);
|
|
3780
3990
|
const userHistory = useMemo10(
|
|
3781
3991
|
() => messages.filter((m) => m.role === "user").map((m) => m.parts.filter((p) => p.type === "text").map((p) => p.text).join("\n").trim()).filter(Boolean),
|
|
3782
3992
|
[messages]
|
|
@@ -3935,6 +4145,29 @@ ${content}
|
|
|
3935
4145
|
...mentionNote ? [mentionNote] : []
|
|
3936
4146
|
].filter(Boolean).join("\n\n") || text;
|
|
3937
4147
|
setMentionedFiles([]);
|
|
4148
|
+
const resolvedAttachments = await resolveAttachmentUrls(files);
|
|
4149
|
+
if (isStreaming) {
|
|
4150
|
+
if (!pendingMessage) {
|
|
4151
|
+
const nextPending = {
|
|
4152
|
+
text,
|
|
4153
|
+
files: files ?? [],
|
|
4154
|
+
serverMessage,
|
|
4155
|
+
attachments: resolvedAttachments
|
|
4156
|
+
};
|
|
4157
|
+
pendingMessageRef.current = nextPending;
|
|
4158
|
+
setPendingMessage(nextPending);
|
|
4159
|
+
fetch(`/api/v1/agent/chat/${encodeURIComponent(sessionId)}/followup`, {
|
|
4160
|
+
method: "POST",
|
|
4161
|
+
headers: {
|
|
4162
|
+
"Content-Type": "application/json",
|
|
4163
|
+
...requestHeaders
|
|
4164
|
+
},
|
|
4165
|
+
body: JSON.stringify({ message: serverMessage, attachments: resolvedAttachments })
|
|
4166
|
+
}).catch(() => {
|
|
4167
|
+
});
|
|
4168
|
+
}
|
|
4169
|
+
return;
|
|
4170
|
+
}
|
|
3938
4171
|
void sendMessage(
|
|
3939
4172
|
{
|
|
3940
4173
|
// Rendered bubble: unchanged user text + file chips via files parts.
|
|
@@ -3951,13 +4184,7 @@ ${content}
|
|
|
3951
4184
|
// schema treats it as optional; omitting it keeps the existing
|
|
3952
4185
|
// 'off' default behaviour for hosts that don't expose the toggle.
|
|
3953
4186
|
...thinkingControl ? { thinkingLevel } : {},
|
|
3954
|
-
attachments:
|
|
3955
|
-
filename: f.filename,
|
|
3956
|
-
mediaType: f.mediaType,
|
|
3957
|
-
// Keep the data URL so the server could later forward images
|
|
3958
|
-
// to multimodal-capable providers.
|
|
3959
|
-
url: f.url
|
|
3960
|
-
})) ?? []
|
|
4187
|
+
attachments: resolvedAttachments
|
|
3961
4188
|
}
|
|
3962
4189
|
}
|
|
3963
4190
|
);
|
|
@@ -4007,7 +4234,7 @@ ${content}
|
|
|
4007
4234
|
"mx-auto flex w-full flex-col gap-6",
|
|
4008
4235
|
chrome ? "max-w-3xl px-6 py-8" : "max-w-[680px] px-4 py-4"
|
|
4009
4236
|
), children: [
|
|
4010
|
-
|
|
4237
|
+
displayMessages.length === 0 && /* @__PURE__ */ jsx23(
|
|
4011
4238
|
ChatEmptyState,
|
|
4012
4239
|
{
|
|
4013
4240
|
eyebrow: emptyEyebrow,
|
|
@@ -4031,7 +4258,7 @@ ${content}
|
|
|
4031
4258
|
}
|
|
4032
4259
|
}
|
|
4033
4260
|
),
|
|
4034
|
-
|
|
4261
|
+
displayMessages.map((message, messageIndex) => {
|
|
4035
4262
|
const role = message.role === "user" || message.role === "assistant" ? message.role : "assistant";
|
|
4036
4263
|
const textParts = message.parts.filter(isTextPart);
|
|
4037
4264
|
const fileParts = message.parts.filter(isFilePart);
|
|
@@ -4063,6 +4290,7 @@ ${reasoningPart.text}`;
|
|
|
4063
4290
|
} else {
|
|
4064
4291
|
acc.push({ kind: "tool-group", tools: [{ part: item.part, key: item.key }], key: item.key });
|
|
4065
4292
|
}
|
|
4293
|
+
} else if (item.kind === "part" && !isTextPart(item.part)) {
|
|
4066
4294
|
} else {
|
|
4067
4295
|
acc.push(item);
|
|
4068
4296
|
}
|
|
@@ -4196,6 +4424,10 @@ ${reasoningPart.text}`;
|
|
|
4196
4424
|
message.id
|
|
4197
4425
|
);
|
|
4198
4426
|
}),
|
|
4427
|
+
pendingMessage && /* @__PURE__ */ jsx23(Message, { from: "user", children: /* @__PURE__ */ jsxs21(MessageContent, { className: "opacity-50", children: [
|
|
4428
|
+
/* @__PURE__ */ jsx23("div", { className: "text-[11px] font-medium text-muted-foreground mb-1", children: "Follow-up" }),
|
|
4429
|
+
/* @__PURE__ */ jsx23("div", { className: "text-sm whitespace-pre-wrap", children: pendingMessage.text })
|
|
4430
|
+
] }) }),
|
|
4199
4431
|
(() => {
|
|
4200
4432
|
if (!error) return null;
|
|
4201
4433
|
const friendly = friendlyError(error);
|
|
@@ -4328,6 +4560,7 @@ ${reasoningPart.text}`;
|
|
|
4328
4560
|
{
|
|
4329
4561
|
"data-boring-state": status,
|
|
4330
4562
|
onSubmit: handleSubmit,
|
|
4563
|
+
onUploadFile,
|
|
4331
4564
|
multiple: true,
|
|
4332
4565
|
maxFiles: 20,
|
|
4333
4566
|
maxFileSize: 5 * 1024 * 1024,
|
|
@@ -4403,7 +4636,7 @@ ${reasoningPart.text}`;
|
|
|
4403
4636
|
PromptInputSubmit,
|
|
4404
4637
|
{
|
|
4405
4638
|
status,
|
|
4406
|
-
onStop:
|
|
4639
|
+
onStop: handleStop,
|
|
4407
4640
|
className: cn(
|
|
4408
4641
|
// Primary action. Uses the warm accent (not `primary`,
|
|
4409
4642
|
// which is a neutral foreground tone) — this is the one
|
|
@@ -4684,15 +4917,20 @@ function AttachmentsList() {
|
|
|
4684
4917
|
// Compact pill — medium border, muted fill, room for a
|
|
4685
4918
|
// thumbnail + name + remove action.
|
|
4686
4919
|
"!h-9 !gap-2 !rounded-full !border-input/80 !bg-muted/40 !pl-1 !pr-2",
|
|
4687
|
-
"transition-colors hover:!bg-muted/70 hover:!text-foreground"
|
|
4920
|
+
"transition-colors hover:!bg-muted/70 hover:!text-foreground",
|
|
4921
|
+
file.status === "error" && "!border-destructive/50 !bg-destructive/10"
|
|
4688
4922
|
),
|
|
4689
4923
|
children: [
|
|
4690
|
-
/* @__PURE__ */
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4924
|
+
/* @__PURE__ */ jsxs21("div", { className: "relative shrink-0", children: [
|
|
4925
|
+
/* @__PURE__ */ jsx23(
|
|
4926
|
+
AttachmentPreview,
|
|
4927
|
+
{
|
|
4928
|
+
className: "!size-7 overflow-hidden !rounded-full bg-background/60"
|
|
4929
|
+
}
|
|
4930
|
+
),
|
|
4931
|
+
file.status === "uploading" && /* @__PURE__ */ jsx23("div", { className: "absolute inset-0 flex items-center justify-center rounded-full bg-background/70", children: /* @__PURE__ */ jsx23(Loader2, { className: "size-3.5 animate-spin text-muted-foreground" }) }),
|
|
4932
|
+
file.status === "error" && /* @__PURE__ */ jsx23("div", { className: "absolute inset-0 flex items-center justify-center rounded-full bg-destructive/20", children: /* @__PURE__ */ jsx23(AlertCircleIcon, { className: "size-3.5 text-destructive" }) })
|
|
4933
|
+
] }),
|
|
4696
4934
|
/* @__PURE__ */ jsx23(
|
|
4697
4935
|
AttachmentInfo,
|
|
4698
4936
|
{
|
|
@@ -5020,6 +5258,7 @@ export {
|
|
|
5020
5258
|
mergeToolRenderers,
|
|
5021
5259
|
parseSlashCommand,
|
|
5022
5260
|
resolveToolRenderer,
|
|
5261
|
+
uploadFile,
|
|
5023
5262
|
useAgentChat,
|
|
5024
5263
|
useOpenArtifact,
|
|
5025
5264
|
useSessions
|
package/dist/front/styles.css
CHANGED
|
@@ -978,6 +978,12 @@
|
|
|
978
978
|
--tw-border-style: none;
|
|
979
979
|
border-style: none;
|
|
980
980
|
}
|
|
981
|
+
.\!border-destructive\/50 {
|
|
982
|
+
border-color: var(--destructive) !important;
|
|
983
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
984
|
+
border-color: color-mix(in oklab, var(--destructive) 50%, transparent) !important;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
981
987
|
.\!border-input\/80 {
|
|
982
988
|
border-color: var(--input) !important;
|
|
983
989
|
@supports (color: color-mix(in lab, red, red)) {
|
|
@@ -1089,6 +1095,12 @@
|
|
|
1089
1095
|
.border-l-transparent {
|
|
1090
1096
|
border-left-color: transparent;
|
|
1091
1097
|
}
|
|
1098
|
+
.\!bg-destructive\/10 {
|
|
1099
|
+
background-color: var(--destructive) !important;
|
|
1100
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
1101
|
+
background-color: color-mix(in oklab, var(--destructive) 10%, transparent) !important;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1092
1104
|
.\!bg-muted\/40 {
|
|
1093
1105
|
background-color: var(--muted) !important;
|
|
1094
1106
|
@supports (color: color-mix(in lab, red, red)) {
|
|
@@ -1206,6 +1218,12 @@
|
|
|
1206
1218
|
background-color: color-mix(in oklab, var(--background) 60%, transparent);
|
|
1207
1219
|
}
|
|
1208
1220
|
}
|
|
1221
|
+
.bg-background\/70 {
|
|
1222
|
+
background-color: var(--background);
|
|
1223
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
1224
|
+
background-color: color-mix(in oklab, var(--background) 70%, transparent);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1209
1227
|
.bg-background\/80 {
|
|
1210
1228
|
background-color: var(--background);
|
|
1211
1229
|
@supports (color: color-mix(in lab, red, red)) {
|
|
@@ -1278,6 +1296,12 @@
|
|
|
1278
1296
|
background-color: color-mix(in oklab, var(--destructive) 10%, transparent);
|
|
1279
1297
|
}
|
|
1280
1298
|
}
|
|
1299
|
+
.bg-destructive\/20 {
|
|
1300
|
+
background-color: var(--destructive);
|
|
1301
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
1302
|
+
background-color: color-mix(in oklab, var(--destructive) 20%, transparent);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1281
1305
|
.bg-foreground {
|
|
1282
1306
|
background-color: var(--foreground);
|
|
1283
1307
|
}
|