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.
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* @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;
|
||||
Reference in New Issue
Block a user