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:
@@ -7,7 +7,7 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a provider and its models. Harnesses (default and per bot) that
|
||||
* Removes a provider and its models. Harnesses (default, per bot and per workspace) that
|
||||
* picked one of its models, or were limited to it, go back to automatic
|
||||
* on any provider.
|
||||
*/
|
||||
@@ -18,7 +18,7 @@ export default async function deleteHarnessProvider({ provider_id }: Params) {
|
||||
throw new AppError(`Provider not found.`);
|
||||
}
|
||||
|
||||
for (const table of ["harness_settings", "bot_harness_settings"]) {
|
||||
for (const table of ["harness_settings", "bot_harness_settings", "workspace_harness_settings"]) {
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE ${table} SET model_id = NULL, provider_id = NULL, updated_at = ?
|
||||
WHERE provider_id = ? OR model_id IN (SELECT id FROM harness_models WHERE provider_id = ?)`,
|
||||
|
||||
@@ -8,23 +8,44 @@ type Params = {
|
||||
/**
|
||||
* A worker bot the caller has checked belongs to the session user.
|
||||
*/
|
||||
bot_id: number;
|
||||
bot_id?: number;
|
||||
/**
|
||||
* A workspace the caller has checked belongs to the session user. Then
|
||||
* the settings shown are the workspace's own, over what it inherits:
|
||||
* the bot's (when given) or the defaults.
|
||||
*/
|
||||
workspace_id?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* A worker bot's model settings page: per harness, the bot's own setting
|
||||
* (or the default it inherits), the models it can pick, what it runs on
|
||||
* now, and what the default runs on.
|
||||
* A worker bot's (or a workspace's) model settings: per harness, its own
|
||||
* setting (or what it inherits), the models it can pick, what it runs on
|
||||
* now, and what it would run on without its own setting.
|
||||
*/
|
||||
export default async function grabBotHarnessOverview({
|
||||
bot_id,
|
||||
workspace_id,
|
||||
}: Params): Promise<BotHarnessOverviewPayload> {
|
||||
/**
|
||||
* In a workspace, "the default" is what a run there gets without the
|
||||
* workspace's own setting: the bot's, else the defaults.
|
||||
*/
|
||||
const inherited_bot_id = workspace_id ? bot_id : undefined;
|
||||
|
||||
const [own, defaults] = await Promise.all([
|
||||
Promise.all(
|
||||
Harnesses.map((harness) => grabHarnessSetting({ harness: harness.value, bot_id })),
|
||||
Harnesses.map((harness) =>
|
||||
grabHarnessSetting({
|
||||
harness: harness.value,
|
||||
bot_id: workspace_id ? undefined : bot_id,
|
||||
workspace_id,
|
||||
}),
|
||||
),
|
||||
),
|
||||
Promise.all(
|
||||
Harnesses.map((harness) => grabHarnessSetting({ harness: harness.value })),
|
||||
Harnesses.map((harness) =>
|
||||
grabHarnessSetting({ harness: harness.value, bot_id: inherited_bot_id }),
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -38,11 +59,18 @@ export default async function grabBotHarnessOverview({
|
||||
harnesses: await Promise.all(
|
||||
own.map(async (setting, index) => {
|
||||
const [item, default_item] = await Promise.all([
|
||||
grabHarnessOverviewItem({ harness: setting.harness, setting, entries, bot_id }),
|
||||
grabHarnessOverviewItem({
|
||||
harness: setting.harness,
|
||||
setting,
|
||||
entries,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
}),
|
||||
grabHarnessOverviewItem({
|
||||
harness: setting.harness,
|
||||
setting: defaults[index]!,
|
||||
entries,
|
||||
bot_id: inherited_bot_id,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ type Params = {
|
||||
* Resolve what runs now for this bot instead of the default.
|
||||
*/
|
||||
bot_id?: number | string;
|
||||
workspace_id?: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -25,10 +26,11 @@ export default async function grabHarnessOverviewItem({
|
||||
setting,
|
||||
entries,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
}: Params): Promise<HarnessOverview> {
|
||||
const meta = Harnesses.find((h) => h.value == harness)!;
|
||||
const candidates = setting.enabled
|
||||
? await resolveHarnessCandidates({ harness, bot_id }).catch(() => [])
|
||||
? await resolveHarnessCandidates({ harness, bot_id, workspace_id }).catch(() => [])
|
||||
: [];
|
||||
const first = candidates[0];
|
||||
const serving = entries.filter(({ model }) => modelServesHarness({ model, harness }));
|
||||
|
||||
@@ -2,6 +2,7 @@ import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type {
|
||||
BUN_SQLITE_COBBLE_BOT_HARNESS_SETTINGS,
|
||||
BUN_SQLITE_COBBLE_HARNESS_SETTINGS,
|
||||
BUN_SQLITE_COBBLE_WORKSPACE_HARNESS_SETTINGS,
|
||||
} from "@/db/types/db";
|
||||
import type { HarnessKind } from "@/src/dict/harness-dict";
|
||||
import parseJobJson from "@/src/bots-queue/parse-job-json";
|
||||
@@ -22,18 +23,24 @@ export type HarnessSetting = {
|
||||
enabled: boolean;
|
||||
options: Record<string, unknown>;
|
||||
/**
|
||||
* `bot` when a worker bot overrides the default for this harness.
|
||||
* `workspace` when the call's workspace sets this harness, `bot` when a
|
||||
* worker bot overrides the default for it.
|
||||
*/
|
||||
source: "default" | "bot";
|
||||
source: "default" | "bot" | "workspace";
|
||||
};
|
||||
|
||||
type Params = {
|
||||
harness: HarnessKind;
|
||||
/**
|
||||
* The bot the call is for. A worker bot's own setting wins; the
|
||||
* coordinator always uses the default.
|
||||
* The bot the call is for. A worker bot's own setting wins over the
|
||||
* default; the coordinator always uses the default.
|
||||
*/
|
||||
bot_id?: number | string;
|
||||
/**
|
||||
* The workspace the call is in. Its setting wins over the bot's, for
|
||||
* the coordinator too.
|
||||
*/
|
||||
workspace_id?: number | string | null;
|
||||
};
|
||||
|
||||
type SettingRow = Pick<
|
||||
@@ -42,13 +49,23 @@ type SettingRow = Pick<
|
||||
>;
|
||||
|
||||
/**
|
||||
* A harness's setting for a call. With no row it is automatic on the
|
||||
* default provider (OpenRouter), on, with fallback. Text is always on.
|
||||
* A harness's setting for a call: the workspace's, else the bot's, else
|
||||
* the default. With no row it is automatic on the default provider
|
||||
* (OpenRouter), on, with fallback. Text is always on.
|
||||
*/
|
||||
export default async function grabHarnessSetting({
|
||||
harness,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
}: Params): Promise<HarnessSetting> {
|
||||
const workspace_row = workspace_id
|
||||
? await grabWorkspaceOverride({ harness, workspace_id })
|
||||
: undefined;
|
||||
|
||||
if (workspace_row) {
|
||||
return toSetting({ harness, row: workspace_row, source: "workspace" });
|
||||
}
|
||||
|
||||
const bot_row = bot_id
|
||||
? await grabBotOverride({ harness, bot_id })
|
||||
: undefined;
|
||||
@@ -74,6 +91,21 @@ export default async function grabHarnessSetting({
|
||||
return toSetting({ harness, row, source: "default" });
|
||||
}
|
||||
|
||||
async function grabWorkspaceOverride({
|
||||
harness,
|
||||
workspace_id,
|
||||
}: {
|
||||
harness: HarnessKind;
|
||||
workspace_id: number | string;
|
||||
}) {
|
||||
return (
|
||||
await BunSQLite.sql<BUN_SQLITE_COBBLE_WORKSPACE_HARNESS_SETTINGS>({
|
||||
sql: `SELECT * FROM workspace_harness_settings WHERE workspace_id = ? AND harness = ? LIMIT 1`,
|
||||
values: [workspace_id, harness],
|
||||
})
|
||||
).payload?.[0];
|
||||
}
|
||||
|
||||
async function grabBotOverride({
|
||||
harness,
|
||||
bot_id,
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { HarnessKind } from "@/src/dict/harness-dict";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import assertHarnessModel from "./assert-harness-model";
|
||||
import assertHarnessProvider from "./assert-harness-provider";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* A workspace the caller has checked belongs to the session user.
|
||||
*/
|
||||
workspace_id: number;
|
||||
harness: HarnessKind;
|
||||
/**
|
||||
* Drop the workspace's setting: each bot uses its own or the default
|
||||
* again.
|
||||
*/
|
||||
inherit: boolean;
|
||||
model_id: number | null;
|
||||
/**
|
||||
* Limits the harness to one provider; null: any.
|
||||
*/
|
||||
provider_id?: number | null;
|
||||
fallback: boolean;
|
||||
enabled: boolean;
|
||||
options?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves (or, with `inherit`, removes) a workspace's setting for one
|
||||
* harness, which wins over its bots' own. Same rules as the default: the
|
||||
* model must be available and fit the job, and text can't be turned off.
|
||||
*/
|
||||
export default async function saveWorkspaceHarnessSetting({
|
||||
workspace_id,
|
||||
harness,
|
||||
inherit,
|
||||
model_id,
|
||||
provider_id = null,
|
||||
fallback,
|
||||
enabled,
|
||||
options,
|
||||
}: Params) {
|
||||
if (inherit) {
|
||||
await BunSQLite.sql({
|
||||
sql: `DELETE FROM workspace_harness_settings WHERE workspace_id = ? AND harness = ?`,
|
||||
values: [workspace_id, harness],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (harness == "text" && !enabled) {
|
||||
throw new AppError(`The text model can't be turned off.`);
|
||||
}
|
||||
|
||||
if (provider_id) {
|
||||
await assertHarnessProvider({ provider_id });
|
||||
}
|
||||
|
||||
if (model_id) {
|
||||
await assertHarnessModel({ harness, model_id, provider_id });
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
const res = await BunSQLite.sql({
|
||||
sql: `INSERT INTO workspace_harness_settings (created_at, updated_at, workspace_id, harness, model_id, provider_id, fallback, enabled, options)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(workspace_id, harness) DO UPDATE SET updated_at = excluded.updated_at,
|
||||
model_id = excluded.model_id, provider_id = excluded.provider_id, fallback = excluded.fallback,
|
||||
enabled = excluded.enabled, options = excluded.options`,
|
||||
values: [
|
||||
now,
|
||||
now,
|
||||
workspace_id,
|
||||
harness,
|
||||
model_id,
|
||||
provider_id,
|
||||
fallback ? 1 : 0,
|
||||
enabled ? 1 : 0,
|
||||
JSON.stringify(options || {}),
|
||||
],
|
||||
});
|
||||
|
||||
if (!res.success) {
|
||||
throw new Error(`Couldn't save the workspace's model setting: ${res.error}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user