Skip to content

Wire the 3D Watch Agent surface to the real snapshot endpoint #23

Closed

Description

@Dillpickleschmidt

Part of #1

Question

Turn the 3D Watch Agent from a throwaway route into a real surface, fed by the real endpoint.

Render Tree and Terrain verbatim against fixture data left the scenes reachable only at /mindwalk-3d?threadId=, in static-map (locHeights) mode with no trace at all. Land the trace projection and snapshot endpoint now serves { trace, citymap } from GET /api/mindwalk/threads/:threadId. This ticket joins them.

Shape:

  • Fetch the snapshot over HTTP (not the websocket — it is hundreds of kilobytes gzipped, and the citymap wants sending once). Read the contract from packages/contracts/src/trace.ts; it is byte-identical to apps/web/src/mindwalk/types.ts, so the ported scene code consumes it unchanged.
  • Drive the scenes from the real trace rather than locHeights, and wire the playback deck (playback/, already ported) to it.
  • Add the 3D Watch Agent card to RightPanelEmptyState. Removing the Browser card left three cards in a grid-cols-2; this restores a clean 2x2.
  • Delete the /mindwalk-3d route once nothing needs it.

Watch out for:

  • The render-on-change gate from Make the scenes render on change, not every frame is load-bearing. Every data effect calls invalidate(); a continuous loop runs only while playback is actually running. Do not reintroduce an unconditional requestAnimationFrame, and re-measure the idle draw-call count before calling this done.
  • Replay first. Live-watch ("pin the scrubber to the end") is fog, not this ticket.
  • lens is a no-op query parameter today; pass main or omit it. Per-agent lenses are #21.
  • A thread whose citymap failed to build returns an empty citymap and a real trace. The surface must render that as a timeline with no buildings, not as an error.

Not in scope: the ui/ chrome port and restyle — that is Restyle the ported UI to T3 conventions. A plain surface that plays a real trace can land first; the two are separable the way the render loop was.

AGENTS.md "Hit every surface" applies before this is called done, and web verification goes through test-t3-app.

