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
+61 -2
View File
@@ -553,7 +553,7 @@ export type AuditEntry = {
};
/** Who is using the dashboard: local mode has no login at all. */
export type AuthUser = { id: number; username: string; role: "admin" | "analyst" | "member" };
export type AuthUser = { id: number; username: string; role: "admin" | "analyst" | "developer" };
export type AuthState = { mode: "local" } | { mode: "server"; user: AuthUser | null };
/** GETs in flight, by path: concurrent callers of the same read (several
@@ -596,7 +596,12 @@ async function send<T>(path: string, options?: RequestInit): Promise<T> {
if (res.status === 401 && body?.error?.code === "EAUTH") {
window.dispatchEvent(new Event(AUTH_REQUIRED_EVENT));
}
throw Object.assign(new Error(body?.error?.message || `HTTP ${res.status}`), {
// Server mode: a role refusal reads the same in every toast, translated.
const message =
body?.error?.code === "EADMIN"
? i18n.t("errors:adminOnly")
: body?.error?.message || `HTTP ${res.status}`;
throw Object.assign(new Error(message), {
code: body?.error?.code,
status: res.status,
// EGITAUTH from a lane git action: which host wants a login.
@@ -670,6 +675,14 @@ export const api = {
/** POST /api/auth/logout — ends this login. */
logout: () => request<{ ok: true }>("/auth/logout", { method: "POST" }),
},
// ───────────────────────── The account's own UI prefs (routes/me.js) ─────────────────────────
me: {
/** GET /api/me/prefs — this account's prefs (`local` in local mode). */
prefs: () => request<MePrefs>("/me/prefs"),
/** PUT /api/me/prefs — merges `prefs` (a string per key, null removes it). */
savePrefs: (prefs: Record<string, string | null>) =>
request<MePrefs>("/me/prefs", { method: "PUT", body: JSON.stringify({ prefs }) }),
},
// ───────────────────────── Server mode staff views (routes/users.js) ─────────────────────────
users: {
/** GET /api/users — every account with its recent usage (admin/analyst). */
@@ -1253,6 +1266,13 @@ export const api = {
/** POST /api/settings/quit — close every workspace terminal and stop the
* server until `agenthub start` (or a boot/login with autostart on). */
quit: () => request<{ ok: true; killed: number }>("/settings/quit", { method: "POST" }),
/** POST /api/settings/uninstall — remove AgentHub from this machine, its
* database included, in a detached worker (scripts/uninstall.js). */
uninstall: () =>
request<{ ok: true; log: string }>("/settings/uninstall", {
method: "POST",
body: JSON.stringify({ confirm: true }),
}),
/** Opt-in secret redaction of stored payloads and the folders whose hooks
* are dropped (`server/lib/privacy.js`). */
privacy: {
@@ -1265,6 +1285,19 @@ export const api = {
body: JSON.stringify(data),
}),
},
/** The daily database backup (`server/lib/db-backup.js`). */
backup: {
/** GET /api/settings/backup */
get: () => request<BackupState>("/settings/backup"),
/** PUT /api/settings/backup — any subset; an empty `dir` turns it off */
save: (data: { dir?: string; hour?: number; keep?: number }) =>
request<BackupState & { ok: boolean }>("/settings/backup", {
method: "PUT",
body: JSON.stringify(data),
}),
/** POST /api/settings/backup/run — starts one; poll `get` while `running` */
run: () => request<BackupState & { ok: boolean }>("/settings/backup/run", { method: "POST" }),
},
/** The bearer token for `POST /api/hooks/ingest-batch`. Only its hash is
* stored: `rotate` returns the new token this once, `get` never does. */
pushToken: {
@@ -4279,6 +4312,12 @@ export interface RunStartArgs {
/** A lane's tmux-backed terminal run — one per lane, id is the tmux session
* name (`agenthub-lane-<laneId>`). */
/** One account's own UI preferences (server/routes/me.js): known keys → string values. */
export interface MePrefs {
prefs: Record<string, string>;
onboardedAt: string | null;
}
export interface RunHandle {
id: string;
laneId: number | null;
@@ -4510,6 +4549,26 @@ export interface PrivacySettings {
env_owned: { redact_secrets: boolean; ignore_cwd: boolean };
}
/** GET /api/settings/backup. `files` are AgentHub's own backups in `dir`,
* newest first; `last` is this server process's last run. */
export interface BackupState {
dir: string;
hour: number;
keep: number;
running: boolean;
last: {
at: string;
ok: boolean;
file?: string;
size?: number;
ms?: number;
error?: string;
} | null;
files: { name: string; size: number; mtime: string }[];
dir_error: string | null;
env_owned: { dir: boolean; hour: boolean; keep: boolean };
}
/** GET /api/settings/push-token — whether push ingest is on; never the token. */
export interface PushTokenStatus {
enabled: boolean;