feat(FN-1130): add project memory editing in settings

- Add GET /api/memory and PUT /api/memory routes with project-scoped file access and validation
- Add fetchMemory and saveMemory dashboard API helpers for memory file operations
- Extend the Settings modal Memory section with inline FileEditor loading, dirty tracking, save action, and read-only behavior when memory is disabled
- Add SettingsModal tests covering memory load, loading state, save flow, and empty-content handling
This commit is contained in:
gsxdsm
2026-04-08 04:30:10 -07:00
parent 757942bd18
commit 4ba4cca01f
4 changed files with 236 additions and 3 deletions

View File

@@ -6,13 +6,13 @@ import { resolve, sep, join } from "node:path";
import * as nodeFs from "node:fs";
import * as nodeChildProcess from "node:child_process";
import type { TaskStore, Column, MergeResult, ScheduleType, ActivityEventType, ModelPreset, AutomationStep, MessageType, ParticipantType, MessageCreateInput } from "@fusion/core";
import { COLUMNS, VALID_TRANSITIONS, GLOBAL_SETTINGS_KEYS, type BatchStatusEntry, type BatchStatusResponse, type BatchStatusResult, type IssueInfo, type PrInfo, type Task, isGhAuthenticated, AUTOMATION_PRESETS, AutomationStore, validateBackupSchedule, validateBackupRetention, validateBackupDir, syncBackupAutomation, exportSettings, importSettings, validateImportData, MessageStore } from "@fusion/core";
import { COLUMNS, VALID_TRANSITIONS, GLOBAL_SETTINGS_KEYS, type BatchStatusEntry, type BatchStatusResponse, type BatchStatusResult, type IssueInfo, type PrInfo, type Task, isGhAuthenticated, AUTOMATION_PRESETS, AutomationStore, validateBackupSchedule, validateBackupRetention, validateBackupDir, syncBackupAutomation, exportSettings, importSettings, validateImportData, MessageStore, MEMORY_FILE_PATH } from "@fusion/core";
import type { ServerOptions } from "./server.js";
import { GitHubClient, getCurrentGitHubRepo, parseBadgeUrl } from "./github.js";
import { githubRateLimiter } from "./github-poll.js";
import { terminalSessionManager } from "./terminal.js";
import { getTerminalService } from "./terminal-service.js";
import { listFiles, readFile, writeFile, listWorkspaceFiles, readWorkspaceFile, writeWorkspaceFile, copyWorkspaceFile, moveWorkspaceFile, deleteWorkspaceFile, renameWorkspaceFile, getWorkspaceFileForDownload, getWorkspaceFolderForZip, FileServiceError, type FileListResponse, type FileContentResponse, type SaveFileResponse, type FileOperationResponse } from "./file-service.js";
import { listFiles, readFile, writeFile, listWorkspaceFiles, readWorkspaceFile, writeWorkspaceFile, copyWorkspaceFile, moveWorkspaceFile, deleteWorkspaceFile, renameWorkspaceFile, getWorkspaceFileForDownload, getWorkspaceFolderForZip, readProjectFile, writeProjectFile, FileServiceError, type FileListResponse, type FileContentResponse, type SaveFileResponse, type FileOperationResponse } from "./file-service.js";
import { fetchAllProviderUsage } from "./usage.js";
import {
getGitHubAppConfig,
@@ -1428,6 +1428,48 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
}
});
// ── Project Memory Routes ─────────────────────────────────────
/**
* GET /api/memory
* Returns the project memory file content.
* If .fusion/memory.md does not exist yet, returns an empty string.
*/
router.get("/memory", async (req, res) => {
try {
const scopedStore = await getScopedStore(req);
const memory = await readProjectFile(scopedStore, MEMORY_FILE_PATH);
res.json({ content: memory.content });
} catch (err: any) {
if (err instanceof FileServiceError && err.code === "ENOENT") {
res.json({ content: "" });
return;
}
res.status(500).json({ error: err.message ?? "Failed to read memory" });
}
});
/**
* PUT /api/memory
* Updates the project memory file content.
* Body: { content: string }
*/
router.put("/memory", async (req, res) => {
try {
const { content } = req.body ?? {};
if (typeof content !== "string") {
res.status(400).json({ error: "content must be a string" });
return;
}
const scopedStore = await getScopedStore(req);
await writeProjectFile(scopedStore, MEMORY_FILE_PATH, content);
res.json({ success: true });
} catch (err: any) {
res.status(500).json({ error: err.message ?? "Failed to save memory" });
}
});
// ── Global Settings Routes ─────────────────────────────────────
/**