aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-02 21:20:28 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-02 21:25:32 +0300
commit295b09ca25edbccac65401ee84fa420dd3888aad (patch)
treed54ed875518e042869ed3f43e95f0b9ee59f5d34 /src/app
parent48641e253f1aa3886b470cbebec60103247a7622 (diff)
feat: make claude codex opencode hooks session-only
Diffstat (limited to 'src/app')
-rw-r--r--src/app/actions.rs119
-rw-r--r--src/app/api.rs3
-rw-r--r--src/app/api/panes.rs32
3 files changed, 125 insertions, 29 deletions
diff --git a/src/app/actions.rs b/src/app/actions.rs
index f6c76a36..95e634be 100644
--- a/src/app/actions.rs
+++ b/src/app/actions.rs
@@ -2041,17 +2041,38 @@ impl AppState {
custom_status,
seq,
session_ref,
+ } => {
+ if crate::agent_resume::is_reserved_native_state_source(&source, &agent_label) {
+ self.update_terminal_state(pane_id, |terminal| {
+ terminal.set_agent_session_ref(source, agent_label, session_ref, seq)
+ })
+ .into_iter()
+ .collect()
+ } else {
+ self.update_terminal_state(pane_id, |terminal| {
+ terminal.set_hook_authority_with_session_ref(
+ source,
+ agent_label,
+ state,
+ message,
+ custom_status,
+ session_ref,
+ seq,
+ )
+ })
+ .into_iter()
+ .collect()
+ }
+ }
+ AppEvent::AgentSessionReported {
+ pane_id,
+ source,
+ agent_label,
+ seq,
+ session_ref,
} => self
.update_terminal_state(pane_id, |terminal| {
- terminal.set_hook_authority_with_session_ref(
- source,
- agent_label,
- state,
- message,
- custom_status,
- session_ref,
- seq,
- )
+ terminal.set_agent_session_ref(source, agent_label, session_ref, seq)
})
.into_iter()
.collect(),
@@ -2106,12 +2127,17 @@ impl AppState {
agent_label,
seq,
..
- } => self
- .update_terminal_state(pane_id, |terminal| {
- terminal.release_agent_with_mutation(&source, &agent_label, seq)
- })
- .into_iter()
- .collect(),
+ } => {
+ if crate::agent_resume::is_reserved_native_state_source(&source, &agent_label) {
+ Vec::new()
+ } else {
+ self.update_terminal_state(pane_id, |terminal| {
+ terminal.release_agent_with_mutation(&source, &agent_label, seq)
+ })
+ .into_iter()
+ .collect()
+ }
+ }
// Intercepted in App::handle_internal_event before reaching this
// dispatch; never touches AppState.
AppEvent::ClipboardWrite { .. } => Vec::new(),
@@ -3603,20 +3629,20 @@ mod tests {
}
#[test]
- fn visible_idle_waits_before_overriding_claude_hook_working() {
- let mut state = app_with_workspaces(&["active", "background"]);
+ fn reserved_native_state_report_does_not_override_screen_state() {
+ let mut state = app_with_workspaces(&["active"]);
state.active = Some(0);
state.toast_config.delivery = crate::config::ToastDelivery::Herdr;
- let bg_pane_id = *state.workspaces[1].panes.keys().next().unwrap();
- let bg_terminal_id = state.workspaces[1]
+ let pane_id = *state.workspaces[0].panes.keys().next().unwrap();
+ let terminal_id = state.workspaces[0]
.panes
- .get(&bg_pane_id)
+ .get(&pane_id)
.unwrap()
.attached_terminal_id
.clone();
state.handle_app_event(AppEvent::StateChanged {
- pane_id: bg_pane_id,
+ pane_id,
agent: Some(Agent::Claude),
state: AgentState::Working,
visible_blocker: false,
@@ -3626,17 +3652,22 @@ mod tests {
observed_at: std::time::Instant::now(),
});
state.handle_app_event(AppEvent::HookStateReported {
- pane_id: bg_pane_id,
+ pane_id,
source: "herdr:claude".into(),
agent_label: "claude".into(),
- state: AgentState::Working,
+ state: AgentState::Blocked,
message: None,
custom_status: None,
seq: Some(1),
- session_ref: None,
+ session_ref: crate::agent_resume::AgentSessionRef::id("claude-session"),
});
+ let terminal = state.terminals.get(&terminal_id).unwrap();
+ assert_eq!(terminal.state, AgentState::Working);
+ assert!(terminal.hook_authority.is_none());
+ assert!(terminal.persisted_agent_session.is_some());
+
state.handle_app_event(AppEvent::StateChanged {
- pane_id: bg_pane_id,
+ pane_id,
agent: Some(Agent::Claude),
state: AgentState::Idle,
visible_blocker: false,
@@ -3646,12 +3677,46 @@ mod tests {
observed_at: std::time::Instant::now(),
});
- let terminal = state.terminals.get(&bg_terminal_id).unwrap();
- assert_eq!(terminal.state, AgentState::Working);
+ let terminal = state.terminals.get(&terminal_id).unwrap();
+ assert_eq!(terminal.state, AgentState::Idle);
assert!(state.toast.is_none());
}
#[test]
+ fn reserved_native_release_report_does_not_clear_screen_state() {
+ let mut state = app_with_workspaces(&["active"]);
+ let pane_id = *state.workspaces[0].panes.keys().next().unwrap();
+ let terminal_id = state.workspaces[0]
+ .panes
+ .get(&pane_id)
+ .unwrap()
+ .attached_terminal_id
+ .clone();
+
+ state.handle_app_event(AppEvent::StateChanged {
+ pane_id,
+ agent: Some(Agent::Claude),
+ state: AgentState::Working,
+ visible_blocker: false,
+ visible_idle: false,
+ visible_working: true,
+ process_exited: false,
+ observed_at: std::time::Instant::now(),
+ });
+ state.handle_app_event(AppEvent::HookAgentReleased {
+ pane_id,
+ source: "herdr:claude".into(),
+ agent_label: "claude".into(),
+ known_agent: Some(Agent::Claude),
+ seq: Some(1),
+ });
+
+ let terminal = state.terminals.get(&terminal_id).unwrap();
+ assert_eq!(terminal.state, AgentState::Working);
+ assert_eq!(terminal.detected_agent, Some(Agent::Claude));
+ }
+
+ #[test]
fn hidden_session_ref_only_update_marks_session_dirty_without_visible_update() {
let mut state = app_with_workspaces(&["active"]);
let pane_id = *state.workspaces[0].panes.keys().next().unwrap();
diff --git a/src/app/api.rs b/src/app/api.rs
index 1442012b..733fb8b8 100644
--- a/src/app/api.rs
+++ b/src/app/api.rs
@@ -562,6 +562,9 @@ impl App {
Method::PaneReportAgent(params) => {
return self.handle_pane_report_agent(request.id, params);
}
+ Method::PaneReportAgentSession(params) => {
+ return self.handle_pane_report_agent_session(request.id, params);
+ }
Method::PaneReportMetadata(params) => {
return self.handle_pane_report_metadata(request.id, params);
}
diff --git a/src/app/api/panes.rs b/src/app/api/panes.rs
index 0902ac39..d027f732 100644
--- a/src/app/api/panes.rs
+++ b/src/app/api/panes.rs
@@ -3,8 +3,9 @@ use bytes::Bytes;
use crate::api::schema::{
EventData, EventEnvelope, EventKind, PaneClearAgentAuthorityParams, PaneListParams,
PaneReadParams, PaneReadResult, PaneReleaseAgentParams, PaneRenameParams,
- PaneReportAgentParams, PaneReportMetadataParams, PaneSendInputParams, PaneSendKeysParams,
- PaneSendTextParams, PaneSplitParams, PaneTarget, ReadFormat, ReadSource, ResponseResult,
+ PaneReportAgentParams, PaneReportAgentSessionParams, PaneReportMetadataParams,
+ PaneSendInputParams, PaneSendKeysParams, PaneSendTextParams, PaneSplitParams, PaneTarget,
+ ReadFormat, ReadSource, ResponseResult,
};
use crate::app::{App, Mode};
@@ -200,6 +201,33 @@ impl App {
encode_success(id, ResponseResult::Ok {})
}
+ pub(super) fn handle_pane_report_agent_session(
+ &mut self,
+ id: String,
+ params: PaneReportAgentSessionParams,
+ ) -> String {
+ let Some((_ws_idx, pane_id)) = self.parse_pane_id(&params.pane_id) else {
+ return pane_not_found(id, &params.pane_id);
+ };
+ let Some(agent_label) = normalize_reported_agent_label(&params.agent) else {
+ return invalid_agent(id);
+ };
+ self.handle_internal_event(crate::events::AppEvent::AgentSessionReported {
+ pane_id,
+ session_ref: crate::agent_resume::session_ref_from_report(
+ &params.source,
+ &agent_label,
+ params.agent_session_id,
+ params.agent_session_path,
+ ),
+ source: params.source,
+ agent_label,
+ seq: params.seq,
+ });
+
+ encode_success(id, ResponseResult::Ok {})
+ }
+
pub(super) fn handle_pane_report_metadata(
&mut self,
id: String,