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
+23 -16
View File
@@ -74,7 +74,7 @@ import {
} from "lucide-react";
import type { LucideIcon } from "lucide-react";
import { api } from "../lib/api";
import { useAuth } from "./AuthGate";
import { useAuth, usePermissions } from "./AuthGate";
import { eventBus } from "../lib/eventBus";
import { WorkspaceTree } from "./WorkspaceTree";
import type { UpdateStatusPayload, WSMessage } from "../lib/types";
@@ -192,6 +192,8 @@ export function Sidebar({
}: SidebarProps) {
const { t } = useTranslation();
const auth = useAuth();
// Checking for (and applying) an update acts on the host: an admin's.
const canUpdate = usePermissions().isAdmin;
const asideRef = useRef<HTMLElement>(null);
const drawerOpen = Boolean(drawer?.open);
useDialog(drawerOpen, asideRef, drawer?.onClose);
@@ -468,7 +470,9 @@ export function Sidebar({
{[
...NAV_KEYS,
// Server mode: the Users page, for an admin or analyst.
...(auth.state.mode === "server" && auth.state.user && auth.state.user.role !== "member"
...(auth.state.mode === "server" &&
auth.state.user &&
auth.state.user.role !== "developer"
? [{ to: "/users", icon: UsersRound, key: "nav:users" }]
: []),
].map(({ to, icon: Icon, key }) => {
@@ -591,7 +595,7 @@ export function Sidebar({
checking,
available: updateAvailable,
failed: checkError,
onCheck: onCheckUpdates,
onCheck: canUpdate ? onCheckUpdates : undefined,
}}
onResetStats={() => {
eventCountRef.current = 0;
@@ -633,7 +637,8 @@ interface ConnectionStatusModalProps {
checking: boolean;
available: boolean;
failed: boolean;
onCheck: () => void;
/** Absent for a user who may not update (server mode, not an admin). */
onCheck?: () => void;
};
onResetStats: () => void;
}
@@ -775,18 +780,20 @@ function ConnectionStatusModal({
{update.title}
</p>
</div>
<button
type="button"
onClick={update.onCheck}
disabled={update.checking}
className="btn-ghost shrink-0 text-xs disabled:opacity-60"
>
<RefreshCw
className={`h-3.5 w-3.5 ${update.checking ? "animate-spin" : ""}`}
aria-hidden
/>
{t("nav:checkForUpdates")}
</button>
{update.onCheck && (
<button
type="button"
onClick={update.onCheck}
disabled={update.checking}
className="btn-ghost shrink-0 text-xs disabled:opacity-60"
>
<RefreshCw
className={`h-3.5 w-3.5 ${update.checking ? "animate-spin" : ""}`}
aria-hidden
/>
{t("nav:checkForUpdates")}
</button>
)}
</div>
</Section>