Add workspaces, conversation sessions, app-managed computers and pictures

- Rail: Add icon with stacked form modals (bot, workspace, computer),
  coordinator, recent bots with "See all bots", Workspaces section, and
  a workspace mode listing only its crew
- Workspaces: page for context, bots (per-workspace instructions and
  memory), computers (default, link, unlink), models and delete;
  conversations scoped per bot per workspace
- Sessions: conversations split into searchable, reopenable sessions
  with carry-over summaries; sessions and new-session header icons
- Computers: created and provisioned over the docker socket, home dir
  kept at DATA_DIR/computers/<id> after delete; super admins only
- Models: workspace settings win over a bot's own, then the defaults
- Pictures for bots and workspaces; bots editable from the header
- Coordinator tools: update_workspace, set_bot_workspace_instructions

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-19 08:39:33 +01:00
co-authored by Claude Opus 5.5
parent 56daf0c462
commit 0ce61cdb49
223 changed files with 8166 additions and 585 deletions
+43 -10
View File
@@ -1,6 +1,11 @@
import type { BUN_SQLITE_COBBLE_BOTS } from "@/db/types/db";
import { AppData } from "@/src/data/app-data";
import { ChatReplyNote, CoordinatorReplyNote, CoordinatorSoloNote } from "@/src/data/harness-prompts";
import {
ChatReplyNote,
CoordinatorReplyNote,
CoordinatorSoloNote,
WorkspaceLearningNote,
} from "@/src/data/harness-prompts";
import grabBotMessages from "@/src/functions/backend/db/bots/grab-bot-messages";
import grabBotPrompt from "@/src/functions/backend/db/bots/grab-bot-prompt";
import grabBotComputers from "@/src/functions/backend/db/computers/grab-bot-computers";
@@ -8,6 +13,8 @@ import runTextLoop from "@/src/harness/text/run-text-loop";
import grabBotChatTools from "@/src/harness/tools/bot/grab-bot-chat-tools";
import botMessagesToHarness from "./bot-messages-to-harness";
import composeComputersNote from "./compose-computers-note";
import grabSessionCarryOver from "./grab-session-carry-over";
import jobSessionId from "@/src/functions/backend/db/bots-queue/job-session-id";
import loadAttachmentParts from "./load-attachment-parts";
import withTakeOverRefs from "./with-take-over-refs";
import type { BotJobContext, BotJobOutcome } from "./types";
@@ -17,11 +24,14 @@ type Params = BotJobContext & {
};
/**
* Answers the user in chat: the bot reads the recent conversation (and
* the files of the latest messages, `loadAttachmentParts`) and
* replies, knowing its computers (`composeComputersNote`), with its chat tools (memory, instructions, start_task, and for
* the coordinator delegate_task). Finishes silently when the last message
* is already the bot's, i.e. an earlier reply answered everything.
* Answers the user in chat: the bot reads the session's recent messages
* (and the files of the latest ones, `loadAttachmentParts`) and replies,
* knowing where it is (its workspace, if the session is in one), what
* happened in the session before (`grabSessionCarryOver`) and its
* computers (`composeComputersNote`), with its chat tools (memory,
* instructions, start_task, and for the coordinator delegate_task).
* Finishes silently when the last message is already the bot's, i.e. an
* earlier reply answered everything.
*/
export default async function runBotReply({
bot,
@@ -29,8 +39,11 @@ export default async function runBotReply({
signal,
report,
}: Params): Promise<BotJobOutcome> {
const session_id = await jobSessionId({ job });
const workspace_id = job.workspace_id || null;
const history = await grabBotMessages({
bot_id: bot.id,
session_id,
limit: AppData.BotReplyHistoryMessages,
});
@@ -38,8 +51,19 @@ export default async function runBotReply({
return { result: "", silent: true };
}
const prompt = await grabBotPrompt({ bot_id: bot.id, user_id: bot.user_id });
const computers = await grabBotComputers({ bot_id: bot.id, user_id: bot.user_id });
const prompt = await grabBotPrompt({ bot_id: bot.id, user_id: bot.user_id, workspace_id });
const computers = await grabBotComputers({ bot_id: bot.id, user_id: bot.user_id, workspace_id });
/**
* A summary that can't be written now (no model free) is left out
* rather than holding up the reply.
*/
const carry_over = await grabSessionCarryOver({
session_id,
bot_id: bot.id,
workspace_id,
signal,
}).catch(() => "");
const take_overs: string[] = [];
@@ -47,6 +71,8 @@ export default async function runBotReply({
bot,
user_id: bot.user_id,
computers,
session_id,
workspace_id,
message_id: job.message_id || undefined,
onTakeOver: ({ ref }) => take_overs.push(ref),
});
@@ -64,8 +90,14 @@ export default async function runBotReply({
const result = await runTextLoop({
system: [
prompt,
carry_over,
ChatReplyNote,
composeComputersNote({ computers, is_coordinator: bot.kind == "coordinator" }),
workspace_id ? WorkspaceLearningNote : "",
composeComputersNote({
computers,
is_coordinator: bot.kind == "coordinator",
in_workspace: Boolean(workspace_id),
}),
crew_note,
`Now: ${new Date().toUTCString()}.`,
]
@@ -78,6 +110,7 @@ export default async function runBotReply({
tools,
max_steps: AppData.BotReplyMaxToolSteps,
bot_id: bot.id,
workspace_id,
signal,
onStep: report,
});