aboutsummaryrefslogtreecommitdiff
path: root/src
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
parent318e6a3033b61082d9c330c79ef584cf37e6c6d0 (diff)
feat: allow hook-reported custom agent labels
Diffstat (limited to 'src')
-rw-r--r--src/app/actions.rs77
-rw-r--r--src/app/api.rs30
-rw-r--r--src/app/mod.rs47
-rw-r--r--src/detect.rs53
-rw-r--r--src/events.rs5
-rw-r--r--src/pane.rs126
-rw-r--r--src/workspace.rs50
7 files changed, 244 insertions, 144 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::*;
diff --git a/src/detect.rs b/src/detect.rs
index 178fe0a5..df5bbae4 100644
--- a/src/detect.rs
+++ b/src/detect.rs
@@ -32,6 +32,40 @@ pub enum Agent {
Amp,
}
+pub fn agent_label(agent: Agent) -> &'static str {
+ match agent {
+ Agent::Pi => "pi",
+ Agent::Claude => "claude",
+ Agent::Codex => "codex",
+ Agent::Gemini => "gemini",
+ Agent::Cursor => "cursor",
+ Agent::Cline => "cline",
+ Agent::OpenCode => "opencode",
+ Agent::GithubCopilot => "copilot",
+ Agent::Kimi => "kimi",
+ Agent::Droid => "droid",
+ Agent::Amp => "amp",
+ }
+}
+
+pub fn parse_agent_label(agent: &str) -> Option<Agent> {
+ let name = agent.trim().to_lowercase();
+ match name.as_str() {
+ "pi" => Some(Agent::Pi),
+ "claude" | "claude-code" => Some(Agent::Claude),
+ "codex" => Some(Agent::Codex),
+ "gemini" => Some(Agent::Gemini),
+ "cursor" => Some(Agent::Cursor),
+ "cline" => Some(Agent::Cline),
+ "opencode" | "open-code" => Some(Agent::OpenCode),
+ "copilot" | "github-copilot" | "ghcs" => Some(Agent::GithubCopilot),
+ "kimi" => Some(Agent::Kimi),
+ "droid" => Some(Agent::Droid),
+ "amp" | "amp-local" => Some(Agent::Amp),
+ _ => None,
+ }
+}
+
/// Identify which agent is running from the process name.
/// Returns `None` for plain shells or unrecognized programs.
pub fn identify_agent(process_name: &str) -> Option<Agent> {
@@ -624,6 +658,25 @@ mod tests {
}
#[test]
+ fn parse_known_agent_labels() {
+ assert_eq!(parse_agent_label("pi"), Some(Agent::Pi));
+ assert_eq!(parse_agent_label("claude"), Some(Agent::Claude));
+ assert_eq!(parse_agent_label("copilot"), Some(Agent::GithubCopilot));
+ assert_eq!(
+ parse_agent_label("github-copilot"),
+ Some(Agent::GithubCopilot)
+ );
+ assert_eq!(parse_agent_label("amp-local"), Some(Agent::Amp));
+ }
+
+ #[test]
+ fn agent_labels_use_display_names() {
+ assert_eq!(agent_label(Agent::Pi), "pi");
+ assert_eq!(agent_label(Agent::GithubCopilot), "copilot");
+ assert_eq!(agent_label(Agent::OpenCode), "opencode");
+ }
+
+ #[test]
fn identify_unknown_processes() {
assert_eq!(identify_agent("bash"), None);
assert_eq!(identify_agent("zsh"), None);
diff --git a/src/events.rs b/src/events.rs
index 52efef61..dd433d55 100644
--- a/src/events.rs
+++ b/src/events.rs
@@ -21,7 +21,7 @@ pub enum AppEvent {
HookStateReported {
pane_id: PaneId,
source: String,
- agent: Agent,
+ agent_label: String,
state: AgentState,
message: Option<String>,
},
@@ -34,7 +34,8 @@ pub enum AppEvent {
HookAgentReleased {
pane_id: PaneId,
source: String,
- agent: Agent,
+ agent_label: String,
+ known_agent: Option<Agent>,
},
/// A new version was downloaded and installed. Restart to use it.
UpdateReady { version: String },
diff --git a/src/pane.rs b/src/pane.rs
index 5e71e6c9..fb34b6ab 100644
--- a/src/pane.rs
+++ b/src/pane.rs
@@ -105,16 +105,18 @@ fn stabilize_agent_state(
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HookAuthority {
pub source: String,
- pub agent: Agent,
+ pub agent_label: String,
pub state: AgentState,
pub message: Option<String>,
}
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EffectiveStateChange {
- 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,
}
@@ -146,40 +148,36 @@ impl PaneState {
agent: Option<Agent>,
fallback_state: AgentState,
) -> Option<EffectiveStateChange> {
- let previous_agent = self.detected_agent;
+ let previous_agent_label = self.effective_agent_label().map(str::to_string);
+ let previous_known_agent = self.effective_known_agent();
let previous_state = self.state;
self.detected_agent = agent;
self.fallback_state = fallback_state;
- if self
- .hook_authority
- .as_ref()
- .is_some_and(|authority| Some(authority.agent) != self.detected_agent)
- {
- self.hook_authority = None;
- }
- self.recompute_effective_state(previous_agent, previous_state)
+ self.recompute_effective_state(previous_agent_label, previous_known_agent, previous_state)
}
pub fn set_hook_authority(
&mut self,
source: String,
- agent: Agent,
+ agent_label: String,
state: AgentState,
message: Option<String>,
) -> Option<EffectiveStateChange> {
- let previous_agent = self.detected_agent;
+ let previous_agent_label = self.effective_agent_label().map(str::to_string);
+ let previous_known_agent = self.effective_known_agent();
let previous_state = self.state;
self.hook_authority = Some(HookAuthority {
source,
- agent,
+ agent_label,
state,
message,
});
- self.recompute_effective_state(previous_agent, previous_state)
+ self.recompute_effective_state(previous_agent_label, previous_known_agent, previous_state)
}
pub fn clear_hook_authority(&mut self, source: Option<&str>) -> Option<EffectiveStateChange> {
- let previous_agent = self.detected_agent;
+ let previous_agent_label = self.effective_agent_label().map(str::to_string);
+ let previous_known_agent = self.effective_known_agent();
let previous_state = self.state;
let should_clear = self
.hook_authority
@@ -189,51 +187,75 @@ impl PaneState {
return None;
}
self.hook_authority = None;
- self.recompute_effective_state(previous_agent, previous_state)
+ self.recompute_effective_state(previous_agent_label, previous_known_agent, previous_state)
}
- pub fn release_agent(&mut self, source: &str, agent: Agent) -> Option<EffectiveStateChange> {
- if self.detected_agent != Some(agent) {
+ pub fn release_agent(
+ &mut self,
+ source: &str,
+ agent_label: &str,
+ ) -> Option<EffectiveStateChange> {
+ let Some(current_agent_label) = self.effective_agent_label() else {
+ return None;
+ };
+ if current_agent_label != agent_label {
return None;
}
- if self
- .hook_authority
- .as_ref()
- .is_some_and(|authority| authority.agent != agent || authority.source != source)
- {
+ if self.hook_authority.as_ref().is_some_and(|authority| {
+ authority.agent_label != agent_label || authority.source != source
+ }) {
return None;
}
- let previous_agent = self.detected_agent;
+ let previous_agent_label = self.effective_agent_label().map(str::to_string);
+ let previous_known_agent = self.effective_known_agent();
let previous_state = self.state;
self.detected_agent = None;
self.fallback_state = AgentState::Unknown;
self.hook_authority = None;
- self.recompute_effective_state(previous_agent, previous_state)
+ self.recompute_effective_state(previous_agent_label, previous_known_agent, previous_state)
+ }
+
+ pub fn effective_agent_label(&self) -> Option<&str> {
+ self.hook_authority
+ .as_ref()
+ .map(|authority| authority.agent_label.as_str())
+ .or_else(|| self.detected_agent.map(crate::detect::agent_label))
+ }
+
+ pub fn effective_known_agent(&self) -> Option<Agent> {
+ if let Some(authority) = &self.hook_authority {
+ return crate::detect::parse_agent_label(&authority.agent_label);
+ }
+ self.detected_agent
}
fn recompute_effective_state(
&mut self,
- previous_agent: Option<Agent>,
+ previous_agent_label: Option<String>,
+ previous_known_agent: Option<Agent>,
previous_state: AgentState,
) -> Option<EffectiveStateChange> {
let state = self
.hook_authority
.as_ref()
- .filter(|authority| Some(authority.agent) == self.detected_agent)
.map(|authority| authority.state)
.unwrap_or(self.fallback_state);
+ let agent_label = self.effective_agent_label().map(str::to_string);
+ let known_agent = self.effective_known_agent();
- if previous_agent == self.detected_agent && previous_state == state {
+ if previous_agent_label == agent_label && previous_state == state {
return None;
}
self.state = state;
Some(EffectiveStateChange {
- previous_agent,
+ previous_agent_label,
+ previous_known_agent,
previous_state,
- agent: self.detected_agent,
+ agent_label,
+ known_agent,
state,
})
}
@@ -3312,33 +3334,57 @@ mod tests {
fn hook_authority_overrides_fallback_for_same_agent() {
let mut pane = PaneState::new();
pane.set_detected_state(Some(Agent::Pi), AgentState::Idle);
- pane.set_hook_authority("herdr:pi".into(), Agent::Pi, AgentState::Working, None);
+ pane.set_hook_authority("herdr:pi".into(), "pi".into(), AgentState::Working, None);
assert_eq!(pane.detected_agent, Some(Agent::Pi));
assert_eq!(pane.fallback_state, AgentState::Idle);
+ assert_eq!(pane.effective_agent_label(), Some("pi"));
assert_eq!(pane.state, AgentState::Working);
}
#[test]
- fn hook_authority_clears_when_detected_agent_changes() {
+ fn hook_authority_can_override_with_unknown_agent_label() {
let mut pane = PaneState::new();
pane.set_detected_state(Some(Agent::Pi), AgentState::Idle);
- pane.set_hook_authority("herdr:pi".into(), Agent::Pi, AgentState::Working, None);
+ pane.set_hook_authority(
+ "herdr:custom".into(),
+ "hermes".into(),
+ AgentState::Working,
+ None,
+ );
+
+ assert_eq!(pane.detected_agent, Some(Agent::Pi));
+ assert_eq!(pane.effective_agent_label(), Some("hermes"));
+ assert_eq!(pane.effective_known_agent(), None);
+ assert_eq!(pane.state, AgentState::Working);
+ }
+
+ #[test]
+ fn hook_authority_survives_detected_agent_changes() {
+ let mut pane = PaneState::new();
+ pane.set_detected_state(Some(Agent::Pi), AgentState::Idle);
+ pane.set_hook_authority(
+ "herdr:custom".into(),
+ "hermes".into(),
+ AgentState::Working,
+ None,
+ );
pane.set_detected_state(None, AgentState::Unknown);
- assert!(pane.hook_authority.is_none());
+ assert!(pane.hook_authority.is_some());
assert_eq!(pane.detected_agent, None);
- assert_eq!(pane.state, AgentState::Unknown);
+ assert_eq!(pane.effective_agent_label(), Some("hermes"));
+ assert_eq!(pane.state, AgentState::Working);
}
#[test]
fn release_agent_clears_identity_immediately() {
let mut pane = PaneState::new();
pane.set_detected_state(Some(Agent::Pi), AgentState::Idle);
- pane.set_hook_authority("herdr:pi".into(), Agent::Pi, AgentState::Working, None);
+ pane.set_hook_authority("herdr:pi".into(), "pi".into(), AgentState::Working, None);
- pane.release_agent("herdr:pi", Agent::Pi);
+ pane.release_agent("herdr:pi", "pi");
assert!(pane.hook_authority.is_none());
assert_eq!(pane.detected_agent, None);
diff --git a/src/workspace.rs b/src/workspace.rs
index 23d9a8e1..30e968fe 100644
--- a/src/workspace.rs
+++ b/src/workspace.rs
@@ -289,20 +289,17 @@ impl Tab {
.pane_ids()
.iter()
.filter_map(|id| {
- let pane = self.panes.get(id);
- let agent = pane.and_then(|p| p.detected_agent)?;
- let state = pane.map(|p| p.state).unwrap_or(AgentState::Unknown);
- let seen = pane.map(|p| p.seen).unwrap_or(true);
- let agent_label = agent_name(agent).to_string();
+ let pane = self.panes.get(id)?;
+ let agent_label = pane.effective_agent_label()?.to_string();
Some(PaneDetail {
pane_id: *id,
tab_idx: self.number.saturating_sub(1),
tab_label: self.display_name(),
label: agent_label.clone(),
agent_label,
- agent: Some(agent),
- state,
- seen,
+ agent: pane.effective_known_agent(),
+ state: pane.state,
+ seen: pane.seen,
})
})
.collect()
@@ -740,22 +737,6 @@ fn pane_attention_priority(state: AgentState, seen: bool) -> u8 {
}
}
-fn agent_name(agent: Agent) -> &'static str {
- match agent {
- Agent::Pi => "pi",
- Agent::Claude => "claude",
- Agent::Codex => "codex",
- Agent::Gemini => "gemini",
- Agent::Cursor => "cursor",
- Agent::Cline => "cline",
- Agent::OpenCode => "opencode",
- Agent::GithubCopilot => "copilot",
- Agent::Kimi => "kimi",
- Agent::Droid => "droid",
- Agent::Amp => "amp",
- }
-}
-
pub fn derive_label_from_cwd(cwd: &Path) -> String {
if let Some(repo_root) = git_repo_root(cwd) {
if let Some(name) = repo_root.file_name().and_then(|n| n.to_str()) {
@@ -1091,4 +1072,25 @@ mod tests {
assert!(details.iter().any(|detail| detail.label == "main·pi"));
assert!(details.iter().any(|detail| detail.label == "logs·claude"));
}
+
+ #[test]
+ fn pane_details_include_hook_reported_unknown_agents() {
+ let mut ws = Workspace::test_new("test");
+ let root_pane = ws.tabs[0].root_pane;
+ ws.tabs[0]
+ .panes
+ .get_mut(&root_pane)
+ .unwrap()
+ .set_hook_authority(
+ "custom:hermes".into(),
+ "hermes".into(),
+ AgentState::Working,
+ None,
+ );
+
+ let details = ws.pane_details();
+ assert_eq!(details.len(), 1);
+ assert_eq!(details[0].agent_label, "hermes");
+ assert_eq!(details[0].agent, None);
+ }
}