From f9c4ec9d8ea836283f054e9e4a331e47e8145ab5 Mon Sep 17 00:00:00 2001 From: Jean Mertz Date: Tue, 6 Oct 2026 17:07:47 +0200 Subject: [PATCH] feat(cli): Show the conversation's model in `jp c show` `jp conversation show` now reports the model a conversation is configured to use. The text view adds a `Model` row beneath the title (and assistant name, when set), and the JSON payload gains a `model` key next to `assistant`: ``` ID jp-c10000 Title Rework the config pipeline Model anthropic/claude-sonnet-4-5 ``` The value is read from the conversation's own config (its base snapshot plus every recorded config delta), with aliases resolved to `provider/name`. `--cfg` overrides on the `show` invocation itself do not change it, so each conversation listed reports its own model. As with the other optional fields, the `model` key is always present in the JSON output and is `null` when no model is set. Signed-off-by: Jean Mertz --- crates/jp_cli/src/cmd/conversation/show.rs | 10 ++--- crates/jp_cli/src/format/conversation.rs | 19 +++++++++ .../jp_cli/src/format/conversation_tests.rs | 39 +++++++++++++++++++ 3 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 crates/jp_cli/src/format/conversation_tests.rs diff --git a/crates/jp_cli/src/cmd/conversation/show.rs b/crates/jp_cli/src/cmd/conversation/show.rs index 5b15a861e..ca9d2399d 100644 --- a/crates/jp_cli/src/cmd/conversation/show.rs +++ b/crates/jp_cli/src/cmd/conversation/show.rs @@ -33,13 +33,10 @@ impl Show { let conversation = ctx.workspace.metadata(&handle)?; let events = ctx.workspace.events(&handle)?; + let config = events.config().map_err(ConversationError::from)?; + let mut attachments = vec![]; - for attachment in events - .config() - .map_err(ConversationError::from)? - .conversation - .attachments - { + for attachment in &config.conversation.attachments { attachments.push(attachment_detail_item(&attachment.to_url()?)); } @@ -52,6 +49,7 @@ impl Show { .with_event_count(events.len()) .with_turn_count(events.iter_turns().len()) .with_title(conversation.title.as_ref()) + .with_model(config.assistant.model.id.resolved().to_string()) .with_last_activated_at(Some(conversation.last_activated_at)) .with_pinned_flag(conversation.is_pinned()) .with_local_flag(local) diff --git a/crates/jp_cli/src/format/conversation.rs b/crates/jp_cli/src/format/conversation.rs index 95a3d67bd..3d71d051a 100644 --- a/crates/jp_cli/src/format/conversation.rs +++ b/crates/jp_cli/src/format/conversation.rs @@ -23,6 +23,9 @@ pub struct DetailsFmt { /// The conversation title. pub title: Option, + /// The model the conversation is configured to use, as `provider/name`. + pub model: Option, + /// The number of events in the conversation. pub message_count: usize, @@ -72,6 +75,7 @@ impl DetailsFmt { heading: None, assistant_name: None, title: None, + model: None, message_count: 0, turn_count: 0, pinned: None, @@ -141,6 +145,12 @@ impl DetailsFmt { self } + #[must_use] + pub fn with_model(mut self, model: impl Into) -> Self { + self.model = Some(model.into()); + self + } + /// Set the line rendered above the rows. #[must_use] pub fn with_heading(mut self, heading: impl Into) -> Self { @@ -206,6 +216,7 @@ impl DetailsFmt { "id": self.id.to_string(), "title": self.title, "assistant": self.assistant_name, + "model": self.model, "active": self.active, "pinned": self.pinned, "local": self.local, @@ -249,6 +260,10 @@ impl DetailsFmt { rows.push(self.scalar("Assistant", name)); } + if let Some(model) = self.model.clone() { + rows.push(self.scalar("Model", model)); + } + if self.message_count > 0 { rows.push(self.scalar("Events", self.message_count.to_string())); } @@ -351,3 +366,7 @@ impl fmt::Display for DetailsFmt { write!(f, "{}", details(self.heading(), self.rows())) } } + +#[cfg(test)] +#[path = "conversation_tests.rs"] +mod tests; diff --git a/crates/jp_cli/src/format/conversation_tests.rs b/crates/jp_cli/src/format/conversation_tests.rs new file mode 100644 index 000000000..22649d1f8 --- /dev/null +++ b/crates/jp_cli/src/format/conversation_tests.rs @@ -0,0 +1,39 @@ +use std::time::Duration; + +use chrono::{DateTime, Utc}; +use serde_json::json; + +use super::*; + +fn make_id(secs: u64) -> ConversationId { + ConversationId::try_from(DateTime::::UNIX_EPOCH + Duration::from_secs(secs)).unwrap() +} + +#[test] +fn details_show_the_model_beneath_the_title() { + let details = DetailsFmt::new(make_id(1000)) + .with_title(Some("Rework the config pipeline")) + .with_model("anthropic/claude-sonnet-4-5") + .with_pretty_printing(false); + + assert_eq!( + details.to_string(), + " ID jp-c10000\n Title Rework the config pipeline\n Model \ + anthropic/claude-sonnet-4-5" + ); + assert_eq!( + details.json()["model"], + json!("anthropic/claude-sonnet-4-5") + ); +} + +/// The payload keeps every key present, so a consumer reads `model: null` +/// rather than a missing key when no model was set. +#[test] +fn json_reports_a_missing_model_as_null() { + let details = DetailsFmt::new(make_id(1000)); + + let json = details.json(); + + assert_eq!(json.get("model"), Some(&serde_json::Value::Null)); +}