Skip to content

feat(commands): /dryrun — preview the next chat completion request without sending it #1004

@peixl

Description

@peixl

Problem

When iterating on a long DeepSeek V4 Pro turn — long system prompt,
many cached repo files, tool definitions, attached @mentions,
multi-step thinking — the developer cannot see what is actually
about to be sent
without sending it.

For V4 Pro users this has a concrete, recurring cost:

Proposed solution

A new read-only slash command, /dryrun (alias /preview-request),
that renders exactly what the next chat completion request would
look like without sending it.

Two output modes:

  • /dryrun — one-screen summary:
    • active provider + base URL (with /v1 vs /beta resolved),
    • active model + reasoning effort,
    • message count broken down by role (system / user / assistant /
      tool) and a note if reasoning_content replay would be
      attached on assistant tool-call messages,
    • system prompt size in chars / approx tokens,
    • tool count + total tool-schema approx tokens,
    • composer-draft size if non-empty,
    • estimated total input tokens via the existing
      crate::compaction::estimate_tokens helper, and a footer line
      explicitly labeling it as an approximation.
  • /dryrun --full — the full JSON body that would be POSTed,
    pretty-printed, with:
    • Authorization / API-key headers redacted to sk-…<last4>,
    • tool-result message bodies truncated past N chars with a
      [truncated K chars] marker,
    • any field added later (e.g. strict-tool toggle) automatically
      included because the body is built from the same struct the
      engine sends.

/dryrun is read-only: it does not mutate api_messages,
session.*, telemetry, cache history, or the composer. It does
not call the network. It does not record a turn.

Why this is a good fit

  • High frequency, V4-Pro-specific value: directly attacks the
    "I don't know what I'm about to spend" problem that the 1M
    context window + thinking pricing creates.
  • Pure read of in-memory state — outside the telemetry / cost /
    session-accounting risky surface that PR Expose DeepSeek V4 reasoning usage ledger #967 was closed for.
  • Minimal new surface: 1 new command file plus three tiny wirings.
  • Composes well with existing /tokens (post-hoc) and /cost
    (post-hoc) by giving the missing pre-hoc view.

Scope (single small PR, target 4 files)

Files touched:

  1. NEW crates/tui/src/commands/dryrun.rs (~200 LOC):

    • Builds a synthetic message list from app.api_messages plus
      a synthetic user message from app.composer.input if it is
      non-empty.
    • Calls crate::compaction::estimate_tokens for the input
      token estimate (no new estimator).
    • Formats the summary or the JSON body via serde_json against
      the same outbound message struct the engine already uses.
    • Redaction helpers + truncation helpers live in this file with
      unit tests.
  2. crates/tui/src/commands/mod.rs — register the command in
    pub mod, in COMMANDS, and in the dispatch match (mirrors
    /tokens).

  3. crates/tui/src/localization.rs — add 2 message IDs
    (CmdDryrunDescription, DryrunFooterApprox) with strings for
    every shipped locale, following the existing pattern.

  4. (Only if the existing engine request-build helper is
    pub(crate) and not reachable from commands/) a tiny
    visibility bump on that single function, with a comment.
    No new public crate API.

No change to telemetry, cost, session accounting, cache history,
provider/model switch logic, /clear, or /load.

Alternatives considered

  • Extending /tokens with a "would-send" mode: rejected — /tokens
    is post-hoc and overloading it muddies its contract.
  • Logging the request to a file behind a debug flag: rejected —
    forces the user to leave the TUI and grep, doesn't help during
    composition.
  • A --dry-run flag on the engine: rejected — engine surface is
    the wrong layer for an interactive ergonomic and would be a
    much larger PR.

Additional context

AI-use disclosure / human-owner statement

This proposal was drafted with AI assistance (GitHub Copilot /
Claude). The human owner is @peixl, who reviews each change
line-by-line, owns the resulting PR, and is responsible for the
test plan and merge.

Test plan for the eventual PR

Unit tests in crates/tui/src/commands/dryrun.rs:

  • dryrun_summary_includes_model_provider_and_token_estimate
  • dryrun_summary_appends_composer_draft_as_synthetic_user_turn
  • dryrun_summary_does_not_mutate_app_state (snapshot
    api_messages, session.*, composer.input before and after).
  • dryrun_full_redacts_api_key_to_last4
  • dryrun_full_truncates_long_tool_result_bodies
  • dryrun_handles_empty_session_without_panic

Workspace gates:

  • cargo test -p deepseek-tui dryrun_ --locked
  • cargo test -p deepseek-tui --locked
  • cargo fmt --all -- --check
  • cargo clippy -p deepseek-tui --all-targets --all-features --locked -- -D warnings
  • git diff --check origin/main...HEAD

Manual:

  • Start a session with a non-trivial system prompt and one tool
    call, type a draft, run /dryrun and /dryrun --full, confirm
    numbers and JSON match what the next send produces.
  • Confirm /dryrun on a fresh session prints non-zero system
    prompt size and 0 for everything else, no panics, no network.

Awaiting maintainer accept

Per the contributor guide and PR #967's closing note, no
implementation will be pushed until this issue is accepted by a
maintainer. Filing as a proposal first.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or request

    Projects

    Status
    In progress

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions