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>
This commit is contained in:
parent
86ac10a1d9
commit
02d96b593e
1 changed files with 122 additions and 0 deletions
|
|
@ -0,0 +1,122 @@
|
||||||
|
# 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.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 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 *only* — `preventDefault()` 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.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 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.
|
||||||
Loading…
Reference in a new issue