nexus/.planning/phases/25-file-system/25-05-PLAN.md

239 lines
8.7 KiB
Markdown

---
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"
---
<objective>
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
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/25-file-system/25-01-SUMMARY.md
<interfaces>
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(...)
};
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Add promoteToProject service method and API endpoint</name>
<files>server/src/services/chat-files.ts, server/src/routes/chat-files.ts</files>
<read_first>
- server/src/services/chat-files.ts
- server/src/routes/chat-files.ts
- packages/db/src/schema/chat_files.ts
</read_first>
<action>
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`.
</action>
<verify>
<automated>cd /opt/nexus && grep -n "promoteToProject" server/src/services/chat-files.ts server/src/routes/chat-files.ts</automated>
</verify>
<acceptance_criteria>
- `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)`
</acceptance_criteria>
<done>PATCH /files/:fileId/promote endpoint exists, validates input, updates projectId on chat file</done>
</task>
<task type="auto">
<name>Task 2: Add promote button to ChatFileCard and API client method</name>
<files>ui/src/api/chat.ts, ui/src/components/ChatFileCard.tsx, .planning/REQUIREMENTS.md</files>
<read_first>
- ui/src/api/chat.ts
- ui/src/components/ChatFileCard.tsx
- .planning/REQUIREMENTS.md
</read_first>
<action>
1. In `ui/src/api/chat.ts`, add a new method to `chatApi`:
```typescript
promoteFile(fileId: string, projectId: string) {
return api.patch<ChatFile>(`/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 && (
<button
onClick={async (e) => {
e.stopPropagation();
setPromoting(true);
try {
const updated = await chatApi.promoteFile(file.id, projectId);
onPromoted(updated);
} finally {
setPromoting(false);
}
}}
disabled={promoting}
className="shrink-0 rounded p-1 text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
aria-label="Promote to project"
title="Promote to project"
>
<FolderUp className="h-4 w-4" />
</button>
)}
```
- 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`
</action>
<verify>
<automated>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</automated>
</verify>
<acceptance_criteria>
- `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**`
</acceptance_criteria>
<done>Chat files can be promoted to project scope via UI button; FILE-12 marked Complete</done>
</task>
</tasks>
<verification>
- `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
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
After completion, create `.planning/phases/25-file-system/25-05-SUMMARY.md`
</output>