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:
2026-10-08 23:04:57 +07:00
parent a9c71cd905
commit b7f3735b29
147 changed files with 5644 additions and 2561 deletions
+14 -7
View File
@@ -6,9 +6,11 @@
* A workspace (`lanes.owner_id`) belongs to the user who created it. A session
* belongs to its workspace: `sessions.lane_id`, or a `dashboard_runs` row of
* that workspace holding it (a session not stamped yet). Agents, events and
* token rows belong to their session. A member sees only what their own
* workspaces own; an admin and an analyst see everything, and so does every
* request in local mode (`req.user` is never set there).
* token rows belong to their session. A developer sees and changes only what
* their own workspaces own; an analyst SEES everything but changes only their
* own (every non-GET request is scoped like a developer's); an admin sees and
* changes everything, and so does every request in local mode (`req.user` is
* never set there).
*
* Every helper takes `req`, so no caller can pick whose rows it reads.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
@@ -16,10 +18,14 @@
const sql = (q) => require("../db").db.prepare(q);
/** The member whose rows this request is limited to, or null for "all". */
/** The user whose rows this request is limited to, or null for "all". */
function memberId(req) {
const u = req?.user;
return u && u.role === "member" ? u.id : null;
if (!u) return null;
if (u.role === "developer") return u.id;
// An analyst reads every user's rows but writes only their own.
if (u.role === "analyst" && req.method !== "GET" && req.method !== "HEAD") return u.id;
return null;
}
/** The owner a workspace created by this request gets (null outside server mode). */
@@ -191,7 +197,8 @@ function namedIds(req) {
/**
* Express middleware (on `/api`, after authGuard): a member naming a row that
* is not theirs — in the path, the query or the body — gets 404, the same
* answer as a row that does not exist. A no-op for admin, analyst and local.
* answer as a row that does not exist. A no-op for an admin, an analyst's
* reads, and local mode.
*/
function scopeGuard(req, res, next) {
if (memberId(req) == null) return next();
@@ -273,7 +280,7 @@ function broadcastAudience(type, data) {
/** Whether a client logged in as `user` (undefined in local mode) gets it. */
function audienceIncludes(audience, user) {
if (!user || user.role !== "member") return true;
if (!user || user.role !== "developer") return true;
if (audience === null || audience.strip) return true;
return Boolean(audience.owners && audience.owners.has(user.id));
}