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:
1 parent
81ee084ff2
commit
d152ad836d
26 files changed
+836
-52
No files matched your search
@@ -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>
|
||||
))}
|
||||
|
||||
Reference in new issue
Block a user