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 (