nexus/.planning/milestones/v1.3-phases/25-file-system/25-05-PLAN.md
Nexus Dev ffc7b130e4 chore: archive v1.3 phase directories to milestones/
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 03:55:48 +00:00

8.7 KiB

phase plan type wave depends_on files_modified autonomous gap_closure requirements must_haves
25-file-system 05 execute 1
25-01
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
true true
FILE-12
truths artifacts key_links
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
path provides
server/src/services/chat-files.ts promoteToProject service method
path provides
server/src/routes/chat-files.ts PATCH /files/:fileId/promote endpoint
path provides
ui/src/components/ChatFileCard.tsx Promote button when file has no projectId
from to via pattern
ui/src/components/ChatFileCard.tsx ui/src/api/chat.ts chatApi.promoteFile call promoteFile
from to via pattern
server/src/routes/chat-files.ts server/src/services/chat-files.ts fileSvc.promoteToProject 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

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.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:

export function chatFileService(db: Db) {
  return {
    create(...), getById(...), listByConversation(...), listByMessage(...),
    createReference(...), listReferences(...), attachToMessage(...)
  };
}

From ui/src/api/chat.ts:

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); }, ```
  1. In server/src/routes/chat-files.ts, add a new route BEFORE the existing PATCH /files/:fileId route:
// 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 <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> 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.
  1. 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:
      {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
  2. 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 <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> 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

<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>
After completion, create `.planning/phases/25-file-system/25-05-SUMMARY.md`