feat: support unified prompt file attachments#107
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds model-aware attachment routing and a native picker orchestration, surfaces approved local-file data-URL reads via platform/IPC, reworks ingestion to route/embed/reject based on model capabilities, preserves workspace path parts for office/text files, and adds comprehensive tests plus i18n keys. Changes
Sequence Diagram(s)sequenceDiagram
participant UI as "PromptInput UI"
participant PickerFlow as "pickAttachments"
participant Picker as "Platform.openFilePickerDialog"
participant Fallback as "HiddenInputFallback"
participant Router as "attachment-routing"
participant PlatformRead as "Platform.readFileDataUrl"
participant ModelSel as "openModelSelector"
UI->>PickerFlow: start pick (fallbackInputClick, addPickedPaths)
PickerFlow->>Picker: openFilePickerDialog({multiple:true,extensions:[]})
alt picker throws or not provided
Picker-->>PickerFlow: error / undefined
PickerFlow->>Fallback: fallbackInputClick()
PickerFlow-->>UI: return false
else picker returns paths
Picker-->>PickerFlow: [paths]
loop for each path
PickerFlow->>Router: routePickedPath(path, model)
alt route == direct
Router-->>PickerFlow: direct (mime)
PickerFlow->>PlatformRead: readFileDataUrl(path,mime)
alt dataUrl returned
PlatformRead-->>PickerFlow: dataUrl
PickerFlow-->>UI: add direct attachment
else null
PlatformRead-->>PickerFlow: null
PickerFlow-->>UI: insert file path part
end
else route == reject-image
Router-->>PickerFlow: reject-image
PickerFlow->>ModelSel: openModelSelector() (toast action)
PickerFlow-->>UI: show image-unsupported toast
else route == path
Router-->>PickerFlow: path (office/text/unknown/unsupported-pdf)
PickerFlow-->>UI: insert file path part (e.g., "@path")
end
end
PickerFlow-->>UI: return addPickedPaths result
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Code Review
This pull request implements a comprehensive attachment routing system that handles files based on model capabilities, including support for images, PDFs, and Office documents. It introduces a native file picker for the desktop application and adds IPC handlers to read local files as data URLs. The backend prompt logic is also updated to filter tool-produced attachments and handle path-only fallbacks for unsupported formats. Feedback highlights opportunities to improve error reporting for bulk file uploads, expand media support to include audio and video, and prevent information loss when filtering tool outputs.
c68b60f to
9f3c761
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 910e233c3b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
9f3c761 to
e955612
Compare
|
Addressed the review comments in the latest force-push:\n\n- Batch attachment drops now still show feedback when some files are skipped after other files attach successfully.\n- Native picker failures now fall back to the browser file input instead of surfacing an unhandled rejection.\n- Backend media filtering now covers audio and video modalities consistently with provider capabilities.\n- Mixed supported and unsupported Read attachments now preserve the original path part when any attachment is filtered out.\n\nVerification rerun:\n- Focused prompt attachment tests\n- Focused session prompt tests\n- bun turbo typecheck\n- bun turbo test:ci |
Astro-Han
left a comment
There was a problem hiding this comment.
Nitpicks
Overall implementation is solid. A few minor observations for consistency and edge cases:
e955612 to
9e82f88
Compare
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/app/src/components/prompt-input/attachment-routing.ts`:
- Around line 87-89: routeBrowserFile currently only checks OFFICE_EXTS and
falls back to reason "unknown", causing inconsistent routing versus
routePickedPath which checks TEXT_EXTS; update routeBrowserFile to compute the
file extension (using ext(file.name) as already done), then if
TEXT_EXTS.has(suffix) return { type: "path", reason: "text" } before the office
check or alongside it so .html/.txt/etc. are routed as text consistently with
routePickedPath; keep the existing OFFICE_EXTS check and fallback for unknowns.
In `@packages/app/src/components/prompt-input/attachments.ts`:
- Around line 107-116: The add function redundantly calls attachmentMime(file)
for route.type === "direct" even though routeBrowserFile already returns
route.mime; replace the async attachmentMime call with route.mime: after
obtaining route from routeBrowserFile (via routeBrowserFile and inspecting
route.type), use const mime = route.mime (or route.mime directly), check if mime
is falsy and call warn() and return false as before, then call await
dataUrl(file, mime) and pass file.name, mime into addDirect; remove the
attachmentMime(file) invocation and keep calls to routeBrowserFile, addDirect,
dataUrl, and warn unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 13ce2d01-93df-4dd7-a59c-74ecb91eae7d
📒 Files selected for processing (18)
packages/app/src/components/prompt-input.tsxpackages/app/src/components/prompt-input/attachment-routing.test.tspackages/app/src/components/prompt-input/attachment-routing.tspackages/app/src/components/prompt-input/attachments.test.tspackages/app/src/components/prompt-input/attachments.tspackages/app/src/components/prompt-input/build-request-parts.test.tspackages/app/src/components/prompt-input/pick-attachments.test.tspackages/app/src/components/prompt-input/pick-attachments.tspackages/app/src/context/platform.tsxpackages/app/src/i18n/en.tspackages/app/src/i18n/zh.tspackages/desktop-electron/src/main/ipc.tspackages/desktop-electron/src/preload/index.tspackages/desktop-electron/src/preload/types.tspackages/desktop-electron/src/renderer/index.tsxpackages/opencode/src/session/prompt.tspackages/opencode/test/session/prompt-effect.test.tspackages/opencode/test/session/prompt.test.ts
9e82f88 to
1a9dcf6
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/opencode/src/session/prompt.ts (1)
1094-1175:⚠️ Potential issue | 🟠 MajorCapability gating is still bypassed for
file:parts with real MIME types.This new downgrade logic only runs when the incoming local part is labeled
text/plain. A caller that sendsfile://.../report.docxwith its actual Office MIME, orfile://.../foo.pdfwithapplication/pdf, still falls through to the rawfsys.readFile(...)->data:branch below and gets attached directly to the model. That undermines the new routing contract and can reintroduce unsupported-media failures. Apply the same path-preservation/capability check before any localfile:part is converted to a data URL, not just afterReador fortext/plaininputs.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/opencode/src/session/prompt.ts` around lines 1094 - 1175, The code only applies the path-preservation/capability gating for local parts when part.mime === "text/plain"; update the logic to detect any local file part (e.g., part.url starting with "file:" or using officePathOnly(filepath)) before the code path that converts local files to data URLs and apply the same capability checks (use mediaInputKind(attachment.mime) and modelCanReadMedia(model, kind)) and path-preservation behavior: if the destination model cannot read the media, keep the file-as-path synthetic message and attach the original part instead of converting to a data: URL. Locate and modify the branch that currently handles text/plain and the later raw fsys.readFile(...)->data: conversion so the gating runs once earlier (using filepath, part.url, officePathOnly, execRead/provider.getModel/Exit handling) for all local file parts regardless of MIME.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/app/src/components/prompt-input/attachment-routing.ts`:
- Around line 3-4: InputKind currently only lists "image" | "pdf" while
ModelInputMap (and the backend's MediaInputKind) also include "audio" and
"video", causing a type mismatch and future confusion; update InputKind to
"image" | "pdf" | "audio" | "video" to match ModelInputMap and backend
MediaInputKind (or if you intentionally want to limit supported types, add a
clear comment above InputKind referencing that it's intentionally limited and
link to backend MediaInputKind), ensuring the symbols InputKind and
ModelInputMap remain consistent across frontend and backend.
In `@packages/app/src/components/prompt-input/attachments.ts`:
- Around line 71-81: warnRouteFailure contains a fallback warn() that is
theoretically unreachable because AttachRoute.direct denotes success and should
never be passed in; make this explicit by replacing the generic fallback with an
exhaustive check that signals a programmer error: convert the if-chain into a
switch or if/else blocks over route.type (handling "reject-image" ->
warnImageUnsupported, "path" -> pathRequired) and for any other case (e.g.,
route.type === "direct" or unexpected values) throw an explicit Error or call an
assertUnreachable helper mentioning warnRouteFailure and route.type so the
unreachable state is obvious at runtime and during typechecking (reference
warnRouteFailure, AttachRoute, warnImageUnsupported, pathRequired, warn, and
consider adding/asserting an assertUnreachable(route) or throw new
Error(`Unexpected AttachRoute in warnRouteFailure: ${route.type}`)).
- Around line 127-142: The addAttachments function redundantly calls
routeBrowserFile after add fails; change add(file, ...) to return the failure
AttachRoute (e.g., Promise<boolean | AttachRoute>) instead of just false so it
captures the route before returning, then update addAttachments to check the
returned value from add and set failure = returned AttachRoute when present (no
second routeBrowserFile call), preserving use of warnRouteFailure(failure) and
input.model() where needed.
In `@packages/desktop-electron/src/main/ipc.ts`:
- Around line 122-129: The "read-file-data-url" IPC handler currently reads any
path directly with fs.readFile, allowing arbitrary and oversized files; update
the handler to first validate that the supplied path was produced by your
approved picker flow (e.g. check a secure allowlist or token via a new helper
like isApprovedAttachment(path) or lookup in the picker-provided Map) and then
call fs.stat(path) to enforce the attachment size limit (compare size to
MAX_ATTACHMENT_BYTES) before reading; only if both checks pass call
fs.readFile(path) and return the base64 data URL, otherwise reject/return null
or throw a clear error.
---
Outside diff comments:
In `@packages/opencode/src/session/prompt.ts`:
- Around line 1094-1175: The code only applies the path-preservation/capability
gating for local parts when part.mime === "text/plain"; update the logic to
detect any local file part (e.g., part.url starting with "file:" or using
officePathOnly(filepath)) before the code path that converts local files to data
URLs and apply the same capability checks (use mediaInputKind(attachment.mime)
and modelCanReadMedia(model, kind)) and path-preservation behavior: if the
destination model cannot read the media, keep the file-as-path synthetic message
and attach the original part instead of converting to a data: URL. Locate and
modify the branch that currently handles text/plain and the later raw
fsys.readFile(...)->data: conversion so the gating runs once earlier (using
filepath, part.url, officePathOnly, execRead/provider.getModel/Exit handling)
for all local file parts regardless of MIME.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: d36aaf73-62fe-4da1-aceb-8d90764ce1dc
📒 Files selected for processing (18)
packages/app/src/components/prompt-input.tsxpackages/app/src/components/prompt-input/attachment-routing.test.tspackages/app/src/components/prompt-input/attachment-routing.tspackages/app/src/components/prompt-input/attachments.test.tspackages/app/src/components/prompt-input/attachments.tspackages/app/src/components/prompt-input/build-request-parts.test.tspackages/app/src/components/prompt-input/pick-attachments.test.tspackages/app/src/components/prompt-input/pick-attachments.tspackages/app/src/context/platform.tsxpackages/app/src/i18n/en.tspackages/app/src/i18n/zh.tspackages/desktop-electron/src/main/ipc.tspackages/desktop-electron/src/preload/index.tspackages/desktop-electron/src/preload/types.tspackages/desktop-electron/src/renderer/index.tsxpackages/opencode/src/session/prompt.tspackages/opencode/test/session/prompt-effect.test.tspackages/opencode/test/session/prompt.test.ts
1a9dcf6 to
ce662ed
Compare
722f3c2 to
7be033f
Compare
9fc49f6 to
2325e22
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
packages/opencode/test/session/prompt.test.ts (1)
121-145:⚠️ Potential issue | 🟠 MajorUse
pathToFileURL()for the missing-file case too.Line 130 and Line 142 still use
file://${missing}, which is not a portable file URL on Windows and mis-encodes reserved characters like#and%. The new Office-path test below already usespathToFileURL(...).href; this case should match it.♻️ Proposed fix
const missing = path.join(tmp.path, "does-not-exist.ts") + const missingUrl = pathToFileURL(missing).href const msg = yield* prompt.prompt({ sessionID: session.id, agent: "build", noReply: true, parts: [ { type: "text", text: "please review `@does-not-exist.ts`" }, { type: "file", mime: "text/plain", - url: `file://${missing}`, + url: missingUrl, filename: "does-not-exist.ts", }, ], }) @@ - expect(msg.parts.some((part) => part.type === "file" && part.url === `file://${missing}`)).toBe(true) + expect(msg.parts.some((part) => part.type === "file" && part.url === missingUrl)).toBe(true)#!/bin/bash rg -n 'file://\$\{' packages/opencode/test/session/prompt.test.ts packages/opencode/test/session/prompt-effect.test.tsExpected: no interpolated
file://URLs remain in the session prompt tests.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/opencode/test/session/prompt.test.ts` around lines 121 - 145, Replace the literal interpolated file URL strings with a proper file URL via pathToFileURL(missing).href: in the prompt.prompt call (the parts array) use pathToFileURL(missing).href instead of `file://${missing}`, and update the assertion that checks for the file part URL (the msg.parts.some(...) check) to compare against pathToFileURL(missing).href as well; import pathToFileURL from 'url' if not already present and keep the rest of the logic (prompt.prompt call and msg.parts checks) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/opencode/src/session/prompt.ts`:
- Around line 1163-1181: The current filter keeps attachments when
mediaInputKind(attachment.mime) is undefined, allowing unknown/office types
(e.g., .docx) to be re-emitted as synthetic attachments; change the predicate in
the attachments creation so it only keeps attachments with a defined kind that
the model can read (i.e., require kind !== undefined && modelCanReadMedia(model,
kind)) before mapping to synthetic pieces; keep the existing fallback that
pushes the original { ...part, messageID: info.id, sessionID: input.sessionID }
when filtered attachments.length < result.attachments.length so unmapped/unknown
Read attachments preserve the original file: part.
---
Duplicate comments:
In `@packages/opencode/test/session/prompt.test.ts`:
- Around line 121-145: Replace the literal interpolated file URL strings with a
proper file URL via pathToFileURL(missing).href: in the prompt.prompt call (the
parts array) use pathToFileURL(missing).href instead of `file://${missing}`, and
update the assertion that checks for the file part URL (the msg.parts.some(...)
check) to compare against pathToFileURL(missing).href as well; import
pathToFileURL from 'url' if not already present and keep the rest of the logic
(prompt.prompt call and msg.parts checks) unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 82e88811-0700-4b7b-a41e-2d4f6109020c
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (23)
packages/app/src/components/prompt-input.tsxpackages/app/src/components/prompt-input/attachment-routing.test.tspackages/app/src/components/prompt-input/attachment-routing.tspackages/app/src/components/prompt-input/attachments.test.tspackages/app/src/components/prompt-input/attachments.tspackages/app/src/components/prompt-input/build-request-parts.test.tspackages/app/src/components/prompt-input/build-request-parts.tspackages/app/src/components/prompt-input/files.tspackages/app/src/components/prompt-input/pick-attachments.test.tspackages/app/src/components/prompt-input/pick-attachments.tspackages/app/src/context/platform.tsxpackages/app/src/i18n/en.tspackages/app/src/i18n/zh.tspackages/desktop-electron/package.jsonpackages/desktop-electron/src/main/ipc.tspackages/desktop-electron/src/main/server.test.tspackages/desktop-electron/src/preload/index.tspackages/desktop-electron/src/preload/types.tspackages/desktop-electron/src/renderer/index.tsxpackages/opencode/src/session/prompt.tspackages/opencode/test/session/prompt-effect.test.tspackages/opencode/test/session/prompt.test.tspackages/util/src/file-extensions.ts
2325e22 to
745411b
Compare
Summary
Why
Closes #100. PawWork users need to attach files from outside the worktree without hidden copies, while keeping media direct only when the selected model can read it.
Related Issue
Closes #100
How To Verify
Fresh-eyes review found no P0/P1/P2/P3 findings.
Screenshots or Recordings
Not captured. This change is covered by unit tests and backend prompt tests. Manual desktop picker smoke was not run in this headless session.
Checklist
devbranchSummary by CodeRabbit
New Features
Improvements
Tests