mod attention; mod keymap; mod panel_registry; mod theme; mod usage_feed; use std::collections::{BTreeMap, VecDeque}; use std::path::PathBuf; use std::time::{Duration, Instant}; use gpui::{ App, Application, Bounds, Context, FocusHandle, FontWeight, KeyDownEvent, Pixels, Rgba, Size, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, relative, rgb, size, }; use lumbridge_core::UsageProvenance; use lumbridge_runtime::{ CommandConfig, PtyOptions, RuntimeActorError, RuntimeActorOptions, RuntimeCommand, RuntimeEvent, RuntimeRegistry, RuntimeRegistryError, TerminalSize, }; use lumbridge_storage::Store; use lumbridge_terminal::{ KeyModifiers, TerminalCellStyle, TerminalColor, TerminalCursorShape, TerminalDimensions, TerminalEngine, TerminalEngineOptions, TerminalKey, TerminalKeyEvent, TerminalNamedColor, TerminalScroll, TerminalSnapshot, }; use lumbridge_ui_fixture::{ ActionOutcome, OutputSource, PaneId as FixturePaneId, PaneStatus, ShellAction, ShellModel, SurfaceKind, }; use attention::{Attention, AttentionKind, AttentionSignal, AttentionSource}; use lumbridge_harness::MonotonicWallClock; use panel_registry::{PanelId, PanelKind, PanelRegistry, SeedPane}; use theme::{ActiveTheme, ThemeColors}; use usage_feed::{UsageFeed, UsageSegment}; const TIMING_SAMPLE_LIMIT: usize = 256; const RUNTIME_POLL_INTERVAL: Duration = Duration::from_millis(16); const RUNTIME_DRAIN_LIMIT: usize = 64; const WORKSPACE_SNAPSHOT_ID: &str = "lumbridge-code-gpui-spike"; 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 = 46.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_PANEL_GAP: f32 = 4.0; const TERMINAL_ROW_STEP: u16 = 2; const TERMINAL_COLUMN_STEP: u16 = 10; actions!( lumbridge, [ FocusLeft, FocusRight, FocusUp, FocusDown, OpenPalette, ClosePalette, RestartPane, TerminatePane, PasteIntoPane, SelectPane1, SelectPane2, SelectPane3, SelectPane4, SelectPane5, SelectPane6, AddPanel, DetachSelectedPanel, TerminalTaller, TerminalShorter, TerminalWider, TerminalNarrower, ] ); struct LumbridgeShell { theme: ActiveTheme, model: ShellModel, panels: PanelRegistry, timing: RenderTiming, runtimes: RuntimeRegistry, live_terminals: BTreeMap, store: Option, persistence_status: String, usage: UsageFeed, /// Which surface tab each panel is showing. A panel is absent until the /// user picks something other than the surface it provides natively. surfaces: BTreeMap, /// Which decision-shelf choice each panel has selected. Selecting one is /// inert by design: it prepares nothing and runs nothing. shelf_choice: BTreeMap, add_panel_chooser_open: bool, /// The pane a terminate has been asked for but not yet confirmed. pending_terminate: Option, /// Why the last keystroke went nowhere, if it did. input_gap: Option, /// What is waiting on a human, and how we know. attention: Attention, /// Stamps attention signals. Monotonic, so a signal cannot appear to have /// arrived before one recorded earlier. clock: MonotonicWallClock, root_focus: FocusHandle, } /// The surfaces a pane can be viewed through. /// /// A pane is the durable unit of work and its surface is a view over that work. /// Switching one never launches a process, moves the pane, or changes which /// agent owns the session — it only changes what is drawn. #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum SurfaceTab { Terminal, Browser, Tools, Context, Goal, Review, } impl SurfaceTab { const ALL: [Self; 6] = [ Self::Terminal, Self::Browser, Self::Tools, Self::Context, Self::Goal, Self::Review, ]; const fn label(self) -> &'static str { match self { Self::Terminal => "TERMINAL", Self::Browser => "BROWSER", Self::Tools => "TOOLS", Self::Context => "CONTEXT", Self::Goal => "GOAL", Self::Review => "REVIEW", } } const fn ordinal(self) -> u64 { match self { Self::Terminal => 0, Self::Browser => 1, Self::Tools => 2, Self::Context => 3, Self::Goal => 4, Self::Review => 5, } } /// The one surface a pane of this kind actually provides. const fn native_for(kind: SurfaceKind) -> Self { match kind { SurfaceKind::Terminal => Self::Terminal, SurfaceKind::Browser => Self::Browser, SurfaceKind::Markdown => Self::Context, SurfaceKind::Review => Self::Review, } } /// Why this pane cannot show this surface. Stated plainly, because the /// product boundary says an unsupported surface is shown as unavailable /// rather than simulated. const fn unavailable_reason(self) -> &'static str { match self { Self::Terminal => "Only a terminal pane owns a live shell.", Self::Browser => "No isolated web engine is wired yet.", Self::Tools => "Tool calls arrive with the ACP client.", Self::Context => "Context projection is not built yet.", Self::Goal => "Goal tracking is not built yet.", Self::Review => "Review is not wired to Git yet.", } } } #[derive(Clone)] struct PanelView { id: PanelId, kind: SurfaceKind, title: String, badge: String, target: String, status: PaneStatus, lines: Vec, output_source: OutputSource, } impl PanelView { const fn needs_input(&self) -> bool { matches!(self.status, PaneStatus::NeedsInput) } } struct LiveTerminalState { status: LiveRuntimeStatus, terminal: TerminalEngine, snapshot: TerminalSnapshot, last_runtime_sequence: u64, } impl LiveTerminalState { fn new(dimensions: TerminalDimensions) -> Self { let terminal = TerminalEngine::new(TerminalEngineOptions { dimensions, ..TerminalEngineOptions::default() }); let snapshot = terminal.snapshot(); Self { status: LiveRuntimeStatus::Starting, terminal, snapshot, last_runtime_sequence: 0, } } } #[derive(Clone, Debug, Eq, PartialEq)] enum LiveRuntimeStatus { Starting, Running { session_id: u64, process_id: Option, }, Exited(String), Fault(String), } impl LiveRuntimeStatus { fn badge(&self) -> &'static str { match self { Self::Starting => "PTY STARTING", Self::Running { .. } => "LIVE PTY", Self::Exited(_) => "PTY EXITED", Self::Fault(_) => "PTY FAULT", } } fn detail(&self) -> String { match self { Self::Starting => "local · runtime actor starting".to_owned(), Self::Running { session_id, process_id, } => match process_id { Some(process_id) => { format!("local · runtime session {session_id} · pid {process_id}") } None => format!("local · runtime session {session_id}"), }, Self::Exited(status) => format!("local · {status}"), Self::Fault(message) => format!("local · {message}"), } } const fn is_terminal(&self) -> bool { matches!(self, Self::Exited(_) | Self::Fault(_)) } } #[derive(Default)] struct RenderTiming { pending_dispatch: Option, dispatch_to_element_micros: VecDeque, } #[derive(Clone, PartialEq)] struct TerminalPaintRun { text: String, columns: u16, foreground: Rgba, background: Rgba, style: TerminalCellStyle, cursor: Option, hyperlink: bool, } impl TerminalPaintRun { fn can_merge(&self, other: &Self) -> bool { self.foreground == other.foreground && self.background == other.background && self.style == other.style && self.cursor == other.cursor && self.hyperlink == other.hyperlink } } fn terminal_paint_rows( snapshot: &TerminalSnapshot, theme: ThemeColors, ) -> Vec> { (0..snapshot.dimensions.rows()) .map(|row| { let mut runs: Vec = Vec::new(); for column in 0..snapshot.dimensions.columns() { let Some(cell) = snapshot.cell(row, column) else { continue; }; if cell.wide_spacer { continue; } let mut foreground = terminal_color(cell.foreground, theme); let mut background = terminal_color(cell.background, theme); if cell.style.contains(TerminalCellStyle::INVERSE) { std::mem::swap(&mut foreground, &mut background); } if cell.style.contains(TerminalCellStyle::DIM) { foreground = dim_color(foreground); } if cell.style.contains(TerminalCellStyle::HIDDEN) { foreground = background; } let cursor = (snapshot.cursor.row == row && snapshot.cursor.column == column && snapshot.cursor.shape != TerminalCursorShape::Hidden) .then_some(snapshot.cursor.shape); let text = if cell.text.is_empty() { " ".to_owned() } else { cell.text.clone() }; let next = TerminalPaintRun { text, columns: if cell.wide { 2 } else { 1 }, foreground, background, style: cell.style, cursor, hyperlink: cell.hyperlink.is_some(), }; if let Some(current) = runs.last_mut() && current.can_merge(&next) { current.text.push_str(&next.text); current.columns = current.columns.saturating_add(next.columns); continue; } runs.push(next); } runs }) .collect() } #[allow( clippy::unreadable_literal, reason = "six-digit colour hex reads whole" )] /// Widens a small count for layout arithmetic. /// /// `f32` represents every integer below 2^24 exactly, and the values passed /// here are panel and column counts. Saturating there rather than at `usize::MAX` /// keeps the conversion exact for every value this can actually receive. fn lossless_f32(value: usize) -> f32 { const EXACT_LIMIT: usize = 1 << 24; #[allow( clippy::cast_precision_loss, reason = "clamped below 2^24, where f32 is exact" )] { value.min(EXACT_LIMIT) as f32 } } /// Narrows an already-clamped dimension. /// /// Every caller clamps into `2.0..=u16::MAX` first, so this is a narrowing of a /// value known to fit — but `as` would silently produce garbage if a caller ever /// stopped clamping, and a NaN would become zero. This saturates instead, which /// is why the `with_cell_size` expect below is honest rather than hopeful. fn clamp_to_u16(value: f32) -> u16 { if value.is_nan() { return 1; } #[allow( clippy::cast_possible_truncation, clippy::cast_sign_loss, reason = "clamped into u16 range on the line above" )] { value.clamp(0.0, f32::from(u16::MAX)) as u16 } } #[allow( clippy::unreadable_literal, reason = "six-digit colour hex reads whole" )] fn terminal_color(color: TerminalColor, theme: ThemeColors) -> Rgba { match color { TerminalColor::Rgb { red, green, blue } => { rgb((u32::from(red) << 16) | (u32::from(green) << 8) | u32::from(blue)) } TerminalColor::Indexed(index) => rgb(indexed_terminal_color(index)), TerminalColor::Named(named) => match named { TerminalNamedColor::Black => rgb(0x1d2430), TerminalNamedColor::Red => rgb(0xff6b6b), TerminalNamedColor::Green => rgb(0x70d6a8), TerminalNamedColor::Yellow => rgb(0xf1c76a), TerminalNamedColor::Blue => rgb(0x68b5f8), TerminalNamedColor::Magenta => rgb(0xc79bf2), TerminalNamedColor::Cyan => rgb(0x63d5da), TerminalNamedColor::White => rgb(0xdbe5f4), TerminalNamedColor::BrightBlack => rgb(0x6d7a91), TerminalNamedColor::BrightRed => rgb(0xff8b8b), TerminalNamedColor::BrightGreen => rgb(0x93e6be), TerminalNamedColor::BrightYellow => rgb(0xf8d98c), TerminalNamedColor::BrightBlue => rgb(0x8bc8ff), TerminalNamedColor::BrightMagenta => rgb(0xd9b4fb), TerminalNamedColor::BrightCyan => rgb(0x86e7eb), TerminalNamedColor::BrightWhite | TerminalNamedColor::BrightForeground | TerminalNamedColor::Cursor => rgb(0xf4f8ff), TerminalNamedColor::Foreground => theme.text, TerminalNamedColor::Background => theme.chrome, TerminalNamedColor::DimBlack => rgb(0x121820), TerminalNamedColor::DimRed => rgb(0x9f4a4a), TerminalNamedColor::DimGreen => rgb(0x4e9878), TerminalNamedColor::DimYellow => rgb(0xa88a49), TerminalNamedColor::DimBlue => rgb(0x477fac), TerminalNamedColor::DimMagenta => rgb(0x896ca4), TerminalNamedColor::DimCyan => rgb(0x459397), TerminalNamedColor::DimWhite | TerminalNamedColor::DimForeground => theme.muted, }, } } #[allow( clippy::unreadable_literal, reason = "six-digit colour hex reads whole" )] fn indexed_terminal_color(index: u8) -> u32 { const ANSI: [u32; 16] = [ 0x1d2430, 0xff6b6b, 0x70d6a8, 0xf1c76a, 0x68b5f8, 0xc79bf2, 0x63d5da, 0xdbe5f4, 0x6d7a91, 0xff8b8b, 0x93e6be, 0xf8d98c, 0x8bc8ff, 0xd9b4fb, 0x86e7eb, 0xf4f8ff, ]; match index { 0..=15 => ANSI[usize::from(index)], 16..=231 => { const LEVELS: [u32; 6] = [0, 95, 135, 175, 215, 255]; let offset = u32::from(index - 16); let red = LEVELS[(offset / 36) as usize]; let green = LEVELS[((offset % 36) / 6) as usize]; let blue = LEVELS[(offset % 6) as usize]; (red << 16) | (green << 8) | blue } 232..=255 => { let level = 8 + 10 * u32::from(index - 232); (level << 16) | (level << 8) | level } } } /// SGR 2. Not a theme role: dimming is a property of the escape sequence, so it /// scales whatever colour the cell already had. fn dim_color(color: Rgba) -> Rgba { const DIM: f32 = 13.0 / 20.0; Rgba { r: color.r * DIM, g: color.g * DIM, b: color.b * DIM, a: color.a, } } fn terminal_dimensions_for_window( window_size: Size, attached_panel_count: usize, ) -> TerminalDimensions { let width = f32::from(window_size.width); let height = f32::from(window_size.height); let panel_count = visible_panel_count(window_size) .min(attached_panel_count) .max(1); let workspace_height = (height - APP_HEADER_HEIGHT - TAB_BAR_HEIGHT - APP_FOOTER_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); // A panel count is single digits; the precision lint is about 2^24 and up. let panel_gaps = WORK_PANEL_GAP * lossless_f32(panel_count.saturating_sub(1)); let panel_width = ((workspace_width - panel_gaps).max(0.0) / lossless_f32(panel_count)).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)); let columns = (terminal_width / TERMINAL_CELL_WIDTH) .floor() .clamp(20.0, f32::from(u16::MAX)); TerminalDimensions::with_cell_size( clamp_to_u16(rows), clamp_to_u16(columns), clamp_to_u16(TERMINAL_CELL_WIDTH.round()), clamp_to_u16(TERMINAL_CELL_HEIGHT.round()), ) .expect("geometry clamps terminal dimensions above zero") } fn visible_panel_count(window_size: Size) -> usize { let workspace_width = (f32::from(window_size.width) - SIDEBAR_WIDTH).max(0.0); if workspace_width >= 2_800.0 { 5 } else if workspace_width >= 1_100.0 { 3 } else { 1 } } fn visible_panel_range( total: usize, selected: usize, panel_count: usize, ) -> std::ops::Range { let count = panel_count.clamp(1, total.max(1)).min(total); let start = selected .saturating_sub(count / 2) .min(total.saturating_sub(count)); start..start + count } impl RenderTiming { fn mark_dispatch(&mut self) { self.pending_dispatch = Some(Instant::now()); } fn observe_element_build(&mut self) { let Some(started) = self.pending_dispatch.take() else { return; }; if self.dispatch_to_element_micros.len() == TIMING_SAMPLE_LIMIT { self.dispatch_to_element_micros.pop_front(); } self.dispatch_to_element_micros .push_back(started.elapsed().as_micros()); } fn summary(&self) -> String { if self.dispatch_to_element_micros.is_empty() { return "dispatch→element collecting…".to_owned(); } let mut samples = self .dispatch_to_element_micros .iter() .copied() .collect::>(); samples.sort_unstable(); let percentile = |percent: usize| { let index = ((samples.len() - 1) * percent) / 100; samples[index] }; format!( "dispatch→element p50 {}µs · p95 {}µs · n{}", percentile(50), percentile(95), samples.len() ) } } fn workspace_database_path() -> Option { if let Some(path) = std::env::var_os("LUMBRIDGE_SPIKE_DB") { return Some(PathBuf::from(path)); } if let Some(root) = std::env::var_os("XDG_DATA_HOME") { return Some(PathBuf::from(root).join("lumbridge/lumbridge.db")); } std::env::var_os("HOME") .map(PathBuf::from) .map(|home| home.join(".local/share/lumbridge/lumbridge.db")) } fn load_panel_registry() -> (Option, PanelRegistry, String) { let Some(path) = workspace_database_path() else { return ( None, PanelRegistry::first_run(), "layout memory-only · no data directory".to_owned(), ); }; if let Some(parent) = path.parent() && let Err(error) = std::fs::create_dir_all(parent) { return ( None, PanelRegistry::first_run(), format!("layout memory-only · {error}"), ); } let store = match Store::open(&path) { Ok(store) => store, Err(error) => { return ( None, PanelRegistry::first_run(), format!("layout memory-only · {error}"), ); } }; match store.workspace_snapshot(WORKSPACE_SNAPSHOT_ID) { Ok(Some(json)) => match PanelRegistry::from_json(&json) { Ok(panels) => (Some(store), panels, "layout restored · SQLite".to_owned()), Err(error) => ( Some(store), PanelRegistry::first_run(), format!("invalid layout ignored · {error}"), ), }, Ok(None) => ( Some(store), PanelRegistry::first_run(), "layout ready · SQLite".to_owned(), ), Err(error) => ( Some(store), PanelRegistry::first_run(), format!("layout read failed · {error}"), ), } } impl LumbridgeShell { fn new(window: &mut Window, cx: &mut Context) -> Self { let root_focus = cx.focus_handle(); window.focus(&root_focus); cx.spawn(async move |this, cx| { loop { cx.background_executor() .timer(Duration::from_millis(650)) .await; if this .update(cx, |shell, cx| { shell.dispatch(ShellAction::SyntheticStreamTick); shell.usage.advance(); shell.usage.tick(); cx.notify(); }) .is_err() { break; } } }) .detach(); cx.spawn(async move |this, cx| { loop { cx.background_executor().timer(RUNTIME_POLL_INTERVAL).await; if this .update(cx, |shell, cx| { if shell.drain_runtime_events() { cx.notify(); } }) .is_err() { break; } } }) .detach(); let (store, panels, persistence_status) = load_panel_registry(); let terminal_dimensions = terminal_dimensions_for_window(window.bounds().size, panels.attached_count()); let mut runtimes = RuntimeRegistry::new(); let mut live_terminals = BTreeMap::new(); for panel in panels .panels() .iter() .filter(|panel| panel.kind == PanelKind::Terminal) { let mut terminal = LiveTerminalState::new(terminal_dimensions); if let Err(error) = spawn_live_runtime(&mut runtimes, panel.id, panel.seed, terminal_dimensions) { terminal.status = LiveRuntimeStatus::Fault(error.to_string()); } live_terminals.insert(panel.id, terminal); } cx.observe_window_bounds(window, |shell, window, cx| { let dimensions = terminal_dimensions_for_window(window.bounds().size, shell.panels.attached_count()); shell.resize_attached_terminals(dimensions.rows(), dimensions.columns(), cx); }) .detach(); Self { // The catalog is compiled in, so the first frame paints the right // theme: nothing to load, nothing to cache, no flash of the wrong // colours on startup. theme: ActiveTheme::new( lumbridge_theme::theme_or_default(lumbridge_theme::DEFAULT_THEME), lumbridge_theme::DEFAULT_ACCENT, ), model: ShellModel::with_external_outputs([ FixturePaneId::CodexRuntime, FixturePaneId::ClaudeUi, FixturePaneId::PiDocs, ]) .expect("all live comparison panes are terminals"), panels, timing: RenderTiming::default(), runtimes, live_terminals, store, persistence_status, usage: UsageFeed::start(), surfaces: BTreeMap::new(), shelf_choice: BTreeMap::new(), add_panel_chooser_open: false, pending_terminate: None, input_gap: None, attention: Attention::new(), clock: MonotonicWallClock::start(), root_focus, } } fn persist_panels(&mut self) { let Some(store) = &self.store else { return; }; let result = self .panels .to_json() .map_err(|error| error.clone()) .and_then(|json| { store .save_workspace_snapshot(WORKSPACE_SNAPSHOT_ID, &json) .map_err(|error| error.to_string()) }); self.persistence_status = match result { Ok(()) => "layout saved · SQLite".to_owned(), Err(error) => format!("layout save failed · {error}"), }; } fn dispatch(&mut self, action: ShellAction) -> ActionOutcome { self.timing.mark_dispatch(); self.model.dispatch(action) } #[allow( clippy::too_many_lines, reason = "one match over the runtime event enum; splitting it hides the exhaustiveness" )] fn drain_runtime_events(&mut self) -> bool { let mut changed = false; let panes = self.live_terminals.keys().copied().collect::>(); for pane in panes { if self .live_terminals .get(&pane) .is_none_or(|terminal| terminal.status.is_terminal()) { continue; } for _ in 0..RUNTIME_DRAIN_LIMIT { let event = match self.runtimes.try_recv(&pane) { Ok(Some(event)) => event, Ok(None) => break, Err(RuntimeRegistryError::Actor(RuntimeActorError::Disconnected)) => { if let Some(terminal) = self.live_terminals.get_mut(&pane) { terminal.status = LiveRuntimeStatus::Fault("runtime actor disconnected".to_owned()); } changed = true; break; } Err(error) => { if let Some(terminal) = self.live_terminals.get_mut(&pane) { terminal.status = LiveRuntimeStatus::Fault(error.to_string()); } changed = true; break; } }; match event { RuntimeEvent::Started { session_id, process_id, } => { if let Some(terminal) = self.live_terminals.get_mut(&pane) { terminal.status = LiveRuntimeStatus::Running { session_id: session_id.get(), process_id, }; } changed = true; } RuntimeEvent::Output { sequence, bytes, .. } => { let responses = { let Some(terminal) = self.live_terminals.get_mut(&pane) else { break; }; if sequence <= terminal.last_runtime_sequence { terminal.status = LiveRuntimeStatus::Fault(format!( "non-monotonic PTY output sequence {sequence}" )); changed = true; break; } terminal.last_runtime_sequence = sequence; terminal.terminal.process(&bytes).outbound }; for response in responses { if let Err(error) = self.send_runtime_command(pane, RuntimeCommand::Input(response)) { if let Some(terminal) = self.live_terminals.get_mut(&pane) { terminal.status = LiveRuntimeStatus::Fault(error.to_string()); } return true; } } self.publish_terminal_snapshot(pane); changed = true; } RuntimeEvent::InputClosed { .. } => {} RuntimeEvent::Exited { status, .. } => { if let Some(terminal) = self.live_terminals.get_mut(&pane) { terminal.status = LiveRuntimeStatus::Exited(format!( "PTY exited with code {}", status.code )); } // Observed in a process Lumbridge supervises, so it // counts. The exit code is carried rather than being // formatted into a sentence and thrown away. self.attention.observe(AttentionSignal { pane, kind: AttentionKind::Finished { exit_code: status.code, }, source: AttentionSource::RuntimeObserved, observed_at_ms: self.clock.now_ms(), }); changed = true; break; } RuntimeEvent::Fault { operation, message, .. } => { if let Some(terminal) = self.live_terminals.get_mut(&pane) { terminal.status = LiveRuntimeStatus::Fault(format!("{operation:?}: {message}")); } self.attention.observe(AttentionSignal { pane, kind: AttentionKind::Faulted, source: AttentionSource::RuntimeObserved, observed_at_ms: self.clock.now_ms(), }); changed = true; break; } } } } changed } fn send_runtime_command( &self, pane: PanelId, command: RuntimeCommand, ) -> Result<(), RuntimeRegistryError> { self.runtimes.try_send(&pane, command) } fn publish_terminal_snapshot(&mut self, pane: PanelId) { let lines = { let Some(terminal) = self.live_terminals.get_mut(&pane) else { return; }; let snapshot = terminal.terminal.snapshot(); let lines = snapshot.plain_rows(); terminal.snapshot = snapshot; lines }; let fixture = self .panels .panel(pane) .and_then(|panel| panel.seed) .map(SeedPane::fixture_id); if let Some(pane) = fixture { self.dispatch(ShellAction::ReplaceExternalOutput { pane, lines }); } } fn resize_terminal(&mut self, pane: PanelId, rows: u16, columns: u16) -> bool { let dimensions = TerminalDimensions::new(rows, columns) .expect("resize actions always retain non-zero dimensions"); let Some(terminal) = self.live_terminals.get_mut(&pane) else { return false; }; if !terminal.terminal.resize(dimensions) { return false; } let pty_size = TerminalSize::new(rows, columns) .expect("terminal engine dimensions are valid PTY dimensions"); if let Err(error) = self.send_runtime_command(pane, RuntimeCommand::Resize(pty_size)) && let Some(terminal) = self.live_terminals.get_mut(&pane) { terminal.status = LiveRuntimeStatus::Fault(error.to_string()); } self.publish_terminal_snapshot(pane); true } fn resize_attached_terminals(&mut self, rows: u16, columns: u16, cx: &mut Context) { let mut changed = false; let panes = self.live_terminals.keys().copied().collect::>(); for pane in panes { if self.panels.panel(pane).is_some_and(|panel| panel.attached) { changed |= self.resize_terminal(pane, rows, columns); } } if changed { cx.notify(); } } fn scroll_terminal(&mut self, pane: PanelId, scroll: TerminalScroll, cx: &mut Context) { let Some(terminal) = self.live_terminals.get_mut(&pane) else { return; }; if !terminal.terminal.scroll_display(scroll) { return; } self.publish_terminal_snapshot(pane); cx.notify(); } fn select_pane(&mut self, pane: PanelId, window: &mut Window, cx: &mut Context) { self.timing.mark_dispatch(); if self.panels.select(pane) { self.persist_panels(); } window.focus(&self.root_focus); cx.notify(); } /// Changes which surface a pane is viewed through. /// /// This is a view change only. It never touches the PTY, the agent, or the /// pane's execution target, so a pane keeps running whatever it was running. fn select_surface( &mut self, pane: PanelId, tab: SurfaceTab, window: &mut Window, cx: &mut Context, ) { self.timing.mark_dispatch(); self.surfaces.insert(pane, tab); if self.panels.select(pane) { self.persist_panels(); } window.focus(&self.root_focus); cx.notify(); } /// Records which suggestion the user picked. Deliberately does nothing else. fn select_shelf_choice( &mut self, pane: PanelId, index: usize, window: &mut Window, cx: &mut Context, ) { self.timing.mark_dispatch(); if self.shelf_choice.get(&pane) == Some(&index) { self.shelf_choice.remove(&pane); } else { self.shelf_choice.insert(pane, index); } if self.panels.select(pane) { self.persist_panels(); } window.focus(&self.root_focus); cx.notify(); } fn select_pane_at(&mut self, index: usize, window: &mut Window, cx: &mut Context) { self.timing.mark_dispatch(); if self.panels.select_attached_at(index) { self.persist_panels(); } window.focus(&self.root_focus); cx.notify(); } fn resize_terminal_for_workspace(&mut self, window: &Window, cx: &mut Context) { let dimensions = terminal_dimensions_for_window(window.bounds().size, self.panels.attached_count()); self.resize_attached_terminals(dimensions.rows(), dimensions.columns(), cx); } fn attach_panel(&mut self, pane: PanelId, window: &mut Window, cx: &mut Context) { self.timing.mark_dispatch(); if self.panels.attach(pane) { self.persist_panels(); self.resize_terminal_for_workspace(window, cx); } window.focus(&self.root_focus); cx.notify(); } fn create_panel(&mut self, kind: PanelKind, window: &mut Window, cx: &mut Context) { self.timing.mark_dispatch(); let pane = self.panels.create(kind); if kind == PanelKind::Terminal { let dimensions = terminal_dimensions_for_window(window.bounds().size, self.panels.attached_count()); let mut terminal = LiveTerminalState::new(dimensions); if let Err(error) = spawn_live_runtime(&mut self.runtimes, pane, None, dimensions) { terminal.status = LiveRuntimeStatus::Fault(error.to_string()); } self.live_terminals.insert(pane, terminal); } self.add_panel_chooser_open = false; self.persist_panels(); self.resize_terminal_for_workspace(window, cx); window.focus(&self.root_focus); cx.notify(); } fn detach_panel(&mut self, pane: PanelId, window: &mut Window, cx: &mut Context) { self.timing.mark_dispatch(); if self.panels.detach(pane) { self.persist_panels(); self.resize_terminal_for_workspace(window, cx); } window.focus(&self.root_focus); cx.notify(); } /// Restarts the selected pane's process, keeping the pane. /// /// The pane's identity is durable and its process is not, which is the whole /// point of decision 0011: a crashed shell should not cost you the pane, its /// position, or its place in the workspace. fn restart_pane(&mut self, _: &RestartPane, window: &mut Window, cx: &mut Context) { let pane = self.panels.selected(); if !self.live_terminals.contains_key(&pane) { return; } self.timing.mark_dispatch(); // Stop the old one first. A failure here is reported rather than // swallowed: a restart that silently left the old process running would // leak a shell every time it was pressed. if let Err(error) = self.runtimes.shutdown(&pane) { if let Some(terminal) = self.live_terminals.get_mut(&pane) { terminal.status = LiveRuntimeStatus::Fault(error.to_string()); } cx.notify(); return; } let dimensions = terminal_dimensions_for_window(window.bounds().size, self.panels.attached_count()); let mut terminal = LiveTerminalState::new(dimensions); if let Err(error) = spawn_live_runtime(&mut self.runtimes, pane, None, dimensions) { terminal.status = LiveRuntimeStatus::Fault(error.to_string()); } self.live_terminals.insert(pane, terminal); self.attention.clear(pane); self.pending_terminate = None; window.focus(&self.root_focus); cx.notify(); } /// Asks before terminating, then terminates on a second press. /// /// Decision 0010 requires terminate to be a distinct, named, confirmed /// operation rather than a close icon, and the confirmation names the /// process being killed. Detaching a pane leaves its process alive; this is /// the only path in the application that ends one. fn terminate_pane(&mut self, _: &TerminatePane, window: &mut Window, cx: &mut Context) { let pane = self.panels.selected(); if !self.live_terminals.contains_key(&pane) { return; } self.timing.mark_dispatch(); if self.pending_terminate != Some(pane) { self.pending_terminate = Some(pane); cx.notify(); return; } self.pending_terminate = None; let outcome = self.runtimes.shutdown(&pane); if let Some(terminal) = self.live_terminals.get_mut(&pane) { terminal.status = match outcome { Ok(()) => LiveRuntimeStatus::Exited("terminated by request".to_owned()), Err(error) => LiveRuntimeStatus::Fault(error.to_string()), }; } window.focus(&self.root_focus); cx.notify(); } /// Pastes the clipboard into the selected terminal. /// /// Through the engine's bracketed-paste path rather than as synthetic /// keystrokes, so a shell or editor that asked for bracketed paste is told /// this is a paste and does not run every newline as a command. /// /// There is no matching copy: the terminal engine has no selection yet, so /// there is nothing to copy from. Adding a key that copied the whole screen /// would not be the same feature under the same name. fn paste_into_pane(&mut self, _: &PasteIntoPane, _: &mut Window, cx: &mut Context) { let pane = self.panels.selected(); let Some(terminal) = self.live_terminals.get(&pane) else { return; }; if terminal.status.is_terminal() { return; } let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) else { return; }; if text.is_empty() { return; } let bytes = terminal.terminal.encode_paste(&text); if let Err(error) = self.send_runtime_command(pane, RuntimeCommand::Input(bytes)) && let Some(terminal) = self.live_terminals.get_mut(&pane) { terminal.status = LiveRuntimeStatus::Fault(error.to_string()); } cx.notify(); } fn add_panel_action(&mut self, _: &AddPanel, window: &mut Window, cx: &mut Context) { self.dispatch(ShellAction::CloseCommandPalette); self.add_panel_chooser_open = !self.add_panel_chooser_open; window.focus(&self.root_focus); cx.notify(); } fn detach_selected_panel( &mut self, _: &DetachSelectedPanel, window: &mut Window, cx: &mut Context, ) { self.detach_panel(self.panels.selected(), window, cx); } fn move_focus(&mut self, delta: isize, window: &mut Window, cx: &mut Context) { self.timing.mark_dispatch(); if self.panels.move_horizontal(delta) { self.persist_panels(); } window.focus(&self.root_focus); cx.notify(); } fn focus_left(&mut self, _: &FocusLeft, window: &mut Window, cx: &mut Context) { self.move_focus(-1, window, cx); } fn focus_right(&mut self, _: &FocusRight, window: &mut Window, cx: &mut Context) { self.move_focus(1, window, cx); } fn focus_up(&mut self, _: &FocusUp, window: &mut Window, cx: &mut Context) { self.move_focus(-1, window, cx); } fn focus_down(&mut self, _: &FocusDown, window: &mut Window, cx: &mut Context) { self.move_focus(1, window, cx); } fn open_palette(&mut self, _: &OpenPalette, _: &mut Window, cx: &mut Context) { self.add_panel_chooser_open = false; self.dispatch(ShellAction::OpenCommandPalette); cx.notify(); } fn close_palette(&mut self, _: &ClosePalette, _: &mut Window, cx: &mut Context) { self.dispatch(ShellAction::CloseCommandPalette); cx.notify(); } fn selected_terminal_dimensions(&self) -> Option<(PanelId, TerminalDimensions)> { let pane = self.panels.selected(); self.live_terminals .get(&pane) .map(|terminal| (pane, terminal.terminal.dimensions())) } fn terminal_taller(&mut self, _: &TerminalTaller, _: &mut Window, cx: &mut Context) { let Some((pane, dimensions)) = self.selected_terminal_dimensions() else { return; }; if self.resize_terminal( pane, dimensions.rows().saturating_add(TERMINAL_ROW_STEP), dimensions.columns(), ) { cx.notify(); } } fn terminal_shorter(&mut self, _: &TerminalShorter, _: &mut Window, cx: &mut Context) { let Some((pane, dimensions)) = self.selected_terminal_dimensions() else { return; }; if self.resize_terminal( pane, dimensions.rows().saturating_sub(TERMINAL_ROW_STEP).max(2), dimensions.columns(), ) { cx.notify(); } } fn terminal_wider(&mut self, _: &TerminalWider, _: &mut Window, cx: &mut Context) { let Some((pane, dimensions)) = self.selected_terminal_dimensions() else { return; }; if self.resize_terminal( pane, dimensions.rows(), dimensions.columns().saturating_add(TERMINAL_COLUMN_STEP), ) { cx.notify(); } } fn terminal_narrower(&mut self, _: &TerminalNarrower, _: &mut Window, cx: &mut Context) { let Some((pane, dimensions)) = self.selected_terminal_dimensions() else { return; }; if self.resize_terminal( pane, dimensions.rows(), dimensions .columns() .saturating_sub(TERMINAL_COLUMN_STEP) .max(20), ) { cx.notify(); } } fn on_key_down(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context) { if self.add_panel_chooser_open { match event.keystroke.key.as_str() { "escape" => { self.add_panel_chooser_open = false; cx.notify(); } "1" => self.create_panel(PanelKind::Terminal, window, cx), "2" => self.create_panel(PanelKind::Browser, window, cx), "3" => self.create_panel(PanelKind::Markdown, window, cx), "4" => self.create_panel(PanelKind::Review, window, cx), _ => {} } return; } if !self.model.command_palette().is_open() { let pane = self.panels.selected(); // Typing into a pane with nothing to type into used to vanish // without a word: no beep, no message, no indication that the // keystroke had gone anywhere. Say where it went. match self.live_terminals.get(&pane) { None => { self.input_gap = Some( "This pane has no terminal. Select a terminal pane to type into it." .to_owned(), ); cx.notify(); return; } Some(terminal) if terminal.status.is_terminal() => { self.input_gap = Some("This pane's process has ended. ⌘⌥R restarts it.".to_owned()); cx.notify(); return; } Some(_) => self.input_gap = None, } // Any key that is not the confirmation cancels a pending terminate. self.pending_terminate = None; let modifiers = key_modifiers(event); if let Some(scroll) = terminal_scroll_from_parts(event.keystroke.key.as_str(), modifiers) { self.scroll_terminal(pane, scroll, cx); return; } let Some(event) = terminal_key_from_parts( event.keystroke.key.as_str(), event.keystroke.key_char.as_deref(), modifiers, ) else { return; }; let Some(bytes) = self .live_terminals .get(&pane) .map(|terminal| terminal.terminal.encode_key(&event)) else { return; }; if bytes.is_empty() { return; } if let Err(error) = self.send_runtime_command(pane, RuntimeCommand::Input(bytes)) && let Some(terminal) = self.live_terminals.get_mut(&pane) { terminal.status = LiveRuntimeStatus::Fault(error.to_string()); } let moved_to_bottom = self.live_terminals.get_mut(&pane).is_some_and(|terminal| { terminal.terminal.display_offset() > 0 && terminal.terminal.scroll_display(TerminalScroll::Bottom) }); if moved_to_bottom { self.publish_terminal_snapshot(pane); } cx.notify(); return; } let mut query = self.model.command_palette().query().to_owned(); match event.keystroke.key.as_str() { "backspace" => { query.pop(); } // Enter and Escape both dismiss: there is nothing to commit yet. // When the palette runs a command they diverge again. "enter" | "escape" => { self.dispatch(ShellAction::CloseCommandPalette); cx.notify(); return; } _ => { if let Some(character) = event.keystroke.key_char.as_deref() && !event.keystroke.modifiers.control && !event.keystroke.modifiers.platform { query.push_str(character); } else { return; } } } self.dispatch(ShellAction::SetCommandPaletteQuery(query)); cx.notify(); } fn terminal_run(run: TerminalPaintRun, theme: ThemeColors) -> gpui::AnyElement { let cursor = run.cursor; div() .flex_none() .h(px(TERMINAL_CELL_HEIGHT)) .w(px(f32::from(run.columns) * TERMINAL_CELL_WIDTH)) .overflow_hidden() .whitespace_nowrap() .text_color(run.foreground) .bg(run.background) .when(run.style.contains(TerminalCellStyle::BOLD), |view| { view.font_weight(FontWeight::BOLD) }) .when(run.style.contains(TerminalCellStyle::ITALIC), |view| { view.italic() }) .when( run.style.contains(TerminalCellStyle::UNDERLINE) || run.style.contains(TerminalCellStyle::DOUBLE_UNDERLINE) || run.style.contains(TerminalCellStyle::UNDERCURL) || run.style.contains(TerminalCellStyle::DOTTED_UNDERLINE) || run.style.contains(TerminalCellStyle::DASHED_UNDERLINE) || run.hyperlink, gpui::Styled::underline, ) .when(run.style.contains(TerminalCellStyle::STRIKEOUT), |view| { view.line_through() }) .when(cursor == Some(TerminalCursorShape::Block), |view| { view.bg(theme.text).text_color(theme.chrome) }) .when(cursor == Some(TerminalCursorShape::HollowBlock), |view| { view.border_1().border_color(theme.text) }) .when(cursor == Some(TerminalCursorShape::Underline), |view| { view.border_b_2().border_color(theme.text) }) .when(cursor == Some(TerminalCursorShape::Beam), |view| { view.border_l_2().border_color(theme.text) }) .child(run.text) .into_any_element() } fn panel_view(&self, id: PanelId) -> PanelView { let panel = self.panels.panel(id).expect("workspace panel exists"); if let Some(seed) = panel.seed { let fixture = self.model.pane(seed.fixture_id()); return PanelView { id, kind: fixture.kind(), title: panel.title.clone(), badge: fixture.fixture().badge.to_owned(), target: panel.target.clone(), status: fixture.status(), lines: fixture.lines().to_vec(), output_source: fixture.output_source(), }; } let (kind, badge, lines) = match panel.kind { PanelKind::Terminal => (SurfaceKind::Terminal, "TERMINAL", Vec::new()), PanelKind::Browser => ( SurfaceKind::Browser, "BROWSER", vec![ "about:blank".to_owned(), "No web engine is embedded in this build.".to_owned(), "Choose a URL or open externally ↗".to_owned(), ], ), PanelKind::Markdown => ( SurfaceKind::Markdown, "MARKDOWN", vec![ "# Untitled note".to_owned(), String::new(), "Local-first workspace document.".to_owned(), ], ), PanelKind::Review => ( SurfaceKind::Review, "REVIEW", vec![ "No working tree is loaded.".to_owned(), "No approval has been requested.".to_owned(), "0 files selected".to_owned(), ], ), }; PanelView { id, kind, title: panel.title.clone(), badge: badge.to_owned(), target: panel.target.clone(), status: PaneStatus::Ready, lines, output_source: if panel.kind == PanelKind::Terminal { OutputSource::External } else { OutputSource::Deterministic }, } } fn terminal_view(&self, pane: PanelId, cx: &mut Context) -> gpui::AnyElement { let theme = self.theme.colors; // A pane can outlive its runtime state — a failed spawn, a terminate, a // restored snapshot whose process is gone. Saying so beats panicking in // the middle of a paint. let Some(terminal) = self.live_terminals.get(&pane) else { return div() .flex() .size_full() .items_center() .justify_center() .text_sm() .text_color(theme.muted) .child("No runtime is attached to this pane.") .into_any_element(); }; let scroll_pane = pane; let rows = terminal_paint_rows(&terminal.snapshot, theme) .into_iter() .map(|runs| { div() .flex() .h(px(TERMINAL_CELL_HEIGHT)) .flex_none() .children(runs.into_iter().map(|run| Self::terminal_run(run, theme))) }) .collect::>(); div() .flex() .flex_col() .size_full() .overflow_hidden() .bg(theme.chrome) .font_family("monospace") .text_size(px(13.0)) // Shift+PageUp was the only way into scrollback, which is not // something anyone guesses. A wheel is how people scroll. .on_scroll_wheel( cx.listener(move |shell, event: &gpui::ScrollWheelEvent, _, cx| { let delta = event.delta.pixel_delta(px(TERMINAL_CELL_HEIGHT)); let lines = (f32::from(delta.y) / TERMINAL_CELL_HEIGHT).clamp(-64.0, 64.0); #[expect( clippy::cast_possible_truncation, reason = "clamped into -64..=64 on the line above" )] let rows = lines.trunc() as i32; // A wheel notch shorter than one row still scrolls one row, // otherwise a fine-grained trackpad does nothing at all. let rows = if rows == 0 { if lines > 0.0 { 1 } else if lines < 0.0 { -1 } else { return; } } else { rows }; shell.scroll_terminal(scroll_pane, TerminalScroll::Delta(rows), cx); }), ) .children(rows) .into_any_element() } #[allow( clippy::too_many_lines, reason = "a single declarative element tree, not a sequence of steps" )] fn pane_context( &self, pane: &PanelView, selected: bool, cx: &mut Context, ) -> gpui::AnyElement { let theme = self.theme.colors; let pane_id = pane.id; let can_detach = self.panels.attached_count() > 1; let external = pane.output_source == OutputSource::External; // A pane can be marked external and still have no runtime: a failed // spawn, a terminated session, a snapshot restored past its process. let live_terminal = external .then(|| self.live_terminals.get(&pane_id)) .flatten(); let status = live_terminal.map_or(pane.badge.as_str(), |terminal| terminal.status.badge()); let detail = live_terminal.map_or_else(|| pane.target.clone(), |terminal| terminal.status.detail()); let surface_status = live_terminal.map_or_else( || status.to_owned(), |terminal| { let dimensions = terminal.terminal.dimensions(); if terminal.snapshot.display_offset > 0 { format!( "{} · ↑{} · {}×{}", status, terminal.snapshot.display_offset, dimensions.columns(), dimensions.rows() ) } else { format!( "{} · {}×{}", status, dimensions.columns(), dimensions.rows() ) } }, ); let native = SurfaceTab::native_for(pane.kind); let shown = self.surface_for(pane.id, pane.kind); let tabs = SurfaceTab::ALL .into_iter() .map(|tab| { let active = tab == shown; let provided = tab == native; div() .id(("surface-tab", pane_id.get() * 16 + tab.ordinal())) .cursor_pointer() .h_full() .flex() .items_center() .px_3() .text_xs() // Provided but unselected reads as available; unprovided // stays dim, so the tab strip shows what this pane can do // before you click rather than after. .text_color(if active { theme.accent } else if provided { theme.text } else { theme.muted }) .when(active, |view| view.border_b_2().border_color(theme.accent)) .hover(|view| view.bg(theme.surface_active).text_color(theme.text)) .child(tab.label()) .on_click(cx.listener(move |shell, _, window, cx| { cx.stop_propagation(); shell.select_surface(pane_id, tab, window, cx); })) }) .collect::>(); div() .flex() .flex_col() .size_full() .min_h_0() .overflow_hidden() .px_3() .py_2() .bg(if selected { theme.surface_active } else { theme.surface_raised }) .border_b_1() .border_color(theme.border) .child( div() .flex() .items_center() .justify_between() .gap_2() .child( div() .min_w_0() .child( div() .truncate() .text_sm() .text_color(theme.text) .child(pane.title.clone()), ) .child( div() .mt_1() .truncate() .text_xs() .text_color(theme.muted) .child(detail), ), ) .child( div() .flex() .flex_col() .items_end() .flex_none() .gap_1() .child( div() .text_xs() .text_color(if pane.needs_input() { theme.attention } else if selected { theme.success } else { theme.muted }) .child(surface_status), ) .child( div() .id(("detach-panel", pane_id.get())) .cursor_pointer() .px_2() .py_1() .rounded(px(4.0)) .border_1() .border_color(theme.border) .text_xs() .text_color(if can_detach { theme.muted } else { theme.border }) .when(can_detach, |view| { view.hover(|view| { view.border_color(theme.attention) .text_color(theme.attention) }) }) .child(if can_detach { "− DETACH" } else { "LAST PANEL" }) .on_click(cx.listener(move |shell, _, window, cx| { cx.stop_propagation(); shell.detach_panel(pane_id, window, cx); })), ), ), ) .child( div() .flex() .h(px(28.0)) .flex_none() .mt_2() .overflow_hidden() .border_t_1() .border_color(theme.border_quiet) .children(tabs), ) .child( div() .flex() .items_center() .justify_between() .mt_2() .text_xs() .text_color(theme.muted) .child("TOOLS · CONTEXT · GOAL") .child(if pane.needs_input() { "NEEDS INPUT" } else if selected { "KEYBOARD OWNER" } else { "RUNNING" }), ) .into_any_element() } /// The surface a panel is currently being viewed through. fn surface_for(&self, id: PanelId, kind: SurfaceKind) -> SurfaceTab { self.surfaces .get(&id) .copied() .unwrap_or_else(|| SurfaceTab::native_for(kind)) } /// What a pane shows when asked for a surface it does not provide. /// /// It says so, and it says what is still running underneath, because the /// pane's process identity does not change when the view does. fn unavailable_surface( pane: &PanelView, shown: SurfaceTab, theme: ThemeColors, ) -> gpui::AnyElement { let native = SurfaceTab::native_for(pane.kind); div() .flex() .flex_col() .size_full() .min_w_0() .min_h_0() .overflow_hidden() .bg(theme.chrome) .border_y_1() .border_color(theme.border) .child( div() .flex() .flex_col() .gap_2() .p_4() .child( div() .text_sm() .text_color(theme.text) .child(format!("{} unavailable", shown.label())), ) .child( div() .text_xs() .text_color(theme.muted) .child(shown.unavailable_reason()), ) .child( div() .mt_2() .text_xs() .text_color(theme.success) .child(format!( "{} is still this pane's live surface. Nothing was stopped.", native.label() )), ), ) .into_any_element() } /// What a pane shows when its process has ended. /// /// The stale screen stays, dimmed, rather than being cleared: what the /// process last printed is usually why it stopped. Decision 0010 wants a /// terminated session to be visibly terminated, not silently blank. fn exited_banner(status: &LiveRuntimeStatus, theme: ThemeColors) -> Option { let (label, tone) = match status { LiveRuntimeStatus::Exited(detail) => (format!("Process ended · {detail}"), theme.muted), LiveRuntimeStatus::Fault(detail) => (format!("Runtime fault · {detail}"), theme.danger), LiveRuntimeStatus::Starting | LiveRuntimeStatus::Running { .. } => return None, }; Some( div() .flex_none() .px_3() .py_1() .text_xs() .bg(theme.danger_container) .text_color(tone) .child(format!("{label} · ⌘⌥R restarts it")) .into_any_element(), ) } /// The confirmation decision 0010 requires before a process is ended. /// /// It names the process and says what will happen, and it is a separate /// keystroke rather than a close icon, because detaching a pane and killing /// its process are different operations with different consequences. fn terminate_confirmation( &self, pane: PanelId, theme: ThemeColors, ) -> Option { if self.pending_terminate != Some(pane) { return None; } let process = self.live_terminals.get(&pane).map_or_else( || "this pane".to_owned(), |terminal| match terminal.status { LiveRuntimeStatus::Running { process_id: Some(id), .. } => format!("pid {id} on this machine"), _ => "this pane's process".to_owned(), }, ); Some( div() .flex_none() .px_3() .py_1() .text_xs() .bg(theme.danger_container) .border_1() .border_color(theme.danger) .text_color(theme.text) .child(format!( "Terminate session? {process} will be ended. ⌘⌥X again to confirm, any other key to cancel." )) .into_any_element(), ) } fn pane_work_surface(&self, pane: &PanelView, cx: &mut Context) -> gpui::AnyElement { let theme = self.theme.colors; let shown = self.surface_for(pane.id, pane.kind); if shown != SurfaceTab::native_for(pane.kind) { return Self::unavailable_surface(pane, shown, theme); } let external = pane.output_source == OutputSource::External; let content = if external { self.terminal_view(pane.id, cx) } else { let start = pane.lines.len().saturating_sub(18); div() .flex() .flex_col() .gap_1() .size_full() .overflow_hidden() .font_family("monospace") .text_sm() .text_color(theme.text) .children(pane.lines[start..].iter().cloned()) .into_any_element() }; let banner = self .live_terminals .get(&pane.id) .and_then(|terminal| Self::exited_banner(&terminal.status, theme)); div() .flex() .flex_col() .size_full() .min_w_0() .min_h_0() .overflow_hidden() .children(self.terminate_confirmation(pane.id, theme)) .children(banner) .children( self.input_gap .as_ref() .filter(|_| pane.id == self.panels.selected()) .map(|reason| { div() .flex_none() .px_3() .py_1() .text_xs() .bg(theme.surface_raised) .text_color(theme.attention) .child(reason.clone()) }), ) .bg(theme.chrome) .border_y_1() .border_color(theme.border) .child( div() .flex_1() .min_h_0() .overflow_hidden() .p_3() .child(content), ) .into_any_element() } #[allow( clippy::too_many_lines, reason = "a single declarative element tree, not a sequence of steps" )] fn pane_decision_region(&self, pane: &PanelView, cx: &mut Context) -> gpui::AnyElement { let theme = self.theme.colors; let pane_id = pane.id; let picked = self.shelf_choice.get(&pane_id).copied(); // Every choice names the capability it would need. None of them holds // that capability: choosing prepares, the command plane executes. let choices: [(&'static str, &'static str, &'static str, bool); 3] = if pane.needs_input() { [ ("Review request", "inspect scope", "needs Observe", true), ("Steer…", "edit response", "needs Execute", false), ("Dismiss", "leave inert", "inert", false), ] } else { [ ("Continue", "keep moving", "needs Execute", false), ("Review plan", "inspect commands", "needs Observe", false), ("Ask…", "refine prompt", "inert", false), ] }; div() .flex() .flex_col() .size_full() .min_h_0() .overflow_hidden() .px_2() .py_2() .bg(theme.surface) .border_t_1() .border_color(theme.border) .child( div() .flex() .items_center() .justify_between() .text_xs() .child(div().text_color(theme.muted).child("DECISION SHELF")) .child( div() .text_color(if pane.needs_input() { theme.attention } else { theme.muted }) .child(if pane.needs_input() { "REVIEW REQUIRED" } else { "SUGGESTIONS INERT" }), ), ) .child(div().flex().flex_col().gap_1().mt_2().children( choices.into_iter().enumerate().map( |(index, (label, detail, _capability, attention))| { let chosen = picked == Some(index); div() .id(("shelf-choice", pane_id.get() * 8 + index as u64)) .cursor_pointer() .flex() .items_center() .justify_between() .min_w_0() .px_2() .py_1() .rounded(px(4.0)) .border_1() .border_color(if chosen { theme.accent } else if attention { theme.attention } else { theme.border }) .bg(if chosen { theme.surface_active } else { theme.surface_raised }) .hover(|view| view.border_color(theme.accent)) .text_xs() .text_color(theme.text) .child(label) .child(div().truncate().text_color(theme.muted).child(detail)) .on_click(cx.listener(move |shell, _, window, cx| { cx.stop_propagation(); shell.select_shelf_choice(pane_id, index, window, cx); })) }, ), )) .child( div() .mt_2() .text_xs() .text_color(if picked.is_some() { theme.accent } else { theme.muted }) .child(picked.map_or_else( || "Pick one to see what it would need".to_owned(), |index| { format!( "{} · {} · nothing has run", choices[index].0, choices[index].2 ) }, )), ) .into_any_element() } fn workspace_panel( &self, pane: &PanelView, selected: bool, cx: &mut Context, ) -> gpui::AnyElement { let theme = self.theme.colors; let pane_id = pane.id; div() .id(("workspace-panel", pane_id.get())) .cursor_pointer() .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(if selected { theme.accent } else { theme.border }) .child( div() .h(relative(0.20)) .flex_none() .min_h_0() .overflow_hidden() .child(self.pane_context(pane, selected, cx)), ) .child( div() .h(relative(0.60)) .flex_none() .min_h_0() .overflow_hidden() .child(self.pane_work_surface(pane, cx)), ) .child( div() .h(relative(0.20)) .flex_none() .min_h_0() .overflow_hidden() .child(self.pane_decision_region(pane, cx)), ) .into_any_element() } #[allow( clippy::too_many_lines, reason = "a single declarative element tree, not a sequence of steps" )] fn command_palette(&self) -> impl IntoElement { let theme = self.theme.colors; let query = self.model.command_palette().query(); let prompt = if query.is_empty() { "Type a command…".to_owned() } else { query.to_owned() }; div() .absolute() .inset_0() .flex() .justify_center() .items_start() .pt(px(92.0)) .bg(gpui::black().opacity(0.72)) .child( div() .w(px(620.0)) .overflow_hidden() .bg(theme.surface_raised) .border_1() .border_color(theme.accent) .rounded(px(8.0)) .child( div() .px_4() .py_3() .border_b_1() .border_color(theme.border) .text_color(if query.is_empty() { theme.muted } else { theme.text }) .child(prompt), ) .child( div() .flex() .flex_col() .p_2() .text_sm() .child( div() .px_3() .py_2() .rounded(px(5.0)) .bg(theme.surface_active) .child("Focus next pane") .child( div() .mt_1() .text_xs() .text_color(theme.muted) .child("Workspace · navigation"), ), ) .child( div() .px_3() .py_2() .text_color(theme.muted) .child("Add workspace panel") .child( div() .mt_1() .text_xs() .child("Alt+Shift+N · choose a surface or reattach"), ), ) .child( div() .px_3() .py_2() .text_color(theme.muted) .child("Detach selected panel") .child( div() .mt_1() .text_xs() .child("Alt+Shift+W · session keeps running"), ), ) .child( div() .px_3() .py_2() .text_color(theme.muted) .child("Share selected pane to Buzz…"), ), ) .child( div() .flex() .justify_between() .px_4() .py_2() .border_t_1() .border_color(theme.border) .text_xs() .text_color(theme.muted) .child("Enter to run") .child("Esc to close"), ), ) } #[allow( clippy::too_many_lines, reason = "a single declarative element tree, not a sequence of steps" )] fn add_panel_chooser(&self, cx: &mut Context) -> impl IntoElement { let theme = self.theme.colors; let kinds = PanelKind::ALL .into_iter() .enumerate() .map(|(index, kind)| { let shortcut = index + 1; div() .id(("create-panel-kind", shortcut)) .cursor_pointer() .flex() .items_center() .gap_3() .px_3() .py_3() .rounded(px(6.0)) .border_1() .border_color(theme.border) .bg(theme.surface) .child( div() .flex() .items_center() .justify_center() .size(px(34.0)) .flex_none() .rounded(px(5.0)) .bg(theme.surface_active) .text_color(theme.accent) .child(match kind { PanelKind::Terminal => ">_", PanelKind::Browser => "◎", PanelKind::Markdown => "¶", PanelKind::Review => "±", }), ) .child( div() .min_w_0() .flex_1() .child(div().text_sm().text_color(theme.text).child(kind.label())) .child( div() .mt_1() .text_xs() .text_color(theme.muted) .child(kind.description()), ), ) .child( div() .flex_none() .text_xs() .text_color(theme.muted) .child(shortcut.to_string()), ) .on_click(cx.listener(move |shell, _, window, cx| { cx.stop_propagation(); shell.create_panel(kind, window, cx); })) }) .collect::>(); let detached = self .panels .detached_ids() .into_iter() .map(|id| { let panel = self.panels.panel(id).expect("detached panel exists"); (id, panel.title.clone(), panel.kind.label()) }) .collect::>(); div() .absolute() .inset_0() .flex() .justify_center() .items_start() .pt(px(72.0)) .bg(gpui::black().opacity(0.72)) .child( div() .w(px(620.0)) .max_h(px(760.0)) .overflow_hidden() .rounded(px(9.0)) .border_1() .border_color(theme.accent) .bg(theme.surface_raised) .child( div() .flex() .items_start() .justify_between() .px_4() .py_3() .border_b_1() .border_color(theme.border) .child( div() .child( div() .text_lg() .text_color(theme.text) .child("Add workspace panel"), ) .child( div() .mt_1() .text_xs() .text_color(theme.muted) .child("Created beside the selected pane with a persistent local identity."), ), ) .child( div() .id("close-add-panel-chooser") .cursor_pointer() .px_2() .py_1() .rounded(px(4.0)) .border_1() .border_color(theme.border) .text_xs() .text_color(theme.muted) .child("ESC") .on_click(cx.listener(|shell, _, _, cx| { shell.add_panel_chooser_open = false; cx.notify(); })), ), ) .child(div().flex().flex_col().gap_2().p_3().children(kinds)) .when(!detached.is_empty(), |view| { view.child( div() .px_4() .pt_2() .pb_1() .border_t_1() .border_color(theme.border) .text_xs() .text_color(theme.muted) .child("REATTACH RUNNING SESSION"), ) .children(detached.into_iter().map(|(id, title, kind)| { div() .id(("chooser-reattach", id.get())) .cursor_pointer() .flex() .items_center() .justify_between() .mx_3() .mb_2() .px_3() .py_2() .rounded(px(5.0)) .bg(theme.surface) .border_1() .border_color(theme.border) .child( div() .child(div().text_sm().text_color(theme.text).child(title)) .child( div() .mt_1() .text_xs() .text_color(theme.muted) .child(format!("{kind} · pane-{}", id.get())), ), ) .child(div().text_xs().text_color(theme.accent).child("REATTACH")) .on_click(cx.listener(move |shell, _, window, cx| { shell.add_panel_chooser_open = false; shell.attach_panel(id, window, cx); })) })) }) .child( div() .flex() .justify_between() .px_4() .py_2() .border_t_1() .border_color(theme.border) .text_xs() .text_color(theme.muted) .child("1–4 create · click reattaches") .child("Esc closes"), ), ) } /// Attached panels whose agent is blocked on a human answer. /// /// The count is derived rather than declared: a sidebar that claims one /// pane needs input while no pane is blocked is the same class of untruth /// as an invented usage number. /// Panes waiting on a human, most trustworthy first. /// /// Read from observed signals rather than from the fixture's /// `needs_input` flag, which no live pane ever set — the count was frozen /// at whatever the demo data said. fn attention_rows(&self) -> Vec { self.attention .signals() .iter() .filter_map(|signal| { let panel = self.panels.panel(signal.pane)?; Some(AttentionRow { id: signal.pane, title: panel.title.clone(), reason: signal.kind.label(), source: signal.source.label(), countable: signal.is_countable(), }) }) .collect() } /// Where sessions are owned, and what each host is actually doing. fn runtime_rows(&self) -> Vec { let theme = self.theme.colors; let running = self .live_terminals .values() .filter(|terminal| matches!(terminal.status, LiveRuntimeStatus::Running { .. })) .count(); let total = self.live_terminals.len(); let local = RuntimeRow { label: "this machine", detail: format!("{running}/{total} live PTYs"), tone: if running == 0 { theme.muted } else { theme.success }, }; // The usage adapter is neither a host nor an endpoint, but its health // belongs beside them: it is the reason the footer can or cannot answer. let adapter = RuntimeRow { label: "usage adapter", detail: format!( "{} · {}/{} profiles reporting", self.usage.probe_status(), self.usage.reporting_profile_count(), self.usage.declared_profile_count() ), tone: if self.usage.reporting_profile_count() == 0 { theme.muted } else { theme.success }, }; vec![local, adapter] } /// The pane tab strip above the workspace row. #[allow( clippy::too_many_lines, reason = "a single declarative element tree, not a sequence of steps" )] fn render_tabs( attached_panes: &[PanelId], visible_count: usize, detached_count: usize, theme: ThemeColors, cx: &mut Context, ) -> gpui::AnyElement { let attached_count = attached_panes.len(); div() .flex() .items_center() .h(px(38.0)) .flex_none() .px_2() .gap_1() .bg(theme.surface) .border_b_1() .border_color(theme.border_quiet) // One tab, because there is one workspace. Two more used to sit // here looking like navigation and doing nothing. .child( div() .h_full() .flex() .items_center() .px_3() .border_b_2() .border_color(theme.accent) .text_sm() .child("Lumbridge Code"), ) .child(div().flex_1()) .child(div().px_3().text_xs().text_color(theme.muted).child(format!( "{visible_count} shown · {attached_count} attached · {detached_count} detached" ))) .child( div() .id("add-workspace-panel") .cursor_pointer() .ml_2() .px_3() .py_1() .rounded(px(4.0)) .border_1() .border_color(theme.accent) .text_xs() .text_color(theme.accent) .child("+ ADD PANEL ▾") .on_click(cx.listener(|shell, _, window, cx| { shell.dispatch(ShellAction::CloseCommandPalette); shell.add_panel_chooser_open = !shell.add_panel_chooser_open; window.focus(&shell.root_focus); cx.notify(); })), ) .into_any_element() } /// Assembles the window: header, then sidebar beside the workspace, then footer. #[allow( clippy::too_many_lines, reason = "a single declarative element tree, not a sequence of steps" )] fn render_root(&self, parts: RootParts, cx: &mut Context) -> gpui::AnyElement { let theme = self.theme.colors; let RootParts { sidebar, tabs, workspace_row, footer_left, timing, runtime_summary, running_runtime_count, } = parts; div() .id("lumbridge-shell") .relative() .track_focus(&self.root_focus) .key_context("LumbridgeShell") .on_click(cx.listener(|shell, _, window, _| { window.focus(&shell.root_focus); })) .on_action(cx.listener(Self::focus_left)) .on_action(cx.listener(Self::focus_right)) .on_action(cx.listener(Self::focus_up)) .on_action(cx.listener(Self::focus_down)) .on_action(cx.listener(Self::open_palette)) .on_action(cx.listener(Self::close_palette)) .on_action(cx.listener(Self::add_panel_action)) .on_action(cx.listener(Self::detach_selected_panel)) .on_action(cx.listener(Self::restart_pane)) .on_action(cx.listener(Self::terminate_pane)) .on_action(cx.listener(Self::paste_into_pane)) .on_action(cx.listener(Self::terminal_taller)) .on_action(cx.listener(Self::terminal_shorter)) .on_action(cx.listener(Self::terminal_wider)) .on_action(cx.listener(Self::terminal_narrower)) .on_action(cx.listener(|shell, _: &SelectPane1, window, cx| { shell.select_pane_at(0, window, cx); })) .on_action(cx.listener(|shell, _: &SelectPane2, window, cx| { shell.select_pane_at(1, window, cx); })) .on_action(cx.listener(|shell, _: &SelectPane3, window, cx| { shell.select_pane_at(2, window, cx); })) .on_action(cx.listener(|shell, _: &SelectPane4, window, cx| { shell.select_pane_at(3, window, cx); })) .on_action(cx.listener(|shell, _: &SelectPane5, window, cx| { shell.select_pane_at(4, window, cx); })) .on_action(cx.listener(|shell, _: &SelectPane6, window, cx| { shell.select_pane_at(5, window, cx); })) .on_key_down(cx.listener(Self::on_key_down)) .flex() .flex_col() .size_full() .bg(theme.chrome) .text_color(theme.text) .child( div() .flex() .items_center() .h(px(48.0)) .flex_none() .px_4() .bg(theme.surface_raised) .border_b_1() .border_color(theme.border_quiet) .child(div().text_lg().child("Lumbridge")) .child( div() .ml_3() .text_sm() .text_color(theme.muted) .child("Lumbridge Code"), ) .child(div().flex_1()) // The same tone rule runtime_rows applies: nothing live is // not a success, and "0/5 LIVE PTYS" in green said it was. .child( div() .mr_4() .text_xs() .text_color(if running_runtime_count == 0 { theme.muted } else { theme.success }) .child(runtime_summary), ) .child( div() .text_sm() .text_color(theme.accent) .child("Ctrl/⌘ K · Commands"), ), ) .child( div().flex().flex_1().min_h_0().child(sidebar).child( div() .flex() .flex_col() .flex_1() .min_w_0() .min_h_0() .overflow_hidden() .child(tabs) .child(workspace_row), ), ) .child( div() .flex() .items_center() .justify_between() .h(px(APP_FOOTER_HEIGHT)) .flex_none() .px_3() .gap_4() .bg(theme.surface_raised) .border_t_1() .border_color(theme.border_quiet) .text_xs() .text_color(theme.muted) .child( div() .flex() .flex_col() .flex_none() .min_w_0() .child(div().truncate().child(footer_left)) .child(div().truncate().child(timing)), ) .child(div().flex_1()) .child(self.footer_usage_zone()), ) .when(self.model.command_palette().is_open(), |view| { view.child(self.command_palette()) }) .when(self.add_panel_chooser_open, |view| { view.child(self.add_panel_chooser(cx)) }) .into_any_element() } /// The left rail: what needs you, what is running, what is detached. /// /// Split out of `render`, which was 353 lines. It is the first thing the /// sidebar rework needs, and a 350-line render function is where a UI stops /// being reviewable. #[allow( clippy::too_many_lines, reason = "a single declarative element tree; the sidebar rework replaces it wholesale" )] fn render_sidebar( &self, attention_rows: &[AttentionRow], detached_entries: &[(PanelId, String)], cx: &mut Context, ) -> gpui::AnyElement { let theme = self.theme.colors; let waiting = self.attention.countable(); div() .flex() .flex_col() .w(px(248.0)) .flex_none() .bg(theme.surface) .border_r_1() .border_color(theme.border_quiet) .child( div() .px_4() .pt_4() .pb_2() .text_xs() .text_color(if waiting == 0 { theme.muted } else { theme.attention }) // The count is only what is certainly waiting. A guessed // signal still gets a row below, but it may not make the // sidebar assert that something needs you. .child(format!("ATTENTION · {waiting}")), ) .when(attention_rows.is_empty(), |view| { view.child( div() .mx_2() .mb_3() .px_3() .py_2() .text_xs() .text_color(theme.muted) .child("No pane is waiting on you"), ) }) .children(attention_rows.iter().map(|row| { let id = row.id; div() .id(("attention-panel", id.get())) .cursor_pointer() .mx_2() .mb_3() .px_3() .py_2() .rounded(px(5.0)) .bg(theme.surface_active) .border_1() // A guess is outlined quietly; a report is outlined in the // attention colour. The row looks like what it is. .border_color(if row.countable { theme.attention } else { theme.border }) .hover(|view| view.bg(theme.surface_raised)) .child( div() .text_sm() .text_color(theme.text) .child(row.title.clone()), ) .child( div() .mt_1() .text_xs() .text_color(theme.muted) .child(format!("{} · {}", row.reason, row.source)), ) .on_click(cx.listener(move |shell, _, window, cx| { shell.select_pane(id, window, cx); })) })) .child( div() .mt_3() .px_4() .py_2() .text_xs() .text_color(theme.muted) .child("RUNTIMES"), ) .children(self.runtime_rows().into_iter().map(|row| { div() .px_4() .py_1() .child(div().text_sm().text_color(row.tone).child(row.label)) .child(div().text_xs().text_color(theme.muted).child(row.detail)) })) .child( div() .px_4() .pt_2() .text_xs() .text_color(theme.muted) .child(self.persistence_status.clone()), ) .child( div() .mt_3() .px_4() .py_2() .text_xs() .text_color(theme.muted) .child(format!("DETACHED SESSIONS · {}", detached_entries.len())), ) .children(detached_entries.iter().map(|(pane, title)| { let (pane, title) = (*pane, title.clone()); div() .id(("detached-session", pane.get())) .cursor_pointer() .mx_2() .mb_1() .px_3() .py_2() .rounded(px(4.0)) .border_1() .border_color(theme.border_quiet) .hover(|view| view.border_color(theme.accent).text_color(theme.text)) .text_xs() .text_color(theme.muted) .child(format!("↪ {title}")) .on_click(cx.listener(move |shell, _, window, cx| { shell.attach_panel(pane, window, cx); })) })) .child(div().flex_1()) .into_any_element() } /// The standing of every harness, always, regardless of what is selected. /// /// The old footer answered only for the selected pane, so the moment you /// focused a plain shell your quota vanished. This strip keeps every /// harness on screen and leads with what is left, because that is the /// question. The selected harness expands with its rate and its trust. fn footer_usage_zone(&self) -> impl IntoElement { let theme = self.theme.colors; let active = self .panels .panel(self.panels.selected()) .and_then(|panel| self.usage.profile_for_seed(panel.seed)) .cloned(); let segments = self.usage.strip(); let orphan = active.is_none(); div() .flex() .items_center() .gap_4() .when(orphan, |view| { view.child( div() .flex_none() .text_color(theme.muted) .child("no harness on this pane"), ) }) // The harness name is printed once per group. Repeating // "CLAUDE CODE" in front of four of its own windows spends the // strip's width on a word the eye has already read. .children({ let mut previous: Option = None; segments .into_iter() .map(|segment| { let repeats = previous.as_deref() == Some(segment.label.as_str()); previous = Some(segment.label.clone()); let selected = active.as_ref() == Some(&segment.id); usage_segment(segment, selected, repeats, &self.theme) }) .collect::>() }) } } /// The provenance of a value is carried by the colour of its meter, so the /// strip reads at a glance without a legend on every segment. fn provenance_color(provenance: UsageProvenance, theme: &ActiveTheme) -> Rgba { theme.provenance(provenance) } /// A quota meter. The filled part is spent; the empty part is what is left, /// which is the way round an engineer reads it. fn usage_meter( consumed_permille: Option, color: Rgba, theme: ThemeColors, ) -> impl IntoElement { let Some(permille) = consumed_permille else { // No capsule. A full-length empty gauge reads as "plenty left" from // across the room, which is the opposite of the truth. A hairline is // visibly not a measurement. return div() .w(px(80.0)) .h(px(2.0)) .flex_none() .bg(theme.border) .into_any_element(); }; let clamped = u16::try_from(permille.min(1_000)).unwrap_or(1_000); div() .w(px(80.0)) .h(px(8.0)) .flex_none() .rounded(px(4.0)) .bg(theme.border_quiet) .overflow_hidden() .child( div() .h_full() .w(relative(f32::from(clamped) / 1_000.0)) .bg(color), ) .into_any_element() } /// One quota in the strip. /// /// `continues_group` means the harness above this one is the same, so its name /// is left off and only the window is named. fn usage_segment( segment: UsageSegment, selected: bool, continues_group: bool, active: &ActiveTheme, ) -> impl IntoElement { let theme = active.colors; let color = provenance_color(segment.provenance, active); let name_color = if selected { theme.text } else { theme.muted }; let headline_color = if segment.headline.is_none() { theme.muted } else if segment.critical { theme.attention } else { theme.text }; div() .flex() .items_center() .gap_2() .when(selected, |view| { view.px_2().py_1().rounded(px(4.0)).bg(theme.surface_active) }) .when(!continues_group, |view| { view.child( div() .flex_none() .text_color(name_color) .child(segment.label), ) }) // A quiet pill rather than more running text. The window's name is a // label on the number, not another number, and at border weight it was // simply invisible. .child( div() .flex_none() .px(px(5.0)) .py(px(1.0)) .rounded(px(3.0)) .bg(if selected { theme.border } else { theme.border_quiet }) .text_color(if selected { theme.text } else { theme.muted }) .child(segment.scope), ) .child(usage_meter(segment.consumed_permille, color, theme)) .child( div() .flex_none() .text_color(headline_color) .child(segment.headline.unwrap_or_else(|| "no reading".to_owned())), ) .when_some(segment.reset, |view, reset| { view.child(div().flex_none().text_color(theme.muted).child(reset)) }) // Only the harness you are looking at spends footer width on its rate // and its trust. The rest stay one glance wide. .when(selected, |view| { view.child(footer_separator(theme)) .child( div() .flex_none() .text_color(provenance_color(segment.burn_provenance, active)) .child(segment.burn), ) .child(provenance_chip(&segment.trust, segment.provenance, active)) }) } /// The pieces `render` assembles, bundled so the assembly step takes one /// argument rather than six. struct RootParts { sidebar: gpui::AnyElement, tabs: gpui::AnyElement, workspace_row: gpui::AnyElement, footer_left: String, timing: String, runtime_summary: String, running_runtime_count: usize, } /// One pane waiting on a human, as the sidebar shows it. struct AttentionRow { id: PanelId, title: String, /// What it wants, in words. reason: String, /// How we know. Shown so a guess never reads like a report. source: &'static str, countable: bool, } /// One host or endpoint row in the sidebar's runtime list. struct RuntimeRow { label: &'static str, detail: String, tone: Rgba, } fn footer_separator(theme: ThemeColors) -> impl IntoElement { div().text_color(theme.border).child("│") } /// The trust marker. Its colour is the provenance, never the value. fn provenance_chip( text: &str, provenance: UsageProvenance, active: &ActiveTheme, ) -> impl IntoElement { let theme = active.colors; let color = match provenance { UsageProvenance::ProviderReported => theme.success, UsageProvenance::HarnessReported => theme.accent, UsageProvenance::LocallyMeasured => theme.text, UsageProvenance::Estimated => theme.attention, UsageProvenance::Unavailable => theme.muted, }; div() .px_2() .py(px(1.0)) .rounded(px(3.0)) .border_1() .border_color(color) .text_color(color) .child(text.to_owned()) } impl Render for LumbridgeShell { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let panel_capacity = visible_panel_count(window.bounds().size); let attached_panes = self.panels.attached_ids(); let attached_count = attached_panes.len(); let detached_entries = self .panels .detached_ids() .into_iter() .map(|pane| { let title = self .panels .panel(pane) .expect("detached panel exists") .title .clone(); (pane, title) }) .collect::>(); let detached_count = detached_entries.len(); let visible_count = panel_capacity.min(attached_count).max(1); let running_runtime_count = self .live_terminals .values() .filter(|terminal| matches!(terminal.status, LiveRuntimeStatus::Running { .. })) .count(); let runtime_summary = format!( "{running_runtime_count}/{} LIVE PTYS", self.live_terminals.len() ); let attention_rows = self.attention_rows(); let sidebar = self.render_sidebar(&attention_rows, &detached_entries, cx); let selected_position = attached_panes .iter() .position(|pane| *pane == self.panels.selected()) .expect("the selected pane must remain attached"); let tabs = Self::render_tabs( &attached_panes, visible_count, detached_count, self.theme.colors, cx, ); let panel_range = visible_panel_range(attached_count, selected_position, visible_count); let workspace_panels = panel_range .map(|index| { let pane = self.panel_view(attached_panes[index]); div() .flex_1() .min_w_0() .min_h_0() .child(self.workspace_panel(&pane, pane.id == self.panels.selected(), cx)) }) .collect::>(); let workspace_row = div() .flex() .gap_1() .flex_1() .min_w_0() .min_h_0() .overflow_hidden() .children(workspace_panels); let counters = self.model.counters(); let terminal_revision = self .live_terminals .values() .map(|terminal| terminal.terminal.revision()) .sum::(); let footer_left = format!( "rev {} · {} PTYs · VT rev {} · {} focus · {} VT snapshots · {} synthetic lines", self.model.revision(), self.runtimes.len(), terminal_revision, counters.focus_moves, counters.external_snapshot_updates, counters.terminal_lines_appended ); let timing = self.timing.summary(); let root = self.render_root( RootParts { sidebar, tabs, workspace_row: workspace_row.into_any_element(), footer_left, timing, runtime_summary, running_runtime_count, }, cx, ); // Measured after the tree is built, which is what the sample means. self.timing.observe_element_build(); root } } fn key_modifiers(event: &KeyDownEvent) -> KeyModifiers { let mut modifiers = KeyModifiers::default(); if event.keystroke.modifiers.shift { modifiers = modifiers.union(KeyModifiers::SHIFT); } if event.keystroke.modifiers.alt { modifiers = modifiers.union(KeyModifiers::ALT); } if event.keystroke.modifiers.control { modifiers = modifiers.union(KeyModifiers::CONTROL); } if event.keystroke.modifiers.platform { modifiers = modifiers.union(KeyModifiers::PLATFORM); } modifiers } fn terminal_key_from_parts( key: &str, key_char: Option<&str>, modifiers: KeyModifiers, ) -> Option { let key = match key { "enter" => TerminalKey::Enter, "backspace" => TerminalKey::Backspace, "tab" => TerminalKey::Tab, "escape" => TerminalKey::Escape, "up" => TerminalKey::Up, "down" => TerminalKey::Down, "left" => TerminalKey::Left, "right" => TerminalKey::Right, "home" => TerminalKey::Home, "end" => TerminalKey::End, "insert" => TerminalKey::Insert, "delete" => TerminalKey::Delete, "pageup" => TerminalKey::PageUp, "pagedown" => TerminalKey::PageDown, function if function.strip_prefix('f').is_some_and(|suffix| { !suffix.is_empty() && suffix.bytes().all(|byte| byte.is_ascii_digit()) }) => { let number = function.strip_prefix('f')?.parse::().ok()?; TerminalKey::Function(number) } _ if !modifiers.contains(KeyModifiers::PLATFORM) => { // GPUI reports no `key_char` for a control chord, because ctrl-k // produces no printable character. Taking `key_char` alone meant // every control byte was dropped before it reached the PTY — not // just ctrl-k, but ctrl-c, ctrl-d, ctrl-a and ctrl-r as well, which // is most of what makes a terminal usable. Fall back to the key // name when it names a single character; the engine applies the // control transformation. let text = key_char .filter(|text| !text.is_empty()) .map(str::to_owned) .or_else(|| (key.chars().count() == 1).then(|| key.to_owned()))?; TerminalKey::Text(text) } _ => return None, }; Some(TerminalKeyEvent { key, modifiers }) } fn terminal_scroll_from_parts(key: &str, modifiers: KeyModifiers) -> Option { if !modifiers.contains(KeyModifiers::SHIFT) { return None; } match key { "pageup" => Some(TerminalScroll::PageUp), "pagedown" => Some(TerminalScroll::PageDown), "home" => Some(TerminalScroll::Top), "end" => Some(TerminalScroll::Bottom), _ => None, } } fn live_pty_script(seed: Option) -> &'static str { match seed { Some(SeedPane::CodexRuntime) => { "printf 'Codex runtime · independent Lumbridge PTY\\n'; exec /bin/sh -i" } Some(SeedPane::ClaudeUi) => { "printf 'Claude workspace · independent Lumbridge PTY\\n'; exec /bin/sh -i" } Some(SeedPane::PiDocs) => { "printf 'Pi docs · independent Lumbridge PTY\\n'; exec /bin/sh -i" } None => "printf 'Lumbridge terminal · persistent pane identity\\n'; exec /bin/sh -i", Some(SeedPane::Architecture | SeedPane::AcpPreview | SeedPane::RuntimeReview) => { unreachable!("only terminal fixtures own live PTYs") } } } fn spawn_live_runtime( runtimes: &mut RuntimeRegistry, pane: PanelId, seed: Option, dimensions: TerminalDimensions, ) -> Result<(), RuntimeRegistryError> { let command = CommandConfig::new("/bin/sh") .map_err(RuntimeActorError::Start)? .args(["-c", live_pty_script(seed)]); let pty_size = TerminalSize::new(dimensions.rows(), dimensions.columns()) .map_err(RuntimeActorError::Start)?; runtimes.spawn( pane, command, PtyOptions::new(pty_size), RuntimeActorOptions::default(), )?; Ok(()) } fn main() { Application::new().run(|cx: &mut App| { cx.bind_keys(keymap::bindings()); let bounds = Bounds::centered(None, size(px(1500.0), px(960.0)), cx); cx.open_window( WindowOptions { window_bounds: Some(WindowBounds::Windowed(bounds)), titlebar: Some(gpui::TitlebarOptions { title: Some("Lumbridge · GPUI workspace".into()), ..Default::default() }), ..Default::default() }, |window, cx| cx.new(|cx| LumbridgeShell::new(window, cx)), ) .expect("GPUI workspace window should open"); cx.activate(true); }); } #[cfg(test)] 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_panel_count, visible_panel_range, }; use gpui::{px, size}; #[test] fn maps_named_and_composed_gpui_keys_to_terminal_input() { let enter = terminal_key_from_parts("enter", None, KeyModifiers::default()).unwrap(); assert_eq!(enter.key, TerminalKey::Enter); let literal_f = terminal_key_from_parts("f", Some("f"), KeyModifiers::default()).unwrap(); assert_eq!(literal_f.key, TerminalKey::Text("f".into())); let function = terminal_key_from_parts("f12", None, KeyModifiers::default()).unwrap(); assert_eq!(function.key, TerminalKey::Function(12)); let composed = terminal_key_from_parts("é", Some("é"), KeyModifiers::default()).unwrap(); assert_eq!(composed.key, TerminalKey::Text("é".into())); } /// The regression this test exists for: a control chord carries no /// printable character, so requiring one dropped every control byte. #[test] fn a_control_chord_still_reaches_the_terminal_without_a_key_char() { for letter in ["k", "c", "d", "a", "r"] { let event = terminal_key_from_parts(letter, None, KeyModifiers::CONTROL) .unwrap_or_else(|| panic!("ctrl-{letter} must reach the terminal")); assert_eq!(event.key, TerminalKey::Text(letter.to_owned())); assert!(event.modifiers.contains(KeyModifiers::CONTROL)); } } #[test] fn a_printable_character_still_wins_over_the_key_name() { // Shift-a arrives as key "a" with key_char "A"; the character is what // the terminal should receive. let event = terminal_key_from_parts("a", Some("A"), KeyModifiers::SHIFT).expect("shift-a"); assert_eq!(event.key, TerminalKey::Text("A".to_owned())); } #[test] fn a_named_key_with_no_character_is_still_refused() { // Multi-character key names that are not in the table above are not // text and must not be sent as though they were. assert!(terminal_key_from_parts("capslock", None, KeyModifiers::default()).is_none()); assert!(terminal_key_from_parts("shift", None, KeyModifiers::default()).is_none()); } #[test] fn reserves_platform_text_shortcuts_for_the_workspace() { assert!(terminal_key_from_parts("c", Some("c"), KeyModifiers::PLATFORM).is_none()); let control = terminal_key_from_parts("c", Some("c"), KeyModifiers::CONTROL).unwrap(); assert_eq!(control.key, TerminalKey::Text("c".into())); } #[test] fn terminal_geometry_tracks_middle_sixty_percent_per_panel() { let dimensions = terminal_dimensions_for_window(size(px(1500.0), px(960.0)), 5); assert_eq!(dimensions.rows(), 26); assert_eq!(dimensions.columns(), 45); let ultrawide = size(px(3440.0), px(1440.0)); assert_eq!(visible_panel_count(ultrawide), 5); let dimensions = terminal_dimensions_for_window(ultrawide, 5); assert_eq!(dimensions.rows(), 42); assert_eq!(dimensions.columns(), 71); let two_panels = terminal_dimensions_for_window(ultrawide, 2); assert_eq!(two_panels.rows(), 42); assert_eq!(two_panels.columns(), 185); } #[test] 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] fn shift_navigation_controls_scrollback_without_reaching_the_pty() { assert_eq!( terminal_scroll_from_parts("pageup", KeyModifiers::SHIFT), Some(TerminalScroll::PageUp) ); assert_eq!( terminal_scroll_from_parts("end", KeyModifiers::SHIFT), Some(TerminalScroll::Bottom) ); assert_eq!( terminal_scroll_from_parts("pageup", KeyModifiers::default()), None ); } #[test] fn terminal_paint_rows_merge_runs_and_preserve_cursor_boundary() { let mut terminal = TerminalEngine::new(TerminalEngineOptions::default()); terminal.process(b"\x1b[31mAB\x1b[0m"); let rows = terminal_paint_rows( &terminal.snapshot(), crate::theme::ActiveTheme::new( lumbridge_theme::theme_or_default(lumbridge_theme::DEFAULT_THEME), lumbridge_theme::DEFAULT_ACCENT, ) .colors, ); assert!( rows[0] .iter() .any(|run| run.text == "AB" && run.columns == 2) ); assert!(rows[0].iter().any(|run| run.cursor.is_some())); } #[test] #[allow( clippy::unreadable_literal, reason = "six-digit colour hex reads whole" )] fn xterm_color_cube_and_grayscale_are_deterministic() { assert_eq!(indexed_terminal_color(16), 0x000000); assert_eq!(indexed_terminal_color(231), 0xffffff); assert_eq!(indexed_terminal_color(232), 0x080808); assert_eq!(indexed_terminal_color(255), 0xeeeeee); } }