Skip to content

M4 (3/3): timelines, failed deliveries, tasks and the taskbar mark - #7

Merged
httpsphl merged 9 commits into
mainfrom
feat/m4-messages
Sep 28, 2026
Merged

httpsphl merged 9 commits into
mainfrom
feat/m4-messages

Conversation

@httpsphl

@httpsphl httpsphl commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Last of three slices of M4: messages, deliveries and tasks in the app. This PR is built on #6, which is built on #5. Merge them in order (merge commits); this diff then shrinks to the two commits below. With it, M4 is complete: crews, bots, terminals, the timeline and failed deliveries all work from the app, without opening a terminal.

What changes

  • Store (spec 15.1):
    • Deliveries by message: the 500 most recently updated plus every dead one, then each delivery.changed.
    • Every task, then each task.changed.
    • Loading moves to store/sync.ts.
    • Messages are not stored globally: each timeline pages its own.
  • Timelines:
    • The crew timeline (Timeline tab) and a bot's (Messages tab: messages to or from it).
    • Oldest first. The view follows new messages while the owner is at the end, and older pages load on demand.
    • Each message shows sender, recipient, a task or result tag with the task's live state, and its delivery: waiting, retrying (next try and last error), delivering, delivered, or not delivered with Retry.
  • Composer:
    • The owner writes to any bot of the crew from the crew timeline, or to the bot itself from its Messages tab.
    • Ctrl+Enter sends, with the daemon's length limit shown.
  • Failed deliveries:
    • A title-bar button appears while any message to an active bot gave up.
    • It opens the list, with each message, its error and Retry, plus Retry all.
    • Dead deliveries to archived bots are left out, because they were abandoned on purpose.
  • Tasks tab:
    • Open tasks by default, every task on demand.
    • Each task shows status (color, icon and label), requester and assignee, its hop, the deadline or when it settled, and the result.
  • Taskbar overlay (spec 15.2):
    • A dot on the app icon while a bot waits for approval or sign-in, or a message to an active bot was not delivered.
    • It uses the window's setOverlayIcon (new permission core:window:allow-set-overlay-icon), with no command of our own.
  • Test support: FakeConversation plays the bots and the courier in tests (say, deliver, task), and src/test/app.tsx renders the whole app. The fake's clock is now the real one.

Found in the real test (spec 9.3)

Claude Code 2.1.284 presents everything from the inbox as coming from another session and not typed by the user, including the owner's message. It also adds a note that a peer cannot grant permissions or approve a pending prompt.

So a timeline message has a colleague's authority. To approve a prompt or change settings as the bot's user, the owner types in the bot's terminal. The "Needs approval" notice already points there.

Tests

  • messages.test.tsx:
    • the crew timeline with a bot's message and its delivery;
    • the owner sending to a chosen recipient: waiting, then delivered when the courier moves;
    • the bot tab showing only its messages, with a fixed recipient and Ctrl+Enter;
    • paging 60 messages;
    • a dead delivery retried from the title bar ("Retry all", with the message body shown) and from the timeline;
    • the taskbar mark following a bot in needs_approval.
  • Tasks: the open filter, hops and deadline, the "All" filter with results, and a task changing live.
  • Totals: 41 app tests and 140 Rust tests. pnpm check, pnpm build, clippy (including the Tauri capability check) and fmt pass. Each commit passes on its own.

Checked with real Claude Code 2.1.284

The test used the dev preview connected to a real dev daemon.

  1. A message was sent from the bot's Messages tab.
  2. The courier delivered it, and the UI showed Delivered.
  3. The bot went Working, then Idle, and answered "PONG" in its terminal, below the [botloft] from the owner · crew … envelope.

Manual test (Tauri window)

  1. With a bot waiting on a permission prompt, the taskbar icon shows the dot, and it clears once the prompt is answered.
  2. A dead delivery is hard to cause by hand: a paused or stopped bot only defers its messages, which do not count as attempts. The title-bar list and Retry are covered by the component tests.

The app's forms need the daemon's length limits and color palette; the
generated file now carries them so the UI never restates them.
Commands for what the page cannot do (spec 15.2): daemon_status checks
/health on the port from <home>\config.toml, daemon_start launches the
botloftd.exe next to the app detached so it outlives the window,
read_owner_token reads the owner token, and open_path opens folders only.

The window has no decorations (the app draws its own title bar), and the
CSP allows ws://127.0.0.1 on any port so a dev daemon with its own port
is reachable.
rpc.ts speaks spec 11 over WebSocket: session.hello first, requests queued
until it is accepted, answers matched by id, typed RpcError codes, and
reconnection with backoff (0.5 s doubling to 5 s). Requests already sent
when the socket drops fail instead of being replayed, since they may have
run. A refused hello (token or protocol) stops for good.

BotloftApi is the contract components use; client.ts implements it over
rpc.ts and FakeBotloft in memory, typed from the generated RpcMethods so
every method is covered. Host is the same idea for the Tauri commands
and the window.
- Onboarding: finds the daemon, offers to start it, reads the owner token
  and connects; a taken port, another protocol version or a refused hello
  each get their own message. The first run lists what is ready (daemon,
  Claude Code) and explains the one-time folder trust prompt per bot.
- Store: crews, bots and system.status load on every (re)connection, then
  follow crew.changed, bot.changed and bot.state; archived records leave.
- Screens: crews and their bots in the sidebar, a crew overview with bot
  cards, and a bot view with pause, restart (fresh or not), edit, open
  folder and archive. State reads as color, icon and label; paused bots
  read as paused. Daemon validation errors stay in the form.
- Own title bar with Windows controls, light and dark themes following
  Windows, IBM Plex type, bot avatars drawn from the mascot.
- `pnpm dev` in a plain browser runs the UI against FakeBotloft, for UI
  work without Tauri or the daemon. The preview is not in release builds.
The docs say a parent folder's trust does not cover a project's
permissions.allow rules, and the generated settings allow mcp__botloft,
so every new bot asks once; trusting the workspaces root would not help.
The app explains the prompt instead, and Botloft leaves ~/.claude.json
alone. Spec 15 gains daemon_start (M4, replaced by the service in M5) and
how the app finds the daemon's port. The dev config now lives inside the
dev BOTLOFT_HOME so the daemon and the app share it.
The replay carries the queries a terminal answers (cursor position,
XTVERSION, keyboard flags). A client that answers them again sends
stale replies into the bot as keystrokes; with Claude Code 2.1.284 a
stray character showed up in the prompt after attaching. The attach
result now has liveOffset, where live output starts, so the client can
leave the queries before it unanswered (spec 8).
- TerminalSession follows spec 8 without depending on xterm: attach and
  replay, a newer generation is a new screen, a gap attaches again from
  the last byte, duplicates and old generations are ignored, and a new
  connection resumes where the old one stopped. Replayed bytes are
  marked as history; the view drops what xterm "types" while parsing
  them. Keys typed while disconnected are dropped instead of sent late.
- TerminalView: xterm.js with fit and WebGL, a dark screen in both
  themes, Ctrl+C copies a selection, and size changes reach the PTY.
- The bot view has Terminal and Details tabs; the terminal stays mounted
  across tabs. A start that takes more than 4 s explains the one-time
  folder trust prompt; a bot that never started says how to start it.
- FakeBotloft gains terminals: buffers, replay after the attach answer,
  live output, input and sizes. The dev preview shows sample screens,
  and `?live=<port>` connects it to a real dev daemon.

Checked with Claude Code 2.1.284 through the preview connected to a dev
daemon: the trust prompt showed in the terminal and was accepted with
Down and Enter, the bot went idle, a typed prompt made it busy and back
to idle with the answer on screen, and a reload replayed the screen
without stray input.
The store now keeps deliveries by message (the 500 most recently updated
and every dead one, then each delivery.changed) and every task (then
each task.changed); loading moves to store/sync.ts. Messages stay out of
the store: each timeline pages its own.

FakeBotloft's messages, deliveries and tasks move into FakeConversation,
with helpers to play the bots and the courier in tests (say, deliver,
task); terminal handlers move into FakeTerminals. The fake's clock is
the real one, so deadlines read as they will in the app.
- Timelines: a crew's messages and a bot's (to or from it), oldest first,
  following new ones while the owner reads the end, with older pages on
  demand. Each message shows sender, recipient, task or result tag with
  the task's state, and where its delivery is: waiting, retrying with
  the next try and last error, delivering, delivered, or not delivered
  with Retry.
- The owner writes from a composer: to any bot of the crew from the crew
  timeline, to the bot itself from its Messages tab. Ctrl+Enter sends.
- Failed deliveries: a title-bar button while any message to an active
  bot gave up, opening the list with each message, its error and Retry
  (or Retry all).
- Tasks tab: open tasks by default, every task on demand, with status,
  requester and assignee, hop, deadline or when it settled, and result.
- Taskbar overlay (spec 15.2): a dot on the app icon while a bot waits for
  approval or sign-in, or a message to an active bot was not delivered.
  It uses the window's setOverlayIcon; no command of our own.
- Spec 9.3 records what the real test showed: Claude Code presents inbox
  messages, the owner's included, as coming from another session, so a
  timeline message cannot approve a prompt; that happens in the terminal.

Checked with Claude Code 2.1.284 through the dev preview on a real
daemon: a message from the bot's composer was delivered by the courier,
the bot went busy and back to idle, and its answer showed in the
terminal.
@httpsphl
httpsphl merged commit 3363e24 into main Sep 28, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant