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
67 changes: 67 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@fontsource/ibm-plex-mono": "^5.3.0",
"@fontsource/ibm-plex-sans": "^5.3.0",
"@tauri-apps/api": "^2.12.0",
"@tauri-apps/plugin-dialog": "^2.8.0",
"@tauri-apps/plugin-updater": "^2.13.0",
"lucide-react": "^1.48.0",
"react": "^19.3.0",
Expand Down
10 changes: 10 additions & 0 deletions app/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ dirs = "7.0.0"
serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.151"
tauri = { version = "2", features = [] }
tauri-plugin-dialog = "2"
tauri-plugin-updater = "2"
toml = "1.1.6"

Expand Down
4 changes: 3 additions & 1 deletion app/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"core:window:allow-toggle-maximize",
"core:window:allow-close",
"core:window:allow-set-overlay-icon",
"updater:default"
"core:webview:allow-set-webview-zoom",
"updater:default",
"dialog:allow-open"
]
}
2 changes: 2 additions & 0 deletions app/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ pub fn run() {
tauri::Builder::default()
// Checks the release feed and installs signed updates (spec 15.5).
.plugin(tauri_plugin_updater::Builder::new().build())
// The folder picker for a crew's work folder (spec 5).
.plugin(tauri_plugin_dialog::init())
.invoke_handler(tauri::generate_handler![
daemon_status,
daemon_install,
Expand Down
4 changes: 3 additions & 1 deletion app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import type { Host } from "./lib/host";
import { TitleBar } from "./shell/TitleBar";
import { useThemeRoot } from "./shell/theme";
import { Workspace } from "./shell/Workspace";
import { useZoomRoot } from "./shell/zoom";
import { DaemonProvider, HostProvider } from "./store/context";
import { Toaster } from "./ui/toast";

export function App({ host, connect }: { host: Host; connect: Connect }) {
useThemeRoot();
useZoomRoot(host);
const [link] = useState(() => createLink(host, connect));
const current = useStore(link, (state) => state.current);
useEffect(() => {
Expand All @@ -25,7 +27,7 @@ export function App({ host, connect }: { host: Host; connect: Connect }) {
</DaemonProvider>
) : (
<div className="flex h-full flex-col">
<TitleBar />
<TitleBar language />
<Onboarding link={link} />
</div>
)}
Expand Down
42 changes: 40 additions & 2 deletions app/src/dev/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { FakeHost } from "../lib/fakeHost";
import type { Host } from "../lib/host";
import { PROTOCOL_VERSION } from "../lib/protocol.gen";
import { seedChats } from "./seedChats";
import { seedChief } from "./seedChief";

export function previewProps(search: string): { host: Host; connect: Connect } {
const params = new URLSearchParams(search);
Expand All @@ -19,7 +20,7 @@ export function previewProps(search: string): { host: Host; connect: Connect } {
return liveProps(live);
}
const fake = new FakeBotloft();
const host = new FakeHost();
const host = previewHost();
if (params.has("stopped")) {
host.status = {
state: "stopped",
Expand All @@ -34,7 +35,7 @@ export function previewProps(search: string): { host: Host; connect: Connect } {
}

function liveProps(port: number): { host: Host; connect: Connect } {
const host = new FakeHost();
const host = previewHost();
host.status = {
state: "running",
port,
Expand All @@ -52,7 +53,33 @@ function liveProps(port: number): { host: Host; connect: Connect } {
};
}

/** A FakeHost whose zoom shows in a plain browser, like the app's does. */
function previewHost(): FakeHost {
const host = new FakeHost();
host.setZoom = (factor) => {
host.zoom = factor;
document.documentElement.style.zoom = String(factor);
return Promise.resolve();
};
// A plain browser has no folder picker: every pick gives this folder.
host.nextFolder = "D:\\Projects\\Bakery site";
return host;
}

function seed(fake: FakeBotloft): void {
const now = Date.now();
fake.system = {
...fake.system,
usage: {
status: "allowed",
resetsAt: now + 2 * 3600_000,
observedAt: now - 5 * 60_000,
windows: [
{ name: "five_hour", utilization: 0.34, resetsAt: now + 2 * 3600_000 },
{ name: "seven_day", utilization: 0.62, resetsAt: now + 3 * 86_400_000 },
],
},
};
const research = fake.addCrew("Research");
const scout = fake.addBot(research.id, "Scout", "Finds sources and summarizes them");
const writer = fake.addBot(research.id, "Writer", "Turns notes into a weekly report");
Expand All @@ -62,20 +89,31 @@ function seed(fake: FakeBotloft): void {
"Checks facts and tone before anything ships",
);
const analyst = fake.addBot(research.id, "Analyst", "Crunches the numbers behind each claim");
const planner = fake.addBot(research.id, "Planner", "Plans changes before touching the code");
void fake.call("bots.setPermissionMode", { botId: planner.id, mode: "plan" });
void fake.call("bots.setModel", { botId: analyst.id, model: "haiku" });
// What Claude Code reported: the plan's default, and the chosen Haiku.
for (const bot of [scout, writer, reviewer, planner]) {
fake.bot(bot.id).modelInUse = "claude-opus-5-5";
}
fake.bot(analyst.id).modelInUse = "claude-haiku-4-5-20251001";
fake.setBotState(scout.id, "idle", 3);
fake.setBotState(writer.id, "busy", 2);
fake.setBotState(reviewer.id, "needs_approval", 5);
fake.setBotState(analyst.id, "rate_limited", 1);
fake.setBotState(planner.id, "needs_approval", 2);
seedChats(fake, {
scout: scout.id,
writer: writer.id,
reviewer: reviewer.id,
analyst: analyst.id,
planner: planner.id,
});
const ops = fake.addCrew("Ops");
const deploy = fake.addBot(ops.id, "Deploy", "Ships the site on Fridays");
const watcher = fake.addBot(ops.id, "Watcher", "Keeps an eye on the error log");
void fake.call("crews.setPaused", { crewId: ops.id, paused: true });
fake.setBotState(deploy.id, "offline");
fake.setBotState(watcher.id, "offline");
seedChief(fake);
}
24 changes: 23 additions & 1 deletion app/src/dev/seedChats.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Dev only: a morning's work between four research bots, for the preview.
// Dev only: a morning's work between five research bots, for the preview.

import type { FakeBotloft } from "../lib/fake";
import type { BotId } from "../lib/protocol.gen";
Expand All @@ -23,11 +23,22 @@ const SUMMARY = `Done. The summary is in \`shared/summaries/week-39.md\`.

Your chart matches the numbers in the first paper. I asked @writer to turn this into the weekly report.`;

const PLAN = `## Cache the weekly report

The report is rebuilt on every visit, and Monday is when everyone opens it.

1. Build the report once when the week closes and save it in \`shared/cache/\`.
2. Serve the saved copy; rebuild it only when a source file changes.
3. Add a test that the saved copy matches a fresh build.

**Not changing:** the report's layout or its sources.`;

interface Crew {
scout: BotId;
writer: BotId;
reviewer: BotId;
analyst: BotId;
planner: BotId;
}

const MINUTE = 60 * 1000;
Expand Down Expand Up @@ -124,5 +135,16 @@ export function seedChats(fake: FakeBotloft, crew: Crew): void {
text: "The account reached its usage limit. Messages wait until it resets.",
});
chat.turn(crew.analyst, "rate_limit");

at(8);
void fake.call("messages.send", {
botId: crew.planner,
body: "The report pages are slow on Mondays. Plan a fix, but don't change anything yet.",
});
at(7);
chat.finish(chat.tool(crew.planner, "Grep", { summary: "buildReport" }), "3 matches");
const plan = JSON.stringify({ plan: PLAN });
chat.tool(crew.planner, "ExitPlanMode", { summary: "Cache the weekly report", input: plan });
chat.ask(crew.planner, "ExitPlanMode", "Cache the weekly report", plan);
fake.now = end;
}
57 changes: 57 additions & 0 deletions app/src/dev/seedChief.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Dev only: a crew led by its chief, who asks the owner for a new bot.

import type { FakeBotloft } from "../lib/fake";
import { SUGGEST_TOOL } from "../lib/fakeChat";

const PLAN = `Here's how I'd split it:

1. **Pages and copy**: I write the menu, hours and "about us" text myself.
2. **Look and feel**: this needs a designer. The crew has none, so I'm suggesting one.
3. **Build**: once the design is ready, @writer and I turn it into the site in the work folder.

I'll hand out the first tasks as soon as you approve the designer.`;

const DESIGNER = {
name: "Designer",
role: "Designs the pages: layout, colors and photos",
instructions:
"Design the bakery's site: home, menu, hours and contact. Put the mockups in design/ in the work folder, one image per page, and tell @chief when each is ready. Keep it warm and simple; the owner likes the colors of the shop's sign.",
model: "sonnet",
reason: "Nobody in the crew designs; the site needs a look before anyone builds it.",
};

export function seedChief(fake: FakeBotloft): void {
void fake
.call("crews.create", {
name: "Bakery site",
workFolder: "D:\\Projects\\Bakery site",
lead: {
name: "Chief",
role: "Leads the crew: plans the work, suggests new bots and hands out tasks",
instructions: "Build and keep up the website of my bakery.",
},
})
.then((crew) => {
const chief = crew.leadBotId;
if (!chief) {
return;
}
const writer = fake.addBot(crew.id, "Writer", "Writes the pages' text");
fake.setBotState(writer.id, "idle", 1);
fake.bot(chief).modelInUse = "claude-opus-5-5";
const { chat } = fake;
void fake.call("messages.send", {
botId: chief,
body: "Let's build the website for my bakery. What do you need?",
});
chat.finish(
chat.tool(chief, "mcp__botloft__crew_roster", {}),
'{"bots":[{"handle":"writer"}]}',
);
chat.reply(chief, PLAN);
const input = JSON.stringify(DESIGNER);
chat.tool(chief, SUGGEST_TOOL, { summary: "Designer", input });
chat.ask(chief, SUGGEST_TOOL, "Designer", input);
fake.setBotState(chief, "needs_approval", 1);
});
}
Loading
Loading