@@ -5414,13 +5414,37 @@ impl CassApp {
54145414 };
54155415 pane_map.entry(key).or_default().push(hit.clone());
54165416 }
5417+
5418+ // Fix #79: Apply per-agent-type result quota so one agent type cannot
5419+ // monopolize all result slots. When `per_pane_limit` is set the user
5420+ // has an explicit cap; otherwise we apply a fair-share cap derived
5421+ // from the total result count divided across the number of groups.
5422+ let num_groups = pane_map.len().max(1);
5423+ let effective_limit = if self.per_pane_limit > 0 {
5424+ self.per_pane_limit
5425+ } else if num_groups > 1 {
5426+ // Fair share: total results / groups, with minimum floor to avoid
5427+ // showing too few results per pane.
5428+ let total: usize = pane_map.values().map(|v| v.len()).sum();
5429+ let fair = total / num_groups;
5430+ // At least 20 results per pane, or all of them if the total is small.
5431+ fair.max(20).max(total.min(50))
5432+ } else {
5433+ 0 // single group: no cap
5434+ };
5435+
54175436 self.panes = pane_map
54185437 .into_iter()
54195438 .map(|(key, hits)| {
54205439 let total = hits.len();
5440+ let capped = if effective_limit > 0 && hits.len() > effective_limit {
5441+ hits.into_iter().take(effective_limit).collect()
5442+ } else {
5443+ hits
5444+ };
54215445 AgentPane {
54225446 agent: key,
5423- hits,
5447+ hits: capped ,
54245448 selected: 0,
54255449 total_count: total,
54265450 }
@@ -12442,6 +12466,24 @@ impl SearchService for TantivySearchService {
1244212466 let started = Instant::now();
1244312467 let limit = params.limit;
1244412468 let offset = params.offset;
12469+
12470+ // Fix #79: Empty queries bypass BM25 and use date-sorted browsing.
12471+ // BM25 relevance scoring is meaningless without search terms, so we
12472+ // query SQLite directly with ORDER BY created_at instead.
12473+ if params.query.trim().is_empty() {
12474+ let newest_first = !matches!(params.ranking, RankingMode::DateOldest);
12475+ let hits = self
12476+ .client
12477+ .browse_by_date(params.filters.clone(), limit, offset, newest_first)
12478+ .map_err(|e| e.to_string())?;
12479+ return Ok(SearchResult {
12480+ hits,
12481+ elapsed_ms: started.elapsed().as_millis(),
12482+ suggestions: Vec::new(),
12483+ wildcard_fallback: false,
12484+ });
12485+ }
12486+
1244512487 let sparse_threshold = 3;
1244612488 let field_mask = FieldMask::new(true, true, true, true);
1244712489
@@ -13813,12 +13855,9 @@ impl super::ftui_adapter::Model for CassApp {
1381313855 limit: self.search_page_size.max(1),
1381413856 offset: 0,
1381513857 };
13816- // Skip empty queries.
13817- if params.query.trim().is_empty() {
13818- self.clear_loading_context(LoadingContext::Search);
13819- return ftui::Cmd::none();
13820- }
1382113858 // Dispatch async search if a service is available.
13859+ // Note: empty queries are allowed — the backend handles them
13860+ // by browsing recent sessions sorted by date (fix #79).
1382213861 if let Some(svc) = self.search_service.clone() {
1382313862 self.search_generation = generation;
1382413863 self.search_backend_offset = 0;
@@ -13861,9 +13900,6 @@ impl super::ftui_adapter::Model for CassApp {
1386113900 limit: self.search_page_size.max(1),
1386213901 offset: self.search_backend_offset,
1386313902 };
13864- if params.query.trim().is_empty() {
13865- return ftui::Cmd::none();
13866- }
1386713903 if let Some(svc) = self.search_service.clone() {
1386813904 self.search_in_flight = true;
1386913905 self.status = format!("Loading more\u{2026} ({} loaded)", self.results.len());
@@ -14141,7 +14177,10 @@ impl super::ftui_adapter::Model for CassApp {
1414114177 RankingMode::DateOldest => RankingMode::RecentHeavy,
1414214178 };
1414314179 self.dirty_since = Some(Instant::now());
14144- ftui::Cmd::none()
14180+ // Fix #79: re-fetch results from backend so ranking mode
14181+ // changes are reflected (especially for empty-query date
14182+ // browsing where sort order matters).
14183+ ftui::Cmd::msg(CassMsg::SearchRequested)
1414514184 }
1414614185 CassMsg::ContextWindowCycled => {
1414714186 self.context_window = match self.context_window {
@@ -15437,7 +15476,7 @@ impl super::ftui_adapter::Model for CassApp {
1543715476 Self::markdown_filename_from_html(&export_state.filename_preview);
1543815477 let output_path = unique_filename(&output_dir, &output_filename);
1543915478 let include_tools = export_state.include_tools;
15440- self.status = format!( "Exporting markdown to {}", output_path.display() );
15479+ self.status = "Exporting markdown...".to_string( );
1544115480 ftui::Cmd::task(move || {
1544215481 export_session_markdown_task(&source_path, &output_path, include_tools)
1544315482 })
@@ -15466,7 +15505,7 @@ impl super::ftui_adapter::Model for CassApp {
1546615505 let include_tools = state.include_tools;
1546715506 let title = state.title_preview.clone();
1546815507 let agent_name = state.agent_name.clone();
15469- self.status = format!( "Exporting HTML to {}", output_path.display() );
15508+ self.status = "Exporting HTML...".to_string( );
1547015509
1547115510 // Dispatch the export as a background task.
1547215511 return ftui::Cmd::task(move || {
@@ -16227,7 +16266,10 @@ impl super::ftui_adapter::Model for CassApp {
1622716266 self.focus_manager.focus(focus_ids::HELP_OVERLAY);
1622816267 }
1622916268 self.dirty_since = None;
16230- ftui::Cmd::none()
16269+ // Fix #79: Trigger an initial search/browse on startup so the
16270+ // TUI is populated with recent sessions immediately, even when
16271+ // the query is empty.
16272+ ftui::Cmd::msg(CassMsg::SearchRequested)
1623116273 }
1623216274 CassMsg::StateLoadFailed(err) => {
1623316275 self.clear_loading_context(LoadingContext::StateLoad);
@@ -22884,7 +22926,8 @@ mod tests {
2288422926 app.search_dirty_since = Some(Instant::now());
2288522927 let _ = app.update(CassMsg::SearchRequested);
2288622928 assert!(app.search_dirty_since.is_none(), "dirty state should clear");
22887- // No search dispatched (no service, query is empty whitespace)
22929+ // No service attached — no search dispatched. Empty queries are
22930+ // allowed by the handler (fix #79) but still require a service.
2288822931 assert!(app.status.is_empty());
2288922932 assert!(app.loading_context.is_none());
2289022933 }
@@ -24934,14 +24977,14 @@ mod tests {
2493424977 assert!(app.show_detail_modal, "detail modal should remain open");
2493524978 assert_eq!(app.detail_tab, DetailTab::Export);
2493624979 assert!(
24937- app.status.starts_with( "Exporting markdown to ") ,
24938- "status should reflect quick markdown export , got: {}",
24980+ app.status == "Exporting markdown..." ,
24981+ "status should use generic in-flight text , got: {}",
2493924982 app.status
2494024983 );
2494124984 }
2494224985
2494324986 #[test]
24944- fn detail_modal_quick_markdown_export_status_uses_unique_filename () {
24987+ fn detail_modal_quick_markdown_export_status_stays_path_agnostic () {
2494524988 let tmp = tempfile::TempDir::new().expect("tempdir");
2494624989 let mut app = app_with_hits(1);
2494724990 app.show_detail_modal = true;
@@ -24961,8 +25004,8 @@ mod tests {
2496125004 "quick markdown export should dispatch a task"
2496225005 );
2496325006 assert!(
24964- app.status.contains("collision_1.md") ,
24965- "status should reference deduped markdown filename , got: {}",
25007+ app.status == "Exporting markdown..." ,
25008+ "status should avoid precomputed output path , got: {}",
2496625009 app.status
2496725010 );
2496825011 }
0 commit comments