Skip to content

Commit ddff1bb

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/share-file
2 parents 6d2c982 + fee3b6f commit ddff1bb

36 files changed

Lines changed: 1624 additions & 103 deletions

‎.design-sync/NOTES.md‎

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,40 @@ Tailwind and TypeScript). Nothing there reimplements a component.
3737
- Fixed overlays (`Dialog`, `Confirm`, `Toaster`) render inside a box with `transform:
3838
translateZ(0)`, which makes it their containing block; without it the per-story capture is
3939
0 px tall.
40-
- `ListAvatar` is excluded (cfg.componentSrcMap): a wrapper over `BotAvatar` that needs a bot object.
40+
- The app screens (`Sidebar`, `CrewBots`, `BotRun`, `InboundRow`, `ApprovalCard`) read the app's
41+
store and API. `pkg/provider.tsx` (`BotloftProvider`) wires the app's own `DaemonProvider` and
42+
`HostProvider` to `FakeBotloft` and `FakeHost`, seeded with the crews and bots it is given, and
43+
turns `approvals.answer` into an `onAnswer` callback. `pkg/scene.ts` builds protocol objects
44+
(`makeCrew`, `makeBot`, `makeMessage`, `chat.*`) with the fake daemon's defaults; request
45+
inputs and summaries follow the daemon (`crates/botloft-core/src/chat.rs` `tool_summary`,
46+
`crates/botloftd/src/tools/browser_sites.rs`): a site request is `{site, url}` with the site as
47+
its summary.
48+
- `BrowserPanel`: `BotloftProvider` `browsers` opens each bot's browser in `FakeBrowser` and
49+
paints it with the app's dev page drawer (`app/src/dev/seedPage.ts`). It must happen in the
50+
provider's `useState` initializer, before the store connects: the fake answers the store's
51+
first `browser.list` synchronously, so an answer taken before the browsers opened arrives after
52+
their `browser.changed` and wins (the panel then says "Browser closed" over the page).
53+
- Routines: `BotloftProvider` `routines` go into `FakeRoutines` in the initializer too (same
54+
first-answer race as the browsers) and as `routine.changed` after. `pkg/routines.ts` builds
55+
them; with no `nextInMinutes` the next run follows the schedule (local time; cron is not worked
56+
out, an hour from now), so the row's "Next" agrees with its schedule. `pkg/ids.ts` holds the
57+
shared id and time helpers.
58+
- Type emission covers the whole package with the repo as root; `.design-sync` is renamed to `ds`
59+
in `dist/types/emit` (ts-morph skips dot folders), and `paths` points bare imports at
60+
`app/node_modules` (`@types` first, or React resolves to its JS). `vite/client.d.ts` is added
61+
for the fake daemon's `?raw` import.
62+
- Bot colors come from `AVATAR_PALETTE` (protocol.gen.ts); previews and conventions use it.
4163

4264
## Known render warns
4365

4466
- `[FONT_MISSING] "Cascadia Mono"`: a system fallback after IBM Plex Mono in `--font-mono`; Plex
4567
Mono ships. Accepted.
4668
- `Menu` shows only its closed button: opening takes a click.
4769
- `SelectField` Disabled looks like the enabled one: the app's own control does not style disabled.
70+
- `[RENDER_ERRORS] AbortError: Transition was skipped` on the app screens: each preview cell has
71+
its own `BotloftProvider`, and each opens its first crew with the app's view transition
72+
(`ui/motion.ts`); the transitions of several cells on one page cut each other off. Renders fine;
73+
a design uses one provider.
4874

4975
## Re-sync risks
5076

