Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 56 additions & 40 deletions crates/jp_cli/src/cmd/conversation/print.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,31 @@ use crate::{
pub(crate) struct Print {
#[command(flatten)]
target: PositionalIds<true, true>,

/// Print only the last N turns. Without a value, prints the last turn.
#[arg(long, num_args = 0..=1, default_missing_value = "1")]
last: Option<usize>,
}

impl Print {
pub(crate) fn conversation_load_request(&self) -> ConversationLoadRequest {
ConversationLoadRequest::explicit_or_session(&self.target.ids)
}

#[expect(clippy::unused_self)]
pub(crate) fn run(self, ctx: &mut Ctx, handles: &[ConversationHandle]) -> Output {
for handle in handles {
Self::print_conversation(ctx, handle)?;
Self::print_conversation(ctx, handle, self.last)?;
}
ctx.printer.println("");
ctx.printer.flush();
Ok(())
}

fn print_conversation(ctx: &mut Ctx, handle: &ConversationHandle) -> Output {
fn print_conversation(
ctx: &mut Ctx,
handle: &ConversationHandle,
last: Option<usize>,
) -> Output {
let events = ctx.workspace.events(handle)?.clone();
let cfg = ctx.config();
let pretty = ctx.printer.pretty_printing_enabled();
Expand All @@ -54,53 +61,62 @@ impl Print {
ctx.term.is_tty,
);

let turns = events.iter_turns();
let skip = last.map_or(0, |n| turns.len().saturating_sub(n));

let mut is_first_turn = true;

for event_with_cfg in events.iter() {
match &event_with_cfg.event.kind {
EventKind::TurnStart(_) => {
if !is_first_turn {
printer.println("\n---\n");
for turn in turns.skip(skip) {
for event_with_cfg in turn {
match &event_with_cfg.event.kind {
EventKind::TurnStart(_) => {
if !is_first_turn {
printer.println("\n---\n");
}
is_first_turn = false;
}
is_first_turn = false;
}

EventKind::ChatRequest(req) => {
render_user_message(&printer, &cfg, pretty, &req.content)?;
}
EventKind::ChatRequest(req) => {
render_user_message(&printer, &cfg, pretty, &req.content)?;
}

EventKind::ChatResponse(resp) => {
render_chat_response(&printer, &cfg, pretty, resp)?;
}
EventKind::ChatResponse(resp) => {
render_chat_response(&printer, &cfg, pretty, resp)?;
}

EventKind::ToolCallRequest(req) => {
let tool_cfg = cfg.conversation.tools.get(&req.name);
if !tool_cfg.as_ref().is_some_and(|c| c.style().hidden) {
let params_style = tool_cfg
.as_ref()
.map(|c| c.style().parameters.clone())
.unwrap_or_default();
tool_renderer.render_tool_call(&req.name, &req.arguments, &params_style);
EventKind::ToolCallRequest(req) => {
let tool_cfg = cfg.conversation.tools.get(&req.name);
if !tool_cfg.as_ref().is_some_and(|c| c.style().hidden) {
let params_style = tool_cfg
.as_ref()
.map(|c| c.style().parameters.clone())
.unwrap_or_default();
tool_renderer.render_tool_call(
&req.name,
&req.arguments,
&params_style,
);
}
}
}

EventKind::ToolCallResponse(resp) => {
let name = find_tool_name_for_response(&events, resp);
let tool_cfg = name.as_deref().and_then(|n| cfg.conversation.tools.get(n));
if !tool_cfg.as_ref().is_some_and(|c| c.style().hidden) {
let inline = tool_cfg
.as_ref()
.map(|c| c.style().inline_results.clone())
.unwrap_or_default();
let link = tool_cfg
.as_ref()
.map(|c| c.style().results_file_link.clone())
.unwrap_or_default();
tool_renderer.render_result(resp, &inline, &link);
EventKind::ToolCallResponse(resp) => {
let name = find_tool_name_for_response(&events, resp);
let tool_cfg = name.as_deref().and_then(|n| cfg.conversation.tools.get(n));
if !tool_cfg.as_ref().is_some_and(|c| c.style().hidden) {
let inline = tool_cfg
.as_ref()
.map(|c| c.style().inline_results.clone())
.unwrap_or_default();
let link = tool_cfg
.as_ref()
.map(|c| c.style().results_file_link.clone())
.unwrap_or_default();
tool_renderer.render_result(resp, &inline, &link);
}
}
}

EventKind::InquiryRequest(_) | EventKind::InquiryResponse(_) => {}
EventKind::InquiryRequest(_) | EventKind::InquiryResponse(_) => {}
}
}
}

Expand Down
135 changes: 135 additions & 0 deletions crates/jp_cli/src/cmd/conversation/print_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ fn prints_user_message() {
target: PositionalIds {
ids: vec![ConversationTarget::Id(id)],
},
last: None,
};
let h = ctx.workspace.acquire_conversation(&id).unwrap();
let result = print.run(&mut ctx, &[h]);
Expand All @@ -104,6 +105,7 @@ fn prints_assistant_message() {
target: PositionalIds {
ids: vec![ConversationTarget::Id(id)],
},
last: None,
};
let h = ctx.workspace.acquire_conversation(&id).unwrap();
let result = print.run(&mut ctx, &[h]);
Expand Down Expand Up @@ -131,6 +133,7 @@ fn prints_reasoning_full() {
target: PositionalIds {
ids: vec![ConversationTarget::Id(id)],
},
last: None,
};
let h = ctx.workspace.acquire_conversation(&id).unwrap();
let result = print.run(&mut ctx, &[h]);
Expand Down Expand Up @@ -159,6 +162,7 @@ fn hides_reasoning_when_hidden() {
target: PositionalIds {
ids: vec![ConversationTarget::Id(id)],
},
last: None,
};
let h = ctx.workspace.acquire_conversation(&id).unwrap();
let result = print.run(&mut ctx, &[h]);
Expand Down Expand Up @@ -188,6 +192,7 @@ fn truncates_reasoning() {
target: PositionalIds {
ids: vec![ConversationTarget::Id(id)],
},
last: None,
};
let h = ctx.workspace.acquire_conversation(&id).unwrap();
let result = print.run(&mut ctx, &[h]);
Expand Down Expand Up @@ -227,6 +232,7 @@ fn prints_tool_call_and_result() {
target: PositionalIds {
ids: vec![ConversationTarget::Id(id)],
},
last: None,
};
let h = ctx.workspace.acquire_conversation(&id).unwrap();
let result = print.run(&mut ctx, &[h]);
Expand All @@ -253,6 +259,7 @@ fn prints_structured_data() {
target: PositionalIds {
ids: vec![ConversationTarget::Id(id)],
},
last: None,
};
let h = ctx.workspace.acquire_conversation(&id).unwrap();
let result = print.run(&mut ctx, &[h]);
Expand All @@ -279,6 +286,7 @@ fn turn_separators_between_turns() {
target: PositionalIds {
ids: vec![ConversationTarget::Id(id)],
},
last: None,
};
let h = ctx.workspace.acquire_conversation(&id).unwrap();
let result = print.run(&mut ctx, &[h]);
Expand All @@ -301,6 +309,7 @@ fn prints_conversation_by_id() {
target: PositionalIds {
ids: vec![ConversationTarget::Id(id)],
},
last: None,
};
let h = ctx.workspace.acquire_conversation(&id).unwrap();
let result = print.run(&mut ctx, &[h]);
Expand All @@ -322,6 +331,7 @@ fn empty_conversation_produces_no_content() {
target: PositionalIds {
ids: vec![ConversationTarget::Id(id)],
},
last: None,
};
let h = ctx.workspace.acquire_conversation(&id).unwrap();
let result = print.run(&mut ctx, &[h]);
Expand Down Expand Up @@ -372,6 +382,7 @@ fn full_conversation_round_trip() {
target: PositionalIds {
ids: vec![ConversationTarget::Id(id)],
},
last: None,
};
let h = ctx.workspace.acquire_conversation(&id).unwrap();
let result = print.run(&mut ctx, &[h]);
Expand All @@ -390,3 +401,127 @@ fn full_conversation_round_trip() {
assert!(plain.contains("Calling tool write_file"), "got: {plain}");
assert!(plain.contains("simple Rust program"), "got: {plain}");
}

#[test]
fn last_prints_only_last_turn() {
let (mut ctx, id, out, _rt) = setup_ctx(vec![
ConversationEvent::new(TurnStart, ts(0, 0, 0)),
ConversationEvent::new(ChatRequest::from("First question"), ts(0, 0, 1)),
ConversationEvent::new(ChatResponse::message("First answer.\n\n"), ts(0, 0, 2)),
ConversationEvent::new(TurnStart, ts(0, 1, 0)),
ConversationEvent::new(ChatRequest::from("Second question"), ts(0, 1, 1)),
ConversationEvent::new(ChatResponse::message("Second answer.\n\n"), ts(0, 1, 2)),
]);

let print = Print {
target: PositionalIds {
ids: vec![ConversationTarget::Id(id)],
},
last: Some(1),
};
let h = ctx.workspace.acquire_conversation(&id).unwrap();
let result = print.run(&mut ctx, &[h]);
ctx.printer.flush();

result.unwrap();
let output = out.lock().clone();
assert!(
!output.contains("First question"),
"first turn should be excluded, got: {output}"
);
assert!(
output.contains("Second question"),
"last turn should be present, got: {output}"
);
assert!(
output.contains("Second answer."),
"last turn response should be present, got: {output}"
);
}

#[test]
fn last_two_with_three_turns() {
let (mut ctx, id, out, _rt) = setup_ctx(vec![
ConversationEvent::new(TurnStart, ts(0, 0, 0)),
ConversationEvent::new(ChatRequest::from("Turn one"), ts(0, 0, 1)),
ConversationEvent::new(ChatResponse::message("Answer one.\n\n"), ts(0, 0, 2)),
ConversationEvent::new(TurnStart, ts(0, 1, 0)),
ConversationEvent::new(ChatRequest::from("Turn two"), ts(0, 1, 1)),
ConversationEvent::new(ChatResponse::message("Answer two.\n\n"), ts(0, 1, 2)),
ConversationEvent::new(TurnStart, ts(0, 2, 0)),
ConversationEvent::new(ChatRequest::from("Turn three"), ts(0, 2, 1)),
ConversationEvent::new(ChatResponse::message("Answer three.\n\n"), ts(0, 2, 2)),
]);

let print = Print {
target: PositionalIds {
ids: vec![ConversationTarget::Id(id)],
},
last: Some(2),
};
let h = ctx.workspace.acquire_conversation(&id).unwrap();
let result = print.run(&mut ctx, &[h]);
ctx.printer.flush();

result.unwrap();
let output = out.lock().clone();
assert!(
!output.contains("Turn one"),
"first turn should be excluded, got: {output}"
);
assert!(output.contains("Turn two"), "got: {output}");
assert!(output.contains("Turn three"), "got: {output}");
}

#[test]
fn last_exceeding_turn_count_prints_all() {
let (mut ctx, id, out, _rt) = setup_ctx(vec![
ConversationEvent::new(TurnStart, ts(0, 0, 0)),
ConversationEvent::new(ChatRequest::from("Only question"), ts(0, 0, 1)),
ConversationEvent::new(ChatResponse::message("Only answer.\n\n"), ts(0, 0, 2)),
]);

let print = Print {
target: PositionalIds {
ids: vec![ConversationTarget::Id(id)],
},
last: Some(5),
};
let h = ctx.workspace.acquire_conversation(&id).unwrap();
let result = print.run(&mut ctx, &[h]);
ctx.printer.flush();

result.unwrap();
let output = out.lock().clone();
assert!(
output.contains("Only question"),
"should print everything when --last exceeds turn count, got: {output}"
);
}

#[test]
fn last_zero_prints_nothing() {
let (mut ctx, id, out, _rt) = setup_ctx(vec![
ConversationEvent::new(TurnStart, ts(0, 0, 0)),
ConversationEvent::new(ChatRequest::from("Hello"), ts(0, 0, 1)),
ConversationEvent::new(ChatResponse::message("World.\n\n"), ts(0, 0, 2)),
]);

let print = Print {
target: PositionalIds {
ids: vec![ConversationTarget::Id(id)],
},
last: Some(0),
};
let h = ctx.workspace.acquire_conversation(&id).unwrap();
let result = print.run(&mut ctx, &[h]);
ctx.printer.flush();

result.unwrap();
let output = out.lock().clone();
let trimmed = output.trim();
assert!(
trimmed.is_empty(),
"--last 0 should produce no content, got: {trimmed:?}"
);
}
Loading