@hachej/boring-agent 0.1.47 → 0.1.49
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/{chunk-HEEJSOFF.js → chunk-G6YPXDHR.js} +2 -1
- package/dist/front/index.d.ts +45 -40
- package/dist/front/index.js +102 -39
- package/dist/front/styles.css +3 -3
- package/dist/{piChatEvent-DNKo77Xw.d.ts → piChatEvent-B6GDifo2.d.ts} +1 -0
- package/dist/server/index.js +211 -68
- package/dist/shared/index.d.ts +2 -2
- package/dist/shared/index.js +1 -1
- package/package.json +2 -2
|
@@ -170,7 +170,8 @@ var BoringChatPartSchema = z2.discriminatedUnion("type", [
|
|
|
170
170
|
id: z2.string().optional(),
|
|
171
171
|
filename: z2.string().optional(),
|
|
172
172
|
mediaType: z2.string().optional(),
|
|
173
|
-
url: z2.string().optional()
|
|
173
|
+
url: z2.string().optional(),
|
|
174
|
+
path: z2.string().optional()
|
|
174
175
|
}),
|
|
175
176
|
z2.object({
|
|
176
177
|
type: z2.literal("notice"),
|
package/dist/front/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import * as react from 'react';
|
|
3
|
-
import {
|
|
4
|
-
import { FileUIPart,
|
|
5
|
-
import { T as ToolUiMetadata, B as BoringChatPart, d as BoringChatMessage, m as PiChatStatus, r as QueuedUserMessage, C as ChatError, P as PiChatEvent, o as PromptPayload, p as PromptReceipt, F as FollowUpPayload, i as FollowUpReceipt, Q as QueueClearPayload, q as QueueClearReceipt, I as InterruptPayload, e as CommandReceipt, S as StopPayload, s as StopReceipt } from '../piChatEvent-
|
|
3
|
+
import { ComponentProps, HTMLAttributes, FormEvent, ReactNode, ComponentType } from 'react';
|
|
4
|
+
import { FileUIPart, ChatStatus, UIMessage } from 'ai';
|
|
5
|
+
import { T as ToolUiMetadata, B as BoringChatPart, d as BoringChatMessage, m as PiChatStatus, r as QueuedUserMessage, C as ChatError, P as PiChatEvent, o as PromptPayload, p as PromptReceipt, F as FollowUpPayload, i as FollowUpReceipt, Q as QueueClearPayload, q as QueueClearReceipt, I as InterruptPayload, e as CommandReceipt, S as StopPayload, s as StopReceipt } from '../piChatEvent-B6GDifo2.js';
|
|
6
6
|
import { e as SessionSummary } from '../chatSubmitPayload-DHqQL2wD.js';
|
|
7
|
-
import {
|
|
7
|
+
import { InputGroupAddon, InputGroupButton, InputGroupTextarea, Button, Collapsible, CollapsibleContent, CollapsibleTrigger } from '@hachej/boring-ui-kit';
|
|
8
8
|
import { Streamdown } from 'streamdown';
|
|
9
9
|
import { StickToBottom } from 'use-stick-to-bottom';
|
|
10
10
|
import { ClassValue } from 'clsx';
|
|
@@ -15,6 +15,7 @@ interface UploadFileOptions {
|
|
|
15
15
|
workspaceRequestId?: string | null;
|
|
16
16
|
directory?: string;
|
|
17
17
|
sourcePath?: string;
|
|
18
|
+
responseUrl?: 'markdown' | 'raw';
|
|
18
19
|
fetch?: typeof globalThis.fetch;
|
|
19
20
|
}
|
|
20
21
|
interface UploadFileResult {
|
|
@@ -23,6 +24,45 @@ interface UploadFileResult {
|
|
|
23
24
|
}
|
|
24
25
|
declare function uploadFile(file: File, opts?: UploadFileOptions): Promise<UploadFileResult>;
|
|
25
26
|
|
|
27
|
+
type PromptInputFilePart = FileUIPart & {
|
|
28
|
+
path?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type PromptInputFooterProps = ComponentProps<typeof InputGroupAddon>;
|
|
32
|
+
declare const PromptInputFooter: ({ align, className, ...props }: PromptInputFooterProps) => react_jsx_runtime.JSX.Element;
|
|
33
|
+
|
|
34
|
+
interface PromptInputMessage {
|
|
35
|
+
text: string;
|
|
36
|
+
files: PromptInputFilePart[];
|
|
37
|
+
}
|
|
38
|
+
type PromptInputProps = Omit<HTMLAttributes<HTMLFormElement>, "onSubmit" | "onError"> & {
|
|
39
|
+
accept?: string;
|
|
40
|
+
multiple?: boolean;
|
|
41
|
+
globalDrop?: boolean;
|
|
42
|
+
syncHiddenInput?: boolean;
|
|
43
|
+
maxFiles?: number;
|
|
44
|
+
maxFileSize?: number;
|
|
45
|
+
onError?: (err: {
|
|
46
|
+
code: "max_files" | "max_file_size" | "accept";
|
|
47
|
+
message: string;
|
|
48
|
+
}) => void;
|
|
49
|
+
onSubmit: (message: PromptInputMessage, event: FormEvent<HTMLFormElement>) => false | void | Promise<false | void>;
|
|
50
|
+
/** When provided, files are uploaded to the server immediately on add and the
|
|
51
|
+
* attachment URL is replaced with the stable server path before submit. */
|
|
52
|
+
onUploadFile?: (file: File) => Promise<{
|
|
53
|
+
url: string;
|
|
54
|
+
path?: string;
|
|
55
|
+
}>;
|
|
56
|
+
};
|
|
57
|
+
declare const PromptInput: ({ className, accept, multiple, globalDrop, syncHiddenInput, maxFiles, maxFileSize, onError, onSubmit, onUploadFile, children, ...props }: PromptInputProps) => react_jsx_runtime.JSX.Element;
|
|
58
|
+
type PromptInputTextareaProps = ComponentProps<typeof InputGroupTextarea>;
|
|
59
|
+
declare const PromptInputTextarea: ({ onChange, onKeyDown, className, placeholder, ...props }: PromptInputTextareaProps) => react_jsx_runtime.JSX.Element;
|
|
60
|
+
type PromptInputSubmitProps = ComponentProps<typeof InputGroupButton> & {
|
|
61
|
+
status?: ChatStatus;
|
|
62
|
+
onStop?: () => void;
|
|
63
|
+
};
|
|
64
|
+
declare const PromptInputSubmit: ({ className, variant, size, status, onStop, onClick, children, ...props }: PromptInputSubmitProps) => react_jsx_runtime.JSX.Element;
|
|
65
|
+
|
|
26
66
|
/**
|
|
27
67
|
* Extended-thinking budget. Sent through the agent runtime to providers that
|
|
28
68
|
* support reasoning controls. 'off' means no reasoning chunks; the higher
|
|
@@ -499,7 +539,7 @@ interface ComposerBlocker {
|
|
|
499
539
|
|
|
500
540
|
type ChatSubmitSource = 'composer' | 'suggestion' | 'auto-submit';
|
|
501
541
|
interface ChatSubmitContext {
|
|
502
|
-
files:
|
|
542
|
+
files: PromptInputFilePart[];
|
|
503
543
|
sessionId: string;
|
|
504
544
|
source: ChatSubmitSource;
|
|
505
545
|
}
|
|
@@ -674,41 +714,6 @@ type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
|
|
|
674
714
|
};
|
|
675
715
|
declare const CodeBlock: ({ code, language, showLineNumbers, className, children, ...props }: CodeBlockProps) => react_jsx_runtime.JSX.Element;
|
|
676
716
|
|
|
677
|
-
type PromptInputFooterProps = ComponentProps<typeof InputGroupAddon>;
|
|
678
|
-
declare const PromptInputFooter: ({ align, className, ...props }: PromptInputFooterProps) => react_jsx_runtime.JSX.Element;
|
|
679
|
-
|
|
680
|
-
interface PromptInputMessage {
|
|
681
|
-
text: string;
|
|
682
|
-
files: FileUIPart[];
|
|
683
|
-
}
|
|
684
|
-
type PromptInputProps = Omit<HTMLAttributes<HTMLFormElement>, "onSubmit" | "onError"> & {
|
|
685
|
-
accept?: string;
|
|
686
|
-
multiple?: boolean;
|
|
687
|
-
globalDrop?: boolean;
|
|
688
|
-
syncHiddenInput?: boolean;
|
|
689
|
-
maxFiles?: number;
|
|
690
|
-
maxFileSize?: number;
|
|
691
|
-
onError?: (err: {
|
|
692
|
-
code: "max_files" | "max_file_size" | "accept";
|
|
693
|
-
message: string;
|
|
694
|
-
}) => void;
|
|
695
|
-
onSubmit: (message: PromptInputMessage, event: FormEvent<HTMLFormElement>) => false | void | Promise<false | void>;
|
|
696
|
-
/** When provided, files are uploaded to the server immediately on add and the
|
|
697
|
-
* attachment URL is replaced with the stable server path before submit. */
|
|
698
|
-
onUploadFile?: (file: File) => Promise<{
|
|
699
|
-
url: string;
|
|
700
|
-
path?: string;
|
|
701
|
-
}>;
|
|
702
|
-
};
|
|
703
|
-
declare const PromptInput: ({ className, accept, multiple, globalDrop, syncHiddenInput, maxFiles, maxFileSize, onError, onSubmit, onUploadFile, children, ...props }: PromptInputProps) => react_jsx_runtime.JSX.Element;
|
|
704
|
-
type PromptInputTextareaProps = ComponentProps<typeof InputGroupTextarea>;
|
|
705
|
-
declare const PromptInputTextarea: ({ onChange, onKeyDown, className, placeholder, ...props }: PromptInputTextareaProps) => react_jsx_runtime.JSX.Element;
|
|
706
|
-
type PromptInputSubmitProps = ComponentProps<typeof InputGroupButton> & {
|
|
707
|
-
status?: ChatStatus;
|
|
708
|
-
onStop?: () => void;
|
|
709
|
-
};
|
|
710
|
-
declare const PromptInputSubmit: ({ className, variant, size, status, onStop, onClick, children, ...props }: PromptInputSubmitProps) => react_jsx_runtime.JSX.Element;
|
|
711
|
-
|
|
712
717
|
declare function cn(...inputs: ClassValue[]): string;
|
|
713
718
|
|
|
714
719
|
export { type AgentCommandContribution, type AgentCommandOptions, ArtifactOpenProvider, ChatEmptyState, type ChatEmptyStateProps, PiChatPanel as ChatPanel, type PiChatPanelProps as ChatPanelProps, type ChatSuggestion, CodeBlock, type CommandRegistry, Conversation, ConversationContent, ConversationScrollButton, DebugDrawer, type GroupedToolEntry, Message, MessageAction, MessageActions, MessageContent, MessageResponse, type OpenArtifactHandler, type ParsedCommand, PiChatPanel, type PiChatPanelProps, SessionBrowser as PiSessionBrowser, type PiSessionCreateInit, SessionList as PiSessionList, PromptInput, PromptInputFooter, PromptInputSubmit, PromptInputTextarea, Reasoning, ReasoningContent, ReasoningTrigger, type SessionListProps, type SlashCommand, type SlashCommandContext, ToolCallGroup, type ToolPart, type ToolRenderer, type ToolRendererOverrides, type UploadFileOptions, type UploadFileResult, type UsePiSessionsOptions, type UsePiSessionsResult, activeSessionStorageKey, builtinCommands, clearActiveSessionId, cn, createCommandRegistry, defaultChatSuggestions, defaultToolRenderers, getAgentCommands, mergeShadcnToolRenderers, mergeToolRenderers, parseSlashCommand, readActiveSessionId, resolveToolRenderer, uploadFile, useOpenArtifact, usePiSessions, writeActiveSessionId };
|
package/dist/front/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
StopReceiptSchema,
|
|
14
14
|
extractToolUiMetadata,
|
|
15
15
|
sanitizeToolUiMetadata
|
|
16
|
-
} from "../chunk-
|
|
16
|
+
} from "../chunk-G6YPXDHR.js";
|
|
17
17
|
import {
|
|
18
18
|
DebugDrawer,
|
|
19
19
|
cn,
|
|
@@ -30,7 +30,7 @@ function readAsDataUrl(file) {
|
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
async function uploadFile(file, opts = {}) {
|
|
33
|
-
const { apiBaseUrl = "", workspaceRequestId, directory, sourcePath, fetch: fetchImpl = globalThis.fetch } = opts;
|
|
33
|
+
const { apiBaseUrl = "", workspaceRequestId, directory, sourcePath, responseUrl = "markdown", fetch: fetchImpl = globalThis.fetch } = opts;
|
|
34
34
|
const dataUrl = await readAsDataUrl(file);
|
|
35
35
|
const comma = dataUrl.indexOf(",");
|
|
36
36
|
const contentBase64 = comma >= 0 ? dataUrl.slice(comma + 1) : "";
|
|
@@ -51,9 +51,20 @@ async function uploadFile(file, opts = {}) {
|
|
|
51
51
|
});
|
|
52
52
|
if (!res.ok) throw new Error(`Upload failed: ${res.status}`);
|
|
53
53
|
const body = await res.json();
|
|
54
|
-
const
|
|
54
|
+
const path = body.path;
|
|
55
|
+
if (responseUrl === "raw") {
|
|
56
|
+
if (!path) throw new Error("Upload response missing path");
|
|
57
|
+
return { url: rawWorkspaceFileUrl(path, { apiBaseUrl, workspaceRequestId }), path };
|
|
58
|
+
}
|
|
59
|
+
const url = body.markdownUrl ?? path;
|
|
55
60
|
if (!url) throw new Error("Upload response missing url");
|
|
56
|
-
return { url, path:
|
|
61
|
+
return { url, path: path ?? url };
|
|
62
|
+
}
|
|
63
|
+
function rawWorkspaceFileUrl(path, opts) {
|
|
64
|
+
const base = (opts.apiBaseUrl ?? "").replace(/\/$/, "");
|
|
65
|
+
const params = new URLSearchParams({ path });
|
|
66
|
+
if (opts.workspaceRequestId) params.set("workspaceId", opts.workspaceRequestId);
|
|
67
|
+
return `${base}/api/v1/files/raw?${params.toString()}`;
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
// src/front/chat/PiChatPanel.tsx
|
|
@@ -291,15 +302,6 @@ var builtinCommands = [
|
|
|
291
302
|
if (ctx.openThinkingPicker?.() === false) return { preserveDraft: true };
|
|
292
303
|
}
|
|
293
304
|
},
|
|
294
|
-
{
|
|
295
|
-
name: "think",
|
|
296
|
-
description: "Alias for /thinking",
|
|
297
|
-
handler(args, ctx) {
|
|
298
|
-
const query = args.trim();
|
|
299
|
-
if (query) return ctx.selectComposerThinking?.(query);
|
|
300
|
-
if (ctx.openThinkingPicker?.() === false) return { preserveDraft: true };
|
|
301
|
-
}
|
|
302
|
-
},
|
|
303
305
|
{
|
|
304
306
|
name: "help",
|
|
305
307
|
description: "Show available commands",
|
|
@@ -2231,7 +2233,7 @@ async function resolveAttachmentUrls(files) {
|
|
|
2231
2233
|
filename: file.filename,
|
|
2232
2234
|
mediaType: file.mediaType,
|
|
2233
2235
|
url: file.url.startsWith("blob:") ? await convertBlobUrlToDataUrl(file.url) ?? file.url : file.url,
|
|
2234
|
-
...
|
|
2236
|
+
...file.path ? { path: file.path } : {}
|
|
2235
2237
|
})));
|
|
2236
2238
|
}
|
|
2237
2239
|
async function readFileAsText(file) {
|
|
@@ -2247,6 +2249,9 @@ async function readFileAsText(file) {
|
|
|
2247
2249
|
}
|
|
2248
2250
|
|
|
2249
2251
|
// src/front/chatSubmit.ts
|
|
2252
|
+
function escapeAttachmentAttr(value) {
|
|
2253
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
2254
|
+
}
|
|
2250
2255
|
async function createEnrichedSubmitPayload({
|
|
2251
2256
|
text,
|
|
2252
2257
|
files,
|
|
@@ -2256,18 +2261,23 @@ async function createEnrichedSubmitPayload({
|
|
|
2256
2261
|
for (const file of files ?? []) {
|
|
2257
2262
|
const label = file.filename ?? "attachment";
|
|
2258
2263
|
const mime = file.mediaType ?? "application/octet-stream";
|
|
2259
|
-
const workspacePath =
|
|
2260
|
-
const
|
|
2261
|
-
|
|
2262
|
-
|
|
2264
|
+
const workspacePath = file.path;
|
|
2265
|
+
const attrs = [
|
|
2266
|
+
["data-boring-agent", "composer-file"],
|
|
2267
|
+
["filename", label],
|
|
2268
|
+
["mime", mime]
|
|
2269
|
+
];
|
|
2270
|
+
if (workspacePath) attrs.push(["path", workspacePath]);
|
|
2271
|
+
const attrText = attrs.map(([name, value]) => `${name}="${escapeAttachmentAttr(value)}"`).join(" ");
|
|
2263
2272
|
const content = await readFileAsText(file);
|
|
2264
2273
|
if (content !== null) {
|
|
2265
|
-
attachmentSummaries.push(
|
|
2274
|
+
attachmentSummaries.push(`<attachment ${attrText}>
|
|
2266
2275
|
\`\`\`
|
|
2267
2276
|
${content}
|
|
2268
|
-
|
|
2277
|
+
\`\`\`
|
|
2278
|
+
</attachment>`);
|
|
2269
2279
|
} else {
|
|
2270
|
-
attachmentSummaries.push(
|
|
2280
|
+
attachmentSummaries.push(`<attachment ${attrText} binary="true" />`);
|
|
2271
2281
|
}
|
|
2272
2282
|
}
|
|
2273
2283
|
const mentionNote = mentionedFiles.length > 0 ? `@files: ${mentionedFiles.join(", ")}` : null;
|
|
@@ -2500,7 +2510,8 @@ var PiComposerPolicyController = class {
|
|
|
2500
2510
|
return files.map((file) => ({
|
|
2501
2511
|
filename: file.filename,
|
|
2502
2512
|
mediaType: file.mediaType,
|
|
2503
|
-
url: file.url
|
|
2513
|
+
url: file.url,
|
|
2514
|
+
...file.path ? { path: file.path } : {}
|
|
2504
2515
|
}));
|
|
2505
2516
|
}
|
|
2506
2517
|
};
|
|
@@ -4904,7 +4915,8 @@ function toOptimisticUserMessage(payload) {
|
|
|
4904
4915
|
id: `optimistic:${payload.clientNonce}:file:${index}`,
|
|
4905
4916
|
filename: attachment.filename,
|
|
4906
4917
|
mediaType: attachment.mediaType,
|
|
4907
|
-
url: attachment.url
|
|
4918
|
+
url: attachment.url,
|
|
4919
|
+
path: attachment.path
|
|
4908
4920
|
}))
|
|
4909
4921
|
]
|
|
4910
4922
|
};
|
|
@@ -6736,6 +6748,8 @@ function PiTimelineMessage({ message, isLast, isStreaming, showThoughts, toolRen
|
|
|
6736
6748
|
const textParts = message.parts.filter((part) => part.type === "text");
|
|
6737
6749
|
const fileParts = message.parts.filter((part) => part.type === "file");
|
|
6738
6750
|
const finalParts = groupRenderableParts(message);
|
|
6751
|
+
const attachmentSummaryPaths = role === "user" ? attachmentPathsFromTextParts(textParts) : [];
|
|
6752
|
+
const openArtifact = useOpenArtifact();
|
|
6739
6753
|
const shouldReserveStreamingActions = isStreaming && isAssistant && isLast;
|
|
6740
6754
|
return /* @__PURE__ */ jsxs20(
|
|
6741
6755
|
Message,
|
|
@@ -6767,17 +6781,39 @@ function PiTimelineMessage({ message, isLast, isStreaming, showThoughts, toolRen
|
|
|
6767
6781
|
role === "user" ? "mb-2 w-full gap-2" : "gap-1.5",
|
|
6768
6782
|
role === "user" ? "[&>div]:max-w-full [&>div]:border-input/60 [&>div]:bg-background/35 [&>div]:px-1.5 [&>div]:text-secondary-foreground [&>div:hover]:bg-background/45" : void 0
|
|
6769
6783
|
),
|
|
6770
|
-
children: fileParts.map((file, index) =>
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6784
|
+
children: fileParts.map((file, index) => {
|
|
6785
|
+
const openPath = file.path ?? attachmentSummaryPaths[index];
|
|
6786
|
+
const canOpen = Boolean(openArtifact && openPath);
|
|
6787
|
+
const openAttachment = () => {
|
|
6788
|
+
if (!openPath) return;
|
|
6789
|
+
openArtifact?.(openPath);
|
|
6790
|
+
};
|
|
6791
|
+
const openAttachmentFromKeyboard = (event) => {
|
|
6792
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
6793
|
+
event.preventDefault();
|
|
6794
|
+
openAttachment();
|
|
6795
|
+
};
|
|
6796
|
+
return /* @__PURE__ */ jsxs20(
|
|
6797
|
+
Attachment,
|
|
6798
|
+
{
|
|
6799
|
+
data: toAttachmentData(file, `file-${message.id}-${index}`),
|
|
6800
|
+
...canOpen ? {
|
|
6801
|
+
role: "button",
|
|
6802
|
+
tabIndex: 0,
|
|
6803
|
+
title: `Open ${openPath} in workspace`,
|
|
6804
|
+
"aria-label": `Open ${file.filename ?? openPath} in workspace`,
|
|
6805
|
+
"data-workspace-path": openPath,
|
|
6806
|
+
onClick: openAttachment,
|
|
6807
|
+
onKeyDown: openAttachmentFromKeyboard
|
|
6808
|
+
} : void 0,
|
|
6809
|
+
children: [
|
|
6810
|
+
/* @__PURE__ */ jsx22(AttachmentPreview, { className: role === "user" ? "!size-5 shrink-0 rounded-[var(--radius-sm)]" : "size-10 shrink-0 rounded-[var(--radius-md)]" }),
|
|
6811
|
+
/* @__PURE__ */ jsx22(AttachmentInfo, { className: role === "user" ? "min-w-0 max-w-[220px] flex-1 text-[12px]" : "min-w-0 flex-1" })
|
|
6812
|
+
]
|
|
6813
|
+
},
|
|
6814
|
+
`file-${message.id}-${index}`
|
|
6815
|
+
);
|
|
6816
|
+
})
|
|
6781
6817
|
}
|
|
6782
6818
|
) : null,
|
|
6783
6819
|
finalParts.map((item) => {
|
|
@@ -6951,7 +6987,27 @@ function stripAttachmentSummaryBlocks(text) {
|
|
|
6951
6987
|
return lines.slice(0, firstSummaryIndex).join("\n").trim();
|
|
6952
6988
|
}
|
|
6953
6989
|
function isAttachmentSummaryLine(line) {
|
|
6954
|
-
|
|
6990
|
+
const trimmed = line.trim();
|
|
6991
|
+
return trimmed.startsWith("[attached: ") || trimmed.startsWith("<attachment ") && trimmed.includes('data-boring-agent="composer-file"');
|
|
6992
|
+
}
|
|
6993
|
+
function attachmentPathsFromTextParts(parts) {
|
|
6994
|
+
return parts.flatMap((part) => part.text.split("\n").flatMap((line) => {
|
|
6995
|
+
const trimmed = line.trim();
|
|
6996
|
+
const taggedPath = attachmentTagAttr(trimmed, "path");
|
|
6997
|
+
if (taggedPath) return [taggedPath];
|
|
6998
|
+
const oldInline = trimmed.match(/^\[attached: .+ Saved in workspace at: (.+?) Use the workspace file\/read tools/);
|
|
6999
|
+
if (oldInline?.[1]) return [oldInline[1]];
|
|
7000
|
+
const oldMultiline = trimmed.match(/^Saved in workspace at: (.+)$/);
|
|
7001
|
+
return oldMultiline?.[1] ? [oldMultiline[1]] : [];
|
|
7002
|
+
}));
|
|
7003
|
+
}
|
|
7004
|
+
function attachmentTagAttr(line, name) {
|
|
7005
|
+
if (!line.startsWith("<attachment ") || !line.includes('data-boring-agent="composer-file"')) return void 0;
|
|
7006
|
+
const match = line.match(new RegExp(`\\b${name}="([^"]*)"`));
|
|
7007
|
+
return match?.[1] ? unescapeAttachmentAttr(match[1]) : void 0;
|
|
7008
|
+
}
|
|
7009
|
+
function unescapeAttachmentAttr(value) {
|
|
7010
|
+
return value.replace(/"/g, '"').replace(/>/g, ">").replace(/</g, "<").replace(/&/g, "&");
|
|
6955
7011
|
}
|
|
6956
7012
|
function MessageActionsBar({
|
|
6957
7013
|
text,
|
|
@@ -8802,11 +8858,13 @@ function PiChatComposerSurface({
|
|
|
8802
8858
|
onSubmitMessage,
|
|
8803
8859
|
onStop
|
|
8804
8860
|
}) {
|
|
8861
|
+
const workspaceRequestId = getHeaderValue(requestHeaders, "x-boring-workspace-id");
|
|
8805
8862
|
const uploadAttachment = useCallback16((file) => uploadFile(file, {
|
|
8806
8863
|
apiBaseUrl,
|
|
8807
|
-
workspaceRequestId
|
|
8864
|
+
workspaceRequestId,
|
|
8865
|
+
responseUrl: "raw",
|
|
8808
8866
|
fetch: fetch2
|
|
8809
|
-
}), [apiBaseUrl, fetch2,
|
|
8867
|
+
}), [apiBaseUrl, fetch2, workspaceRequestId]);
|
|
8810
8868
|
const resizeTextarea = useCallback16((node) => {
|
|
8811
8869
|
if (!node) return;
|
|
8812
8870
|
node.style.height = "auto";
|
|
@@ -9025,7 +9083,7 @@ function PiChatComposerSurface({
|
|
|
9025
9083
|
className: cn(
|
|
9026
9084
|
"h-8 w-8 shrink-0 rounded-full",
|
|
9027
9085
|
"bg-foreground",
|
|
9028
|
-
"
|
|
9086
|
+
"text-background",
|
|
9029
9087
|
"transition-all duration-150 ease-[cubic-bezier(0.22,1,0.36,1)]",
|
|
9030
9088
|
"hover:bg-foreground/90 hover:shadow-[0_0_0_3px_oklch(from_var(--foreground)_l_c_h/0.12)] hover:scale-[1.04]",
|
|
9031
9089
|
"active:scale-[0.93] active:brightness-95",
|
|
@@ -9092,6 +9150,10 @@ function PiChatComposerSurface({
|
|
|
9092
9150
|
)
|
|
9093
9151
|
] });
|
|
9094
9152
|
}
|
|
9153
|
+
function getHeaderValue(headers, name) {
|
|
9154
|
+
if (!headers) return void 0;
|
|
9155
|
+
return headers[name] ?? Object.entries(headers).find(([key]) => key.toLowerCase() === name.toLowerCase())?.[1];
|
|
9156
|
+
}
|
|
9095
9157
|
|
|
9096
9158
|
// src/front/chat/piChatPanelHooks.ts
|
|
9097
9159
|
import { useCallback as useCallback17, useEffect as useEffect20, useState as useState19, useSyncExternalStore } from "react";
|
|
@@ -9828,10 +9890,11 @@ function PiChatPanel({
|
|
|
9828
9890
|
}, [addLocalNotice, policy]);
|
|
9829
9891
|
const stop = useCallback18(() => {
|
|
9830
9892
|
onComposerStop?.();
|
|
9893
|
+
clearLocalSubmitted(activeChatSessionId);
|
|
9831
9894
|
void policy?.stop().catch((error) => {
|
|
9832
9895
|
addLocalNotice({ id: "stop-error", level: "error", text: errorMessage2(error, "Could not stop the chat session."), dismissible: true });
|
|
9833
9896
|
});
|
|
9834
|
-
}, [addLocalNotice, onComposerStop, policy]);
|
|
9897
|
+
}, [activeChatSessionId, addLocalNotice, clearLocalSubmitted, onComposerStop, policy]);
|
|
9835
9898
|
const interrupt = useCallback18(() => {
|
|
9836
9899
|
void policy?.interrupt().catch((error) => {
|
|
9837
9900
|
addLocalNotice({ id: "interrupt-error", level: "error", text: errorMessage2(error, "Could not interrupt the chat session."), dismissible: true });
|
package/dist/front/styles.css
CHANGED
|
@@ -1494,9 +1494,6 @@
|
|
|
1494
1494
|
.whitespace-pre-wrap {
|
|
1495
1495
|
white-space: pre-wrap;
|
|
1496
1496
|
}
|
|
1497
|
-
.\!text-background {
|
|
1498
|
-
color: var(--background) !important;
|
|
1499
|
-
}
|
|
1500
1497
|
.\!text-muted-foreground\/75 {
|
|
1501
1498
|
color: var(--muted-foreground) !important;
|
|
1502
1499
|
@supports (color: color-mix(in lab, red, red)) {
|
|
@@ -1578,6 +1575,9 @@
|
|
|
1578
1575
|
.text-amber-600 {
|
|
1579
1576
|
color: var(--color-amber-600);
|
|
1580
1577
|
}
|
|
1578
|
+
.text-background {
|
|
1579
|
+
color: var(--background);
|
|
1580
|
+
}
|
|
1581
1581
|
.text-destructive {
|
|
1582
1582
|
color: var(--destructive);
|
|
1583
1583
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
StopPayloadSchema,
|
|
14
14
|
extractToolUiMetadata,
|
|
15
15
|
sanitizeToolUiMetadata2 as sanitizeToolUiMetadata
|
|
16
|
-
} from "../chunk-
|
|
16
|
+
} from "../chunk-G6YPXDHR.js";
|
|
17
17
|
|
|
18
18
|
// src/server/sandbox/direct/createDirectSandbox.ts
|
|
19
19
|
import { spawn } from "child_process";
|
|
@@ -2621,7 +2621,31 @@ function buildFileRecordsResult(args) {
|
|
|
2621
2621
|
var log2 = createLogger("boring/workspace-settings");
|
|
2622
2622
|
var BORING_SETTINGS_PATH = ".boring/settings";
|
|
2623
2623
|
var DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR = "assets/images";
|
|
2624
|
+
var DEFAULT_FILE_UPLOAD_DIR = "assets/uploads";
|
|
2624
2625
|
var MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
|
|
2626
|
+
var IMAGE_UPLOAD_EXTENSIONS = /* @__PURE__ */ new Set(["avif", "gif", "jpg", "jpeg", "png", "webp"]);
|
|
2627
|
+
var SAFE_UNKNOWN_FILE_EXTENSIONS = /* @__PURE__ */ new Set(["csv", "doc", "docx", "json", "md", "pdf", "ppt", "pptx", "rtf", "txt", "xls", "xlsx", "zip"]);
|
|
2628
|
+
var MIME_UPLOAD_EXTENSIONS = {
|
|
2629
|
+
"application/json": ["json"],
|
|
2630
|
+
"application/msword": ["doc"],
|
|
2631
|
+
"application/pdf": ["pdf"],
|
|
2632
|
+
"application/rtf": ["rtf"],
|
|
2633
|
+
"application/vnd.ms-excel": ["xls"],
|
|
2634
|
+
"application/vnd.ms-powerpoint": ["ppt"],
|
|
2635
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation": ["pptx"],
|
|
2636
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ["xlsx"],
|
|
2637
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ["docx"],
|
|
2638
|
+
"application/zip": ["zip"],
|
|
2639
|
+
"image/avif": ["avif"],
|
|
2640
|
+
"image/gif": ["gif"],
|
|
2641
|
+
"image/jpeg": ["jpg", "jpeg"],
|
|
2642
|
+
"image/png": ["png"],
|
|
2643
|
+
"image/svg+xml": ["bin"],
|
|
2644
|
+
"image/webp": ["webp"],
|
|
2645
|
+
"text/csv": ["csv"],
|
|
2646
|
+
"text/markdown": ["md"],
|
|
2647
|
+
"text/plain": ["txt"]
|
|
2648
|
+
};
|
|
2625
2649
|
function defaultWorkspaceSettings() {
|
|
2626
2650
|
return { markdown: { imageUploadDir: DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR } };
|
|
2627
2651
|
}
|
|
@@ -2716,12 +2740,11 @@ function normalizeUploadDir(value) {
|
|
|
2716
2740
|
}
|
|
2717
2741
|
function extForUpload(filename, contentType) {
|
|
2718
2742
|
const fromName = extname2(filename).toLowerCase().replace(/^\./, "");
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
if (
|
|
2722
|
-
if (contentType
|
|
2723
|
-
if (contentType === "
|
|
2724
|
-
if (contentType === "image/svg+xml") return "svg";
|
|
2743
|
+
const safeFromName = /^[a-z0-9]{1,12}$/.test(fromName) ? fromName : "";
|
|
2744
|
+
const allowed = MIME_UPLOAD_EXTENSIONS[contentType];
|
|
2745
|
+
if (allowed) return safeFromName && allowed.includes(safeFromName) ? safeFromName : allowed[0] ?? "bin";
|
|
2746
|
+
if (contentType.startsWith("image/")) return safeFromName && IMAGE_UPLOAD_EXTENSIONS.has(safeFromName) ? safeFromName : "bin";
|
|
2747
|
+
if (contentType === "application/octet-stream" && safeFromName && SAFE_UNKNOWN_FILE_EXTENSIONS.has(safeFromName)) return safeFromName;
|
|
2725
2748
|
return "bin";
|
|
2726
2749
|
}
|
|
2727
2750
|
function basenameForUpload(filename) {
|
|
@@ -2751,17 +2774,32 @@ function contentTypeForPath(path4) {
|
|
|
2751
2774
|
return "image/svg+xml";
|
|
2752
2775
|
case ".webp":
|
|
2753
2776
|
return "image/webp";
|
|
2754
|
-
case ".
|
|
2755
|
-
return "text/
|
|
2756
|
-
case ".
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
return "text/javascript; charset=utf-8";
|
|
2777
|
+
case ".txt":
|
|
2778
|
+
return "text/plain; charset=utf-8";
|
|
2779
|
+
case ".md":
|
|
2780
|
+
return "text/markdown; charset=utf-8";
|
|
2781
|
+
case ".json":
|
|
2782
|
+
return "application/json; charset=utf-8";
|
|
2783
|
+
case ".csv":
|
|
2784
|
+
return "text/csv; charset=utf-8";
|
|
2763
2785
|
case ".pdf":
|
|
2764
2786
|
return "application/pdf";
|
|
2787
|
+
case ".doc":
|
|
2788
|
+
return "application/msword";
|
|
2789
|
+
case ".docx":
|
|
2790
|
+
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
2791
|
+
case ".xls":
|
|
2792
|
+
return "application/vnd.ms-excel";
|
|
2793
|
+
case ".xlsx":
|
|
2794
|
+
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
2795
|
+
case ".ppt":
|
|
2796
|
+
return "application/vnd.ms-powerpoint";
|
|
2797
|
+
case ".pptx":
|
|
2798
|
+
return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
2799
|
+
case ".rtf":
|
|
2800
|
+
return "application/rtf";
|
|
2801
|
+
case ".zip":
|
|
2802
|
+
return "application/zip";
|
|
2765
2803
|
default:
|
|
2766
2804
|
return "application/octet-stream";
|
|
2767
2805
|
}
|
|
@@ -2795,7 +2833,7 @@ function fileRoutes(app, opts, done) {
|
|
|
2795
2833
|
});
|
|
2796
2834
|
}
|
|
2797
2835
|
const bytes = await workspace.readBinaryFile(path4);
|
|
2798
|
-
return reply.header("content-type", contentTypeForPath(path4)).header("content-length", String(bytes.byteLength)).header("cache-control", "no-store").send(Buffer.from(bytes));
|
|
2836
|
+
return reply.header("content-type", contentTypeForPath(path4)).header("content-length", String(bytes.byteLength)).header("cache-control", "no-store").header("x-content-type-options", "nosniff").send(Buffer.from(bytes));
|
|
2799
2837
|
} catch (err) {
|
|
2800
2838
|
return classifyError(err, reply, "file");
|
|
2801
2839
|
}
|
|
@@ -2857,6 +2895,7 @@ function fileRoutes(app, opts, done) {
|
|
|
2857
2895
|
});
|
|
2858
2896
|
}
|
|
2859
2897
|
const expectedMtimeMs = typeof body.expectedMtimeMs === "number" ? body.expectedMtimeMs : null;
|
|
2898
|
+
const shouldReturnMtimeMs = body.returnMtimeMs !== false || expectedMtimeMs !== null;
|
|
2860
2899
|
try {
|
|
2861
2900
|
const workspace = await resolveWorkspace(request);
|
|
2862
2901
|
if (expectedMtimeMs !== null) {
|
|
@@ -2891,6 +2930,10 @@ function fileRoutes(app, opts, done) {
|
|
|
2891
2930
|
if (dir) await workspace.mkdir(dir, { recursive: true });
|
|
2892
2931
|
}
|
|
2893
2932
|
const content = body.content;
|
|
2933
|
+
if (!shouldReturnMtimeMs) {
|
|
2934
|
+
await workspace.writeFile(path4, content);
|
|
2935
|
+
return { ok: true };
|
|
2936
|
+
}
|
|
2894
2937
|
const stat11 = workspace.writeFileWithStat ? await workspace.writeFileWithStat(path4, content) : await (async () => {
|
|
2895
2938
|
await workspace.writeFile(path4, content);
|
|
2896
2939
|
return await workspace.stat(path4);
|
|
@@ -2906,16 +2949,12 @@ function fileRoutes(app, opts, done) {
|
|
|
2906
2949
|
if (filename === null) return;
|
|
2907
2950
|
const contentBase64 = requireStringParam(body?.contentBase64, "contentBase64", reply);
|
|
2908
2951
|
if (contentBase64 === null) return;
|
|
2909
|
-
const contentType = typeof body.contentType === "string" ? body.contentType.trim().toLowerCase() : "";
|
|
2910
|
-
if (!contentType.startsWith("image/")) {
|
|
2911
|
-
return reply.code(400).send({
|
|
2912
|
-
error: { code: ERROR_CODE_VALIDATION_ERROR, message: "contentType must be an image/* MIME type", field: "contentType" }
|
|
2913
|
-
});
|
|
2914
|
-
}
|
|
2952
|
+
const contentType = typeof body.contentType === "string" && body.contentType.trim() ? body.contentType.trim().toLowerCase() : "application/octet-stream";
|
|
2915
2953
|
try {
|
|
2916
2954
|
const workspace = await resolveWorkspace(request);
|
|
2917
2955
|
const settings = await readWorkspaceSettings(workspace);
|
|
2918
|
-
const
|
|
2956
|
+
const isImage = contentType.startsWith("image/");
|
|
2957
|
+
const dir = isImage ? normalizeUploadDir(body.directory) ?? normalizeUploadDir(settings.markdown?.imageUploadDir) ?? DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR : DEFAULT_FILE_UPLOAD_DIR;
|
|
2919
2958
|
const ext = extForUpload(filename, contentType);
|
|
2920
2959
|
const base = basenameForUpload(filename);
|
|
2921
2960
|
const unique = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -3491,6 +3530,20 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
|
|
|
3491
3530
|
`node -e ${shellQuote3(`const fs=require('fs'); const path=require('path'); const root=process.argv[1]; const relRoot=process.argv[2]; function walk(abs,rel){ const s=fs.statSync(abs); if(!s.isDirectory()) return []; const out=[]; for (const entry of fs.readdirSync(abs,{withFileTypes:true})) { const childRel=rel==='.'?entry.name:rel+'/'+entry.name; out.push(childRel); if(entry.isDirectory()) out.push(...walk(path.join(abs,entry.name),childRel)); } return out; } process.stdout.write(JSON.stringify(walk(root,relRoot)))`)} ${shellQuote3(sandboxPath)} ${shellQuote3(relPath)}`
|
|
3492
3531
|
);
|
|
3493
3532
|
}
|
|
3533
|
+
async function statSandboxPath(sandboxPath) {
|
|
3534
|
+
if (remote.fs?.stat) {
|
|
3535
|
+
const fileStat = await remote.fs.stat(sandboxPath);
|
|
3536
|
+
return {
|
|
3537
|
+
size: fileStat.size,
|
|
3538
|
+
mtimeMs: fileStat.mtimeMs,
|
|
3539
|
+
kind: fileStat.isDirectory() ? "dir" : "file"
|
|
3540
|
+
};
|
|
3541
|
+
}
|
|
3542
|
+
return await runJson(
|
|
3543
|
+
remote,
|
|
3544
|
+
`node -e ${shellQuote3(`const fs=require('fs'); const p=process.argv[1]; const s=fs.statSync(p); process.stdout.write(JSON.stringify({size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}))`)} ${shellQuote3(sandboxPath)}`
|
|
3545
|
+
);
|
|
3546
|
+
}
|
|
3494
3547
|
return {
|
|
3495
3548
|
root: VERCEL_SANDBOX_RUNTIME_CONTEXT.runtimeCwd,
|
|
3496
3549
|
runtimeContext: VERCEL_SANDBOX_RUNTIME_CONTEXT,
|
|
@@ -3591,7 +3644,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
|
|
|
3591
3644
|
async writeFileWithStat(relPath, data) {
|
|
3592
3645
|
const sandboxPath = toSandboxPath(relPath);
|
|
3593
3646
|
const payload = Buffer.from(data, "utf-8");
|
|
3594
|
-
if (payload.byteLength > MAX_INLINE_WRITE_BYTES) {
|
|
3647
|
+
if (remote.fs?.stat || payload.byteLength > MAX_INLINE_WRITE_BYTES) {
|
|
3595
3648
|
await sandbox.writeFiles([
|
|
3596
3649
|
{
|
|
3597
3650
|
path: sandboxPath,
|
|
@@ -3600,17 +3653,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
|
|
|
3600
3653
|
]);
|
|
3601
3654
|
invalidateMetadataCache();
|
|
3602
3655
|
workspaceOpts.onMutation?.();
|
|
3603
|
-
const writtenStat2 =
|
|
3604
|
-
const fileStat = await remote.fs.stat(sandboxPath);
|
|
3605
|
-
return {
|
|
3606
|
-
size: fileStat.size,
|
|
3607
|
-
mtimeMs: fileStat.mtimeMs,
|
|
3608
|
-
kind: fileStat.isDirectory() ? "dir" : "file"
|
|
3609
|
-
};
|
|
3610
|
-
})() : await runJson(
|
|
3611
|
-
remote,
|
|
3612
|
-
`node -e ${shellQuote3(`const fs=require('fs'); const p=process.argv[1]; const s=fs.statSync(p); process.stdout.write(JSON.stringify({size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}))`)} ${shellQuote3(sandboxPath)}`
|
|
3613
|
-
);
|
|
3656
|
+
const writtenStat2 = await statSandboxPath(sandboxPath);
|
|
3614
3657
|
statCache.set(sandboxPath, writtenStat2);
|
|
3615
3658
|
emitChange({ op: "write", path: relPath, mtimeMs: writtenStat2.mtimeMs });
|
|
3616
3659
|
return cloneStat(writtenStat2);
|
|
@@ -3629,7 +3672,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
|
|
|
3629
3672
|
async writeBinaryFileWithStat(relPath, data) {
|
|
3630
3673
|
const sandboxPath = toSandboxPath(relPath);
|
|
3631
3674
|
const payload = Buffer.from(data);
|
|
3632
|
-
if (payload.byteLength > MAX_INLINE_WRITE_BYTES) {
|
|
3675
|
+
if (remote.fs?.stat || payload.byteLength > MAX_INLINE_WRITE_BYTES) {
|
|
3633
3676
|
await sandbox.writeFiles([
|
|
3634
3677
|
{
|
|
3635
3678
|
path: sandboxPath,
|
|
@@ -3638,17 +3681,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
|
|
|
3638
3681
|
]);
|
|
3639
3682
|
invalidateMetadataCache();
|
|
3640
3683
|
workspaceOpts.onMutation?.();
|
|
3641
|
-
const writtenStat2 =
|
|
3642
|
-
const fileStat = await remote.fs.stat(sandboxPath);
|
|
3643
|
-
return {
|
|
3644
|
-
size: fileStat.size,
|
|
3645
|
-
mtimeMs: fileStat.mtimeMs,
|
|
3646
|
-
kind: fileStat.isDirectory() ? "dir" : "file"
|
|
3647
|
-
};
|
|
3648
|
-
})() : await runJson(
|
|
3649
|
-
remote,
|
|
3650
|
-
`node -e ${shellQuote3(`const fs=require('fs'); const p=process.argv[1]; const s=fs.statSync(p); process.stdout.write(JSON.stringify({size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}))`)} ${shellQuote3(sandboxPath)}`
|
|
3651
|
-
);
|
|
3684
|
+
const writtenStat2 = await statSandboxPath(sandboxPath);
|
|
3652
3685
|
statCache.set(sandboxPath, writtenStat2);
|
|
3653
3686
|
emitChange({ op: "write", path: relPath, mtimeMs: writtenStat2.mtimeMs });
|
|
3654
3687
|
return cloneStat(writtenStat2);
|
|
@@ -5007,7 +5040,7 @@ function createVercelSandboxExec(sandbox, execOpts = {}) {
|
|
|
5007
5040
|
};
|
|
5008
5041
|
} catch (error) {
|
|
5009
5042
|
if (timedOut) {
|
|
5010
|
-
return timeoutResult(Date.now() - start);
|
|
5043
|
+
return timeoutResult(Math.max(Date.now() - start, timeoutMs));
|
|
5011
5044
|
}
|
|
5012
5045
|
throw error;
|
|
5013
5046
|
} finally {
|
|
@@ -9833,7 +9866,8 @@ function filePartsFromContent(content, messageId) {
|
|
|
9833
9866
|
id: `${messageId}:file:${index}`,
|
|
9834
9867
|
...optionalString(part.filename) ? { filename: optionalString(part.filename) } : {},
|
|
9835
9868
|
...mediaType ? { mediaType } : {},
|
|
9836
|
-
...imagePartUrl(part, mediaType) ? { url: imagePartUrl(part, mediaType) } : {}
|
|
9869
|
+
...imagePartUrl(part, mediaType) ? { url: imagePartUrl(part, mediaType) } : {},
|
|
9870
|
+
...optionalString(part.path) ? { path: optionalString(part.path) } : {}
|
|
9837
9871
|
}
|
|
9838
9872
|
];
|
|
9839
9873
|
});
|
|
@@ -10397,7 +10431,8 @@ function filePartsFromContent2(content, messageId) {
|
|
|
10397
10431
|
id: `${messageId}:file:${index}`,
|
|
10398
10432
|
...optionalString2(part.filename) ? { filename: optionalString2(part.filename) } : {},
|
|
10399
10433
|
...mediaType ? { mediaType } : {},
|
|
10400
|
-
...url ? { url } : {}
|
|
10434
|
+
...url ? { url } : {},
|
|
10435
|
+
...optionalString2(part.path) ? { path: optionalString2(part.path) } : {}
|
|
10401
10436
|
}];
|
|
10402
10437
|
});
|
|
10403
10438
|
}
|
|
@@ -10491,11 +10526,13 @@ var PiChatMessageMetadataReconciler = class {
|
|
|
10491
10526
|
recordPrompt(sessionId, payload) {
|
|
10492
10527
|
const metadata = this.promptMetadata.get(sessionId) ?? [];
|
|
10493
10528
|
const displayText = payload.displayMessage ?? payload.message;
|
|
10529
|
+
const files = filePartsFromPromptPayload(payload);
|
|
10494
10530
|
metadata.push({
|
|
10495
10531
|
displayText,
|
|
10496
10532
|
...displayText !== payload.message ? { serverText: payload.message } : {},
|
|
10497
10533
|
clientNonce: payload.clientNonce,
|
|
10498
|
-
recordedAt: Date.now()
|
|
10534
|
+
recordedAt: Date.now(),
|
|
10535
|
+
...files ? { files } : {}
|
|
10499
10536
|
});
|
|
10500
10537
|
this.promptMetadata.set(sessionId, metadata);
|
|
10501
10538
|
}
|
|
@@ -10562,9 +10599,16 @@ var PiChatMessageMetadataReconciler = class {
|
|
|
10562
10599
|
}
|
|
10563
10600
|
if (event.type === "message-start" && event.role === "user" && !hasFollowUpSelector2(event)) {
|
|
10564
10601
|
const promptMetadata = this.findPrompt(sessionId, event.text);
|
|
10565
|
-
if (promptMetadata) return
|
|
10602
|
+
if (promptMetadata) return withMessageStartMetadata(event, promptMetadata);
|
|
10566
10603
|
const metadata = this.findFollowUp(sessionId, event.text);
|
|
10567
|
-
if (metadata) return
|
|
10604
|
+
if (metadata) return withMessageStartMetadata(event, metadata);
|
|
10605
|
+
}
|
|
10606
|
+
if (event.type === "message-end" && event.final.role === "user" && !hasMessageSelector(event.final)) {
|
|
10607
|
+
const text = messageText2(event.final);
|
|
10608
|
+
const promptMetadata = this.findPrompt(sessionId, text);
|
|
10609
|
+
if (promptMetadata) return { ...event, final: withMessageSelector(event.final, promptMetadata) };
|
|
10610
|
+
const metadata = this.findFollowUp(sessionId, text);
|
|
10611
|
+
if (metadata) return { ...event, final: withMessageSelector(event.final, metadata) };
|
|
10568
10612
|
}
|
|
10569
10613
|
return event;
|
|
10570
10614
|
}
|
|
@@ -10643,13 +10687,24 @@ var PiChatMessageMetadataReconciler = class {
|
|
|
10643
10687
|
return this.promptMetadata.get(sessionId)?.find((entry) => matchesMetadataText(entry, text));
|
|
10644
10688
|
}
|
|
10645
10689
|
consumePromptFromEvent(sessionId, event) {
|
|
10646
|
-
if (event.type
|
|
10647
|
-
|
|
10648
|
-
|
|
10649
|
-
|
|
10690
|
+
if (event.type === "message-start" && event.role === "user") {
|
|
10691
|
+
if (event.clientSeq !== void 0) return;
|
|
10692
|
+
const prompt = event.clientNonce ? this.promptMetadata.get(sessionId)?.find((entry) => entry.clientNonce === event.clientNonce) : this.findPrompt(sessionId, event.text);
|
|
10693
|
+
if (prompt?.serverText) return;
|
|
10694
|
+
if (event.clientNonce) {
|
|
10695
|
+
this.removePrompt(sessionId, { clientNonce: event.clientNonce });
|
|
10696
|
+
return;
|
|
10697
|
+
}
|
|
10698
|
+
this.removePrompt(sessionId, { displayText: event.text });
|
|
10650
10699
|
return;
|
|
10651
10700
|
}
|
|
10652
|
-
|
|
10701
|
+
if (event.type !== "message-end" || event.final.role !== "user") return;
|
|
10702
|
+
if (event.final.clientSeq !== void 0) return;
|
|
10703
|
+
if (event.final.clientNonce) {
|
|
10704
|
+
this.removePrompt(sessionId, { clientNonce: event.final.clientNonce });
|
|
10705
|
+
return;
|
|
10706
|
+
}
|
|
10707
|
+
this.removePrompt(sessionId, { displayText: messageText2(event.final) });
|
|
10653
10708
|
}
|
|
10654
10709
|
removeFirstFollowUpByText(sessionId, text) {
|
|
10655
10710
|
if (!text) return;
|
|
@@ -10703,9 +10758,18 @@ function matchesMetadataText(entry, text) {
|
|
|
10703
10758
|
if (!entry || !text) return false;
|
|
10704
10759
|
return entry.displayText === text || entry.serverText === text;
|
|
10705
10760
|
}
|
|
10761
|
+
function withMessageStartMetadata(event, metadata) {
|
|
10762
|
+
return {
|
|
10763
|
+
...event,
|
|
10764
|
+
text: metadata.displayText,
|
|
10765
|
+
...metadata.clientNonce ? { clientNonce: metadata.clientNonce } : {},
|
|
10766
|
+
...metadata.clientSeq !== void 0 ? { clientSeq: metadata.clientSeq } : {},
|
|
10767
|
+
...metadata.files && metadata.files.length > 0 ? { files: metadata.files } : {}
|
|
10768
|
+
};
|
|
10769
|
+
}
|
|
10706
10770
|
function withMessageSelector(message, metadata) {
|
|
10707
10771
|
return {
|
|
10708
|
-
...withMessageDisplayText(message, metadata.displayText),
|
|
10772
|
+
...withMessageFiles(withMessageDisplayText(message, metadata.displayText), metadata.files),
|
|
10709
10773
|
...metadata.clientNonce ? { clientNonce: metadata.clientNonce } : {},
|
|
10710
10774
|
...metadata.clientSeq !== void 0 ? { clientSeq: metadata.clientSeq } : {}
|
|
10711
10775
|
};
|
|
@@ -10720,6 +10784,22 @@ function withMessageDisplayText(message, displayText) {
|
|
|
10720
10784
|
});
|
|
10721
10785
|
return { ...message, parts };
|
|
10722
10786
|
}
|
|
10787
|
+
function withMessageFiles(message, files) {
|
|
10788
|
+
if (!files || files.length === 0) return message;
|
|
10789
|
+
const nonFileParts = message.parts.filter((part) => part.type !== "file");
|
|
10790
|
+
return { ...message, parts: [...nonFileParts, ...files] };
|
|
10791
|
+
}
|
|
10792
|
+
function filePartsFromPromptPayload(payload) {
|
|
10793
|
+
if (!payload.attachments || payload.attachments.length === 0) return void 0;
|
|
10794
|
+
return payload.attachments.map((attachment, index) => ({
|
|
10795
|
+
type: "file",
|
|
10796
|
+
id: `prompt:${payload.clientNonce}:file:${index}`,
|
|
10797
|
+
filename: attachment.filename,
|
|
10798
|
+
mediaType: attachment.mediaType,
|
|
10799
|
+
url: attachment.url,
|
|
10800
|
+
path: attachment.path
|
|
10801
|
+
}));
|
|
10802
|
+
}
|
|
10723
10803
|
function withQueuedSelector(followUp, metadata) {
|
|
10724
10804
|
return {
|
|
10725
10805
|
...followUp,
|
|
@@ -11217,10 +11297,13 @@ var PiChatMeteringCoordinator = class {
|
|
|
11217
11297
|
};
|
|
11218
11298
|
|
|
11219
11299
|
// src/server/pi-chat/harnessPiChatService.ts
|
|
11300
|
+
var MAX_PROMPT_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
11301
|
+
var PROMPT_IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".avif", ".gif", ".jpg", ".jpeg", ".png", ".webp"]);
|
|
11220
11302
|
var HarnessPiChatService = class {
|
|
11221
11303
|
harness;
|
|
11222
11304
|
sessionStore;
|
|
11223
11305
|
workdir;
|
|
11306
|
+
workspace;
|
|
11224
11307
|
channels = /* @__PURE__ */ new Map();
|
|
11225
11308
|
// Single-flight guard so concurrent cold callers (e.g. two browser tabs each
|
|
11226
11309
|
// opening /events while the session is still being created) converge on one
|
|
@@ -11236,6 +11319,7 @@ var HarnessPiChatService = class {
|
|
|
11236
11319
|
this.harness = options.harness;
|
|
11237
11320
|
this.sessionStore = options.sessionStore;
|
|
11238
11321
|
this.workdir = options.workdir;
|
|
11322
|
+
this.workspace = options.workspace;
|
|
11239
11323
|
this.metering = options.metering ? new PiChatMeteringCoordinator(options.metering, options.meteringLogger) : void 0;
|
|
11240
11324
|
}
|
|
11241
11325
|
/** Test/diagnostic hook: resolves once queued metering sink calls settle. */
|
|
@@ -11329,7 +11413,8 @@ var HarnessPiChatService = class {
|
|
|
11329
11413
|
const channel = this.channels.get(sessionId);
|
|
11330
11414
|
const receiptCursor = nextPromptReceiptCursor(channel);
|
|
11331
11415
|
try {
|
|
11332
|
-
const
|
|
11416
|
+
const input = await toPiPromptInput(payload, this.workspace);
|
|
11417
|
+
const run2 = this.trackActiveRun(sessionId, adapter.prompt(input));
|
|
11333
11418
|
run2.catch((error) => {
|
|
11334
11419
|
this.metering?.failPromptRun(sessionId, payload.clientNonce);
|
|
11335
11420
|
if (!this.messageMetadata.hasPrompt(sessionId, { clientNonce: payload.clientNonce, displayText: payload.displayMessage ?? payload.message })) return;
|
|
@@ -11629,7 +11714,8 @@ function promptPayloadFileParts(payload, messageId) {
|
|
|
11629
11714
|
id: `${messageId}:file:${index}`,
|
|
11630
11715
|
filename: attachment.filename,
|
|
11631
11716
|
mediaType: attachment.mediaType,
|
|
11632
|
-
url: attachment.url
|
|
11717
|
+
url: attachment.url,
|
|
11718
|
+
path: attachment.path
|
|
11633
11719
|
}));
|
|
11634
11720
|
}
|
|
11635
11721
|
function promptPayloadMessage(payload, messageId, createdAt, turnId) {
|
|
@@ -11667,21 +11753,64 @@ function messageTime(message) {
|
|
|
11667
11753
|
const timestamp = Date.parse(message.createdAt);
|
|
11668
11754
|
return Number.isFinite(timestamp) ? timestamp : void 0;
|
|
11669
11755
|
}
|
|
11670
|
-
function toPiPromptInput(payload) {
|
|
11671
|
-
const images = promptImagesFromAttachments(payload.attachments);
|
|
11756
|
+
async function toPiPromptInput(payload, workspace) {
|
|
11757
|
+
const images = await promptImagesFromAttachments(payload.attachments, workspace);
|
|
11672
11758
|
if (images.length === 0) return payload.message;
|
|
11673
11759
|
return { text: payload.message, options: { images } };
|
|
11674
11760
|
}
|
|
11675
|
-
function promptImagesFromAttachments(attachments) {
|
|
11761
|
+
async function promptImagesFromAttachments(attachments, workspace) {
|
|
11676
11762
|
const images = [];
|
|
11677
11763
|
for (const attachment of attachments ?? []) {
|
|
11678
11764
|
const match = attachment.url.match(/^data:(image\/[^;]+);base64,(.+)$/);
|
|
11679
|
-
if (
|
|
11680
|
-
|
|
11681
|
-
|
|
11765
|
+
if (match) {
|
|
11766
|
+
const [, mimeType, data] = match;
|
|
11767
|
+
images.push({ type: "image", mimeType, data });
|
|
11768
|
+
continue;
|
|
11769
|
+
}
|
|
11770
|
+
if (!workspace?.readBinaryFile || !isWorkspaceImageAttachment(attachment)) continue;
|
|
11771
|
+
try {
|
|
11772
|
+
const stat11 = await workspace.stat(attachment.path);
|
|
11773
|
+
if (stat11.kind !== "file" || stat11.size <= 0 || stat11.size > MAX_PROMPT_IMAGE_BYTES) continue;
|
|
11774
|
+
const bytes = await workspace.readBinaryFile(attachment.path);
|
|
11775
|
+
if (bytes.byteLength <= 0 || bytes.byteLength > MAX_PROMPT_IMAGE_BYTES) continue;
|
|
11776
|
+
const detectedMimeType = detectPromptImageMimeType(bytes);
|
|
11777
|
+
if (!detectedMimeType) continue;
|
|
11778
|
+
images.push({
|
|
11779
|
+
type: "image",
|
|
11780
|
+
mimeType: detectedMimeType,
|
|
11781
|
+
data: Buffer.from(bytes).toString("base64")
|
|
11782
|
+
});
|
|
11783
|
+
} catch {
|
|
11784
|
+
}
|
|
11682
11785
|
}
|
|
11683
11786
|
return images;
|
|
11684
11787
|
}
|
|
11788
|
+
function isWorkspaceImageAttachment(attachment) {
|
|
11789
|
+
if (!attachment.mediaType?.startsWith("image/") || !attachment.path) return false;
|
|
11790
|
+
const lowerPath = attachment.path.toLowerCase();
|
|
11791
|
+
return [...PROMPT_IMAGE_EXTENSIONS].some((ext) => lowerPath.endsWith(ext));
|
|
11792
|
+
}
|
|
11793
|
+
function detectPromptImageMimeType(bytes) {
|
|
11794
|
+
const buffer = Buffer.from(bytes);
|
|
11795
|
+
if (buffer.length >= 8 && buffer.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
|
|
11796
|
+
return "image/png";
|
|
11797
|
+
}
|
|
11798
|
+
if (buffer.length >= 3 && buffer[0] === 255 && buffer[1] === 216 && buffer[2] === 255) {
|
|
11799
|
+
return "image/jpeg";
|
|
11800
|
+
}
|
|
11801
|
+
if (buffer.length >= 6) {
|
|
11802
|
+
const gif = buffer.subarray(0, 6).toString("ascii");
|
|
11803
|
+
if (gif === "GIF87a" || gif === "GIF89a") return "image/gif";
|
|
11804
|
+
}
|
|
11805
|
+
if (buffer.length >= 12 && buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP") {
|
|
11806
|
+
return "image/webp";
|
|
11807
|
+
}
|
|
11808
|
+
if (buffer.length >= 12 && buffer.subarray(4, 8).toString("ascii") === "ftyp") {
|
|
11809
|
+
const brand = buffer.subarray(8, 12).toString("ascii");
|
|
11810
|
+
if (brand === "avif" || brand === "avis") return "image/avif";
|
|
11811
|
+
}
|
|
11812
|
+
return null;
|
|
11813
|
+
}
|
|
11685
11814
|
function removedFollowUps(before, after) {
|
|
11686
11815
|
const afterCounts = /* @__PURE__ */ new Map();
|
|
11687
11816
|
for (const text of after) afterCounts.set(text, (afterCounts.get(text) ?? 0) + 1);
|
|
@@ -11838,6 +11967,7 @@ async function createAgentApp(opts = {}) {
|
|
|
11838
11967
|
harness,
|
|
11839
11968
|
sessionStore: harness.sessions,
|
|
11840
11969
|
workdir: runtimeBundle.workspace.root,
|
|
11970
|
+
workspace: runtimeBundle.workspace,
|
|
11841
11971
|
metering: opts.metering
|
|
11842
11972
|
});
|
|
11843
11973
|
await app.register(piChatRoutes, { service: piChatService });
|
|
@@ -12075,6 +12205,17 @@ function selectRuntimeModeAdapter(mode, sandboxHandleStore) {
|
|
|
12075
12205
|
function getRequestWorkspaceId(request) {
|
|
12076
12206
|
return request.workspaceContext?.workspaceId ?? DEFAULT_WORKSPACE_ID3;
|
|
12077
12207
|
}
|
|
12208
|
+
function promoteRawFileWorkspaceQueryToHeader(request) {
|
|
12209
|
+
const pathname = request.url.split("?")[0] ?? request.url;
|
|
12210
|
+
if (pathname !== "/api/v1/files/raw") return;
|
|
12211
|
+
const hasWorkspaceHeader = Object.keys(request.headers).some((key) => key.toLowerCase() === "x-boring-workspace-id");
|
|
12212
|
+
if (hasWorkspaceHeader) return;
|
|
12213
|
+
const queryIndex = request.url.indexOf("?");
|
|
12214
|
+
if (queryIndex < 0) return;
|
|
12215
|
+
const workspaceId = new URLSearchParams(request.url.slice(queryIndex + 1)).get("workspaceId")?.trim();
|
|
12216
|
+
if (!workspaceId) return;
|
|
12217
|
+
request.headers["x-boring-workspace-id"] = workspaceId;
|
|
12218
|
+
}
|
|
12078
12219
|
function isWorkspaceAgnosticAgentRequest(request, options) {
|
|
12079
12220
|
const pathname = request.url.split("?")[0] ?? request.url;
|
|
12080
12221
|
if (pathname === "/api/v1/ready-status") return !options?.readyStatusWorkspaceScoped;
|
|
@@ -12567,6 +12708,7 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
12567
12708
|
app.addHook("onRequest", async (request, reply) => {
|
|
12568
12709
|
const user = request.user;
|
|
12569
12710
|
let workspaceId = DEFAULT_WORKSPACE_ID3;
|
|
12711
|
+
promoteRawFileWorkspaceQueryToHeader(request);
|
|
12570
12712
|
if (opts.getWorkspaceId && !isWorkspaceAgnosticAgentRequest(request, { readyStatusWorkspaceScoped: requestScopedRuntime })) {
|
|
12571
12713
|
try {
|
|
12572
12714
|
workspaceId = (await opts.getWorkspaceId(request)).trim();
|
|
@@ -12627,6 +12769,7 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
12627
12769
|
harness: binding.harness,
|
|
12628
12770
|
sessionStore: binding.harness.sessions,
|
|
12629
12771
|
workdir: binding.runtimeBundle.workspace.root,
|
|
12772
|
+
workspace: binding.runtimeBundle.workspace,
|
|
12630
12773
|
metering: opts.metering
|
|
12631
12774
|
});
|
|
12632
12775
|
return binding.piChatService;
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { W as Workspace, S as Sandbox, F as FileSearch, A as AgentTool } from '../agentPluginEvents-Dgm57gEU.js';
|
|
2
2
|
export { a as AgentHarness, C as CommandNotifyPayload, E as Entry, b as ExecOptions, c as ExecResult, I as IsolatedCodeInput, d as IsolatedCodeOutput, J as JSONSchema, R as RunContext, e as SandboxCapability, f as SandboxHandleRecord, g as SandboxHandleStore, h as SendMessageInput, i as Stat, T as TelemetryEvent, j as TelemetrySink, k as ToolExecContext, l as ToolResult, m as WORKSPACE_AGENT_PLUGINS_RELOADED_EVENT, n as WORKSPACE_COMMAND_NOTIFY_EVENT, o as WorkspaceRuntimeContext, p as noopTelemetry, s as safeCapture } from '../agentPluginEvents-Dgm57gEU.js';
|
|
3
|
-
import { B as BoringChatPart, T as ToolUiMetadata } from '../piChatEvent-
|
|
4
|
-
export { A as ApiErrorPayload, a as ApiErrorPayloadSchema, b as ApiErrorResponse, c as ApiErrorResponseSchema, d as BoringChatMessage, C as ChatError, e as CommandReceipt, E as ERROR_CODES, f as ErrorCode, g as ErrorLogFields, h as ErrorLogFieldsSchema, F as FollowUpPayload, i as FollowUpReceipt, I as InterruptPayload, j as InterruptReceipt, P as PiChatEvent, k as PiChatHeartbeatFrame, l as PiChatSnapshot, m as PiChatStatus, n as PiChatStreamFrame, o as PromptPayload, p as PromptReceipt, Q as QueueClearPayload, q as QueueClearReceipt, r as QueuedUserMessage, S as StopPayload, s as StopReceipt, t as extractToolUiMetadata, u as isToolUiMetadata } from '../piChatEvent-
|
|
3
|
+
import { B as BoringChatPart, T as ToolUiMetadata } from '../piChatEvent-B6GDifo2.js';
|
|
4
|
+
export { A as ApiErrorPayload, a as ApiErrorPayloadSchema, b as ApiErrorResponse, c as ApiErrorResponseSchema, d as BoringChatMessage, C as ChatError, e as CommandReceipt, E as ERROR_CODES, f as ErrorCode, g as ErrorLogFields, h as ErrorLogFieldsSchema, F as FollowUpPayload, i as FollowUpReceipt, I as InterruptPayload, j as InterruptReceipt, P as PiChatEvent, k as PiChatHeartbeatFrame, l as PiChatSnapshot, m as PiChatStatus, n as PiChatStreamFrame, o as PromptPayload, p as PromptReceipt, Q as QueueClearPayload, q as QueueClearReceipt, r as QueuedUserMessage, S as StopPayload, s as StopReceipt, t as extractToolUiMetadata, u as isToolUiMetadata } from '../piChatEvent-B6GDifo2.js';
|
|
5
5
|
export { C as ChatAttachmentPayload, a as ChatModelSelection, b as ChatSubmitPayload, S as SessionCtx, c as SessionDetail, d as SessionStore, e as SessionSummary, T as ThinkingLevel } from '../chatSubmitPayload-DHqQL2wD.js';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
|
package/dist/shared/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hachej/boring-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.49",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Pane-embeddable coding agent. Ships direct/local/vercel-sandbox execution modes behind one interface.",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"use-stick-to-bottom": "^1.1.3",
|
|
75
75
|
"yaml": "^2.8.3",
|
|
76
76
|
"zod": "^3.25.76",
|
|
77
|
-
"@hachej/boring-ui-kit": "0.1.
|
|
77
|
+
"@hachej/boring-ui-kit": "0.1.49"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
80
|
"@antithesishq/bombadil": "0.5.0",
|