@@ -54,5 +80,8 @@ Tailwind and TypeScript). Nothing there reimplements a component.
5480
ranges. Change both together.
5581
- The icon set follows the app's imports: an icon the app stops using disappears from `icons`.
5682
- Bot colors in previews and conventions are sample values, not tokens.
57-
- The scope is the base pieces plus the bot badges (owner's choice on 2026-10-02). Chat rows,
58-
approval cards and the sidebar are not in the package.
83+
- Scope (owner's choices on 2026-10-02): the base pieces, the bot badges, and the app screens the
84+
explainer video needs (sidebar, crew cards, a bot's turn, messages, requests, the live browser, routines).
85+
Not in it yet: the whole crew view, the composer, the screens panel, settings.
86+
- `scene.ts` mirrors protocol shapes and daemon summaries by hand; a protocol change
87+
(`protocol.gen.ts`) or a new request kind needs it updated, or the tsc step fails.

‎.design-sync/config.json‎

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,15 @@
2121
"SelectField": "label: string; hint?: string; options: { value: string; label: string }[]; value?: string; defaultValue?: string; onChange?: React.ChangeEventHandler<HTMLSelectElement>; name?: string; className?: string; id?: string; style?: React.CSSProperties; disabled?: boolean; autoFocus?: boolean; ",
2222
"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;",
2323
"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;",
24-
"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 }[];"
25-
},
26-
"componentSrcMap": {
27-
"ListAvatar": null
24+
"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 }[];",
25+
"ApprovalCard": "/** A request: Botloft.chat.askCommand / askSite / suggestBot / askPlan / askRoutine(...).body. The card follows its toolName (command, website, suggested bot, plan, routine) and its status (pending, allowed, denied, expired). Needs BotloftProvider around it. */ approval: any; /** A bot from Botloft.makeBot(...). */ bot: any;",
26+
"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 };",
27+
"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;",
28+
"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;",
29+
"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. Put it as the direct first child of the app window's flex row (fixed height); wrapped in anything else it shrinks to its content. */ className?: never;",
30+
"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[]; /** Open browsers for BrowserPanel: [{ bot, url, title, tabs?: [{ url, title }], action?: { kind: \"open\" | \"click\" | \"type\" | \"scroll\" | \"back\", x?, y?, label? } }]. A new action object moves the bot's cursor on the page again. */ browsers?: any[]; /** Routines from Botloft.makeRoutine(...), for CrewRoutines. A new array updates them, e.g. to show a run starting. */ routines?: 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;",
31+
"BrowserPanel": "/** The bot whose browser this is; its page comes from BotloftProvider browsers (it shows \"closed\" without one). Place it as the last child of the app window's flex row, right of the chat column: it is a side panel the owner can widen. */ bot: any; /** Counts up each time the owner asks to take the browser into their own hands. */ take?: number; onClose: () => void;",
32+
"CrewRoutines": "/** A crew from Botloft.makeCrew(...): it lists every routine of the crew's bots that BotloftProvider was given (routines from Botloft.makeRoutine), each with its bot, its schedule in words, when it runs next and how its last run went. */ crew: any;"
2833
},
2934
"overrides": {
3035
"Dialog": {
@@ -44,6 +49,33 @@
4449
"Toaster": {
4550
"cardMode": "single",
4651
"primaryStory": "ErrorNotice"
52+
},
53+
"Sidebar": {
54+
"cardMode": "single",
55+
"viewport": "320x620"
56+
},
57+
"BotRun": {
58+
"cardMode": "column"
59+
},
60+
"InboundRow": {
61+
"cardMode": "column"
62+
},
63+
"ApprovalCard": {
64+
"cardMode": "column"
65+
},
66+
"CrewBots": {
67+
"cardMode": "column"
68+
},
69+
"BotloftProvider": {
70+
"cardMode": "single",
71+
"viewport": "1000x560"
72+
},
73+
"BrowserPanel": {
74+
"cardMode": "single",
75+
"viewport": "1440x620"
76+
},
77+
"CrewRoutines": {
78+
"cardMode": "column"
4779
}
4880
},
4981
"readmeHeader": ".design-sync/conventions.md"

‎.design-sync/conventions.md‎

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ mascot in its own color. The look is quiet and warm: neutral surfaces, one orang
55

66
## Setup
77

8-
- No provider is needed. Link `styles.css` and load `_ds_bundle.js`; components are on `window.Botloft`.
8+
- Link `styles.css` and load `_ds_bundle.js`; components are on `window.Botloft`. The basic pieces
9+
(Button, fields, Dialog, Callout, BotAvatar, BotStateBadge...) need nothing around them.
10+
- **App screens need `BotloftProvider`.** `Sidebar`, `CrewBots`, `BotRun`, `InboundRow` and
11+
`ApprovalCard` are the app's real screens and read the app's data: wrap them in one
12+
`<BotloftProvider crews={[...]} bots={[...]}>` per design (not one per piece). Build the data
13+
with `makeCrew`, `makeBot`, `makeMessage` and `chat.*` (below); never write a bot object by hand.
14+
A new `bots` array updates every screen, which is how a design animates states.
915
- **Theme:** light by default. Put `data-theme="dark"` on a wrapper (or `<html>`) and every token
1016
switches. Give the page `bg-canvas text-ink` so the background follows the theme.
1117
- **Language:** the components' own words (state labels, Close, Cancel) follow the browser's
@@ -34,8 +40,36 @@ use `style={{ height: 360 }}` for one-off sizes.
3440
The tokens behind them are CSS variables: `--canvas`, `--panel`, `--sunken`, `--line`,
3541
`--line-strong`, `--ink`, `--ink-soft`, `--muted`, `--accent`, `--ok`, `--work`, `--warn`,
3642
`--danger`. Use `var(--accent)` in inline styles; never hard-code hex for UI colors.
37-
Bot colors are the exception: each bot has its own (`#FF7A59` Botloft orange, `#4FC382`, `#6EA6FF`,
38-
`#F0B24A`, `#C084FC`), passed to `BotAvatar`.
43+
Bot colors are the exception: each bot has its own from `Botloft.AVATAR_PALETTE` (`#FF7A59`
44+
Botloft orange, `#5EC8FF`, `#A48BFF`, `#9BE564`, `#FFC857`, `#FF7EB6`, `#3DD9C1`, `#E8D5B0`);
45+
`makeBot` hands them out in that order.
46+
47+
## Data for the app screens
48+
49+
```jsx
50+
const { makeCrew, makeBot, makeMessage, chat } = window.Botloft;
51+
const crew = makeCrew({ name: "Research" });
52+
const chief = makeBot({ crew, name: "Chief", chief: true, state: "idle" });
53+
const scout = makeBot({ crew, name: "Scout", state: "busy", role: "Finds sources.",
54+
activity: { kind: "tool", text: "on-device AI news", tool: "WebSearch" } }); // sidebar line
55+
```
56+
57+
- `state`: `idle`, `busy` (working), `needs_approval`, `launching`, `rate_limited`, `backoff`,
58+
`auth_error`, `offline` (with `paused: true` it reads Paused).
59+
- A bot's turn is a list for `BotRun`: `chat.reply(bot, markdown)`, `chat.tool(bot, { name:
60+
"WebSearch" | "WebFetch" | "Read" | "Write" | "Bash", summary })`, `chat.askSite(bot, url)`,
61+
`chat.askCommand(bot, command, { explanation })`, `chat.suggestBot(chief, { name, role, model,
62+
instructions, reason })`, `chat.askPlan(bot, markdown)`, `chat.turn(bot, seconds)`. Each takes
63+
`{ status: "allowed" | "denied" }` to show it answered. `ApprovalCard` takes one of them's `.body`.
64+
`chat.askRoutine(bot, { name, prompt, schedule, bot: "writer" })` is a bot asking to set up a
65+
routine (for itself, or for another bot by handle).
66+
- Routines, for `CrewRoutines` (pass them as `BotloftProvider` `routines`): `makeRoutine({ bot,
67+
name, prompt, schedule: schedule.weekly([5], "09:00") | schedule.weekdays("08:30") |
68+
schedule.every(120), lastRun: { status: "done" | "failed" | "queued" | "skipped" } })`. Days are
69+
1 = Monday ... 7 = Sunday. The next run follows the schedule. `status: "queued"` reads "Running
70+
now": a new `routines` array with it shows a routine starting.
71+
- `makeMessage({ from: bot | "owner" | "system", to: bot, body, kind: "note" | "task" })` for
72+
`InboundRow`: yours on the right, a bot's or Botloft's on the left.
3973

4074
## Components and their roles
4175

@@ -47,6 +81,24 @@ Bot colors are the exception: each bot has its own (`#FF7A59` Botloft orange, `#
4781
`Toaster` + `notifyError(what, error)` for failures away from a form.
4882
- Fields: `TextField`, `TextArea`, `SelectField` (labeled), `Select` (custom dropdown),
4983
`Choices` (2-6 options side by side), `Switch`, `Tabs`, `Menu`, `Details`, `SidePanel`.
84+
- App screens: `Sidebar` (crews and bots, 288px wide), `CrewBots` (a crew's bot cards), `BotRun`
85+
(one turn of a bot in its chat, inside a `<ul>`), `InboundRow` (a message in a chat, inside a
86+
`<ul>`), `ApprovalCard` (a request: a website, a command, a suggested bot, a plan, a routine),
87+
`BrowserPanel` (a bot's browser, live: tabs, address bar, the page and the bot's cursor),
88+
`CrewRoutines` (a crew's routines: each with its bot, schedule in words, next run, last run;
89+
it fills the chat column's place, `<CrewRoutines crew={crew} />`).
90+
`BotloftProvider`'s `onAnswer` hears Allow and Deny on request cards.
91+
- **The app window:** one row, `className="flex bg-canvas text-ink"` with a fixed height
92+
(`style={{ height: 820 }}`): the `Sidebar` as its **direct first child**, then the chat column
93+
(`flex min-w-0 flex-1 flex-col`), then, when a bot's browser is open, `<BrowserPanel bot={...}
94+
onClose={...} />` as the **last direct child**. The Sidebar stretches to the row's full height,
95+
with the account pinned at the bottom-left; wrapped in any other element it shrinks to its
96+
content and looks cut short. Never wrap the Sidebar or the BrowserPanel. Give the window at
97+
least 1400px of width when the browser is open, so the chat keeps its room.
98+
- **A browser:** pass `browsers={[{ bot: scout, url, title, tabs: [{ url, title }], action: { kind:
99+
"click", x, y, label } }]}` to `BotloftProvider`. The page is drawn for you (title, address,
100+
text lines); `action` moves the bot's cursor (`open`, `click`, `type`, `scroll`, `back`), and a
101+
new `browsers` array with another `url` or `action` makes the bot browse on.
50102

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

@@ -58,24 +110,29 @@ you"), never "the agent" or "Claude". Never say "daemon"; say "Botloft" or "in t
58110
## Example
59111

60112
```jsx
61-
const { BotAvatar, BotStateBadge, Button, Callout, icons, setLocaleChoice } = window.Botloft;
113+
const { BotloftProvider, Sidebar, BotRun, BotStateBadge, makeCrew, makeBot, chat, setLocaleChoice } =
114+
window.Botloft;
62115
setLocaleChoice("en");
116+
const crew = makeCrew({ name: "Research" });
117+
const chief = makeBot({ crew, name: "Chief", chief: true });
118+
const scout = makeBot({ crew, name: "Scout", state: "needs_approval" });
119+
const site = chat.askSite(scout, "https://arxiv.org/abs/2609.01234");
63120

64-
function CrewCard() {
121+
function AppWindow() {
122+
const [allowed, setAllowed] = React.useState(false);
123+
const scoutNow = allowed ? { ...scout, state: "busy" } : scout;
124+
const card = allowed ? { ...site, body: { ...site.body, status: "allowed" } } : site;
65125
return (
66-
<div className="flex max-w-md flex-col gap-3 rounded-xl border border-line bg-panel p-4">
67-
<div className="flex items-center gap-3">
68-
<BotAvatar color="#4FC382" size={36} mood="working" />
69-
<div className="min-w-0 flex-1">
70-
<p className="font-semibold text-ink text-sm">Scout</p>
71-
<BotStateBadge bot={{ state: "busy", paused: false }} compact />
72-
</div>
73-
<Button variant="ghost" icon={icons.Pause} label="Pause Scout" />
126+
<BotloftProvider crews={[crew]} bots={[chief, scoutNow]} selectedBotId={scout.id}
127+
onAnswer={(answer) => setAllowed(answer.allow)}>
128+
<div className="flex bg-canvas text-ink" style={{ height: 820 }}>
129+
<Sidebar /> {/* direct child of the row: it runs the full height */}
130+
<ul className="flex min-w-0 flex-1 flex-col gap-5 p-5">
131+
<BotRun bot={scoutNow} items={[chat.reply(scout, "I need to open this paper."), card]}
132+
live={{ draft: "", working: allowed }} />
133+
</ul>
74134
</div>
75-
<Callout tone="warn" title="Scout needs your approval">
76-
It wants to open arxiv.org for the first time.
77-
</Callout>
78-
</div>
135+
</BotloftProvider>
79136
);
80137
}
81138
```

0 commit comments

Comments
 (0)