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:
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import pickFreeHue from "./pick-free-hue";
|
||||
|
||||
describe("pickFreeHue", () => {
|
||||
test("takes the first hue nobody has", () => {
|
||||
expect(pickFreeHue({ taken: [] })).toBe("ember");
|
||||
expect(pickFreeHue({ taken: ["ember", null, "amber"] })).toBe("moss");
|
||||
});
|
||||
|
||||
test("falls back to the least used hue", () => {
|
||||
const all = ["ember", "amber", "moss", "teal", "cobalt", "violet", "rose"];
|
||||
expect(pickFreeHue({ taken: [...all, ...all.filter((h) => h != "teal")] })).toBe("teal");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { BotHues, type BotHue } from "@/src/dict/bot-dict";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* Hues the crew already has.
|
||||
*/
|
||||
taken: (string | null | undefined)[];
|
||||
};
|
||||
|
||||
/**
|
||||
* The first hue nobody on the crew has yet, else the least used one, so a
|
||||
* new bot is told apart at a glance.
|
||||
*/
|
||||
export default function pickFreeHue({ taken }: Params): BotHue {
|
||||
const counts = new Map<string, number>(BotHues.map((hue) => [hue.value, 0]));
|
||||
|
||||
for (const hue of taken) {
|
||||
if (hue && counts.has(hue)) counts.set(hue, counts.get(hue)! + 1);
|
||||
}
|
||||
|
||||
return [...BotHues].sort((a, b) => counts.get(a.value)! - counts.get(b.value)!)[0]!.value;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { BotWithLastMessage } from "@/src/types";
|
||||
import pickRailBots from "./pick-rail-bots";
|
||||
|
||||
const bot = (id: number, extra: Partial<BotWithLastMessage> = {}): BotWithLastMessage => ({
|
||||
id,
|
||||
name: `Bot ${id}`,
|
||||
face: "idle",
|
||||
...extra,
|
||||
});
|
||||
|
||||
describe("pickRailBots", () => {
|
||||
test("lists the most recent up to the limit and hides the rest", () => {
|
||||
const { shown, hidden } = pickRailBots({ workers: [bot(1), bot(2), bot(3)], limit: 2 });
|
||||
|
||||
expect(shown.map((b) => b.id)).toEqual([1, 2]);
|
||||
expect(hidden.map((b) => b.id)).toEqual([3]);
|
||||
});
|
||||
|
||||
test("keeps an older bot that's open, unread or needs you", () => {
|
||||
const { shown, hidden } = pickRailBots({
|
||||
workers: [bot(1), bot(2), bot(3, { unread: 1 }), bot(4, { face: "needs-you" }), bot(5), bot(6)],
|
||||
selected_id: 5,
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
expect(shown.map((b) => b.id)).toEqual([1, 3, 4, 5]);
|
||||
expect(hidden.map((b) => b.id)).toEqual([2, 6]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { BotWithLastMessage } from "@/src/types";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* The worker bots, latest activity first.
|
||||
*/
|
||||
workers: BotWithLastMessage[];
|
||||
/**
|
||||
* The open conversation's bot: always listed.
|
||||
*/
|
||||
selected_id?: number;
|
||||
limit: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Which workers the rail lists: the `limit` most recently active, plus any
|
||||
* older one that's open, unread or needs the user, so nothing that wants
|
||||
* attention hides behind "See all bots". The rest go to `hidden`, in the
|
||||
* same order.
|
||||
*/
|
||||
export default function pickRailBots({ workers, selected_id, limit }: Params) {
|
||||
const shown = workers.filter(
|
||||
(bot, index) =>
|
||||
index < limit || bot.id == selected_id || Boolean(bot.unread) || bot.face == "needs-you",
|
||||
);
|
||||
const hidden = workers.filter((bot) => !shown.includes(bot));
|
||||
|
||||
return { shown, hidden };
|
||||
}
|
||||
Reference in New Issue
Block a user