Skip to content

M4 (2/3): bot terminals in xterm.js - #6

Merged
httpsphl merged 7 commits into
mainfrom
feat/m4-terminal
Sep 28, 2026
Merged

httpsphl merged 7 commits into
mainfrom
feat/m4-terminal

Conversation

@httpsphl

Copy link
Copy Markdown
Owner

Second of three slices of M4: bot terminals in the app. This PR is built on #5. Merge #5 first (merge commit); this diff then shrinks to the two commits below.

What changes

  • terminal.attach returns liveOffset (spec 8, 11.2): the offset where the replay ends and live output starts.
    • The replay contains queries that terminals answer. With Claude Code 2.1.284, a freshly started bot's buffer holds ESC[6n, three ESC[>0q (XTVERSION) and ESC[?u (keyboard flags).
    • Answering them again sends stale replies into the bot as keystrokes. In a real test, a stray character appeared in the prompt after attaching.
    • The app now leaves the queries in the history unanswered.
  • TerminalSession (spec 8, independent of xterm):
    • attach and replay;
    • a newer generation is a new screen;
    • a gap re-attaches from the last byte;
    • duplicates and older generations are ignored;
    • a new connection resumes where the old one stopped;
    • keys typed while disconnected are dropped instead of arriving late.
  • TerminalView:
    • xterm.js 6 with fit, WebGL and windowsPty: conpty;
    • a dark screen in both themes (Claude Code picks its colors for a dark terminal);
    • Ctrl+C copies a selection, as in Windows Terminal;
    • size changes reach the PTY.
  • Bot view:
    • Terminal and Details tabs; the terminal stays mounted when switching tabs.
    • A start longer than 4 s explains the one-time folder trust prompt (spec 7.4.1).
    • A bot that never started says how to start it.
  • FakeBotloft gains terminals: per-bot buffers, a replay sent after the attach answer (as the daemon does), live output, recorded input and sizes.
  • Dev preview:
    • It shows sample terminal screens.
    • ?live=<port> connects it to a real dev daemon. The owner token comes from sessionStorage, never from the URL.
    • Release builds do not include the preview (checked in dist).

Tests

  • session.test.ts:
    • replay then live output, with only the replay marked as history;
    • a new generation;
    • gap recovery, with the resume point checked;
    • duplicate and old chunks;
    • resuming after a reconnection;
    • input, size (deduplicated), and dropped keys while disconnected.
  • App.test.tsx:
    • Terminal and Details tabs;
    • the slow-start trust notice, including when it appears and clears;
    • a never-started bot.
  • Rust: liveOffset in the terminal buffer unit tests and in rpc_terminal.
  • Totals: 34 app tests and 140 Rust tests. pnpm check, pnpm build, clippy and fmt pass. Each commit passes on its own.

Checked with real Claude Code 2.1.284

The test used the dev preview in a browser, connected with ?live= to a fresh dev daemon.

  1. Created a crew and a bot from the UI. The trust prompt appeared in the app's terminal ("This folder pre-approves 1 tool permission: mcp__botloft"), and after 4 s the "Still starting" notice showed.
  2. Pressed Down and Enter in xterm. The trust was accepted, the SessionStart hook set the bot to Idle, and the notice cleared.
  3. Found a stray character in the prompt; the replay-query issue above. After the fix, a restarted bot and a page reload both replayed cleanly with an empty prompt.
  4. Typed "Reply with just the word OK" and pressed Enter. The bot showed Working, the answer "OK" appeared, and the bot went back to Idle.

Manual test (Tauri window)

With BOTLOFT_HOME set to the dev folder, run pnpm tauri dev, then:

  1. Create a bot and answer the trust prompt in its terminal.
  2. Type a prompt.
  3. Resize the window: Claude Code redraws at the new size.
  4. Close the app window and open it again. The screen comes back from the replay, with no stray input.

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.
@httpsphl
httpsphl merged commit abe28f5 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