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:
- File browser (left rail) — context menu, drag-to-move, copy-path.
- Editor header — breadcrumb navigation with sibling browsing, copy-path, save.
- 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.checkpointconfirm gate in ADR-0034. - Delete problem classes. Full-content proposals over patch application; daemon-performed writes over agent writes; one
moveprimitive 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_fileover yourshow_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 prefershow_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_filecall 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_promptis 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_filewith the full content") that is not operator-facing and not thesystem_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_filecontract, baseline/accept, persistence, empty state.specs/ui/file-browser.md— context menu,movesemantics, breadcrumb, copy-path.
Resolved questions
- 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.
- 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.
- 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.
- Rename migration — v1 orphans. As stated in D6. These are throwaway sessions; following the move is a future refinement.
- Tool name —
propose_file. The agent is proposing a not-yet-committed version; the propose/confirm framing is the whole point.