aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/app')
-rw-r--r--src/app/actions.rs57
-rw-r--r--src/app/input.rs22
-rw-r--r--src/app/mod.rs85
3 files changed, 135 insertions, 29 deletions
diff --git a/src/app/actions.rs b/src/app/actions.rs
index 7d9e27fd..dc52b174 100644
--- a/src/app/actions.rs
+++ b/src/app/actions.rs
@@ -11,7 +11,7 @@ use crate::pane::EffectiveStateChange;
use super::state::{AppState, Mode, ToastKind, ToastNotification};
fn notification_sound_for_state_change(
- is_active_ws: bool,
+ is_active_tab: bool,
prev_state: AgentState,
new_state: AgentState,
) -> Option<crate::sound::Sound> {
@@ -21,7 +21,7 @@ fn notification_sound_for_state_change(
match new_state {
AgentState::Blocked => Some(crate::sound::Sound::Request),
- AgentState::Idle if prev_state != AgentState::Idle && !is_active_ws => {
+ AgentState::Idle if prev_state != AgentState::Idle && !is_active_tab => {
Some(crate::sound::Sound::Done)
}
_ => None,
@@ -29,11 +29,11 @@ fn notification_sound_for_state_change(
}
fn notification_toast_for_state_change(
- is_active_ws: bool,
+ is_active_tab: bool,
prev_state: AgentState,
new_state: AgentState,
) -> Option<ToastKind> {
- if is_active_ws || new_state == prev_state {
+ if is_active_tab || new_state == prev_state {
return None;
}
@@ -60,7 +60,11 @@ fn agent_label(agent: Agent) -> &'static str {
}
}
-fn notification_context(ws: &crate::workspace::Workspace, ws_idx: usize, pane_id: PaneId) -> String {
+fn notification_context(
+ ws: &crate::workspace::Workspace,
+ ws_idx: usize,
+ pane_id: PaneId,
+) -> String {
let mut context = format!("{} · {}", ws.display_name(), ws_idx + 1);
if ws.tabs.len() > 1 {
if let Some(tab_idx) = ws.find_tab_index_for_pane(pane_id) {
@@ -86,6 +90,18 @@ pub struct PaneStateUpdate {
// ---------------------------------------------------------------------------
impl AppState {
+ fn pane_is_in_active_tab(&self, ws_idx: usize, pane_id: PaneId) -> bool {
+ let Some(active_ws_idx) = self.active else {
+ return false;
+ };
+ if active_ws_idx != ws_idx {
+ return false;
+ }
+ self.workspaces[ws_idx]
+ .find_tab_index_for_pane(pane_id)
+ .is_some_and(|tab_idx| tab_idx == self.workspaces[ws_idx].active_tab)
+ }
+
pub fn switch_workspace(&mut self, idx: usize) {
if idx < self.workspaces.len() {
self.active = Some(idx);
@@ -380,7 +396,7 @@ impl AppState {
pane_id: PaneId,
change: EffectiveStateChange,
) {
- let is_active_ws = self.active == Some(ws_idx);
+ let is_active_tab = self.pane_is_in_active_tab(ws_idx, pane_id);
let Some(pane) = self.workspaces[ws_idx]
.tabs
.iter_mut()
@@ -391,14 +407,14 @@ impl AppState {
if change.state == AgentState::Idle
&& change.previous_state != AgentState::Idle
- && !is_active_ws
+ && !is_active_tab
{
pane.seen = false;
}
if self.sound.allows(change.agent) {
if let Some(sound) = notification_sound_for_state_change(
- is_active_ws,
+ is_active_tab,
change.previous_state,
change.state,
) {
@@ -410,7 +426,7 @@ impl AppState {
if let (Some(agent), Some(kind)) = (
change.agent,
notification_toast_for_state_change(
- is_active_ws,
+ is_active_tab,
change.previous_state,
change.state,
),
@@ -725,7 +741,28 @@ mod tests {
}
#[test]
- fn active_workspace_does_not_set_toast() {
+ fn background_tab_in_active_workspace_still_sets_toast() {
+ let mut state = app_with_workspaces(&["active"]);
+ state.active = Some(0);
+ state.toast_config.enabled = true;
+ state.workspaces[0].tabs[0].set_custom_name("main".into());
+ let second_tab = state.workspaces[0].test_add_tab(Some("logs"));
+ let bg_pane_id = state.workspaces[0].tabs[second_tab].root_pane;
+
+ state.handle_app_event(AppEvent::StateChanged {
+ pane_id: bg_pane_id,
+ agent: Some(Agent::Pi),
+ state: AgentState::Blocked,
+ });
+
+ let toast = state.toast.as_ref().unwrap();
+ assert_eq!(toast.kind, ToastKind::NeedsAttention);
+ assert_eq!(toast.title, "pi needs attention");
+ assert_eq!(toast.context, "active · 1 · logs");
+ }
+
+ #[test]
+ fn active_workspace_active_tab_does_not_set_toast() {
let mut state = app_with_workspaces(&["active"]);
state.active = Some(0);
state.toast_config.enabled = true;
diff --git a/src/app/input.rs b/src/app/input.rs
index 3b63f28f..7341059c 100644
--- a/src/app/input.rs
+++ b/src/app/input.rs
@@ -383,7 +383,9 @@ impl App {
let protocol = crate::input::KeyboardProtocol::from_kitty_flags(flags);
let bytes = crate::input::encode_terminal_key(key, protocol);
if matches!(key_event.code, KeyCode::Esc)
- || key_event.modifiers.contains(crossterm::event::KeyModifiers::ALT)
+ || key_event
+ .modifiers
+ .contains(crossterm::event::KeyModifiers::ALT)
{
debug!(
code = ?key_event.code,
@@ -1863,7 +1865,8 @@ impl AppState {
return None;
}
- if let Some((ws_idx, tab_idx, pane_id)) = self.agent_detail_target_at(mouse.row) {
+ if let Some((ws_idx, tab_idx, pane_id)) = self.agent_detail_target_at(mouse.row)
+ {
self.switch_workspace(ws_idx);
if let Some(ws) = self.workspaces.get_mut(ws_idx) {
ws.switch_tab(tab_idx);
@@ -2103,10 +2106,7 @@ impl AppState {
None
}
- fn agent_detail_target_at(
- &self,
- row: u16,
- ) -> Option<(usize, usize, crate::layout::PaneId)> {
+ fn agent_detail_target_at(&self, row: u16) -> Option<(usize, usize, crate::layout::PaneId)> {
if self.sidebar_collapsed {
return None;
}
@@ -2129,7 +2129,10 @@ impl AppState {
content.width,
total_h.saturating_sub(ws_h) as u16,
);
- if detail_area.height < 4 || row < detail_area.y + 3 || row >= detail_area.y + detail_area.height {
+ if detail_area.height < 4
+ || row < detail_area.y + 3
+ || row >= detail_area.y + detail_area.height
+ {
return None;
}
@@ -2993,7 +2996,10 @@ mod tests {
app.handle_mouse(mouse(MouseEventKind::Down(MouseButton::Left), 2, 14));
assert_eq!(app.state.workspaces[0].active_tab, 1);
- assert_eq!(app.state.workspaces[0].tabs[1].layout.focused(), second_pane);
+ assert_eq!(
+ app.state.workspaces[0].tabs[1].layout.focused(),
+ second_pane
+ );
assert_eq!(app.state.mode, Mode::Terminal);
}
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 311e5f82..202fa8c3 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -710,7 +710,9 @@ impl App {
workspace_id: self.public_workspace_id(ws_idx),
},
});
- if let Some(tab_id) = self.public_tab_id(ws_idx, self.state.workspaces[ws_idx].active_tab) {
+ if let Some(tab_id) =
+ self.public_tab_id(ws_idx, self.state.workspaces[ws_idx].active_tab)
+ {
self.emit_event(crate::api::schema::EventEnvelope {
event: crate::api::schema::EventKind::TabFocused,
data: crate::api::schema::EventData::TabFocused {
@@ -1402,7 +1404,12 @@ impl App {
})
.unwrap();
};
- let Some(tab_idx) = self.state.workspaces.get(ws_idx).and_then(|ws| ws.find_tab_index_for_pane(pane_id)) else {
+ let Some(tab_idx) = self
+ .state
+ .workspaces
+ .get(ws_idx)
+ .and_then(|ws| ws.find_tab_index_for_pane(pane_id))
+ else {
return serde_json::to_string(&ErrorResponse {
id: request.id,
error: ErrorBody {
@@ -1776,14 +1783,30 @@ impl App {
});
}
+ fn seed_cwd_from_workspace(&self, ws_idx: usize) -> Option<std::path::PathBuf> {
+ self.state.workspaces.get(ws_idx)?.resolved_identity_cwd()
+ }
+
+ fn workspace_creation_source(&self) -> Option<usize> {
+ if self.state.mode == Mode::Navigate
+ && self.state.workspaces.get(self.state.selected).is_some()
+ {
+ return Some(self.state.selected);
+ }
+
+ self.state.active.or_else(|| {
+ self.state
+ .workspaces
+ .get(self.state.selected)
+ .map(|_| self.state.selected)
+ })
+ }
+
/// Create a workspace with a real PTY (needs event_tx).
fn create_workspace(&mut self) {
let initial_cwd = self
- .state
- .active
- .and_then(|i| self.state.workspaces.get(i))
- .and_then(|ws| ws.focused_runtime())
- .and_then(|rt| rt.cwd())
+ .workspace_creation_source()
+ .and_then(|ws_idx| self.seed_cwd_from_workspace(ws_idx))
.or_else(|| std::env::current_dir().ok())
.unwrap_or_else(|| std::path::PathBuf::from("/"));
if let Err(e) = self.create_workspace_with_options(initial_cwd, true) {
@@ -1796,9 +1819,7 @@ impl App {
let initial_cwd = self
.state
.active
- .and_then(|i| self.state.workspaces.get(i))
- .and_then(|ws| ws.focused_runtime())
- .and_then(|rt| rt.cwd())
+ .and_then(|ws_idx| self.seed_cwd_from_workspace(ws_idx))
.or_else(|| std::env::current_dir().ok())
.unwrap_or_else(|| std::path::PathBuf::from("/"));
if let Err(e) = self.create_tab_with_options(initial_cwd, true) {
@@ -1968,7 +1989,9 @@ impl App {
focused: self.state.active == Some(index),
pane_count: ws.public_pane_numbers.len(),
tab_count: ws.tabs.len(),
- active_tab_id: self.public_tab_id(index, ws.active_tab).unwrap_or_else(|| format!("{}:{}", index + 1, ws.active_tab + 1)),
+ active_tab_id: self
+ .public_tab_id(index, ws.active_tab)
+ .unwrap_or_else(|| format!("{}:{}", index + 1, ws.active_tab + 1)),
agent_state: pane_agent_state(agg_state),
}
}
@@ -2061,6 +2084,20 @@ fn agent_name(agent: crate::detect::Agent) -> String {
#[cfg(test)]
mod tests {
use super::*;
+ use crate::config::Config;
+ use crate::workspace::Workspace;
+
+ fn test_app() -> App {
+ let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
+ App::new(
+ &Config::default(),
+ true,
+ None,
+ None,
+ api_rx,
+ crate::api::EventHub::default(),
+ )
+ }
#[tokio::test]
async fn raw_input_waits_when_reader_is_gone() {
@@ -2089,4 +2126,30 @@ mod tests {
assert!(!api_request_changes_ui(&read_only));
assert!(api_request_changes_ui(&mutating));
}
+
+ #[test]
+ fn workspace_creation_in_navigate_mode_uses_selected_workspace_seed_cwd() {
+ let mut app = test_app();
+ let mut first = Workspace::test_new("herdr");
+ let first_root = first.tabs[0].root_pane;
+ first.tabs[0]
+ .pane_cwds
+ .insert(first_root, std::path::PathBuf::from("/tmp/herdr"));
+ let mut second = Workspace::test_new("pion");
+ let second_root = second.tabs[0].root_pane;
+ second.tabs[0]
+ .pane_cwds
+ .insert(second_root, std::path::PathBuf::from("/tmp/pion"));
+
+ app.state.workspaces = vec![first, second];
+ app.state.active = Some(0);
+ app.state.selected = 1;
+ app.state.mode = Mode::Navigate;
+
+ let ws_idx = app.workspace_creation_source().unwrap();
+ let seed_cwd = app.seed_cwd_from_workspace(ws_idx).unwrap();
+
+ assert_eq!(ws_idx, 1);
+ assert_eq!(seed_cwd, std::path::PathBuf::from("/tmp/pion"));
+ }
}