@hachej/boring-agent 0.1.48 → 0.1.50

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
  }
@@ -2911,16 +2949,12 @@ function fileRoutes(app, opts, done) {
2911
2949
  if (filename === null) return;
2912
2950
  const contentBase64 = requireStringParam(body?.contentBase64, "contentBase64", reply);
2913
2951
  if (contentBase64 === null) return;
2914
- const contentType = typeof body.contentType === "string" ? body.contentType.trim().toLowerCase() : "";
2915
- if (!contentType.startsWith("image/")) {
2916
- return reply.code(400).send({
2917
- error: { code: ERROR_CODE_VALIDATION_ERROR, message: "contentType must be an image/* MIME type", field: "contentType" }
2918
- });
2919
- }
2952
+ const contentType = typeof body.contentType === "string" && body.contentType.trim() ? body.contentType.trim().toLowerCase() : "application/octet-stream";
2920
2953
  try {
2921
2954
  const workspace = await resolveWorkspace(request);
2922
2955
  const settings = await readWorkspaceSettings(workspace);
2923
- 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;
2924
2958
  const ext = extForUpload(filename, contentType);
2925
2959
  const base = basenameForUpload(filename);
2926
2960
  const unique = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
@@ -5006,7 +5040,7 @@ function createVercelSandboxExec(sandbox, execOpts = {}) {
5006
5040
  };
5007
5041
  } catch (error) {
5008
5042
  if (timedOut) {
5009
- return timeoutResult(Date.now() - start);
5043
+ return timeoutResult(Math.max(Date.now() - start, timeoutMs));
5010
5044
  }
5011
5045
  throw error;
5012
5046
  } finally {
@@ -9832,7 +9866,8 @@ function filePartsFromContent(content, messageId) {
9832
9866
  id: `${messageId}:file:${index}`,
9833
9867
  ...optionalString(part.filename) ? { filename: optionalString(part.filename) } : {},
9834
9868
  ...mediaType ? { mediaType } : {},
9835
- ...imagePartUrl(part, mediaType) ? { url: imagePartUrl(part, mediaType) } : {}
9869
+ ...imagePartUrl(part, mediaType) ? { url: imagePartUrl(part, mediaType) } : {},
9870
+ ...optionalString(part.path) ? { path: optionalString(part.path) } : {}
9836
9871
  }
9837
9872
  ];
9838
9873
  });
@@ -10396,7 +10431,8 @@ function filePartsFromContent2(content, messageId) {
10396
10431
  id: `${messageId}:file:${index}`,
10397
10432
  ...optionalString2(part.filename) ? { filename: optionalString2(part.filename) } : {},
10398
10433
  ...mediaType ? { mediaType } : {},
10399
- ...url ? { url } : {}
10434
+ ...url ? { url } : {},
10435
+ ...optionalString2(part.path) ? { path: optionalString2(part.path) } : {}
10400
10436
  }];
10401
10437
  });
10402
10438
  }
@@ -10490,11 +10526,13 @@ var PiChatMessageMetadataReconciler = class {
10490
10526
  recordPrompt(sessionId, payload) {
10491
10527
  const metadata = this.promptMetadata.get(sessionId) ?? [];
10492
10528
  const displayText = payload.displayMessage ?? payload.message;
10529
+ const files = filePartsFromPromptPayload(payload);
10493
10530
  metadata.push({
10494
10531
  displayText,
10495
10532
  ...displayText !== payload.message ? { serverText: payload.message } : {},
10496
10533
  clientNonce: payload.clientNonce,
10497
- recordedAt: Date.now()
10534
+ recordedAt: Date.now(),
10535
+ ...files ? { files } : {}
10498
10536
  });
10499
10537
  this.promptMetadata.set(sessionId, metadata);
10500
10538
  }
@@ -10561,9 +10599,16 @@ var PiChatMessageMetadataReconciler = class {
10561
10599
  }
10562
10600
  if (event.type === "message-start" && event.role === "user" && !hasFollowUpSelector2(event)) {
10563
10601
  const promptMetadata = this.findPrompt(sessionId, event.text);
10564
- if (promptMetadata) return { ...event, text: promptMetadata.displayText, clientNonce: promptMetadata.clientNonce };
10602
+ if (promptMetadata) return withMessageStartMetadata(event, promptMetadata);
10565
10603
  const metadata = this.findFollowUp(sessionId, event.text);
10566
- 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) };
10567
10612
  }