Activity

  1. Dillpickleschmidt commented on Aug 1, 2026

    @Dillpickleschmidt
    OwnerAuthor

    Status: built and pushed, not yet verified in a browser. Branch feat/watch-agent-surface (86917c80a). Leaving this open and claimed until the client pass lands.

    What landed

    • apps/web/src/mindwalk/WatchAgentSurface.tsx — ours, not a verbatim copy (the equivalent of mindwalk's App.tsx/state//api/). Fetches { trace, citymap } over HTTP from GET /api/mindwalk/threads/:threadId, drives PlaybackEngine off the real trace, and drops locHeights — that mode was the static-map baseline only.
    • A minimal transport deck: play/pause, scrub, speed, step-to-start. Cadence is mindwalk's own (BASE_TICK_MS 340, speeds 1/4/16, higher speeds advancing several events per tick rather than ticking faster). It opens at the last event, as setData does, and pressing play at the end restarts. Deliberately plain — mindwalk's real Timeline replaces it wholesale in Restyle the ported UI to T3 conventions — so no keyboard shortcuts (they would collide with T3's keybindings) and no HUD/inspector.
    • Scrubbing does not pause playback, matching upstream, even though the timer then fights the drag. Faithful port; Restyle the ported UI to T3 conventions #10 brings the real deck anyway.
    • watch3d is a new singleton right-panel surface kind. Card added to RightPanelEmptyState (four cards → clean 2x2) and to the + menu — without the latter a closed surface is unreopenable, which "Reverse states" makes a bug. That is the complete entry-point set: surfaces have no command-palette entries at all, and only Diff has a keybinding, which predates the surface model.
    • Gated on isServerThread (not && isGitRepo like Diff): the trace comes from the activity store, and a non-repo root degrades to an empty citymap by design rather than failing.
    • /mindwalk-3d, MindwalkHarness.tsx, and the route-tree entries are gone; harness.css → surface.css.

    Verified

    • Typecheck clean, lint clean, rightPanelStore tests pass with one added for the new singleton.
    • The endpoint serves real data end-to-end against two real threads in the dev store — "Add 3D Agent Visualizations" (385 events, 32 marks, 15,826 files, 338 targets with a backfilled fileId) and "Take Wayfinder Ticket 6" (169 events, 148 with ids). observability reads estimated/errors: exact, as the projection ticket predicted for Claude.
    • three.js still splits into its own chunk — 578 kB raw / 148.6 kB gzipped as WatchAgentSurface-*.js, present in no other chunk. This was the real regression risk in moving the lazy import from a route into the eagerly-loaded ChatView; constraint (1) from the map holds.

    Not verified — needs a browser

    No preview automation host was available in this session, so none of the following has been checked:

    1. The scenes actually render and play a real trace in the panel.
    2. The idle draw-call re-measure this ticket names. The render-on-change gate from Make the scenes render on change, not every frame is untested against the new playing wiring — an always-true playing prop would silently restore the continuous loop and nothing above would catch it.
    3. The degraded empty-citymap case rendering as a timeline with no buildings.

    Nothing here should be called done until those three land.

  2. Dillpickleschmidt commented on Aug 1, 2026

    @Dillpickleschmidt
    OwnerAuthor

    Browser pass done — the gap this ticket was held open for

    Driven with headless Chromium against a dev server on the real .t3 state, thread Add 3D Agent Visualizations.

    The measurement that mattered: idle is zero

    Sampled every 5s with WebGL draw calls and requestAnimationFrame instrumented before page load:

    t+15s: 439 draws / 19 frames
    t+20s: 483 / 21
    t+25s: 414 / 18
    t+30s:   0 / 0
    t+35s → t+50s: 0 / 0
    

    No regression. The render-on-change gate holds with a real trace driving it. playing is useState(false) and properly driven — not the always-true hazard flagged at handoff.

    The ~30s settle is not the loop failing to stop: it is the frame-count-based lerp already recorded as an upstream defect, ~100 frames of easing at SwiftShader's 4 fps. On a real GPU that is under two seconds. This run is independent confirmation of that defect.

    Both scenes render against the real endpoint

    Terrain shows the trail arcs between touched files with directory labels; the deck reads 385/385 with the last command. The picker is back to a clean 2×2 with 3D Watch Agent as the fourth card. One canvas, no page errors, three.js in its own chunk.

    One real issue found, cause unknown

    The Tree view is misframed — off-centre and clipped, where Terrain fits correctly.

    I thought this was the deferred || 1 aspect-fallback defect from #24: a zero-sized host makes the aspect NaN, fitDistance defers on aspect <= 0 (sceneUtils.ts:28), and || 1 looks usable enough to clear fitPendingRef so the real resize never refits. The mechanism fit, and this surface mounts inside a right panel.

    I changed it to || 0 and the framing did not change. The hypothesis was wrong — the card is clicked after the panel is already open, so the host has size at mount and that path never runs. I reverted the change: it was a deviation from verbatim justified solely by fixing a visible bug, and it does not fix one.

    So the misframing is real and unexplained. It belongs to Restyle the ported UI to T3 conventions, which owns the camera and layout pass, with the || 1 path ruled out as the cause.

    Nothing else of that shape exists in the ported code — the only other numeric falsy-fallbacks are boolean guards, and the resize path already early-returns on a zero-sized host rather than fabricating a value.

  3. Dillpickleschmidt commented on Aug 1, 2026

    @Dillpickleschmidt
    OwnerAuthor

    Correction: the Tree view is fine

    The comment above reports the Tree view as misframed. It is not. The dev looked at it directly and confirmed it renders properly centred; what I read as misframing is a radial tree with a few long branches reaching out from a mostly central mass.

    I should not have asserted it. I never demonstrated the symptom — I inferred it from screenshots and then produced three explanations in turn, each of which failed its own test:

    • the deferred || 1 aspect fallback — changing it to || 0 altered nothing, because the surface opens from a panel that already has size
    • a missed refit on view switch — forcing a viewport resize altered nothing
    • a fit that assumes an origin-centred layout — the layout is a full circle (slotAngle = 2π / total), so it is centred

    A fourth attempt to measure it objectively also failed: WebGL canvases read back blank through drawImage without preserveDrawingBuffer.

    The || 0 change was reverted at the time and nothing else was altered, so no code carries the mistake. The scope note added to Restyle the ported UI has been withdrawn, and the PR description is corrected.

    Everything else in the browser pass stands: idle draw calls at zero, both scenes rendering the real endpoint, the picker back to a clean 2×2.

  4. Dillpickleschmidt commented on Aug 1, 2026

    @Dillpickleschmidt
    OwnerAuthor

    Merged in #25. The browser pass this was held open for is done: idle draw calls measured at zero on a parked view, both scenes rendering the real snapshot endpoint, picker restored to a clean 2x2. The framing concern raised during that pass was withdrawn — see the correction above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions