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
+35 -2
View File
@@ -30,6 +30,8 @@
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const fs = require("node:fs");
const path = require("node:path");
const tmux = require("./tmux");
const { resolveProxy, proxyForSpawn } = require("./proxy-apply");
const { desktopEnv } = require("./desktop-env");
@@ -137,6 +139,23 @@ function promptArg(p) {
return p.startsWith("-") ? ` ${p}` : p;
}
// A prompt past INLINE_PROMPT_BYTES reaches the pane in a 0600 file, not in
// tmux's argv: one tmux message holds 16 KB with the env and the rest of the
// argv beside it (`command too long`, issue #68). The pane's own sh reads it
// back as the CLI's last argument and deletes the file. No CLI can take one
// argument past Linux's MAX_ARG_STRLEN (128 KB, E2BIG), so that is refused.
const INLINE_PROMPT_BYTES = 4 * 1024;
const MAX_PROMPT_BYTES = 128 * 1024 - 2; // the NUL, and promptArg's space
const READ_PROMPT = 'p=$(cat "$1") && rm -f "$1" && shift && exec "$@" "$p"';
function promptFileArgv(id, argv) {
const dir = path.join(require("./claude-home").getDataDir(), "run-prompts");
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
const file = path.join(dir, `${id}.txt`);
fs.writeFileSync(file, argv[argv.length - 1], { mode: 0o600 });
return ["sh", "-c", READ_PROMPT, "sh", file, ...argv.slice(0, -1)];
}
function buildClaudeArgv({
model,
permissionMode,
@@ -316,6 +335,12 @@ function spawnRun(args) {
if (typeof cwd !== "string" || !cwd) {
throw makeErr("EBADCWD", "cwd is required");
}
if (typeof initialPrompt === "string" && Buffer.byteLength(initialPrompt) > MAX_PROMPT_BYTES) {
throw makeErr(
"EBADPROMPT",
`the prompt is ${Math.ceil(Buffer.byteLength(initialPrompt) / 1024)} KB; an AI CLI takes at most 128 KB as its first message — attach the text as a file instead`
);
}
// A lane whose tool has since left the catalog keeps its row; it just
// cannot start until the catalog knows the tool again.
const entry = aiTools.get(tool);
@@ -434,6 +459,14 @@ function spawnRun(args) {
entry
);
const pasteLater = entry.prompt?.mode === "after-start" && initialPrompt ? initialPrompt : null;
// Every launcher puts the prompt LAST; Windows has no sh to read it back.
const paneCmd =
initialPrompt &&
!pasteLater &&
process.platform !== "win32" &&
Buffer.byteLength(argv[argv.length - 1]) > INLINE_PROMPT_BYTES
? promptFileArgv(id, argv)
: argv;
const record = () => {
if (!dashboardRuns) return;
@@ -524,7 +557,7 @@ function spawnRun(args) {
};
if (!tmux.hasSession(id)) {
tmux.newSession({ name: id, cwd, argv: paneArgv(argv), env });
tmux.newSession({ name: id, cwd, argv: paneArgv(paneCmd), env });
record();
schedulePaste();
} else if (SHELL_COMMANDS.has(tmux.paneCommand(id) || "")) {
@@ -534,7 +567,7 @@ function spawnRun(args) {
// `--resume` and an initial prompt would never be typed, while the API
// still answered 200. Run the argv in the pane the user already sees
// instead of erroring or opening a second session.
tmux.sendCommand(id, argv, env, cwd);
tmux.sendCommand(id, paneCmd, env, cwd);
record();
schedulePaste();
}