aboutsummaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-13 03:26:03 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-13 03:30:13 +0300
commit5cfe5e5e730246948b0c9428ad876b6938590aad (patch)
tree53d4bbe1aff6887f63f279eed1b5e4e11e7331b7 /src/server
parent7f3364a468f7dcfd8c9c84e5737fe5699f0e3a93 (diff)
feat: add configurable sidebar metadata tokens
Diffstat (limited to 'src/server')
-rw-r--r--src/server/headless.rs122
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,