Split view, personal modes, budget warnings, server move script

- Up to three chats side by side (#/s/a+b+c), "Open beside" in the sidebar menu
- File manager tabs of a chat stored per user on the server (/sessions/{id}/tabs)
- Personal modes for every user (Settings → My modes, /account/modes)
- Budget meters (total, provider, images, opencode Go windows) with push warnings
  for admins at the warning share and 100 %, shown in the status overlay
- scripts/move-server.sh: move to another Docker host with bind mounts (Unraid);
  scripts/smoke-test.sh

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-09 12:06:20 +01:00
1 parent 81ee084ff2
commit d152ad836d
26 files changed
+836 -52

No files matched your search

+14 -3
View File
@@ -1,7 +1,7 @@
import { lazy, Suspense, useEffect, useState } from "react";
import { connectEvents, get, setUnauthorizedHandler } from "./api";
import { useT } from "./i18n";
import { navigate, useRoute } from "./router";
import { navigate, useRoute, closePane, MAX_PANES } from "./router";
import { applyEvent, loadSessions, resync, setState } from "./store";
import type { Info } from "./types";
import { Sidebar } from "./components/Sidebar";
@@ -80,7 +80,18 @@ export function App() {
const [section, arg] = route;
let content;
if (section === "s" && arg) content = <ChatView key={arg} sessionId={arg} onMenu={() => setMenuOpen(true)} />;
const chatIds = section === "s" && arg ? arg.split("+").filter(Boolean).slice(0, MAX_PANES) : [];
if (chatIds.length === 1) content = <ChatView key={chatIds[0]} sessionId={chatIds[0]} onMenu={() => setMenuOpen(true)} />;
else if (chatIds.length > 1)
content = (
<div className="split-view">
{chatIds.map((id) => (
<div key={id} className="split-pane">
<ChatView sessionId={id} onMenu={() => setMenuOpen(true)} onClosePane={() => closePane(id)} />
</div>
))}
</div>
);
else if (section === "settings") content = <Settings tab={arg} onMenu={() => setMenuOpen(true)} />;
else if (section === "stats") content = <Stats onMenu={() => setMenuOpen(true)} />;
else if (section === "images") content = <Images onMenu={() => setMenuOpen(true)} />;
@@ -102,7 +113,7 @@ export function App() {
return (
<div className="app">
{menuOpen && <div className="backdrop" onClick={() => setMenuOpen(false)} />}
<Sidebar open={menuOpen} activeId={section === "s" ? arg : undefined} section={section ?? ""} />
<Sidebar open={menuOpen} activeIds={chatIds} section={section ?? ""} />
<main className="main">{content}</main>
<WhatsNew version={info.version} />
<Palette />