shuvarie-core 0.3.3

Blazingly fast AI coding TUI for chivalrous people (core module)
use shuvarie_config::ProviderConfig;
use shuvarie_db::StoredScroll;

#[derive(Debug, Clone)]
pub enum Command {
    Ping,
    ListModels {
        provider_name: String,
    },
    AddProvider {
        id: String,
        config: ProviderConfig,
    },
    /// Register or overwrite a decision-model connection and make it the
    /// active one. The two halves are written together —
    /// `decision-providers { provider "<name>" { … } }` and
    /// `decision { provider "<name>"; model "<model>" }` — because a decision
    /// connection is unusable without a model. `connections.kdl` is the only
    /// store: Selune has no decision-vendor registry, so `model` is free text
    /// and nothing validates it until the first call.
    SetDecisionProvider {
        name: String,
        api_key: Option<String>,
        base_url: Option<String>,
        model: String,
    },
    /// Sign in to an OAuth-backed provider (`chatgpt`, `copilot`) through the
    /// device flow: the verification URL + user code surface as
    /// [`crate::Event::AuthPrompt`], and completion or failure arrives as
    /// [`crate::Event::AuthSuccess`] / [`crate::Event::AuthFailed`]. A cached
    /// or pasted credential is verified in place; providers without OAuth
    /// sign-in report a failure explaining that.
    AuthProviderLogin {
        name: String,
    },
    RemoveProvider {
        name: String,
    },
    SetActiveProvider {
        name: String,
    },
    SetActiveModel {
        model: String,
    },
    /// Cycle the active model's reasoning-effort variant (Ctrl+T): forward
    /// through the Selune catalog's variants, wrapping past the last back to
    /// the unset default. A no-op when the active model is not in the catalog.
    CycleVariant,
    /// Set the active model's reasoning-effort variant directly
    /// (`/variant [name]`). The TUI validates the value against the catalog
    /// before sending; `None` selects the unset default.
    SelectVariant {
        variant: Option<String>,
    },
    /// Set the configured TUI theme (`/theme [name[:variant]]`): writes
    /// `ui.theme` to the config file, `None` returning to the default
    /// (built-in Faerun by the terminal's mode). The TUI validates the value
    /// against the theme set before sending and reports the swap itself;
    /// the core reports [`crate::Event::ConfigSaved`] once the file is
    /// written.
    SetUiTheme {
        pref: Option<String>,
    },
    SaveConfig,
    NewSession,
    /// Start a user turn. `model` optionally overrides the streaming target
    /// for this turn only: a `<provider_kind>/<model>` spec resolved to a
    /// provider connection through the `default-providers` config (see
    /// `resolve_model_override`); `None` streams on the active provider.
    /// An unresolvable spec reports [`crate::Event::StreamError`]. A prompt
    /// queued behind a busy agent (steered) keeps its override.
    SendMessage {
        content: String,
        /// The composer's `@path` attachment directives for this turn:
        /// relative paths resolve against the workspace root (`@`-prefix
        /// stripped), absolute paths are user intent. The core resolves them
        /// (sniffs, bounds images, converts documents) before the turn
        /// starts; any failure reports [`crate::Event::StreamError`] and
        /// aborts the send without persisting anything.
        attachments: Vec<String>,
        model: Option<String>,
    },
    /// Run a bash-mode (`!`-prefixed) command through the resolved shell.
    /// Display-only: the output never persists or reaches the model.
    RunBash {
        command: String,
    },
    CancelStream,
    ListSessions,
    /// Fetch attachment blob bytes for the TUI's display. The reply is one
    /// [`crate::Event::AttachmentMedia`] event whose items carry each hash's
    /// bytes (or `None` when the store cannot serve it). Only sent from the
    /// TUI; the request itself is read-only.
    LoadAttachmentMedia {
        hashes: Vec<String>,
    },
    /// Preview the composer's pending `@path` directives for the attach
    /// strip: per-path existence/size/kind metadata only — no reads or
    /// conversion (the authoritative preparation runs at send time), so the
    /// handler answers directly instead of using the blocking pool. Reply:
    /// [`crate::Event::DirectivesProbed`], matched by `token`. Read-only.
    ProbeDirectives {
        token: u64,
        paths: Vec<String>,
    },
    /// Directory entries for the composer's `@` mention completion (reply:
    /// [`crate::Event::PathCompletions`], matched by `token`). `query` is the
    /// partial path typed after `@` — an optional directory part plus a name
    /// prefix, resolved against the workspace root (absolute too). The
    /// handler answers directly: one `read_dir`, no blocking pool.
    RequestPathCompletions {
        token: u64,
        query: String,
    },
    LoadSession {
        id: uuid::Uuid,
    },
    /// Persist the chat pane's last scroll position for a session, sent by
    /// the TUI when it is about to leave the session (switch or quit).
    SaveScroll {
        id: uuid::Uuid,
        scroll: StoredScroll,
    },
    DeleteSession {
        id: uuid::Uuid,
    },
    /// Rename the active session. The core trims the title; an empty (or
    /// unchanged) title is a no-op.
    SetTitle {
        title: String,
    },
    /// Draft the active session's title with the LLM configured under
    /// `ui.title` `llm` (defaults: the active provider's catalog small model
    /// and the built-in title prompt), from the active path's first user
    /// prompt. Reports [`crate::Event::SessionTitleChanged`] on success and
    /// [`crate::Event::SessionError`] when there is no session, no user
    /// prompt, or no resolvable model. A manual rename that lands while the
    /// call runs wins over the generated title.
    GenTitle,
    SearchHistory {
        query: String,
    },
    AnswerQuestion {
        id: u64,
        /// `None` when the user dismissed the question.
        answers: Option<Vec<Vec<String>>>,
    },
    /// The user's answer to a pending `ask` permission prompt (`id` from
    /// [`crate::Event::PermissionRequested`]). `Allow` grants one call,
    /// `AllowSession` also remembers the ask's scope for the rest of the
    /// run, `AllowDirSession` remembers the path's whole directory tree
    /// (path asks), and `Deny` cuts the turn. A missing id is a no-op.
    PermissionDecide {
        id: u64,
        decision: crate::permissions::PermissionAnswer,
    },
    /// Fork the session: with `node: None`, walk to the active path's last
    /// user prompt (`/undo`); otherwise the fork targets `node`. Turn nodes
    /// fork *before* themselves — their parent becomes the tip and their
    /// content is recalled into the input — while summary/system nodes walk
    /// to themselves. With `after`, the targeted turn node is walked to
    /// instead: it becomes the tip and nothing is recalled (the tree's tool
    /// rows walk to their reply and reply rows walk to themselves this
    /// way). When `summarize`, an LLM summary of the prefix before the fork
    /// point is created first and the forked-away node is reparented under
    /// it.
    ForkSession {
        node: Option<u64>,
        summarize: bool,
        /// Fork *after* the node's turn instead of before it: the node
        /// itself becomes the tip and nothing is recalled into the input.
        after: bool,
    },
    /// Delete a branch of the session tree: the node and all of its
    /// descendants (must not contain the active leaf).
    DeleteBranch {
        node: u64,
    },
    /// Manually compact the active session (`/compact [instruction]`): run
    /// the standard compaction flow over the active path (keep the recent
    /// tail within `keep_recent_tokens`, summarize the head, splice the
    /// summary in at the cut point, reparent the tail under it) and reload
    /// the session, reporting [`crate::Event::SessionCompacted`].
    /// `instruction` optionally focuses the summary. Refused with
    /// [`crate::Event::SessionError`] while a turn is in flight, without an
    /// active session/provider/model, or when there is no summarizable span.
    CompactSession {
        instruction: Option<String>,
    },
    /// Load the active session's tree for the `/tree` popup; reports
    /// [`crate::Event::SessionTree`].
    OpenTree,
    /// Write the active session to a JSON file (`/export [path]`); `None`
    /// picks `<session_id>-<timestamp>.json` in the working directory.
    /// Reports [`crate::Event::SessionExported`] or
    /// [`crate::Event::SessionError`].
    ExportSession {
        path: Option<std::path::PathBuf>,
    },
    /// Switch the active session's scene (`None` = the built-in default
    /// scene). Refused with [`crate::Event::SceneError`] while a stream is
    /// busy or the name does not resolve.
    SwitchScene {
        name: Option<String>,
    },
    Replay,
    Reload,
    /// Recall the most recently steered prompt into the input area. `stacked`
    /// (Alt+Shift+Up) prepends the content to the existing text (separated by
    /// two line feeds) instead of overwriting it.
    RecallSteered {
        stacked: bool,
    },
    LspStart {
        name: String,
    },
    LspStop {
        name: String,
    },
    LspRestart {
        name: String,
    },
    LspList {
        all: bool,
        filter: Option<String>,
    },
    /// Snapshot the configured MCP servers' states and report
    /// [`crate::Event::McpStatus`] (`/mcp`).
    McpList,
    /// Force a fresh connection to an MCP server, dropping any existing one.
    /// Reports [`crate::Event::McpStatus`] on success; on failure
    /// [`crate::Event::McpError`] plus a status snapshot in which the server
    /// carries its failure detail.
    McpReconnect {
        name: String,
    },
    /// Fetch one registry's online source on demand: the built-in hosted
    /// (Selune) registry or a custom `[registries]` one. A disabled registry
    /// or one without an online source is reported as an error. Reports
    /// [`Event::RegistryLoaded`] or [`Event::RegistryError`], tagged with the
    /// registry id.
    FetchRegistry {
        /// The registry id (`selune` for the built-in one).
        registry: String,
    },
}