1use crate::handlers::default_excludes_subagents;
8use crate::wire::{
9 GetResponse, GetResult, MessageView, PartKind, PartSummary, ResponsePart, SearchModeWire,
10 SearchRequest, SearchResponse, SortBy,
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Surface {
21 Mcp,
22 Cli,
23}
24
25fn count_noun(count: usize, noun: &str) -> String {
27 if count == 1 {
28 format!("{count} {noun}")
29 } else {
30 format!("{count} {noun}s")
31 }
32}
33
34pub fn render_subagents_footer(children: &[crate::wire::Session], surface: Surface) -> String {
39 use std::fmt::Write;
40 let how = match surface {
41 Surface::Mcp => "pass an id to pond_get_session(id=...)",
42 Surface::Cli => "pass an id to `pond get-session <ID>`",
43 };
44 let mut out = String::new();
45 let _ = writeln!(out);
46 let _ = writeln!(out, "subagents ({}) - {how}:", children.len());
47 for child in children {
48 let _ = writeln!(out, " {} | {}", child.id, child.source_agent);
49 }
50 out
51}
52
53fn fmt_ts(ts: &chrono::DateTime<chrono::Utc>) -> String {
55 ts.format("%Y-%m-%d %H:%M:%SZ").to_string()
56}
57
58fn opt_name(value: &Option<crate::adapter::extract::Extracted<String>>) -> &str {
61 value.as_deref().map(String::as_str).unwrap_or("?")
62}
63
64fn push_lines(out: &mut String, body: &str, indent: &str) {
68 use std::fmt::Write;
69 for line in body.lines() {
70 let _ = writeln!(out, "{indent}{line}");
71 }
72}
73
74const SEARCH_TRANSCRIPT_BUDGET: usize = 10_000;
80
81pub fn render_search_transcript(
82 response: &SearchResponse,
83 request: &SearchRequest,
84 surface: Surface,
85) -> String {
86 use std::fmt::Write;
87 let prefix = match surface {
88 Surface::Mcp => "pond_search",
89 Surface::Cli => "pond search",
90 };
91 let subagent_note = match (default_excludes_subagents(&request.filters), surface) {
92 (false, _) => "",
93 (true, Surface::Mcp) => {
94 " Subagent sessions excluded; reach them via pond_sql (parent_session_id)."
95 }
96 (true, Surface::Cli) => {
97 " Subagent sessions excluded; reach them via `pond sql` (parent_session_id)."
98 }
99 };
100 let recency_note = if matches!(request.sort_by, SortBy::Recency) {
101 " Sorted by recency (newest first) - rank is NOT match strength."
102 } else {
103 ""
104 };
105 if response.sessions.is_empty() {
106 if response.searchable_in_scope == 0 {
111 let scoped = request.filters.project.is_some()
112 || request.filters.session_id.is_some()
113 || request.filters.from_date.is_some()
114 || request.filters.to_date.is_some();
115 if scoped {
116 return format!(
117 "{prefix}: 0 searchable messages in scope - the filters exclude \
118 everything before retrieval. Widen or drop project/date filters.\
119 {subagent_note}\n"
120 );
121 }
122 return match surface {
125 Surface::Cli => format!(
126 "{prefix}: no sessions stored yet - run `pond init` to set up \
127 adapters, then `pond sync` to import your history.\n"
128 ),
129 Surface::Mcp => format!(
130 "{prefix}: the store has no searchable messages yet (nothing \
131 ingested so far). Not an absence signal about the topic.\n"
132 ),
133 };
134 }
135 let fts_hint = match surface {
136 Surface::Mcp => {
137 " For exact strings or identifiers, try pond_sql: SELECT \
138 message_id, session_id, search_text FROM messages WHERE \
139 contains_tokens(search_text, '...')."
140 }
141 Surface::Cli => {
142 " For exact strings or identifiers, try: pond sql \"SELECT \
143 message_id, session_id, search_text FROM messages WHERE \
144 contains_tokens(search_text, '...')\"."
145 }
146 };
147 return format!(
148 "{prefix}: no matches for {:?} across {} in \
149 scope.{subagent_note}{fts_hint}\n",
150 request.query,
151 count_noun(response.searchable_in_scope, "searchable message"),
152 );
153 }
154 let shown: usize = response.sessions.iter().map(|s| s.matches.len()).sum();
155 let vector_mode = matches!(request.mode, SearchModeWire::Vector);
161 let head_noun = if vector_mode {
162 "nearest message"
163 } else {
164 "matching message"
165 };
166 let mut out = String::new();
167 let _ = writeln!(
168 out,
169 "{prefix}: {} ({} searchable in scope), showing {} from {}.{}{}",
170 count_noun(response.matched_total, head_noun),
171 response.searchable_in_scope,
172 count_noun(shown, "hit"),
173 count_noun(response.sessions.len(), "session"),
174 subagent_note,
175 recency_note,
176 );
177 let order = if matches!(request.sort_by, SortBy::Recency) {
178 "newest session first"
179 } else {
180 "ordered by best hit"
181 };
182 let full_hint = match surface {
183 Surface::Mcp => "pond_get_message <message_id> for full, pond_get_session for the session",
184 Surface::Cli => "`pond get-message <ID>` for full, `pond get-session <ID>` for the session",
185 };
186 let mode_note = match (vector_mode, surface) {
187 (false, _) => "",
188 (true, Surface::Cli) => {
189 " Vector mode returns the closest rows by meaning even when none are strong; for exact-word matching use --mode fts."
190 }
191 (true, Surface::Mcp) => {
192 " Vector mode returns the closest rows by meaning even when none are strong; for exact-word matching set mode=\"fts\"."
193 }
194 };
195 let _ = writeln!(
196 out,
197 "key: session rules group hits by session, {order}; within a session, messages are newest-first. \"--- [n] score | role | time | message_id | project | agent | session ---\" delimits each hit + matched text. {full_hint}; raise limit for more (no pagination).{mode_note}"
198 );
199 let mut index = 0;
200 let n_sessions = response.sessions.len();
201 for (session_index, session) in response.sessions.iter().enumerate() {
202 let best = session
205 .matches
206 .iter()
207 .map(|hit| hit.score)
208 .fold(0.0_f64, f64::max);
209 let _ = writeln!(out);
210 let _ = writeln!(
211 out,
212 "{}",
213 rule_line(&format!(
214 "session [{}] best {:.2} | {}/{} matched | {} | {} | {}",
215 session_index + 1,
216 best,
217 session.matched_message_count,
218 session.session_messages_count,
219 session.project,
220 session.source_agent,
221 session.session_id,
222 )),
223 );
224 let remaining = SEARCH_TRANSCRIPT_BUDGET.saturating_sub(out.len());
229 let share = remaining / (n_sessions - session_index);
230 let session_start = out.len();
231 let mut rendered = 0usize;
232 for hit in &session.matches {
233 if rendered > 0 && out.len().saturating_sub(session_start) >= share {
234 break;
235 }
236 index += 1;
237 let _ = writeln!(out);
238 let _ = writeln!(
239 out,
240 "{}",
241 rule_line(&format!(
242 "[{index}] {:.2} | {} | {} | {} | {} | {} | {}",
243 hit.score,
244 hit.role.as_str(),
245 fmt_ts(&hit.timestamp),
246 hit.message_id,
247 session.project,
248 session.source_agent,
249 session.session_id,
250 )),
251 );
252 push_lines(&mut out, &hit.text, "");
253 rendered += 1;
254 }
255 let omitted = session.matches.len() - rendered;
259 if omitted > 0 {
260 let latest_hint = match surface {
261 Surface::Mcp => "read with pond_get_session from=end",
262 Surface::Cli => "read with `pond get-session <ID> --from end`",
263 };
264 let _ = writeln!(
265 out,
266 "... {omitted} more match(es) in this session not shown (char budget); \
267 {latest_hint} for the session's latest state"
268 );
269 }
270 }
271 out
272}
273
274pub fn render_get_transcript(response: &GetResponse, surface: Surface) -> String {
275 use std::fmt::Write;
276 let session = &response.session;
277 let mut out = String::new();
278 match &response.result {
279 GetResult::Session {
280 messages,
281 before_remaining,
282 after_remaining,
283 resolved_from_message_id,
284 } => {
285 let prefix = match surface {
286 Surface::Mcp => "pond_get_session",
287 Surface::Cli => "pond get-session",
288 };
289 let total = *before_remaining + messages.len() + *after_remaining;
293 if total > messages.len() {
294 let start = *before_remaining + 1;
295 let end = *before_remaining + messages.len();
296 let _ = writeln!(
297 out,
298 "{prefix}: session {}, messages {start}-{end} of {total} \
299 (pages are bounded by limit and a size budget).",
300 session.id,
301 );
302 } else {
303 let _ = writeln!(
304 out,
305 "{prefix}: session {}, {}.",
306 session.id,
307 count_noun(messages.len(), "message"),
308 );
309 }
310 if let Some(message_id) = resolved_from_message_id {
313 let expand = match surface {
314 Surface::Mcp => format!("pond_get_message id={message_id}"),
315 Surface::Cli => format!("`pond get-message {message_id}`"),
316 };
317 let _ = writeln!(
318 out,
319 "note: {message_id} is a message id - resolved to its session; this page \
320 starts at that message ({expand} for its full part bodies)."
321 );
322 }
323 let (expand_hint, page_hint) = match surface {
324 Surface::Mcp => (
325 "pond_get_message id=<id> to expand any tool body",
326 "Page with before_message_id / after_message_id.",
327 ),
328 Surface::Cli => (
329 "`pond get-message <ID>` to expand any tool body",
330 "Page with --before-message-id / --after-message-id.",
331 ),
332 };
333 let _ = writeln!(
334 out,
335 "key: \"--- [n] role | time | message_id ---\" delimits each message; \"->\" tool call, \"<-\" result; {expand_hint}. {page_hint}"
336 );
337 if *before_remaining > 0
339 && let Some(first) = messages.first()
340 {
341 let page_up = match surface {
342 Surface::Mcp => format!("pass before_message_id={}", first.id),
343 Surface::Cli => {
344 format!("pass --before-message-id {}", first.id)
345 }
346 };
347 let _ = writeln!(
348 out,
349 "... {before_remaining} earlier messages; {page_up} to page up",
350 );
351 }
352 for (idx, message) in messages.iter().enumerate() {
353 let _ = writeln!(out);
354 render_message(
355 &mut out,
356 idx + 1,
357 message,
358 None,
359 &message.parts_summary,
360 false,
361 );
362 }
363 let _ = writeln!(out);
364 let _ = writeln!(
365 out,
366 "session {} | {} | {}",
367 session.id, session.source_agent, session.project,
368 );
369 if *after_remaining > 0
374 && let Some(last) = messages.last()
375 {
376 let page_down = match surface {
377 Surface::Mcp => format!("pass after_message_id={}", last.id),
378 Surface::Cli => format!("pass --after-message-id {}", last.id),
379 };
380 let latest = match surface {
381 Surface::Mcp => "from=\"end\"",
382 Surface::Cli => "--from end",
383 };
384 let _ = writeln!(
385 out,
386 "... {after_remaining} later messages; {page_down} to page down \
387 (conclusions may have been revised - {latest} reads the latest turns)",
388 );
389 }
390 }
391 GetResult::Message {
392 target,
393 target_parts,
394 target_parts_remaining,
395 siblings,
396 context_before,
397 context_after,
398 } => {
399 let prefix = match surface {
400 Surface::Mcp => "pond_get_message",
401 Surface::Cli => "pond get-message",
402 };
403 let _ = writeln!(
404 out,
405 "{prefix}: thread around {} in session {} (context -{}/+{}).",
406 target.id, session.id, context_before, context_after,
407 );
408 let expand_hint = match surface {
409 Surface::Mcp => "pond_get_message id=<id> to expand any line",
410 Surface::Cli => "`pond get-message <ID>` to expand any line",
411 };
412 let _ = writeln!(
413 out,
414 "key: \"--- [n] role | time | message_id ---\" delimits each message; \">\" = the one you requested; \"->\" tool call, \"<-\" result. {expand_hint}."
415 );
416 let mut thread: Vec<(&MessageView, bool)> =
423 siblings.iter().map(|view| (view, false)).collect();
424 thread.push((target, true));
425 thread.sort_by(|a, b| {
426 a.0.timestamp
427 .cmp(&b.0.timestamp)
428 .then_with(|| a.0.id.cmp(&b.0.id))
429 });
430 thread.retain(|(view, is_target)| *is_target || message_has_content(view));
431 for (idx, (view, is_target)) in thread.iter().enumerate() {
432 let _ = writeln!(out);
433 let parts: Option<&[ResponsePart]> = is_target.then_some(target_parts.as_slice());
436 render_message(
437 &mut out,
438 idx + 1,
439 view,
440 parts,
441 &view.parts_summary,
442 *is_target,
443 );
444 }
445 let _ = writeln!(out);
446 let _ = writeln!(
447 out,
448 "session {} | {} | {}",
449 session.id, session.source_agent, session.project,
450 );
451 if *target_parts_remaining > 0 {
452 let _ = writeln!(
453 out,
454 "... {} more parts of {} omitted (response budget)",
455 target_parts_remaining, target.id,
456 );
457 }
458 }
459 }
460 out
461}
462
463fn message_has_content(view: &MessageView) -> bool {
467 view.text.as_deref().is_some_and(|t| !t.trim().is_empty())
468 || view
469 .content
470 .as_deref()
471 .is_some_and(|c| !c.trim().is_empty())
472 || !view.parts_summary.is_empty()
473}
474
475const RULE_WIDTH: usize = 72;
477
478fn rule_line(inner: &str) -> String {
482 let head = format!("--- {inner} ");
483 let pad = RULE_WIDTH.saturating_sub(head.chars().count()).max(3);
484 format!("{head}{}", "-".repeat(pad))
485}
486
487fn render_message(
492 out: &mut String,
493 index: usize,
494 view: &MessageView,
495 parts: Option<&[ResponsePart]>,
496 summary: &[PartSummary],
497 is_target: bool,
498) {
499 use std::fmt::Write;
500 let marker = if is_target { "> " } else { "" };
501 let _ = writeln!(
502 out,
503 "{}",
504 rule_line(&format!(
505 "[{index}] {marker}{} | {} | {}",
506 view.role.as_str(),
507 fmt_ts(&view.timestamp),
508 view.id,
509 )),
510 );
511 if let Some(text) = &view.text {
512 push_lines(out, text, "");
513 }
514 if let Some(content) = &view.content {
515 push_lines(out, content, "");
516 }
517 match parts {
518 Some(parts) => {
519 for part in parts {
520 render_part_full(out, part);
521 }
522 }
523 None => {
524 for part in summary {
525 render_part_summary(out, part);
526 }
527 }
528 }
529}
530
531fn render_part_full(out: &mut String, part: &ResponsePart) {
532 use std::fmt::Write;
533 match &part.kind {
534 PartKind::Text { text } => {
535 if let Some(text) = text {
536 push_lines(out, text, "");
537 }
538 }
539 PartKind::Reasoning { text } => {
540 let _ = writeln!(out, " (reasoning)");
541 if let Some(text) = text {
542 push_lines(out, text, " ");
543 }
544 }
545 PartKind::ToolCall {
546 name,
547 call_id,
548 params,
549 ..
550 } => {
551 let _ = writeln!(out, " -> {} [{}]", opt_name(name), opt_name(call_id));
552 push_lines(out, &value_to_text(params), " ");
553 }
554 PartKind::ToolResult {
555 name,
556 call_id,
557 is_failure,
558 result,
559 } => {
560 let status = if *is_failure { "failed" } else { "ok" };
561 let _ = writeln!(
562 out,
563 " <- {} [{}] ({status})",
564 opt_name(name),
565 opt_name(call_id),
566 );
567 push_lines(out, &value_to_text(result), " ");
568 }
569 PartKind::File {
570 media_type,
571 file_name,
572 ..
573 } => {
574 let label = file_name
575 .as_deref()
576 .or(media_type.as_deref())
577 .unwrap_or("file");
578 let _ = writeln!(out, " [file {label}]");
579 }
580 PartKind::ToolApprovalRequest { approval_id, .. } => {
581 let _ = writeln!(out, " [approval request {approval_id}]");
582 }
583 PartKind::ToolApprovalResponse {
584 approval_id,
585 approved,
586 ..
587 } => {
588 let verb = if *approved { "approved" } else { "denied" };
589 let _ = writeln!(out, " [approval {approval_id} {verb}]");
590 }
591 }
592}
593
594fn render_part_summary(out: &mut String, summary: &PartSummary) {
595 use std::fmt::Write;
596 let label = summary.label.as_deref().unwrap_or("");
597 let call = summary
598 .call_id
599 .as_deref()
600 .map(|id| format!(" [{id}]"))
601 .unwrap_or_default();
602 match summary.kind.as_str() {
603 "tool_call" => {
604 let _ = writeln!(out, " -> {label}{call}");
605 }
606 "tool_result" => {
607 let _ = writeln!(out, " <- {label}{call}");
608 }
609 "file" => {
610 let _ = writeln!(out, " [file {label}]");
611 }
612 other => {
613 let _ = writeln!(out, " [{other} {label}]");
614 }
615 }
616}
617
618fn value_to_text(value: &serde_json::Value) -> String {
621 match value {
622 serde_json::Value::String(text) => text.clone(),
623 serde_json::Value::Null => String::new(),
624 other => serde_json::to_string(other).unwrap_or_default(),
625 }
626}
627
628#[cfg(test)]
629mod tests {
630 #![allow(clippy::expect_used, clippy::unwrap_used)]
631
632 use super::*;
633 use crate::wire::{Role, SearchFilters, SearchModeWire, SearchResult};
634
635 #[test]
636 fn get_transcript_marks_target_and_renders_tool_parts() {
637 let ts = chrono::DateTime::from_timestamp(0, 0).unwrap();
638 let tool_call: ResponsePart = serde_json::from_value(serde_json::json!({
639 "id": "p1", "ordinal": 0, "provenance": "conversational",
640 "type": "tool_call", "name": "Bash", "call_id": "toolu_x",
641 "params": { "command": "ls" }, "provider_executed": false,
642 }))
643 .unwrap();
644 let tool_result: ResponsePart = serde_json::from_value(serde_json::json!({
645 "id": "p2", "ordinal": 1, "provenance": "conversational",
646 "type": "tool_result", "name": "Bash", "call_id": "toolu_x",
647 "is_failure": false, "result": "file.txt",
648 }))
649 .unwrap();
650 let target = MessageView {
651 id: "m1".to_owned(),
652 role: crate::wire::Role::Assistant,
653 timestamp: ts,
654 text: Some("Let me list files.".to_owned()),
655 content: None,
656 parts_summary: Vec::new(),
657 };
658 let response = GetResponse {
659 session: crate::wire::GetSession {
660 id: "s1".to_owned(),
661 source_agent: "claude-code".to_owned(),
662 project: "/p".to_owned(),
663 created_at: ts,
664 },
665 result: GetResult::Message {
666 target,
667 target_parts: vec![tool_call, tool_result],
668 target_parts_remaining: 0,
669 siblings: Vec::new(),
670 context_before: 3,
671 context_after: 3,
672 },
673 };
674
675 let transcript = crate::render::render_get_transcript(&response, Surface::Mcp);
676 assert!(transcript.contains("--- [1] > assistant | 1970-01-01 00:00:00Z | m1 ---"));
677 assert!(transcript.contains("Let me list files."));
678 assert!(transcript.contains(" -> Bash [toolu_x]"));
679 assert!(transcript.contains(" <- Bash [toolu_x] (ok)"));
680 assert!(transcript.contains("session s1 | claude-code | /p"));
681 }
682
683 #[test]
684 fn session_header_states_span_and_total_on_partial_pages() {
685 let ts = chrono::DateTime::from_timestamp(0, 0).unwrap();
686 let message = |id: &str| MessageView {
687 id: id.to_owned(),
688 role: crate::wire::Role::User,
689 timestamp: ts,
690 text: Some("hi".to_owned()),
691 content: None,
692 parts_summary: Vec::new(),
693 };
694 let session = crate::wire::GetSession {
695 id: "s1".to_owned(),
696 source_agent: "claude-code".to_owned(),
697 project: "/p".to_owned(),
698 created_at: ts,
699 };
700 let partial = GetResponse {
701 session: session.clone(),
702 result: GetResult::Session {
703 messages: vec![message("m1"), message("m2")],
704 before_remaining: 100,
705 after_remaining: 32,
706 resolved_from_message_id: None,
707 },
708 };
709 let transcript = crate::render::render_get_transcript(&partial, Surface::Mcp);
710 assert!(transcript.starts_with(
711 "pond_get_session: session s1, messages 101-102 of 134 \
712 (pages are bounded by limit and a size budget)."
713 ));
714
715 let full = GetResponse {
716 session,
717 result: GetResult::Session {
718 messages: vec![message("m1"), message("m2")],
719 before_remaining: 0,
720 after_remaining: 0,
721 resolved_from_message_id: None,
722 },
723 };
724 let transcript = crate::render::render_get_transcript(&full, Surface::Cli);
725 assert!(transcript.starts_with("pond get-session: session s1, 2 messages."));
726 }
727
728 #[test]
729 fn search_transcript_renders_header_and_hits() {
730 let response = SearchResponse {
731 sessions: vec![crate::wire::SearchSession {
732 session_id: "s1".to_owned(),
733 project: "pond".to_owned(),
734 source_agent: "claude-code".to_owned(),
735 session_messages_count: 2,
736 matched_message_count: 1,
737 matches: vec![SearchResult {
738 message_id: "m1".to_owned(),
739 role: Role::User,
740 timestamp: chrono::DateTime::from_timestamp(0, 0).unwrap(),
741 text: "hello\nworld".to_owned(),
742 score: 1.0,
743 parts_summary: Vec::new(),
744 }],
745 }],
746 matched_total: 1,
747 searchable_in_scope: 2,
748 has_more: false,
749 };
750 let request = SearchRequest {
751 protocol_version: crate::PROTOCOL_VERSION,
752 namespace: None,
753 query: "hi".to_owned(),
754 mode: SearchModeWire::Vector,
755 sort_by: SortBy::Relevance,
756 filters: SearchFilters::default(),
757 limit: 10,
758 };
759
760 let transcript = crate::render::render_search_transcript(&response, &request, Surface::Mcp);
761 assert!(transcript.starts_with(
762 "pond_search: 1 nearest message (2 searchable in scope), showing 1 hit from 1 \
763 session."
764 ));
765 assert!(transcript.contains("Vector mode returns the closest rows"));
767 assert!(
768 transcript.contains("key: session rules group hits by session, ordered by best hit")
769 );
770 assert!(
771 transcript
772 .contains("--- session [1] best 1.00 | 1/2 matched | pond | claude-code | s1")
773 );
774 assert!(
777 transcript.contains(
778 "--- [1] 1.00 | user | 1970-01-01 00:00:00Z | m1 | pond | claude-code | s1"
779 )
780 );
781 assert!(transcript.contains("hello\nworld"));
783 }
784
785 #[test]
786 fn search_transcript_budget_keeps_every_session_and_footers_the_truncated_one() {
787 let big = "x".repeat(600);
788 let hit = |id: usize| SearchResult {
789 message_id: format!("m{id}"),
790 role: Role::Assistant,
791 timestamp: chrono::DateTime::from_timestamp(id as i64, 0).unwrap(),
792 text: big.clone(),
793 score: 0.9,
794 parts_summary: Vec::new(),
795 };
796 let session = |id: &str, matches: Vec<SearchResult>| crate::wire::SearchSession {
797 session_id: id.to_owned(),
798 project: "pond".to_owned(),
799 source_agent: "claude-code".to_owned(),
800 session_messages_count: 100,
801 matched_message_count: matches.len(),
802 matches,
803 };
804 let mut sessions = vec![session("fat", (0..40).map(hit).collect())];
807 for s in 1..=5 {
808 sessions.push(session(&format!("s{s}"), vec![hit(s * 1000)]));
809 }
810 let response = SearchResponse {
811 sessions,
812 matched_total: 45,
813 searchable_in_scope: 200,
814 has_more: false,
815 };
816 let request = SearchRequest {
817 protocol_version: crate::PROTOCOL_VERSION,
818 namespace: None,
819 query: "x".to_owned(),
820 mode: SearchModeWire::Vector,
821 sort_by: SortBy::Relevance,
822 filters: SearchFilters::default(),
823 limit: 10,
824 };
825 let transcript = crate::render::render_search_transcript(&response, &request, Surface::Mcp);
826
827 assert!(
830 transcript.len() < SEARCH_TRANSCRIPT_BUDGET + 3_000,
831 "transcript {} exceeds the soft budget",
832 transcript.len(),
833 );
834 for id in ["fat", "s1", "s2", "s3", "s4", "s5"] {
836 assert!(
837 transcript.contains(&format!("| {id}\n"))
838 || transcript.contains(&format!("| {id} ")),
839 "session {id} did not render",
840 );
841 }
842 assert!(transcript.contains("more match(es) in this session not shown (char budget)"));
845 assert!(transcript.contains("pond_get_session from=end"));
846 }
847}