Skip to content

[API-206] Add conversations top-level command to CLI#57

Merged
lorenjphillips merged 1 commit into
mainfrom
loren/api-206-conversations-command
Apr 4, 2026
Merged

[API-206] Add conversations top-level command to CLI#57
lorenjphillips merged 1 commit into
mainfrom
loren/api-206-conversations-command

Conversation

@lorenjphillips

@lorenjphillips lorenjphillips commented Apr 3, 2026

Copy link
Copy Markdown
Contributor
  • Add coval conversations command with full subcommand parity to simulations (list, get, delete, audio, metrics, metric-detail)
  • New ConversationsClient, Conversation model, ConversationAudioUrlResponse (proper response type, not reusing simulation's)
  • New ConversationStatus enum, ConversationProgress struct, Tabular impl
  • 404 hints on both simulations and conversations get/delete pointing users to the other resource type
  • Replaces the --monitoring flag + auto-detect approach from [API-206] Add --monitoring flag with auto-detect to simulations commands #56 with explicit, predictable commands

Closes Dakota's #56 (superseded by this approach).

ID: b10c01b3-82c2-4f6e-913a-df1c2a1f38b7

Summary by CodeRabbit

  • New Features

    • Added a top-level "conversations" CLI subcommand for listing, viewing, deleting conversations, downloading audio, and viewing metrics (with pagination, filtering, and metric detail).
    • New client API and data models to support conversation listing, retrieval, audio URLs, and metric endpoints.
    • Audio download shows progress and supports saving to a file.
  • Bug Fixes

    • Improved "not found" handling with clearer user hints when fetching or deleting items.

@coderabbitai

coderabbitai Bot commented Apr 3, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds conversation management: new CLI subcommand and handlers, client routing for /v1/conversations endpoints, conversation data models and responses, audio download support, and improved NotFound hinting in simulation commands.

Changes

Cohort / File(s) Summary
CLI Layer
src/cli.rs
Added Conversations variant to Commands and dispatch to commands::conversations::execute(...).
Client API Layer
src/client/mod.rs
Added ConversationsClient<'_> and async methods: list, get, delete, audio, list_metrics, get_metric mapping to /v1/conversations endpoints.
Data Models
src/client/models/conversation.rs, src/client/models/mod.rs
New conversation domain models and response types (Conversation, ConversationStatus, ConversationProgress, list/get/audio/metrics responses), serde annotations, Display and Tabular impls, and re-export via models::conversation.
Command Handlers
src/commands/conversations.rs, src/commands/mod.rs
New ConversationCommands subcommands (list, get, delete, metrics, metric-detail, audio) with execute; audio download helper using reqwest, progress bar, and file write.
Error Handling Enhancement
src/commands/simulations.rs
Get and Delete now match ApiError::NotFound, print a stderr hint via helper, and remap NotFound resource strings; explicit error handling rather than await?.

Sequence Diagram

sequenceDiagram
    participant User as User/CLI
    participant CLI as CLI Dispatcher
    participant Handler as Conversations Handler
    participant Client as Conversations Client
    participant API as HTTP API
    participant FS as Filesystem

    User->>CLI: invoke "conversations" subcommand
    CLI->>Handler: dispatch with parsed args
    Handler->>Client: call (list|get|delete|audio|metrics)
    Client->>API: HTTP request to /v1/conversations...
    API-->>Client: JSON response
    Client-->>Handler: parsed model/response
    alt audio download
        Handler->>API: fetch audio URL (or use returned URL)
        API-->>Handler: audio bytes/stream
        Handler->>FS: write file with progress
    end
    Handler-->>User: display output or save file
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I'm a rabbit, nibbling bytes with flair, 🐇
Conversations bloom from CLI and air,
Metrics, audio, models in a row,
I hop through code where new features grow,
A tiny cheer for changes fair.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[API-206] Add conversations top-level command to CLI' accurately and concisely summarizes the main change: adding a new conversations command to the CLI. It is specific, clear, and directly related to the primary objective of the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch loren/api-206-conversations-command

Comment @coderabbitai help to get the list of available commands and usage tips.

@lorenjphillips lorenjphillips force-pushed the loren/api-206-conversations-command branch from 3da846c to 58f427b Compare April 3, 2026 20:32

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
src/client/models/conversation.rs (2)

39-59: Consider using #[serde(alias)] to simplify the enum.

The duplicate variants (InQueue/InQueueSpace, InProgress/InProgressSpace) handle API inconsistency between underscore and space separators. Using alias would consolidate them.

♻️ Alternative using serde alias
 #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
 pub enum ConversationStatus {
     #[serde(rename = "PENDING")]
     Pending,
-    #[serde(rename = "IN_QUEUE")]
+    #[serde(rename = "IN_QUEUE", alias = "IN QUEUE")]
     InQueue,
-    #[serde(rename = "IN QUEUE")]
-    InQueueSpace,
-    #[serde(rename = "IN_PROGRESS")]
+    #[serde(rename = "IN_PROGRESS", alias = "IN PROGRESS")]
     InProgress,
-    #[serde(rename = "IN PROGRESS")]
-    InProgressSpace,
     #[serde(rename = "COMPLETED")]
     Completed,
     // ... rest unchanged
 }

This would also simplify the Display impl by removing the | Self::InQueueSpace and | Self::InProgressSpace patterns.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/models/conversation.rs` around lines 39 - 59, The
ConversationStatus enum defines duplicate variants (InQueue/InQueueSpace and
InProgress/InProgressSpace) to handle API variations; replace these duplicates
by keeping a single variant for each logical state (InQueue, InProgress) and add
serde aliases (e.g., #[serde(rename = "...", alias = "IN QUEUE")]) on those
variants so both "IN_QUEUE" and "IN QUEUE" (and similarly for IN_PROGRESS/IN
PROGRESS) deserialize to the same variant, then update the Display impl to
remove patterns matching the removed duplicate variants and only match the
consolidated variants (ConversationStatus::InQueue and
ConversationStatus::InProgress).

103-110: ConversationAudioUrlResponse is missing Serialize derive.

Looking at src/commands/conversations.rs, this type isn't passed to print_one (only the audio_url string is printed), so this isn't a compilation blocker. However, for consistency with other response types and to enable JSON output in the future, consider adding Serialize.

♻️ Suggested change
-#[derive(Debug, Deserialize)]
+#[derive(Debug, Serialize, Deserialize)]
 pub struct ConversationAudioUrlResponse {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/models/conversation.rs` around lines 103 - 110,
ConversationAudioUrlResponse only derives Deserialize; add Serialize so it can
be serialized for JSON output. Update the struct's derives to include Serialize
(e.g., #[derive(Debug, Serialize, Deserialize)]) and ensure serde::Serialize is
in scope (adjust the use/import statements if necessary) so
ConversationAudioUrlResponse matches other response types and can be
printed/returned as JSON in the future.
src/commands/conversations.rs (1)

154-176: Consider extracting download_audio to a shared module.

This function is duplicated verbatim from src/commands/simulations.rs. Extracting it to a shared utility module (e.g., src/commands/common.rs or src/utils.rs) would reduce duplication.

♻️ Example extraction

Create a shared module and import from both files:

// src/commands/common.rs (or src/utils.rs)
use std::path::Path;
use anyhow::Result;
use indicatif::{ProgressBar, ProgressStyle};

pub async fn download_audio(url: &str, path: &Path) -> Result<()> {
    let client = reqwest::Client::new();
    let resp = client.get(url).send().await?;

    if !resp.status().is_success() {
        anyhow::bail!("Failed to download audio: HTTP {}", resp.status());
    }

    let total = resp.content_length().unwrap_or(0);
    let pb = ProgressBar::new(total);
    pb.set_style(
        ProgressStyle::default_bar()
            .template("{spinner:.green} [{bar:40.cyan/blue}] {bytes}/{total_bytes}")?
            .progress_chars("=>-"),
    );

    let bytes = resp.bytes().await?;
    pb.set_position(bytes.len() as u64);
    pb.finish_and_clear();

    std::fs::write(path, &bytes)?;
    Ok(())
}

Then in both simulations.rs and conversations.rs:

use super::common::download_audio;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/conversations.rs` around lines 154 - 176, The download_audio
function is duplicated; extract it into a shared module (e.g., create
src/commands/common.rs or src/utils.rs) as a public async fn download_audio(url:
&str, path: &Path) -> Result<()> and move the reqwest/indicatif/anyhow/std::path
uses into that module; then replace the local download_audio definitions in both
conversations.rs and simulations.rs with a use import (e.g., use
crate::commands::common::download_audio or use crate::utils::download_audio) so
both files call the single shared function, ensuring the function remains public
and the module is declared in mod.rs or lib.rs so the imports resolve.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/client/mod.rs`:
- Around line 305-311: The function formatting in the Conversation client method
audio (pub async fn audio(&self, id: &str) ->
Result<models::ConversationAudioUrlResponse, ApiError>) and the format! call
inside (self.0.url(&format!("/v1/conversations/{id}/audio"))) does not match the
project's rustfmt rules; reformat the function signature and the format!
invocation to match rustfmt (or simply run cargo fmt) so the signature,
parameter placement and the format! macro call adhere to the repository style
and CI will pass.
- Around line 326-328: The format! invocation should be split out so rustfmt can
reflow it: create a temporary variable (e.g. let path =
format!("/v1/conversations/{id}/metrics/{metric_output_id}");) and then call
self.0.url(&path) instead of embedding format! directly in the url(...) call;
update the code around the self.0.url and format! usage accordingly.

In `@src/commands/conversations.rs`:
- Around line 97-112: The block handling ConversationCommands::Delete is
misformatted; run rustfmt-style adjustments: break the chained call on
client.conversations().delete so the await is on its own line and align the
match arms, ensure commas and closing braces are properly spaced, and keep
print_success("Conversation deleted.") on its own line; also reformat the
print_not_found_hint function (the hint block around the NotFound arm) to follow
rustfmt conventions (consistent indentation, single spaces, and proper line
breaks). Locate ConversationCommands::Delete,
client.conversations().delete(&args.conversation_id).await, print_success and
print_not_found_hint to apply these formatting fixes.

In `@src/commands/simulations.rs`:
- Around line 145-149: The eprintln! in print_not_found_hint is split across
multiple lines and fails cargo fmt; collapse the eprintln! invocation into a
single line so the entire string interpolation is on one line (update function
print_not_found_hint to call eprintln!("hint: not found as a simulation. Try
`coval {try_command} get {id}` instead.");).

---

Nitpick comments:
In `@src/client/models/conversation.rs`:
- Around line 39-59: The ConversationStatus enum defines duplicate variants
(InQueue/InQueueSpace and InProgress/InProgressSpace) to handle API variations;
replace these duplicates by keeping a single variant for each logical state
(InQueue, InProgress) and add serde aliases (e.g., #[serde(rename = "...", alias
= "IN QUEUE")]) on those variants so both "IN_QUEUE" and "IN QUEUE" (and
similarly for IN_PROGRESS/IN PROGRESS) deserialize to the same variant, then
update the Display impl to remove patterns matching the removed duplicate
variants and only match the consolidated variants (ConversationStatus::InQueue
and ConversationStatus::InProgress).
- Around line 103-110: ConversationAudioUrlResponse only derives Deserialize;
add Serialize so it can be serialized for JSON output. Update the struct's
derives to include Serialize (e.g., #[derive(Debug, Serialize, Deserialize)])
and ensure serde::Serialize is in scope (adjust the use/import statements if
necessary) so ConversationAudioUrlResponse matches other response types and can
be printed/returned as JSON in the future.

In `@src/commands/conversations.rs`:
- Around line 154-176: The download_audio function is duplicated; extract it
into a shared module (e.g., create src/commands/common.rs or src/utils.rs) as a
public async fn download_audio(url: &str, path: &Path) -> Result<()> and move
the reqwest/indicatif/anyhow/std::path uses into that module; then replace the
local download_audio definitions in both conversations.rs and simulations.rs
with a use import (e.g., use crate::commands::common::download_audio or use
crate::utils::download_audio) so both files call the single shared function,
ensuring the function remains public and the module is declared in mod.rs or
lib.rs so the imports resolve.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 64b63993-1777-4a9c-89bd-c2dd4c7aefce

📥 Commits

Reviewing files that changed from the base of the PR and between dbe02cf and 3da846c.

📒 Files selected for processing (7)
  • src/cli.rs
  • src/client/mod.rs
  • src/client/models/conversation.rs
  • src/client/models/mod.rs
  • src/commands/conversations.rs
  • src/commands/mod.rs
  • src/commands/simulations.rs

Comment thread src/client/mod.rs Outdated
Comment thread src/client/mod.rs Outdated
Comment thread src/commands/conversations.rs
Comment thread src/commands/simulations.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/commands/conversations.rs (1)

142-146: ⚠️ Potential issue | 🟡 Minor

Fix formatting to pass CI.

The pipeline indicates the eprintln! macro needs to be on a single line.

🔧 Proposed fix
 fn print_not_found_hint(id: &str, try_command: &str) {
-    eprintln!(
-        "hint: not found as a conversation. Try `coval {try_command} get {id}` instead."
-    );
+    eprintln!("hint: not found as a conversation. Try `coval {try_command} get {id}` instead.");
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/conversations.rs` around lines 142 - 146, The eprintln!
invocation in print_not_found_hint currently spans multiple lines and fails CI
formatting; collapse the macro call into a single line so the entire
eprintln!(...) is on one line (update the eprintln! in function
print_not_found_hint to a single-line invocation with the same formatted string
and variables).
🧹 Nitpick comments (1)
src/commands/conversations.rs (1)

148-169: Consider extracting download_audio to a shared module.

This function is duplicated from simulations.rs (see lines 151-173 in that file). While acceptable for this PR, consider extracting it to a shared utility module to reduce duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/conversations.rs` around lines 148 - 169, The download_audio
function is duplicated; extract it into a shared utility module by moving async
fn download_audio(url: &str, path: &Path) -> Result<()> into a new/common module
(make it pub async fn download_audio(...)->anyhow::Result<()>), preserve its
implementation and required imports (reqwest, ProgressBar, ProgressStyle,
std::fs, Path, anyhow::Result), update both conversations.rs and simulations.rs
to import and call the shared download_audio (replace the local definitions),
and run cargo check to fix any visibility or use path issues.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/commands/conversations.rs`:
- Around line 77-90: The match arm in ConversationCommands::Get (around
client.conversations().get and the Err(ApiError::NotFound { .. }) branch that
calls print_not_found_hint) is misformatted; run cargo fmt to reformat the
chained call and match arms so the Err(ApiError::NotFound { .. }) => { block and
its returned ApiError::NotFound construction follow the project's rustfmt rules,
then re-commit the formatted file.
- Around line 122-137: Reformat the chained async call in the
ConversationCommands::Audio arm so it satisfies rustfmt: break the chain on
separate lines instead of one long expression for
client.conversations().audio(&args.conversation_id).await?; adjust the let audio
= ... assignment accordingly in the ConversationCommands::Audio match arm (the
surrounding symbols are ConversationCommands::Audio, client.conversations(),
audio(), and the later uses download_audio and print_success remain unchanged).

---

Duplicate comments:
In `@src/commands/conversations.rs`:
- Around line 142-146: The eprintln! invocation in print_not_found_hint
currently spans multiple lines and fails CI formatting; collapse the macro call
into a single line so the entire eprintln!(...) is on one line (update the
eprintln! in function print_not_found_hint to a single-line invocation with the
same formatted string and variables).

---

Nitpick comments:
In `@src/commands/conversations.rs`:
- Around line 148-169: The download_audio function is duplicated; extract it
into a shared utility module by moving async fn download_audio(url: &str, path:
&Path) -> Result<()> into a new/common module (make it pub async fn
download_audio(...)->anyhow::Result<()>), preserve its implementation and
required imports (reqwest, ProgressBar, ProgressStyle, std::fs, Path,
anyhow::Result), update both conversations.rs and simulations.rs to import and
call the shared download_audio (replace the local definitions), and run cargo
check to fix any visibility or use path issues.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9987465a-6cfe-4f92-b4d7-2fc989e27606

📥 Commits

Reviewing files that changed from the base of the PR and between 3da846c and 58f427b.

📒 Files selected for processing (7)
  • src/cli.rs
  • src/client/mod.rs
  • src/client/models/conversation.rs
  • src/client/models/mod.rs
  • src/commands/conversations.rs
  • src/commands/mod.rs
  • src/commands/simulations.rs
✅ Files skipped from review due to trivial changes (2)
  • src/commands/mod.rs
  • src/client/models/mod.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/client/models/conversation.rs

Comment thread src/commands/conversations.rs
Comment thread src/commands/conversations.rs
@lorenjphillips lorenjphillips force-pushed the loren/api-206-conversations-command branch from 58f427b to 2b55ca7 Compare April 3, 2026 20:55

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/client/models/conversation.rs (1)

127-129: Clarify timezone in OCCURRED AT table output.

The occurred_at field is DateTime<Utc>, but the format string "%Y-%m-%d %H:%M" displays the timestamp without a timezone indicator, which can be misread as local time in CLI output. Appending UTC to the format string clarifies the timezone.

Proposed diff
-            self.occurred_at
-                .map(|t| t.format("%Y-%m-%d %H:%M").to_string())
-                .unwrap_or_else(|| "-".into()),
+            self.occurred_at
+                .map(|t| t.format("%Y-%m-%d %H:%M UTC").to_string())
+                .unwrap_or_else(|| "-".into()),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/models/conversation.rs` around lines 127 - 129, The CLI output for
the occurred_at DateTime<Utc] lacks a timezone label; update the formatting call
on the occurred_at field (the closure that calls t.format("%Y-%m-%d
%H:%M").to_string()) to include a timezone indicator—e.g., append " UTC" to the
format string or use a format specifier that emits the offset—so the table shows
the timestamp with its UTC timezone; leave the unwrap_or_else fallback ("-")
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/client/models/conversation.rs`:
- Around line 127-129: The CLI output for the occurred_at DateTime<Utc] lacks a
timezone label; update the formatting call on the occurred_at field (the closure
that calls t.format("%Y-%m-%d %H:%M").to_string()) to include a timezone
indicator—e.g., append " UTC" to the format string or use a format specifier
that emits the offset—so the table shows the timestamp with its UTC timezone;
leave the unwrap_or_else fallback ("-") unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e770a854-1571-45ca-bdb7-c736fa1832ab

📥 Commits

Reviewing files that changed from the base of the PR and between 58f427b and 2b55ca7.

📒 Files selected for processing (7)
  • src/cli.rs
  • src/client/mod.rs
  • src/client/models/conversation.rs
  • src/client/models/mod.rs
  • src/commands/conversations.rs
  • src/commands/mod.rs
  • src/commands/simulations.rs
✅ Files skipped from review due to trivial changes (2)
  • src/client/models/mod.rs
  • src/commands/mod.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/client/mod.rs
  • src/commands/conversations.rs

@kdmelon kdmelon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lil nit, thoughts on adding 404 hints not metrics/metric-detail? so if a user runs coval conversations metrics <sim_id>, they get the hint to try coval simulations metrics instead.

@lorenjphillips lorenjphillips merged commit 729e704 into main Apr 4, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants