Skip to content

3D surfaces: port mindwalk's Tree and Terrain into the web app #1

Description

@Dillpickleschmidt

Destination

Both 3D surfaces — 3D Watch Agent and 3D Diff — working in the T3 Code web app, with Browser removed from the surface picker. A faithful port of cosmtrek/mindwalk's Tree and Terrain visualizations, minus its session sidebar and judge. Watch Agent ships first; 3D Diff follows on the same citymap and scene foundation.

Notes

Domain: T3 Code web app (apps/web), server (apps/server), contracts (packages/contracts). Reference: mindwalk (MIT, © 2026 Ricko Yu) — Go backend + React/three.js frontend.

A 3D surface is a different renderer of an existing T3 query, not a new feature. Same store, same settings, same data, same server call — only the drawing differs. New state is fine when the capability is new: the commit scrubber's position has no 2D counterpart, so it owns that. Duplicating state or logic that T3 already has is drift, and it always shows up later as two panels disagreeing.

The rule binds the data, not just the settings, and that distinction cost four bugs. Building #34 it was read as being about stores and preferences, so 3D Diff went on computing its own branch aggregate, working-tree scan, whitespace plumbing and cwd handling — and every single defect on that branch came from one of them: a --minimal that only one side passed (385/61 against the panel's 384/60), a workspace-boundary retry only the 2D panel knew about (the surface died where the panel beside it recovered), checkpoint counts captured with whitespace forced off, and the endpoint's own settings drift before that. The two things with no counterpart — the per-commit breakdown and the citymap — produced none. The test is not "do the two agree?" but "is there a second thing that could disagree?" If a number can be computed twice, it eventually will be, differently. Resolved by moving per-file counts onto ReviewDiffPreviewSource so both panels read one field.

Corollary: a workaround that lives in one caller is a bug in every other one. The boundary retry sat inside DiffPanel for months and read as robustness; it was really a shared concern with one implementor. When you find a caller compensating for something, move the compensation, do not copy it.

Bounded by the fork's merge constraint, which the unification nearly broke. Sharing beats duplicating, but the sharing has to land in our files wherever it can: apps/server/src/vcs/GitVcsDriverCore.ts and apps/web/src/components/DiffPanel.tsx are upstream T3 files with 26 and 18 commits in the last 90 days, and every line we change there is a merge conflict waiting. Two rules came out of doing it wrong on #34: prefer additive — the final GitVcsDriverCore change is +114 −0, which conflicts far less than the same size of edit — and never "fix" upstream behaviour without asking. That change also claimed the 2D panel's capped line counts were a bug; they are not, the panel renders a banner saying the diff is truncated and the counts describe what it shows. A preview is meant to be bounded. What 3D needed was a second answer to a different question, so the counts are opt-in behind includeFileStats and the 2D path costs exactly what it did upstream — measured, because it is not free: ~40ms of extra git calls plus ~1.5ms per untracked file, on a path that runs every time the diff panel opens.

This was violated three times before anyone noticed, all from one root cause. #7 settled that 3D Diff is a repository view, and that decision was then allowed to answer questions it had no business answering. What data a view shows and where its settings live are different questions, and T3 keeps them at three different scopes on purpose: the diff data is repository-scoped, the selected scope is per-thread (diffPanelStore.byThreadKey), and ignore-whitespace is a global setting. Building 3D Diff as its own surface keyed by cwd flattened all three, and every gap then got filled by inventing something.

The three, all fixed together: the endpoint never sent diffIgnoreWhitespace (which defaults to on, so the two panels disagreed out of the box on any reformat); the commit scrubber hand-rolled a date format instead of formatShortTimestamp; and Inspector/Timeline carried mindwalk's HH:MM:SS clock, ignoring the 12/24-hour setting. The test to write for this class of bug asks "if the user changes this setting, does the 3D surface move too?"

Execution override. This map carries execution, not just decisions. This is a port, and ports surface their decisions by being attempted. Prototype tickets produce real code in the repo; mechanical porting lands as task tickets rather than a handoff spec.

How build tickets finish. A ticket that produces code ends by committing to a branch and pushing it — unpushed work is invisible to everyone else. It does not open its own PR and does not shepherd its own review.

