app: mode-threaded completion, overlay, and validity indicator

The dispatch-layer mode gate (previous commit) made the submit
behaviour correct — `select` runs in advanced mode and shows
the SQL hint in simple mode. This commit extends that gating to
the ambient assistance layer so simple-mode users do not see
SQL leak through Tab completion, the live error overlay, or the
`[ERR]`/`[WRN]` validity indicator either.

`_in_mode` walker variants
--------------------------
- `completion_probe_in_mode`, `expected_at_input_in_mode`,
  `input_verdict_in_mode`. Each sets `ctx.mode` before walking.
  The empty-input / unknown-entry fallback in `completion_probe`
  and `expected_at_input` filters the `REGISTRY` listing by
  `is_advanced_only` so Tab does not offer `select` in simple
  mode. Old signatures keep delegating to `Mode::Advanced`
  (back-compat for tests + other callers).

`_in_mode` completion variants
------------------------------
- `candidates_at_cursor_in_mode`, `candidates_at_cursor_with_in_mode`.
  Internally they route the `parse_command` completeness probe
  through `parse_command_in_mode(input, mode)`, the
  `completion_probe` call through `completion_probe_in_mode`,
  and the `expected_at` fallback through
  `expected_at_input_in_mode`. Old signatures default to
  `Mode::Advanced`.

`EffectiveMode::as_mode`
------------------------
- Collapses the persistent / one-shot distinction the UI cares
  about into the plain `Mode` the walker reads from
  `WalkContext::mode`. App-level call sites that thread mode
  into the walker chain use this.

App / input-render wiring
-------------------------
- `App::input_validity_verdict` runs only when effective mode
  is plain `Simple` (per ADR-0027), so it hardcodes
  `Mode::Simple` into the new `input_verdict_in_mode` call
  rather than threading.
- `App::start_or_complete_at` / `_last` (the Tab handlers)
  pass `self.effective_mode().as_mode()` into
  `candidates_at_cursor_in_mode`, so a `:` one-shot or
  persistent advanced gives full SQL completion, persistent
  simple does not offer SQL.
- `input_render::render_input_runs` and `ambient_hint` are
  invoked from `ui.rs` only when effective mode is plain
  `Simple` (advanced rendering uses `plain_input_spans` and
  skips ambient hinting per ADR-0022 §12). Their internal
  `classify_input_with_schema` / `candidates_at_cursor` /
  `parse_command` calls now go through the mode-aware variants
  with `Mode::Simple` hardcoded — a SQL form in simple mode
  surfaces as a definite-error overlay and the hint panel does
  not offer it.

After this commit a simple-mode user typing `select` or
`sel<Tab>` sees nothing SQL-shaped: no live highlight, no Tab
completion candidate, the `[ERR]` indicator lit, and the on-
submit hint that names the recovery paths. An advanced-mode
user or a `:` one-shot sees the full SQL surface.
This commit is contained in:
claude@clouddev1
2026-05-19 21:48:21 +00:00
parent 6369066fe4
commit 83e0ddc2ff
4 changed files with 157 additions and 35 deletions
+21 -4
View File
@@ -24,7 +24,8 @@
use ratatui::style::{Color, Modifier, Style};
use crate::dsl::parser::parse_command_with_schema;
use crate::dsl::parser::{parse_command_in_mode, parse_command_with_schema, parse_command_with_schema_in_mode};
use crate::mode::Mode;
use crate::dsl::walker;
use crate::dsl::{ParseError, parse_command};
use crate::theme::Theme;
@@ -67,7 +68,15 @@ pub fn render_input_runs(
cache: &crate::completion::SchemaCache,
) -> Vec<StyledRun> {
let mut runs = lex_to_runs(input, theme);
if let InputState::DefiniteErrorAt(pos) = classify_input_with_schema(input, cache) {
// `render_input_runs` is invoked from `ui.rs` only when the
// effective mode is plain `Simple` (advanced rendering uses
// `plain_input_spans`), so the classification must use the
// simple-mode walker view (ADR-0030 §2): a SQL form here
// surfaces as a definite error overlay, consistent with the
// dispatch path's "this is SQL" hint on submit.
if let InputState::DefiniteErrorAt(pos) =
classify_parse_result(parse_command_with_schema_in_mode(input, cache, Mode::Simple))
{
overlay_error(&mut runs, pos, theme);
}
if let Some(inv) = crate::completion::invalid_ident_at_cursor(input, cursor_byte, cache) {
@@ -320,7 +329,13 @@ pub fn ambient_hint(
// Candidates win when any exist — the panel surfaces them
// directly because they're more actionable than prose
// framings.
if let Some(comp) = crate::completion::candidates_at_cursor(input, cursor, cache) {
// `ambient_hint` is only called for `EffectiveMode::Simple`
// (ui.rs gates it), so completion runs through the
// simple-mode walker view — `select` does not surface here
// (ADR-0030 §2).
if let Some(comp) =
crate::completion::candidates_at_cursor_in_mode(input, cursor, cache, Mode::Simple)
{
return Some(AmbientHint::Candidates {
items: comp.candidates,
selected: None,
@@ -348,7 +363,9 @@ pub fn ambient_hint(
)));
}
// Otherwise fall back to the prose framings from stage 5.
match parse_command(input) {
// ADR-0030 §2: simple-mode hint uses the simple-mode walker
// view so a SQL form surfaces with the "this is SQL" hint.
match parse_command_in_mode(input, Mode::Simple) {
Ok(_) => Some(AmbientHint::Prose(crate::t!("hint.ambient_complete"))),
Err(ParseError::Empty) => None,
Err(ParseError::Invalid {