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

+7 -4
View File
@@ -5,7 +5,7 @@ import { useEffect, useMemo, useState, useSyncExternalStore } from "react";
import { get, patch, post } from "../api";
import { promptDialog, toast } from "../dialogs";
import { fmtRelative, useT, type Key } from "../i18n";
import { navigate } from "../router";
import { navigate, openBeside } from "../router";
import { useStore } from "../store";
import type { Session } from "../types";
@@ -82,6 +82,9 @@ function SessionRow({ s, active, attention, busy, folders }: { s: Session; activ
<Icon name="pin" /> {t(s.pinned ? "sidebar.unpin" : "sidebar.pin")}
</button>
)}
<button role="menuitem" className="hide-mobile" onClick={() => (setMenu(false), openBeside(s.id))}>
<Icon name="external" /> {t("sidebar.openBeside")}
</button>
<button role="menuitem" onClick={() => (setMenu(false), void moveTo())}>
<Icon name="folder" /> {t("sidebar.moveTo")}
</button>
@@ -99,7 +102,7 @@ function SessionRow({ s, active, attention, busy, folders }: { s: Session; activ
);
}
export function Sidebar({ open, activeId, section }: { open: boolean; activeId?: string; section: string }) {
export function Sidebar({ open, activeIds, section }: { open: boolean; activeIds: string[]; section: string }) {
const t = useT();
const sessions = useStore((s) => s.sessions);
const busy = useStore((s) => s.busy);
@@ -210,7 +213,7 @@ export function Sidebar({ open, activeId, section }: { open: boolean; activeId?:
<div className="session-group">{t("sidebar.searchResults", { n: hits.length })}</div>
{hits.length === 0 && <div className="empty">{t("sidebar.noResults")}</div>}
{hits.map((h) => (
<button key={h.id} className={`session-item ${h.id === activeId ? "active" : ""}`} onClick={() => navigate(`/s/${h.id}`)}>
<button key={h.id} className={`session-item ${activeIds.includes(h.id) ? "active" : ""}`} onClick={() => navigate(`/s/${h.id}`)}>
<div className="title">
<span>{h.title || t("session.untitled")}</span>
</div>
@@ -242,7 +245,7 @@ export function Sidebar({ open, activeId, section }: { open: boolean; activeId?:
</button>
{!collapsed.has(g.id) &&
g.items.map((s) => (
<SessionRow key={s.id} s={s} active={s.id === activeId} attention={attention.has(s.id)} busy={!!busy[s.id]} folders={folderNames} />
<SessionRow key={s.id} s={s} active={activeIds.includes(s.id)} attention={attention.has(s.id)} busy={!!busy[s.id]} folders={folderNames} />
))}
</div>
))}