aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-02 01:23:10 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-02 01:23:10 +0300
commit45005f1f733a96e3d9e6748577c01ca2c594304b (patch)
tree58710dd980f8fa42d8df6f0c04290dc5e23c948d /src
parent9ae93b125249dead6169bdc505c11e0f43309e40 (diff)
fix: mark visible done panes seen on terminal focus
Diffstat (limited to 'src')
-rw-r--r--src/app/actions.rs51
-rw-r--r--src/app/mod.rs41
-rw-r--r--src/app/runtime.rs2
-rw-r--r--src/server/headless.rs3
4 files changed, 92 insertions, 5 deletions
diff --git a/src/app/actions.rs b/src/app/actions.rs
index 83e9bec6..0b187fed 100644
--- a/src/app/actions.rs
+++ b/src/app/actions.rs
@@ -179,6 +179,28 @@ impl AppState {
}
}
+ pub(crate) fn mark_active_tab_seen(&mut self) -> bool {
+ let Some(ws_idx) = self.active else {
+ return false;
+ };
+ let Some(tab) = self
+ .workspaces
+ .get_mut(ws_idx)
+ .and_then(crate::workspace::Workspace::active_tab_mut)
+ else {
+ return false;
+ };
+
+ let mut changed = false;
+ for pane in tab.panes.values_mut() {
+ if !pane.seen {
+ pane.seen = true;
+ changed = true;
+ }
+ }
+ changed
+ }
+
pub fn next_workspace(&mut self) {
if !self.workspaces.is_empty() {
let current = self.active.unwrap_or(self.selected);
@@ -591,10 +613,10 @@ impl AppState {
return;
};
- if is_background_completion_transition(change.previous_state, change.state)
- && !suppress_active_tab_notifications
- {
- pane.seen = false;
+ if change.state != AgentState::Idle {
+ pane.seen = true;
+ } else if is_background_completion_transition(change.previous_state, change.state) {
+ pane.seen = suppress_active_tab_notifications;
}
if self.local_sound_playback && self.sound.allows(change.known_agent) {
@@ -914,6 +936,27 @@ mod tests {
}
#[test]
+ fn active_tab_completion_marks_pane_seen() {
+ let mut state = app_with_workspaces(&["active"]);
+ state.active = Some(0);
+ state.outer_terminal_focus = Some(true);
+ let pane_id = *state.workspaces[0].panes.keys().next().unwrap();
+ let pane = state.workspaces[0].panes.get_mut(&pane_id).unwrap();
+ pane.state = AgentState::Working;
+ pane.seen = false;
+
+ state.handle_app_event(AppEvent::StateChanged {
+ pane_id,
+ agent: Some(Agent::Pi),
+ state: AgentState::Idle,
+ });
+
+ let pane = state.workspaces[0].panes.get(&pane_id).unwrap();
+ assert_eq!(pane.state, AgentState::Idle);
+ assert!(pane.seen);
+ }
+
+ #[test]
fn initial_idle_in_background_stays_seen() {
let mut state = app_with_workspaces(&["active", "background"]);
state.active = Some(0);
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 9e437e62..1a287a38 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -1153,6 +1153,47 @@ mod tests {
}
#[tokio::test]
+ async fn outer_focus_gained_marks_visible_done_panes_seen() {
+ let mut app = test_app();
+ let mut workspace = Workspace::test_new("test");
+ let root_pane = workspace.tabs[0].root_pane;
+ let split_pane = workspace.test_split(ratatui::layout::Direction::Horizontal);
+ let background_tab = workspace.test_add_tab(Some("background"));
+ let background_pane = workspace.tabs[background_tab].root_pane;
+
+ workspace.tabs[0].panes.get_mut(&root_pane).unwrap().state = AgentState::Idle;
+ workspace.tabs[0].panes.get_mut(&root_pane).unwrap().seen = false;
+ workspace.tabs[0].panes.get_mut(&split_pane).unwrap().state = AgentState::Idle;
+ workspace.tabs[0].panes.get_mut(&split_pane).unwrap().seen = false;
+ workspace.tabs[background_tab]
+ .panes
+ .get_mut(&background_pane)
+ .unwrap()
+ .state = AgentState::Idle;
+ workspace.tabs[background_tab]
+ .panes
+ .get_mut(&background_pane)
+ .unwrap()
+ .seen = false;
+
+ app.state.workspaces = vec![workspace];
+ app.state.active = Some(0);
+ app.state.selected = 0;
+ app.state.mode = Mode::Terminal;
+ app.state.outer_terminal_focus = Some(false);
+
+ let handled = app
+ .handle_raw_input_event(crate::raw_input::RawInputEvent::OuterFocusGained)
+ .await;
+
+ assert!(handled);
+ assert_eq!(app.state.outer_terminal_focus, Some(true));
+ assert!(app.state.workspaces[0].tabs[0].panes[&root_pane].seen);
+ assert!(app.state.workspaces[0].tabs[0].panes[&split_pane].seen);
+ assert!(!app.state.workspaces[0].tabs[background_tab].panes[&background_pane].seen);
+ }
+
+ #[tokio::test]
async fn repeat_key_events_are_ignored_outside_terminal_mode() {
let mut app = test_app();
app.state.mode = Mode::ReleaseNotes;
diff --git a/src/app/runtime.rs b/src/app/runtime.rs
index 094b26f1..27c061bd 100644
--- a/src/app/runtime.rs
+++ b/src/app/runtime.rs
@@ -91,7 +91,7 @@ impl App {
}
crate::raw_input::RawInputEvent::OuterFocusGained => {
self.state.outer_terminal_focus = Some(true);
- false
+ self.state.mark_active_tab_seen()
}
crate::raw_input::RawInputEvent::OuterFocusLost => {
self.state.outer_terminal_focus = Some(false);
diff --git a/src/server/headless.rs b/src/server/headless.rs
index b4e21320..d3e81a10 100644
--- a/src/server/headless.rs
+++ b/src/server/headless.rs
@@ -636,6 +636,9 @@ impl HeadlessServer {
self.effective_size = client.terminal_size;
self.app.state.outer_terminal_focus = client.outer_terminal_focus;
+ if client.outer_terminal_focus == Some(true) {
+ self.app.state.mark_active_tab_seen();
+ }
if !client.host_terminal_theme.is_empty() {
self.app.set_host_terminal_theme(client.host_terminal_theme);
}