Skip to content

Add --ws: stream a run to a remote UI as a log destination - #114

Merged
DavertMik merged 5 commits into
mainfrom
remote-log-destination
Aug 16, 2026
Merged

Add --ws: stream a run to a remote UI as a log destination#114
DavertMik merged 5 commits into
mainfrom
remote-log-destination

Conversation

@DavertMik

Copy link
Copy Markdown
Contributor

explorbot --ws <url> (or EXPLORBOT_WS_URL) dials out to a host UI and streams the run over one WebSocket — logs, activity, and the questions the run needs answered. Dialling out rather than listening means a child process the host spawned and a CI bot connecting from elsewhere are the same case.

Built for the Testeiya app, which spawns a run and renders it as a live card, but it assumes nothing about who is on the other end.

The whole integration is two existing extension points

Remote is one class in one file. Nothing else in explorbot knows it exists — no bot instance is handed to it, no agent imports it.

  • It is a LogDestination, and registers itself through the logger's new addDestination(). It receives the entry the logger already built, so the existing LogType travels as the frame's level and the host styles steps, substeps, markdown and errors distinctly instead of scraping them back out of flattened text. html is dropped, ANSI stripped, content capped.
  • It installs executionController.setInputCallback(). Every ask in the codebase already goes through the controller, so the Pilot's askUser, the Navigator's failed-login prompt and drill_ask are all answered remotely without any of them knowing where the answer came from. Installing it is also what stops a TTY-less child hanging on the readline fallback.

Consequently isInteractive() becomes INK_RUNNING || executionController.hasInputCallback()"somebody can answer", asked of the controller rather than of any particular front end. That is what unlocks the credential prompts in a bridged run.

No frame types

A frame is {type, ts, ...whatever}; send(type, data) puts data on the wire and the host renders what it recognises. Neither side validates the other's shape, so either can start sending more at any time without a version bump.

The connection queues while disconnected, reconnects with backoff, and remote.close(exitCode) flushes before exit.

Diff shape

src/remote.ts is new (238 lines). Everything else is 32 lines across 6 files, all generic:

File Change
src/utils/logger.ts export LogDestination, add addDestination() (idempotent) + the fan-out
src/execution-controller.ts hasInputCallback() / clearInputCallback()
src/ai/task-agent.ts isInteractive() asks the controller
bin/explorbot-cli.ts remote.registerOption(program); flush in showStatsAndExit
boat/*/bin/*.ts remote.registerOption(program)

src/explorbot.ts and every agent are untouched.

remote.registerOption(program) adds the flag through a Commander preAction hook, so it covers every command including the mounted api/docs subcommands and the standalone boat bins.

Testing

tests/unit/remote.test.ts — 7 tests against an in-process WebSocket fixture: hello, queue-then-flush ordering, one frame per LogType (with the step's error alongside, and html dropped), the ask round-trip through executionController.requestInput, skip, interrupt, an ignored unknown frame, and close reporting the exit code while resolving outstanding asks.

Also driven end to end against the real Testeiya app-server: the CLI connects, sends hello, streams its config and error logs, and the host resolves the run from the child's exit code.

Known gap

A run currently sends hello, log, activity, ask and result. It does not send page state or the test checklist — those live on the per-instance stateManager / Plan emitters, and reaching them needs a bot-specific hook rather than a log destination. Left out deliberately; happy to follow up with a general side channel if that data is wanted.

🤖 Generated with Claude Code

DavertMik and others added 5 commits August 14, 2026 13:09
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`explorbot --ws <url>` (or EXPLORBOT_WS_URL) dials out to a host UI and
streams the run over one WebSocket — logs, activity, and the questions the
run needs answered. Dialling out rather than listening means a child process
the host spawned and a CI bot connecting from elsewhere are the same case.

It is one class in one file, and nothing else in explorbot knows it exists.
It plugs into the two extension points that already exist:

- `Remote implements LogDestination` and registers itself via the logger's
  new `addDestination()`. It receives the entry the logger already built, so
  the existing `LogType` travels as the frame's level and the host styles
  steps, substeps, markdown and errors distinctly instead of scraping them
  back out of flattened text.
- `executionController.setInputCallback()` — every ask already goes through
  the controller, so the Pilot's `askUser`, the Navigator's failed-login
  prompt and `drill_ask` are all answered remotely without any of them
  knowing where the answer came from. Installing it is also what stops a
  TTY-less child hanging on the readline fallback.

Consequently `isInteractive()` becomes `INK_RUNNING ||
executionController.hasInputCallback()` — "somebody can answer", asked of
the controller rather than of any particular front end.

There are no frame types: a frame is `{type, ts, ...whatever}` and the host
renders what it recognises, so either side can start sending more at any
time. The connection queues while disconnected, reconnects with backoff, and
flushes before exit.

`remote.registerOption(program)` adds the flag through a Commander
preAction hook, so it covers every command including the mounted api/docs
subcommands and the standalone boat bins.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Explorbot Self-Regression

Commit c21a31d · run

Scenario Result Attempts Duration
basic (native) PASS 1/3 5m
experience: control OK — failed as expected 1/1 2m
experience: seeded PASS 1/3 1m

Attempt details

  • basic (native) attempt 1 — PASS: login evidence: PASS (post-login plan=true, post-login research=true); research: PASS (files=6, wellFormed=true, keywords=7/3); scenarios: PASS (tests=6/5, features=4/3); tests passed: PASS (5 passed, 0 failed (reporter: 5 passed, 0 failed))
  • experience: control attempt 1 — PASS: control: OK — failed as expected (0 passed, 1 failed)
  • experience: seeded attempt 1 — PASS: seeded: PASS (1 passed, 0 failed)

Session analysis — basic (native):

Session Analysis

The Issues list feature was explored across 5 tests covering creation, search, label filtering, status filtering, and empty state handling. All core flows work correctly: issues can be created and appear in the list, search and filters correctly narrow results, and the empty state displays appropriately for no matches. One test had an automation issue where the "Apply filters" button click failed but the filter still applied via form submission.

Coverage

  • Pages: /issues
  • Features: Create issue, search by keyword, filter by label, filter by status, empty state handling

What works

  • Create issueET-1 Create new issue via toolbar button and verify it appears in the list
  • Search by keywordET-2 Search for existing issue by keyword and verify results filter correctly
  • Filter by labelET-3 Filter issues by label using the label dropdown and verify results change
  • Filter by statusET-4 Filter issues by status using the status combobox and verify filtering works
  • Empty stateET-5 Search with no matching results and verify empty state behavior

Execution Issues

  • ET-4 Filter issues by status using the status combobox — multiple "Apply filters" button clicks failed but filter applied via form submission on select; verification still passed

@DavertMik
DavertMik merged commit 143aa72 into main Aug 16, 2026
2 of 3 checks passed
@DavertMik
DavertMik deleted the remote-log-destination branch August 16, 2026 16:07
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