ADR-0051 — File-editing overhaul: browser interactions, breadcrumb navigation, and an agentic edit panel

  • Status: Accepted
  • Date: 2026-06-27
  • Deciders: karasu (operator)
  • Related: ADR-0003 (doc-first), ADR-0011 (substrate escape hatch), ADR-0031 (transcript parts rendering), ADR-0034 (issue-bound todos / operator-confirm via kaged.checkpoint), ADR-0043/0044 (UI PWA), ADR-0050 (git integration — in progress; soft dependency)

Context

The current file-editing surface is a starter Monaco integration: browse the tree, open a file, edit, save. It works, but it is missing interaction affordances operators expect, and it offers no assisted editing despite a large class of edits being trivially LLM-amenable (rename-this-across-the-file, reformat, mechanical refactors).

This ADR overhauls file editing across three areas:

  1. File browser (left rail) — context menu, drag-to-move, copy-path.
  2. Editor header — breadcrumb navigation with sibling browsing, copy-path, save.
  3. Agentic edit panel (right rail, new) — a constrained assistant for quick LLM-driven edits with a propose/accept loop.

The first two are mostly table-stakes UI and carry little contested design. The agentic edit panel is the architecturally significant decision: it introduces a new DSL primitive (file_editing), a new built-in tool, a new persisted session kind, and a new operator-agency surface. The bulk of this ADR's weight is there.

Granular implementation is delegated to two specs: specs/ui/file-editing.md (panel, tool contract, baseline/accept model, persistence) and specs/ui/file-browser.md (context menu, move semantics, breadcrumb).


Decision drivers

  • Operator agency is load-bearing. Agent proposes, operator confirms. Nothing mutates the working tree without an explicit accept. Direct parallel to the kaged.checkpoint confirm gate in ADR-0034.
  • Delete problem classes. Full-content proposals over patch application; daemon-performed writes over agent writes; one move primitive over a rename/move split.
  • Minimal surface. The edit panel is for quick assisted edits, not a second session engine. No tools (beyond the one intrinsic proposal tool), no orchestration, no todos/issues/checkpoints/queueing.
  • Reuse the substrate, don't fork it. The agent run-loop, streaming, and persistence already exist; reimplementing them for the panel would be a divergence class.
  • DSL is the portable artifact. Enabling assisted editing is an explicit operator decision expressed in project config, hot-reloaded.
  • Self-hostable / offline-first. No new external dependency.

Decisions

D1 — Edit panel is a new session kind, not a new engine

The panel runs on the existing run engine as session kind: "edit". The kind discriminator opts the session out of: tool allowlists, run queueing/concurrency gating, issue binding, kaged.todo, and kaged.checkpoint. It carries exactly one intrinsic tool (D2) and nothing else.

  • Rejected: a separate lightweight edit-loop implementation. It would duplicate streaming/persistence/model-invocation and drift from the main loop over time.
  • Consequence (good): reuse, behavioural consistency, free streaming and persistence.
  • Consequence (bad): the engine must cleanly honour the kind=edit opt-outs; these guards need explicit tests so orchestration features can't leak in.

D2 — Proposals are full-content via an intrinsic tool

The agent proposes changes by calling a single intrinsic tool that takes the entire new file content — no patch, no path argument (the panel is single-file scoped, so "which file" is never ambiguous).

propose_file(content: string)
  • Naming: I'm recommending propose_file over your show_file. The agent isn't displaying, it's proposing a not-yet-committed version, and the propose/confirm framing is the whole point. Low-stakes call — if you prefer show_file's mental model, say so and I'll flip it in the doc.
  • Rejected: a patch/diff-applying tool. Patch fuzz and apply-failure is an entire bug class; full-content replacement deletes it outright and matches the "quick edit" scope.
  • Consequence (bad): token cost on large files. Mitigated by a size threshold (open question Q1) that warns and steers to a real session.
  • Transcript rendering: the propose_file call renders as a compact "proposed changes" chip that drives the diff view — it does not dump file content into the transcript (density constraint; consistent with ADR-0031 tool-call grammar). The content lives in the Monaco diff view, not the message log.

D3 — The daemon writes, never the agent

The agent only ever emits proposed text. On Accept, the daemon performs the write through the same file API the browser uses for create/rename, path-confined to the project root.

  • Consequence: the agent cannot touch the working tree, which deletes the "assisted edit escapes the sandbox via a file write" class and keeps the propose/confirm invariant honest. Accept is a privileged daemon op — we already have the API from the browser work.

