From 5f1eb2969c7ca3714da097a74951b6b980abd0b0 Mon Sep 17 00:00:00 2001 From: Jean Mertz Date: Sun, 23 Aug 2026 15:42:54 +0200 Subject: [PATCH 1/3] enhance(cli): Search all text in `use --grep` `jp conversation use --grep` finds conversations by matching tool calls, tool results, and inquiry questions, alongside titles and chat content. Its search scope matches `jp conversation grep`. Signed-off-by: Jean Mertz --- crates/jp_cli/src/cmd/conversation/use_.rs | 6 +-- crates/jp_cli/src/shared/search.rs | 26 ++--------- crates/jp_cli/src/shared/search_tests.rs | 54 ++++++++++++++++++---- 3 files changed, 54 insertions(+), 32 deletions(-) diff --git a/crates/jp_cli/src/cmd/conversation/use_.rs b/crates/jp_cli/src/cmd/conversation/use_.rs index 2e45163ae..20365625a 100644 --- a/crates/jp_cli/src/cmd/conversation/use_.rs +++ b/crates/jp_cli/src/cmd/conversation/use_.rs @@ -26,10 +26,10 @@ pub(crate) struct Use { #[command(flatten)] target: PositionalIds, - /// Restrict picker candidates to conversations whose title or chat content - /// matches. + /// Restrict picker candidates to conversations containing the pattern. /// - /// Substring match. + /// Substring match against the title, chat text, reasoning, structured + /// output, tool calls, tool results, and inquiry questions. /// Case-insensitive unless the pattern contains an uppercase character /// (smart-case). /// Composable with target keywords (`?`, `?p`, `?s`, `?a`) and with diff --git a/crates/jp_cli/src/shared/search.rs b/crates/jp_cli/src/shared/search.rs index fa8c70733..9d0f18310 100644 --- a/crates/jp_cli/src/shared/search.rs +++ b/crates/jp_cli/src/shared/search.rs @@ -319,17 +319,13 @@ impl Matcher { } } -/// Filter conversation IDs to those whose title or chat content contains -/// `pattern` as a literal. +/// Filter conversation IDs to those containing `pattern` as a literal. /// /// Smart-case: case-insensitive unless `pattern` contains an uppercase /// character. -/// Searched scopes: title, user, assistant, reasoning, and structured. -/// Deliberately narrower than `conversation grep`, which also reads tool calls, -/// tool results, and inquiries: this backs `conversation use --grep`, whose job -/// is finding a conversation by what was *said* in it. -/// The scope set is fixed because the flag has no way to express one; reach for -/// `conversation grep` to search tool traffic, then pass the ID back to `use`. +/// Every searchable scope is read: title, chat text, reasoning, structured +/// output, tool call names and arguments, tool results, and inquiry questions. +/// That is the same set `conversation grep` searches by default. /// Runs in parallel via rayon and short-circuits on the first match per /// conversation. /// @@ -350,7 +346,7 @@ pub(crate) fn filter_ids( .collect()) } -/// Whether the conversation's title or chat content matches. +/// Whether any of the conversation's searchable text matches. fn id_matches(ctx: &Ctx, id: ConversationId, matcher: &Matcher) -> bool { let Ok(handle) = ctx.workspace.acquire_conversation(&id) else { return false; @@ -371,18 +367,6 @@ fn id_matches(ctx: &Ctx, id: ConversationId, matcher: &Matcher) -> bool { }; for event in events.iter() { - let Some(scope) = event_scope(&event.event.kind) else { - continue; - }; - if !matches!( - scope, - ConcreteScope::User - | ConcreteScope::Assistant - | ConcreteScope::Reasoning - | ConcreteScope::Structured - ) { - continue; - } for line in event_lines(&event.event.kind) { if matcher.is_match(&line) { return true; diff --git a/crates/jp_cli/src/shared/search_tests.rs b/crates/jp_cli/src/shared/search_tests.rs index 483d6892e..cee7cd4df 100644 --- a/crates/jp_cli/src/shared/search_tests.rs +++ b/crates/jp_cli/src/shared/search_tests.rs @@ -5,7 +5,10 @@ use chrono::{TimeZone as _, Utc}; use jp_config::AppConfig; use jp_conversation::{ Conversation, ConversationEvent, ConversationId, EventKind, - event::{ChatRequest, ChatResponse, ToolCallResponse}, + event::{ + ChatRequest, ChatResponse, InquiryQuestion, InquiryRequest, InquirySource, ToolCallRequest, + ToolCallResponse, + }, }; use jp_printer::{OutputFormat, Printer}; use jp_workspace::Workspace; @@ -137,9 +140,9 @@ fn a_poisoned_matcher_stops_matching() { // --- filter_ids ------------------------------------------------------------- // -// `filter_ids` uses fixed scopes (title + chat) and smart-case matching, and -// returns matching IDs without building hit metadata. These tests pin the -// scope set and the smart-case rule. +// `filter_ids` searches every scope with smart-case matching, and returns +// matching IDs without building hit metadata. These tests pin the scope set and +// the smart-case rule. /// The matching IDs, for a pattern expected to compile. fn matching(ctx: &Ctx, ids: &[ConversationId], pattern: &str) -> Vec { @@ -201,9 +204,7 @@ fn filter_ids_matches_title() { } #[test] -fn filter_ids_ignores_tool_call_response() { - // Tool call results sit outside the chat-style scope set. A match in - // tool output should NOT pull the conversation into the picker. +fn filter_ids_matches_tool_call_response() { let id = make_id(20_500); let ctx = setup_ctx_with_events(vec![(id, vec![ConversationEvent::new( ToolCallResponse { @@ -213,7 +214,44 @@ fn filter_ids_ignores_tool_call_response() { Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap(), )])]); - assert!(matching(&ctx, &[id], "secret-keyword").is_empty()); + assert_eq!(matching(&ctx, &[id], "secret-keyword"), vec![id]); +} + +#[test] +fn filter_ids_matches_tool_call_request_arguments() { + // Arguments are serialized on demand rather than stored as text, so this + // pins the one scope whose searchable content doesn't already exist as a + // string in the event. + let id = make_id(20_510); + let mut arguments = serde_json::Map::new(); + arguments.insert( + "pattern".to_owned(), + serde_json::Value::String("integer_literal_enum_has_integer_type".to_owned()), + ); + let ctx = setup_ctx_with_events(vec![(id, vec![ConversationEvent::new( + ToolCallRequest::new("tc1".into(), "fs_grep_files".into(), arguments), + Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap(), + )])]); + + assert_eq!( + matching(&ctx, &[id], "integer_literal_enum_has_integer_type"), + vec![id] + ); +} + +#[test] +fn filter_ids_matches_inquiry_question() { + let id = make_id(20_520); + let ctx = setup_ctx_with_events(vec![(id, vec![ConversationEvent::new( + InquiryRequest::new( + "iq1", + InquirySource::Assistant, + InquiryQuestion::text("Which migration strategy should I use?".to_owned()), + ), + Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap(), + )])]); + + assert_eq!(matching(&ctx, &[id], "migration strategy"), vec![id]); } #[test] From b170f2e98c30163a139121509bf8488c8e834534 Mon Sep 17 00:00:00 2001 From: Jean Mertz Date: Sun, 23 Aug 2026 18:51:17 +0200 Subject: [PATCH 2/3] review feedback Signed-off-by: Jean Mertz --- .../jp_cli/src/cmd/conversation/grep_tests.rs | 22 +++++++++++ crates/jp_cli/src/shared/search.rs | 21 ++++++---- crates/jp_cli/src/shared/search_tests.rs | 38 +++++++++++++++++++ 3 files changed, 73 insertions(+), 8 deletions(-) diff --git a/crates/jp_cli/src/cmd/conversation/grep_tests.rs b/crates/jp_cli/src/cmd/conversation/grep_tests.rs index a0c5aff06..d4818e49e 100644 --- a/crates/jp_cli/src/cmd/conversation/grep_tests.rs +++ b/crates/jp_cli/src/cmd/conversation/grep_tests.rs @@ -1311,6 +1311,28 @@ fn tool_results_are_searched() { )]); } +#[test] +fn scope_structured_searches_serialized_json() { + let id = make_id(9500); + let (mut ctx, out) = setup(vec![( + id, + turn(vec![ConversationEvent::new( + ChatResponse::structured(json!({ "name": "Alice" })), + ts(), + )]), + )]); + + let grep = Grep { + scopes: vec![Scope::Structured], + ..grep("Alice") + }; + // The persisted value is a JSON object, so the searchable text is the + // pretty-printed serialization rather than the value itself. + assert_eq!(run(grep, &mut ctx, &out), [format!( + "{id}:1:structured:m: \"name\": \"Alice\"" + )]); +} + #[test] fn expand_scopes_empty_is_all() { assert_eq!(expand_scopes(&[]).len(), ConcreteScope::ALL.len()); diff --git a/crates/jp_cli/src/shared/search.rs b/crates/jp_cli/src/shared/search.rs index 9d0f18310..794630188 100644 --- a/crates/jp_cli/src/shared/search.rs +++ b/crates/jp_cli/src/shared/search.rs @@ -87,8 +87,8 @@ pub(crate) fn event_scope(kind: &EventKind) -> Option { /// Extract all searchable text lines from an event. /// -/// Lines may be borrowed from the event or owned (tool call arguments are -/// serialized on demand). +/// Lines may be borrowed from the event or owned (tool call arguments and +/// structured responses are serialized on demand). pub(crate) fn event_lines(kind: &EventKind) -> Vec> { match kind { EventKind::ChatRequest(req) => req.content.lines().map(Cow::Borrowed).collect(), @@ -98,12 +98,17 @@ pub(crate) fn event_lines(kind: &EventKind) -> Vec> { EventKind::ChatResponse(ChatResponse::Reasoning { reasoning }) => { reasoning.lines().map(Cow::Borrowed).collect() } - EventKind::ChatResponse(ChatResponse::Structured { data }) => data - .as_str() - .iter() - .flat_map(|text| text.lines()) - .map(Cow::Borrowed) - .collect(), + EventKind::ChatResponse(ChatResponse::Structured { data }) => match data.as_str() { + // A response whose JSON failed to parse is kept as a raw string. + // Searching it verbatim avoids re-quoting and escaping it. + Some(text) => text.lines().map(Cow::Borrowed).collect(), + // Anything else was parsed into a `Value` before it was persisted, + // so its text has to be rebuilt. Pretty-printed for the same reason + // tool call arguments are. + None => serde_json::to_string_pretty(data) + .map(|json| json.lines().map(|line| Cow::Owned(line.to_owned())).collect()) + .unwrap_or_default(), + }, EventKind::ToolCallRequest(req) => { let mut out: Vec> = req.name.lines().map(Cow::Borrowed).collect(); if !req.arguments.is_empty() { diff --git a/crates/jp_cli/src/shared/search_tests.rs b/crates/jp_cli/src/shared/search_tests.rs index cee7cd4df..1174c3488 100644 --- a/crates/jp_cli/src/shared/search_tests.rs +++ b/crates/jp_cli/src/shared/search_tests.rs @@ -85,6 +85,33 @@ fn event_lines_chat_response_reasoning() { assert_eq!(collect_lines(&kind), vec!["thinking...".to_string()]); } +#[test] +fn event_lines_chat_response_structured_object() { + // A structured response is parsed into a `Value` before it is persisted, so + // the searchable text has to be re-serialized. Pretty-printed, matching how + // tool call arguments are handled. + let kind = EventKind::ChatResponse(ChatResponse::structured(serde_json::json!({ + "name": "Alice" + }))); + + assert_eq!(collect_lines(&kind), vec![ + "{".to_string(), + " \"name\": \"Alice\"".to_string(), + "}".to_string(), + ]); +} + +#[test] +fn event_lines_chat_response_structured_string_is_verbatim() { + // A response whose JSON failed to parse is preserved as a raw string. It is + // searched as-is rather than re-quoted. + let kind = EventKind::ChatResponse(ChatResponse::structured(serde_json::Value::String( + "not json {".to_owned(), + ))); + + assert_eq!(collect_lines(&kind), vec!["not json {".to_string()]); +} + #[test] fn event_lines_turn_start_is_empty() { let kind = EventKind::TurnStart(jp_conversation::event::TurnStart); @@ -239,6 +266,17 @@ fn filter_ids_matches_tool_call_request_arguments() { ); } +#[test] +fn filter_ids_matches_structured_object() { + let id = make_id(20_530); + let ctx = setup_ctx_with_events(vec![(id, vec![ConversationEvent::new( + ChatResponse::structured(serde_json::json!({ "name": "Alice" })), + Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap(), + )])]); + + assert_eq!(matching(&ctx, &[id], "Alice"), vec![id]); +} + #[test] fn filter_ids_matches_inquiry_question() { let id = make_id(20_520); From b993a5414d7a3c48ada94f9ba0b84fa6c4eccec0 Mon Sep 17 00:00:00 2001 From: Jean Mertz Date: Sun, 23 Aug 2026 18:54:35 +0200 Subject: [PATCH 3/3] fixup! review feedback Signed-off-by: Jean Mertz --- crates/jp_cli/src/shared/search.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/jp_cli/src/shared/search.rs b/crates/jp_cli/src/shared/search.rs index 794630188..651a3303c 100644 --- a/crates/jp_cli/src/shared/search.rs +++ b/crates/jp_cli/src/shared/search.rs @@ -106,7 +106,11 @@ pub(crate) fn event_lines(kind: &EventKind) -> Vec> { // so its text has to be rebuilt. Pretty-printed for the same reason // tool call arguments are. None => serde_json::to_string_pretty(data) - .map(|json| json.lines().map(|line| Cow::Owned(line.to_owned())).collect()) + .map(|json| { + json.lines() + .map(|line| Cow::Owned(line.to_owned())) + .collect() + }) .unwrap_or_default(), }, EventKind::ToolCallRequest(req) => {