# 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.