Review happens in a separate pass, deliberately: a session that just spent its context building is the least skeptical reader of that build, and the findings that have mattered — the citymap build stampede, the symlink escape, the upstream-versus-ours split on the scene defects — were all caught by coming at the branch cold. The reviewing pass opens the PR, triages findings against source and against .repos/mindwalk before acting, and merges.

Macroscope is out of credits as of #33, and is not being renewed. The cold pass is now the only review, so it has to do the work Macroscope was doing. Its two catches on #30 and #29 are the pattern to imitate, and neither was the kind of thing tests find:

  • Follow every caller-supplied value to what it reaches. The diff overlay took a cwd from the client and ran git and a citymap build at it, having caught and discarded the boundary rejection — while the endpoint's own contract promised the opposite.
  • Check that two data sources agree on their vocabulary. git log --numstat reports root-relative paths and the citymap walked the directory it was handed, so a subdirectory cwd paired whole-repo steps against a fragment of a city and every column silently became a ghost.

Also worth keeping: two of its seven findings this session were wrong, and both wrong ones were graded High — one suggested a patch that would have introduced the bug it described. Verify before acting was already the rule; without a second reader it is the only safeguard left.

This is the dev's standing instruction, which is what AGENTS.md's "never open a PR unless the developer explicitly asks" requires.

Standing decisions from charting:

  • Replay first, live-watch later. Both eventually; replay is the mindwalk-shaped thing and live-watch is then "pin the scrubber to the end."
  • Data source is T3's own event store, not raw provider JSONL — chosen so classification is provider-agnostic by construction, and so remote and relay connections work. Verified against Claude and Codex only: those are the two the dev uses (18 and 2 threads; zero for the rest). ACP and OpenCode stay implemented-but-unverified behind a graceful fallback, so adding one later is a mapping, not a redesign.
  • Plain three.js — no react-three-fiber. Charting assumed r3f, but mindwalk's scenes are plain three.js components driving their own rAF loops, so a verbatim port adds only three (found resolving Render Tree and Terrain verbatim against fixture data). The three constraints agreed with the dev stand, restated for plain three: (1) lazy-loaded as its own chunk so non-users pay nothing — done; (2) render on change, not every frame — the plain-three equivalent of frameloop="demand", so a visible but idle scene costs no GPU; (3) a continuous loop only while playback is actually running, additionally paused on tab-hidden and surface-inactive. Note (2) is the binding one: pausing only when hidden would still peg the GPU on a parked, visible view, which is the case AGENTS.md names.
  • Whole tree, no aggregation. Mindwalk applies no cap and precomputes the squarified treemap server-side; the client renders it in one THREE.InstancedMesh per layer, so node count is a non-issue. Served over HTTP with gzip (HttpResponseCompression), never the websocket. Cached server-side on git commit + dirty.
  • Faithful port. No behavior changes. Any deviation from mindwalk's intended behavior is post-port work, recorded as fog. Scoped, though: this is a defence against mistranslating graphics code that renders almost right when wrong — it is not a merge obligation. This fork's merge constraint is T3 (pingdotgg/t3code), and none of apps/web/src/mindwalk/ exists there, so its T3 conflict surface is zero and a git subtree pull of mindwalk is a convenience we can give up. Diverge from mindwalk where it is a real improvement for how we use it, stay verbatim everywhere else, and record which is which. See AGENTS.md § This fork.
  • Port split. Copy scene/ (1,891 lines), playback/ (251), and types.ts (271) near-verbatim — that's where mistranslation renders almost right and costs days. Write App.tsx, state/, and api/ clean against T3. Adapt styling to this repo's conventions and Tailwind while retaining what's functionally load-bearing.
  • Browser removal is the picker card only — not the + dropdown, keybindings, or the preview surface kind. Keeps fork divergence from pingdotgg/t3code near zero. Disabled-with-tooltip is intentional UX elsewhere; we are not hiding unavailable cards generally.
  • 3D Diff is milestone two, and gets no card until it works. The charting-era instruction to build against a generic per-file overlay interface is superseded by Define the per-file overlay interface: there is no generic type — the scene's prop surface is the seam, one prop per drive mode. 3D Diff is still a second adapter, not a refactor.
  • One thread = one trace. T3's activities stream is already continuous across resumes and compactions, so no stitching is needed.

