ADR-0084: Session changeset in the files panel¶
- Status: Accepted, implemented
- Date: 2026-10-26
- Issue: #331
- Files:
internal/session/changeset.go,internal/agent/local/filetrace.go,cmd/milk/changeset.go,cmd/milk/panel_project.go,cmd/milk/theme.go
Context¶
During a long run neither the TUI nor an operator can answer "what has this
session changed so far?" without reading the transcript or shelling out to
git status — and in non-git workspaces not even that works: the files panel
(panel_project.go) styled every entry identically and nothing tracked
session-touched paths at all.
Decision¶
One session-scoped trace + git enrichment, rendered as the files panel's changeset.
- Tracing at the dispatch boundary. The five mutating tools
(
write_file,edit_file,multiedit,delete_file,move_file) declare their deltas indispatchTool; they are recorded only when the tool's result carries no error. Each record is an O(1) map insert into a package-level registry keyed by session ID (internal/session) — no stat, hash, or diff, per the issue's performance budget. Kinds are net-aware: created-then-deleted nets to nothing, a moved file this session created stayscreatedat its new path,move_filerecords both sides. - Git as truth where it exists.
git status --porcelain -uall(the plumbing the git panel already used) runs off the UI goroutine on the panel's existing 3s poll — a burst of tool calls between ticks schedules exactly one refresh — and merges over the trace: porcelain is authoritative for working-tree state (it catches user-side edits and corrects kinds), and traced entries the index reports clean are stale (reverted or committed) and drop out. Directories known to be outside a repo stop the refresh entirely (re-probed on cwd change, session build, or re-toggling the panel — e.g. after a mid-sessiongit init). - Rendering consults, never computes. Results land on the model from
Update;Viewrenders the cached lines as before. The tree builder takes the merged map and adds: achanges 1A 2M 1Dsummary line under the title (deletions never appear as rows — gone from disk — so counts are where they stay visible), a compactA/M/D/Rglyph + per-kind color on changed files, and a muted·on ancestor directories so changes under collapsed branches stay discoverable. Without a changeset the rendering is byte-identical to the flat tree. - Both skins.
styleChange{Created,Modified,Deleted,Renamed}are filled inrebuildStylesper skin — adaptive pairs foroff, supermilk's own ANSI ramp forsupermilk; the zero value renders plain.
Consequences¶
- Non-git workspaces get first-class changeset visibility (previously impossible); git workspaces get real per-file status in the same place.
- The trace is in-memory and session-keyed: it never enters session JSON, dies with the process, is capped (4096 paths/session FIFO, 8 sessions FIFO in-process), and follows session switches naturally.
write_filecannot tell create from overwrite without a pre-write stat (forbidden by the budget) — outside a repo an overwrite of a pre-existing file showsAuntil edited;git statuscorrects it wherever a repo exists. edit/multiedit are the modify tools.- Escalation-only surfaces (claude-cli) don't cross
dispatchTool, so in non-git workspaces their edits aren't traced — git workspaces still show them via porcelain.
Alternatives considered¶
- Git-only (issue): loses non-git workspaces, where users can't run
git statusthemselves. - Polling/diffing file-index snapshots: subprocess + full diff even when nothing changed, and still no agent attribution without git.
- mtime/content hashing: O(files) per refresh — the class of problem ADR- 0057 / issue #198 exist to avoid.
- Showing it in the git panel: graph-centric and repo-required; the files panel is where file-level navigation lives.
- A changeset filter/toggle (issue's optional item): deferred — the summary line + glyphs answer the at-a-glance question first.