fix: make workspace panels self contained
CI / rust (push) Successful in 2m54s

This commit is contained in:
2026-08-31 18:02:01 -07:00
parent 3f15b13312
commit 63c9cb6210
10 changed files with 297 additions and 350 deletions
+3 -2
View File
@@ -24,8 +24,9 @@ shared model retain the all-deterministic mode for like-for-like framework
comparison. GPUI feeds raw output through `lumbridge-terminal` and sends encoded
keyboard input and terminal protocol replies through the bounded runtime actor.
Its visual adapter coalesces VT cells into native styled runs, paints cursor
shapes, and resizes the engine and PTY from a responsive one/three/five-lane
middle 60% work band. Retained-history navigation is wired; text selection and
shapes, and resizes the engine and PTY from the middle 60% of a responsive
one/three/five-panel workspace. Every panel owns separate context and decision
regions. Retained-history navigation is wired; text selection and
mouse input remain intentionally unfinished.
Build independently:
+215 -275
View File
@@ -39,11 +39,11 @@ const SIDEBAR_WIDTH: f32 = 248.0;
const APP_HEADER_HEIGHT: f32 = 48.0;
const TAB_BAR_HEIGHT: f32 = 38.0;
const APP_FOOTER_HEIGHT: f32 = 32.0;
const TERMINAL_CHROME_HEIGHT: f32 = 72.0;
const TERMINAL_CONTENT_VERTICAL_INSET: f32 = 24.0;
const TERMINAL_HORIZONTAL_INSET: f32 = 32.0;
const TERMINAL_CELL_WIDTH: f32 = 8.4;
const TERMINAL_CELL_HEIGHT: f32 = 18.0;
const WORK_LANE_GAP: f32 = 4.0;
const WORK_PANEL_GAP: f32 = 4.0;
const TERMINAL_ROW_STEP: u16 = 2;
const TERMINAL_COLUMN_STEP: u16 = 10;
@@ -78,7 +78,6 @@ struct LumbridgeShell {
terminal_snapshot: TerminalSnapshot,
last_runtime_sequence: u64,
root_focus: FocusHandle,
pane_focus: [FocusHandle; 6],
}
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -119,10 +118,6 @@ impl LiveRuntimeStatus {
}
}
const fn is_fault(&self) -> bool {
matches!(self, Self::Fault(_))
}
const fn is_terminal(&self) -> bool {
matches!(self, Self::Exited(_) | Self::Fault(_))
}
@@ -280,14 +275,15 @@ fn dim_color(color: u32) -> u32 {
fn terminal_dimensions_for_window(window_size: Size<Pixels>) -> TerminalDimensions {
let width = f32::from(window_size.width);
let height = f32::from(window_size.height);
let lane_count = visible_lane_count(window_size);
let panel_count = visible_panel_count(window_size);
let workspace_height =
(height - APP_HEADER_HEIGHT - TAB_BAR_HEIGHT - APP_FOOTER_HEIGHT).max(0.0);
let terminal_height = (workspace_height * 0.60 - TERMINAL_CHROME_HEIGHT).max(0.0);
let terminal_height =
(workspace_height * 0.60 - TERMINAL_CONTENT_VERTICAL_INSET).max(0.0);
let workspace_width = (width - SIDEBAR_WIDTH).max(0.0);
let lane_gaps = WORK_LANE_GAP * lane_count.saturating_sub(1) as f32;
let lane_width = ((workspace_width - lane_gaps).max(0.0) / lane_count as f32).max(0.0);
let terminal_width = (lane_width - TERMINAL_HORIZONTAL_INSET).max(0.0);
let panel_gaps = WORK_PANEL_GAP * panel_count.saturating_sub(1) as f32;
let panel_width = ((workspace_width - panel_gaps).max(0.0) / panel_count as f32).max(0.0);
let terminal_width = (panel_width - TERMINAL_HORIZONTAL_INSET).max(0.0);
let rows = (terminal_height / TERMINAL_CELL_HEIGHT)
.floor()
.clamp(2.0, f32::from(u16::MAX));
@@ -303,7 +299,7 @@ fn terminal_dimensions_for_window(window_size: Size<Pixels>) -> TerminalDimensio
.expect("geometry clamps terminal dimensions above zero")
}
fn visible_lane_count(window_size: Size<Pixels>) -> usize {
fn visible_panel_count(window_size: Size<Pixels>) -> usize {
let workspace_width = (f32::from(window_size.width) - SIDEBAR_WIDTH).max(0.0);
if workspace_width >= 2_800.0 {
5
@@ -314,8 +310,12 @@ fn visible_lane_count(window_size: Size<Pixels>) -> usize {
}
}
fn visible_lane_range(total: usize, selected: usize, lane_count: usize) -> std::ops::Range<usize> {
let count = lane_count.clamp(1, total.max(1)).min(total);
fn visible_panel_range(
total: usize,
selected: usize,
panel_count: usize,
) -> std::ops::Range<usize> {
let count = panel_count.clamp(1, total.max(1)).min(total);
let start = selected
.saturating_sub(count / 2)
.min(total.saturating_sub(count));
@@ -363,11 +363,6 @@ impl RenderTiming {
impl LumbridgeShell {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let pane_focus = std::array::from_fn(|index| {
cx.focus_handle()
.tab_index(index as isize + 1)
.tab_stop(true)
});
let root_focus = cx.focus_handle();
window.focus(&root_focus);
@@ -433,7 +428,6 @@ impl LumbridgeShell {
terminal_snapshot,
last_runtime_sequence: 0,
root_focus,
pane_focus,
}
}
@@ -707,83 +701,6 @@ impl LumbridgeShell {
cx.notify();
}
fn pane_selector(
pane: &PaneState,
selected: bool,
runtime_status: LiveRuntimeStatus,
focus: FocusHandle,
cx: &mut Context<Self>,
) -> gpui::AnyElement {
let id = pane.id();
let needs_input = pane.needs_input();
let external = pane.output_source() == OutputSource::External;
let label = if external {
runtime_status.badge()
} else {
match pane.kind() {
SurfaceKind::Terminal => pane.status().label(),
_ => pane.fixture().badge,
}
};
let state_color = if needs_input || (external && runtime_status.is_fault()) {
ATTENTION
} else if matches!(pane.kind(), SurfaceKind::Terminal) {
SUCCESS
} else {
ACCENT
};
div()
.id(("pane", id.index()))
.track_focus(&focus)
.tab_index(id.index() as isize + 1)
.flex()
.items_center()
.justify_between()
.gap_3()
.h_full()
.min_w(px(172.0))
.px_3()
.bg(rgb(if selected { PANEL_ACTIVE } else { PANEL_ALT }))
.border_1()
.border_color(rgb(if selected || needs_input {
state_color
} else {
BORDER_QUIET
}))
.rounded(px(5.0))
.on_click(cx.listener(move |shell, _, window, cx| {
shell.select_pane(id, window, cx);
}))
.child(
div()
.flex()
.flex_col()
.min_w_0()
.child(
div()
.truncate()
.text_sm()
.text_color(rgb(TEXT))
.child(if external {
"Terminal · local shell"
} else {
pane.fixture().title
}),
)
.child(
div()
.mt_1()
.truncate()
.text_xs()
.text_color(rgb(MUTED))
.child(pane.fixture().target.to_owned()),
),
)
.child(div().text_xs().text_color(rgb(state_color)).child(label))
.into_any_element()
}
fn terminal_run(run: TerminalPaintRun) -> gpui::AnyElement {
let cursor = run.cursor;
div()
@@ -851,13 +768,7 @@ impl LumbridgeShell {
.into_any_element()
}
fn work_surface(
&self,
pane: &PaneState,
selected: bool,
cx: &mut Context<Self>,
) -> gpui::AnyElement {
let pane_id = pane.id();
fn pane_context(&self, pane: &PaneState, selected: bool) -> gpui::AnyElement {
let external = pane.output_source() == OutputSource::External;
let status = if external {
self.runtime_status.badge()
@@ -909,6 +820,90 @@ impl LumbridgeShell {
.child(label)
})
.collect::<Vec<_>>();
div()
.flex()
.flex_col()
.size_full()
.min_h_0()
.overflow_hidden()
.px_3()
.py_2()
.bg(rgb(if selected { PANEL_ACTIVE } else { PANEL_ALT }))
.border_b_1()
.border_color(rgb(BORDER))
.child(
div()
.flex()
.items_center()
.justify_between()
.gap_2()
.child(
div()
.min_w_0()
.child(
div()
.truncate()
.text_sm()
.text_color(rgb(TEXT))
.child(pane.fixture().title),
)
.child(
div()
.mt_1()
.truncate()
.text_xs()
.text_color(rgb(MUTED))
.child(detail),
),
)
.child(
div()
.flex_none()
.text_xs()
.text_color(rgb(if pane.needs_input() {
ATTENTION
} else if selected {
SUCCESS
} else {
MUTED
}))
.child(surface_status),
),
)
.child(
div()
.flex()
.h(px(28.0))
.flex_none()
.mt_2()
.overflow_hidden()
.border_t_1()
.border_color(rgb(BORDER_QUIET))
.children(tabs),
)
.child(
div()
.flex()
.items_center()
.justify_between()
.mt_2()
.text_xs()
.text_color(rgb(MUTED))
.child("TOOLS · CONTEXT · GOAL")
.child(if pane.needs_input() {
"NEEDS INPUT"
} else if selected {
"KEYBOARD OWNER"
} else {
"RUNNING"
}),
)
.into_any_element()
}
fn pane_work_surface(&self, pane: &PaneState) -> gpui::AnyElement {
let external = pane.output_source() == OutputSource::External;
let content = if external {
self.terminal_view()
} else {
@@ -927,63 +922,15 @@ impl LumbridgeShell {
};
div()
.id(("work-lane", pane_id.index()))
.on_click(cx.listener(move |shell, _, window, cx| {
shell.select_pane(pane_id, window, cx);
}))
.flex()
.flex_col()
.size_full()
.min_w_0()
.min_h_0()
.overflow_hidden()
.bg(rgb(PANEL))
.border_1()
.border_color(rgb(if selected { ACCENT } else { BORDER }))
.child(
div()
.flex()
.items_center()
.justify_between()
.h(px(36.0))
.flex_none()
.px_3()
.bg(rgb(PANEL_ALT))
.child(
div()
.flex()
.items_center()
.gap_2()
.min_w_0()
.text_sm()
.child(div().flex_none().child(pane.fixture().title))
.child(
div()
.truncate()
.text_xs()
.text_color(rgb(MUTED))
.child(detail),
),
)
.child(
div()
.flex_none()
.text_xs()
.text_color(rgb(if selected { SUCCESS } else { MUTED }))
.child(surface_status),
),
)
.child(
div()
.flex()
.h(px(28.0))
.flex_none()
.px_1()
.bg(rgb(PANEL_ALT))
.border_t_1()
.border_color(rgb(BORDER_QUIET))
.children(tabs),
)
.bg(rgb(BG))
.border_y_1()
.border_color(rgb(BORDER))
.child(
div()
.flex_1()
@@ -995,22 +942,36 @@ impl LumbridgeShell {
.into_any_element()
}
fn decision_region(&self) -> gpui::AnyElement {
let choice = |label: &'static str, detail: &'static str| {
fn pane_decision_region(&self, pane: &PaneState) -> gpui::AnyElement {
let choice = |label: &'static str, detail: &'static str, attention: bool| {
div()
.flex()
.flex_col()
.min_w(px(200.0))
.px_3()
.py_2()
.rounded(px(5.0))
.items_center()
.justify_between()
.min_w_0()
.px_2()
.py_1()
.rounded(px(4.0))
.border_1()
.border_color(rgb(BORDER))
.border_color(rgb(if attention { ATTENTION } else { BORDER }))
.bg(rgb(PANEL_ALT))
.text_sm()
.text_xs()
.text_color(rgb(TEXT))
.child(label)
.child(div().mt_1().text_xs().text_color(rgb(MUTED)).child(detail))
.child(div().truncate().text_color(rgb(MUTED)).child(detail))
};
let choices = if pane.needs_input() {
[
("Review request", "inspect scope", true),
("Steer…", "edit response", false),
("Dismiss", "leave inert", false),
]
} else {
[
("Continue", "keep moving", false),
("Review plan", "inspect commands", false),
("Ask…", "refine prompt", false),
]
};
div()
.flex()
@@ -1018,9 +979,11 @@ impl LumbridgeShell {
.size_full()
.min_h_0()
.overflow_hidden()
.px_3()
.px_2()
.py_2()
.bg(rgb(PANEL))
.border_t_1()
.border_color(rgb(BORDER))
.child(
div()
.flex()
@@ -1030,23 +993,78 @@ impl LumbridgeShell {
.child(
div()
.text_color(rgb(MUTED))
.child("DECISION SHELF · answer, choices, approvals"),
.child("DECISION SHELF"),
)
.child(
div()
.text_color(rgb(ATTENTION))
.child("LOCAL ANALYST OFF · suggestions inert"),
.text_color(rgb(if pane.needs_input() {
ATTENTION
} else {
MUTED
}))
.child(if pane.needs_input() {
"REVIEW REQUIRED"
} else {
"SUGGESTIONS INERT"
}),
),
)
.child(
div()
.flex()
.items_center()
.gap_2()
.flex_col()
.gap_1()
.mt_2()
.child(choice("Continue", "Keep the selected agent moving"))
.child(choice("Review plan", "Inspect capability-scoped commands"))
.child(choice("Ask…", "Refine before any action")),
.children(choices.map(|(label, detail, attention)| {
choice(label, detail, attention)
})),
)
.into_any_element()
}
fn workspace_panel(
&self,
pane: &PaneState,
selected: bool,
cx: &mut Context<Self>,
) -> gpui::AnyElement {
let pane_id = pane.id();
div()
.id(("workspace-panel", pane_id.index()))
.on_click(cx.listener(move |shell, _, window, cx| {
shell.select_pane(pane_id, window, cx);
}))
.flex()
.flex_col()
.size_full()
.min_w_0()
.min_h_0()
.overflow_hidden()
.border_1()
.border_color(rgb(if selected { ACCENT } else { BORDER }))
.child(
div()
.h(relative(0.20))
.flex_none()
.min_h_0()
.overflow_hidden()
.child(self.pane_context(pane, selected)),
)
.child(
div()
.h(relative(0.60))
.flex_none()
.min_h_0()
.overflow_hidden()
.child(self.pane_work_surface(pane)),
)
.child(
div()
.h(relative(0.20))
.flex_none()
.min_h_0()
.overflow_hidden()
.child(self.pane_decision_region(pane)),
)
.into_any_element()
}
@@ -1140,6 +1158,7 @@ impl LumbridgeShell {
impl Render for LumbridgeShell {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let attention = self.model.pane(PaneId::ClaudeUi);
let panel_count = visible_panel_count(window.bounds().size);
let sidebar = div()
.flex()
.flex_col()
@@ -1303,117 +1322,38 @@ impl Render for LumbridgeShell {
.px_3()
.text_xs()
.text_color(rgb(MUTED))
.child("1 interactive VT · 5 deterministic · 1 waiting"),
.child(format!(
"{panel_count} visible panels · 1 interactive VT · 1 waiting"
)),
);
let pane_selectors = self
.model
.panes()
.iter()
.map(|pane| {
Self::pane_selector(
pane,
self.model.selected_pane() == pane.id(),
self.runtime_status.clone(),
self.pane_focus[pane.id().index()].clone(),
cx,
)
})
.collect::<Vec<_>>();
let selected = self.model.pane(self.model.selected_pane());
let lane_count = visible_lane_count(window.bounds().size);
let lane_range = visible_lane_range(
let panel_range = visible_panel_range(
PaneId::ALL.len(),
self.model.selected_pane().index(),
lane_count,
panel_count,
);
let work_lanes = lane_range
let workspace_panels = panel_range
.map(|index| {
let pane = self.model.pane(PaneId::ALL[index]);
div()
.flex_1()
.min_w_0()
.min_h_0()
.child(self.work_surface(
.child(self.workspace_panel(
pane,
pane.id() == self.model.selected_pane(),
cx,
))
})
.collect::<Vec<_>>();
let workspace_stack = div()
let workspace_row = div()
.flex()
.flex_col()
.gap_1()
.flex_1()
.min_w_0()
.min_h_0()
.overflow_hidden()
.child(
div()
.h(relative(0.20))
.flex_none()
.flex()
.flex_col()
.min_h_0()
.overflow_hidden()
.px_3()
.py_2()
.bg(rgb(PANEL))
.child(
div()
.flex()
.items_center()
.justify_between()
.text_xs()
.child(
div()
.text_color(rgb(MUTED))
.child("CONTEXT · panes, agents, tools, goal"),
)
.child(
div()
.text_color(rgb(if selected.needs_input() {
ATTENTION
} else {
SUCCESS
}))
.child(if selected.needs_input() {
"NEEDS INPUT · decision below"
} else {
"WORKSPACE HEALTHY"
}),
),
)
.child(
div()
.flex()
.items_center()
.gap_2()
.mt_2()
.flex_1()
.min_h_0()
.overflow_hidden()
.children(pane_selectors),
),
)
.child(
div()
.h(relative(0.60))
.flex_none()
.flex()
.gap_1()
.min_h_0()
.overflow_hidden()
.children(work_lanes),
)
.child(
div()
.h(relative(0.20))
.flex_none()
.min_h_0()
.overflow_hidden()
.child(self.decision_region()),
);
.children(workspace_panels);
let counters = self.model.counters();
let footer_left = format!(
@@ -1511,7 +1451,7 @@ impl Render for LumbridgeShell {
.min_h_0()
.overflow_hidden()
.child(tabs)
.child(workspace_stack),
.child(workspace_row),
),
)
.child(
@@ -1658,7 +1598,7 @@ mod tests {
use super::{
KeyModifiers, TerminalEngine, TerminalEngineOptions, TerminalKey, TerminalScroll,
indexed_terminal_color, terminal_dimensions_for_window, terminal_key_from_parts,
terminal_paint_rows, terminal_scroll_from_parts, visible_lane_count, visible_lane_range,
terminal_paint_rows, terminal_scroll_from_parts, visible_panel_count, visible_panel_range,
};
use gpui::{px, size};
@@ -1685,24 +1625,24 @@ mod tests {
}
#[test]
fn terminal_geometry_tracks_the_middle_sixty_percent() {
fn terminal_geometry_tracks_middle_sixty_percent_per_panel() {
let dimensions = terminal_dimensions_for_window(size(px(1500.0), px(960.0)));
assert_eq!(dimensions.rows(), 24);
assert_eq!(dimensions.rows(), 26);
assert_eq!(dimensions.columns(), 45);
let ultrawide = size(px(3440.0), px(1440.0));
assert_eq!(visible_lane_count(ultrawide), 5);
assert_eq!(visible_panel_count(ultrawide), 5);
let dimensions = terminal_dimensions_for_window(ultrawide);
assert_eq!(dimensions.rows(), 40);
assert_eq!(dimensions.rows(), 42);
assert_eq!(dimensions.columns(), 71);
}
#[test]
fn lane_window_keeps_the_selected_pane_visible() {
assert_eq!(visible_lane_range(6, 0, 5), 0..5);
assert_eq!(visible_lane_range(6, 5, 5), 1..6);
assert_eq!(visible_lane_range(6, 3, 3), 2..5);
assert_eq!(visible_lane_range(6, 4, 1), 4..5);
fn panel_window_keeps_the_selected_pane_visible() {
assert_eq!(visible_panel_range(6, 0, 5), 0..5);
assert_eq!(visible_panel_range(6, 5, 5), 1..6);
assert_eq!(visible_panel_range(6, 3, 3), 2..5);
assert_eq!(visible_panel_range(6, 4, 1), 4..5);
}
#[test]