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
+40
View File
@@ -524,6 +524,46 @@ describe("pty-run", () => {
assert.match(pty.paneArgv(null, "linux")[2], /exec "\$SHELL" -l$/);
});
it("hands a long prompt to the pane through a file, never tmux's argv (issue #68)", (t) => {
process.env.DASHBOARD_DATA_DIR = TMP;
t.after(() => delete process.env.DASHBOARD_DATA_DIR);
// tmux refuses a message past 16 KB (`command too long`).
const prompt = `-${"x".repeat(20_000)}\nend`;
const call = captureNewSession(() =>
pty.spawnRun({ laneId: 15, cwd: "/tmp/repo", initialPrompt: prompt })
);
assert.ok(Buffer.byteLength(call.join("")) < 8 * 1024);
const argv = paneArgv(call);
assert.deepEqual([argv[0], argv[1], argv[3]], ["sh", "-c", "sh"]);
const file = argv[4];
assert.equal(fs.statSync(file).mode & 0o777, 0o600);
assert.equal(argv[5], "claude");
// Run the reader with a stand-in CLI: the prompt arrives as ONE last
// argument, leading-dash guard included, and the file is gone after.
const { execFileSync } = require("node:child_process");
const out = execFileSync(argv[0], [
...argv.slice(1, 5),
process.execPath,
"-e",
"process.stdout.write(process.argv.at(-1))",
]).toString();
assert.equal(out, ` ${prompt}`);
assert.ok(!fs.existsSync(file));
});
it("refuses a prompt no CLI can take as one argument, before starting anything", () => {
const calls = [];
tmux.__setExecImpl((args) => {
calls.push(args);
return "";
});
assert.throws(
() => pty.spawnRun({ laneId: 16, cwd: "/tmp/repo", initialPrompt: "é".repeat(70_000) }),
{ code: "EBADPROMPT" }
);
assert.equal(calls.length, 0);
});
it("spawnRun starts the CLI only once the pane has its real size, argv untouched", () => {
// A claude drawn at tmux's birth size duplicated its banner on the first
// resize; the argv rides as positional parameters, never script text.