Live activity page for runs (/runs/{id}/live) #12

Merged
SebastienDenooz merged 1 commit from agent/issue-11-in-the-run-add-a-page-to-connect-to-the into main 2026-11-03 22:46:18 +00:00
Owner

What

Every run page gets a Watch live button. It opens /runs/{id}/live in the same tab: a read-only feed of what the run's agent says and does, labelled by kind and in order.

  • Running: new entries are appended every 2 s without a reload.
  • Pending: the page says the run has not started yet, then shows the activity once it starts.
  • Ended (succeeded, failed or cancelled): the page shows the final status and the last captured entries, and stops updating. Opening it after the end gives the same view.
  • Access: the same as /runs/{id}. Another organization gets a 404, guests are redirected to login, and nothing leaks.
  • Watch only: there is no input, prompt box or control on the page.

How

  • Storage: a new run_activities table (RunActivity, ActivityKind). The run executes in a worker and the page in the web process, so the database carries the feed.
  • Recording: ActivityRecorder writes each batch as soon as it is read. Entries are masked by the Redactor, with the run's credentials registered, capped at alfred.llm.activity_excerpt_chars (4000), and limited to alfred.llm.activity_max_entries (2000) per run. Errors are logged and never reach the run.
  • Runners: ContainerRuntime::exec() takes an optional $onOutput callback. CliContainerRuntime passes it to Process::run, and HostRuntime forwards latestOutput() from its polling loop. ExecResult is unchanged.
  • CLI agents: JsonLineStream and ActivityTap split the output into JSON lines, and each adapter maps the events it knows through activity(); unknown events are ignored. Grok's streamed text is merged into one message per run of text.
    • Claude Code switches to --output-format stream-json --verbose. The text, usage and raw_output are read from the final result line, so they look the same as before.
  • API mode: ToolContext records each tool call before it runs and its result after. The model's text of each step comes from laravel/ai's StepCompleted event.
  • Routes: the old auto-refresh fragment moves to /runs/{id}/fragment. /runs/{id}/live is now the page, and /runs/{id}/live/feed?after=N returns the entries after N. live.js gets an append mode; the existing replace mode is unchanged.
  • Retention: PurgeRetention clears entry bodies, and the titles that name files or commands.
  • Docs: spec §16.5, §24.5, §27.2, §27.5 and §27.6 are updated.

Changes from the plan

  • Entries are written per output chunk instead of being buffered for 500 ms or 50 entries. Buffering could hold an entry back for minutes while a long, silent command runs.
  • The feed is documented under spec §27.5 (Live refresh), since §27.6 is the routes table.

Tests

  • Unit:
    • Each adapter's activity() mapping.
    • Claude Code reading stream-json and the old single-object output.
    • JsonLineStream, ActivityTap (merging, a mapping that throws), and the tool classification in ActivityEntry.
    • The stdout callback, with real processes for the container CLI and the host runtime, and the fake runtime.
  • Feature:
    • ActivityRecorder: sequencing, redaction, truncation, the entry cap, and a database failure that is swallowed.
    • ToolContext and per-step messages in API mode.
    • RunExecutor end to end, with the real Claude Code and API adapters on the container and host runners: the feed rows are written and the outcome, text and raw output are unchanged.
    • The live page: button, states, cursor, escaping, watch-only, purged content, access control.
    • Purge retention.
  • Browser (Playwright): click "Watch live", see entries appear in order without a reload, see polling stop when the run ends, and see a pending run switch to its activity once it starts.

php artisan test: everything passes except CliProcessTest, which fails only in the local sandbox because its temp directory sits inside a git checkout; it is unrelated to this change. PHPStan and Pint are clean. CI picks up the new tests on GitHub, Forgejo and GitLab without pipeline changes.

Risks

  • CLI event formats can change between versions. Parsing is best-effort, so an unknown event only shows less in the feed.
  • Claude Code needs --verbose with stream-json in -p mode. This is covered by tests only; no real Claude Code 2.x binary was run.

🤖 Generated with Claude Code

Closes #11

