You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Wire the 3D Watch Agent surface to the real snapshot endpoint #23
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.
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:
The scenes actually render and play a real trace in the panel.
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.
The degraded empty-citymap case rendering as a timeline with no buildings.
Nothing here should be called done until those three land.
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.
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.
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.
Activity
Dillpickleschmidt commentedon Aug 1, 2026
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'sApp.tsx/state//api/). Fetches{ trace, citymap }over HTTP fromGET /api/mindwalk/threads/:threadId, drivesPlaybackEngineoff the real trace, and dropslocHeights— that mode was the static-map baseline only.BASE_TICK_MS340, speeds 1/4/16, higher speeds advancing several events per tick rather than ticking faster). It opens at the last event, assetDatadoes, and pressing play at the end restarts. Deliberately plain — mindwalk's realTimelinereplaces 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.watch3dis a new singleton right-panel surface kind. Card added toRightPanelEmptyState(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.isServerThread(not&& isGitRepolike 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
rightPanelStoretests pass with one added for the new singleton.fileId) and "Take Wayfinder Ticket 6" (169 events, 148 with ids).observabilityreadsestimated/errors: exact, as the projection ticket predicted for Claude.WatchAgentSurface-*.js, present in no other chunk. This was the real regression risk in moving the lazy import from a route into the eagerly-loadedChatView; 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:
playingwiring — an always-trueplayingprop would silently restore the continuous loop and nothing above would catch it.Nothing here should be called done until those three land.
Dillpickleschmidt commentedon Aug 1, 2026
Browser pass done — the gap this ticket was held open for
Driven with headless Chromium against a dev server on the real
.t3state, thread Add 3D Agent Visualizations.The measurement that mattered: idle is zero
Sampled every 5s with WebGL draw calls and
requestAnimationFrameinstrumented before page load:No regression. The render-on-change gate holds with a real trace driving it.
playingisuseState(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
|| 1aspect-fallback defect from #24: a zero-sized host makes the aspect NaN,fitDistancedefers onaspect <= 0(sceneUtils.ts:28), and|| 1looks usable enough to clearfitPendingRefso the real resize never refits. The mechanism fit, and this surface mounts inside a right panel.I changed it to
|| 0and 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
|| 1path 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.
Dillpickleschmidt commentedon Aug 1, 2026
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:
|| 1aspect fallback — changing it to|| 0altered nothing, because the surface opens from a panel that already has sizeslotAngle = 2π / total), so it is centredA fourth attempt to measure it objectively also failed: WebGL canvases read back blank through
drawImagewithoutpreserveDrawingBuffer.The
|| 0change 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.
Dillpickleschmidt commentedon Aug 1, 2026
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.