Files tab — when the Edit button is not there

Editability is decided by the server (editable / editableReason on the GET, fileEdit on /api/health) and never re-derived in the browser. Each pane below is the same file-preview.tsx card in a different state.

Not valid UTF-8 — editable: false
fixtures/legacy-cp1252.txt 1.2 kB
1Rapport gnral — 2024
2Auteur : Franois Lefvre
3Cot estim : 12 500

The NUL-byte binary sniff passes this file, which is exactly why the strict (fatal: true) decode is the gate. content is still returned — removing it would be a breaking response change and would blank a preview that works today.

Hosted mode / kill switch — fileEdit: false
src/server/capabilities.ts 2.4 kB
42export function resolveCapabilities(env: NodeJS.ProcessEnv) {
43 return {
44 localHandoff: env.CEZ_REMOTE !== '1',
45 followups: followupsEnabled(env),
46 };
47}

No banner, no disabled button, no affordance at all — a hosted cockpit looks like today's read-only Files tab. Hiding the button is UX only; the 409 from the route is the actual control, re-derived server-side per request.

No grammar for this extension — highlighting degrades, editing does not
Dockerfile 640 B
1FROM node:22-alpine
2WORKDIR /app
3COPY package*.json ./
4RUN npm ci --omit=dev
5CMD ["node", "dist/index.js"]

langForPath('Dockerfile') is null (no dot), so the preview already renders it untinted as data-lang="plaintext". The editor must land on the same plaintext, not an error and not a different fallback — identical bytes must look identical either side of the Edit button. Editability is a UTF-8 question, never a grammar-coverage one.

Past the preview cap — tooLarge
web/dist/assets/index-D4f2a1.js 1.8 MB

Too large to preview

1.8 MB — past the preview cap. Open it in your editor instead.

Unchanged from today. hash is omitted for tooLarge and binary files, so there is no baseHash to send and the PUT could not be satisfied even if the button were forged.

Phase 2 — the agent overwrote a saved edit
src/server/capabilities.ts 2.4 kB
42export function resolveCapabilities(env: NodeJS.ProcessEnv) {
43 // rewritten by the agent at 11:42:07
44 return buildCapabilities(env);
45}

This is the direction the baseHash guard cannot cover: the agent sends no baseHash, so nothing rejects its write. The banner and the file-edited snapshot are the whole answer — the loss becomes visible and recoverable rather than prevented. Phase 1 ships the snapshot; Phase 2 ships this banner.