supercode-cli 0.5.71

Volter Harness — a lightweight, fully-customizable AI coding agent CLI in Rust. Any model via OpenRouter; natively continues Claude Code and Codex sessions.
//! Tab-completion for the interactive `chat` REPL (UX-19).
//!
//! rustyline only offers completion through a `Helper` implementing
//! `Completer` (`Hinter`/`Highlighter`/`Validator` come along for free via
//! [`rustyline::Helper`]'s supertrait bound). [`ReplHelper`] is that helper;
//! `chat()` in `main.rs` is the only place it's constructed and wired into
//! the `Editor`, so this module has zero effect outside the interactive
//! REPL — non-interactive `run`/`resume`/`--quiet`/machine-output paths
//! never build an `Editor` at all, let alone this helper.

use rustyline::completion::{Completer, FilenameCompleter, Pair};
use rustyline::highlight::Highlighter;
use rustyline::hint::Hinter;
use rustyline::validate::Validator;
use rustyline::{Context, Result};

use crate::slash::{slash_command_names, slash_command_takes_path};

/// The `chat` REPL's rustyline helper. Tab completion has exactly two
/// modes, chosen by what's already typed:
///
/// 1. Line starts with `/` and the command name isn't finished (no
///    whitespace yet) → complete against [`slash::slash_command_names`],
///    the same table `handle_slash` dispatches from (UX-19 dev/01).
/// 2. Line starts with `/<known-path-taking-command> ` (`/add`, `/save`)
///    → delegate to rustyline's built-in [`FilenameCompleter`] for the
///    argument.
///
/// Anything else — including all free-form chat text that doesn't start
/// with `/` — yields zero candidates, so completion is inert for normal
/// prompt input (UX-19 dev/03) and can't interfere with history, Ctrl-C, or
/// any other `Editor` behavior `chat()` relies on.
pub struct ReplHelper {
    filename_completer: FilenameCompleter,
}

impl ReplHelper {
    pub fn new() -> Self {
        Self {
            filename_completer: FilenameCompleter::new(),
        }
    }
}

impl Default for ReplHelper {
    fn default() -> Self {
        Self::new()
    }
}

impl Completer for ReplHelper {
    type Candidate = Pair;

    fn complete(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Result<(usize, Vec<Pair>)> {
        complete_repl_line(&self.filename_completer, line, pos, ctx)
    }
}

/// The actual candidate-generation logic, factored out of the `Completer`
/// impl so it's directly unit-testable without going through rustyline's
/// terminal machinery.
fn complete_repl_line(
    filename_completer: &FilenameCompleter,
    line: &str,
    pos: usize,
    ctx: &Context<'_>,
) -> Result<(usize, Vec<Pair>)> {
    let prefix = &line[..pos];

    // BP-5 (catalog D2 "@-file mentions / attachments", cc§2 "`@` in the
    // prompt triggers file-path autocomplete and injects file context"):
    // the WORD being typed decides, so a mention completes anywhere in the
    // line — mid-sentence is where one is usually written. The injection
    // half is `Agent::expand_file_mentions`; this is the composer half, and
    // it delegates to the same `FilenameCompleter` the path-taking slash
    // commands already use rather than growing a second path walker.
    if let Some(at) = mention_start(prefix) {
        let (_, pairs) = filename_completer.complete(line, pos, ctx)?;
        return Ok((at + 1, pairs));
    }

    // Otherwise only ever activate at the start of the line — free-form chat
    // text (anything not beginning with `/`) gets zero candidates.
    let Some(rest) = prefix.strip_prefix('/') else {
        return Ok((pos, Vec::new()));
    };

    if !rest.contains(char::is_whitespace) {
        // Still typing the command name itself: complete `/xyz` against
        // every accepted spelling (canonical + aliases) that shares this
        // prefix.
        let mut candidates: Vec<Pair> = slash_command_names()
            .filter(|name| name.starts_with(prefix))
            .map(|name| Pair {
                display: name.to_string(),
                replacement: name.to_string(),
            })
            .collect();
        candidates.sort_by(|a, b| a.display.cmp(&b.display));
        return Ok((0, candidates));
    }

    // Command name is finished; completing its argument. Only commands
    // whose argument is a path (`/add`, `/save` — see `slash.rs`) get
    // filename completion; everything else stays inert.
    let cmd_name = prefix.split_whitespace().next().unwrap_or("");
    if slash_command_takes_path(cmd_name) {
        return filename_completer.complete(line, pos, ctx);
    }
    Ok((pos, Vec::new()))
}

/// BP-5: the byte offset of the `@` opening the mention the cursor is
/// inside, or `None` when the cursor is not in one. A mention starts at a
/// word boundary (so an email address's `@` never opens one) and ends at the
/// first whitespace.
fn mention_start(prefix: &str) -> Option<usize> {
    let at = prefix.rfind('@')?;
    if prefix[at..].contains(char::is_whitespace) {
        return None;
    }
    let starts_word = at == 0
        || prefix[..at]
            .chars()
            .next_back()
            .is_some_and(char::is_whitespace);
    starts_word.then_some(at)
}

// Hinter/Highlighter/Validator: no-op implementations, written by hand
// (rather than via the `rustyline-derive` proc-macro's `#[derive(...)]`,
// which would pull in an extra crate) since each trait already provides a
// default method body — the only obligation is `Hinter::Hint`'s associated
// type, which has no default.
impl Hinter for ReplHelper {
    type Hint = String;
}

impl Highlighter for ReplHelper {}

impl Validator for ReplHelper {}

impl rustyline::Helper for ReplHelper {}