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
+4 -2
View File
@@ -21,8 +21,10 @@ binary is still a scaffold, while the isolated native UI spikes now exercise an
interactive 20/60/20 workspace backed by six live comparison surfaces, and the interactive 20/60/20 workspace backed by six live comparison surfaces, and the
root workspace contains the first bounded local PTY, runtime actor, VT engine, root workspace contains the first bounded local PTY, runtime actor, VT engine,
and capability-gated workspace command boundaries. The GPUI slice shows a and capability-gated workspace command boundaries. The GPUI slice shows a
responsive one-, three-, or five-lane work band—five on a 3440 px ultrawide—with responsive one-, three-, or five-panel workspace—five on a 3440 px ultrawide—
one styled actor-owned VT session and five deterministic comparison surfaces. where every panel owns its own context, work surface, and decision shelf. One
panel contains a styled actor-owned VT session and five comparison surfaces
remain deterministic.
Retained-history navigation is wired. We are still validating terminal text Retained-history navigation is wired. We are still validating terminal text
selection and mouse input, standalone runtime IPC/durability, ACP integration, selection and mouse input, standalone runtime IPC/durability, ACP integration,
packaging, and usage-data contracts before a large implementation. packaging, and usage-data contracts before a large implementation.
+8 -8
View File
@@ -80,14 +80,14 @@ durability step. See decisions 0004 and 0005.
## Default workspace composition ## Default workspace composition
The default desktop workspace has three horizontal bands: the top 20% holds pane, The default desktop workspace is a responsive horizontal row of self-contained
agent, tool, context, and goal state; the middle 60% holds responsive vertical vertical panels: one panel in compact windows, three at normal desktop widths,
work lanes; and the bottom 20% is the answer, choice, chat, and approval shelf. and five on a 3440 px ultrawide. Inside every panel, the top 20% holds that pane's
The work region shows one lane in compact windows, three at normal desktop agent, tool, context, goal, target, and status; the middle 60% holds its terminal
widths, and five on a 3440 px ultrawide. The selected lane alone owns keyboard or work surface; and the bottom 20% holds that pane's answer, choices, chat, and
input. Six surfaces continue updating so performance comparisons remain approval boundary. The selected panel alone owns keyboard input. Six surfaces
meaningful, with a sliding visible window keeping the selected pane onscreen. continue updating so performance comparisons remain meaningful, with a sliding
See decisions 0008 and 0009. visible window keeping the selected pane onscreen. See decisions 0008 and 0009.
We should evaluate, not blindly copy, WezTerm, Zellij, RMUX, tmux, and cmux. The We should evaluate, not blindly copy, WezTerm, Zellij, RMUX, tmux, and cmux. The
first spike must compare a reusable terminal crate with a small first-party layer. first spike must compare a reusable terminal crate with a small first-party layer.
+7 -8
View File
@@ -66,15 +66,14 @@ surface change never silently launches a process, moves the pane to another
host, or changes which agent owns the session. Unsupported surfaces are shown as host, or changes which agent owns the session. Unsupported surfaces are shown as
unavailable rather than simulated. unavailable rather than simulated.
The default workspace has three horizontal bands: the top 20% shows context, The default workspace is a responsive row of self-contained vertical panels:
pane/agent state, tools, and goal; the middle 60% is a responsive set of vertical one panel in compact windows, three at normal desktop widths, and up to five on
work lanes; and the bottom 20% is the decision shelf. The work band shows one an ultrawide display. Every panel has its own 20% context region, 60% terminal or
lane in compact windows, three at normal desktop widths, and up to five on an work surface, and 20% decision shelf. One panel owns keyboard input at a time,
ultrawide display. One lane owns keyboard input at a time, and selecting a lane and selecting it never pauses the others. Explicit split trees and dashboard
never pauses the others. Explicit split trees and dashboard layouts remain layouts remain supported, but they do not displace this cmux-style default.
supported, but they do not displace this terminal-first default.
An optional decision shelf sits below the active work surface. It may show an An optional decision shelf sits below each pane's work surface. It may show an
answer draft, two or three concrete choices, why each was suggested, and the answer draft, two or three concrete choices, why each was suggested, and the
capability each choice would need. Suggestions are inert data. Choosing one may capability each choice would need. Suggestions are inert data. Choosing one may
prepare a typed workspace plan, but any command, file mutation, credential use, prepare a typed workspace plan, but any command, file mutation, credential use,
+2 -2
View File
@@ -118,8 +118,8 @@ who already have the final cargo-watch release installed.
blocked OSC 52 behavior, key encoding, application-cursor mode, and resize. blocked OSC 52 behavior, key encoding, application-cursor mode, and resize.
- `lumbridge-core` tests capability-gated split/select/rename/close commands, - `lumbridge-core` tests capability-gated split/select/rename/close commands,
idempotent request IDs, close-tree promotion, and atomic agentic setup plans. idempotent request IDs, close-tree promotion, and atomic agentic setup plans.
- The GPUI slice tests its key-event adapter, responsive one/three/five-lane - The GPUI slice tests its key-event adapter, responsive one/three/five-panel
geometry, selected-pane lane windowing, 20/60/20 geometry-to-PTY sizing, xterm geometry, selected-panel windowing, per-panel 20/60/20 PTY sizing, xterm
256-color conversion, styled-run coalescing, cursor-run boundaries, and 256-color conversion, styled-run coalescing, cursor-run boundaries, and
scrollback shortcut routing. It renders one real actor-owned VT session while scrollback shortcut routing. It renders one real actor-owned VT session while
five surfaces continue their deterministic background workload. five surfaces continue their deterministic background workload.
+6 -5
View File
@@ -24,8 +24,8 @@ from macOS and Linux and the hard gates pass.
The current programs establish dependency, build, launch, and interaction The current programs establish dependency, build, launch, and interaction
baselines. GPUI now routes a real PTY through a VT engine and keyboard encoder, baselines. GPUI now routes a real PTY through a VT engine and keyboard encoder,
paints coalesced styled cell runs and cursor shapes, and derives PTY rows/columns paints coalesced styled cell runs and cursor shapes, and derives PTY rows/columns
from its responsive one/three/five-lane 60% terminal band when the window from the 60% terminal region inside each responsive one/three/five-panel layout
changes. Retained-history page/top/bottom navigation is wired. Text selection, when the window changes. Retained-history page/top/bottom navigation is wired. Text selection,
mouse modes, a native Markdown editor, and one isolated browser child remain. mouse modes, a native Markdown editor, and one isolated browser child remain.
## Ubuntu baseline — metal, 2026-08-31 ## Ubuntu baseline — metal, 2026-08-31
@@ -87,9 +87,10 @@ fallback.
## Measurement semantics ## Measurement semantics
Both renderers retain the same all-deterministic six-surface action stream for Both renderers retain the same all-deterministic six-surface action stream for
comparison. GPUI now presents a 20/60/20 workspace whose middle band shows one, comparison. GPUI now presents one, three, or five vertical panels, each with its
three, or five vertical work lanes while one terminal fixture is replaced with a own 20/60/20 context/work/decision composition, while one terminal fixture is
real actor-owned VT session and five deterministic surfaces keep running. Counters replaced with a real actor-owned VT session and five deterministic surfaces keep
running. Counters
separate external PTY batches/lines from total model updates. The GPUI footer separate external PTY batches/lines from total model updates. The GPUI footer
reports dispatch-to-element-build p50/p95 over a bounded 256-sample window. It reports dispatch-to-element-build p50/p95 over a bounded 256-sample window. It
is deliberately not called key-to-present or frame-present latency: neither is deliberately not called key-to-present or frame-present latency: neither
+21 -20
View File
@@ -10,14 +10,14 @@ framework decision gate, not a decorative dashboard.
## Current-run audit ## Current-run audit
The first 2026-08-31 GPUI baseline proved six-surface density but gave every The first 2026-08-31 GPUI baseline proved six-surface density but gave every
pane equal visual priority. The current slice replaces that dashboard with three pane equal visual priority. The current slice replaces that dashboard with one,
horizontal bands: 20% context and pane selection, a 60% responsive work region, three, or five cmux-style vertical panels according to available width. Every
and a 20% decision shelf. The work region shows one, three, or five vertical panel owns its own 20% context region, 60% work surface, and 20% decision shelf.
lanes according to available width. Keyboard focus, command-palette input, live Keyboard focus, command-palette input, live PTY input, styled VT cells, cursor
PTY input, styled VT cells, cursor shapes, geometry-driven resize, and retained- shapes, geometry-driven resize, and retained-history navigation are wired.
history navigation are wired. Remaining visible gaps are terminal text selection Remaining visible gaps are terminal text selection and mouse modes, real
and mouse modes, real decision-shelf actions, usage-detail provenance, and end-to- decision-shelf actions, usage-detail provenance, and end-to-end platform
end platform accessibility/IME. accessibility/IME.
Screenshots for the audit are stored outside Git under Screenshots for the audit are stored outside Git under
`~/shots/2026-08/lumbridge-ui-audit/`. Accessibility and IME correctness cannot `~/shots/2026-08/lumbridge-ui-audit/`. Accessibility and IME correctness cannot
@@ -42,8 +42,8 @@ research, not source assets for Lumbridge. We adapt these interaction patterns:
cards; cards;
- a quiet top tab strip for mixed terminal, Markdown, browser, and review - a quiet top tab strip for mixed terminal, Markdown, browser, and review
surfaces; surfaces;
- a primary work region with a small responsive number of equal vertical lanes, - a small responsive number of self-contained vertical panels, instead of
instead of either one ultrawide terminal or a dense equal-card dashboard; either one ultrawide terminal or a dense equal-card dashboard;
- narrow contextual tools, such as files, review, or Buzz, that can collapse - narrow contextual tools, such as files, review, or Buzz, that can collapse
when the terminal needs the space; when the terminal needs the space;
- usage and agent state at the edge of the workspace rather than in modal - usage and agent state at the edge of the workspace rather than in modal
@@ -60,20 +60,21 @@ surface rather than a hosted Lumbridge control plane.
palette shortcut. palette shortcut.
2. The sidebar starts with a compact `ATTENTION` group, followed by workspaces 2. The sidebar starts with a compact `ATTENTION` group, followed by workspaces
and remote hosts. It does not become a notification feed. and remote hosts. It does not become a notification feed.
3. A compact top 20% context band keeps all pane identities and states visible; 3. The top 20% of every visible panel keeps that pane's identity, surfaces,
the selected pane receives the accent treatment. execution target, context, goal, and state visible; the selected panel
4. A waiting pane uses an amber semantic label in both the context band and receives the accent treatment.
attention sidebar. The label always includes words such as `NEEDS INPUT`. 4. A waiting pane uses an amber semantic label in both its panel context region
5. The middle 60% gives one, three, or five terminal/work lanes uninterrupted and the attention sidebar. The label always includes words such as
height and retains target, harness state, dimensions, and surface tabs. The `NEEDS INPUT`.
selected lane alone owns keyboard input. 5. The middle 60% of every panel gives its terminal or work surface uninterrupted
height. The selected panel alone owns keyboard input.
6. The footer groups connection state, selected-harness identity, usage-window 6. The footer groups connection state, selected-harness identity, usage-window
provenance, and burn forecast into readable regions. provenance, and burn forecast into readable regions.
7. The selected pane has a quiet surface strip for Terminal, Browser, Tools, 7. The selected pane has a quiet surface strip for Terminal, Browser, Tools,
Context, Goal, and Review. Switching surfaces keeps one pane identity. Context, Goal, and Review. Switching surfaces keeps one pane identity.
8. A compact decision shelf may show local-analyst choices below the work 8. Every panel has a compact decision shelf below its work surface. It labels
surface. It labels its model/provider, trace scope, and whether suggestions whether suggestions are inert or review is required; no choice is visually
are inert; no choice is visually conflated with an approved action. conflated with an approved action.
## Keyboard task flow ## Keyboard task flow
@@ -1,22 +1,23 @@
# 0008: The default workspace is a primary-terminal 3×1 composition # 0008: Every workspace panel uses a terminal-first 3×1 composition
Status: accepted for the native vertical slice. Status: accepted for the native vertical slice.
Lumbridge defaults to three horizontal bands in one column. The top 20% exposes Every Lumbridge workspace panel defaults to three horizontal regions in one
pane, agent, tools, context, goal, target, and attention state. The middle 60% column. The top 20% exposes that pane's agent, tools, context, goal, target, and
is the terminal-first work region. The bottom 20% holds answers, choices, chat, attention state. The middle 60% is that pane's terminal-first work surface. The
approvals, and the local-analyst boundary. bottom 20% is that pane's answer, choice, chat, approval, and local-analyst
boundary.
This composition favors the region where engineers type and read for most of the This composition keeps the information needed to understand and steer a process
day. It keeps context and decisions visible without making every surface a small attached to the same visual object as its terminal. The workspace may contain a
dashboard card. The middle region may contain a responsive number of vertical responsive number of these panels under decision 0009; the selected panel alone
work lanes under decision 0009; the selected lane alone owns keyboard input. owns keyboard input.
The framework spike retains six updating surfaces even when five are offscreen. The framework spike retains six updating surfaces even when one is outside the
That preserves the earlier streaming and reducer workload for performance five-panel ultrawide window. That preserves the earlier streaming and reducer
comparison. Hidden surfaces do not receive keyboard input merely because they workload for performance comparison. Hidden panels do not receive keyboard input
continue to update. merely because they continue to update.
Window and lane geometry determine each terminal's rows and columns. The UI Window and panel geometry determine each terminal's rows and columns. The UI
resizes the terminal engine and PTY through the bounded runtime actor; it does resizes the terminal engine and PTY through the bounded runtime actor; it does
not resize only the text view or infer terminal wrapping after the fact. not resize only the text view or infer terminal wrapping after the fact.
@@ -1,24 +1,26 @@
# 0009: The primary work region uses responsive vertical lanes # 0009: The workspace uses responsive self-contained vertical panels
Status: accepted for the native vertical slice. Status: accepted for the native vertical slice.
The middle 60% work region shows one vertical lane in compact windows, three at The workspace shows one vertical panel in compact windows, three at normal
normal desktop widths, and five when at least 2800 px remains after the sidebar. desktop widths, and five when at least 2800 px remains after the sidebar. A
A 3440 px ultrawide therefore presents five lanes. A sliding window over the six 3440 px ultrawide therefore presents five panels. A sliding window over the six
comparison panes always keeps the selected pane visible. comparison panes always keeps the selected pane visible.
This is not a return to the original 2×3 dashboard. Every lane receives the full This is not a return to the original 2×3 dashboard and it is not one shared
work-region height, the surrounding context and decision bands remain stable, 20/60/20 frame sliced only through the middle. Every panel receives the full
and only the selected lane owns keyboard input. Clicking a lane selects it. Pane workspace height and independently contains its own 20% context region, 60%
processes and output continue independently of selection. terminal/work surface, and 20% decision shelf. Only the selected panel owns
keyboard input. Clicking a panel selects it. Pane processes and output continue
independently of selection.
Terminal geometry is computed from the actual width of one lane, including gaps Terminal geometry is computed from the width of one panel and the height of its
and insets. The selected real PTY is resized through the runtime actor when the middle 60% region, including gaps and content insets. The selected real PTY is
responsive lane count or window bounds change. On the 3440×1440 reference resized through the runtime actor when the responsive panel count or window
display the current fixed-cell spike derives approximately 71 columns by 40 bounds change. On the 3440×1440 reference display the current fixed-cell spike
rows per lane. derives approximately 71 columns by 42 rows per panel.
The first actor slice still owns one real PTY and five deterministic comparison The first actor slice still owns one real PTY and five deterministic comparison
surfaces. Promoting the runtime boundary from one actor to a pane-indexed session surfaces. Promoting the runtime boundary from one actor to a pane-indexed session
registry is required before all five visible terminal lanes can own independent registry is required before all five visible terminal panels can own independent
real processes. real processes.
+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 comparison. GPUI feeds raw output through `lumbridge-terminal` and sends encoded
keyboard input and terminal protocol replies through the bounded runtime actor. keyboard input and terminal protocol replies through the bounded runtime actor.
Its visual adapter coalesces VT cells into native styled runs, paints cursor 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 shapes, and resizes the engine and PTY from the middle 60% of a responsive
middle 60% work band. Retained-history navigation is wired; text selection and 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. mouse input remain intentionally unfinished.
Build independently: Build independently:
+215 -275
View File
@@ -39,11 +39,11 @@ const SIDEBAR_WIDTH: f32 = 248.0;
const APP_HEADER_HEIGHT: f32 = 48.0; const APP_HEADER_HEIGHT: f32 = 48.0;
const TAB_BAR_HEIGHT: f32 = 38.0; const TAB_BAR_HEIGHT: f32 = 38.0;
const APP_FOOTER_HEIGHT: f32 = 32.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_HORIZONTAL_INSET: f32 = 32.0;
const TERMINAL_CELL_WIDTH: f32 = 8.4; const TERMINAL_CELL_WIDTH: f32 = 8.4;
const TERMINAL_CELL_HEIGHT: f32 = 18.0; 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_ROW_STEP: u16 = 2;
const TERMINAL_COLUMN_STEP: u16 = 10; const TERMINAL_COLUMN_STEP: u16 = 10;
@@ -78,7 +78,6 @@ struct LumbridgeShell {
terminal_snapshot: TerminalSnapshot, terminal_snapshot: TerminalSnapshot,
last_runtime_sequence: u64, last_runtime_sequence: u64,
root_focus: FocusHandle, root_focus: FocusHandle,
pane_focus: [FocusHandle; 6],
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[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 { const fn is_terminal(&self) -> bool {
matches!(self, Self::Exited(_) | Self::Fault(_)) 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 { fn terminal_dimensions_for_window(window_size: Size<Pixels>) -> TerminalDimensions {
let width = f32::from(window_size.width); let width = f32::from(window_size.width);
let height = f32::from(window_size.height); 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 = let workspace_height =
(height - APP_HEADER_HEIGHT - TAB_BAR_HEIGHT - APP_FOOTER_HEIGHT).max(0.0); (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 workspace_width = (width - SIDEBAR_WIDTH).max(0.0);
let lane_gaps = WORK_LANE_GAP * lane_count.saturating_sub(1) as f32; let panel_gaps = WORK_PANEL_GAP * panel_count.saturating_sub(1) as f32;
let lane_width = ((workspace_width - lane_gaps).max(0.0) / lane_count as f32).max(0.0); let panel_width = ((workspace_width - panel_gaps).max(0.0) / panel_count as f32).max(0.0);
let terminal_width = (lane_width - TERMINAL_HORIZONTAL_INSET).max(0.0); let terminal_width = (panel_width - TERMINAL_HORIZONTAL_INSET).max(0.0);
let rows = (terminal_height / TERMINAL_CELL_HEIGHT) let rows = (terminal_height / TERMINAL_CELL_HEIGHT)
.floor() .floor()
.clamp(2.0, f32::from(u16::MAX)); .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") .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); let workspace_width = (f32::from(window_size.width) - SIDEBAR_WIDTH).max(0.0);
if workspace_width >= 2_800.0 { if workspace_width >= 2_800.0 {
5 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> { fn visible_panel_range(
let count = lane_count.clamp(1, total.max(1)).min(total); 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 let start = selected
.saturating_sub(count / 2) .saturating_sub(count / 2)
.min(total.saturating_sub(count)); .min(total.saturating_sub(count));
@@ -363,11 +363,6 @@ impl RenderTiming {
impl LumbridgeShell { impl LumbridgeShell {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self { 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(); let root_focus = cx.focus_handle();
window.focus(&root_focus); window.focus(&root_focus);
@@ -433,7 +428,6 @@ impl LumbridgeShell {
terminal_snapshot, terminal_snapshot,
last_runtime_sequence: 0, last_runtime_sequence: 0,
root_focus, root_focus,
pane_focus,
} }
} }
@@ -707,83 +701,6 @@ impl LumbridgeShell {
cx.notify(); 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 { fn terminal_run(run: TerminalPaintRun) -> gpui::AnyElement {
let cursor = run.cursor; let cursor = run.cursor;
div() div()
@@ -851,13 +768,7 @@ impl LumbridgeShell {
.into_any_element() .into_any_element()
} }
fn work_surface( fn pane_context(&self, pane: &PaneState, selected: bool) -> gpui::AnyElement {
&self,
pane: &PaneState,
selected: bool,
cx: &mut Context<Self>,
) -> gpui::AnyElement {
let pane_id = pane.id();
let external = pane.output_source() == OutputSource::External; let external = pane.output_source() == OutputSource::External;
let status = if external { let status = if external {
self.runtime_status.badge() self.runtime_status.badge()
@@ -909,6 +820,90 @@ impl LumbridgeShell {
.child(label) .child(label)
}) })
.collect::<Vec<_>>(); .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 { let content = if external {
self.terminal_view() self.terminal_view()
} else { } else {
@@ -927,63 +922,15 @@ impl LumbridgeShell {
}; };
div() div()
.id(("work-lane", pane_id.index()))
.on_click(cx.listener(move |shell, _, window, cx| {
shell.select_pane(pane_id, window, cx);
}))
.flex() .flex()
.flex_col() .flex_col()
.size_full() .size_full()
.min_w_0() .min_w_0()
.min_h_0() .min_h_0()
.overflow_hidden() .overflow_hidden()
.bg(rgb(PANEL)) .bg(rgb(BG))
.border_1() .border_y_1()
.border_color(rgb(if selected { ACCENT } else { BORDER })) .border_color(rgb(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),
)
.child( .child(
div() div()
.flex_1() .flex_1()
@@ -995,22 +942,36 @@ impl LumbridgeShell {
.into_any_element() .into_any_element()
} }
fn decision_region(&self) -> gpui::AnyElement { fn pane_decision_region(&self, pane: &PaneState) -> gpui::AnyElement {
let choice = |label: &'static str, detail: &'static str| { let choice = |label: &'static str, detail: &'static str, attention: bool| {
div() div()
.flex() .flex()
.flex_col() .items_center()
.min_w(px(200.0)) .justify_between()
.px_3() .min_w_0()
.py_2() .px_2()
.rounded(px(5.0)) .py_1()
.rounded(px(4.0))
.border_1() .border_1()
.border_color(rgb(BORDER)) .border_color(rgb(if attention { ATTENTION } else { BORDER }))
.bg(rgb(PANEL_ALT)) .bg(rgb(PANEL_ALT))
.text_sm() .text_xs()
.text_color(rgb(TEXT)) .text_color(rgb(TEXT))
.child(label) .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() div()
.flex() .flex()
@@ -1018,9 +979,11 @@ impl LumbridgeShell {
.size_full() .size_full()
.min_h_0() .min_h_0()
.overflow_hidden() .overflow_hidden()
.px_3() .px_2()
.py_2() .py_2()
.bg(rgb(PANEL)) .bg(rgb(PANEL))
.border_t_1()
.border_color(rgb(BORDER))
.child( .child(
div() div()
.flex() .flex()
@@ -1030,23 +993,78 @@ impl LumbridgeShell {
.child( .child(
div() div()
.text_color(rgb(MUTED)) .text_color(rgb(MUTED))
.child("DECISION SHELF · answer, choices, approvals"), .child("DECISION SHELF"),
) )
.child( .child(
div() div()
.text_color(rgb(ATTENTION)) .text_color(rgb(if pane.needs_input() {
.child("LOCAL ANALYST OFF · suggestions inert"), ATTENTION
} else {
MUTED
}))
.child(if pane.needs_input() {
"REVIEW REQUIRED"
} else {
"SUGGESTIONS INERT"
}),
), ),
) )
.child( .child(
div() div()
.flex() .flex()
.items_center() .flex_col()
.gap_2() .gap_1()
.mt_2() .mt_2()
.child(choice("Continue", "Keep the selected agent moving")) .children(choices.map(|(label, detail, attention)| {
.child(choice("Review plan", "Inspect capability-scoped commands")) choice(label, detail, attention)
.child(choice("Ask…", "Refine before any action")), })),
)
.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() .into_any_element()
} }
@@ -1140,6 +1158,7 @@ impl LumbridgeShell {
impl Render for LumbridgeShell { impl Render for LumbridgeShell {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let attention = self.model.pane(PaneId::ClaudeUi); let attention = self.model.pane(PaneId::ClaudeUi);
let panel_count = visible_panel_count(window.bounds().size);
let sidebar = div() let sidebar = div()
.flex() .flex()
.flex_col() .flex_col()
@@ -1303,117 +1322,38 @@ impl Render for LumbridgeShell {
.px_3() .px_3()
.text_xs() .text_xs()
.text_color(rgb(MUTED)) .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 let panel_range = visible_panel_range(
.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(
PaneId::ALL.len(), PaneId::ALL.len(),
self.model.selected_pane().index(), self.model.selected_pane().index(),
lane_count, panel_count,
); );
let work_lanes = lane_range let workspace_panels = panel_range
.map(|index| { .map(|index| {
let pane = self.model.pane(PaneId::ALL[index]); let pane = self.model.pane(PaneId::ALL[index]);
div() div()
.flex_1() .flex_1()
.min_w_0() .min_w_0()
.min_h_0() .min_h_0()
.child(self.work_surface( .child(self.workspace_panel(
pane, pane,
pane.id() == self.model.selected_pane(), pane.id() == self.model.selected_pane(),
cx, cx,
)) ))
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let workspace_stack = div() let workspace_row = div()
.flex() .flex()
.flex_col() .gap_1()
.flex_1() .flex_1()
.min_w_0() .min_w_0()
.min_h_0() .min_h_0()
.overflow_hidden() .overflow_hidden()
.child( .children(workspace_panels);
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()),
);
let counters = self.model.counters(); let counters = self.model.counters();
let footer_left = format!( let footer_left = format!(
@@ -1511,7 +1451,7 @@ impl Render for LumbridgeShell {
.min_h_0() .min_h_0()
.overflow_hidden() .overflow_hidden()
.child(tabs) .child(tabs)
.child(workspace_stack), .child(workspace_row),
), ),
) )
.child( .child(
@@ -1658,7 +1598,7 @@ mod tests {
use super::{ use super::{
KeyModifiers, TerminalEngine, TerminalEngineOptions, TerminalKey, TerminalScroll, KeyModifiers, TerminalEngine, TerminalEngineOptions, TerminalKey, TerminalScroll,
indexed_terminal_color, terminal_dimensions_for_window, terminal_key_from_parts, 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}; use gpui::{px, size};
@@ -1685,24 +1625,24 @@ mod tests {
} }
#[test] #[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))); 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); assert_eq!(dimensions.columns(), 45);
let ultrawide = size(px(3440.0), px(1440.0)); 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); let dimensions = terminal_dimensions_for_window(ultrawide);
assert_eq!(dimensions.rows(), 40); assert_eq!(dimensions.rows(), 42);
assert_eq!(dimensions.columns(), 71); assert_eq!(dimensions.columns(), 71);
} }
#[test] #[test]
fn lane_window_keeps_the_selected_pane_visible() { fn panel_window_keeps_the_selected_pane_visible() {
assert_eq!(visible_lane_range(6, 0, 5), 0..5); assert_eq!(visible_panel_range(6, 0, 5), 0..5);
assert_eq!(visible_lane_range(6, 5, 5), 1..6); assert_eq!(visible_panel_range(6, 5, 5), 1..6);
assert_eq!(visible_lane_range(6, 3, 3), 2..5); assert_eq!(visible_panel_range(6, 3, 3), 2..5);
assert_eq!(visible_lane_range(6, 4, 1), 4..5); assert_eq!(visible_panel_range(6, 4, 1), 4..5);
} }
#[test] #[test]