Chat tabs, chat mode, own modes, compaction notice, CLI binaries again

- Terminals and file managers as tabs of a chat; chat terminals keep running on
  the server (no idle timeout) and re-attach from any device
- "Just chat" in New session and a built-in chat mode with a lean prompt and only
  web tools; custom modes with kind (coding/conversation), read-only and tool picker
- Mode selector as a dropdown
- The chat shows running compactions (automatic: tokens and share of the limit)
  and labels summaries
- Docker image builds and serves the terminal client binaries again
- Fix: atomic session updates; background title/usage writes no longer undo each
  other (auto compaction triggered one turn late)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-09 11:24:58 +01:00
1 parent 242b80605c
commit c850586a81
29 files changed
+831 -187

No files matched your search

+18 -9
View File
@@ -56,10 +56,16 @@ function icon(e: Entry) {
return <Icon name="file" className="ficon" />;
}
export function Files({ onMenu }: { onMenu: () => void }) {
/** Embedded in a chat tab: own start folder, no page header, folder kept by the caller. */
export interface FilesEmbed {
startDir: string;
onDirChange?: (dir: string) => void;
}
export function Files({ onMenu, embed }: { onMenu?: () => void; embed?: FilesEmbed }) {
const t = useT();
const roots = useFetch<{ path: string; name: string }[]>("/fs/roots").data ?? [];
const [dir, setDir] = useState<string>(() => localStorage.getItem("codecraft.filesDir") ?? "");
const [dir, setDir] = useState<string>(() => embed?.startDir ?? localStorage.getItem("codecraft.filesDir") ?? "");
const [listing, setListing] = useState<Listing>();
const [hidden, setHidden] = useState(false);
const [error, setError] = useState<string>();
@@ -79,7 +85,8 @@ export function Files({ onMenu }: { onMenu: () => void }) {
const l = await get<Listing>(`/files/list?path=${encodeURIComponent(path)}&hidden=${hidden}`);
setListing(l);
setDir(l.path);
localStorage.setItem("codecraft.filesDir", l.path);
if (embed) embed.onDirChange?.(l.path);
else localStorage.setItem("codecraft.filesDir", l.path);
setError(undefined);
} catch (e: any) {
setError(e.message);
@@ -143,7 +150,7 @@ export function Files({ onMenu }: { onMenu: () => void }) {
// "Edit file" links from other pages (e.g. automations) open the file directly.
useEffect(() => {
const target = sessionStorage.getItem("codecraft.openFile");
const target = embed ? null : sessionStorage.getItem("codecraft.openFile");
if (!target) return;
sessionStorage.removeItem("codecraft.openFile");
setDir(target.slice(0, target.lastIndexOf("/")) || "/");
@@ -432,12 +439,14 @@ export function Files({ onMenu }: { onMenu: () => void }) {
return (
<>
<div className="topbar">
<button className="ghost menu-btn" onClick={onMenu} aria-label={t("nav.menu")}>
<Icon name="menu" />
</button>
<div className={embed ? "files-embed-bar" : "topbar"}>
{!embed && (
<button className="ghost menu-btn" onClick={onMenu} aria-label={t("nav.menu")}>
<Icon name="menu" />
</button>
)}
<div className="grow">
<h1>{t("nav.files")}</h1>
{!embed && <h1>{t("nav.files")}</h1>}
<div className="path">{listing?.path}</div>
</div>
{listing?.repo && (