Version. 0.1 Date. 2026-05-28 Status. Working draft. Scopes item 3 (the file-upload-through-the-in-engagement-composer port) ahead of a Change Request. Shape. Shape 2 — extract the upload orchestration into a shared hook both surfaces consume. Confirmed by the Operator 2026-05-28.
Today an Operator can bring files into an engagement only through the older /chat?project=<id> surface, which renders ChatView. The surface the Operator actually works in — the in-engagement composer, CenterPane — cannot accept files. It carries a disabled "Attachments coming soon" button instead. This note scopes the work to fix that: mount file upload on the in-engagement composer so files arrive through the same surface as conversation.
The shape is settled: extract the upload logic that currently lives inside ChatView into a shared hook, then have both ChatView and the in-engagement composer use that hook. The small visible pieces (the attach button, the per-file result card, the upload call) are already shared components; only the roughly hundred lines of orchestration that tie them together are stuck inside ChatView. The work pulls that orchestration out so both surfaces can use it without one copying the other.
What is decided: the shape (Shape 2). What this note hands forward: the in-scope list, the one architectural question the Change Request must answer first (which backend the composer's upload talks to), the principal risk (the extraction touches ChatView's working upload path), and the open questions the Change Request resolves. What is explicitly not in this work: the assertion-grain gap and two documentation corrections, all three already filed as queued-directions §15.3 so they are not carried here as loose clauses.
This note does not assign a phase number. Sequencing into the build queue is the Operator's call, and the next queue slot (Phase 61) is already spoken for by narration multi-pathway.
The architecture specification §13 ("Memory population surface unification") names the destination: files should arrive through the in-engagement composer, the same surface as conversation, rather than through a separate older surface. Queued-directions §15.1 records this as the second of three sequenced actions — the first (restore the /chat?project=<id> route so the affordance is reachable at all) is already resolved; this note scopes the second.
In plain terms: the in-engagement composer is where the Operator talks to the Companion. It should also be where the Operator hands the Companion a file. Right now it cannot, and it shows a disabled placeholder button advertising the missing feature.
Three shapes were named in the record (§15.1). Shape 1 extends the composer directly. Shape 2 extracts a shared hook. Shape 3 keeps both surfaces.
The current code makes the choice clear. The visible upload pieces are already factored out of ChatView as reusable components: UploadAttachmentButton (the attach affordance), UploadResultCard (the per-file result), postUpload (the upload call), and filterPlatformSystemFiles (the platform-junk filter). What is not reusable is the orchestration — the roughly hundred-line handleUpload callback, inlined in ChatView, that sequences those pieces and threads them through ChatView's state.
Shape 1 would write a second copy of that orchestration inside the composer. Two copies drift: every upload behaviour finding from the Phase 58 and Phase 60 work (typed-message preservation, the attachment marker, the optimistic-turn sequence) would then live in two places and have to stay in sync by hand. Shape 2 puts the orchestration in one hook both surfaces call, so there is one copy. Shape 3 declines the work entirely and leaves the disabled button in place.
Shape 2 is also the only shape that honours the composer's deliberate narrowness. CenterPane's own docstring states it carries conversation-read plus composer-write and deliberately does not pull in ChatView's other machinery (suggested actions, draft specifications, side-effect stubs, exhaustion dialog, token usage). A shared hook lets the composer gain upload without inheriting any of that — it calls the hook and renders results its own way.
Verified from the repositories on 2026-05-28 (Operator Layer main 2186d13, engine main d5d57ee). Stated here so the Change Request begins from fact, not from this note's paraphrase — re-verify against the live repositories at Change Request time.
ChatView (src/components/chat/ChatView.tsx, 661 lines) carries the working upload affordance. The attach button mounts at lines 607-612, gated by projectId. The orchestration is handleUpload at lines 374-470. It filters platform junk, sets a thinking gate, captures and clears the typed draft, optimistically appends the typed text as an Operator turn so it does not vanish, appends an attachment-marker turn, calls postUpload against the engine, and on success appends a Companion acknowledgment turn and writes the per-file results into a per-turn attachments state.
That attachments state is a fat per-turn shape: the upload results sit in one slot alongside suggestedActions, draftSpecification, readiness, sideEffects, and tokenUsage. The upload write piggybacks on the same state the other ChatView affordances use.
CenterPane (src/app/operator/engagement/[engagement_address]/CenterPane.tsx, 796 lines) is the in-engagement composer. It is text-only. It converses by posting to the Operator Layer route /operator/converse. It holds turns, draft, sending, sendError. It has no upload affordance, no multipart call, and no per-turn attachments state. It carries a single disabled ⊕ button (lines 559-574) labelled "Attachments coming soon" — no handler, no file input.
The cross-layer seam. ChatView uploads direct to the engine (/engagements/{eid}/uploads, multipart). CenterPane converses through the Operator Layer (/operator/converse). They talk to different backends. Mounting upload on the composer forces a decision about which backend the composer's upload talks to — see the open questions.
handleUpload and the results-rendering channel) into a shared hook.ChatView to consume the hook, preserving its current behaviour exactly.UploadResultCard.⊕ placeholder and replace it with the live UploadAttachmentButton.All three of the following are already filed as first-class queued-directions §15.3 entries. They are named here only so the boundary is explicit; they are not loose clauses this note carries, and they do not gate this work.
UploadEventReceived Memory event with full provenance, but does not draft individually-affirmable Assertions from it (assertions_drafted is hardcoded empty). This is engine work, present on the path ChatView uses today, unchanged by any frontend shape. This port mounts the affordance; it does not change what the engine does with the bytes.
Extracting the hook touches ChatView's working, Operator-verified upload path — the path exercised end-to-end on 2026-05-27. That is the principal regression surface. The Change Request must name the frontend test coverage guarding it (the vitest suite) and add coverage where thin. Note that the eight engine upload-test fixtures recently brought to the production envelope guard the engine side; they do not guard this frontend refactor.
postUpload, which targets the engine directly; the composer gains a direct-to-engine call encapsulated inside the hook, while its conversation traffic stays on /operator/converse. This is the working contract and there is no current Operator Layer upload route. The alternative — routing upload through an /operator/ gateway — would need a new route and a reason (for example, a single Operator Layer gateway for all engagement traffic, or attestation at that boundary). The Change Request decides this first; the rest of the shape follows from it.UploadResultCard from it, rather than adopting ChatView's fat per-turn attachments shape. This keeps the composer narrow.ChatView's handleUpload clears a persisted draft (clearDraft(projectId)). Whether the composer participates in persisted-draft was not confirmed in inspection. The Change Request checks this and decides whether the hook clears the persisted draft conditionally or both surfaces adopt it.ChatView optimistically appends typed text as an Operator turn before uploading so it does not vanish (Phase 60 Sub-arc Finding #4). The hook should own this so the composer gets parity. Confirm in the Change Request.ChatView's upload path; add coverage for the extracted hook and the composer mount.Checked against the seed's settled commitments. No conflict found; the work resolves a standing violation.
This note frames the work. The Change Request comes next and is the artifact that enumerates the exact changes and resolves the five open questions above, beginning with the cross-layer-seam decision. No Claude Code execution until the Change Request is written and approved — this is phase-shaped work, and the quick-fix approach is exactly what produced the transaction bug that hid for two weeks.
DUNIN7 · Loomworks · Composer Upload Port · Scoping Note · v0.1 · 2026-05-28