* docs(plans): chat attachment & image support implementation plan
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* feat(db): add chat_session_id/chat_message_id to attachment
Co-authored-by: multica-agent <github@multica.ai>
* feat(db): sqlc — chat_session_id on CreateAttachment + LinkAttachmentsToChatMessage
Co-authored-by: multica-agent <github@multica.ai>
* feat(file): upload-file accepts chat_session_id form field
Co-authored-by: multica-agent <github@multica.ai>
* feat(chat): SendChatMessage links uploaded attachments to the new message
Co-authored-by: multica-agent <github@multica.ai>
* feat(api): uploadFile accepts chatSessionId; sendChatMessage accepts attachmentIds
Co-authored-by: multica-agent <github@multica.ai>
* feat(core): useFileUpload supports chatSessionId context
Co-authored-by: multica-agent <github@multica.ai>
* feat(chat): support paste/drag/upload attachments in chat input
Co-authored-by: multica-agent <github@multica.ai>
* test(e2e): chat input attachment upload + send round-trip
Co-authored-by: multica-agent <github@multica.ai>
* chore(chat): keep lazy-created session title empty so untitled fallback localizes
Co-authored-by: multica-agent <github@multica.ai>
* fix(chat): address review — dedupe ensureSession + parse upload response
- chat-window: cache in-flight createSession promise in a ref so a file drop
followed by a quick send no longer spawns two sessions (and orphans the
attachment on the losing one).
- Attachment type + EMPTY_ATTACHMENT + AttachmentResponseSchema: include the
new chat_session_id / chat_message_id fields the server now returns.
- uploadFile: route the response through parseWithFallback so a malformed
body returns EMPTY_ATTACHMENT instead of an undefined-keyed Attachment,
matching the API boundary rule.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(chat): address PR #2445 review — test ctx, send gating, attachment surface
1. Backend test was 400ing because the handler reads workspace from
middleware-injected ctx, and `newRequest` only sets the header. Helper
`withChatTestWorkspaceCtx` mirrors the agent-access-test pattern and
loads the member row + SetMemberContext before invoking the handler.
2. Attachment metadata now flows end-to-end:
- new sqlc `ListAttachmentsByChatMessageIDs` (batch lookup, mirrors the
comment-side query)
- `chatMessageToResponse` takes `attachments` and `ChatMessageResponse`
surfaces them — same shape as CommentResponse
- `ListChatMessages` loads them via a new `groupChatMessageAttachments`
helper so the chat bubble can render file cards
- daemon claim path pulls `ListAttachmentsByChatMessage` for the latest
user message and ships `ChatMessageAttachments` to the daemon
- `buildChatPrompt` lists id+filename+content_type and instructs the
agent to `multica attachment download <id>` — fixes the private-CDN
expiring-URL problem where the markdown URL would have expired by
the time the agent acts
- TS `ChatMessage` gains an optional `attachments` field
3. Chat composer now blocks send while uploads are in flight:
- `pendingUploads` counter increments in handleUpload, SubmitButton
uses it to disable
- handleSend also gates on `editorRef.current.hasActiveUploads()` to
catch the Mod+Enter path that bypasses the button
- new vitest covers the "drop large file → immediate send" scenario
where attachment id would otherwise be silently dropped
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* chore: drop implementation plan doc
Process artefact, not something the repo needs to keep.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Problem
-------
The v2 workspace URL refactor (#1141) switched the frontend from sending
X-Workspace-ID (UUID) to X-Workspace-Slug. The workspace middleware was
updated to accept the slug and translate it via GetWorkspaceBySlug.
But the handler package maintained a PARALLEL resolver
(`resolveWorkspaceID` in handler.go) used by endpoints that sit outside
the workspace middleware — and that resolver was never updated. It only
checked context / ?workspace_id / X-Workspace-ID, never the slug.
/api/upload-file is the one production route that hit the broken path:
it's user-scoped (not behind workspace middleware) because it also
serves avatar uploads (no workspace). Post-refactor requests from the
frontend arrived with only X-Workspace-Slug; the handler resolver
returned "", the code fell into the "no workspace context" branch, and
every file upload since v2 landed in S3 with no corresponding DB
attachment row — files orphaned, invisible to the UI.
Root cause is structural: two resolvers doing the same job, written
independently, diverged silently when one was updated.
Fix
---
Collapse to a single shared helper. middleware.ResolveWorkspaceIDFromRequest
is the new canonical resolver; both the middleware's internal
`resolveWorkspaceUUID` (for middleware gating) and the handler-side
`(h *Handler).resolveWorkspaceID` (promoted from a package function)
now delegate to it. Priority order matches what the middleware has had
since v2: context > X-Workspace-Slug header > ?workspace_slug query >
X-Workspace-ID header > ?workspace_id query.
Impact analysis
---------------
47 call sites of the old `resolveWorkspaceID(r)` are renamed to
`h.resolveWorkspaceID(r)`. 46 of them sit behind workspace middleware,
so they hit the context fast path and see zero behavior change. The
one caller that actually gains capability is UploadFile — which now
correctly recognizes slug requests and creates DB attachment rows.
Tests
-----
- New table-driven unit test for ResolveWorkspaceIDFromRequest covers
all priority levels and the unknown-slug fallback.
- Regression tests for UploadFile: once with X-Workspace-Slug only
(the broken path), once with X-Workspace-ID only (legacy CLI/daemon
compat path). Both assert that a DB attachment row is created.
- Full Go test suite passes; typecheck + pnpm test unaffected.
Plan
----
See docs/plans/2026-04-16-unify-workspace-identity-resolver.md for the
full first-principles writeup.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add GET /api/config endpoint exposing cdn_domain from CLOUDFRONT_DOMAIN
- Create packages/core/config/ zustand store, fetched at app startup
- Extract file card preprocessing to packages/ui/markdown/file-cards.ts
with isCdnUrl(url, cdnDomain) using exact hostname match
- Add file card support to packages/ui/markdown/Markdown.tsx (was missing)
- Remove hardcoded .copilothub.ai hostname check from file-card.tsx
- Fix LocalStorage.CdnDomain() to return hostname not full URL
- Always run preprocessFileCards regardless of cdnDomain availability
(!file syntax works without CDN domain, only legacy matching needs it)
- Use useConfigStore hook in common/markdown.tsx for reactive updates
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(server): validate workspace membership for subscription targets and file uploads
Closes MED-1 (cross-workspace subscription injection) and MED-2 (file upload
missing workspace member validation) from the security audit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(server): add negative tests for cross-workspace subscription and upload
Address PR review feedback:
- Add tests verifying cross-workspace user_id is rejected with 403 on
subscribe and unsubscribe
- Add test verifying upload with foreign workspace_id is rejected with 403
- Make isWorkspaceEntity explicitly enumerate "member"/"agent" and reject
unknown user types
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>