From 9af5323530c25e58482ba1fd693a9e0cc8534887 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Thu, 22 Oct 2026 08:30:51 +0100 Subject: [PATCH] Square off the rail header and add a back button The rail header now sits in the standard 64px row with a hairline under it, matching the pane headers, and shows the full logo off the chat pages instead of the bare stone. A back button joins the conversation and settings headers (workspace pages only for the conversation). The rail's own back link and the dividers around it are switched off for now. Also renames the bot and workspace Delete group to Danger Zone, lifts the step budget to 50, and lets Logo render without its home link. --- src/components/general/back-button.tsx | 21 +++++++++ src/components/general/logo.tsx | 34 +++++++++------ src/data/app-data.ts | 5 ++- src/dict/settings-record-sections-dict.ts | 4 +- .../backend/storage/resolve-storage-path.ts | 43 ++++++++++++++----- .../bots/(partials)/rail-back-link.tsx | 2 + src/layouts/bots/(partials)/rail-header.tsx | 34 +++++++++++++-- .../bots/(partials)/rail-pages-list.tsx | 15 +++++-- .../bots/(partials)/rail-settings-aside.tsx | 9 ++-- .../bots/(partials)/rail-settings-list.tsx | 2 +- .../bots/(partials)/rail-workspace-list.tsx | 25 ++++++++--- src/layouts/main/(sections)/Header.tsx | 2 +- src/pages/__root.tsx | 3 -- .../(partials)/conversation-header.tsx | 4 ++ .../(partials)/settings-pane-header.tsx | 24 +++++++++-- .../[bot_id]/(partials)/bot-delete-group.tsx | 6 +-- .../(partials)/workspace-delete-group.tsx | 22 +++++++--- 17 files changed, 194 insertions(+), 61 deletions(-) create mode 100644 src/components/general/back-button.tsx diff --git a/src/components/general/back-button.tsx b/src/components/general/back-button.tsx new file mode 100644 index 0000000..8626d62 --- /dev/null +++ b/src/components/general/back-button.tsx @@ -0,0 +1,21 @@ +import Button from "@moduletrace/twui/layout/Button"; +import { ChevronLeft, type LucideIcon } from "lucide-react"; + +/** + * Back Button + */ +export default function BackButton() { + return ( + + ); +} diff --git a/src/components/general/logo.tsx b/src/components/general/logo.tsx index 20baeea..26ac8cb 100644 --- a/src/components/general/logo.tsx +++ b/src/components/general/logo.tsx @@ -13,6 +13,7 @@ type Props = { * Only `className` applies: it sets the wordmark colour. */ text_props?: ComponentProps; + no_link?: boolean; }; /** @@ -22,21 +23,30 @@ export default function Logo({ icon_size = 36, text_props, text_wrapper_props, + no_link, }: Props) { + const content = ( + + + + + + + ); + + if (no_link) { + return content; + } + return ( - - - - - - + {content} ); } diff --git a/src/data/app-data.ts b/src/data/app-data.ts index f494144..e2a88b5 100644 --- a/src/data/app-data.ts +++ b/src/data/app-data.ts @@ -240,7 +240,7 @@ export const AppData = { /** * Model turns in one text run before it must stop and report. */ - HarnessMaxToolSteps: 30, + HarnessMaxToolSteps: 50, /** * Longest tool result handed back to the model. */ @@ -462,7 +462,8 @@ export const AppData = { /** * What a speech model reads when a voice is tried in the model settings. */ - VoicePreviewText: "Hi there. This is how I'll sound when I read my replies aloud.", + VoicePreviewText: + "Hi there. This is how I'll sound when I read my replies aloud.", /** * Largest picture a bot or a workspace can have. */ diff --git a/src/dict/settings-record-sections-dict.ts b/src/dict/settings-record-sections-dict.ts index 14eff17..8476719 100644 --- a/src/dict/settings-record-sections-dict.ts +++ b/src/dict/settings-record-sections-dict.ts @@ -41,7 +41,7 @@ export const SettingsRecordSections: Record< { title: "Models", url: "#models", Icon: CpuAi }, { title: "Workspaces", url: "#workspaces", Icon: Layers }, { title: "Computers", url: "#computers", Icon: Monitor }, - { title: "Delete", url: "#delete", Icon: Trash2 }, + { title: "Danger Zone", url: "#danger-zone", Icon: CircleAlert }, ], workspaces: [ { title: "Picture", url: "#picture", Icon: Image }, @@ -49,7 +49,7 @@ export const SettingsRecordSections: Record< { title: "Bots", url: "#bots", Icon: Bot }, { title: "Computers", url: "#computers", Icon: Monitor }, { title: "Models", url: "#models", Icon: CpuAi }, - { title: "Delete", url: "#delete", Icon: Trash2 }, + { title: "Danger Zone", url: "#danger-zone", Icon: CircleAlert }, ], users: [ { title: "Overview", url: "#overview", Icon: Info }, diff --git a/src/functions/backend/storage/resolve-storage-path.ts b/src/functions/backend/storage/resolve-storage-path.ts index 080af20..b149ce8 100644 --- a/src/functions/backend/storage/resolve-storage-path.ts +++ b/src/functions/backend/storage/resolve-storage-path.ts @@ -3,6 +3,8 @@ import path from "node:path"; import { AppData } from "@/src/data/app-data"; import AppError from "@/src/utils/app-error"; +const empty_folder_text = `Empty Folder`; + type Params = { /** * The volume's absolute root (`resolveStorageSpec`'s `root`), already @@ -48,7 +50,10 @@ export type ResolvedStoragePath = { * the bot's tools); a caller that needs an existing file checks it * afterwards. */ -export default function resolveStoragePath({ root, path: requested }: Params): ResolvedStoragePath { +export default function resolveStoragePath({ + root, + path: requested, +}: Params): ResolvedStoragePath { const resolved_root = path.resolve(root); const raw = (requested || "").trim().replace(/\\/g, "/"); @@ -57,19 +62,26 @@ export default function resolveStoragePath({ root, path: requested }: Params): R } if (raw.startsWith("/") || /^[a-zA-Z]:/.test(raw)) { - throw new AppError(`Use a path inside the folder, not an absolute one.`); + throw new AppError( + `Use a path inside the folder, not an absolute one.`, + ); } - const segments = raw.split("/").map((segment) => segment.trim()).filter(Boolean); + const segments = raw + .split("/") + .map((segment) => segment.trim()) + .filter(Boolean); for (const segment of segments) { if (segment == "." || segment == "..") { - throw new AppError(`That path isn't valid: it must stay inside the folder.`); + throw new AppError(empty_folder_text); } } if (segments.length > AppData.BotFilePathMaxDepth) { - throw new AppError(`That path is too deep: at most ${AppData.BotFilePathMaxDepth} folders.`); + throw new AppError( + `That path is too deep: at most ${AppData.BotFilePathMaxDepth} folders.`, + ); } const relative = segments.join("/"); @@ -81,7 +93,7 @@ export default function resolveStoragePath({ root, path: requested }: Params): R const absolute = path.join(resolved_root, ...segments); if (!isInsideRoot({ root: resolved_root, absolute })) { - throw new AppError(`That path isn't valid: it must stay inside the folder.`); + throw new AppError(empty_folder_text); } guardRealPath({ root: resolved_root, absolute }); @@ -99,7 +111,9 @@ export default function resolveStoragePath({ root, path: requested }: Params): R * symlink (or any other reason). */ function guardRealPath({ root, absolute }: { root: string; absolute: string }) { - const real_root = existsSync(root) ? realpathSync(root) : path.resolve(root); + const real_root = existsSync(root) + ? realpathSync(root) + : path.resolve(root); let probe = absolute; while (!existsSync(probe)) { @@ -113,16 +127,25 @@ function guardRealPath({ root, absolute }: { root: string; absolute: string }) { const real_probe = realpathSync(probe); if (!isInsideRoot({ root: real_root, absolute: real_probe })) { - throw new AppError(`That path isn't valid: it must stay inside the folder.`); + throw new AppError(empty_folder_text); } } /** * Whether a path is inside a root, before it exists. */ -export function isInsideRoot({ root, absolute }: { root: string; absolute: string }) { +export function isInsideRoot({ + root, + absolute, +}: { + root: string; + absolute: string; +}) { const resolved_root = path.resolve(root); const resolved = path.resolve(absolute); - return resolved == resolved_root || resolved.startsWith(`${resolved_root}${path.sep}`); + return ( + resolved == resolved_root || + resolved.startsWith(`${resolved_root}${path.sep}`) + ); } diff --git a/src/layouts/bots/(partials)/rail-back-link.tsx b/src/layouts/bots/(partials)/rail-back-link.tsx index f066bec..b4523e3 100644 --- a/src/layouts/bots/(partials)/rail-back-link.tsx +++ b/src/layouts/bots/(partials)/rail-back-link.tsx @@ -17,6 +17,8 @@ export default function RailBackLink({ title = `Back to ${SiteData["SiteSlug"]}`, href = AdminHomePath, }: Props) { + return null; + return ( + - - + {pageProps?.url?.pathname.startsWith(`/admin/bots/`) ? ( + <> + + + + ) : ( + <> + + + )} {show_add ? : null} diff --git a/src/layouts/bots/(partials)/rail-pages-list.tsx b/src/layouts/bots/(partials)/rail-pages-list.tsx index 823d14f..37eeb03 100644 --- a/src/layouts/bots/(partials)/rail-pages-list.tsx +++ b/src/layouts/bots/(partials)/rail-pages-list.tsx @@ -17,17 +17,24 @@ type Props = { export default function RailPagesList({ pathname }: Props) { const { pageProps } = useContext(AppContext); const current = pathname?.replace(/\/$/, ""); - const is_super_admin = isPassThroughUser({ user_types: pageProps?.user_types }); + const is_super_admin = isPassThroughUser({ + user_types: pageProps?.user_types, + }); return ( <> - - {RailPages.filter((page) => is_super_admin || !page.super_admin_only).map((page) => ( + {/* */} + {RailPages.filter( + (page) => is_super_admin || !page.super_admin_only, + ).map((page) => ( ))} diff --git a/src/layouts/bots/(partials)/rail-settings-aside.tsx b/src/layouts/bots/(partials)/rail-settings-aside.tsx index 70eec7e..180fa93 100644 --- a/src/layouts/bots/(partials)/rail-settings-aside.tsx +++ b/src/layouts/bots/(partials)/rail-settings-aside.tsx @@ -64,7 +64,8 @@ export default function RailSettingsAside({ pathname }: Props) { ? Boolean(pageProps?.bot_harness_overview?.harnesses?.length) : record.kind == "workspaces" ? Boolean( - pageProps?.workspace_harness_overview?.harnesses?.length, + pageProps?.workspace_harness_overview?.harnesses + ?.length, ) : true; } @@ -101,9 +102,7 @@ export default function RailSettingsAside({ pathname }: Props) { title = workspace.name || `Workspace ${workspace.id}`; subtitle = workspace.description || "A workspace"; - avatar = ( - - ); + avatar = ; } else if (record.kind == "users") { const person = pageProps?.target_person; @@ -139,7 +138,7 @@ export default function RailSettingsAside({ pathname }: Props) { return ( <> - + {/* */} - + {/* */} {SettingsPages.filter((page) => page.can_view ? page.can_view({ user_types: pageProps?.user_types }) diff --git a/src/layouts/bots/(partials)/rail-workspace-list.tsx b/src/layouts/bots/(partials)/rail-workspace-list.tsx index 87b2bb8..ec7a8cd 100644 --- a/src/layouts/bots/(partials)/rail-workspace-list.tsx +++ b/src/layouts/bots/(partials)/rail-workspace-list.tsx @@ -29,7 +29,12 @@ type Props = { * page), the coordinator working in it, then the workspace's bots. Every * conversation here runs with the workspace's context. */ -export default function RailWorkspaceList({ workspace, bots, selected_id, on_workspace_page }: Props) { +export default function RailWorkspaceList({ + workspace, + bots, + selected_id, + on_workspace_page, +}: Props) { const { openLinkBots } = useContext(AddFlowContext); const coordinator = bots.find((bot) => bot.kind === "coordinator"); const workers = bots.filter((bot) => bot.kind !== "coordinator"); @@ -38,11 +43,16 @@ export default function RailWorkspaceList({ workspace, bots, selected_id, on_wor return ( <> - - + {/* */} + {coordinator ? ( ) : null} } + action={ + + } title="Workspace bots" workers={workers} selected_id={on_workspace_page ? undefined : selected_id} diff --git a/src/layouts/main/(sections)/Header.tsx b/src/layouts/main/(sections)/Header.tsx index 0fe036c..18aee97 100755 --- a/src/layouts/main/(sections)/Header.tsx +++ b/src/layouts/main/(sections)/Header.tsx @@ -53,4 +53,4 @@ export default function Header() { ); -} \ No newline at end of file +} diff --git a/src/pages/__root.tsx b/src/pages/__root.tsx index 7b45f51..ac00493 100755 --- a/src/pages/__root.tsx +++ b/src/pages/__root.tsx @@ -79,9 +79,6 @@ export default function App({ return ( + {is_workspace ? : null}

diff --git a/src/pages/admin/settings/(partials)/settings-pane-header.tsx b/src/pages/admin/settings/(partials)/settings-pane-header.tsx index 99393fc..ba1b18b 100644 --- a/src/pages/admin/settings/(partials)/settings-pane-header.tsx +++ b/src/pages/admin/settings/(partials)/settings-pane-header.tsx @@ -4,6 +4,8 @@ import Stack from "@moduletrace/twui/layout/Stack"; import H1 from "@moduletrace/twui/layout/H1"; import Span from "@moduletrace/twui/layout/Span"; import RailDrawerButton from "@/src/layouts/bots/(partials)/rail-drawer-button"; +import BackButton from "@/src/components/general/back-button"; +import { twMerge } from "tailwind-merge"; type Props = { title: string; @@ -25,13 +27,29 @@ export default function SettingsPaneHeader({ action, }: Props) { return ( - + + -

+

{title}

- + {description}
diff --git a/src/pages/admin/settings/bots/[bot_id]/(partials)/bot-delete-group.tsx b/src/pages/admin/settings/bots/[bot_id]/(partials)/bot-delete-group.tsx index 192cc49..bc723b2 100644 --- a/src/pages/admin/settings/bots/[bot_id]/(partials)/bot-delete-group.tsx +++ b/src/pages/admin/settings/bots/[bot_id]/(partials)/bot-delete-group.tsx @@ -45,9 +45,9 @@ export default function BotDeleteGroup({ bot }: Props) { return ( - + ); }