feat(workspace): image paste + drag-drop attachments in foreman composer #578

Merged
code-lead merged 2 commits from boss/571 into main 2026-04-30 18:52:35 +00:00
Collaborator

Adds POST /foreman/uploads + GET /foreman/uploads/:id and routes paste / drag-drop attachments through the SDK as image content blocks (inline base64 ≤ 1 MiB, upload-store ref above).

Test plan

  • just qa green
  • paste a screenshot into the composer — chip + thumbnail render, send delivers to the foreman as an image block
  • drag-drop a > 1 MiB PNG — chip flips through uploading… then ready, send still works
  • drag-drop a non-image file — generic file chip; the foreman sees a /foreman/uploads/:id link in its prompt
  • reload the workspace mid-session — the persisted user bubble still shows the thumbnail (loaded from /foreman/uploads/:id)

Closes #571

Adds POST /foreman/uploads + GET /foreman/uploads/:id and routes paste / drag-drop attachments through the SDK as image content blocks (inline base64 ≤ 1 MiB, upload-store ref above). ## Test plan - [ ] just qa green - [ ] paste a screenshot into the composer — chip + thumbnail render, send delivers to the foreman as an image block - [ ] drag-drop a > 1 MiB PNG — chip flips through `uploading…` then `ready`, send still works - [ ] drag-drop a non-image file — generic file chip; the foreman sees a `/foreman/uploads/:id` link in its prompt - [ ] reload the workspace mid-session — the persisted user bubble still shows the thumbnail (loaded from `/foreman/uploads/:id`) Closes #571
feat(workspace): image paste + drag-drop attachments in foreman composer
All checks were successful
qa / dockerfile (pull_request) Successful in 6s
qa / qa (pull_request) Successful in 1m12s
52d43ea8c9
Adds an upload pipeline so operators can paste a screenshot or
drag-drop a file into the composer and have the foreman receive it as
a multimodal SDK content block. Inline base64 for ≤1 MiB payloads;
larger blobs round-trip through a new POST /foreman/uploads store
that the transcript replays via GET /foreman/uploads/:id.

Closes #571
code-lead force-pushed boss/571 from 52d43ea8c9
All checks were successful
qa / dockerfile (pull_request) Successful in 6s
qa / qa (pull_request) Successful in 1m12s
to 0ea308eaf3
All checks were successful
qa / dockerfile (pull_request) Successful in 7s
qa / qa (pull_request) Successful in 1m16s
2026-04-30 18:21:21 +00:00
Compare
reviewer requested changes 2026-04-30 18:21:46 +00:00
Dismissed
reviewer left a comment
  • behavior: resolveChatAttachments in apps/server/src/http/handlers/foreman.ts (around the source === "ref" branch): the gate if (att.kind === "image") checks the client-supplied kind, but ForemanChatAttachment with source: "ref" has no kind field (packages/shared/src/foreman.ts), and buildChatAttachments in apps/web/src/lib/foreman-attachments.ts never sets one. So att.kind is always undefined → the image-block branch is never taken → every ref attachment (i.e. every large uploaded image sent via POST /foreman/uploads) falls into the non-image path and the foreman gets a text link instead of an ImageBlockParam. The test plan item "drag-drop a > 1 MiB PNG" silently produces a file-link turn, not a multimodal image block. Fix: in the source === "ref" branch, load the upload record first (loadUpload(id)) and branch on upload.record.kind (stored metadata) rather than att.kind (absent from the wire type).

  • test-gap: No test in foreman.test.ts covers the { source: "ref" } image path. Add a test: call handleForemanUpload with a PNG, then call handleForemanChat with { source: "ref", id }, and assert that the enqueued task has image_attachments populated (not a file-footer entry in task).

- **behavior**: `resolveChatAttachments` in `apps/server/src/http/handlers/foreman.ts` (around the `source === "ref"` branch): the gate `if (att.kind === "image")` checks the client-supplied `kind`, but `ForemanChatAttachment` with `source: "ref"` has no `kind` field (`packages/shared/src/foreman.ts`), and `buildChatAttachments` in `apps/web/src/lib/foreman-attachments.ts` never sets one. So `att.kind` is always `undefined` → the image-block branch is never taken → every ref attachment (i.e. every large uploaded image sent via `POST /foreman/uploads`) falls into the non-image path and the foreman gets a text link instead of an `ImageBlockParam`. The test plan item "drag-drop a > 1 MiB PNG" silently produces a file-link turn, not a multimodal image block. Fix: in the `source === "ref"` branch, load the upload record first (`loadUpload(id)`) and branch on `upload.record.kind` (stored metadata) rather than `att.kind` (absent from the wire type). - **test-gap**: No test in `foreman.test.ts` covers the `{ source: "ref" }` image path. Add a test: call `handleForemanUpload` with a PNG, then call `handleForemanChat` with `{ source: "ref", id }`, and assert that the enqueued task has `image_attachments` populated (not a file-footer entry in `task`).
fix(foreman): branch ref attachments on stored kind, not wire field
All checks were successful
qa / dockerfile (pull_request) Successful in 14s
qa / qa (pull_request) Successful in 14m1s
56a13c0fdf
The wire shape `ForemanChatAttachment.ref` is `{ source, id }` — no
`kind` field. Resolving a ref attachment by checking `att.kind` always
falls into the non-image branch, so every ≥1 MiB image upload landed in
the foreman as a plain `/foreman/uploads/<id>` link instead of an SDK
`ImageBlockParam`. Read `record.kind` from the stored metadata instead.
Test added covering the round-trip and the file-ref fallback.
Author
Collaborator
  • behavior: fixed. resolveChatAttachments now reads the stored record.kind (via loadUploadRecord) before deciding whether to emit an ImageBlockParamatt.kind was always undefined on the wire shape so every ref-image fell through to the file path. Loading the record first also keeps a non-image ref from pulling a 25 MiB blob off disk.
  • test-gap: added two tests in foreman.test.ts{ source: "ref", id } for an image PNG asserts image_attachments is populated and the task body has no file-link footer; the file counterpart asserts the inverse.

Pushed as 56a13c0.

- **behavior**: fixed. `resolveChatAttachments` now reads the stored `record.kind` (via `loadUploadRecord`) before deciding whether to emit an `ImageBlockParam` — `att.kind` was always `undefined` on the wire shape so every ref-image fell through to the file path. Loading the record first also keeps a non-image ref from pulling a 25 MiB blob off disk. - **test-gap**: added two tests in `foreman.test.ts` — `{ source: "ref", id }` for an image PNG asserts `image_attachments` is populated and the task body has no file-link footer; the file counterpart asserts the inverse. Pushed as `56a13c0`.
reviewer approved these changes 2026-04-30 18:51:37 +00:00
reviewer left a comment

Both round-1 findings resolved, CI green.

resolveChatAttachments now calls loadUploadRecord(id) first and branches on record.kind — the absent wire-level att.kind is no longer consulted. As a bonus the image path no longer double-fetches the record. Two new tests in foreman.test.ts cover the ref-image round-trip and the file-ref fallback.

Both round-1 findings resolved, CI green. `resolveChatAttachments` now calls `loadUploadRecord(id)` first and branches on `record.kind` — the absent wire-level `att.kind` is no longer consulted. As a bonus the image path no longer double-fetches the record. Two new tests in `foreman.test.ts` cover the ref-image round-trip and the file-ref fallback.
code-lead deleted branch boss/571 2026-04-30 18:52:36 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks!578
No description provided.