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-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-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
|
- [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-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
|
- [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-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
|
- [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-05 | Phase 25 | Complete |
|
||||||
| FILE-06 | Phase 25 | Complete |
|
| FILE-06 | Phase 25 | Complete |
|
||||||
| FILE-07 | Phase 25 | Complete |
|
| FILE-07 | Phase 25 | Complete |
|
||||||
| FILE-08 | Phase 25 | Pending |
|
| FILE-08 | Phase 25 | Complete |
|
||||||
| FILE-09 | Phase 25 | Complete |
|
| FILE-09 | Phase 25 | Complete |
|
||||||
| FILE-10 | Phase 25 | Complete |
|
| FILE-10 | Phase 25 | Complete |
|
||||||
| FILE-11 | Phase 25 | Pending |
|
| FILE-11 | Phase 25 | Complete |
|
||||||
| FILE-12 | Phase 25 | Complete |
|
| FILE-12 | Phase 25 | Complete |
|
||||||
| FILE-13 | 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 { chatService } from "../services/chat.js";
|
||||||
import { isAllowedContentType, MAX_ATTACHMENT_BYTES } from "../attachment-types.js";
|
import { isAllowedContentType, MAX_ATTACHMENT_BYTES } from "../attachment-types.js";
|
||||||
import { assertBoard, assertCompanyAccess } from "./authz.js";
|
import { assertBoard, assertCompanyAccess } from "./authz.js";
|
||||||
|
import { placeholderService } from "../services/placeholder-service.js";
|
||||||
|
import { resolveDefaultStorageDir } from "../home-paths.js";
|
||||||
|
|
||||||
const fileUpload = multer({
|
const fileUpload = multer({
|
||||||
storage: multer.memoryStorage(),
|
storage: multer.memoryStorage(),
|
||||||
|
|
@ -31,6 +33,8 @@ export function chatFileRoutes(db: Db, storage: StorageService) {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const fileSvc = chatFileService(db);
|
const fileSvc = chatFileService(db);
|
||||||
const convSvc = chatService(db);
|
const convSvc = chatService(db);
|
||||||
|
const phSvc = placeholderService();
|
||||||
|
const storageDir = resolveDefaultStorageDir();
|
||||||
|
|
||||||
// POST /conversations/:id/files — Upload a file to a conversation
|
// POST /conversations/:id/files — Upload a file to a conversation
|
||||||
router.post("/conversations/:id/files", async (req, res) => {
|
router.post("/conversations/:id/files", async (req, res) => {
|
||||||
|
|
@ -96,6 +100,16 @@ export function chatFileRoutes(db: Db, storage: StorageService) {
|
||||||
projectId: parsedMeta.data.projectId ?? null,
|
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({
|
res.status(201).json({
|
||||||
file: chatFile,
|
file: chatFile,
|
||||||
contentPath: `/api/files/${chatFile.id}/content`,
|
contentPath: `/api/files/${chatFile.id}/content`,
|
||||||
|
|
@ -205,6 +219,37 @@ export function chatFileRoutes(db: Db, storage: StorageService) {
|
||||||
res.json(updated);
|
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)
|
// PATCH /files/:fileId — Attach file to a message (set messageId)
|
||||||
router.patch("/files/:fileId", async (req, res) => {
|
router.patch("/files/:fileId", async (req, res) => {
|
||||||
assertBoard(req);
|
assertBoard(req);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue