Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 27 additions & 27 deletions .design-sync/conventions.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Building with Botloft

Botloft is a desktop app where a crew of Claude Code bots works together. Each bot is a small flame
Botloft is a desktop app where a crew of AI agents works together. Each agent is a small flame
mascot in its own color. The look is quiet and warm: neutral surfaces, one orange accent, IBM Plex.

## Setup
Expand All @@ -9,9 +9,9 @@ mascot in its own color. The look is quiet and warm: neutral surfaces, one orang
(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
`<BotloftProvider crews={[...]} bots={[...]}>` 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.
`<BotloftProvider crews={[...]} agents={[...]}>` per design (not one per piece). Build the data
with `makeCrew`, `makeBot`, `makeMessage` and `chat.*` (below); never write an agent object by hand.
A new `agents` array updates every screen, which is how a design animates states.
- **Theme:** light by default. Put `data-theme="dark"` on a wrapper (or `<html>`) 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
Expand Down Expand Up @@ -40,7 +40,7 @@ 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 from `Botloft.AVATAR_PALETTE` (`#FF7A59`
Agent colors are the exception: each agent has its own from `Botloft.AVATAR_PALETTE` (`#FF7A59`
Botloft orange, `#5EC8FF`, `#A48BFF`, `#9BE564`, `#FFC857`, `#FF7EB6`, `#3DD9C1`, `#E8D5B0`);
`makeBot` hands them out in that order.

Expand All @@ -56,55 +56,55 @@ const scout = makeBot({ crew, name: "Scout", state: "busy", role: "Finds sources

- `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
- An agent's turn is a list for `BotRun`: `chat.reply(agent, markdown)`, `chat.tool(agent, { name:
"WebSearch" | "WebFetch" | "Read" | "Write" | "Bash", summary })`, `chat.askSite(agent, url)`,
`chat.askCommand(agent, command, { explanation })`, `chat.suggestBot(chief, { name, role, model,
instructions, reason })`, `chat.askPlan(agent, markdown)`, `chat.turn(agent, seconds)`. Each takes
`{ status: "allowed" | "denied" }` to show it answered. `ApprovalCard` takes one of them's `.body`.
`chat.askRoutine(bot, { name, prompt, schedule, bot: "writer" })` is a bot asking to set up a
routine (for itself, or for another bot by handle).
- Routines, for `CrewRoutines` (pass them as `BotloftProvider` `routines`): `makeRoutine({ bot,
`chat.askRoutine(agent, { name, prompt, schedule, agent: "writer" })` is an agent asking to set up a
routine (for itself, or for another agent by handle).
- Routines, for `CrewRoutines` (pass them as `BotloftProvider` `routines`): `makeRoutine({ agent,
name, prompt, schedule: schedule.weekly([5], "09:00") | schedule.weekdays("08:30") |
schedule.every(120), lastRun: { status: "done" | "failed" | "queued" | "skipped" } })`. Days are
1 = Monday ... 7 = Sunday. The next run follows the schedule. `status: "queued"` reads "Running
now": a new `routines` array with it shows a routine starting.
- `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.
- `makeMessage({ from: agent | "owner" | "system", to: agent, body, kind: "note" | "task" })` for
`InboundRow`: yours on the right, an agent's or Botloft's on the left.

## Components and their roles

- `BotAvatar` is the mascot: `color`, `size`, `mood` (idle, working, waiting, tired, sleeping;
it animates), `framed` for Botloft itself on its black square.
- `BotStateBadge` shows a bot's state with icon and word; `ChiefBadge` marks a crew's lead bot.
- `BotStateBadge` shows an agent's state with icon and word; `ChiefBadge` marks a crew's lead agent.
- `Button` variants: `primary` (one per view), `secondary`, `ghost` (toolbars), `danger`.
- `Dialog`, `Confirm` are modal (fixed, covering the window). `Callout` for notices in a page,
`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), `CrewBots` (a crew's bot cards), `BotRun`
(one turn of a bot in its chat, inside a `<ul>`), `InboundRow` (a message in a chat, inside a
`<ul>`), `ApprovalCard` (a request: a website, a command, a suggested bot, a plan, a routine),
`BrowserPanel` (a bot's browser, live: tabs, address bar, the page and the bot's cursor),
`CrewRoutines` (a crew's routines: each with its bot, schedule in words, next run, last run;
- App screens: `Sidebar` (crews and agents, 288px wide), `CrewBots` (a crew's agent cards), `BotRun`
(one turn of an agent in its chat, inside a `<ul>`), `InboundRow` (a message in a chat, inside a
`<ul>`), `ApprovalCard` (a request: a website, a command, a suggested agent, a plan, a routine),
`BrowserPanel` (an agent's browser, live: tabs, address bar, the page and the agent's cursor),
`CrewRoutines` (a crew's routines: each with its agent, schedule in words, next run, last run;
it fills the chat column's place, `<CrewRoutines crew={crew} />`).
`BotloftProvider`'s `onAnswer` hears Allow and Deny on request cards.
- **The app window:** one row, `className="flex bg-canvas text-ink"` with a fixed height
(`style={{ height: 820 }}`): the `Sidebar` as its **direct first child**, then the chat column
(`flex min-w-0 flex-1 flex-col`), then, when a bot's browser is open, `<BrowserPanel bot={...}
(`flex min-w-0 flex-1 flex-col`), then, when an agent's browser is open, `<BrowserPanel agent={...}
onClose={...} />` as the **last direct child**. The Sidebar stretches to the row's full height,
with the account pinned at the bottom-left; wrapped in any other element it shrinks to its
content and looks cut short. Never wrap the Sidebar or the BrowserPanel. Give the window at
least 1400px of width when the browser is open, so the chat keeps its room.
- **A browser:** pass `browsers={[{ bot: scout, url, title, tabs: [{ url, title }], action: { kind:
- **A browser:** pass `browsers={[{ agent: scout, url, title, tabs: [{ url, title }], action: { kind:
"click", x, y, label } }]}` to `BotloftProvider`. The page is drawn for you (title, address,
text lines); `action` moves the bot's cursor (`open`, `click`, `type`, `scroll`, `back`), and a
new `browsers` array with another `url` or `action` makes the bot browse on.
text lines); `action` moves the agent's cursor (`open`, `click`, `type`, `scroll`, `back`), and a
new `browsers` array with another `url` or `action` makes the agent browse on.

Read `components/<group>/<Name>/<Name>.prompt.md` for each one's props.

## Voice

Plain words for people who are not programmers. Name bots by their name ("Scout is waiting for
Plain words for people who are not programmers. Name agents by their name ("Scout is waiting for
you"), never "the agent" or "Claude". Never say "daemon"; say "Botloft" or "in the background".

## Example
Expand All @@ -123,12 +123,12 @@ function AppWindow() {
const scoutNow = allowed ? { ...scout, state: "busy" } : scout;
const card = allowed ? { ...site, body: { ...site.body, status: "allowed" } } : site;
return (
<BotloftProvider crews={[crew]} bots={[chief, scoutNow]} selectedBotId={scout.id}
<BotloftProvider crews={[crew]} agents={[chief, scoutNow]} selectedBotId={scout.id}
onAnswer={(answer) => setAllowed(answer.allow)}>
<div className="flex bg-canvas text-ink" style={{ height: 820 }}>
<Sidebar /> {/* direct child of the row: it runs the full height */}
<ul className="flex min-w-0 flex-1 flex-col gap-5 p-5">
<BotRun bot={scoutNow} items={[chat.reply(scout, "I need to open this paper."), card]}
<BotRun agent={scoutNow} items={[chat.reply(scout, "I need to open this paper."), card]}
live={{ draft: "", working: allowed }} />
</ul>
</div>
Expand Down
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Botloft

Workspace desktop Windows-first, com código público sob a FSL (vira Apache 2.0 após dois anos), para rodar bots de agentes de código sempre ligados que colaboram entre si: Claude Code, Antigravity e Codex (os dois últimos experimentais). A seção 30 da spec e o ADR 0003 dizem como cada agente entra. Daemon Rust (`botloftd`) + app Tauri v2 + React.
Workspace desktop Windows-first, com código público sob a FSL (vira Apache 2.0 após dois anos), para rodar agentes de IA sempre ligados que colaboram entre si, sobre Claude Code, Antigravity e Codex (os dois últimos experimentais). A seção 30 da spec e o ADR 0003 dizem como cada agente entra. Daemon Rust (`botloftd`) + app Tauri v2 + React.

## Fonte de verdade

Expand Down Expand Up @@ -65,6 +65,7 @@ Em dev, use `$env:BOTLOFT_HOME = "$PWD\.dev\home"` (caminho absoluto) no daemon
- A UI depende só da interface `BotloftApi` (`app/src/lib/api.ts`). Testes de componente usam `FakeBotloft`.
- Nenhum texto de interface escrito direto no componente: tudo vai em `app/src/i18n/{en,pt-BR,es}/<área>.ts` e é lido com `useT()` (ou `t()` fora do React). Texto novo entra nos três idiomas no mesmo commit; o TypeScript acusa o que faltar (spec 15.6). Palavras simples, sem "daemon" nem outro jargão.
- Nunca edite `app/src/lib/protocol.gen.ts` à mão.
- No texto que o dono lê (app, README, notas de release) cada um é um **agente**; "bot" fica só no nome Botloft e em identificadores (`bot_`, `bots.create`). O que roda por baixo (Claude Code, Antigravity, Codex) é o **motor**.
- Commits pequenos, no formato Conventional Commits (`feat(courier): ...`).

## Windows: armadilhas conhecidas
Expand Down
Loading
Loading