Add the harness, envs, per-bot models and chat attachments

- Harness: providers, model catalog synced hourly, automatic free picks,
  cooldowns, tool loop with context trimming, MCP computer tools
- Bots queue runs replies and tasks through the harness; jobs wait
  (attempt refunded) while every model is resting
- Envs: Settings > Environment, scoped general/workspace/computer,
  written to computers and redacted from tool output
- Models: Settings > Models, per-harness provider select defaulting to
  OpenRouter, and a models modal on each bot's chat (the coordinator's
  edits the defaults)
- Chat: thinking row while a reply runs, file attachments (upload,
  paste, drop) that replies read as images, audio and document text
- Fix media uploads failing after a user's first file, and private
  files being served from the public media path

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-19 03:38:38 +01:00
co-authored by Claude Opus 5.5
parent d49f524fe8
commit 6a2665dae9
223 changed files with 12342 additions and 162 deletions
@@ -1,7 +1,10 @@
import { useContext, useState } from "react";
import Stack from "@moduletrace/twui/layout/Stack";
import type { BUN_SQLITE_COBBLE_BOT_MESSAGES } from "@/db/types/db";
import type { BotMessageWithAttachments } from "@/src/types";
import { AppContext } from "@/src/pages/__root";
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
import useLiveBots from "@/src/hooks/use-live-bots";
import useServerEvent from "@/src/hooks/use-server-event";
import ConversationHeader from "../(partials)/conversation-header";
import MessageThread from "../(partials)/message-thread";
import Composer from "../(partials)/composer";
@@ -13,20 +16,47 @@ export default function Conversation() {
const { pageProps } = useContext(AppContext);
const bot = pageProps?.bot;
const [messages, setMessages] = useState<BUN_SQLITE_COBBLE_BOT_MESSAGES[]>(
const [messages, setMessages] = useState<BotMessageWithAttachments[]>(
pageProps?.bot_messages || [],
);
if (!bot?.id) return null;
const bots = useLiveBots({ bots: pageProps?.bots || [], open_bot_id: bot?.id || undefined });
const live_bot = bots.find((b) => b.id == bot?.id) || bot;
useServerEvent({
event: "server:bot-message",
onData: (message: BotMessageWithAttachments) => {
if (message.bot_id != bot?.id) return;
setMessages((prev) =>
prev.some((m) => m.id == message.id) ? prev : [...prev, message],
);
if (message.author == "bot") {
fetchApi(`/api/admin/bots/mark-read`, {
method: "POST",
body: { bot_id: message.bot_id },
}).catch(() => {});
}
},
});
if (!bot?.id || !live_bot) return null;
return (
<Stack className="grow min-w-0 h-full gap-0! items-stretch">
<ConversationHeader bot={bot} bots={pageProps?.bots || []} />
<MessageThread bot={bot} messages={messages} />
<ConversationHeader
bot={live_bot}
bots={bots}
harnesses={pageProps?.bot_harness_overview?.harnesses}
/>
<MessageThread bot={live_bot} messages={messages} />
<Composer
bot={bot}
onSent={({ message }) =>
setMessages((prev) => [...prev, message])
setMessages((prev) =>
prev.some((m) => m.id == message.id) ? prev : [...prev, message],
)
}
/>
</Stack>