aboutsummaryrefslogtreecommitdiff
path: root/src/ui
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-17 01:41:16 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-17 01:41:16 +0300
commita36da363f14639085bec91d7a378d6c2eeaa6c58 (patch)
tree2c86155b115f8c79db2b8826fd8b2199d153f7bf /src/ui
parente4de58b37f58d59d146158239ed218a8accd5c3c (diff)
refactor: extract terminal state and runtime from panes
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/mobile.rs6
-rw-r--r--src/ui/panes.rs45
-rw-r--r--src/ui/scrollbar.rs2
-rw-r--r--src/ui/sidebar.rs38
4 files changed, 56 insertions, 35 deletions
diff --git a/src/ui/mobile.rs b/src/ui/mobile.rs
index 9e8c57e9..8bcdf184 100644
--- a/src/ui/mobile.rs
+++ b/src/ui/mobile.rs
@@ -266,7 +266,7 @@ fn render_header_status(app: &AppState, frame: &mut Frame, area: Rect) {
return;
};
- let (state, seen) = ws.aggregate_state();
+ let (state, seen) = ws.aggregate_state(&app.terminals);
let (dot, dot_style) = if matches!(state, AgentState::Working) {
(
super::spinner_frame(app.spinner_tick),
@@ -430,7 +430,7 @@ fn render_mobile_switcher_content(app: &AppState, frame: &mut Frame, viewport: R
let active = Some(idx) == app.active;
let selected = idx == app.selected;
let bg = mobile_item_bg(selected, active, p);
- let (state, seen) = ws.aggregate_state();
+ let (state, seen) = ws.aggregate_state(&app.terminals);
let (dot, dot_style) = state_dot(state, seen, p);
let title = Line::from(vec![
Span::styled(" ", Style::default().bg(bg)),
@@ -824,7 +824,7 @@ fn agent_priority_label(app: &AppState) -> String {
let mut blocked = 0usize;
let mut working = 0usize;
let mut done = 0usize;
- for detail in ws.pane_details() {
+ for detail in ws.pane_details(&app.terminals) {
match (detail.state, detail.seen) {
(AgentState::Blocked, _) => blocked += 1,
(AgentState::Working, _) => working += 1,
diff --git a/src/ui/panes.rs b/src/ui/panes.rs
index 289d5fab..c90183ea 100644
--- a/src/ui/panes.rs
+++ b/src/ui/panes.rs
@@ -11,9 +11,9 @@ use super::widgets::panel_contrast_fg;
use crate::app::state::Palette;
use crate::app::{AppState, Mode};
use crate::layout::PaneInfo;
-use crate::pane::PaneRuntime;
+use crate::terminal::TerminalRuntime;
-pub(crate) fn pane_is_scrolled_back(rt: &PaneRuntime) -> bool {
+pub(crate) fn pane_is_scrolled_back(rt: &TerminalRuntime) -> bool {
rt.scroll_metrics()
.is_some_and(|metrics| metrics.offset_from_bottom > 0)
}
@@ -55,7 +55,20 @@ fn stable_terminal_inner_rect(pane_inner: Rect) -> Rect {
)
}
-fn stable_scrollbar_gutter(rt: &PaneRuntime, pane_inner: Rect) -> (Rect, Option<Rect>) {
+fn runtime_for_tab_pane<'a>(
+ app: &'a AppState,
+ tab: &'a crate::workspace::Tab,
+ pane_id: crate::layout::PaneId,
+) -> Option<&'a TerminalRuntime> {
+ #[cfg(test)]
+ if let Some(runtime) = tab.runtimes.get(&pane_id) {
+ return Some(runtime);
+ }
+ let terminal_id = tab.terminal_id(pane_id)?;
+ app.terminal_runtimes.get(terminal_id)
+}
+
+fn stable_scrollbar_gutter(rt: &TerminalRuntime, pane_inner: Rect) -> (Rect, Option<Rect>) {
let inner_rect = stable_terminal_inner_rect(pane_inner);
if inner_rect == pane_inner {
return (inner_rect, None);
@@ -76,6 +89,7 @@ fn stable_scrollbar_gutter(rt: &PaneRuntime, pane_inner: Rect) -> (Rect, Option<
/// Resize every visible runtime in a tab to the geometry it would receive if the tab were selected.
pub(super) fn resize_tab_panes(
+ app: &AppState,
tab: &crate::workspace::Tab,
area: Rect,
cell_size: crate::kitty_graphics::HostCellSize,
@@ -84,7 +98,7 @@ pub(super) fn resize_tab_panes(
if tab.zoomed {
let focused_id = tab.layout.focused();
- if let Some(rt) = tab.runtimes.get(&focused_id) {
+ if let Some(rt) = runtime_for_tab_pane(app, tab, focused_id) {
let inner_rect = stable_terminal_inner_rect(area);
rt.resize(
inner_rect.height,
@@ -103,7 +117,7 @@ pub(super) fn resize_tab_panes(
area
};
- if let Some(rt) = tab.runtimes.get(&info.id) {
+ if let Some(rt) = runtime_for_tab_pane(app, tab, info.id) {
let inner_rect = stable_terminal_inner_rect(pane_inner);
rt.resize(
inner_rect.height,
@@ -136,7 +150,7 @@ pub(super) fn compute_pane_infos(
let focused_id = ws.layout.focused();
let mut inner_rect = area;
let mut scrollbar_rect = None;
- if let Some(rt) = ws.runtimes.get(&focused_id) {
+ if let Some(rt) = app.runtime_for_pane_in_workspace(ws_idx, focused_id) {
(inner_rect, scrollbar_rect) = stable_scrollbar_gutter(rt, area);
if resize_panes {
rt.resize(
@@ -175,7 +189,7 @@ pub(super) fn compute_pane_infos(
let mut inner_rect = pane_inner;
let mut scrollbar_rect = None;
- if let Some(rt) = ws.runtimes.get(&info.id) {
+ if let Some(rt) = app.runtime_for_pane_in_workspace(ws_idx, info.id) {
(inner_rect, scrollbar_rect) = stable_scrollbar_gutter(rt, pane_inner);
if resize_panes {
rt.resize(
@@ -208,7 +222,7 @@ pub(super) fn render_panes(app: &AppState, frame: &mut Frame, area: Rect) {
let terminal_active = app.mode == Mode::Terminal;
for info in &app.view.pane_infos {
- if let Some(rt) = ws.runtimes.get(&info.id) {
+ if let Some(rt) = app.runtime_for_pane_in_workspace(ws_idx, info.id) {
if multi_pane {
let (border_style, border_set) = if info.is_focused && terminal_active {
(
@@ -233,7 +247,10 @@ pub(super) fn render_panes(app: &AppState, frame: &mut Frame, area: Rect) {
.border_set(border_set);
if let Some(title) = ws
.pane_state(info.id)
- .and_then(|pane| pane.border_label(app.show_agent_labels_on_pane_borders))
+ .and_then(|pane| app.terminals.get(&pane.attached_terminal_id))
+ .and_then(|terminal| {
+ terminal.border_label(app.show_agent_labels_on_pane_borders)
+ })
.and_then(|label| pane_border_title(label, info.rect.width))
{
block = block.title(Line::from(Span::styled(title, border_style)));
@@ -360,7 +377,7 @@ fn render_empty(app: &AppState, frame: &mut Frame, area: Rect) {
#[cfg(test)]
mod tests {
use super::*;
- use crate::pane::PaneRuntime;
+ use crate::terminal::TerminalRuntime;
use crate::workspace::Workspace;
#[test]
@@ -381,7 +398,7 @@ mod tests {
let root_pane = workspace.tabs[0].root_pane;
workspace.tabs[0].runtimes.insert(
root_pane,
- PaneRuntime::test_with_scrollback_bytes(40, 8, 1024, b"ready\n"),
+ TerminalRuntime::test_with_scrollback_bytes(40, 8, 1024, b"ready\n"),
);
app.workspaces = vec![workspace];
app.active = Some(0);
@@ -408,7 +425,7 @@ mod tests {
let root_pane = workspace.tabs[0].root_pane;
workspace.tabs[0].runtimes.insert(
root_pane,
- PaneRuntime::test_with_scrollback_bytes(40, 8, 1024, b"ready\n"),
+ TerminalRuntime::test_with_scrollback_bytes(40, 8, 1024, b"ready\n"),
);
app.workspaces = vec![workspace];
app.active = Some(0);
@@ -434,7 +451,7 @@ mod tests {
let root_pane = workspace.tabs[0].root_pane;
workspace.tabs[0].runtimes.insert(
root_pane,
- PaneRuntime::test_with_scrollback_bytes(4, 8, 1024, b"ready\n"),
+ TerminalRuntime::test_with_scrollback_bytes(4, 8, 1024, b"ready\n"),
);
app.workspaces = vec![workspace];
app.active = Some(0);
@@ -460,7 +477,7 @@ mod tests {
let root_pane = workspace.tabs[0].root_pane;
workspace.tabs[0].runtimes.insert(
root_pane,
- PaneRuntime::test_with_scrollback_bytes(
+ TerminalRuntime::test_with_scrollback_bytes(
40,
8,
1024,
diff --git a/src/ui/scrollbar.rs b/src/ui/scrollbar.rs
index 34e739d0..b73b2c50 100644
--- a/src/ui/scrollbar.rs
+++ b/src/ui/scrollbar.rs
@@ -165,7 +165,7 @@ pub(super) fn render_pane_scrollbar(
app: &AppState,
frame: &mut Frame,
info: &PaneInfo,
- rt: &crate::pane::PaneRuntime,
+ rt: &crate::terminal::TerminalRuntime,
) {
let Some(metrics) = rt.scroll_metrics() else {
return;
diff --git a/src/ui/sidebar.rs b/src/ui/sidebar.rs
index 29f3525d..eb6d6fab 100644
--- a/src/ui/sidebar.rs
+++ b/src/ui/sidebar.rs
@@ -116,7 +116,7 @@ pub(crate) fn agent_panel_entries(app: &AppState) -> Vec<AgentPanelEntry> {
let Some(ws) = app.workspaces.get(ws_idx) else {
return Vec::new();
};
- ws.pane_details()
+ ws.pane_details(&app.terminals)
.into_iter()
.map(|detail| AgentPanelEntry {
ws_idx,
@@ -138,7 +138,7 @@ pub(crate) fn agent_panel_entries(app: &AppState) -> Vec<AgentPanelEntry> {
.flat_map(|(ws_idx, ws)| {
let multi_tab = ws.tabs.len() > 1;
let workspace_label = ws.display_name();
- ws.pane_details()
+ ws.pane_details(&app.terminals)
.into_iter()
.map(move |detail| AgentPanelEntry {
ws_idx,
@@ -429,7 +429,7 @@ pub(super) fn render_sidebar_collapsed(app: &AppState, frame: &mut Frame, area:
if y >= ws_area.y + ws_area.height {
break;
}
- let (agg_state, agg_seen) = ws.aggregate_state();
+ let (agg_state, agg_seen) = ws.aggregate_state(&app.terminals);
let (icon, icon_style) = state_dot(agg_state, agg_seen, p);
let is_selected = visible_idx == app.selected && is_navigating;
let is_active = Some(visible_idx) == app.active;
@@ -487,7 +487,7 @@ pub(super) fn render_sidebar_collapsed(app: &AppState, frame: &mut Frame, area:
if detail_content_area != Rect::default() {
if let Some(ws_idx) = detail_ws_idx {
if let Some(ws) = app.workspaces.get(ws_idx) {
- for (detail_idx, detail) in ws.pane_details().iter().enumerate() {
+ for (detail_idx, detail) in ws.pane_details(&app.terminals).iter().enumerate() {
let y = detail_content_area.y + detail_idx as u16;
if y >= detail_content_area.y + detail_content_area.height {
break;
@@ -603,7 +603,7 @@ fn render_workspace_list(app: &AppState, frame: &mut Frame, area: Rect, is_navig
let is_active = Some(i) == app.active;
let is_dragged = dragged_ws_idx == Some(i);
let highlighted = selected || is_active || is_dragged;
- let (agg_state, agg_seen) = ws.aggregate_state();
+ let (agg_state, agg_seen) = ws.aggregate_state(&app.terminals);
if highlighted {
let bg = if selected {
@@ -899,24 +899,28 @@ mod tests {
#[test]
fn all_workspaces_agent_panel_entries_use_workspace_and_optional_tab_labels() {
let mut app = crate::app::state::AppState::test_new();
- let mut first = Workspace::test_new("one");
+ let first = Workspace::test_new("one");
let first_pane = first.tabs[0].root_pane;
- first.tabs[0]
- .panes
- .get_mut(&first_pane)
- .unwrap()
- .detected_agent = Some(Agent::Pi);
-
let mut second = Workspace::test_new("two");
let second_tab = second.test_add_tab(Some("logs"));
let second_pane = second.tabs[second_tab].root_pane;
- second.tabs[second_tab]
- .panes
- .get_mut(&second_pane)
- .unwrap()
- .detected_agent = Some(Agent::Claude);
app.workspaces = vec![first, second];
+ app.ensure_test_terminals();
+ let first_terminal_id = app.workspaces[0].tabs[0].panes[&first_pane]
+ .attached_terminal_id
+ .clone();
+ app.terminals
+ .get_mut(&first_terminal_id)
+ .unwrap()
+ .detected_agent = Some(Agent::Pi);
+ let second_terminal_id = app.workspaces[1].tabs[second_tab].panes[&second_pane]
+ .attached_terminal_id
+ .clone();
+ app.terminals
+ .get_mut(&second_terminal_id)
+ .unwrap()
+ .detected_agent = Some(Agent::Claude);
app.active = Some(0);
app.selected = 0;
app.agent_panel_scope = AgentPanelScope::AllWorkspaces;