fix(session-folders): persist via server endpoint, not client-built home path

Client previously built `${homeDirectory}/.config/openchamber/sessions-directories.json`
using client-side homeDirectory, which in some boot paths resolved to the
active workspace and wrote the file inside the project. Replace with
GET/POST /api/session-folders — server uses os.homedir() directly, so the
file always lands in ~/.config/openchamber/ regardless of client state.
This commit is contained in:
Bohdan Triapitsyn
2026-04-22 19:39:02 +03:00
parent a2730b793e
commit 1ab522e656
4 changed files with 90 additions and 85 deletions
@@ -261,6 +261,7 @@ export const registerCommonRequestMiddleware = (app, dependencies) => {
req.path.startsWith('/api/opencode') ||
req.path.startsWith('/api/push') ||
req.path.startsWith('/api/notifications') ||
req.path.startsWith('/api/session-folders') ||
req.path.startsWith('/api/text') ||
req.path.startsWith('/api/voice') ||
req.path.startsWith('/api/tts') ||
@@ -3,6 +3,7 @@ import { registerQuotaRoutes } from '../quota/routes.js';
import { registerGitHubRoutes } from '../github/routes.js';
import { registerGitRoutes } from '../git/routes.js';
import { registerMagicPromptRoutes } from '../magic-prompts/routes.js';
import { registerSessionFoldersRoutes } from '../session-folders/routes.js';
import { registerConfigEntityRoutes } from './config-entity-routes.js';
import { registerSettingsUtilityRoutes } from './core-routes.js';
import { registerProjectIconRoutes } from './project-icon-routes.js';
@@ -216,6 +217,11 @@ export const createFeatureRoutesRuntime = (dependencies) => {
path,
openchamberDataDir,
});
registerSessionFoldersRoutes(app, {
fsPromises,
path,
openchamberDataDir,
});
registerFsRoutes(app, {
os,
path,
@@ -0,0 +1,57 @@
const MAX_BODY_BYTES = 4 * 1024 * 1024;
export const registerSessionFoldersRoutes = (app, dependencies) => {
const {
fsPromises,
path,
openchamberDataDir,
} = dependencies;
const filePath = path.join(openchamberDataDir, 'sessions-directories.json');
const ensureDir = async () => {
await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
};
app.get('/api/session-folders', async (_req, res) => {
try {
const raw = await fsPromises.readFile(filePath, 'utf8').catch((error) => {
if (error && error.code === 'ENOENT') return null;
throw error;
});
if (!raw) {
return res.json({ version: 1, foldersMap: {}, collapsedFolderIds: [], updatedAt: 0 });
}
try {
const parsed = JSON.parse(raw);
return res.json(parsed);
} catch {
return res.json({ version: 1, foldersMap: {}, collapsedFolderIds: [], updatedAt: 0 });
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to read session folders';
return res.status(500).json({ error: message });
}
});
app.post('/api/session-folders', async (req, res) => {
const body = req.body;
if (!body || typeof body !== 'object' || Array.isArray(body)) {
return res.status(400).json({ error: 'Body must be an object' });
}
const serialized = JSON.stringify(body, null, 2);
if (Buffer.byteLength(serialized, 'utf8') > MAX_BODY_BYTES) {
return res.status(413).json({ error: 'Payload too large' });
}
try {
await ensureDir();
const tmp = `${filePath}.tmp`;
await fsPromises.writeFile(tmp, serialized, 'utf8');
await fsPromises.rename(tmp, filePath);
return res.json({ success: true });
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to write session folders';
return res.status(500).json({ error: message });
}
});
};