## What Every run page gets a **Watch live** button. It opens `/runs/{id}/live` in the same tab: a read-only feed of what the run's agent says and does, labelled by kind and in order. - **Running:** new entries are appended every 2 s without a reload. - **Pending:** the page says the run has not started yet, then shows the activity once it starts. - **Ended (succeeded, failed or cancelled):** the page shows the final status and the last captured entries, and stops updating. Opening it after the end gives the same view. - **Access:** the same as `/runs/{id}`. Another organization gets a 404, guests are redirected to login, and nothing leaks. - **Watch only:** there is no input, prompt box or control on the page. ## How - **Storage:** a new `run_activities` table (`RunActivity`, `ActivityKind`). The run executes in a worker and the page in the web process, so the database carries the feed. - **Recording:** `ActivityRecorder` writes each batch as soon as it is read. Entries are masked by the `Redactor`, with the run's credentials registered, capped at `alfred.llm.activity_excerpt_chars` (4000), and limited to `alfred.llm.activity_max_entries` (2000) per run. Errors are logged and never reach the run. - **Runners:** `ContainerRuntime::exec()` takes an optional `$onOutput` callback. `CliContainerRuntime` passes it to `Process::run`, and `HostRuntime` forwards `latestOutput()` from its polling loop. `ExecResult` is unchanged. - **CLI agents:** `JsonLineStream` and `ActivityTap` split the output into JSON lines, and each adapter maps the events it knows through `activity()`; unknown events are ignored. Grok's streamed text is merged into one message per run of text. - Claude Code switches to `--output-format stream-json --verbose`. The text, usage and `raw_output` are read from the final `result` line, so they look the same as before. - **API mode:** `ToolContext` records each tool call before it runs and its result after. The model's text of each step comes from `laravel/ai`'s `StepCompleted` event. - **Routes:** the old auto-refresh fragment moves to `/runs/{id}/fragment`. `/runs/{id}/live` is now the page, and `/runs/{id}/live/feed?after=N` returns the entries after `N`. `live.js` gets an append mode; the existing replace mode is unchanged. - **Retention:** `PurgeRetention` clears entry bodies, and the titles that name files or commands. - **Docs:** spec §16.5, §24.5, §27.2, §27.5 and §27.6 are updated. ### Changes from the plan - Entries are written per output chunk instead of being buffered for 500 ms or 50 entries. Buffering could hold an entry back for minutes while a long, silent command runs. - The feed is documented under spec §27.5 (Live refresh), since §27.6 is the routes table. ## Tests - **Unit:** - Each adapter's `activity()` mapping. - Claude Code reading stream-json and the old single-object output. - `JsonLineStream`, `ActivityTap` (merging, a mapping that throws), and the tool classification in `ActivityEntry`. - The stdout callback, with real processes for the container CLI and the host runtime, and the fake runtime. - **Feature:** - `ActivityRecorder`: sequencing, redaction, truncation, the entry cap, and a database failure that is swallowed. - `ToolContext` and per-step messages in API mode. - `RunExecutor` end to end, with the real Claude Code and API adapters on the container and host runners: the feed rows are written and the outcome, text and raw output are unchanged. - The live page: button, states, cursor, escaping, watch-only, purged content, access control. - Purge retention. - **Browser (Playwright):** click "Watch live", see entries appear in order without a reload, see polling stop when the run ends, and see a pending run switch to its activity once it starts. `php artisan test`: everything passes except `CliProcessTest`, which fails only in the local sandbox because its temp directory sits inside a git checkout; it is unrelated to this change. PHPStan and Pint are clean. CI picks up the new tests on GitHub, Forgejo and GitLab without pipeline changes. ## Risks - CLI event formats can change between versions. Parsing is best-effort, so an unknown event only shows less in the feed. - Claude Code needs `--verbose` with `stream-json` in `-p` mode. This is covered by tests only; no real Claude Code 2.x binary was run. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- agent:key=dfe1a8112c5f:kind=merge_request --> Closes #11
feat: add a live activity page to follow a run's agent
All checks were successful
CI / checks (push) Successful in 3m3s
CI / checks (pull_request) Successful in 4m55s
93f5ccfcc9
Add /runs/{id}/live, reached from a "Watch live" button on every run page.
It shows a read-only feed of what the run's agent does — messages,
thinking, tool calls and results, files read or edited, commands, errors —
appended while the run is going on, then frozen with the final status.

- run_activities table, RunActivity model, ActivityKind enum and the
  ActivityRecorder, which writes masked, capped entries as they come and
  never fails the run
- ContainerRuntime::exec() takes an optional stdout callback, implemented
  by the container and host runtimes and by the fake
- local adapters map the JSON events of Claude Code (now stream-json),
  Codex, OpenCode, Copilot and Grok Build to feed entries; raw_output,
  text, usage and outcome are unchanged
- API mode records each tool call and result live, and each step's text
  through laravel/ai's StepCompleted event
- the run page's refresh fragment moves to /runs/{id}/fragment;
  live.js gains an append mode for the feed
- the retention purge clears feed contents; spec §16.5, §24.5, §27.2,
  §27.5 and §27.6 updated
- unit, feature and Playwright browser tests

Refs: #11
Alfred self-assigned this 2026-11-03 16:25:05 +00:00
Alfred removed their assignment 2026-11-03 16:39:09 +00:00
Alfred force-pushed agent/issue-11-in-the-run-add-a-page-to-connect-to-the from 93f5ccfcc9
All checks were successful
CI / checks (push) Successful in 3m3s
CI / checks (pull_request) Successful in 4m55s
to a1c6284bc1
All checks were successful
CI / checks (pull_request) Successful in 3m27s
CI / checks (push) Successful in 3m28s
2026-11-03 22:14:29 +00:00
Compare
SebastienDenooz changed title from WIP: Live activity page for runs (/runs/{id}/live) to Live activity page for runs (/runs/{id}/live) 2026-11-03 22:32:32 +00:00
Sign in to join this conversation.
No description provided.