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
@@ -56,7 +56,7 @@ before(async () => {
BASE = `http://127.0.0.1:${server.address().port}`;
ids.boss = (await auth.createUser("boss", BOSS_PASS, "admin")).id;
ids.ana = (await auth.createUser("ana", "analyst-pass-1", "analyst")).id;
ids.mia = (await auth.createUser("mia", "member-pass-1", "member")).id;
ids.mia = (await auth.createUser("mia", "member-pass-1", "developer")).id;
ids.lane = db
.prepare("INSERT INTO lanes (cwd, tool) VALUES ('/w/ownerless', 'claude')")
.run().lastInsertRowid;
@@ -76,14 +76,14 @@ describe("managing accounts from the dashboard", () => {
body: {
username: "neo",
password: "neo-pass-123",
role: "member",
role: "developer",
currentPassword: BOSS_PASS,
},
});
assert.equal(r.status, 201);
assert.deepEqual(
{ username: r.body.user.username, role: r.body.user.role },
{ username: "neo", role: "member" }
{ username: "neo", role: "developer" }
);
assert.ok(!JSON.stringify(r.body).includes("scrypt"), "no password hash");
assert.ok(await login("neo", "neo-pass-123"), "the new user can log in");
@@ -179,14 +179,14 @@ describe("managing accounts from the dashboard", () => {
assert.equal(bad.status, 400);
const none = await call("PATCH", "/api/users/9999", {
cookie: boss,
body: { role: "member", currentPassword: BOSS_PASS },
body: { role: "developer", currentPassword: BOSS_PASS },
});
assert.equal(none.status, 404);
});
it("never lets an admin demote or disable themselves", async () => {
const boss = await login("boss", BOSS_PASS);
for (const body of [{ role: "member" }, { disabled: true }]) {
for (const body of [{ role: "developer" }, { disabled: true }]) {
const r = await call("PATCH", `/api/users/${ids.boss}`, {
cookie: boss,
body: { ...body, currentPassword: BOSS_PASS },
@@ -239,7 +239,7 @@ describe("managing accounts from the dashboard", () => {
}
assert.equal(last.status, 429);
assert.equal(last.body.error.code, "ETOOMANY");
assert.equal(auth.findUser("mia").role, "member");
assert.equal(auth.findUser("mia").role, "developer");
});
});