Drop .limit(20) from the recent_rows query so the full history window is
available for the future recent-players modal; derive recent_players_overview
(first 10) and recent_players_total_count from the unbounded result and pass
both into _live_state.html alongside the existing recent_players key.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Groups server state into a single top cluster (lifecycle + live state
+ config), demotes log/console/files to a tabbed inspection strip with
expand-to-modal, and routes the job log behind a modal so the page no
longer reflows during HTMX polls.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three labelled swatches mirror the dropdown's name colors so users
can decode the cvar/command/sourcemod color scheme without guesswork.
Plain-text caveat next to the sourcemod swatch notes that those
commands are plugin-dependent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sm_* commands depend on the SourceMod plugin being loaded on the
target server, which is not always the case. Render their names in
the third syntax-palette color (purple via --cm-number) so the user
can tell at a glance that these may not exist on the server they
are targeting. Vanilla cvars and commands keep their existing
pink/green colors. Theme-aware via the existing token swap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New test module (test_server_detail.py) — the server-detail page is
NOT a files-overlay (files_overlay=False in the template), it just
reuses _overlay_file_tree.html in read-only mode. Tests live
separately to make the semantic split visible.
The test navigates to /servers/<id>, hovers the server.cfg row to
defeat the CSS :hover gate on .files-row-actions
(opacity:0/pointer-events:none → 1/auto), clicks the ⬇ download
link, and asserts both the suggested filename and the byte content
of the downloaded file.
The :hover gate is load-bearing: without locator.hover() first,
pointer-events:none blocks the click. A regression that ships
actions always-visible would change the user-flow ergonomics and
needs to update this test deliberately.
Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md
(Tier 3).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds server_with_files fixture: seeds alice + a Blueprint + a Server
row pinned to port 27015, then pre-creates
LEFT4ME_ROOT/runtime/<server_id>/merged/ with a single seed file.
The /servers/<id> page lists files from that merged directory (the
kernel-overlayfs view of a running server), which never exists on
dev/test boxes — without the pre-mkdir + seed, the page renders
the empty-state branch instead of file rows.
Server.port is a global UNIQUE constraint on the model, but every
e2e test gets its own SQLite DB, so a fixed L4D2-default port is
fine.
Sets the stage for the Tier 3 hover-download test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drags server.cfg from the overlay root onto the cfg/ folder row,
asserts both that the file actually moved on disk AND that the tree
reflects the move after the debounced HTMX refresh of both parents.
Exercises the internal-drag path in uploads.js:332-366: the
custom-MIME setData/getData contract, the POST /files/move call,
and the dual scheduleRefresh(parentOf(src)) + scheduleRefresh(dst)
on success. Playwright's locator.drag_to() synthesizes setData
correctly — it relies on dataTransfer.getData(), NOT
webkitGetAsEntry which Playwright cannot fake.
Pitfalls handled inline: scoped the source/target locators to
li.file-tree-row-{file,dir} because action buttons inside the row
duplicate data-target-path + data-row-kind and would trip strict
mode. Used to_have_count instead of to_be_visible because cfg's
children div is collapsed (hidden) by default — the moved row is
in the DOM but not visually rendered until cfg is expanded.
Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md
(Tier 2 case C).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>