diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-16 22:12:44 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-16 22:17:09 +0300 |
| commit | 0b831c86274486af9f9f7052c2534374b498bb3a (patch) | |
| tree | 3e8130be85cb387c8c3e57591617c4c7be0a26ec /src/app | |
| parent | 318e6a3033b61082d9c330c79ef584cf37e6c6d0 (diff) | |
feat: allow hook-reported custom agent labels
Diffstat (limited to 'src/app')
| -rw-r--r-- | src/app/actions.rs | 77 | ||||
| -rw-r--r-- | src/app/api.rs | 30 | ||||
| -rw-r--r-- | src/app/mod.rs | 47 |
3 files changed, 76 insertions, 78 deletions
diff --git a/src/app/actions.rs b/src/app/actions.rs index ffb41ba1..3617bb07 100644 --- a/src/app/actions.rs +++ b/src/app/actions.rs @@ -53,20 +53,8 @@ fn notification_toast_for_state_change( } } -fn agent_label(agent: Agent) -> &'static str { - match agent { - crate::detect::Agent::Pi => "pi", - crate::detect::Agent::Claude => "claude", - crate::detect::Agent::Codex => "codex", - crate::detect::Agent::Gemini => "gemini", - crate::detect::Agent::Cursor => "cursor", - crate::detect::Agent::Cline => "cline", - crate::detect::Agent::OpenCode => "opencode", - crate::detect::Agent::GithubCopilot => "copilot", - crate::detect::Agent::Kimi => "kimi", - crate::detect::Agent::Droid => "droid", - crate::detect::Agent::Amp => "amp", - } +fn toast_agent_label(agent_label: &str) -> &str { + agent_label } fn notification_context( @@ -84,13 +72,15 @@ fn notification_context( context } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct PaneStateUpdate { pub pane_id: PaneId, pub ws_idx: usize, - pub previous_agent: Option<Agent>, + pub previous_agent_label: Option<String>, + pub previous_known_agent: Option<Agent>, pub previous_state: AgentState, - pub agent: Option<Agent>, + pub agent_label: Option<String>, + pub known_agent: Option<Agent>, pub state: AgentState, } @@ -497,12 +487,12 @@ impl AppState { AppEvent::HookStateReported { pane_id, source, - agent, + agent_label, state, message, } => self .update_pane_state(pane_id, |pane| { - pane.set_hook_authority(source, agent, state, message) + pane.set_hook_authority(source, agent_label, state, message) }) .into_iter() .collect(), @@ -513,9 +503,10 @@ impl AppState { AppEvent::HookAgentReleased { pane_id, source, - agent, + agent_label, + .. } => self - .update_pane_state(pane_id, |pane| pane.release_agent(&source, agent)) + .update_pane_state(pane_id, |pane| pane.release_agent(&source, &agent_label)) .into_iter() .collect(), // Intercepted in App::handle_internal_event before reaching this @@ -539,22 +530,25 @@ impl AppState { .find_map(|tab| tab.panes.get_mut(&pane_id))?; update(pane)? }; - self.apply_pane_state_change(ws_idx, pane_id, change); - Some(PaneStateUpdate { + let update = PaneStateUpdate { pane_id, ws_idx, - previous_agent: change.previous_agent, + previous_agent_label: change.previous_agent_label.clone(), + previous_known_agent: change.previous_known_agent, previous_state: change.previous_state, - agent: change.agent, + agent_label: change.agent_label.clone(), + known_agent: change.known_agent, state: change.state, - }) + }; + self.apply_pane_state_change(ws_idx, pane_id, &change); + Some(update) } fn apply_pane_state_change( &mut self, ws_idx: usize, pane_id: PaneId, - change: EffectiveStateChange, + change: &EffectiveStateChange, ) { let is_active_tab = self.pane_is_in_active_tab(ws_idx, pane_id); let Some(pane) = self.workspaces[ws_idx] @@ -571,7 +565,7 @@ impl AppState { pane.seen = false; } - if self.sound.allows(change.agent) { + if self.sound.allows(change.known_agent) { if let Some(sound) = notification_sound_for_state_change( is_active_tab, change.previous_state, @@ -582,8 +576,8 @@ impl AppState { } if self.toast_config.enabled { - if let (Some(agent), Some(kind)) = ( - change.agent, + if let (Some(agent_label), Some(kind)) = ( + change.agent_label.as_deref(), notification_toast_for_state_change( is_active_tab, change.previous_state, @@ -598,7 +592,7 @@ impl AppState { let context = notification_context(&self.workspaces[ws_idx], ws_idx, pane_id); self.toast = Some(ToastNotification { kind, - title: format!("{} {}", agent_label(agent), event_text), + title: format!("{} {}", toast_agent_label(agent_label), event_text), context, }); } @@ -927,6 +921,27 @@ mod tests { } #[test] + fn hook_reported_unknown_agent_sets_toast_title_from_label() { + let mut state = app_with_workspaces(&["active", "background"]); + state.active = Some(0); + state.toast_config.enabled = true; + let bg_pane_id = *state.workspaces[1].panes.keys().next().unwrap(); + + state.handle_app_event(AppEvent::HookStateReported { + pane_id: bg_pane_id, + source: "custom:hermes".into(), + agent_label: "hermes".into(), + state: AgentState::Blocked, + message: None, + }); + + let toast = state.toast.as_ref().unwrap(); + assert_eq!(toast.kind, ToastKind::NeedsAttention); + assert_eq!(toast.title, "hermes needs attention"); + assert_eq!(toast.context, "background ยท 2"); + } + + #[test] fn background_idle_sets_finished_toast() { let mut state = app_with_workspaces(&["active", "background"]); state.active = Some(0); diff --git a/src/app/api.rs b/src/app/api.rs index 1e969b64..2ed224c7 100644 --- a/src/app/api.rs +++ b/src/app/api.rs @@ -1,10 +1,21 @@ //! API request handling and response building for [`App`]. use super::{ - agent_name, detect_state_from_api, encode_api_keys, encode_api_text, pane_agent_status, - parse_agent_name, tab_attention_priority, App, Mode, + detect_state_from_api, encode_api_keys, encode_api_text, pane_agent_status, + tab_attention_priority, App, Mode, }; +fn normalize_reported_agent_label(agent: &str) -> Option<String> { + let trimmed = agent.trim(); + if trimmed.is_empty() { + return None; + } + if let Some(agent) = crate::detect::parse_agent_label(trimmed) { + return Some(crate::detect::agent_label(agent).to_string()); + } + Some(trimmed.to_string()) +} + pub(super) fn api_request_changes_ui(request: &crate::api::schema::Request) -> bool { use crate::api::schema::Method; @@ -775,12 +786,12 @@ impl App { }) .unwrap(); }; - let Some(agent) = parse_agent_name(¶ms.agent) else { + let Some(agent_label) = normalize_reported_agent_label(¶ms.agent) else { return serde_json::to_string(&ErrorResponse { id: request.id, error: ErrorBody { code: "invalid_agent".into(), - message: format!("unsupported agent {}", params.agent), + message: "agent label must not be empty".into(), }, }) .unwrap(); @@ -788,7 +799,7 @@ impl App { self.handle_internal_event(crate::events::AppEvent::HookStateReported { pane_id, source: params.source, - agent, + agent_label, state: detect_state_from_api(params.state), message: params.message, }); @@ -828,12 +839,12 @@ impl App { }) .unwrap(); }; - let Some(agent) = parse_agent_name(¶ms.agent) else { + let Some(agent_label) = normalize_reported_agent_label(¶ms.agent) else { return serde_json::to_string(&ErrorResponse { id: request.id, error: ErrorBody { code: "invalid_agent".into(), - message: format!("unsupported agent {}", params.agent), + message: "agent label must not be empty".into(), }, }) .unwrap(); @@ -841,7 +852,8 @@ impl App { self.handle_internal_event(crate::events::AppEvent::HookAgentReleased { pane_id, source: params.source, - agent, + known_agent: crate::detect::parse_agent_label(&agent_label), + agent_label, }); SuccessResponse { id: request.id, @@ -1185,7 +1197,7 @@ impl App { cwd: runtime .and_then(|rt| rt.cwd()) .map(|cwd| cwd.display().to_string()), - agent: pane.detected_agent.map(agent_name), + agent: pane.effective_agent_label().map(str::to_string), agent_status: pane_agent_status(pane.state, pane.seen), revision: 0, }) diff --git a/src/app/mod.rs b/src/app/mod.rs index 11a034f9..7f821da2 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -795,8 +795,13 @@ impl App { } } - let released_agent = if let AppEvent::HookAgentReleased { pane_id, agent, .. } = &ev { - Some((*pane_id, *agent)) + let released_agent = if let AppEvent::HookAgentReleased { + pane_id, + known_agent, + .. + } = &ev + { + known_agent.map(|agent| (*pane_id, agent)) } else { None }; @@ -827,13 +832,13 @@ impl App { }; let workspace_id = self.public_workspace_id(update.ws_idx); - if update.previous_agent != update.agent { + if update.previous_agent_label != update.agent_label { self.emit_event(crate::api::schema::EventEnvelope { event: crate::api::schema::EventKind::PaneAgentDetected, data: crate::api::schema::EventData::PaneAgentDetected { pane_id: pane_id.clone(), workspace_id: workspace_id.clone(), - agent: update.agent.map(agent_name), + agent: update.agent_label.clone(), }, }); } @@ -1275,40 +1280,6 @@ fn pane_agent_status( } } -fn parse_agent_name(agent: &str) -> Option<crate::detect::Agent> { - match agent { - "pi" => Some(crate::detect::Agent::Pi), - "claude" => Some(crate::detect::Agent::Claude), - "codex" => Some(crate::detect::Agent::Codex), - "gemini" => Some(crate::detect::Agent::Gemini), - "cursor" => Some(crate::detect::Agent::Cursor), - "cline" => Some(crate::detect::Agent::Cline), - "opencode" => Some(crate::detect::Agent::OpenCode), - "copilot" => Some(crate::detect::Agent::GithubCopilot), - "kimi" => Some(crate::detect::Agent::Kimi), - "droid" => Some(crate::detect::Agent::Droid), - "amp" => Some(crate::detect::Agent::Amp), - _ => None, - } -} - -fn agent_name(agent: crate::detect::Agent) -> String { - match agent { - crate::detect::Agent::Pi => "pi", - crate::detect::Agent::Claude => "claude", - crate::detect::Agent::Codex => "codex", - crate::detect::Agent::Gemini => "gemini", - crate::detect::Agent::Cursor => "cursor", - crate::detect::Agent::Cline => "cline", - crate::detect::Agent::OpenCode => "opencode", - crate::detect::Agent::GithubCopilot => "copilot", - crate::detect::Agent::Kimi => "kimi", - crate::detect::Agent::Droid => "droid", - crate::detect::Agent::Amp => "amp", - } - .to_string() -} - #[cfg(test)] mod tests { use super::*; |