D4 — Baseline and accept semantics

  • Baseline = on-disk content when the panel opens, advancing to the last accepted content thereafter.
  • Diff = latest proposal vs current baseline. Inline / side-by-side toggle.
  • Accept writes the proposal, advances the baseline.
  • Reject / keep talking leaves the baseline untouched; the agent iterates from its own last proposal (already in context).
  • External-edit divergence: if the file changes on disk (direct Monaco edit, git op, another session) while an edit session is live, detect via hash/mtime and, on the next run, re-inject the current disk content with a "file changed on disk" marker rather than silently diverging.

D5 — Enablement and DSL surface

A file_editing block enables the panel. Presence of file_editing.model is the enable flag — there is no default model, so an absent block means the feature is off and the panel renders its empty state with the enable snippet.

file_editing:
  model: smart-generalist           # required; presence enables the feature
  system_prompt:
    - config:/prompts/shared_header.md   # optional, config: ref, hot-reloaded
    - config:/prompts/files.md   # optional, config: ref, hot-reloaded
  • No model ⇒ no feature. You can't enable assisted editing without naming what runs it, which deletes the "enabled-but-unconfigured" class.
  • system_prompt is optional. Default is no operator prompt. Per the federated-override convention this block is set-not-merge; a per-session model override remains available in the UI.
  • Two prompt layers, kept distinct. There is always an intrinsic harness prompt (mechanics only: "when you change the file, call propose_file with the full content") that is not operator-facing and not the system_prompt. "No system prompt" therefore still yields a functioning tool-caller — the operator prompt is for persona/rules, the harness is for the contract.
  • Empty-state copy shows exactly the snippet above, plus a one-line note about context limits steering heavier work to a real session.

D6 — Persistence and lifecycle

One persisted edit session per file path. A Clear button wipes its history so a fresh edit on the same file starts cold. These are ephemeral by intent — use for a task, then nuke.

  • Rename/move: for v1 the edit session for a path is orphaned on rename (simple; these are throwaway anyway). Migrating context across a rename is a future refinement, not a v1 requirement.

D7 — File browser interactions

  • Keep the existing show/hide hidden files toggle.
  • Right-click context menu: New File, New Folder, Rename, Delete (destructive → confirm), Copy Path, Cut / Copy / Paste (Copy = duplicate, Cut+Paste = move).
  • Copy Path yields a project-relative path by default — that's what the agent consumes and what's portable into a session. Absolute path is a secondary option.
  • Drag-and-drop moves files/folders into folders.
  • One move(from, to) primitive underlies Rename, DnD, and Cut/Paste — rename is just a move within the same directory. Deletes the rename-vs-move distinction.
  • Future (deferred): dragging a file into a session drops a path string (the agent reads the file itself), not the file contents. Not in this ADR.
  • Guards needed: drop-into-self, drop-onto-existing (overwrite vs reject).

D8 — Breadcrumb navigation in the editor header

The editor header shows a breadcrumb of the current path. Each segment is clickable and opens a sibling dropdown — directories navigate, files open — VSCode-style (per reference screenshots). The header also carries a Copy Path button and the Save button.

D9 — Monaco: minimap now, git gutter on a deferred data source

  • Minimap enabled, operator-toggleable, preference persisted.
  • Git change markers: define the gutter-decoration rendering contract now (gutter fed by a diff source), but defer the data source to ADR-0050. This ADR does not block on git; the gutter is a stub contract until ADR-0050 lands the diff API.

Decision outcome

Adopt D1–D9. Implementation detail flows into:

  • specs/ui/file-editing.md — panel, propose_file contract, baseline/accept, persistence, empty state.
  • specs/ui/file-browser.md — context menu, move semantics, breadcrumb, copy-path.

Resolved questions

  1. Large-file threshold — 256 KB, warn-and-proceed. Covers typical source files (~1k lines); blocks minified JS, large JSON, lock files. Operator sees a size warning with a "this belongs in a session" nudge, and confirms to proceed. Not a hard block — respects operator agency.
  2. External-edit reconciliation — auto re-inject with marker. Matches D4's stated direction. On the next run after a disk change, the current disk content is re-injected with a "file changed on disk" marker. The operator retains the accept/reject gate.
  3. Clear semantics — delete the session row entirely. Matches D6's "ephemeral by intent" framing. Simpler than truncating in-place. Lazily re-created on next use.
  4. Rename migration — v1 orphans. As stated in D6. These are throwaway sessions; following the move is a future refinement.
  5. Tool name — propose_file. The agent is proposing a not-yet-committed version; the propose/confirm framing is the whole point.