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>
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.jsondata file. - Out of scope: argument-value completion (player names for
kick, map names forchangelevel, etc. — needs runtime data); fuzzy/typo-tolerant matching; custom per-game vocabs (the API accepts a URL so the door is open, but onlysrccfg-vocab.jsonships); 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:
-
Extract the ranker. Move pure ranking logic out of
l4d2web/scripts/editor-src/autocomplete.jsinto new modulel4d2web/scripts/editor-src/vocab-rank.js. Function signature:rankVocab(query: string, vocab: {cvars, commands}) → Array<{name, desc, kind}>. No CodeMirror dependency.autocomplete.jsre-imports it; the editor build keeps working unchanged. -
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). -
Wire-up on server-detail. A new
<script>tag inserver_detail.html(placed alongside the existingconsole-history.jstag) callsattachAutocomplete(form.querySelector('input[name=command]'), '/static/data/srccfg-vocab.json')once on page load.console-history.jsis not modified — it keeps owning ArrowUp/Down, Enter, and the input'sdata-console-formattribute. The two modules cooperate by owning disjoint key sets.
Components & Interfaces
vocab-rank.js—rankVocab(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 callsrankVocabinstead 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 withgetBoundingClientRect()of the input focuslistener (one-shot) → triggers fetchinputevent listener → recompute matches, show/hide dropdown based on first-token rulekeydownlistener for Tab / Shift+Tab / Esc only —preventDefault()only for those keysblur/Esc/ formsubmit→ close dropdown
- module-scoped fetch promise (deduplicates concurrent fetches;
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.js— new. 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.js— new. Vanilla dropdown.l4d2web/l4d2web/static/js/console-history.js— untouched (verify by diff after wiring change).l4d2web/l4d2web/templates/server_detail.html— add one<script>tag forconsole-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.warnfor 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
attachAutocompleteper form. Each instance owns its own dropdown DOM node.
Verification
End-to-end check after implementation:
-
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(withsv_cheatshighlighted in dropdown) → server receivessv_che(the typed text), notsv_cheats. - Refocus input → no second fetch (cached).
- Focus console input. Network panel shows one fetch for
-
Editor regression check: open a
.cfgfile in the editor, verify autocomplete still works exactly as before (ranker extraction is transparent). -
Unit test for
rankVocab: small test file covering exact/prefix/substring ordering and the truncation limit. Pure function, fast. -
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.