Measured during charting (this repo, from a live mindwalk serve): 15,685 files / 1,681 dirs → 6.9 MB raw, 744 KB gzipped. Rounding rects to 3 decimals → 446 KB. Excluding .repos/ → 115 KB. Note .repos/ is 12,961 of 15,685 files — 83% of the map — and visually dominates the terrain. Both fixes are deliberately deferred as post-port fog.

Skills: /grilling and /domain-modeling by default. test-t3-app for web verification. AGENTS.md "Hit every surface" applies before anything is called done.

Decisions so far

  • Classify read and search tool calls per provider adapter — the trace projection must run server-side: projectActivityPayload strips data to a six-key whitelist before the wire, so a client-side classifier is impossible without widening it. Only Claude uses { toolName, input, result }; Codex's unused commandActions[] makes it the best source of the five; ACP's kind survives the whitelist. Reuse classifyToolAction in packages/shared/src/toolActivity.ts. Full findings on branch research/tool-call-classification.

  • Vendor mindwalk as a reference repo — vendored at .repos/mindwalk via a plain git subtree add --squash from v0.3.0 (upstream b9c12798d). Deliberately not registered in scripts/lib/reference-repos.ts: nothing validates .repos/ against it, and registering it would make every routine sync:repos pull unrelated mindwalk changes. Diff vs main is 104 purely additive files — no shared-file divergence for upstream merges to conflict with. Update later with a deliberate git subtree pull.

  • Remove the Browser card from the surface picker — dropped from the RightPanelEmptyState grid only; the + menu, preview.* keybindings, and the preview surface kind are untouched. Net −10 lines on branch feat/remove-browser-surface-card. Note the picker was four cards, not five (no Plan card), and is now three in a grid-cols-2 — adding 3D Watch Agent restores a clean 2x2.

  • Port the citymap builder to the T3 server — landed on branch feat/citymap-builder-server, split into a pure layout half (CitymapLayout.ts) and the walk/git/cache half (CitymapBuilder.ts), served at GET /api/citymap/threads/:threadId. Differentially verified against the real Go binary: zero diffs on this repo's 15,796 files and 1,714 dirs, and identical on a mixed fixture. 752 KB gzipped, ~1s cold build. Ghosts survive as a ghostPaths: string[] parameter, so the builder stays trace-independent without deleting the behavior.

  • Render Tree and Terrain verbatim against fixture data — both scenes render this repo's citymap in the web app and match a live mindwalk serve screenshot-for-screenshot; landed on branch feat/3d-scenes-verbatim. Copy lives at apps/web/src/mindwalk/ (MIT notices in THIRD_PARTY_NOTICES.md); deviations from verbatim are only tsconfig-strictness assertions and repo autoformat, all erased at runtime. Fixture path is a throwaway /mindwalk-3d?threadId= route in static-map (locHeights) mode — no trace projection needed for the baseline.

  • Make the scenes render on change, not every frame — carved out of Restyle the ported UI to T3 conventions and landed on branch feat/scenes-render-on-change. Stayed plain three.js: OrbitControls.update() already returns whether the camera moved, so the gate is cameraMoved || labelsEasing || lerpMoving || playbackRunning, with an invalidate() from every data effect and an IntersectionObserver + document.hidden awake gate. Measured, not inspected: an idle visible scene goes from 397 draw calls per 5s to zero. Cost two behavior changes — autoRotate dropped, and the firefly pulse moved under a new playing prop.

  • Close the agent-lenses data gap — the interior was never missing: T3 ingests Claude via the Agent SDK stream, which already forwards subagent tool calls tagged parent_tool_use_id, and T3 discards the field. So it's an attribution gap, not an ingest gap. Extend T3's model with one optional top-level parentToolCallId on ItemLifecyclePayload — self-recursive, so depth and ancestry fall out; everything else on mindwalk's AgentNode is derived server-side, never stored. Model only: no chat-UI change. No backfill — old threads lose lenses permanently. Lenses are a per-provider capability via mindwalk's own traceAvailability; the all-providers promise binds the main trace and citymap, which are untouched. The main trace now excludes attributed children, matching mindwalk's Main lens.

  • Measure the fidelity of forwarded subagent tool payloads — measured against a live SDK run, and it corrected the premise of the ticket above. Subagent content arrives only as complete assistant/user messages: 0 of 32 stream_event messages carried a parent_tool_use_id, and T3 emits tool items only from the stream_event path. So subagent tool calls surface nowhere in T3 today — they are on the wire and dropped, not "already in the store, flattened." Payloads are full, not abridged (complete file_path, command, description, and a 9.5 KB tool_result), so attribution remains viable. Two consequences: Persist the subagent attribution edge must start emitting subagent items rather than just tag them, and the main-trace exclusion rule costs nothing — chat is already quiet during a Task, so excluding matches T3's behavior as well as mindwalk's.

  • Project T3's activity stream onto mindwalk's Trace model — settled every design question; the build is Land the trace projection and snapshot endpoint. One endpoint returning { trace, citymap }, because the two are mutually dependent (citymap needs target paths for ghosts; trace needs fileId and filesInRepo). Copy mindwalk's classifier and ~250 lines of path scraping verbatim; leave toolActivity.ts alone — the ticket's "reuse classifyToolAction" premise was wrong twice over: it is not exported and has one consumer (ACP title strings), and the tool census settles it — Bash is 66% of all 3,600 tool calls and Grep/Glob are absent entirely, so scraping is load-bearing, and T3 files a Read under itemType dynamic_tool_call, which its own classifier would call other. Claude's payload already is mindwalk's { toolName, input, result: { content, is_error } } shape and file_path is snake_case — the casing Capture live tool-call payloads for Cursor/Grok and OpenCode #11 left unverified is now verified without a capture. Fold at tool.completed; tool.denied becomes an error event; unfinished calls and all T3 bookkeeping activities drop. Compaction comes from activity kind context-compaction, not the context_compaction item type (non-tool item types never become activities). User-message marks merge by createdAt — turn-anchoring is impossible, user messages are always stored turnId: null. Caching is exact, unlike the citymap's: an append-only log makes the activity count a perfect key, so no TTL. Codex's commandActions is typed, so not weak — the one deliberate deviation.

  • Land the trace projection and snapshot endpoint — landed on branch feat/trace-projection-endpoint; GET /api/mindwalk/threads/:threadId returns { trace, citymap } from apps/server/src/mindwalk/. Differentially verified against the real Go binary over the same Claude session: 1204/1204 identical actions, 1284/1284 of our events have an exactly matching target set, and the per-file touch map agrees on 146/150 paths with none differing. Our event count is lower and more correct — Claude's JSONL replays history on resume, so 1,819 of mindwalk's events are only 1,348 unique calls, while T3's append-only log records each once. Confirmed the design's bets: 3,030 of 3,971 targets are weak Bash scrapes, and reads grades estimated almost everywhere. Two deviations beyond the agreed one: Codex's error grade is exact (T3 stores status/exitCode structurally, so estimated would be a lie), and the exec/js JS-wrapper branches are dropped as unreachable. The build also corrected two design assumptions — provider must come from the session row, not the active-only thread shell, and an unnamed provider is read by payload shape rather than assumed.

  • Wire the 3D Watch Agent surface to the real snapshot endpoint — the surface is live: a watch3d singleton card in the picker and the + menu, fed by GET /api/mindwalk/threads/:threadId, with the throwaway /mindwalk-3d route removed. Verified in a browser — idle draw calls measured at zero on a parked view with playing correctly driven, both scenes rendering 385 events over 15,826 files, three.js still in its own chunk. A Tree-framing concern raised during that pass was investigated and withdrawn: the view is correctly centred.

  • Restyle the ported UI to T3 conventionsthe one ticket that deliberately departs from the faithful-port rule, since replacing mindwalk's visual identity is the deliverable. Structural greys became distances from --background toward --foreground (one table, both themes, LIGHT_MIX_GAIN 1.4); the coloured actions stay declared by the dev's call; mindwalk's 900/1180 breakpoints stay. Selection panning is now measured and reversible — upstream's fixed 348px right-hand reserve dodges the wrong edge below its own 900px rule, and it never restores. Found by the browser pass: the token resolution had never actually run (oklch serializes as oklab(…), so a # guard rejected every read and the static fallback was silently in use in both themes).

  • Define the per-file overlay interface — the seam already existed and upstream put it above the scenes: they take city + FilePlayback, never a Trace. So there is nothing to refactor, and no generic overlay type — one spanning both views would union two unrelated vocabularies with every field null for one consumer. The scene's prop surface is the seam, and diff lands in CityScene rather than a sibling scene (five of six preceding commits fixed the same camera logic twice over, once per scene). The bigger change is to 3D Diff itself: OrchestrationCheckpointSummary is out — turn checkpoints snapshot the whole workspace including the dev's own scratch work, so summing them renders console logs as terrain. 3D Diff steps commits, its range delegated to T3's existing ReviewDiffPreview sources so its final frame is literally the 2D Diff panel's diff. Terrain only, no tree; one stacked column per file, height ∝ additions + deletions (churn, not net), green from the ground up and red capping it — red on top because a tilted camera occludes bases and the footprint tile swallows small bottom segments. Deleted files ride the existing ghostPaths parameter. The surface is repo-scoped, a sibling of the 2D diff surface. Builds: Serve the commit-stepped diff overlay and Render the stacked diff terrain and ship the 3D Diff surface. Refined after the fact: "one prop per drive mode" was the wrong end state — locHeights?: boolean plus a third optional prop gives eight representable combinations for three valid states, and an undocumented precedence rule. #29 instead deepens the seam to a single columns: readonly Column[], each column a file with stacked segments, so attention, size, and diff are all plain functions computed by the surface. Stacking then needs no second mesh, the height math becomes unit-testable, and CityScene stops growing when a fourth mode arrives. Corrected on sight of the built surface: this ticket said step i shows the sum of commits 1..i, and that was wrong — every step is one commit, isolated, the same reading as the working tree, a branch against its base, a single turn, and any ordinary git client. Nothing in T3 accumulates. The built scrubber's bars were already per-commit, so the terrain and the bars disagreed on one screen. Height scale therefore moves from the frame to the whole range, or every commit's busiest file reaches full height and a three-line change looks like a three-thousand-line one.

  • Serve the commit-stepped diff overlay — landed on branch feat/diff-overlay-endpoint; GET /api/mindwalk/diff?cwd= returns { range, steps, citymap } from apps/server/src/mindwalk/DiffOverlay.ts, keyed by working directory rather than by thread. Differentially verified: every commit step matches an independently spelled git show --numstat — line-oriented, not the -z grammar the service parses, so the parser cannot agree with itself — across this repo and a scratch repo built for the awkward shapes (a merge, two empty commits, an empty message, a binary). The window is reused rather than reimplemented: ReviewService.getDiffPreview is called for its refs and its patches thrown away, which costs a measured 255ms of a 467ms warm request and buys agreement with the 2D panel by construction. Three deliberate deviations: --first-parent --diff-merges=first-parent, because git log shows no diff at all for a merge and a merged branch's work would otherwise render as a step that changed nothing; --no-renames, so a rename reads as one building emptying and another filling; and a binary arriving as 0/0 rather than being dropped. Fallback for an undiverged branch is the last 20 commits, reported with a null baseRef. Corrects #29's verify step: the final frame matches the 2D panel on the path set and on any file touched once, but not on numbers — the frame sums churn, and git diff nets it out. No second cache: the steps are 1 KB of JSON against the citymap's 5.2 MB.

  • Render the stacked diff terrain and ship the 3D Diff surfacethe destination's second surface is live, on branch feat/3d-diff-surface. The seam landed as scoped: CityScene takes columns: readonly Column[] and nothing else, locHeights is gone, and scene/columns.ts holds one pure function per mode — so stacking needed no second mesh and the height maths is unit-tested without WebGL. Two surprises: static-map mode had no caller at all (the throwaway route went with #23) and now serves a trace that recorded nothing, and selection stayed in the scene rather than the seam. Height is linear in churn, unlike the static map's log ramp — tens of files, no tail to compress. The segment minimum was corrected by looking at it: flooring both halves independently makes every small column read fifty-fifty whatever its real split was, so it is now bought out of the larger half and the column's height stays honest. The Timeline's histogram is replaced by the commits themselves, one bar each rather than bucketed, in a sibling scrubber — the action buckets and marks are trace vocabulary with no meaning here. Verified in a browser: the final frame's path set and churn totals are identical to git's (40/40, +4884/−856), idle draw calls are zero in both themes at the final frame and mid-scrub, and the one deleted file in range renders as a red ghost column. Green and red resolve from T3's --success/--destructive.

  • Give 3D Diff the same scopes as the 2D Diff panelyes, the same four scopes, split into two builds: Give 3D Diff the 2D panel's four scopes, then Let 3D Diff name its own base ref behind it. The seam is not tidiness — whether 3D follows the 2D panel's stored selection or keeps its own is a scopes question, and it decides whether the picker is one query param or a second combobox; they also verify differently, one by looking at the surface and one differentially against git log. Branch changes was the ticket's one wrong premise: it called the scope presentation-only, but #29 removed step accumulation so there is no aggregate frame to reuse, and a client sum would be churn where the 2D panel shows net — so it earns a server-computed git diff --numstat base...HEAD and a new overlay field, because a scope disagreeing with the 2D entry of the same name is the exact failure this ticket exists to prevent. Turn scopes are free and their paths line up — checked, not assumed: checkpoint files parse to a//b/-stripped repo-root-relative paths, the citymap's vocabulary. The surface takes a thread ref as well as cwdactiveThreadRef was already two lines above the mount, so keying by cwd was a prop choice. And the 255ms prize is conditional, not outright: Automatic stays the default and still needs getDiffPreview. Amends #7: the surface is no longer purely repo-scoped — working tree and branch are repo scopes, turns are thread scopes, the same mixture the 2D panel already is.

  • Give 3D Diff the 2D panel's four scopes — landed on feat/3d-diff-scopes, merged at 992594957. Scope selection read reactively from diffPanelStore, resolution shared via diffScope.ts, per-file counts unified onto getReviewDiffPreview --numstat (opt-in includeFileStats; the 2D panel pays nothing), one numstat parser in vcs/numstat.ts. The scrubber is a drill-down of Branch changes with null as the whole-range position. The cold pass earned its keep twice: the new files contract field was required and would have been a decode failure against any older server (now wire-optional with a decoded default), and the boundary retry died on exactly the scopes that disable the review-preview query (target now read from server config). The browser pass surfaced the workspace-boundary retry silently answering from a different repository — verified byte-identical in upstream (PR Add diff scope switching and provider update settings pingdotgg/t3code#3169); the 2D panel does the same swap invisibly and 3D exposes it because a map must agree with a diff. Dev ruling: intended behaviour, no divergence, no fix — and unreachable on a normal launch. The dev stack passes ~/Projects as the server's cwd argument (upstream's own knob) so turn scopes are testable. One turn's diff reads fine as terrain; the scratch-work objection applied to summing, not viewing.

  • 3D scene loses its selection marker on a theme switch — one dependency each on fix/scene-selection-after-theme-switch, merged at 3b6939eea: palette on TreeScene's selection effect and on CityScene's selection-pan effect. The cause is narrower than the ticket's read. Every other effect that paints state onto the stage already survives a palette rebuild by carrying a palette-derived dependency it never reads — TreeScene's playback and trail effects carry colors, CityScene's trail effect carries palette — so the two selection effects had missed an idiom the file already had, rather than needing a new mechanism. And the split is marker/pan, not Tree/City: TreeScene pans inside the marker's own effect and lost both, while CityScene marks a selection by recolouring the tile and column stack, an effect that already depended on colors and never broke. Verified in a browser by measuring framing against the unfixed build at the same step — 19.4% of terrain pixels and 10.9% of tree pixels differ after a theme switch, against 0.22% and 0.3% before one, the diff a solid block the size of the whole stage. The marker itself was not separately observed: at 15,845 files the ring is ~3px and the beam a sub-pixel hairline, indistinguishable from the trail arc beside it, so what is measured is the effect re-running — and ring.visible = true is three lines above the pan in that same body.

  • Subagent attribution: adopt upstream, don't inventupstream PR #5219 (t3dotgg, live-tested, active) ships native tool attribution (agentId/parentToolUseId), Claude and Codex linkage, a quiet timeline, and its own Agents right-panel, all additive with zero migrations. #20 is retitled and parked as an adoption/mapping ticket blocked on that PR merging upstream and then into the fork; #21 reads upstream's fields when it unblocks. Two collisions avoided: duplicate attribution fields in contract files, and Adopt upstream's subagent attribution once pingdotgg#5219 lands #20's "no suppression" chat stance, which [WIP] feat: native subagent & workflow observability pingdotgg/t3code#5219's quiet timeline reverses. The lens gate in TraceProjection.ts already treats the edge as optional, so adoption is a field-name pass, not a redesign.

  • Scene shortcuts through T3's keybinding registry rather than mindwalk's own key handler, so they appear in the command palette and honour user rebinds.

Not yet specified

  • approval.requested and runtime.error as mark types — both are genuinely interesting on a timeline ("agent stalled waiting on you", "the provider crashed"), and both were deliberately excluded from events by Project T3's activity stream onto mindwalk's Trace model because mindwalk's Event means "the agent used a tool" and errorRate is per-tool-call. Marks are the right home; mindwalk has no upstream equivalent, so this is post-port.

  • Live-watch mode — following an in-progress turn, a live edge on the scrubber, detach-to-scrub.

  • Mobile — late-stage. React Native needs expo-gl or react-native-wgpu; revisit once web is proven.

  • Post-port deviations.repos/ exclusion globs and float rounding. Both measured and worthwhile, both off-limits until the faithful port lands. Now also: mindwalk's idle camera drift (autoRotate) is gone, dropped by Make the scenes render on change, not every frame because unbounded idle motion has no bounded form. If the drift is missed, the cheapest way back is scoping it to playing, which already earns a continuous loop.

  • Timeline / Hud / Inspector / Dock port specifics — 1,044 lines of mindwalk ui/ that get copied then restyled.

  • Citymap cache key precision — settled for now as commit + dirty with a 30s TTL, matching mindwalk. The hole is real and inherited: dirty does not change when a dirty file's contents change, so an edit within the TTL serves a stale map. Tightening it needs a tree mtime fingerprint. Post-port, like the other measured-but-deferred wins. (The worktree half of this is answered — the root is worktreePath ?? workspaceRoot.)

  • Citymap fallback walk exposes gitignored files when git reports zero workspace files (a repo whose contents are entirely ignored). Faithful to mindwalk, whose listFiles gates on len(out) > 0 and otherwise falls through to WalkDir — flagged by review on #13 and deliberately preserved. Revisit alongside the other post-port deviations.

  • Every mindwalk easing is frame-count based, not time based — the terrain lerp moves 13% of the remaining gap per frame, halos 12%, label fades 16%, and OrbitControls damping decays per update(). So the intro animation and the post-drag settle run at whatever the display refresh is: twice as fast on 120Hz as on 60Hz, and minutes long under a software renderer. Faithful to upstream and harmless at real frame rates, but it is the reason the frame profile on Make the scenes render on change, not every frame needed a 75-second settle. Fix with a delta-time lerp once the port is settled.

  • Three upstream mindwalk defects preserved verbatim, found by review on #16 and confirmed present in .repos/mindwalk at v0.3.0: dirLabels.ts:100 scales labels by Euclidean camera distance instead of camera-space depth, so edge labels pop at the wrong zoom on the tilted camera; CityScene.tsx:150's || 1 aspect fallback short-circuits the deferred fit in a zero-sized host; recorder.ts:72 calls captureStream outside the Promise executor, so it throws synchronously instead of rejecting. Fix once the port is settled and a change can be verified against the reference in isolation. Nothing from this fork goes upstream.

  • Nesting subagent work in the chat UI — chat shows no subagent work at all today (measured). Once Persist the subagent attribution edge lands, subagent items are allowed to appear, unsuppressed: they are ordinary tool-lifecycle items, so they join the per-turn work log group that already collapses by default, leaving the message body untouched. No client change needed. What stays fog is nesting them — a Task's interior currently reads as a flat run of entries in arrival order rather than folded under its launch row.

  • Subagent attribution for the other four providers — Claude gets the edge free. Codex (spawn_agent exists in rollouts) and OpenCode are unmeasured; Cursor and Grok emit no collab_agent_tool_call at all and ACP carries no parent linkage. Off the critical path — Claude lenses are enough for the surface to ship — and the question can't be sharpened until Capture live tool-call payloads for Cursor/Grok and OpenCode shows what those transports actually carry. When a provider does gain an edge, its adapter resolves whatever it has (e.g. a Codex parent thread id) to the launching call id.

  • mindwalk's verify patterns do not match this environment's toolinggo test / npm test / pnpm test / pytest / cargo test / make test, against a repo that runs vp test run and projects that use CMake. Measured on the real store: 1 verify event across 3,805. So stats.editsAfterLastVerify, the verify bar on the histogram, and the "did it check its work" reading are all dead until the pattern list grows. Faithful to upstream and left alone by Land the trace projection and snapshot endpoint; post-port, like the other measured-but-deferred deviations.

  • What 3D Diff's inspector holds — clicking a column selects it (highlight plus a camera pan) and clicking the ground clears it, but there is nowhere for per-file detail to go; the hover readout carries +N −M in the meantime. Watch Agent's Inspector is trace-shaped — a list of TraceEvents — so it is not reusable, and what the diff equivalent should be (which commits touched this file, jump to one, the file's own patch) is a question the surface will answer better than an argument. The Dock is already data-driven, so this is a descriptor and a component, not a refactor.

  • Two 3D surfaces open on one repo fetch the citymap twice — Watch Agent takes it from GET /api/mindwalk/threads/:threadId and 3D Diff from GET /api/mindwalk/diff?cwd=, and both are singleton surfaces in the same panel, so this is reachable rather than theoretical. Measured at 5.2 MB raw / ~750 KB gzipped each. The server cache means the second build is cheap; the wire is not. Deferred with the other post-port measured wins, and note the shape of the fix is a client-side citymap cache keyed on commit + dirty rather than anything the endpoints change.

  • ~5% of Claude's tool calls never reach T3's activity store — measured while differentially verifying the trace projection: the JSONL for one session holds 1,348 unique completed tool calls and T3 recorded 1,283, with the shortfall spread evenly across every tool (Bash −23, Edit −15, Read −13, Write −2 …) rather than concentrated in one type. That even spread rules out a missing item-type mapping and points at dropped ingest events. Invisible in chat, visible in the city as buildings that never light. Not a 3D-surfaces question — it belongs to whoever owns provider ingestion — but the trace is the first thing that made it measurable.

Out of scope

  • Live capture of Cursor, Grok, and OpenCode tool payloads (#11, closed) — the dev uses none of the three, and both providers in use resolved without a capture. Verifying the rest is unobservable work; the classifier stays provider-agnostic so they can be added as a mapping later.
  • Seven upstream defects in mindwalk's shell scraping, preserved verbatim — found by review on #24, each confirmed in .repos/mindwalk at v0.3.0: verifyCommand matches substrings so echo 'go test failed' grades as verify (adapter.go:1027); a/ and b/ diff prefixes are stripped from every path, not just patch text (:831-832); a relative path merely starting .. is treated as outside the repo (:709/720/730); segment splitting ignores shell quoting, so rg 'foo|bar' misclassifies (:867/914/945); sed script-arg accounting can drop a real read path, and sedReadsOnly accepts a script containing w; ~ is never expanded. Fixing any of them trades the measured 1204/1204 action parity for an unmeasurable gain, so they wait until the port is settled.
  • Video export (playback/recorder.ts) — ported verbatim and wired to nothing. Recording a canvas to webm is a feature in its own right, not part of getting the surfaces working.
  • Mindwalk's session sidebar (ui/SessionRail.tsx, 316 lines) — T3 already scopes threads and projects.
  • The judge / evaluate system (ui/ReportPanel.tsx, internal/judge, mindwalk analyze) — "evaluate this session with your local CLI" is cut.
  • Mindwalk's Go binary — the citymap builder and trace adapters are translated into Effect TS on apps/server; we do not ship or depend on the Go server.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions