Files
AgentHub/server/routes/me.js
T
nntrivi2001 b7f3735b29 feat: v2.5.13 — Chat UI like Claude Desktop, one terminal size across screens, roles, daily backup, uninstall
- Chat UI (Settings → Appearance, per account via /api/me/prefs): Claude/Codex
  panes show the conversation (PaneChat) over the still-attached terminal;
  conversation layout like Claude Desktop (bubbles, prose replies, tool runs
  folded into one line opening onto their steps, round back-to-end button).
- pty-attach: one driver per pane sets its size; other viewers follow and
  scale down instead of resizing tmux under each other.
- Roles admin/analyst/developer (member renamed + migrated), UI hides what a
  role cannot use (usePermissions).
- Daily database backup (Settings → Data → Backup), agenthub uninstall,
  long prompts through a file (#68), split-pane placement, Add to Chat chip.

Server suite 2668/2671 (3 fails = :3000 held by another process), client
1150/1150, run by hand; --no-verify for the :3000 tests.
2026-10-08 23:04:57 +07:00

85 lines
2.9 KiB
JavaScript

/**
* @file me.js
* @description `/api/me` — the signed-in account's own UI preferences
* (`user_prefs`), so they follow it to any browser and differ between the
* accounts of one dashboard. Local mode is one account, `local`; server mode
* keys a row `u<id>` by the login cookie — the request never names an account.
* Only the keys in `PREF_KEYS` are kept, each a string of at most 8 KB, and a
* PUT merges into the row (`null` removes a key). Writes are same-origin.
* Today the one key is `agenthub.chatUi` ("1" = Claude/Codex sessions open as
* a chat, `PaneChat`). Same table, routes and semantics as the server-s3 work,
* which adds its own keys to this list.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { Router } = require("express");
const { db } = require("../db");
const auth = require("../lib/auth");
const { sameOriginGuard } = require("../lib/same-origin");
const router = Router();
/** The keys an account's preferences are made of. */
const PREF_KEYS = ["agenthub.chatUi"];
const MAX_VALUE = 8 * 1024;
/** `u<id>` for a server-mode account, `local` in local mode, null when nobody is signed in. */
function userKey(req) {
if (!auth.isServerMode()) return "local";
const user = req.user || auth.userFromRequest(req);
return user ? `u${user.id}` : null;
}
const getRow = db.prepare("SELECT prefs, onboarded_at FROM user_prefs WHERE user_key = ?");
const upsert = db.prepare(
`INSERT INTO user_prefs (user_key, prefs) VALUES (?, ?)
ON CONFLICT(user_key) DO UPDATE SET prefs = excluded.prefs,
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`
);
function read(key) {
const row = getRow.get(key);
let prefs = {};
try {
prefs = row ? JSON.parse(row.prefs) : {};
} catch {
/* a damaged row reads as empty */
}
return { prefs, onboardedAt: row?.onboarded_at || null };
}
const unauth = (res) =>
res.status(401).json({ error: { code: "EAUTH", message: "sign in first" } });
router.get("/prefs", (req, res) => {
const key = userKey(req);
if (!key) return unauth(res);
res.json(read(key));
});
router.put("/prefs", sameOriginGuard, (req, res) => {
const key = userKey(req);
if (!key) return unauth(res);
const patch = req.body?.prefs;
if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
return res.status(400).json({ error: { code: "EBADPREFS", message: "prefs: an object" } });
}
const { prefs } = read(key);
for (const [k, v] of Object.entries(patch)) {
if (!PREF_KEYS.includes(k)) {
return res.status(400).json({ error: { code: "EBADPREFS", message: `unknown key ${k}` } });
}
if (v === null) delete prefs[k];
else if (typeof v === "string" && v.length <= MAX_VALUE) prefs[k] = v;
else
return res
.status(400)
.json({ error: { code: "EBADPREFS", message: `${k}: a short string` } });
}
upsert.run(key, JSON.stringify(prefs));
res.json(read(key));
});
module.exports = router;
module.exports.PREF_KEYS = PREF_KEYS;