--- phase: 25-file-system plan: 05 type: execute wave: 1 depends_on: ["25-01"] files_modified: - server/src/services/chat-files.ts - server/src/routes/chat-files.ts - ui/src/components/ChatFileCard.tsx - ui/src/api/chat.ts - .planning/REQUIREMENTS.md autonomous: true gap_closure: true requirements: [FILE-12] must_haves: truths: - "User can promote a chat-scoped file to project scope via a single action" - "PATCH /files/:fileId/promote endpoint sets projectId on a chat file" artifacts: - path: "server/src/services/chat-files.ts" provides: "promoteToProject service method" - path: "server/src/routes/chat-files.ts" provides: "PATCH /files/:fileId/promote endpoint" - path: "ui/src/components/ChatFileCard.tsx" provides: "Promote button when file has no projectId" key_links: - from: "ui/src/components/ChatFileCard.tsx" to: "ui/src/api/chat.ts" via: "chatApi.promoteFile call" pattern: "promoteFile" - from: "server/src/routes/chat-files.ts" to: "server/src/services/chat-files.ts" via: "fileSvc.promoteToProject" pattern: "promoteToProject" --- Add file scope promotion: a chat-scoped file can be promoted to a project scope. Purpose: FILE-12 requires that chat-scoped files can be promoted to project scope. The schema already has a nullable `projectId` FK on chatFiles, but there is no API endpoint or UI to set it. This plan adds a PATCH /files/:fileId/promote endpoint and a promote button on ChatFileCard. Output: Service method, API endpoint, UI promote button, updated REQUIREMENTS.md @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/25-file-system/25-01-SUMMARY.md From packages/db/src/schema/chat_files.ts: ```typescript export const chatFiles = pgTable("chat_files", { id: uuid("id").primaryKey().defaultRandom(), projectId: uuid("project_id").references(() => projects.id, { onDelete: "set null" }), // ... other columns }); ``` From server/src/services/chat-files.ts: ```typescript export function chatFileService(db: Db) { return { create(...), getById(...), listByConversation(...), listByMessage(...), createReference(...), listReferences(...), attachToMessage(...) }; } ``` From ui/src/api/chat.ts: ```typescript export const chatApi = { uploadFile(...), attachFilesToMessage(...) }; ``` Task 1: Add promoteToProject service method and API endpoint server/src/services/chat-files.ts, server/src/routes/chat-files.ts - server/src/services/chat-files.ts - server/src/routes/chat-files.ts - packages/db/src/schema/chat_files.ts 1. In `server/src/services/chat-files.ts`, add a new method `promoteToProject` to the returned object: ```typescript promoteToProject(fileId: string, projectId: string) { return db .update(chatFiles) .set({ projectId, updatedAt: new Date() }) .where(eq(chatFiles.id, fileId)) .returning() .then((rows) => rows[0] ?? null); }, ``` 2. In `server/src/routes/chat-files.ts`, add a new route BEFORE the existing `PATCH /files/:fileId` route: ```typescript // PATCH /files/:fileId/promote — Promote chat file to project scope router.patch("/files/:fileId/promote", async (req, res) => { assertBoard(req); const fileId = req.params.fileId as string; const chatFile = await fileSvc.getById(fileId); if (!chatFile) { res.status(404).json({ error: "File not found" }); return; } assertCompanyAccess(req, chatFile.companyId); const { projectId } = req.body ?? {}; if (!projectId || typeof projectId !== "string") { res.status(400).json({ error: "projectId is required" }); return; } const updated = await fileSvc.promoteToProject(fileId, projectId); if (!updated) { res.status(404).json({ error: "File not found" }); return; } res.json(updated); }); ``` Place this route BEFORE the `PATCH /files/:fileId` route so Express matches `/files/:fileId/promote` before the catch-all `/files/:fileId`. cd /opt/nexus && grep -n "promoteToProject" server/src/services/chat-files.ts server/src/routes/chat-files.ts - `server/src/services/chat-files.ts` contains `promoteToProject(fileId: string, projectId: string)` method - `server/src/routes/chat-files.ts` contains `router.patch("/files/:fileId/promote"` route - The promote route appears BEFORE the generic `router.patch("/files/:fileId"` route - Route validates `projectId` is present and is a string - Route calls `fileSvc.promoteToProject(fileId, projectId)` PATCH /files/:fileId/promote endpoint exists, validates input, updates projectId on chat file Task 2: Add promote button to ChatFileCard and API client method ui/src/api/chat.ts, ui/src/components/ChatFileCard.tsx, .planning/REQUIREMENTS.md - ui/src/api/chat.ts - ui/src/components/ChatFileCard.tsx - .planning/REQUIREMENTS.md 1. In `ui/src/api/chat.ts`, add a new method to `chatApi`: ```typescript promoteFile(fileId: string, projectId: string) { return api.patch(`/files/${fileId}/promote`, { projectId }); }, ``` Also add `ChatFile` to the import from `@paperclipai/shared` if not already imported. 2. In `ui/src/components/ChatFileCard.tsx`: - Add optional props: `projectId?: string | null` and `onPromoted?: (file: ChatFile) => void` to ChatFileCardProps - Import `FolderUp` from lucide-react (for the promote icon) - Import `chatApi` from `../api/chat` - Import `ChatFile` from `@paperclipai/shared` - Add state: `const [promoting, setPromoting] = useState(false)` - Add a promote button that appears ONLY when `file.projectId === null && projectId && onPromoted`: ```tsx {file.projectId === null && projectId && onPromoted && ( )} ``` - Place this button before the download button 3. Update `.planning/REQUIREMENTS.md`: - Change FILE-12 line from `- [ ] **FILE-12**` to `- [x] **FILE-12**` - In Traceability table, change FILE-12 status from `Pending` to `Complete` cd /opt/nexus && grep -n "promoteFile" ui/src/api/chat.ts && grep -n "FolderUp\|onPromoted\|promoteToProject" ui/src/components/ChatFileCard.tsx && grep "FILE-12" .planning/REQUIREMENTS.md | head -3 - `ui/src/api/chat.ts` contains `promoteFile(fileId: string, projectId: string)` method - `ui/src/components/ChatFileCard.tsx` contains `FolderUp` import from lucide-react - `ui/src/components/ChatFileCard.tsx` contains `onPromoted` prop - `ui/src/components/ChatFileCard.tsx` contains `chatApi.promoteFile` call - `ui/src/components/ChatFileCard.tsx` conditionally renders promote button only when `file.projectId === null && projectId && onPromoted` - `.planning/REQUIREMENTS.md` contains `- [x] **FILE-12**` Chat files can be promoted to project scope via UI button; FILE-12 marked Complete - `npx tsc --noEmit -p ui/tsconfig.json` passes - `npx tsc --noEmit -p server/tsconfig.json` passes - `grep "promoteToProject" server/src/services/chat-files.ts` matches - `grep "promoteFile" ui/src/api/chat.ts` matches - `grep "\[x\].*FILE-12" .planning/REQUIREMENTS.md` matches - PATCH /files/:fileId/promote endpoint sets projectId on a chat file - ChatFileCard shows a promote button for chat-scoped files when a project context is available - FILE-12 marked Complete in REQUIREMENTS.md - TypeScript compiles without errors After completion, create `.planning/phases/25-file-system/25-05-SUMMARY.md`