Files
rdbms-playground/docs/handoff/20260512-handoff-7.md
claude@clouddev1 1eb2e0d01f handoff
2026-05-12 09:02:54 +00:00

32 KiB

Session handoff — 2026-05-12 (7)

Seventh handover. The previous session (handoff-6) shipped ADR-0019 (friendly error layer + i18n catalog). This session designed and fully implemented ADR-0020 (tokenization layer) and ADR-0021 (per-command usage templates / H1a), then designed and shipped all eight stages of ADR-0022 (ambient typing assistance) — colour highlighting, error overlay, hint panel, non-modal Tab completion with cycling, schema cache, identifier completion, invalid-identifier detection. Four rounds of real-app testing produced follow-up fixes.

The next agent picks up a clean baseline with no design deferrals or known bugs — only a partial test pass from the user that may turn up further findings.

State at handoff

Branch: main. Working tree clean. Up to date with origin/main at c247f55 — the user pushed along the way, so there is no in-flight commit queue to publish. Verified via git status at handoff time (the per-message "X commits ahead" lines earlier in the session were stale snapshots — do re-check git status rather than trust running totals).

Commits since handoff-6 (in chronological order, 26 total):

857ee75 ADR-0020 + ADR-0021: tokenization layer and parse-error pedagogy (H1a)
fdaf7e3 ADR-0020 implementation: lexer + parser refactor over &[Token]
11071ae ADR-0021 implementation: per-command usage templates in parse errors
f0632af ADR-0022: ambient typing assistance (unifies I3 + I4)
00c9dea ADR-0022 stage 1/8: theme token-class colour fields
cafc455 ADR-0022 stage 2/8: input panel — token-class highlighting
39da399 ADR-0022 stage 3/8: simple-mode echo lines highlighted
313d4f8 ADR-0022 stage 4/8: render-time parse + error overlay
9c4857e ADR-0022 stage 5/8: hint panel ambient typing assistance
6845df1 ADR-0022 stage 6/8: IdentSlot taxonomy + parser audit
aea3224 ADR-0022 stage 7/8: schema query plumbing
06e8d1e ADR-0022 stage 8a: non-modal keyword completion + Esc/Backspace undo
faebeed ADR-0022 stage 8b: hint panel candidate list with scroll markers
51a8d9a ADR-0022 stage 8c: IdentSlot propagation + SchemaCache API
7a32c13 ADR-0022 stage 8d: schema cache refresh wiring
8214e41 ADR-0022 stage 8e: invalid-identifier detection + hint variant
bd1cce6 ADR-0022 stage 8 follow-up: fixes from real-app testing
f94a999 ADR-0022 stage 8 follow-up r2: completion UX fixes from real testing
22119d6 ADR-0022 follow-up r3: identifier colour, NewName hint, "Next:" wording, "type" label
c247f55 ADR-0022 follow-up r4: column-type completion

