aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-04-16 22:12:44 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-04-16 22:17:09 +0300
commit0b831c86274486af9f9f7052c2534374b498bb3a (patch)
tree3e8130be85cb387c8c3e57591617c4c7be0a26ec /src/app
parent318e6a3033b61082d9c330c79ef584cf37e6c6d0 (diff)
feat: allow hook-reported custom agent labels
Diffstat (limited to 'src/app')
-rw-r--r--src/app/actions.rs77
-rw-r--r--src/app/api.rs30
-rw-r--r--src/app/mod.rs47
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(&params.agent) else {
+ let Some(agent_label) = normalize_reported_agent_label(&params.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(&params.agent) else {
+ let Some(agent_label) = normalize_reported_agent_label(&params.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::*;