10568
10613
  return event;
10569
10614
  }
@@ -10642,13 +10687,24 @@ var PiChatMessageMetadataReconciler = class {
10642
10687
  return this.promptMetadata.get(sessionId)?.find((entry) => matchesMetadataText(entry, text));
10643
10688
  }
10644
10689
  consumePromptFromEvent(sessionId, event) {
10645
- if (event.type !== "message-start" || event.role !== "user") return;
10646
- if (event.clientSeq !== void 0) return;
10647
- if (event.clientNonce) {
10648
- 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 });
10649
10699
  return;
10650
10700
  }
10651
- 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) });
10652
10708
  }
10653
10709
  removeFirstFollowUpByText(sessionId, text) {
10654
10710
  if (!text) return;
@@ -10702,9 +10758,18 @@ function matchesMetadataText(entry, text) {
10702
10758
  if (!entry || !text) return false;
10703
10759
  return entry.displayText === text || entry.serverText === text;
10704
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
+ }
10705
10770
  function withMessageSelector(message, metadata) {
10706
10771
  return {
10707
- ...withMessageDisplayText(message, metadata.displayText),
10772
+ ...withMessageFiles(withMessageDisplayText(message, metadata.displayText), metadata.files),
10708
10773
  ...metadata.clientNonce ? { clientNonce: metadata.clientNonce } : {},
10709
10774
  ...metadata.clientSeq !== void 0 ? { clientSeq: metadata.clientSeq } : {}
10710
10775
  };
@@ -10719,6 +10784,22 @@ function withMessageDisplayText(message, displayText) {
10719
10784
  });
10720
10785
  return { ...message, parts };
10721
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
+ }
10722
10803
  function withQueuedSelector(followUp, metadata) {
10723
10804
  return {
10724
10805
  ...followUp,
@@ -11216,10 +11297,13 @@ var PiChatMeteringCoordinator = class {
11216
11297
  };
11217
11298
 
11218
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"]);
11219
11302
  var HarnessPiChatService = class {
11220
11303
  harness;
11221
11304
  sessionStore;
11222
11305
  workdir;
11306
+ workspace;
11223
11307
  channels = /* @__PURE__ */ new Map();
11224
11308
  // Single-flight guard so concurrent cold callers (e.g. two browser tabs each
11225
11309
  // opening /events while the session is still being created) converge on one
@@ -11235,6 +11319,7 @@ var HarnessPiChatService = class {
11235
11319
  this.harness = options.harness;
11236
11320
  this.sessionStore = options.sessionStore;
11237
11321
  this.workdir = options.workdir;
11322
+ this.workspace = options.workspace;
11238
11323
  this.metering = options.metering ? new PiChatMeteringCoordinator(options.metering, options.meteringLogger) : void 0;
11239
11324
  }
11240
11325
  /** Test/diagnostic hook: resolves once queued metering sink calls settle. */
@@ -11328,7 +11413,8 @@ var HarnessPiChatService = class {
11328
11413
  const channel = this.channels.get(sessionId);
11329
11414
  const receiptCursor = nextPromptReceiptCursor(channel);
11330
11415
  try {
11331
- 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));
11332
11418
  run2.catch((error) => {
11333
11419
  this.metering?.failPromptRun(sessionId, payload.clientNonce);
11334
11420
  if (!this.messageMetadata.hasPrompt(sessionId, { clientNonce: payload.clientNonce, displayText: payload.displayMessage ?? payload.message })) return;
@@ -11628,7 +11714,8 @@ function promptPayloadFileParts(payload, messageId) {
11628
11714
  id: `${messageId}:file:${index}`,
11629
11715
  filename: attachment.filename,
11630
11716
  mediaType: attachment.mediaType,
11631
- url: attachment.url
11717
+ url: attachment.url,
11718
+ path: attachment.path
11632
11719
  }));
11633
11720
  }
11634
11721
  function promptPayloadMessage(payload, messageId, createdAt, turnId) {
@@ -11666,21 +11753,64 @@ function messageTime(message) {
11666
11753
  const timestamp = Date.parse(message.createdAt);
11667
11754
  return Number.isFinite(timestamp) ? timestamp : void 0;
11668
11755
  }
11669
- function toPiPromptInput(payload) {
11670
- const images = promptImagesFromAttachments(payload.attachments);
11756
+ async function toPiPromptInput(payload, workspace) {
11757
+ const images = await promptImagesFromAttachments(payload.attachments, workspace);
11671
11758
  if (images.length === 0) return payload.message;
11672
11759
  return { text: payload.message, options: { images } };
11673
11760
  }
11674
- function promptImagesFromAttachments(attachments) {
11761
+ async function promptImagesFromAttachments(attachments, workspace) {
11675
11762
  const images = [];
11676
11763
  for (const attachment of attachments ?? []) {
11677
11764
  const match = attachment.url.match(/^data:(image\/[^;]+);base64,(.+)$/);
11678
- if (!match) continue;
11679
- const [, mimeType, data] = match;
11680
- 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
+ }
11681
11785
  }
11682
11786
  return images;
11683
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
+ }
11684
11814
  function removedFollowUps(before, after) {
11685
11815
  const afterCounts = /* @__PURE__ */ new Map();
11686
11816
  for (const text of after) afterCounts.set(text, (afterCounts.get(text) ?? 0) + 1);
@@ -11837,6 +11967,7 @@ async function createAgentApp(opts = {}) {
11837
11967
  harness,
11838
11968
  sessionStore: harness.sessions,
11839
11969
  workdir: runtimeBundle.workspace.root,
11970
+ workspace: runtimeBundle.workspace,
11840
11971
  metering: opts.metering
11841
11972
  });
11842
11973
  await app.register(piChatRoutes, { service: piChatService });
@@ -12074,6 +12205,17 @@ function selectRuntimeModeAdapter(mode, sandboxHandleStore) {
12074
12205
  function getRequestWorkspaceId(request) {
12075
12206
  return request.workspaceContext?.workspaceId ?? DEFAULT_WORKSPACE_ID3;
12076
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
+ }
12077
12219
  function isWorkspaceAgnosticAgentRequest(request, options) {
12078
12220
  const pathname = request.url.split("?")[0] ?? request.url;
12079
12221
  if (pathname === "/api/v1/ready-status") return !options?.readyStatusWorkspaceScoped;
@@ -12566,6 +12708,7 @@ var registerAgentRoutes = async (app, opts) => {
12566
12708
  app.addHook("onRequest", async (request, reply) => {
12567
12709
  const user = request.user;
12568
12710
  let workspaceId = DEFAULT_WORKSPACE_ID3;
12711
+ promoteRawFileWorkspaceQueryToHeader(request);
12569
12712
  if (opts.getWorkspaceId && !isWorkspaceAgnosticAgentRequest(request, { readyStatusWorkspaceScoped: requestScopedRuntime })) {
12570
12713
  try {
12571
12714
  workspaceId = (await opts.getWorkspaceId(request)).trim();
@@ -12626,6 +12769,7 @@ var registerAgentRoutes = async (app, opts) => {
12626
12769
  harness: binding.harness,
12627
12770
  sessionStore: binding.harness.sessions,
12628
12771
  workdir: binding.runtimeBundle.workspace.root,
12772
+ workspace: binding.runtimeBundle.workspace,
12629
12773
  metering: opts.metering
12630
12774
  });
12631
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.48",
3
+ "version": "0.1.50",
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.48"
77
+ "@hachej/boring-ui-kit": "0.1.50"
78
78
  },
79
79
  "devDependencies": {
80
80
  "@antithesishq/bombadil": "0.5.0",