Skip to content

Commit 3363e24

Browse files
authored
Merge pull request #7 from httpsphl/feat/m4-messages
M4 (3/3): timelines, failed deliveries, tasks and the taskbar mark
2 parents abe28f5 + 7cc6d2e commit 3363e24

28 files changed

Lines changed: 1499 additions & 233 deletions

‎app/src-tauri/capabilities/default.json‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
"$schema": "../gen/schemas/desktop-schema.json",
33
"identifier": "default",
44
"description": "Capability for the main window",
5-
"windows": ["main"],
5+
"windows": [
6+
"main"
7+
],
68
"permissions": [
79
"core:default",
810
"core:window:allow-start-dragging",
911
"core:window:allow-minimize",
1012
"core:window:allow-toggle-maximize",
11-
"core:window:allow-close"
13+
"core:window:allow-close",
14+
"core:window:allow-set-overlay-icon"
1215
]
1316
}

‎app/src/dev/preview.ts‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ function seed(fake: FakeBotloft): void {
6161
fake.setBotState(analyst.id, "rate_limited", 1);
6262
fake.terminals.output(scout.id, SCOUT_SCREEN);
6363
fake.terminals.output(reviewer.id, PROMPT_SCREEN);
64+
conversation(fake, scout.id, writer.id, reviewer.id);
6465
const ops = fake.addCrew("Ops");
6566
const deploy = fake.addBot(ops.id, "Deploy", "Ships the site on Fridays");
6667
const watcher = fake.addBot(ops.id, "Watcher", "Keeps an eye on the error log");
@@ -69,6 +70,54 @@ function seed(fake: FakeBotloft): void {
6970
fake.setBotState(watcher.id, "offline");
7071
}
7172

73+
/** A morning's worth of messages and tasks between three bots. */
74+
function conversation(fake: FakeBotloft, scout: string, writer: string, reviewer: string): void {
75+
const talk = fake.conversation;
76+
void fake.call("messages.send", {
77+
botId: scout,
78+
body: "Summarize the three newest papers in shared/inbox.",
79+
});
80+
const owner = [...talk.deliveries.values()].at(-1);
81+
if (owner) {
82+
talk.deliver(owner.id, "sent");
83+
}
84+
const summary = talk.task(scout, writer, {
85+
status: "done",
86+
result: "Draft in shared/drafts/week-39.md, 2 pages.",
87+
});
88+
const asked = talk.say({
89+
from: scout,
90+
to: writer,
91+
kind: "task",
92+
taskId: summary.id,
93+
body: "Turn shared/summaries/week-39.md into this week's report draft.",
94+
});
95+
talk.deliver(asked.delivery.id, "sent");
96+
const done = talk.say({
97+
from: writer,
98+
to: scout,
99+
kind: "result",
100+
taskId: summary.id,
101+
body: "Draft in shared/drafts/week-39.md, 2 pages.",
102+
});
103+
talk.deliver(done.delivery.id, "sent");
104+
const review = talk.task(writer, reviewer, { hops: 2 });
105+
const check = talk.say({
106+
from: writer,
107+
to: reviewer,
108+
kind: "task",
109+
taskId: review.id,
110+
body: "Check the numbers in section 2 against the sources.",
111+
});
112+
talk.deliver(check.delivery.id, "sent");
113+
const late = talk.say({
114+
from: scout,
115+
to: reviewer,
116+
body: "Two of the links in the draft moved; the new ones are in shared/links.md.",
117+
});
118+
talk.deliver(late.delivery.id, "dead", "the bot's inbox did not answer in 10 s");
119+
}
120+
72121
const ESC = String.fromCharCode(27);
73122
const NEWLINE = String.fromCharCode(13, 10);
74123
const dim = (text: string) => `${ESC}[2m${text}${ESC}[0m`;

‎app/src/features/bots/BotView.tsx‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@ import { useEffect, useState } from "react";
22
import type { Bot, Crew } from "../../lib/protocol.gen";
33
import { Callout } from "../../ui/Callout";
44
import { Tabs, tabId } from "../../ui/Tabs";
5+
import { Composer } from "../messages/Composer";
6+
import { Timeline } from "../messages/Timeline";
57
import { TerminalView } from "../terminal/TerminalView";
68
import { BotHeader } from "./BotHeader";
79
import { stateView } from "./BotStateBadge";
810

9-
type Pane = "terminal" | "details";
11+
type Pane = "terminal" | "messages" | "details";
1012

1113
const TABS: { id: Pane; label: string }[] = [
1214
{ id: "terminal", label: "Terminal" },
15+
{ id: "messages", label: "Messages" },
1316
{ id: "details", label: "Details" },
1417
];
1518

@@ -57,6 +60,19 @@ export function BotView({ bot, crew }: { bot: Bot; crew: Crew }) {
5760
<TerminalView botId={bot.id} />
5861
)}
5962
</div>
63+
{pane === "messages" && (
64+
<div
65+
role="tabpanel"
66+
aria-labelledby={tabId("messages")}
67+
className="flex min-h-0 flex-1 flex-col"
68+
>
69+
<Timeline
70+
filter={{ botId: bot.id }}
71+
empty={`No messages to or from ${bot.name} yet.`}
72+
composer={(onSent) => <Composer crewId={crew.id} botId={bot.id} onSent={onSent} />}
73+
/>
74+
</div>
75+
)}
6076
{pane === "details" && (
6177
<div
6278
role="tabpanel"
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Plus } from "lucide-react";
2+
import type { Bot, Crew } from "../../lib/protocol.gen";
3+
import { useApp } from "../../store/context";
4+
import { Button } from "../../ui/Button";
5+
import { BotAvatar } from "../bots/BotAvatar";
6+
import { BotStateBadge } from "../bots/BotStateBadge";
7+
8+
/** The crew's bots as cards; a card opens the bot. */
9+
export function CrewBots({ crew, bots, onNewBot }: { crew: Crew; bots: Bot[]; onNewBot(): void }) {
10+
const selectBot = useApp((state) => state.selectBot);
11+
if (bots.length === 0) {
12+
return (
13+
<div className="border border-line border-dashed px-6 py-10 text-center">
14+
<p className="font-medium">No bots in {crew.name} yet.</p>
15+
<p className="mt-1 text-muted text-sm">
16+
A bot is a Claude Code session that keeps running, with its own folder and role.
17+
</p>
18+
<Button className="mt-4" variant="primary" icon={Plus} onClick={onNewBot}>
19+
New bot
20+
</Button>
21+
</div>
22+
);
23+
}
24+
return (
25+
<ul className="grid grid-cols-[repeat(auto-fill,minmax(260px,1fr))] gap-3">
26+
{bots.map((bot) => (
27+
<li key={bot.id}>
28+
<button
29+
type="button"
30+
onClick={() => selectBot(bot.id)}
31+
className="flex h-full w-full flex-col gap-2 border border-line bg-panel p-3 text-left hover:border-line-strong"
32+
>
33+
<div className="flex w-full items-center gap-2.5">
34+
<BotAvatar color={bot.color} size={32} />
35+
<div className="min-w-0 flex-1">
36+
<p className="truncate font-semibold">{bot.name}</p>
37+
<p className="truncate font-mono text-muted text-xs">@{bot.handle}</p>
38+
</div>
39+
</div>
40+
<p className="line-clamp-2 min-h-10 text-ink-soft text-sm">
41+
{bot.role || "No role yet."}
42+
</p>
43+
<BotStateBadge bot={bot} crewPaused={crew.paused} />
44+
</button>
45+
</li>
46+
))}
47+
</ul>
48+
);
49+
}

‎app/src/features/crews/CrewView.tsx‎

Lines changed: 46 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ import { useApi, useApp, useHost } from "../../store/context";
77
import { Button } from "../../ui/Button";
88
import { Confirm } from "../../ui/Confirm";
99
import { Menu } from "../../ui/Menu";
10+
import { type Tab, Tabs, tabId } from "../../ui/Tabs";
1011
import { attempt } from "../../ui/toast";
11-
import { BotAvatar } from "../bots/BotAvatar";
1212
import { BotDialog } from "../bots/BotDialog";
13-
import { BotStateBadge } from "../bots/BotStateBadge";
13+
import { Composer } from "../messages/Composer";
14+
import { Timeline } from "../messages/Timeline";
15+
import { TaskList } from "../tasks/TaskList";
16+
import { CrewBots } from "./CrewBots";
1417
import { CrewDialog } from "./CrewDialog";
1518

1619
/** The crew's `shared` folder, next to its bots' workspaces (spec 5). */
@@ -20,14 +23,21 @@ export function sharedFolder(bots: Bot[]): string | null {
2023
}
2124

2225
type Open = "bot" | "rename" | "archive" | null;
26+
type Pane = "bots" | "timeline" | "tasks";
27+
28+
const TABS: Tab<Pane>[] = [
29+
{ id: "bots", label: "Bots" },
30+
{ id: "timeline", label: "Timeline" },
31+
{ id: "tasks", label: "Tasks" },
32+
];
2333

2434
export function CrewView({ crew }: { crew: Crew }) {
2535
const api = useApi();
2636
const host = useHost();
2737
const putCrew = useApp((state) => state.putCrew);
28-
const selectBot = useApp((state) => state.selectBot);
2938
const bots = useApp(useShallow((state) => botsOf(state, crew.id)));
3039
const [open, setOpen] = useState<Open>(null);
40+
const [pane, setPane] = useState<Pane>("bots");
3141
const shared = sharedFolder(bots);
3242
const close = () => setOpen(null);
3343

@@ -38,7 +48,7 @@ export function CrewView({ crew }: { crew: Crew }) {
3848

3949
return (
4050
<section aria-label={crew.name} className="flex min-h-0 flex-1 flex-col">
41-
<header className="flex items-center gap-3 border-line border-b px-6 py-4">
51+
<header className="flex items-center gap-3 border-line border-b px-5 py-3.5">
4252
<div className="min-w-0 flex-1">
4353
<h1 className="truncate font-semibold text-xl tracking-tight">{crew.name}</h1>
4454
<p className="mt-0.5 text-muted text-sm">
@@ -80,43 +90,38 @@ export function CrewView({ crew }: { crew: Crew }) {
8090
/>
8191
</header>
8292

83-
<div className="min-h-0 flex-1 overflow-y-auto p-6">
84-
{bots.length === 0 ? (
85-
<div className="border border-line border-dashed px-6 py-10 text-center">
86-
<p className="font-medium">No bots in {crew.name} yet.</p>
87-
<p className="mt-1 text-muted text-sm">
88-
A bot is a Claude Code session that keeps running, with its own folder and role.
89-
</p>
90-
<Button className="mt-4" variant="primary" icon={Plus} onClick={() => setOpen("bot")}>
91-
New bot
92-
</Button>
93-
</div>
94-
) : (
95-
<ul className="grid grid-cols-[repeat(auto-fill,minmax(260px,1fr))] gap-3">
96-
{bots.map((bot) => (
97-
<li key={bot.id}>
98-
<button
99-
type="button"
100-
onClick={() => selectBot(bot.id)}
101-
className="flex h-full w-full flex-col gap-2 border border-line bg-panel p-3 text-left hover:border-line-strong"
102-
>
103-
<div className="flex w-full items-center gap-2.5">
104-
<BotAvatar color={bot.color} size={32} />
105-
<div className="min-w-0 flex-1">
106-
<p className="truncate font-semibold">{bot.name}</p>
107-
<p className="truncate font-mono text-muted text-xs">@{bot.handle}</p>
108-
</div>
109-
</div>
110-
<p className="line-clamp-2 min-h-10 text-ink-soft text-sm">
111-
{bot.role || "No role yet."}
112-
</p>
113-
<BotStateBadge bot={bot} crewPaused={crew.paused} />
114-
</button>
115-
</li>
116-
))}
117-
</ul>
118-
)}
119-
</div>
93+
<Tabs<Pane> label="Crew views" tabs={TABS} value={pane} onChange={setPane} />
94+
{pane === "bots" && (
95+
<div
96+
role="tabpanel"
97+
aria-labelledby={tabId("bots")}
98+
className="min-h-0 flex-1 overflow-y-auto p-5"
99+
>
100+
<CrewBots crew={crew} bots={bots} onNewBot={() => setOpen("bot")} />
101+
</div>
102+
)}
103+
{pane === "timeline" && (
104+
<div
105+
role="tabpanel"
106+
aria-labelledby={tabId("timeline")}
107+
className="flex min-h-0 flex-1 flex-col"
108+
>
109+
<Timeline
110+
filter={{ crewId: crew.id }}
111+
empty="No messages yet. Write to a bot below; what the bots send each other shows up here too."
112+
composer={(onSent) => <Composer crewId={crew.id} onSent={onSent} />}
113+
/>
114+
</div>
115+
)}
116+
{pane === "tasks" && (
117+
<div
118+
role="tabpanel"
119+
aria-labelledby={tabId("tasks")}
120+
className="flex min-h-0 flex-1 flex-col"
121+
>
122+
<TaskList crewId={crew.id} />
123+
</div>
124+
)}
120125

121126
{open === "bot" && <BotDialog crewId={crew.id} onClose={close} />}
122127
{open === "rename" && <CrewDialog crew={crew} onClose={close} />}

0 commit comments

Comments
 (0)