feat(25-07): add placeholder and agent-generated file routes
- Import placeholderService and resolveDefaultStorageDir in chat-files routes - Track agent_generated project-scoped uploads in PLACEHOLDERS.md manifest - Add POST /files/:fileId/replace endpoint for placeholder replacement - Replace endpoint updates manifest and creates cross-reference chain - Mark FILE-08 and FILE-11 Complete in REQUIREMENTS.md
This commit is contained in:
parent
859ba1707b
commit
7ab49d9824
2 changed files with 49 additions and 4 deletions
|
|
@ -90,10 +90,10 @@
|
|||
- [x] **FILE-05** — File upload from chat input via drag-and-drop or button; file is stored on disk and its metadata is written to libSQL
|
||||
- [x] **FILE-06** — Inline file preview in chat: images render inline, PDFs show a first-page preview, code files show a syntax-highlighted preview
|
||||
- [x] **FILE-07** — One-click file download from chat for any attached or generated file
|
||||
- [ ] **FILE-08** — Agent-generated files (code output, specs, presentations) stored in `files/projects/<slug>/generated/`, linked to the originating task and conversation in libSQL
|
||||
- [x] **FILE-08** — Agent-generated files (code output, specs, presentations) stored in `files/projects/<slug>/generated/`, linked to the originating task and conversation in libSQL
|
||||
- [x] **FILE-09** — Git integration: `files/` is a git repository; every file operation (upload, generate, replace, delete) creates a commit with a descriptive message
|
||||
- [x] **FILE-10** — Version history: user can view the git log for any file and see its change history
|
||||
- [ ] **FILE-11** — Placeholder asset tracking: Nexus auto-maintains a `PLACEHOLDERS.md` manifest in each project directory; when a placeholder is replaced by a final asset, the manifest and DB are updated with the replacement chain
|
||||
- [x] **FILE-11** — Placeholder asset tracking: Nexus auto-maintains a `PLACEHOLDERS.md` manifest in each project directory; when a placeholder is replaced by a final asset, the manifest and DB are updated with the replacement chain
|
||||
- [x] **FILE-12** — File scope promotion: a chat-scoped file can be promoted to a project scope; a project file can be referenced in any chat conversation
|
||||
- [x] **FILE-13** — Cross-device file access: files are served via the Nexus server API so a file uploaded on one device is accessible on any other device on the network
|
||||
|
||||
|
|
@ -175,9 +175,9 @@ The following are explicitly deferred:
|
|||
| FILE-05 | Phase 25 | Complete |
|
||||
| FILE-06 | Phase 25 | Complete |
|
||||
| FILE-07 | Phase 25 | Complete |
|
||||
| FILE-08 | Phase 25 | Pending |
|
||||
| FILE-08 | Phase 25 | Complete |
|
||||
| FILE-09 | Phase 25 | Complete |
|
||||
| FILE-10 | Phase 25 | Complete |
|
||||
| FILE-11 | Phase 25 | Pending |
|
||||
| FILE-11 | Phase 25 | Complete |
|
||||
| FILE-12 | Phase 25 | Complete |
|
||||
| FILE-13 | Phase 25 | Complete |
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import { chatFileService, deriveCategory } from "../services/chat-files.js";
|
|||
import { chatService } from "../services/chat.js";
|
||||
import { isAllowedContentType, MAX_ATTACHMENT_BYTES } from "../attachment-types.js";
|
||||
import { assertBoard, assertCompanyAccess } from "./authz.js";
|
||||
import { placeholderService } from "../services/placeholder-service.js";
|
||||
import { resolveDefaultStorageDir } from "../home-paths.js";
|
||||
|
||||
const fileUpload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
|
|
@ -31,6 +33,8 @@ export function chatFileRoutes(db: Db, storage: StorageService) {
|
|||
const router = Router();
|
||||
const fileSvc = chatFileService(db);
|
||||
const convSvc = chatService(db);
|
||||
const phSvc = placeholderService();
|
||||
const storageDir = resolveDefaultStorageDir();
|
||||
|
||||
// POST /conversations/:id/files — Upload a file to a conversation
|
||||
router.post("/conversations/:id/files", async (req, res) => {
|
||||
|
|
@ -96,6 +100,16 @@ export function chatFileRoutes(db: Db, storage: StorageService) {
|
|||
projectId: parsedMeta.data.projectId ?? null,
|
||||
});
|
||||
|
||||
// Track placeholder if agent-generated and project-scoped
|
||||
if (parsedMeta.data.source === "agent_generated" && chatFile.projectId) {
|
||||
const projectDir = path.join(storageDir, "..", "..", "projects", chatFile.projectId);
|
||||
phSvc.addEntry(projectDir, {
|
||||
fileId: chatFile.id,
|
||||
filename: chatFile.originalFilename,
|
||||
description: "Generated by agent",
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
file: chatFile,
|
||||
contentPath: `/api/files/${chatFile.id}/content`,
|
||||
|
|
@ -205,6 +219,37 @@ export function chatFileRoutes(db: Db, storage: StorageService) {
|
|||
res.json(updated);
|
||||
});
|
||||
|
||||
// POST /files/:fileId/replace — Replace a placeholder with a final asset
|
||||
router.post("/files/:fileId/replace", async (req, res) => {
|
||||
assertBoard(req);
|
||||
const fileId = req.params.fileId as string;
|
||||
const oldFile = await fileSvc.getById(fileId);
|
||||
if (!oldFile) { res.status(404).json({ error: "File not found" }); return; }
|
||||
assertCompanyAccess(req, oldFile.companyId);
|
||||
|
||||
const { newFileId } = req.body ?? {};
|
||||
if (!newFileId || typeof newFileId !== "string") {
|
||||
res.status(400).json({ error: "newFileId is required" }); return;
|
||||
}
|
||||
const newFile = await fileSvc.getById(newFileId);
|
||||
if (!newFile) { res.status(404).json({ error: "Replacement file not found" }); return; }
|
||||
|
||||
// Update placeholder manifest if project-scoped
|
||||
if (oldFile.projectId) {
|
||||
const projectDir = path.join(storageDir, "..", "..", "projects", oldFile.projectId);
|
||||
await phSvc.replaceEntry(projectDir, fileId, newFileId);
|
||||
}
|
||||
|
||||
// Create reference linking replacement to original context
|
||||
await fileSvc.createReference({
|
||||
fileId: newFileId,
|
||||
conversationId: oldFile.conversationId ?? "",
|
||||
messageId: oldFile.messageId ?? undefined,
|
||||
});
|
||||
|
||||
res.json({ replaced: fileId, replacedBy: newFileId });
|
||||
});
|
||||
|
||||
// PATCH /files/:fileId — Attach file to a message (set messageId)
|
||||
router.patch("/files/:fileId", async (req, res) => {
|
||||
assertBoard(req);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue