left4me/docs/superpowers/specs/2026-05-17-console-command-autocomplete-design.md
mwiegand 02d96b593e
docs(console): add design for console command autocomplete
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>
2026-05-17 17:20:47 +02:00

8.7 KiB

Console Command Autocomplete — Design

Date: 2026-05-17 Status: Approved for implementation planning

Context

The L4D2 web admin already has a config editor (CodeMirror 6 based) that offers autocomplete for Source-engine console commands and cvars, sourced from a JSON vocab file (l4d2web/l4d2web/static/data/srccfg-vocab.json, 671 commands + cvars with descriptions). The same vocabulary is useful at the runtime console input on server_detail.html, where admins type commands against a live server — but no autocomplete exists there today. Users have to remember exact command names, leading to typos and trial-and-error against a production server.

Goal: add autocomplete to the runtime console input, reusing the editor's ranking logic where reuse is cheap, without modifying the editor's behavior and without colliding with the existing console-history.js ArrowUp/Down history recall.

Scope

  • In scope: dropdown autocomplete on the server-detail console input, sharing the editor's ranking algorithm and the existing srccfg-vocab.json data file.
  • Out of scope: argument-value completion (player names for kick, map names for changelevel, etc. — needs runtime data); fuzzy/typo-tolerant matching; custom per-game vocabs (the API accepts a URL so the door is open, but only srccfg-vocab.json ships); replacing CodeMirror's built-in dropdown in the editor.

Key Decisions (from brainstorming)

# Decision Rationale
1 Level A reuse: extract ranker into a shared module; build a small vanilla dropdown for the console. Editor keeps CodeMirror's built-in dropdown. Shares the product judgment (ranking algorithm) without coupling the console to CodeMirror. ~10-line shared brain + ~80-line vanilla dropdown.
2 Fetch vocab on console input focus, once per page load. Doesn't compete with page load. Browser cache makes subsequent visits free. By the time the user finishes typing the first character, fetch is usually done.
3 Tab / Shift+Tab cycle the dropdown; Esc dismisses; Enter submits the input as-typed; ArrowUp/Down untouched (history keeps them). ArrowUp/Down already belongs to console-history.js. Autocomplete cannot ever change the submitted command — safety on a live game server. Mouse + Tab covers selection.
4 Dropdown rows show name + description, mirroring the editor's format. Same visual affordance as editor → muscle memory transfers. Vocab JSON already carries desc.
5 First-token only: dropdown hides as soon as the user types a space. Vocab only knows command names, not argument grammars. Quiet-when-uncertain beats confident-and-wrong.

Architecture

Three moving parts:

  1. Extract the ranker. Move pure ranking logic out of l4d2web/scripts/editor-src/autocomplete.js into new module l4d2web/scripts/editor-src/vocab-rank.js. Function signature: rankVocab(query: string, vocab: {cvars, commands}) → Array<{name, desc, kind}>. No CodeMirror dependency. autocomplete.js re-imports it; the editor build keeps working unchanged.

  2. Console autocomplete module. New file l4d2web/l4d2web/static/js/console-autocomplete.js. Vanilla JS, no framework. Imports the ranker (delivery details — separate static script, or compiled into the editor bundle output — to be resolved in the implementation plan). Exposes one entry point: attachAutocomplete(inputEl, vocabUrl).

  3. Wire-up on server-detail. A new <script> tag in server_detail.html (placed alongside the existing console-history.js tag) calls attachAutocomplete(form.querySelector('input[name=command]'), '/static/data/srccfg-vocab.json') once on page load. console-history.js is not modified — it keeps owning ArrowUp/Down, Enter, and the input's data-console-form attribute. The two modules cooperate by owning disjoint key sets.

Components & Interfaces

  • vocab-rank.jsrankVocab(query, vocab) → ranked items. Pure. Score: exact (3) → prefix (2) → substring (1). Truncates to a configurable limit (default 50). Identical algorithm to the editor's current behavior.
  • autocomplete.js (editor) — unchanged externally; internally now calls rankVocab instead of inlining the ranking. Editor build artifacts (l4d2web/l4d2web/static/js/editor.js) should be regenerated.
  • console-autocomplete.js — owns:
    • module-scoped fetch promise (deduplicates concurrent fetches; { once: true } semantics)
    • one dropdown DOM element, lazily created, appended to document.body, positioned with getBoundingClientRect() of the input
    • focus listener (one-shot) → triggers fetch
    • input event listener → recompute matches, show/hide dropdown based on first-token rule
    • keydown listener for Tab / Shift+Tab / Esc onlypreventDefault() only for those keys
    • blur / Esc / form submit → close dropdown
  • console-history.js — unchanged. Continues to own ArrowUp/Down, Enter handling, history fetch endpoint.

Data Flow

[user focuses input]
    → console-autocomplete fetches /static/data/srccfg-vocab.json (once)
    → vocab cached in module-scope promise

[user types 'sv_che']
    → 'input' event fires
    → first-token check: cursor in first token? yes
    → rankVocab('sv_che', vocab) → [{name:'sv_cheats',desc:'...'}, ...]
    → render dropdown rows, highlight first row

[user presses Tab]
    → preventDefault, replace first token with highlighted suggestion
    → keep dropdown open with new matches (or close on exact match)

[user types space]
    → first-token check: cursor past first space? yes
    → hide dropdown

[user presses Enter]
    → console-history.js submits input verbatim (as-typed)
    → autocomplete dropdown closes on input blur after submit

Critical Files

  • l4d2web/scripts/editor-src/autocomplete.js — refactored to import the ranker; behavior unchanged.
  • l4d2web/scripts/editor-src/vocab-rank.jsnew. Pure ranker.
  • l4d2web/scripts/editor-src/editor-entry.js — no functional change; verify the autocomplete extension still wires up correctly after the ranker extraction.
  • l4d2web/l4d2web/static/js/console-autocomplete.jsnew. Vanilla dropdown.
  • l4d2web/l4d2web/static/js/console-history.jsuntouched (verify by diff after wiring change).
  • l4d2web/l4d2web/templates/server_detail.html — add one <script> tag for console-autocomplete.js (or extend an existing module-loader block; check current convention during planning).
  • l4d2web/l4d2web/static/data/srccfg-vocab.json — unchanged data source.

Error Handling & Edge Cases

  • Vocab fetch fails: silent. Dropdown never appears; console keeps working. console.warn for devtools visibility.
  • No matches: dropdown stays hidden (don't render an empty box).
  • Pasted long command: treated as text input. If pasted text contains a space, dropdown hides immediately (paste lands cursor at end, past first token).
  • Input is empty: dropdown hidden. ArrowUp recalls history as before.
  • Dropdown scrolling: max ~8 rows visible (match editor's maxRenderedOptions: 8); internal scroll for overflow.
  • Multiple console forms on one page: call attachAutocomplete per form. Each instance owns its own dropdown DOM node.

Verification

End-to-end check after implementation:

  1. Console smoke test on server-detail page (run dev server: python scripts/dev-server.py, open a server-detail page):

    • Focus console input. Network panel shows one fetch for srccfg-vocab.json.
    • Type sv_ → dropdown appears with cvars, top row highlighted.
    • Press Tab → first token replaced; dropdown stays/closes as expected.
    • Press Esc → dropdown closes; input retains current text.
    • Press ArrowUp → history recall works (no autocomplete interference).
    • Type sv_cheats 1 → after the space, dropdown is hidden.
    • Press Enter on sv_che (with sv_cheats highlighted in dropdown) → server receives sv_che (the typed text), not sv_cheats.
    • Refocus input → no second fetch (cached).
  2. Editor regression check: open a .cfg file in the editor, verify autocomplete still works exactly as before (ranker extraction is transparent).

  3. Unit test for rankVocab: small test file covering exact/prefix/substring ordering and the truncation limit. Pure function, fast.

  4. No automated browser e2e unless the project already has Playwright/Cypress — check during planning phase. Don't add a test framework as part of this work.

Non-Goals (Repeated for Clarity)

These are explicitly not part of this work and should be deferred to follow-up specs if needed:

  • Argument value completions (player/map/etc. names).
  • Fuzzy matching.
  • Replacing CodeMirror's editor dropdown.
  • Multi-vocab/multi-game support beyond the URL parameter being available.