Skip to content

feat(app): single-source the G1 capture dump onto the workspace snapshot (F-mcp-snapshot-g1, #216)#226

Merged
bastien-gallay merged 3 commits into
mainfrom
worktree-216-mcp-snapshot-g1
Jul 25, 2026
Merged

feat(app): single-source the G1 capture dump onto the workspace snapshot (F-mcp-snapshot-g1, #216)#226
bastien-gallay merged 3 commits into
mainfrom
worktree-216-mcp-snapshot-g1

Conversation

@bastien-gallay

Copy link
Copy Markdown
Collaborator

What

The G1 dev-loop dump (~/.termherd/captures/capture-<ts>.json) and the MCP
snapshot tool described the same application with two models:
CaptureDump (tabs + the focused terminal) and WorkspaceSnapshot (config +
sidebar + per-pane tabs + scoped terminals). Two models means a field can mean
one thing on disk and another on the wire, and every later rung has to teach
both. This collapses them onto one.

before                                  after
Event::Capture{focused_pty_text}        Event::Capture(SnapshotInputs)
        │                                       │
App::build_capture ──► CaptureDump      App::snapshot(&SnapshotFilter::capture(), …)
        │                                       │
Effect::Capture(CaptureDump)            Effect::Capture(WorkspaceSnapshot)
        │                                       │
capture::DumpDto ──► JSON               snapshot_dto::SnapshotDto ──► JSON
                                        (the same one the MCP tool uses)

SnapshotFilter::capture() is the fixed shape behind a capture: every
structural section, plus the focused pane's screen kept whole. A capture is
written to a file, not streamed into a call, so it pays for the full picture —
but it stays scoped to the pane the developer is looking at when they press the
key.

core::capture (CaptureDump / CaptureTab) is gone, and the shell gathers a
capture's adapter inputs through the same seam a bridge snapshot uses
(snapshot_inputs_for), so config and terminal text are collected exactly one
way.

Two commits

  1. refactor(app) — tidy-first, no behaviour change: the JSON wire form of
    WorkspaceSnapshot moves out of mcp/handler.rs into app::snapshot_dto,
    field-for-field, so both readers can share it.
  2. feat(app) — the reframe above.

Breaking dump-format change

Deliberate, and the point of the slice:

  • focused_pty: "<text>"terminals: { "<handle>": "<text>" }
  • the dump gains focus, config, sidebar, and per-pane detail (stable
    handle, kind, cwd, status)
  • status words follow the MCP vocabulary — "ready" becomes "idle" — which
    retires strings::status_label (it existed only for this dump)

The contract an AI actually depends on — newest capture = highest-named
pair
— is untouched. AGENTS.md's capture note is refreshed.

Tests

Written red first, then made green. 8 tests over the new behaviour:

  • core::snapshot — the capture filter is every section + the focused terminal,
    untruncated (10 000 lines survive).
  • core::app::capture — the dump carries every section; the focused text rides
    whole; a background pane's injected text is not included; a tab's custom
    title wins; empty workspace; split pane order (the last two are regressions
    kept from the old suite).
  • app::capture — the on-disk JSON is the MCP shape (string handles, "idle").
  • app::shell — end-to-end through the perform_capture dir seam: the written
    file holds focus, tabs, config and the focused terminal.

Full suite green (cargo test --workspace); fmt · clippy · machete ·
deny · check-deps · check-arch · markdownlint all pass. ROADMAP entry
ticked.

Closes #216.

…dler

The DTO mirror of `WorkspaceSnapshot` was private to `mcp/handler.rs`, but
the G1 capture dump is about to serialise the same model. Move it to
`app::snapshot_dto` unchanged — same fields, same order, same omissions —
so both readers share one wire form instead of drifting mirrors.

Pure structural move: no behaviour change.
The dev-loop dump and the MCP `snapshot` tool described the same app with two
models — `CaptureDump` (tabs + focused terminal) and `WorkspaceSnapshot`
(config + sidebar + panes + scoped terminals). Two models means a field can
mean one thing on disk and another on the wire, and every later rung has to
teach both.

Collapse them. `Event::Capture` now carries the adapter-injected
`SnapshotInputs`, `Effect::Capture` a `WorkspaceSnapshot`, and
`App::build_capture` is a call to the snapshot builder under a fixed
`SnapshotFilter::capture()` — every section, the focused pane's screen kept
whole (a file pays for the full picture where a call must not). `core::capture`
and its two types are gone; the shell gathers a capture's inputs through the
same seam a bridge snapshot uses.

The dump's JSON is now `app::snapshot_dto`, shared with the MCP handler, so the
capture gained config, the sidebar and per-pane detail (handle, kind, cwd,
status) for free.

Breaking dump-format change: `focused_pty` becomes `terminals` keyed by handle,
and status words follow the MCP vocabulary (`ready` becomes `idle`), which
retires `strings::status_label`. The newest-stamp discovery contract — the one
an AI actually depends on — is untouched.
The note is the reference document for `capture-<ts>.json`, and every field it
described was renamed or removed: the event/effect payloads, the CaptureDump
field map, the sample JSON, the status vocabulary and the file map. A reader
following it would key on `focused_pty` / `active_tab` / `ready` and match
nothing this build writes.
@bastien-gallay
bastien-gallay force-pushed the worktree-216-mcp-snapshot-g1 branch from 39978c7 to 9dfd37c Compare July 25, 2026 19:14
@bastien-gallay
bastien-gallay merged commit 3d9dcb6 into main Jul 25, 2026
17 checks passed
@bastien-gallay
bastien-gallay deleted the worktree-216-mcp-snapshot-g1 branch July 25, 2026 19:23
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.

F-mcp-snapshot-g1 — single-source the G1 capture dump onto the WorkspaceSnapshot model

1 participant