Tests: 760 passing, 0 failing, 1 ignored (up from 610 at handoff-6's baseline; +150 over this session). The ignored test is the same \``ignoredoc-test insrc/friendly/mod.rs` that was already ignored at handoff-6 — not new debt.

Per-area test deltas:

  • ADR-0020 (lexer + parser refactor): +40 (29 lexer + 11 keyword/punct)
  • ADR-0021 (per-command usage): +18 (8 usage + 1 token-vocab completeness + 9 parse-error-pedagogy integration tests)
  • ADR-0022 stages 1-8e: +37 (4 theme + 9 input_render core
    • 5 input_render ambient + 5 db schema-cache + ~14 completion + others)
  • ADR-0022 round-1 follow-up: +3
  • ADR-0022 round-2 follow-up: +3
  • ADR-0022 round-3 follow-up: +5
  • ADR-0022 round-4 follow-up: +5

Clippy: clean with nursery lints enabled.

Release build: not measured this session; the existing ~7.8 MB binary will grow modestly from the new modules (completion, input_render, keyword, lexer, usage, ident_slot) but they're all hand-rolled, no new dependencies.

What's implemented (delta vs. handoff-6)

ADR-0020 — tokenization layer for the DSL parser

Amends ADR-0001. Adds a lexer between the source string and the chumsky grammar; chumsky now parses &[Token] instead of &str.

What it provides:

  • src/dsl/lexer.rs — tokenizer producing Vec<Token> with byte-offset spans. Always succeeds: lex-shape errors (unterminated string, unrecognised character, malformed flag) embed as TokenKind::Error(_) tokens in the stream rather than as a Result variant. Rationale: I4 (syntax highlighting) needs to render partial / invalid input uniformly; the parser sees Error tokens and raises a structural error at that point.
  • src/dsl/keyword.rsKeyword and Punct enums declared via define_keywords! / define_punct! macro_rules! invocations. Single source of truth: the enum, the lex-side string→variant mapping, the variant→literal rendering, and the parse.token.keyword.* / parse.token.punct.* catalog-key derivation all come from one declaration block. Adding a keyword is one line of Rust + one line of YAML.
  • Parser refactor. All combinators in src/dsl/parser.rs rewritten to operate on &[Token]. Keyword aggregation across choice now works natively ("expected data or table" instead of just "expected table"). Custom try_map content errors (unknown type, mutually-exclusive flags, "with pk needs at least one column", "specified twice") survive unchanged.
  • replay bare-path UX preserved via a one-place source-slice special case (ADR-0020 §6): after matching Keyword(Replay), the parser reads the remainder of the source string directly rather than reassembling tokens.
  • I3 / I4 hooks committed at the parser level: lexer always produces a stream; parser's expected-token-set is queryable.
  • Honest history note in the ADR: the no-lexer shape in dsl/parser.rs arose incrementally without ADR-level deliberation against the known I3/I4/H1a requirements. ADR-0020 corrects that.

ADR-0021 — parser-as-source-of-truth for H1a

Builds on ADR-0020. Pulls forward H1a from requirements.md.

What it provides:

  • src/dsl/usage.rs — per-command UsageEntry registry keyed off entry-keyword. Multi-entry families (add, drop, show) return multiple keys; unique-entry keywords return one. matched_entry(tokens, failure_position) resolves the entry keyword from the consumed prefix.
  • Catalog sections under parse.usage.<command> (templates) and parse.token.{keyword,punct, identifier,...} (single-token vocabulary).
  • Renderer (in app.rs::dispatch_dsl) composes three blocks: caret + structural/custom error + usage template (or "available commands:" fallback when no entry keyword was consumed, e.g. frobulate Customers).
  • tests/parse_error_pedagogy.rs — 9 integration tests covering create/add/drop/show/frobulate/update/ insert pedagogy cases.
  • Anchor-phrase compliance preserved (ADR-0019 §10): existing tests assert on substrings like "no such table", "already exists"; unchanged.

ADR-0022 — ambient typing assistance (the big one)

Unified design that subsumed the originally-planned separate ADRs for syntax highlighting (I4) and tab completion (I3). The framing: colour, hint panel, and Tab are three answers to the same question — what does the user need to know mid-typing? — and planning them separately produces three loose pieces that drift apart.

What it provides (eight implementation stages + four testing-round follow-ups):

  • Token-class colouring in the input pane and simple-mode echo history (stages 1-3). Theme gains seven tok_* fields. Identifier colour was tuned twice from user feedback; final values are #56B6C2 (dark) / #0F6B76 (light) — vivid teal to make identifiers stand out.
  • Render-time parse + error overlay on the failing token (stage 4). ParseError::Invalid gained an at_eof: bool flag; custom errors map to at_eof = true conservatively to avoid over-highlighting.
  • Hint panel ambient mode (stage 5) — sub-states Valid/IncompleteAtEof/DefiniteError/InvalidIdent/ TypingName. Three-block composition (caret + error + usage) when relevant.
  • Identifier-slot taxonomy (stage 6): IdentSlot enum (NewName, TableName, Column, RelationshipName) with expected_label() / from_expected_label() round-trip. ident_ctx(slot) wrapper labels each call site; every ident() in the parser was audited and tagged.
  • Schema query plumbing (stage 7) — Database::list_names_for(slot) worker request.
  • Schema cache refresh wiring (stage 8d) — AppEvent::SchemaCacheRefreshed; runtime posts at every TablesRefreshed site (project load, post-DDL, rebuild, replay).
  • Non-modal Tab completion (stage 8a):
    • Single candidate → insert with trailing space, no memo. Chains naturally: a Tab → add , Tab → add column .
    • Multi candidate → insert WITHOUT trailing space, create memo. Tab cycles forward (Shift-Tab backward, starting from last); space (or any non-Tab key) clears the memo and inserts itself naturally, producing the final <chosen> form.
    • Esc/Backspace while memo alive → restore original text + cursor in one keystroke. The user's preference for symmetric insert-and-undo is baked in.
  • Multi-candidate hint panel (stage 8b) with < / > scroll markers when items overflow; selected item bold. Kind-coloured (keyword purple, identifier teal).
  • Sticky hint during cycling — while the memo is alive, the hint shows the memo's candidate list with the new selection, NOT what comes next at the post-Tab cursor.
  • Grammar-order ordering — keywords come first in chumsky's source-order traversal (so to before table for add column [to] [table] …), then schema identifiers alphabetised. Achieved by dropping a sort in describe_expected rather than alphabetising blindly.
  • Invalid-identifier detection (stage 8e) — when the user types text that doesn't prefix-match any schema entry at a known-set slot, render red overlay + "no such {kind}: {found}" hint.
  • NewName slot probe (round 3) — at user-invents-the- name positions, the hint reads "Type a name, then (" (or whatever follows) instead of the technical "next: …" that would surface once the partial identifier got consumed. The "what follows" is computed by re-parsing with a single-letter placeholder identifier substituted at the cursor.
  • Type-name completion (round 4) — Type::all() surfaces as Tab candidates at ( slots, filtered by partial prefix, in declaration order (text/int/real/decimal/bool/date/datetime/blob/serial/ shortid). Type names live outside the Keyword enum (ADR-0020 §2 keeps them as identifiers validated by Type::from_str) so they needed their own completion path via the TYPE_SLOT_LABEL constant.
  • "Next:" wording instead of "expected:" — friendlier framing. Hint sentences are now capitalised (Submit/Next/Type/No such).

Open testing work — picks up next session

The user has noted that every testing round has turned up issues, and the next session begins with another testing pass. Issues found and fixed so far (rounds 1-4):

  1. r1: hint ordering (keywords alphabetised vs. grammar order); hint-panel kind colouring; tok_identifier equal to theme.fg (no contrast).
  2. r2: stuck-on-unique completions (memo created for single candidate caused Tab to no-op visibly); grammar order regression (alphabetical sort was burying chumsky's source order).
  3. r3: identifier colour still too subtle; "expected: …" reading as a leaked diagnostic; "Next: something else" after ( (unlabelled type_keyword); typing into a NewName slot showed the post-consume "expected: (" prose.
  4. r4: column-type completion entirely missing — type names aren't in the Keyword enum so the completion engine never saw them.

The fixes from each round are described in the matching commit messages. Nothing is currently outstanding from the user's reports — but the user has explicitly noted that they will run another testing pass and expect to find more, so the next session should:

  1. Do nothing structural until the user has tested. Wait.
  2. Expect findings to be small surface-level UX issues like the previous rounds (wording, ordering, colour adjustments, edge cases in completion / hint panel / render).
  3. Approach each finding by tracing the symptom to the minimum-viable layer (completion engine, ambient_hint, render-time classifier, parser labelling, catalog wording), then applying the fix there.

Known surface-level wart (not user-reported but

visible)

The create table Customers case still shows the lowercase custom-error wording "tables need at least one column. Add with pk..." in the hint panel — because that message is hand-written in parser.rs source code rather than coming through the catalog. Capitalising it needs the user's sign-off (rewriting a parser error string). Flagged in commit 22119d6's message.

Push to test the still-quiet areas

What hasn't been tested yet in user-facing terms:

  • Shift-Tab cycling (only forward-Tab has been exercised in the user's reports).
  • Esc / Backspace undo of a multi-candidate Tab (memo path).
  • Cursor-in-the-middle-of-input completion (most tests have been cursor-at-end). The completion engine handles mid-input but it hasn't been driven through the TUI by the user.
  • Long candidate lists that trigger the < > scroll markers — schema with many tables.
  • Multi-byte UTF-8 in string literals + identifiers (the lexer handles this but it hasn't been driven from the keyboard).
  • Advanced mode behaviour — should be plain-text render with no hint panel content, no Tab completion. Confirmed by code but not user-tested in this session.
  • Replay with bare paths and quoted paths, particularly the path special-case (ADR-0020 §6) under the new lexer.
  • Interaction with modals — should completion be suppressed when a modal is open? Code currently short-circuits via if self.modal.is_some() return handle_modal_key(key) BEFORE the completion match arms, so completion is correctly suppressed.

After testing — three sizeable pieces in priority order

  1. A1 (CI workflow). Has been the "easy quick win" for several sessions now and hasn't shipped. Standard GitHub Actions YAML at .github/workflows/ci.yml; cross-platform Linux / macOS / Windows; cargo test

    • cargo clippy --all-targets -- -D warnings. Locks in the 760-test green baseline. 1-2 hours.
  2. Query DSL ADR + implementation. Biggest remaining design piece. Discussed in handoff-6: extend show data into a SELECT-style command with WHERE / projection / order; expose generated SQL as a pedagogical hook; bundle C5a's complex WHERE into one coherent feature. Then QA1 (EXPLAIN QUERY PLAN) becomes meaningful. Probably 600-1000 lines of ADR + 800-1500 of implementation.

  3. Constraint management surface (C3). UNIQUE / CHECK / NOT NULL DDL operations. The friendly-error layer (ADR-0019) has CHECK wording ready; the missing piece is the DDL surface itself.

V-series UX projects

Still pending from handoff-6:

  • V4 — session log + Markdown export.
  • V1/V2 — relationship rendering (the "two structures
    • arrow" view).
  • V3 — ER diagram export.

Smaller items still on the table

  • I1 — multi-line input (Enter inserts newline, Ctrl-Enter submits).
  • I1b — readline shortcuts (Ctrl-A/E, Ctrl-W/K/U).
  • C4 — m:n convenience (auto-junction-table).

Tracked but bounded to other ADRs

  • Q1 (SQL handling in advanced mode) — waits on Q4 (SQL subset ADR), which itself waits on the lexer model for advanced-mode tokenization. Now that ADR-0020 has established the DSL-side lexer, the Q4 ADR can decide whether to share / wrap sqlparser-rs's tokens or add a parallel SQL lexer.
  • U-series undo/snapshot.
  • Settings persistence — feeds the deferred messages persistence and (when it arrives) any user-configurable theme picker.

Sharp edges and subtleties (delta vs. handoff-6)

Carried-over edges still apply. New ones this session:

  • The parser combinators are now Parser<'a, &'a [Token], …> instead of Parser<'a, &'a str, …>. Helper combinators in dsl/parser.rs: kw(Keyword), punct(Punct), ident_inner() / ident_ctx(IdentSlot), number_literal(), string_literal(), string_payload(), flag(name). All defined inline at the top of the parser combinator section.

  • ident_inner is the only call that produces a bare identifier match. Every command parser combinator must use ident_ctx(slot). There is no compile-time enforcement (ident_inner is fn not a sealed type), only convention + code review.

  • ParseError::Invalid gained two fields: at_eof: bool (stage 4) and expected: Vec<String> (stage 5). Pattern matches with .. are unaffected; the two constructions in dsl/parser.rs populate both correctly. at_eof for custom errors is conservatively true — known limitation, noted on the field's docstring.

  • Type names are NOT keywords. They lex as Identifier tokens; the parser's type_keyword() helper uses Type::from_str to classify, which emits the existing "unknown type 'X' (expected one of: …)" custom error on miss. The completion engine recognises the "type" label (from ident_inner().labelled("type") inside type_keyword) via the TYPE_SLOT_LABEL constant in completion.rs. Adding a new type means: add the variant + keyword to Type::all() (ADR-0005), and the completion / error paths pick it up automatically.

  • The lexer eats whitespace. padded() combinators are gone from the parser entirely. Whitespace positioning between tokens is recoverable from token spans if anyone needs it.

  • replay parses via a special-case in try_parse_replay_with_bare_path (before the chumsky parser runs). Quoted paths go through chumsky; everything else is source-sliced from the byte after replay. Documented in ADR-0020 §6.

  • The hint panel render now branches on the AmbientHint enum: Prose(String) or Candidates { items, selected }. The renderer in ui.rs::render_candidate_line builds spans with per-candidate kind colouring and < / > scroll markers when overflowing.

  • The schema cache refresh fires from the runtime, not from the App. App receives AppEvent::SchemaCacheRefreshed(cache) and stores it. Refresh sites: project load, after DDL, after rebuild, after replay. Best-effort: a failed list_names_for for one slot kind leaves that field empty in the cache rather than dropping the whole refresh.

  • Tab/Shift-Tab key handling is at the TOP of handle_key — before the modal short-circuit's match arm and before the regular key matcher. The ordering is: if modal { handle_modal_key }; match Tab/BackTab/Esc-with-memo/Backspace-with-memo { … }; match (the existing key matcher) { … }. Esc / Backspace match arms have if self.last_completion.is_some() guards so they fall through to normal behaviour when no memo is alive.

  • completion::candidates_at_cursor is sync. It consults the in-memory SchemaCache rather than the worker thread. The cache may be slightly stale between a DDL command and its SchemaCacheRefreshed event; acceptable per ADR-0022 §9.

  • NewName slots return &[] from SchemaCache::for_slot — the user invents these names. The typing_name_at_cursor function handles them with a friendlier hint ("Type a name, then …") via a placeholder-substitution re-parse.

  • The TYPE_SLOT_LABEL = "type" const in completion.rs must equal what dsl::parser::type_keyword labels its select_ref! with. The strings are physically separate; no compile-time link. If a future maintainer changes one, the other must change too.

  • The macro-generated Keyword::ALL and Punct::ALL arrays are the canonical iteration source for catalog-validity tests. A new keyword or punct that forgets a YAML entry under parse.token.* fails the keys_validate_against_catalog test loudly.

0000 Record architecture decisions (process)
0001 Language and TUI framework (Rust + Ratatui)
        — amended by ADR-0020 (adds tokenization layer)
0002 Database engine (User-facing posture)
0003 Input modes and command dispatch
0004 Project file format (amended by 0015)
0005 Column type vocabulary
0006 Undo snapshots and replay log
0007 Sharing and export (amended by 0015 amendment 1)
0008 Testing approach
0009 DSL command syntax conventions
0010 Database access via worker thread
0011 FK column type compatibility
0012 Internal metadata for user-facing column types
0013 Relationships, naming, and rebuild-table strategy
0014 Data operations, value literals, and auto-show
0015 Project storage runtime
0016 Pretty table rendering for data and structure views
0017 Column type-change compatibility
0018 Auto-fill contracts for serial and shortid columns
0019 Friendly error layer (H1) and i18n message catalog
0020 Tokenization layer for the DSL parser
        — IMPLEMENTED (this session). Amends ADR-0001.
0021 Parser-as-source-of-truth for H1a
        — IMPLEMENTED (this session). Builds on ADR-0020.
0022 Ambient typing assistance (I3 + I4 unified)
        — IMPLEMENTED through stage 8e + 4 testing rounds.

Repository layout (delta vs. handoff-6)

src/
  completion.rs                — NEW. SchemaCache, Candidate,
                                  CandidateKind, Completion,
                                  LastCompletion structs.
                                  candidates_at_cursor,
                                  invalid_ident_at_cursor,
                                  typing_name_at_cursor.
  input_render.rs              — NEW. StyledRun, AmbientHint,
                                  InputState. render_input_runs
                                  (token-class colour + parse-
                                  error overlay + invalid-ident
                                  overlay + cursor injection).
                                  ambient_hint (the resolution
                                  ladder: memo → candidates →
                                  invalid_ident → typing_name →
                                  parse-prose fallback).
                                  classify_input, lex_to_runs.
  dsl/
    ident_slot.rs              — NEW. IdentSlot enum +
                                  expected_label /
                                  from_expected_label round-trip.
    keyword.rs                 — NEW. define_keywords! /
                                  define_punct! macros + ALL
                                  static tables.
    lexer.rs                   — NEW. Token, TokenKind,
                                  LexError, Span. lex() always
                                  succeeds; embeds Error tokens
                                  in the stream.
    parser.rs                  — Heavily refactored. Parser
                                  now operates on &[Token].
                                  ident_ctx(slot) wrapper.
                                  ParseError gained at_eof +
                                  expected fields.
    usage.rs                   — NEW. UsageEntry registry +
                                  matched_entry +
                                  entry_keywords_alphabetised.
  app.rs                       — Tab/BackTab/Esc-with-memo/
                                  Backspace-with-memo handlers.
                                  completion_tab_forward /
                                  _backward,
                                  start_or_complete_at / _last,
                                  commit_unique / commit_multi,
                                  replace_inserted,
                                  undo_last_completion.
                                  AppEvent::SchemaCacheRefreshed
                                  handler.
                                  last_completion: Option<…>,
                                  schema_cache: SchemaCache
                                  fields on App.
                                  render path in dispatch_dsl
                                  composes the three-block
                                  parse error per ADR-0021 §2.
  ui.rs                        — render_input_panel uses
                                  input_render::render_input_runs
                                  for simple mode; advanced
                                  modes fall back to plain
                                  before/under/after.
                                  render_output_line peels the
                                  dsl::ECHO_PREFIX from simple-
                                  mode echo lines and re-
                                  tokenises (lex_to_runs).
                                  render_hint_panel dispatches
                                  on AmbientHint variant;
                                  render_candidate_line composes
                                  spans with kind colouring +
                                  `<` `>` scroll markers.
  theme.rs                     — Seven new tok_* Color fields
                                  on Theme. Theme::token_color
                                  helper. WCAG-AA contrast
                                  values for dark + light.
  event.rs                     — AppEvent::SchemaCacheRefreshed
                                  variant.
  runtime.rs                   — refresh_schema_cache helper +
                                  call sites at every
                                  TablesRefreshed point.
  friendly/
    keys.rs                    — Massive expansion. hint.*,
                                  parse.usage.*, parse.token.*,
                                  ambient_*, invalid_ident,
                                  typing_name, typing_name_then
                                  keys all validated.
    strings/en-US.yaml         — Same expansion on the YAML
                                  side. Hint sentences are
                                  capitalised (Submit / Next /
                                  Type / No such).
docs/
  adr/
    0020-tokenization-layer-for-the-dsl-parser.md
    0021-parser-as-source-of-truth-for-h1a.md
    0022-ambient-typing-assistance.md
    README.md                  — indexed
  handoff/
    20260512-handoff-7.md      — this file
tests/
  parse_error_pedagogy.rs      — NEW (ADR-0021). 9 integration
                                  tests for caret + structural
                                  error + usage template
                                  composition.

How to take over

  1. Read this file.
  2. Read CLAUDE.md for the working-style rules.
  3. Read docs/requirements.md for the granular progress table (note: probably wants an update reflecting the stage-8 completion of I3 and I4).
  4. Expect the user to begin with another testing pass. Resist the urge to ship anything new until they have. Past rounds: r1-r4 each turned up real findings that needed surgical fixes.
  5. When findings arrive, trace each one to the minimum relevant layer before patching:
    • Completion behaviour → src/completion.rs (candidates_at_cursor, invalid_ident_at_cursor, typing_name_at_cursor).
    • Hint panel content → src/input_render.rs::ambient_hint.
    • Hint panel render → src/ui.rs::render_candidate_line / render_hint_panel.
    • Input pane colours → src/theme.rs (tok_* fields).
    • Parser shape / labels → src/dsl/parser.rs.
    • Catalog wording → src/friendly/strings/en-US.yaml (and keys.rs for the placeholder declarations).
  6. After testing settles, the next bigger move is A1 (CI workflow) — quick win that locks in the now- substantial 760-test baseline before more design work.
  7. Then Query DSL ADR + implementation is the recommended bigger piece.
  8. Run cargo test to confirm the 760-test green baseline.
  9. Run cargo clippy --all-targets to confirm clippy-clean.

End-to-end smoke test (current state, post-r4)

Demonstrates ambient typing assistance. Replaces handoff-6's smoke test, which is now stale (it predates ADR-0022).

$ rm -rf /tmp/handoff7-smoke
$ rdbms-playground --data-dir /tmp/handoff7-smoke

# Inside the app — empty input. Hint panel shows the existing
# panel.hint_empty content (no ambient interference until you
# type).

# Type a single character. Hint immediately offers candidates.
a                                          -- hint: "add"
                                              (single Keyword
                                              candidate, purple)

# Tab inserts with trailing space (single candidate, no memo).
<Tab>                                       -- input becomes "add "
                                              hint: "column"
                                              (next required kw)

# Tab again chains through unique completions.
<Tab>                                       -- input becomes "add column "
                                              hint: "to table"
                                              (multi: `to`, `table`,
                                              in source order)

# Multi-candidate Tab: inserts WITHOUT trailing space.
<Tab>                                       -- input "add column to"
                                              (memo alive,
                                              `to` highlighted)

# Tab cycles within the memo.
<Tab>                                       -- input "add column table"
                                              (memo still alive,
                                              `table` highlighted)

# Pressing space commits the choice. Memo clears.
<space>                                     -- input "add column table "

# Continue. Tab on the table-name slot offers schema entries
# in cyan-teal (identifier colour).
<Tab>                                       -- if schema has Customers,
                                              Orders, etc., they cycle.

# Type a fresh column name yourself.
Customers: Email                            -- builds "add column to
                                              table Customers: Email"

# Now hint reads "Type a name, then `(`" — wait, that's at the
# NewName slot. After typing Email the parser has consumed it.
# Hint reads "Next: `(`".

# Type the type slot.
 (de                                        -- hint: "decimal"
                                              (single match,
                                              keyword colour)

<Tab>                                       -- input completes to
                                              "...Email (decimal "
                                              with trailing space

# Type close paren and submit.
)<Enter>                                    -- command runs.

# Invalid identifier feedback:
show data Custp                             -- if no table starts
                                              with Custp, `Custp`
                                              renders red and hint
                                              says "No such table:
                                              `Custp`"

# Unknown command fallback:
frobulate widgets                           -- on submit, hint
                                              shows usage block;
                                              live hint shows
                                              "available commands"
                                              fallback.

quit

Manual spot-checks worth running

  • Single-Tab chaining: a Tab Tab Tab Tab — should build add column to (the unique-chain runs out at position 4 where to and table are both possible).
  • Multi-Tab cycling: show Tab Tab Tab — should cycle data → table → data (wrap).
  • Esc undo: show Tab Esc — should restore to show .
  • Backspace undo (symmetric): show Tab Backspace — should also restore to show .
  • Sticky hint: show Tab Tab — hint stays as the two-item candidate list with the selection moving.
  • Invalid identifier: type show data X where no table starts with X — X should render red + "No such table" hint.
  • Type completion: add column to T: Name ( Tab Tab Tab — cycles through types.
  • Long candidate list: with many tables (say 12+), position at show data to see the < / > scroll markers in the hint panel.
  • Advanced mode: :show data (one-shot) or mode advanced — input renders plain, hint shows existing panel.hint_empty content, Tab is a no-op.
  • Replay with bare path: replay history.log — should work as before via the source-slice special case.
  • Replay with quoted path: replay 'my project/data.log' — chumsky path.
  • Multi-byte UTF-8 inside a string literal: insert into T values ('café') — lexer + render must not panic.