Opens server.cfg, drives the CM6 controller to a dirty buffer,
presses Escape to close without saving, then reopens the same file
and asserts the editor shows the original disk content — not the
discarded buffer.
Pins two invariants: native <dialog>'s cancel→close path stays
intact (no JS shortcut around Escape), and the reopened editor
fetches a FRESH fragment via htmx.ajax with CM6 re-mounting on
the new textarea. A regression that cached buffer state to "feel
snappy" would fail this test loudly.
Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md
(Tier 2 case B).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Navigates directly to /overlays/<id>?modal=<urlencoded edit path>
and asserts the routed editor auto-opens on the right file.
This is the central guarantee of the URL-addressable modals spec —
copy the URL, share it, and the recipient lands on the same modal
state. A regression in modals.js's DOMContentLoaded bootstrap (the
URLSearchParams.get("modal") → fetchAndShowRouted chain) would
silently dead-link every shared URL; this test fails loudly instead.
Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md
(Tier 2, highest-value case).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Opens server.cfg, changes the filename input to server-renamed.cfg,
clicks Save, and asserts: the routed modal closes, the old name is
gone from disk, the new name carries the original content, and the
tree row swaps over. Pins the rename-on-save branch in
routedSaveClicked (the non-is_new path that diffs originalLeaf vs
editedFilename and emits `new_path`).
Deliberately omits __filesEditor.setContent — rename should preserve
the textarea-seeded content. A regression that wrote an empty body
fails on the content-equality assertion.
Tier 1 complete: 7 tests + fixture extension. Full suite runs in
<10s on warm Chromium.
Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Creates a folder via the inline new-folder dialog's Enter-keydown
submit path, then deletes it via the inline delete-confirm dialog.
Each step asserts disk + tree state. The Enter path is the
direct-bound listener (not delegated), so it's the most likely to
break under future refactors — pinning it here surfaces such
regressions immediately.
Row-action buttons (`✕`) live inside `<li draggable="true">` — the
draggable ancestor confuses Playwright's hit-test even though real
browsers click through fine. The click uses force=True to skip the
hit-test (documented inline).
Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Opens icon.png, attaches new bytes via Playwright's file chooser
(intercepting the click → hidden-input.click() → OS picker chain),
clicks Replace, and asserts the new bytes land on disk under the
unchanged filename. Covers the multipart /files/replace endpoint and
the click → setRoutedReplacement → save-enabled UI wiring.
Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Opens icon.png and asserts the routed editor renders the binary
branch of overlay_file_editor.html: replace-zone present, save
button labelled "Replace" and disabled on open, download link
pointing back at /files/download.
The same /files/edit route serves both text and binary modes — the
server picks the template branch from is_editable() + a magic-byte
check. Without this test, a regression that flipped a binary file
into the text branch would mean rendering raw PNG bytes inside an
editable textarea (and a misleadingly working save button).
Also asserts no textarea[data-rel-path] is in the DOM, so a future
regression that left both branches enabled fails loudly.
Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tries to create `cfg`, which collides with the seeded directory of the
same name. /files/save returns 409 "destination is not a file";
routedSaveClicked routes that through fo.askConflict, which opens the
inline #files-conflict-modal on top of the still-open routed editor.
Clicking keep-both triggers a second POST with the suffixed path
(`cfg (1)`), the routed modal closes, and the new row materialises in
the tree.
This is the F4 path from 8dc14f0 ("wire askConflict into the routed
new-file 409 path"). Before that commit, the routed code branch fell
through to a generic alert(). With this test in place, a missing
call site fails loudly instead of silently.
Pins three invariants:
* The conflict dialog is INLINE, not routed — it appears without a
URL change (the decision tree in AGENTS.md "Modals: inline vs
routed" hinges on this).
* .files-conflict-path echoes the original colliding path, not the
computed suffix — the suffix is internal, the user sees the
collision.
* withCollisionSuffix("cfg") → "cfg (1)" (no dot after the last
slash → trailing-suffix branch in uploads.js).
Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Clicks `+ new file` at the overlay root, fills the routed editor's
filename + CM6 content, and clicks Create. Asserts the modal closes,
the file lands on disk, AND the new row appears in the live tree
after the debounced HTMX refresh — the last assertion catches the
class of bug where /files/save persists but
scheduleRefresh(parentOf(fullPath)) never lands a fresh listing.
The new-file modal reuses overlay_file_editor.html with is_new=True;
this test exercises the branch in routedSaveClicked that composes
fullPath from filename + data-at-folder, distinct from the
rename-on-save path the edit-mode test covers.
Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Opens server.cfg through the file-row name button, drives the CM6
controller via window.__filesEditor.setContent, clicks save, and
asserts both that the routed modal closes and that the new bytes
landed on disk under overlay_root. Guards against four classes of
regression at once: the /files/edit fragment delivering the wrong
data-rel-path, editor.js's htmx:afterSwap re-init failing to wire
__filesEditor, routedSaveClicked stopping short of closeRouted(),
and the /files/save endpoint failing to persist.
Adds a `_wait_for_routed_editor` helper centered on `.cm-content`
inside `#files-editor-fragment` — the textarea itself is
display:none after CM6 mounts, so to_be_visible on the textarea
would always fail; the cm-content surface is the real "editor is
ready" signal.
Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extracts the shared boot/serve plumbing out of live_server into
_boot_app + _serve helpers, then adds files_overlay_server: a
function-scoped fixture that monkey-patches LEFT4ME_ROOT to tmp_path
BEFORE create_app(), seeds a files-type Overlay owned by alice, and
populates the overlay root with one editable text file, one binary
file, and one nested folder. Sets up the surface area the Tier-1
files-overlay e2e tests need without duplicating the live_server
boilerplate.
Also exposes a top-level `login(page, base_url, ...)` helper so
future test modules can share it instead of re-pasting the form-POST
flow.
Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
build-editor.sh was calling npx esbuild directly for editor-entry.js
only, leaving vocab-rank.bundle.js stale when devs used the documented
rebuild path. Switch to npm run build (the single source of truth in
package.json) so both bundles are always rebuilt together. Add
vocab-rank.bundle.js to the sha256 manifest and update the vendor README
to describe both build artifacts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace phantom token refs (--border-strong, --fg-muted, --font-mono)
with the project's real tokens (--color-border, --color-muted) or a
plain font stack where no project-wide token exists.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bite-sized, TDD-style plan for the spec at
docs/superpowers/specs/2026-05-17-console-command-autocomplete-design.md.
Seven tasks (rankVocab extraction → second esbuild target → vanilla
dropdown module → stylesheet → wire-up → smoke test). Will be executed
task-by-task via subagent-driven-development.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Spec for adding srccfg-vocab autocomplete to the runtime console input
on server-detail. Reuses the editor's ranking algorithm (extracted to a
shared module) but ships a small vanilla dropdown so the console stays
independent of CodeMirror. Tab/Esc drive the dropdown; ArrowUp/Down keep
recalling history; Enter always submits the typed text, never the
highlighted suggestion.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Companion to the just-shipped files-overlay rewrite. The rewrite
verified each step's behavior live in Chromium but left no automated
browser regression net. This handoff plan specifies what to add:
* Fixture extension (conftest.py): a files_overlay_server fixture
that seeds a files-type Overlay with one text file, one binary
file, and a nested folder under tmp_path-rooted LEFT4ME_ROOT.
* 11 test cases in three tiers — Tier 1 covers the critical paths
(text edit save, create-new, 409→askConflict, binary replace,
new-folder + delete, rename-on-save), Tier 2 rounds out drag /
upload / deep-link, Tier 3 hits the server-detail download
button.
* Patterns to follow + pitfalls (the SESSION_COOKIE_SECURE=0 gotcha,
the data-rel-path location split between text and binary modes,
the htmx.ajax async wait, why os-drag-with-folders can't be
synthesized).
Pinned references at the bottom point at the existing test_editor.py
pattern model and the relevant module-header comments. Estimated
half-day for the critical 7 cases, full day for the full 11.
Lives under docs/superpowers/plans/ per the project's planning-
artifact convention. Move to specs/ if it's executed and turned into
shipped tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two updates to AGENTS.md after the files-overlay rewrite:
1. The "canonical pattern" reference at the bottom of the inline-vs-
routed modals section pointed at files-overlay.js lines ~599-664.
That file is gone. Updated the reference to point at the new
single-purpose location: editor.js's document-level click listener
and the routedSaveClicked / routedReplaceClicked / routedDeleteClicked
functions.
2. Added a "Files overlay: module layout" subsection right after the
modals one. Names the four modules under static/js/files-overlay/,
what each owns, the window.__filesOverlay action-registry contract,
and the recipe for adding a new file-row action. Future agents
touching the file manager hit a one-paragraph orientation instead
of re-deriving the layout from git log.
No behavior change. The "Modals: inline vs routed" decision tree
itself stays accurate post-rewrite: editor is routed; new-folder,
delete-confirm, conflict stay inline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The is_new branch of routedSaveClicked in editor.js used to alert on
409 and force the user to manually pick a different filename. Restore
the overwrite / keep-both / cancel prompt the legacy openEditorTextNew
flow had (via askConflict, lost when Step 9 deleted legacySaveClicked).
Flow on 409:
* "overwrite" → re-POST /save with the same path. /save overwrites
in place when the destination is a regular file.
* "keep-both" → compose a suffixed path via fo.withCollisionSuffix
(now multi-extension-aware after F3) and POST that.
* "cancel" → leave the routed modal open with the user's typed
content intact so they can edit the filename and retry.
Defensively gates the askConflict + withCollisionSuffix calls on
typeof === "function" so older bookmarks (or a dev environment with
one of the modules missing) fall back to an alert rather than a
TypeError. The 409 alert branch is preserved for that path.
Note on when /save actually 409s: regular-file collisions overwrite
silently (200). 409 fires only when the new path collides with a
directory (or a symlink, or a non-file fs entry) — same contract as
the legacy flow had.
Verified live on /overlays/2 in Chromium with a real round-trip:
1. Click "+ new folder" → create tmp_409_probe
2. Click "+ new file" → type "tmp_409_probe" → click Create
3. /save returns 409 (destination is not a file) → askConflict
opens with the colliding path displayed
4. Routed modal stays open behind the conflict dialog (typed
content preserved)
5. Cancel on conflict → conflict closes, routed modal still open
6. Cleanup: delete the tmp_409_probe folder via the action API
* No console errors throughout
* Demo overlay state unchanged after cleanup
pytest stays at 577 passed, 1 skipped, 3 deselected (no Python
changes in F4).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The legacy implementation used lastIndexOf('.') which splits inside
compressed-tarball extensions: foo.tar.gz collided to foo.tar (1).gz,
which then isn't valid as a .gz to unpack and is also ugly.
Recognize the common double-extensions (.tar.gz, .tar.bz2, .tar.xz,
.tar.zst, .tar.lz, .tar.lzma) and treat the entire suffix as a single
logical extension when collision-renaming. So:
foo.txt → foo (1).txt (unchanged)
foo.tar.gz → foo (1).tar.gz (fixed)
archive.tar.bz2 → archive (1).tar.bz2 (fixed)
ARCHIVE.TAR.XZ → ARCHIVE (1).TAR.XZ (case-insensitive detect)
subdir/foo.tar.gz → subdir/foo (1).tar.gz (works with nested paths)
backup.tar → backup (1).tar (single .tar, unchanged)
config.local.json → config.local (1).json (multi-dot non-tar, unchanged)
README → README (1) (no extension, unchanged)
.hidden → ' (1).hidden' (legacy quirk preserved)
Detection is lowercase against the basename only, so /path/with.dots/
in folder names doesn't trip the match.
The .hidden edge case (leading-dot basename, no extension) keeps its
legacy '" (1).hidden"' result — fixing it requires changing the dot >
slash predicate which would also shift other behaviors. Marked
separately if anyone wants to revisit.
The helper is exposed on window.__filesOverlay.withCollisionSuffix
and is called from two paths in uploads.js (the upload-conflict
'keep both' branch and the move-conflict 'keep both' branch via
askConflict). Both paths now produce a sensible filename when the
colliding path uses a double-extension.
No pytest tests added: the helper is JS-only and the project has no
JS test framework (per the plan's Out-of-Scope clause). Chromium
manual verification via window.__filesOverlay.withCollisionSuffix
called on 10 inputs confirms each case above.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
download_supported was set to True at every call site (3 templates, 2
route render calls) and never to False. The {% set show_download =
download_supported and not entry.broken %} branch in
_overlay_file_node.html was therefore equivalent to {% set
show_download = not entry.broken %}, and {% set has_actions =
(files_overlay or show_download) and not entry.broken %} simplifies
further: when not broken, both clauses make has_actions True
regardless of files_overlay; when broken, both clauses make it False.
So has_actions = not entry.broken.
Collapsed:
* Removed download_supported = True from overlay_detail.html (×2),
server_detail.html, and the two render_template calls in
files_routes.py
* Removed the show_download intermediate and the inner {% if
show_download %} guard in _overlay_file_node.html (the surrounding
{% if has_actions %} already guarantees not entry.broken)
* has_actions now directly equals not entry.broken
If a future requirement actually wants per-overlay download-disable,
re-introduce a flag at that point with a real callsite that sets it
False (and a test that exercises that path).
pytest: 577 passed, 1 skipped, 3 deselected — unchanged. None of the
existing tests gated on download_supported.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The plan at docs/superpowers/plans/2026-05-17-files-overlay-rewrite.md
described work that's now shipped across commits 4fa3964..5f82950
(12 implementation steps + the Step 0 prefactor). The design intent
lives in the resulting code (4 modules under static/js/files-overlay/,
the URL-addressable editor template, the shared route helpers) and
the commit messages capture each step's reasoning.
Git history preserves the plan content if anyone needs the original
roadmap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 12/12 of docs/superpowers/plans/2026-05-17-files-overlay-rewrite.md.
End of Phase C — end of the rewrite plan.
Two cleanups in one commit:
1. Delete GET /overlays/<id>/files/content.
The legacy openEditorForFile in files-overlay.js was its only caller,
and Step 9 deleted that code path. grep confirms no remaining live
callers (the matches in .claude/worktrees/* are other in-flight
branches; matches in docs/ are plan/spec text describing the route's
history). Removed:
* The @bp.get route function (was already a thin wrapper around
_load_file_for_editing from Step 11)
* The endpoint's mention in the module docstring
* test_content_returns_text
* test_content_returns_415_for_binary
* test_content_404_for_non_files_overlay
* The /files/content entry in the batched "non-files-overlay 404s
everywhere" test
The _load_file_for_editing helper from Step 11 becomes single-caller
(only the edit route uses it now). Kept because the function name
gives the prelude a useful named concept and inlining would add ~17
lines of low-density logic into overlay_file_edit_page.
2. Extract _apply_optional_rename.
overlay_file_save and overlay_file_replace had near-identical rename
branches: safe_resolve_for_move → 422-on-traversal, 409-if-dst-exists,
mkdir-parents, os.rename → echo_path = new_path. Extracted into
_apply_optional_rename(overlay, path, new_path) → (write_target,
echo_path) | Response.
The helper handles both cases:
* Rename: atomic rename, returns (dst, new_path)
* No rename: safe_resolve_for_write, mkdir parents, returns
(write_target, path)
Save's "destination is not a file" 409 (creation branch) stays inline
in overlay_file_save — it's save-specific behavior that doesn't apply
to /replace (which assumes a file exists or creates one).
Subtle behavior change in /save: the prior code called
safe_resolve_for_write(new_path or path) upfront and then potentially
overrode write_target via safe_resolve_for_move. The new code only
calls one validator per branch. Confirmed equivalent: per
overlay_files.py:36-58 (safe_resolve_for_write) vs. lines 76-106
(safe_resolve_for_move), the dst-side checks are identical (root
escape, symlink refuse, parent-is-dir) and safe_resolve_for_move adds
strictly more (src must exist, cycle check for directory moves). pytest
covers the save/replace paths and stays green.
pytest: 580 → 577 passed, 1 skipped, 3 deselected. The -3 is the 3
deleted /files/content tests.
files_routes.py: ended at 735 lines. The plan estimated ~450 — the
delta is the new /files/new route (~43 lines, Step 5), the binary
template branch (Step 7), the _load_file_for_editing helper (Step 11),
and module-header expansions. The structural goal (no dead routes,
shared helpers across paths) is met.
End-of-plan summary:
* 1091-line files-overlay.js → 4 focused modules totaling 1191 lines
(core.js 247, editor.js 309, dialogs.js 212, uploads.js 423)
* Editor flows (text edit, binary replace, create new) all run
through URL-addressable modals (?modal= deep-linkable)
* Legacy <dialog id="files-editor-modal"> deleted
* /files/content deleted (dead endpoint)
* Shared helpers _load_file_for_editing + _apply_optional_rename
* pytest stayed green at every step
* Chromium-verified every step
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 11/12 of docs/superpowers/plans/2026-05-17-files-overlay-rewrite.md.
overlay_file_content and overlay_file_edit_page used to repeat the
same prelude: load the files-overlay, resolve sub_path safely, confirm
it's a regular file, attempt a UTF-8 read with the is_editable sniff
+ UnicodeDecodeError-on-tail fallback. The bodies have diverged in
two places (the binary branch differs — content endpoint returns 415,
edit page renders the binary template) but the prelude is identical.
Extract _load_file_for_editing(overlay_id, sub_path, user). Returns
either a Flask Response (400/404/403/500) or a 5-tuple
(overlay, target, content_or_None, is_binary, byte_count). Both
routes call it and translate the tuple into their output shape:
* overlay_file_content: 415 when is_binary, else jsonify text
* overlay_file_edit_page: render binary template when is_binary,
else render text template
The previous _render_binary_editor helper folds back into the route
since it was only called from there and benefits from inlining now
that the prelude is shared.
is_binary in the tuple is True both when is_editable() said no up
front (8-KiB sniff) AND when the tail-of-file read raised
UnicodeDecodeError — collapsing what was a duplicated try/except
branch in each route.
No behavior change. pytest still 580 passed, 1 skipped, 3 deselected.
Step 12 (next) audits /files/content callers and likely deletes the
endpoint since Phase B removed its only caller (legacy
openEditorForFile via files-overlay.js); the shared helper would
then collapse to single-caller status but remains useful for
grouping the prelude into a named concept.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 10/12 of docs/superpowers/plans/2026-05-17-files-overlay-rewrite.md.
End of Phase B.
The 19-line tombstone file at static/js/files-overlay.js had no code
since Step 4; it was kept around as a stable resolution target for the
<script> tag while the modules de-duplicated. After Step 9 made the
modules complete and stand-alone, the stub is just a wasted HTTP
request and a misleading filename in the manifest. Deleted, plus the
matching <script defer> tag in overlay_detail.html.
Final loader shape — exactly 4 script tags, all defer, all in
files-overlay/:
1. core.js — helpers, manager guard, action registry
2. editor.js — URL-addressable editor (text + binary + new-file)
3. dialogs.js — new-folder + delete-confirm + conflict
4. uploads.js — upload queue + drag-drop + zip action
Verified live on /overlays/2 in Chromium:
* Exactly 4 files-overlay script tags load (no more files-overlay.js)
* window.__filesOverlay registry has its 10 keys; askConflict +
withCollisionSuffix + handleAction + registerHandler all functions
* File tree renders (3 file rows + 1 folder row, as before)
* No legacy #files-editor-modal in DOM
* No console errors
* pytest still 580 passed, 1 skipped, 3 deselected
Phase B end-state vs. Phase A end-state:
* editor.js: 550 → 309 lines (Step 9 gutted legacy)
* files-overlay.js: 19 → 0 (deleted in this step)
* 5 new pytest tests for the /files/new + binary template
* Legacy <dialog id="files-editor-modal"> gone from
overlay_detail.html
* Editor flows (text edit, binary replace, create new) all run
through the URL-addressable modal (?modal= deep-linkable)
Phase C (steps 11–12) is server-only: extract a shared path-resolution
helper between overlay_file_content + overlay_file_edit_page, then
delete /files/content if grep confirms no remaining callers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 9/12 of docs/superpowers/plans/2026-05-17-files-overlay-rewrite.md.
The inline <dialog id="files-editor-modal"> block in overlay_detail.html
is gone. All editor flows (text edit, binary replace, create new) now
exclusively use the URL-addressable modal swapped into #modal-content.
editor.js is now single-purpose (URL-addressable only). Removed:
* editorDialog reference and the editorEls DOM-ref struct
* The legacy editor state object
* CM6 bridge wrappers + UI helpers (getEditorValue/setEditorValue/
setEditorTitle/updateByteCount/updateRenameHint/updateSaveEnabled/
setQueuedReplacement) — they only ever drove the legacy dialog
* withCollisionSuffix (uploads.js still has its copy for the
upload-conflict path; editor.js no longer needs it since the
URL-addressable conflict path is "alert + keep modal open" rather
than overwrite/keep-both)
* openEditorTextNew and openEditorForFile — both functions were
already unreachable from a user action after Steps 6/8
* inLegacyEditor predicate
* Direct-bound listeners on editorEls.filename / contentBox /
editorDialog (input, keydown for Ctrl+S, close)
* Legacy branches in every delegated handler (dragover, dragleave,
drop, change, click)
* legacySaveClicked and legacyDeleteClicked
What stays:
* Routed state (routedReplacement, isRoutedBinaryMode,
setRoutedReplacement, updateRoutedBinarySaveEnabled)
* Delegated dragover/dragleave/drop/change/click handlers — now
single-path each, no legacy/routed branching
* Filename input delegated listener for routed binary mode (so
rename-only Replace stays reachable)
* modal-container close listener that clears routedReplacement
* routedSaveClicked (text edit + is_new), routedReplaceClicked
(binary, with rename-or-replace fork), routedDeleteClicked
* "new-file" and "edit" registered handlers (the "edit" handler is
no longer split editable/binary — the server picks the template
branch)
routedDeleteClicked gained one capability that was missing in Step 8:
it now reads rel-path from either the textarea (text mode) OR the
.files-editor-binary panel, so deletion works for binary files in the
URL-addressable modal too (previously routed-mode binary delete fell
through to legacy, which is now gone).
Test added: test_overlay_detail_no_longer_renders_legacy_editor_dialog
asserts the legacy dialog markup is absent from /overlays/<id> while
the other inline dialogs are still present (Step 3 didn't move them).
Numbers:
editor.js: 660 → 309 lines (-351). Plan estimated ~200; actual is
~50% larger due to module-header comments + the rename-or-replace
fork in routedReplaceClicked. Pure-routing-only and single-mode
per click, which was the structural goal.
Total across files-overlay/: 1432 → 1191 lines.
pytest: 579 → 580 passed, 1 skipped, 3 deselected.
Verified live on /overlays/2 in Chromium:
* id="files-editor-modal" not in DOM; new-folder/delete/conflict
dialogs still present
* "+ new file" → routed modal, Create button
* Click other.cfg (editable) → routed modal, Save button, content
pre-filled
* Click test.png (binary) → routed modal, Replace button, initially
disabled
* No console errors
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 8/12 of docs/superpowers/plans/2026-05-17-files-overlay-rewrite.md.
The "edit" action handler in editor.js now ALWAYS opens the URL-
addressable modal — both editable text files and binary files. The
server's /files/edit route picks the template branch (text editor vs.
binary-replace panel) based on is_editable(target); data-editable on
the action element becomes informational only.
Binary-replace handlers extended in editor.js to cover the routed
modal (the panel that .files-editor-binary now lives inside):
* routedReplacement (module-scope nullable) — the queued File for
routed binary mode. Cleared on modal-container close so reopening
starts fresh.
* setRoutedReplacement(mc, file) — updates idle/queued markup,
populates name/size labels, calls updateRoutedBinarySaveEnabled.
* isRoutedBinaryMode(mc) — true when #modal-content holds a binary
panel; used by the click delegation to route Save → replace.
* updateRoutedBinarySaveEnabled(mc) — enables Save when a file is
queued OR the filename input has been edited. Mirrors the legacy
binary-mode updateSaveEnabled logic.
The existing dragover / dragleave / drop / change / click delegated
listeners gained routed-mode branches alongside the legacy branches,
gated by inLegacyEditor / inRoutedEditor (mutually exclusive — the
selectors only match inside one editor at a time).
routedReplaceClicked added, mirroring legacy binary save:
* Queued file present → POST /files/replace (multipart, with optional
new_path for rename-on-replace)
* Queued file absent but filename edited → POST /files/move (rename
only)
* Neither → no-op (Save was disabled, shouldn't be reachable)
Filename-input delegated listener re-evaluates Save enablement when
the user types in routed binary mode (so rename-only is reachable
without queueing a file).
The legacy openEditorForFile function in editor.js is now unreachable
from a user action (the "edit" handler no longer calls it). It stays
in this file until Step 9 deletes the legacy dialog block wholesale.
Verified live on /overlays/2 in Chromium:
* Click test.png (binary, data-editable="0") → URL becomes
?modal=%2Foverlays%2F2%2Ffiles%2Fedit%3Fpath%3Dtest.png
* Routed modal opens with binary panel; Save labeled "Replace" and
disabled
* Synthetic drop event with a File → Save enables, idle label hides,
queued label shows "new.png · 18 B"
* Clear button → idle restored, Save disables
* Type new filename without queueing → Save enables (rename-only)
* Revert filename → Save disables
* No console errors
* pytest still 579 passed, 1 skipped, 3 deselected (no Python changes
in Step 8)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 7/12 of docs/superpowers/plans/2026-05-17-files-overlay-rewrite.md.
overlay_file_edit_page now renders the binary-replace UI instead of
returning 415 for non-editable files. Two cases bridge to the same
binary template: the up-front is_editable() check and the late-binding
UnicodeDecodeError on read (8-KiB sniff said yes but the tail had
non-UTF-8 bytes). Both paths route through a small _render_binary_editor
helper that fills in:
* is_binary=True
* content="" (no inline editor)
* byte_count=target.stat().st_size
* mime_type from mimetypes.guess_type, falling back to
application/octet-stream
Template (overlay_file_editor.html) gains an is_binary branch in the
modal body: hides .files-editor-text (CM6 textarea + language
dropdown), renders .files-editor-binary instead with file-info note,
replace-zone (drop area + browse button + hidden file input), and
the per-state idle/queued labels. The Save button reads "Replace"
in binary mode and starts disabled — Step 8 enables it via JS when
a replacement file is queued.
Delete and Download stay visible in binary mode (binary files can be
deleted and downloaded the same way text files can).
The /files/new route gets is_binary=False + mime_type="" passed
explicitly so the template's binary branch never fires there.
Server-side only — no JS changes in Step 7. Step 8 wires up the
binary-replace handlers (replace-zone drag, browse/clear clicks,
Replace button → POST /files/replace).
Tests:
* Removed test_edit_route_415s_for_non_editable_file — the route
no longer returns 415 for non-editable files; the binary template
is the new contract.
* Added test_edit_route_renders_binary_template_for_non_editable
(asserts 200 + is_binary markup + Save button label + disabled
state + .files-editor-text absent + byte count rendered).
* Added test_binary_template_has_replace_zone (asserts the replace-
zone markup: drop zone, idle/queued labels, browse button, hidden
file input, and that Download stays visible).
pytest: 578 → 579 passed, 1 skipped, 3 deselected. The pre-existing
"still 404 / still 400" cases the plan asks for are covered by the
existing test_edit_route_404s_for_missing_file and
test_edit_route_400s_for_path_traversal tests — left alone rather
than duplicated.
Verified live: GET /overlays/2/files/edit?path=test.png (an actual
binary file in the demo overlay) returns the binary template; DOM
inspection confirms .files-editor-binary present with data-rel-path,
"Replace" save button starts disabled, Download href points at the
file's download endpoint, MIME type and byte count match, .files-
editor-text is absent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 6/12 of docs/superpowers/plans/2026-05-17-files-overlay-rewrite.md.
Two changes in editor.js:
1. The "new-file" registered handler now opens the URL-addressable
modal at GET /overlays/<id>/files/new?at=<folder> via
window.modals.openRouted, replacing the call to openEditorTextNew.
The legacy openEditorTextNew function stays in this file for now —
it's no longer reachable from a user action; Step 9 deletes it
alongside the rest of the legacy dialog block.
2. routedSaveClicked gains an is_new branch. When the textarea's
data-rel-path is empty, the save composes the new file's path from
data-at-folder (set by the /files/new route) + the user-typed
filename and POSTs {path, content} to /files/save. The /save
endpoint creates the file when it doesn't exist; 409 means a file
at that path already exists and the user picks a different name
(alert + modal stays open so the form value is preserved).
The legacy slash-in-filename guard from openEditorTextNew's legacy
save path is deliberately not carried over — the plan permits typing
"sub/foo.txt" in the filename input to create a nested file via
/save, matching the route's path semantics.
Verified live on /overlays/2 in Chromium:
* Click "+ new file" on overlay root → URL becomes
?modal=%2Foverlays%2F2%2Ffiles%2Fnew%3Fat%3D
* Routed modal opens with empty data-rel-path, empty data-at-folder,
empty filename input, save button labeled "Create", no Delete or
Download buttons, title "…new file"
* No console errors
* pytest still 578 passed, 1 skipped, 3 deselected (no Python or
test changes in Step 6)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 5/12 of docs/superpowers/plans/2026-05-17-files-overlay-rewrite.md.
Adds a server-rendered editor page for creating a new file in a target
folder. Renders the same overlay_file_editor.html as the edit route,
but with is_new=True so the template:
* Hides the Delete button
* Hides the Download link (no file to download yet)
* Changes the Save button label to "Create"
* Emits data-at-folder on the textarea for the JS save handler to
compose path = at_folder + "/" + filename
* Updates the title block (browser tab + heading) to "New file" /
"<at_folder>/…new file"
The existing edit route now passes is_new=False and at_folder=""
explicitly so both call sites are explicit about the contract.
Path validation: ?at may be empty (overlay root) or a relative folder
path. safe_resolve_for_listing rejects traversal attempts (400). A
missing or non-directory at returns 404. Reused the helper to keep
the safety story identical to the listing / edit routes.
Phase B Step 5 is server-side only — no JS changes here. Step 6
migrates openEditorTextNew in editor.js to use this route via
window.modals.openRouted().
Tests added (tests/test_url_addressable_modals.py):
* test_new_route_renders_with_empty_content
* test_new_route_renders_with_target_folder_attribute
* test_new_route_renders_create_button_not_save
* test_new_route_400s_for_invalid_at_path
* test_new_route_404s_for_non_files_overlay
pytest: 573 → 578 passed, 1 skipped, 3 deselected.
Verified live: curl /overlays/2/files/new returns markup with
data-at-folder="", "UTF-8 · 0 bytes" byte count, save button label
"Create", and no Delete / Download buttons.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 4/12 of docs/superpowers/plans/2026-05-17-files-overlay-rewrite.md.
End of Phase A. files-overlay.js is now a 19-line tombstone comment;
all behavior lives in the 4 modules under files-overlay/. Step 10
deletes the stub and its <script> tag.
uploads.js owns:
* The upload queue (concurrency 3, XHR-based progress, queued/active/
done/cancelled/error/conflict states) and the progress panel
* Drag-drop on treeRoot (5 events: dragstart, dragend, dragover,
dragleave, drop). Internal drags (row → folder, via the custom
application/x-files-overlay MIME) and external drags (OS files +
folders via webkitGetAsEntry) flow through the same drop handler.
* walkEntry for recursive folder walks during external drops
* withCollisionSuffix (moves here from files-overlay.js — its biggest
caller is the upload-conflict "keep both" path; exposed on
__filesOverlay for editor.js's save-409 path too)
* "zip" action handler (registered into __filesOverlay) — pure URL
navigation; placed here as the closest thematic home
Pattern change: upload-row cancel buttons converted from direct-bound
per row (inside buildUploadRow, which captured `item` in a closure) to
a single document-level delegated click listener. Each row carries
data-upload-id; an uploads Map<uploadId, item> looks up the item at
click time. The Map entry is removed in the uploadsClearBtn handler
when a done/error/cancelled row is cleared, so the Map doesn't grow
unbounded.
Drag-drop stays direct-bound to treeRoot per the plan escape hatch —
the 5 events share coordinated highlight state (is-drag-source,
is-drop-target classes that are toggled across events), and treeRoot
is persistent (never swapped). Delegation would obscure the state
coordination logic without any real benefit.
Phase A end-state:
* core.js (247 lines): helpers, manager guard, registry dispatch
* editor.js (550 lines): editor flows (legacy + URL-addressable)
* dialogs.js (212 lines): new-folder, delete-confirm, conflict
* uploads.js (423 lines): upload queue + drag-drop + zip + collision
* files-overlay.js (19 lines): tombstone comment, deleted in Step 10
Total: ~1432 lines across 4 modules + 19-line stub. The plan estimated
~780 lines across 4 modules; actual is ~1.8× larger, the difference
being module-header comments and the delegation-with-state scaffolding
(e.g., editor.js's dual-editor listener split, dialogs.js's per-dialog
state plus close-event resolvers).
Verified live on /overlays/2 in Chromium:
* 5 script tags load in document order (core → editor → dialogs →
uploads → legacy stub)
* Registry has 10 keys; askConflict and withCollisionSuffix are both
callable; withCollisionSuffix('foo.tar.gz') === 'foo.tar (1).gz'
(legacy behavior preserved — lastIndexOf('.') splits before the
final extension)
* Uploads panel + list elements present in DOM, panel hidden by
default
* "zip" action button exists; registered handler would set
window.location.href to /overlays/2/files/download_zip?path=...
* No console errors
* pytest still 573 passed, 1 skipped, 3 deselected
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 3/12 of docs/superpowers/plans/2026-05-17-files-overlay-rewrite.md.
dialogs.js owns the three inline <dialog> modals that surround the
file manager:
* #files-new-folder-modal — "+ folder" / mkdir prompt
* #files-delete-modal — delete-confirm for files and folders
* #files-conflict-modal — overwrite / keep-both / cancel choice
Pattern change: the legacy file used clone-and-rebind
(replaceWith(cloneNode(true)) + fresh addEventListener) to drop stale
state-bearing click listeners between dialog opens. dialogs.js replaces
that with a single delegated listener per dialog, reading per-dialog
state from module-scope nullable variables (conflictState,
deleteState, newFolderState). The state is set when the dialog opens
and cleared on the 'close' event so dismissals don't leave stale
references. Listener attaches once per page load.
Dialog opens go through window.modals.openInline()/closeInline()
instead of dialog.showModal()/close() directly, completing the inline-
modal convention from commit c51089d.
askConflict now resolves to "cancel" on any dismissal (Esc, backdrop,
programmatic close) thanks to the 'close' event handler — the legacy
version left the promise pending forever in those paths. Verified
live: closeInline() on an open conflict dialog resolves the pending
askConflict promise to "cancel".
Action-registry dispatch: dialogs.js registers "new-folder" and
"delete" handlers into __filesOverlay. Combined with editor.js's
registration of "new-file" and "edit" (Step 2), only "zip" remains in
the legacy click switch (pure URL navigation, no module dependency).
Cross-module exposure: askConflict moves from files-overlay.js to
dialogs.js; both set __filesOverlay.askConflict, but dialogs.js wins
by document order (it loads before legacy via the <script defer>
ordering in overlay_detail.html). The legacy upload + drag-drop call
sites switch from local askConflict() to window.__filesOverlay
.askConflict() — same shape, different lookup.
The orphaned newFolderDialog / conflictDialog / deleteDialog
declarations at the top of legacy are deleted; legacy no longer holds
references to those elements.
Numbers:
files-overlay.js: 669 → 589 lines (-80)
files-overlay/dialogs.js: 212 lines (new)
Net: +132 lines. Growth is from the delegation/state-management
scaffolding and module-header comments. The delete went lighter
than the plan's ~150-line estimate because the new code is more
carefully structured (less duplication across the 3 dialogs).
Verified live on /overlays/2 in Chromium:
* 4 script tags load in order (core → editor → dialogs → legacy)
* Registry has 10 keys; askConflict + withCollisionSuffix still set
* "+ new folder" on overlay root → new-folder dialog opens with
empty name input and "/" target label, closes cleanly
* "✕" on a file row → delete-confirm dialog opens with the file's
name displayed, closes cleanly
* askConflict('a/b.txt') → conflict dialog opens with path shown
- close via window.modals.closeInline() → resolves "cancel"
- click [data-files-conflict-action="overwrite"] → resolves "overwrite"
- click [data-files-conflict-action="keep-both"] → resolves "keep-both"
* No console errors throughout
* pytest still 573 passed, 1 skipped, 3 deselected
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 2/12 of docs/superpowers/plans/2026-05-17-files-overlay-rewrite.md.
editor.js is dual-purpose during Phase A: drives both the legacy
inline #files-editor-modal <dialog> (binary-replace + create-new flows)
and the URL-addressable modal swapped into #modal-content (editable
text files). Phase B migrates the legacy flows to URL-addressable too
and removes the legacy branches.
What moved:
* Editor state object, editorEls DOM refs, CM6 bridge (getEditorValue,
setEditorValue), UI helpers (setEditorTitle, updateByteCount,
updateRenameHint, updateSaveEnabled, setQueuedReplacement)
* openEditorTextNew (create-new file flow)
* openEditorForFile (legacy binary + editable-as-fallback flow)
* All save/delete/replace handlers — converted from direct-bound on
editorEls.{saveBtn,deleteBtn} to a single document-level click
listener that discriminates by ancestor (legacy editorDialog vs.
URL-addressable #modal-content)
* Replace-zone dragover/dragleave/drop — direct-bound on
editorEls.replaceZone → document-level delegation gated on the zone
being inside the legacy dialog
* Replace-input change, replace-clear / replace-browse clicks — also
delegated
* The previously-separate URL-addressable save/delete delegation
block (lines 593-664 of the legacy file) collapses into the same
delegated listeners
What stays direct-bound (per plan escape hatch):
* input on .files-editor-filename
* input + keydown on .files-editor-content (Ctrl+S handling)
* close on the persistent legacy <dialog>
These are high-frequency events on persistent inputs inside the
persistent legacy dialog; delegation would add per-keystroke
selector-matching overhead with no benefit.
Action dispatch: editor.js registers "new-file" and "edit" handlers
into __filesOverlay (set up by core.js). The legacy switch-case in
files-overlay.js's click delegation loses both cases — they're now
dispatched via the registry. The legacy switch still owns new-folder,
zip, and delete (those migrate in Step 3).
Cross-module exposure: askConflict and withCollisionSuffix stay in
files-overlay.js (the upload queue and drag-drop code at lines 857
and 974 still use them) and are exposed on __filesOverlay so editor.js
can call them. They migrate to dialogs.js (askConflict, Step 3) and
uploads.js (withCollisionSuffix, Step 4); the call sites in editor.js
don't change.
Numbers:
files-overlay.js: 1091 → 669 lines (-422)
files-overlay/editor.js: 550 lines (new)
Net: +128 lines; the growth is from the dual-editor delegation
scaffolding (separate handler functions for legacy vs. routed) and
module-header comments. The legacy file is now a stub editor section
comment plus the unmigrated dialogs/uploads/drag-drop blocks.
Verified live on /overlays/2 in Chromium:
* 3 script tags load in document order (core → editor → legacy)
* window.__filesOverlay registry now has 10 keys (added askConflict +
withCollisionSuffix); withCollisionSuffix('foo.txt') = 'foo (1).txt'
* No console errors on page load or after synthetic actions
* E2E dispatch check: clicking a "+ new file" action button opens the
legacy dialog with empty filename + Create save-button label
(proves core → handleAction → editor.js handler → openEditorTextNew
chain works)
* E2E dispatch check: clicking the filename button on an editable
file sets ?modal=%2Foverlays%2F2%2Ffiles%2Fedit%3Fpath%3D... in the
URL (proves editor.js's "edit" handler correctly routes editable
files through window.modals.openRouted)
* pytest still 573 passed, 1 skipped, 3 deselected
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 1/12 of docs/superpowers/plans/2026-05-17-files-overlay-rewrite.md.
Pure scaffolding — no behavior change. core.js is loaded with defer in
overlay_detail.html before the existing files-overlay.js script tag
(both with defer, so execution order follows document order). Both
files run the same .files-manager guard, both attach document-level
click listeners on the action selector. The legacy file's switch-case
still owns dispatch; core.js's listener dispatches into an empty
registry until Steps 2–4 populate handlers.
window.__filesOverlay exposes:
* manager / overlayId / baseUrl / treeRoot / csrfToken (manager-
element-derived state, computed once)
* helpers.{joinPath, parentOf, basename, escapeHtml, humanSize,
fetchJson, postJson, postForm, refreshFolder, findRowByPath,
cssEscape, scheduleRefresh} (duplicated from legacy file for the
duration of Phase A; de-duplicates as feature modules migrate out)
* registerHandler(op, fn) / handleAction(op, path, actionEl) — the
action-dispatch registry that Steps 2–4 populate
Per the canonical plan's errata commit (d76ee05), the script tag goes
in overlay_detail.html (not base.html as the original plan said) and
uses defer to match the existing pattern.
Verified live on /overlays/2 in Chromium: both <script> tags present
in DOM order; window.__filesOverlay shape matches expectation (8
top-level keys, 12 helpers); overlayId="2", baseUrl="/overlays/2",
treeRoot resolved; joinPath('foo/','/bar') === 'foo/bar' smoke test
passes; no console errors; existing 3-file/1-folder tree still
renders. pytest still 573 passed, 1 skipped, 3 deselected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Read-only file explorers (server detail page, read-only overlay) used
to omit the hover-action panel entirely and make the filename itself
the download link. Editable overlays did the opposite: hover-action
panel with download + delete, filename-click opens the editor.
Unify both surfaces around the editable pattern: always emit the
hover-action span when any action applies. Gate the download button
on download_supported only (now visible on all surfaces). Keep
delete + folder actions (+file, +folder, zip) gated on files_overlay,
since read-only surfaces never offer those. In read-only mode, the
filename becomes a plain <span> — the hover ⬇ is the single download
affordance, matching editable mode's filename-click ≠ hover-download
split.
Prefactor for the files-overlay.js rewrite (docs/superpowers/plans/
2026-05-17-files-overlay-rewrite.md). No JS changes; files-overlay.js
doesn't run on read-only surfaces (manager-element guard at line 23-24).
Verified: pytest stayed at 573/1/3 (URL substrings still appear in the
rendered HTML even though they moved out of the filename anchor into
the hover-action span). Direct Jinja render confirmed all four
branches: read-only downloadable file (new hover ⬇, plain <span>
filename), broken read-only symlink (no hover panel — correct, can't
download dangling links), read-only download_supported=False (no
hover panel, plain <span>), editable mode (byte-identical to before).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
files-overlay.js is loaded from overlay_detail.html:285 (with defer),
not base.html — the JS activates only when .files-manager exists,
which is only on overlay detail for files-type overlays. Loading from
base.html would pull it onto every page. The plan's first draft had
this wrong in four places (step 1, step 4, step 10, critical files
table). Following the plan verbatim would have moved the script tag
to the wrong template — exactly the failure mode that
feedback_validate_before_implementing memory warns about.
Added an Errata section at the bottom of the plan documenting this.
Also clarified that all new module script tags should use defer to
match the existing pattern (the modules query the DOM at load and
need the body parsed first).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three-phase plan to consolidate files-overlay.js's mixed event-binding
patterns and complete the URL-addressable modals migration:
Phase A (4 commits): split the 35 KB IIFE into 4 focused modules
under static/js/files-overlay/ — core, editor, dialogs, uploads —
with consistent document-level delegation. Behavior unchanged.
Phase B (6 commits): migrate the two remaining inline-dialog flows
(create-new-file, binary-replace) to URL-addressable modals via a
new /files/new route and a binary-mode branch in the edit route +
template. Delete the legacy <dialog id="files-editor-modal"> from
overlay_detail.html. editor.js becomes single-purpose (~200 lines).
Phase C (2 commits): extract shared path/editability helper for
routes/files_routes.py; delete the now-unused /files/content JSON
endpoint; consolidate save/replace rename duplication.
Each commit is independently verifiable + revertable. Natural pause
points at the end of each phase. Plan is the handoff artifact for
cross-session execution.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two modal pipelines coexisted after the URL-addressable pilot — modal.js
(inline, ~30 lines) and modal-router.js (routed, ~150 lines) — operating
on different attribute namespaces and exposing different APIs. Future
modal authors had two systems to learn with no naming convention to
help them pick the right one for a given use case.
Consolidates both into static/js/modals.js with two clearly-named
pipelines and a single window.modals.* API:
Inline modal — content pre-rendered in the page.
Hooks: data-inline-modal-open="<dialog-id>"
data-inline-modal-close
API: window.modals.openInline(idOrEl)
window.modals.closeInline(idOrEl)
Use: confirmations, transient prompts, in-page forms without
URL value.
Routed modal — content fetched from a URL, ?modal=<path> in URL,
with history + share-link + refresh-survival.
Hooks: <a data-routed-modal href="<path>">
data-routed-modal-dismiss
API: window.modals.openRouted(path)
window.modals.closeRouted()
Use: content with standalone-page meaning.
Single document-level click delegation handles all four attribute
hooks; one DOMContentLoaded handler binds dialog 'close' / 'cancel' /
backdrop on the routed slot; shared popstate and htmx:responseError
listeners. Behaviour unchanged — pure rename + colocation.
Renamed across 11 templates and files-overlay.js. Old data-modal-*
attributes and window.openModal/closeModal globals are gone — clean
break (no back-compat shims). AGENTS.md "Modals: inline vs routed"
section documents the decision guide for new modals.
Verified: 573 backend tests pass. 5/5 Chromium smoke checks pass
(inline open/close, Esc, backdrop, routed open+save, routed Esc).
Console clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Architectural problem flagged after the pilot: "the template renders both
as a standalone page AND as a modal fragment" contract is non-obvious for
future template authors. Task 2 originally used <dialog>, Task 8.5 had to
undo that because nested <dialog> collapses to 2px. The convention is now
in two places:
1. AGENTS.md gains a "URL-addressable modal templates" section under
Non-Negotiable Constraints listing: outer element must be <div>, close
buttons use data-modal-dismiss, form actions need #modal-content-scoped
document delegation, modal chrome CSS is owned by the outer slot.
2. _modal_partial.html (the file template authors will most likely open
when wondering "what's this layout?") carries a Jinja comment header
summarising the rule + linking to AGENTS.md for the full convention.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The URL-addressable modals plan shipped in 14 commits. Three places
where the plan's verbatim code didn't survive contact with the codebase
(has_request_context guard, LEFT4ME_ROOT-aware fixture, save-handler
direct-bind) are now documented at the top of the plan, with commit
references for the fixes. Also notes the inserted tasks 8.5/8.5b/9b
and the Task 6 design refinement (close-event single state sink) so a
future re-executor sees the actual shipped pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>