diff --git a/.design-sync/NOTES.md b/.design-sync/NOTES.md index e3a3880b..0dce557f 100644 --- a/.design-sync/NOTES.md +++ b/.design-sync/NOTES.md @@ -37,7 +37,19 @@ Tailwind and TypeScript). Nothing there reimplements a component. - Fixed overlays (`Dialog`, `Confirm`, `Toaster`) render inside a box with `transform: translateZ(0)`, which makes it their containing block; without it the per-story capture is 0 px tall. -- `ListAvatar` is excluded (cfg.componentSrcMap): a wrapper over `BotAvatar` that needs a bot object. +- The app screens (`Sidebar`, `CrewBots`, `BotRun`, `InboundRow`, `ApprovalCard`) read the app's + store and API. `pkg/provider.tsx` (`BotloftProvider`) wires the app's own `DaemonProvider` and + `HostProvider` to `FakeBotloft` and `FakeHost`, seeded with the crews and bots it is given, and + turns `approvals.answer` into an `onAnswer` callback. `pkg/scene.ts` builds protocol objects + (`makeCrew`, `makeBot`, `makeMessage`, `chat.*`) with the fake daemon's defaults; request + inputs and summaries follow the daemon (`crates/botloft-core/src/chat.rs` `tool_summary`, + `crates/botloftd/src/tools/browser_sites.rs`): a site request is `{site, url}` with the site as + its summary. +- Type emission covers the whole package with the repo as root; `.design-sync` is renamed to `ds` + in `dist/types/emit` (ts-morph skips dot folders), and `paths` points bare imports at + `app/node_modules` (`@types` first, or React resolves to its JS). `vite/client.d.ts` is added + for the fake daemon's `?raw` import. +- Bot colors come from `AVATAR_PALETTE` (protocol.gen.ts); previews and conventions use it. ## Known render warns @@ -45,6 +57,10 @@ Tailwind and TypeScript). Nothing there reimplements a component. Mono ships. Accepted. - `Menu` shows only its closed button: opening takes a click. - `SelectField` Disabled looks like the enabled one: the app's own control does not style disabled. +- `[RENDER_ERRORS] AbortError: Transition was skipped` on the app screens: each preview cell has + its own `BotloftProvider`, and each opens its first crew with the app's view transition + (`ui/motion.ts`); the transitions of several cells on one page cut each other off. Renders fine; + a design uses one provider. ## Re-sync risks @@ -54,5 +70,8 @@ Tailwind and TypeScript). Nothing there reimplements a component. ranges. Change both together. - The icon set follows the app's imports: an icon the app stops using disappears from `icons`. - Bot colors in previews and conventions are sample values, not tokens. -- The scope is the base pieces plus the bot badges (owner's choice on 2026-10-02). Chat rows, - approval cards and the sidebar are not in the package. +- Scope (owner's choices on 2026-10-02): the base pieces, the bot badges, and the app screens the + explainer video needs (sidebar, crew cards, a bot's turn, messages, requests). Not in it yet: + the whole crew view, the composer, the browser and screens panels, routines, settings. +- `scene.ts` mirrors protocol shapes and daemon summaries by hand; a protocol change + (`protocol.gen.ts`) or a new request kind needs it updated, or the tsc step fails. diff --git a/.design-sync/config.json b/.design-sync/config.json index 009509e0..03c3e62c 100644 --- a/.design-sync/config.json +++ b/.design-sync/config.json @@ -21,10 +21,13 @@ "SelectField": "label: string; hint?: string; options: { value: string; label: string }[]; value?: string; defaultValue?: string; onChange?: React.ChangeEventHandler; name?: string; className?: string; id?: string; style?: React.CSSProperties; disabled?: boolean; autoFocus?: boolean; ", "BotStateBadge": "/** The bot: its state and whether the owner paused it. */ bot: { state: \"offline\" | \"launching\" | \"idle\" | \"busy\" | \"needs_approval\" | \"rate_limited\" | \"auth_error\" | \"backoff\" | \"archived\"; paused: boolean }; /** A paused crew shows its stopped bots as paused. */ crewPaused?: boolean; compact?: boolean;", "ChiefBadge": "/** The crew this bot leads; only its name is read, for the tooltip. */ crew: { name: string }; /** Just the crown, for tight rows. */ compact?: boolean;", - "Menu": "/** Accessible name of the button that opens the menu. */ label: string; /** An icon from window.Botloft.icons, e.g. icons.Ellipsis. */ icon: React.ComponentType<{ size?: number; strokeWidth?: number }>; items: { label: string; icon?: React.ComponentType<{ size?: number; strokeWidth?: number }>; danger?: boolean; disabled?: boolean; /** Makes the item one of a set of choices, marked when chosen. */ checked?: boolean; onSelect: () => void }[];" - }, - "componentSrcMap": { - "ListAvatar": null + "Menu": "/** Accessible name of the button that opens the menu. */ label: string; /** An icon from window.Botloft.icons, e.g. icons.Ellipsis. */ icon: React.ComponentType<{ size?: number; strokeWidth?: number }>; items: { label: string; icon?: React.ComponentType<{ size?: number; strokeWidth?: number }>; danger?: boolean; disabled?: boolean; /** Makes the item one of a set of choices, marked when chosen. */ checked?: boolean; onSelect: () => void }[];", + "ApprovalCard": "/** A request: Botloft.chat.askCommand / askSite / suggestBot / askPlan(...).body. The card follows its toolName (command, website, suggested bot, plan) and its status (pending, allowed, denied, expired). Needs BotloftProvider around it. */ approval: any; /** A bot from Botloft.makeBot(...). */ bot: any;", + "BotRun": "/** One turn of the bot, in order: items from Botloft.chat.reply / tool / askCommand / askSite / suggestBot / askPlan / turn(bot, ...). Needs BotloftProvider around it. */ items: any[]; /** A bot from Botloft.makeBot(...). */ bot: any; /** The turn in progress: the reply being written (draft) or the working dots (working). Only a live run moves the avatar. */ live?: { draft: string; working: boolean };", + "InboundRow": "/** A message from Botloft.makeMessage({ from, to, body }). From \"owner\" it is your bubble on the right; from a bot or \"system\" it sits on the left with the sender's avatar. The sender must be among BotloftProvider bots. */ message: any; /** The bot whose chat this is: the message's recipient. */ /** A bot from Botloft.makeBot(...). */ bot: any;", + "CrewBots": "/** A crew from Botloft.makeCrew(...). */ crew: any; /** Its bots, from Botloft.makeBot({ crew, ... }); the Chief (chief: true) gets the crown. Pass the same ones to BotloftProvider. */ bots: any[]; onNewBot: () => void;", + "Sidebar": "/** No props: it lists the crews and bots given to BotloftProvider, with each bot's state and last activity, and the account at the bottom. Give it a fixed height. */ className?: never;", + "BotloftProvider": "/** Crews from Botloft.makeCrew(...). */ crews?: any[]; /** Bots from Botloft.makeBot(...). A new array updates them, e.g. to change a state over time. */ bots?: any[]; /** The bot whose chat is open: highlighted in the sidebar. */ selectedBotId?: string; /** Called when Allow or Deny is clicked on a request card. */ onAnswer?: (answer: { approvalId: string; allow: boolean; note?: string }) => void; children: React.ReactNode;" }, "overrides": { "Dialog": { @@ -44,6 +47,26 @@ "Toaster": { "cardMode": "single", "primaryStory": "ErrorNotice" + }, + "Sidebar": { + "cardMode": "single", + "viewport": "320x620" + }, + "BotRun": { + "cardMode": "column" + }, + "InboundRow": { + "cardMode": "column" + }, + "ApprovalCard": { + "cardMode": "column" + }, + "CrewBots": { + "cardMode": "column" + }, + "BotloftProvider": { + "cardMode": "single", + "viewport": "1000x560" } }, "readmeHeader": ".design-sync/conventions.md" diff --git a/.design-sync/conventions.md b/.design-sync/conventions.md index 59a9d64b..11fc1a3b 100644 --- a/.design-sync/conventions.md +++ b/.design-sync/conventions.md @@ -5,7 +5,13 @@ mascot in its own color. The look is quiet and warm: neutral surfaces, one orang ## Setup -- No provider is needed. Link `styles.css` and load `_ds_bundle.js`; components are on `window.Botloft`. +- Link `styles.css` and load `_ds_bundle.js`; components are on `window.Botloft`. The basic pieces + (Button, fields, Dialog, Callout, BotAvatar, BotStateBadge...) need nothing around them. +- **App screens need `BotloftProvider`.** `Sidebar`, `CrewBots`, `BotRun`, `InboundRow` and + `ApprovalCard` are the app's real screens and read the app's data: wrap them in one + `` per design (not one per piece). Build the data + with `makeCrew`, `makeBot`, `makeMessage` and `chat.*` (below); never write a bot object by hand. + A new `bots` array updates every screen, which is how a design animates states. - **Theme:** light by default. Put `data-theme="dark"` on a wrapper (or ``) and every token switches. Give the page `bg-canvas text-ink` so the background follows the theme. - **Language:** the components' own words (state labels, Close, Cancel) follow the browser's @@ -34,8 +40,29 @@ use `style={{ height: 360 }}` for one-off sizes. The tokens behind them are CSS variables: `--canvas`, `--panel`, `--sunken`, `--line`, `--line-strong`, `--ink`, `--ink-soft`, `--muted`, `--accent`, `--ok`, `--work`, `--warn`, `--danger`. Use `var(--accent)` in inline styles; never hard-code hex for UI colors. -Bot colors are the exception: each bot has its own (`#FF7A59` Botloft orange, `#4FC382`, `#6EA6FF`, -`#F0B24A`, `#C084FC`), passed to `BotAvatar`. +Bot colors are the exception: each bot has its own from `Botloft.AVATAR_PALETTE` (`#FF7A59` +Botloft orange, `#5EC8FF`, `#A48BFF`, `#9BE564`, `#FFC857`, `#FF7EB6`, `#3DD9C1`, `#E8D5B0`); +`makeBot` hands them out in that order. + +## Data for the app screens + +```jsx +const { makeCrew, makeBot, makeMessage, chat } = window.Botloft; +const crew = makeCrew({ name: "Research" }); +const chief = makeBot({ crew, name: "Chief", chief: true, state: "idle" }); +const scout = makeBot({ crew, name: "Scout", state: "busy", role: "Finds sources.", + activity: { kind: "tool", text: "on-device AI news", tool: "WebSearch" } }); // sidebar line +``` + +- `state`: `idle`, `busy` (working), `needs_approval`, `launching`, `rate_limited`, `backoff`, + `auth_error`, `offline` (with `paused: true` it reads Paused). +- A bot's turn is a list for `BotRun`: `chat.reply(bot, markdown)`, `chat.tool(bot, { name: + "WebSearch" | "WebFetch" | "Read" | "Write" | "Bash", summary })`, `chat.askSite(bot, url)`, + `chat.askCommand(bot, command, { explanation })`, `chat.suggestBot(chief, { name, role, model, + instructions, reason })`, `chat.askPlan(bot, markdown)`, `chat.turn(bot, seconds)`. Each takes + `{ status: "allowed" | "denied" }` to show it answered. `ApprovalCard` takes one of them's `.body`. +- `makeMessage({ from: bot | "owner" | "system", to: bot, body, kind: "note" | "task" })` for + `InboundRow`: yours on the right, a bot's or Botloft's on the left. ## Components and their roles @@ -47,6 +74,11 @@ Bot colors are the exception: each bot has its own (`#FF7A59` Botloft orange, `# `Toaster` + `notifyError(what, error)` for failures away from a form. - Fields: `TextField`, `TextArea`, `SelectField` (labeled), `Select` (custom dropdown), `Choices` (2-6 options side by side), `Switch`, `Tabs`, `Menu`, `Details`, `SidePanel`. +- App screens: `Sidebar` (crews and bots, 288px wide, give it a height), `CrewBots` (a crew's bot + cards), `BotRun` (one turn of a bot in its chat, inside a `