@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.
@@ -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"),
@@ -1,10 +1,10 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as react from 'react';
3
- import { ReactNode, ComponentType, HTMLAttributes, ComponentProps, FormEvent } from 'react';
4
- import { FileUIPart, UIMessage, ChatStatus } 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-DNKo77Xw.js';
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 { Button, Collapsible, CollapsibleContent, CollapsibleTrigger, InputGroupAddon, InputGroupButton, InputGroupTextarea } from '@hachej/boring-ui-kit';
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: FileUIPart[];
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 };
@@ -13,7 +13,7 @@ import {
13
13
  StopReceiptSchema,
14
14
  extractToolUiMetadata,
15
15
  sanitizeToolUiMetadata
16
- } from "../chunk-HEEJSOFF.js";
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 url = body.markdownUrl ?? body.path;
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: body.path ?? url };
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
- ...typeof file.path === "string" ? { path: file.path } : {}
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, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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 = typeof file.path === "string" ? file.path : void 0;
2260
- const pathNote = workspacePath ? `
2261
- Saved in workspace at: ${workspacePath}
2262
- Use the workspace file/read tools with this path if you need to inspect it.` : "";
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(`[attached: ${label} (${mime})${pathNote}]
2274
+ attachmentSummaries.push(`<attachment ${attrText}>
2266
2275
  \`\`\`
2267
2276
  ${content}
2268
- \`\`\``);
2277
+ \`\`\`
2278
+ </attachment>`);
2269
2279
  } else {
2270
- attachmentSummaries.push(`[attached: ${label} (${mime}, not inlined \u2014 binary)${pathNote}]`);
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) => /* @__PURE__ */ jsxs20(
6771
- Attachment,
6772
- {
6773
- data: toAttachmentData(file, `file-${message.id}-${index}`),
6774
- children: [
6775
- /* @__PURE__ */ jsx22(AttachmentPreview, { className: role === "user" ? "!size-5 shrink-0 rounded-[var(--radius-sm)]" : "size-10 shrink-0 rounded-[var(--radius-md)]" }),
6776
- /* @__PURE__ */ jsx22(AttachmentInfo, { className: role === "user" ? "min-w-0 max-w-[220px] flex-1 text-[12px]" : "min-w-0 flex-1" })
6777
- ]
6778
- },
6779
- `file-${message.id}-${index}`
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
- return /^\[attached: .+ \(.+\)\]$/.test(line.trim());
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(/&quot;/g, '"').replace(/&gt;/g, ">").replace(/&lt;/g, "<").replace(/&amp;/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: requestHeaders?.["x-boring-workspace-id"],
8864
+ workspaceRequestId,
8865
+ responseUrl: "raw",
8808
8866
  fetch: fetch2
8809
- }), [apiBaseUrl, fetch2, requestHeaders]);
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
- "!text-background",
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 });
@@ -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
  }
@@ -111,6 +111,7 @@ type BoringChatPart = {
111
111
  filename?: string;
112
112
  mediaType?: string;
113
113
  url?: string;
114
+ path?: string;
114
115
  } | {
115
116
  type: 'notice';
116
117
  id?: string;
@@ -13,7 +13,7 @@ import {
13
13
  StopPayloadSchema,
14
14
  extractToolUiMetadata,
15
15
  sanitizeToolUiMetadata2 as sanitizeToolUiMetadata
16
- } from "../chunk-HEEJSOFF.js";
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
- if (/^[a-z0-9]{1,12}$/.test(fromName)) return fromName;
2720
- if (contentType === "image/jpeg") return "jpg";
2721
- if (contentType === "image/png") return "png";
2722
- if (contentType === "image/gif") return "gif";
2723
- if (contentType === "image/webp") return "webp";
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 ".css":
2755
- return "text/css; charset=utf-8";
2756
- case ".html":
2757
- case ".htm":
2758
- return "text/html; charset=utf-8";
2759
- case ".js":
2760
- return "text/javascript; charset=utf-8";
2761
- case ".mjs":
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 dir = normalizeUploadDir(body.directory) ?? normalizeUploadDir(settings.markdown?.imageUploadDir) ?? DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR;
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 = remote.fs?.stat ? await (async () => {
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 = remote.fs?.stat ? await (async () => {
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 { ...event, text: promptMetadata.displayText, clientNonce: promptMetadata.clientNonce };
10602
+ if (promptMetadata) return withMessageStartMetadata(event, promptMetadata);
10566
10603
  const metadata = this.findFollowUp(sessionId, event.text);
10567
- if (metadata) return { ...event, text: metadata.displayText, clientNonce: metadata.clientNonce, clientSeq: metadata.clientSeq };
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 !== "message-start" || event.role !== "user") return;
10647
- if (event.clientSeq !== void 0) return;
10648
- if (event.clientNonce) {
10649
- this.removePrompt(sessionId, { clientNonce: event.clientNonce });
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
- this.removePrompt(sessionId, { displayText: event.text });
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 run2 = this.trackActiveRun(sessionId, adapter.prompt(toPiPromptInput(payload)));
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 (!match) continue;
11680
- const [, mimeType, data] = match;
11681
- images.push({ type: "image", mimeType, data });
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;
@@ -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-DNKo77Xw.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-DNKo77Xw.js';
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
 
@@ -39,7 +39,7 @@ import {
39
39
  extractToolUiMetadata,
40
40
  isToolUiMetadata,
41
41
  sanitizeToolUiMetadata2 as sanitizeToolUiMetadata
42
- } from "../chunk-HEEJSOFF.js";
42
+ } from "../chunk-G6YPXDHR.js";
43
43
 
44
44
  // src/shared/config-schema.ts
45
45
  import { z } from "zod";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-agent",
3
- "version": "0.1.47",
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.47"
77
+ "@hachej/boring-ui-kit": "0.1.49"
78
78
  },
79
79
  "devDependencies": {
80
80
  "@antithesishq/bombadil": "0.5.0",