Show a newly created bot in the rail at once
The rail was built from page props and only knew bots it already had, so a bot the coordinator created did not appear until a reload. create_bot and add_bot_to_workspace now push server:bot-created (BotCreatedData) to everyone who sees the bot: its rail row, the workspaces it is in, and whether it belongs in that user's own chat list. useLiveBots inserts it, after the pinned coordinator outside a workspace and only when the open workspace is one it joined. liveRelay pushes created bots too, so a bot added by another process still shows.
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BUN_SQLITE_COBBLE_BOTS } from "@/db/types/db";
|
||||
import type { BotWithLastMessage } from "@/src/types";
|
||||
import grabBotAudience from "@/src/functions/backend/db/access/grab-bot-audience";
|
||||
import isBotInOwnChat from "@/src/functions/backend/db/bots/is-bot-in-own-chat";
|
||||
import { sendToUser } from "@/src/websocket/(functions)/send-to-user";
|
||||
|
||||
type Params = {
|
||||
bot: BUN_SQLITE_COBBLE_BOTS;
|
||||
/**
|
||||
* Only these users (the connected ones, for the live relay); else
|
||||
* everyone who sees the bot.
|
||||
*/
|
||||
users?: Set<string>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Pushes `server:bot-created` to everyone who sees a bot that just joined
|
||||
* the crew or a workspace: its rail row, the workspaces it's in, and
|
||||
* whether it also belongs in that user's own chat list.
|
||||
*/
|
||||
export default async function pushBotCreated({ bot, users }: Params) {
|
||||
if (!bot.id) return;
|
||||
|
||||
const bot_id = Number(bot.id);
|
||||
|
||||
const workspace_ids = (
|
||||
(
|
||||
await BunSQLite.sql<{ workspace_id: number }>({
|
||||
sql: `SELECT workspace_id FROM workspace_bots WHERE bot_id = ?`,
|
||||
values: [bot_id],
|
||||
})
|
||||
).payload || []
|
||||
).map((row) => Number(row.workspace_id));
|
||||
|
||||
const data: BotWithLastMessage = {
|
||||
...bot,
|
||||
unread: 0,
|
||||
last_activity_at: null,
|
||||
last_message_text: null,
|
||||
last_message_at: null,
|
||||
};
|
||||
|
||||
for (const user_id of await grabBotAudience({ bot_id })) {
|
||||
if (users && !users.has(String(user_id))) continue;
|
||||
|
||||
sendToUser({
|
||||
user_id,
|
||||
data: {
|
||||
event: "server:bot-created",
|
||||
data: {
|
||||
bot: data,
|
||||
workspace_ids,
|
||||
in_own_chat: await isBotInOwnChat({ bot_id, user_id }),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import chatBotsSql from "@/src/utils/crew-access/chat-bots-sql";
|
||||
import grabCrewRole from "../access/grab-crew-role";
|
||||
|
||||
type Params = {
|
||||
bot_id: number | string;
|
||||
user_id: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether a bot belongs in a user's own chat list (`chatBotsSql`), as
|
||||
* opposed to only a workspace they're in. Used to tell a client which
|
||||
* rail a newly created bot shows up in.
|
||||
*/
|
||||
export default async function isBotInOwnChat({ bot_id, user_id }: Params) {
|
||||
const access = chatBotsSql({ role: await grabCrewRole({ user_id }), user_id });
|
||||
|
||||
const res = await BunSQLite.sql<{ present: number }>({
|
||||
sql: `SELECT 1 AS present FROM bots b WHERE b.id = ? AND ${access.sql} LIMIT 1`,
|
||||
values: [bot_id, ...access.values],
|
||||
});
|
||||
|
||||
return Boolean(res.payload?.[0]?.present);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import { z } from "zod";
|
||||
import type { BUN_SQLITE_COBBLE_BOTS, BUN_SQLITE_COBBLE_WORKSPACES } from "@/db/types/db";
|
||||
import pushBotCreated from "@/src/bots-queue/push-bot-created";
|
||||
import addBotToWorkspace from "@/src/functions/backend/db/workspaces/add-bot-to-workspace";
|
||||
import type { HarnessTool } from "../../types";
|
||||
|
||||
@@ -74,6 +75,8 @@ export default function addBotToWorkspaceTool({
|
||||
bot_id: worker.id,
|
||||
});
|
||||
|
||||
await pushBotCreated({ bot: worker });
|
||||
|
||||
return `${worker.name} joined ${workspace.name}.`;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import { z } from "zod";
|
||||
import type { BUN_SQLITE_COBBLE_BOTS, BUN_SQLITE_COBBLE_WORKSPACES } from "@/db/types/db";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import pushBotCreated from "@/src/bots-queue/push-bot-created";
|
||||
import createBot from "@/src/functions/backend/db/bots/create-bot";
|
||||
import addBotToWorkspace from "@/src/functions/backend/db/workspaces/add-bot-to-workspace";
|
||||
import pickFreeHue from "@/src/utils/pick-free-hue";
|
||||
@@ -81,6 +82,18 @@ export default function createBotTool({
|
||||
await addBotToWorkspace({ user_id, workspace_id: target.id, bot_id });
|
||||
}
|
||||
|
||||
const created =
|
||||
(
|
||||
await BunSQLite.sql<BUN_SQLITE_COBBLE_BOTS>({
|
||||
sql: `SELECT * FROM bots WHERE id = ?`,
|
||||
values: [bot_id],
|
||||
})
|
||||
).payload?.[0] || null;
|
||||
|
||||
if (created) {
|
||||
await pushBotCreated({ bot: created });
|
||||
}
|
||||
|
||||
const where = target?.name ? ` and joins ${target.name}` : "";
|
||||
|
||||
return `${args.name} is on the crew (bot #${bot_id})${where}.${target ? "" : " It has no workspace yet: put it in one with add_bot_to_workspace, or give it a computer with assign_computer."}`;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import type { BUN_SQLITE_COBBLE_BOT_MESSAGES } from "@/db/types/db";
|
||||
import type { BotUpdatedData, BotWithLastMessage } from "@/src/types";
|
||||
import type { BotCreatedData, BotUpdatedData, BotWithLastMessage } from "@/src/types";
|
||||
import useServerEvent from "./use-server-event";
|
||||
|
||||
type Params = {
|
||||
@@ -23,6 +23,32 @@ type Params = {
|
||||
export default function useLiveBots({ bots, open_bot_id, workspace_id = null }: Params) {
|
||||
const [live, setLive] = useState(bots);
|
||||
|
||||
useServerEvent({
|
||||
event: "server:bot-created",
|
||||
onData: (data: BotCreatedData) => {
|
||||
/**
|
||||
* In a workspace only its own crew is listed; outside one, the
|
||||
* bots in the user's own chat (a member doesn't get a bot that
|
||||
* only joined a workspace).
|
||||
*/
|
||||
const here = workspace_id
|
||||
? data.workspace_ids.includes(Number(workspace_id))
|
||||
: data.in_own_chat;
|
||||
|
||||
if (!here) return;
|
||||
|
||||
setLive((prev) => {
|
||||
if (prev.some((bot) => bot.id == data.bot.id)) return prev;
|
||||
|
||||
/** The coordinator stays pinned first; a new worker is the latest. */
|
||||
const first_worker = prev.findIndex((bot) => bot.kind != "coordinator");
|
||||
const at = first_worker < 0 ? prev.length : first_worker;
|
||||
|
||||
return [...prev.slice(0, at), data.bot, ...prev.slice(at)];
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
useServerEvent({
|
||||
event: "server:bot-updated",
|
||||
onData: (data: BotUpdatedData) =>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AppData } from "@/src/data/app-data";
|
||||
import { logError } from "@/src/functions/backend/logger";
|
||||
import grabConnectedUserIds from "@/src/websocket/(functions)/grab-connected-user-ids";
|
||||
import relayChangedBots from "./relay-changed-bots";
|
||||
import relayCreatedBots from "./relay-created-bots";
|
||||
import relayNewBotMessages from "./relay-new-bot-messages";
|
||||
import relayNewJobUpdates from "./relay-new-job-updates";
|
||||
import type { LiveRelayCursor } from "./types";
|
||||
@@ -50,7 +51,10 @@ export default async function liveRelay() {
|
||||
* Overlap one interval, so a change written in the same
|
||||
* millisecond as the last look isn't missed.
|
||||
*/
|
||||
await relayChangedBots({ since: cursor.bots_at - AppData.LiveRelayPollMs, users });
|
||||
const bots_since = cursor.bots_at - AppData.LiveRelayPollMs;
|
||||
|
||||
await relayCreatedBots({ since: bots_since, users });
|
||||
await relayChangedBots({ since: bots_since, users });
|
||||
cursor.bots_at = now;
|
||||
} catch (error) {
|
||||
await logError({ title: `Live relay failed`, error }).catch(() => {});
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BUN_SQLITE_COBBLE_BOTS } from "@/db/types/db";
|
||||
import pushBotCreated from "@/src/bots-queue/push-bot-created";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* Unix ms: bots created since then are pushed.
|
||||
*/
|
||||
since: number;
|
||||
users: Set<string>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Pushes `server:bot-created` for every bot created since `since` to the
|
||||
* connected users who see it (`pushBotCreated`), so a bot another process
|
||||
* added still shows in the rail. Pushes are idempotent, so overlapping
|
||||
* windows only repeat them.
|
||||
*/
|
||||
export default async function relayCreatedBots({ since, users }: Params) {
|
||||
if (!users.size) return;
|
||||
|
||||
const bots =
|
||||
(
|
||||
await BunSQLite.sql<BUN_SQLITE_COBBLE_BOTS>({
|
||||
sql: `SELECT * FROM bots WHERE created_at > ?`,
|
||||
values: [since],
|
||||
})
|
||||
).payload || [];
|
||||
|
||||
for (const bot of bots) {
|
||||
await pushBotCreated({ bot, users });
|
||||
}
|
||||
}
|
||||
@@ -124,6 +124,16 @@ export type BotJobUpdateData = BUN_SQLITE_COBBLE_BOTS_QUEUE_UPDATES & {
|
||||
session_id: number | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* `server:bot-created` data: a bot the user can now see, the workspaces
|
||||
* it's in, and whether it belongs in their own chat list (the main rail).
|
||||
*/
|
||||
export type BotCreatedData = {
|
||||
bot: BotWithLastMessage;
|
||||
workspace_ids: number[];
|
||||
in_own_chat: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* `server:bot-updated` data: the bot's new face and status line, and its
|
||||
* active task (null when there is none).
|
||||
@@ -535,6 +545,11 @@ export const WebSocketEvents = [
|
||||
* `bot_messages` row)
|
||||
*/
|
||||
"server:bot-message",
|
||||
/**
|
||||
* A bot joined the crew or one of the user's workspaces (`data`:
|
||||
* `BotCreatedData`)
|
||||
*/
|
||||
"server:bot-created",
|
||||
/**
|
||||
* A bot's face or status line changed (`data`: `BotUpdatedData`)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user