diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-13 03:26:03 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-13 03:30:13 +0300 |
| commit | 5cfe5e5e730246948b0c9428ad876b6938590aad (patch) | |
| tree | 53d4bbe1aff6887f63f279eed1b5e4e11e7331b7 /src/server | |
| parent | 7f3364a468f7dcfd8c9c84e5737fe5699f0e3a93 (diff) | |
feat: add configurable sidebar metadata tokens
Diffstat (limited to 'src/server')
| -rw-r--r-- | src/server/headless.rs | 122 |
1 files changed, 105 insertions, 17 deletions
diff --git a/src/server/headless.rs b/src/server/headless.rs index e6d2b833..6cec130a 100644 --- a/src/server/headless.rs +++ b/src/server/headless.rs @@ -463,6 +463,12 @@ impl HeadlessServer { needs_render = true; crate::render_prof::event("render.request.pty_dirty"); } + let terminal_title_changed = self.app.sync_terminal_titles(); + if terminal_title_changed && self.app.terminal_title_sidebar_configured() { + needs_render = true; + needs_full_render = true; + crate::render_prof::event("full_render_cause.terminal_title"); + } // 2. Drain a bounded internal-event batch. API handlers perform an // exhaustive forwarding-aware drain before reading pane/runtime state. @@ -471,6 +477,11 @@ impl HeadlessServer { needs_full_render = true; crate::render_prof::event("full_render_cause.internal_events"); } + if self.app.expire_due_metadata(Instant::now()) { + needs_render = true; + needs_full_render = true; + crate::render_prof::event("full_render_cause.metadata_expiry"); + } // 3. Drain API requests. if self.drain_api_requests_with_shutdown_check() { @@ -2789,6 +2800,8 @@ impl HeadlessServer { return false; } + let metadata_expired = self.app.expire_due_metadata(Instant::now()); + if let api::schema::Method::ServerLiveHandoff(params) = &msg.request.method { let response = match self.perform_live_handoff(params.clone()) { Ok(()) => serde_json::to_string(&api::schema::SuccessResponse { @@ -2832,7 +2845,7 @@ impl HeadlessServer { _ => {} } - let mut changed = api::request_changes_ui(&msg.request); + let mut changed = metadata_expired | api::request_changes_ui(&msg.request); let skip_default_workspace = skip_default_workspace_for_request || matches!( &msg.request.method, @@ -3713,13 +3726,7 @@ impl HeadlessServer { .agent_metadata_deadline .filter(|deadline| now >= *deadline) { - let previous_toast = self.app.state.toast.clone(); - for update in self.app.state.expire_agent_metadata_at(deadline, now) { - self.app - .refresh_new_herdr_toast_context_for_update(&update, &previous_toast); - self.app.emit_pane_state_update(&update); - } - self.app.sync_agent_metadata_deadline(); + self.app.expire_metadata_at(deadline, now); changed = true; } @@ -4260,6 +4267,91 @@ mod tests { } } + #[tokio::test] + async fn headless_api_reads_latest_title_without_spinner_event_flooding() { + let event_hub = api::EventHub::default(); + let mut server = test_headless_server_with_event_hub(event_hub.clone()); + server.app.state.workspaces = vec![crate::workspace::Workspace::test_new("one")]; + server.app.state.ensure_test_terminals(); + server.app.state.active = Some(0); + server.app.state.selected = 0; + server.app.state.mode = crate::app::Mode::Terminal; + server.app.state.sidebar_agents.rows = vec![vec![ + crate::config::AgentSidebarToken::TerminalTitleStripped, + ]]; + let pane_id = server.app.state.workspaces[0].tabs[0].root_pane; + let terminal_id = server.app.state.workspaces[0].tabs[0].panes[&pane_id] + .attached_terminal_id + .clone(); + server + .app + .state + .terminals + .get_mut(&terminal_id) + .unwrap() + .detected_agent = Some(crate::detect::Agent::Claude); + let runtime = crate::terminal::TerminalRuntime::test_with_screen_bytes(80, 24, b""); + runtime.test_process_pty_bytes(b"\x1b]0;\xe2\xa0\x8b task\x07"); + server + .app + .terminal_runtimes + .insert(terminal_id.clone(), runtime); + + let first = headless_pane_list(&mut server).pop().unwrap(); + assert_eq!(first.terminal_title.as_deref(), Some("⠋ task")); + assert_eq!(first.terminal_title_stripped.as_deref(), Some("task")); + assert_eq!(pane_updated_events(&event_hub), 1); + let (buffer, _) = crate::server::render_stream::render_virtual_with_runtime_registry( + &mut server.app.state, + &server.app.terminal_runtimes, + Rect::new(0, 0, 100, 30), + true, + crate::kitty_graphics::HostCellSize::default(), + ); + let rendered = buffer + .content + .iter() + .map(|cell| cell.symbol()) + .collect::<String>(); + assert!(rendered.contains("task"), "rendered frame: {rendered:?}"); + + server + .app + .terminal_runtimes + .get(&terminal_id) + .unwrap() + .test_process_pty_bytes(b"\x1b]2;\xe2\xa0\x99 task\x1b\\"); + let second = headless_pane_list(&mut server).pop().unwrap(); + assert_eq!(second.terminal_title.as_deref(), Some("⠙ task")); + assert_eq!(second.terminal_title_stripped.as_deref(), Some("task")); + assert_eq!(pane_updated_events(&event_hub), 1); + } + + fn headless_pane_list(server: &mut HeadlessServer) -> Vec<api::schema::PaneInfo> { + let (respond_to, response_rx) = std::sync::mpsc::channel(); + server.handle_api_request_with_shutdown_check(api::ApiRequestMessage { + request: api::schema::Request { + id: "list-titles".into(), + method: api::schema::Method::PaneList(api::schema::PaneListParams::default()), + }, + respond_to, + }); + let response: api::schema::SuccessResponse = + serde_json::from_str(&response_rx.recv().unwrap()).unwrap(); + let api::schema::ResponseResult::PaneList { panes } = response.result else { + panic!("expected pane list"); + }; + panes + } + + fn pane_updated_events(event_hub: &api::EventHub) -> usize { + event_hub + .events_after(0) + .iter() + .filter(|(_, event)| event.event == api::schema::EventKind::PaneUpdated) + .count() + } + #[test] fn headless_api_request_drains_all_pending_internal_events_before_reading_state() { let mut server = test_headless_server(); @@ -5431,7 +5523,6 @@ next_tab = "" agent_label: "pi".into(), state: crate::detect::AgentState::Working, message: None, - custom_status: None, seq: None, session_ref: None, }) @@ -5442,13 +5533,11 @@ next_tab = "" source: "user:pi-display".into(), agent_label: Some("pi".into()), applies_to_source: Some("herdr:pi".into()), - title: None, + title: Some("short lived".into()), display_agent: None, - custom_status: Some("short lived".into()), state_labels: HashMap::new(), clear_title: false, clear_display_agent: false, - clear_custom_status: false, clear_state_labels: false, seq: None, ttl: Some(Duration::from_millis(1)), @@ -5471,7 +5560,7 @@ next_tab = "" .terminals .get(&terminal_id) .expect("terminal") - .effective_custom_status() + .effective_title() .as_deref(), Some("short lived") ); @@ -5486,7 +5575,7 @@ next_tab = "" .terminals .get(&terminal_id) .expect("terminal") - .effective_custom_status(), + .effective_title(), None ); assert!(server @@ -5499,9 +5588,9 @@ next_tab = "" && matches!( &event.data, crate::api::schema::EventData::PaneAgentStatusChanged { - custom_status, + title, .. - } if custom_status.is_none() + } if title.is_none() ) })); } @@ -8587,7 +8676,6 @@ next_tab = "" agent: "pi".into(), state: api::schema::PaneAgentState::Idle, message: None, - custom_status: None, seq: Some(19), agent_session_id: None, agent_session_path: None, |
