use std::collections::VecDeque; use std::time::{Duration, Instant}; use gpui::{ App, Application, Bounds, Context, FocusHandle, FontWeight, KeyBinding, KeyDownEvent, Pixels, Size, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, relative, rgb, size, }; use lumbridge_runtime::{ CommandConfig, PtyOptions, RuntimeActor, RuntimeActorError, RuntimeActorOptions, RuntimeCommand, RuntimeEvent, TerminalSize, }; use lumbridge_spike_model::{ ActionOutcome, FOOTER_RIGHT, FocusDirection, OutputSource, PaneId, PaneState, ShellAction, ShellModel, SurfaceKind, WORKSPACES, }; use lumbridge_terminal::{ KeyModifiers, TerminalCellStyle, TerminalColor, TerminalCursorShape, TerminalDimensions, TerminalEngine, TerminalEngineOptions, TerminalKey, TerminalKeyEvent, TerminalNamedColor, TerminalSnapshot, }; const BG: u32 = 0x090c12; const PANEL: u32 = 0x101620; const PANEL_ALT: u32 = 0x151d29; const PANEL_ACTIVE: u32 = 0x182334; const BORDER: u32 = 0x263246; const BORDER_QUIET: u32 = 0x1c2636; const TEXT: u32 = 0xdbe5f4; const MUTED: u32 = 0x8290a8; const ACCENT: u32 = 0x68b5f8; const ATTENTION: u32 = 0xf1b96a; const SUCCESS: u32 = 0x70d6a8; const TIMING_SAMPLE_LIMIT: usize = 256; const RUNTIME_POLL_INTERVAL: Duration = Duration::from_millis(16); const RUNTIME_DRAIN_LIMIT: usize = 64; const LIVE_PANE: PaneId = PaneId::CodexRuntime; const LIVE_PTY_SCRIPT: &str = "printf 'Lumbridge interactive PTY · type here\\n'; exec /bin/sh -i"; const SIDEBAR_WIDTH: f32 = 248.0; const APP_HEADER_HEIGHT: f32 = 48.0; const TAB_BAR_HEIGHT: f32 = 38.0; const APP_FOOTER_HEIGHT: f32 = 32.0; const TERMINAL_CHROME_HEIGHT: f32 = 72.0; const TERMINAL_HORIZONTAL_INSET: f32 = 32.0; const TERMINAL_CELL_WIDTH: f32 = 8.4; const TERMINAL_CELL_HEIGHT: f32 = 18.0; const TERMINAL_ROW_STEP: u16 = 2; const TERMINAL_COLUMN_STEP: u16 = 10; actions!( lumbridge, [ FocusLeft, FocusRight, FocusUp, FocusDown, OpenPalette, ClosePalette, SelectPane1, SelectPane2, SelectPane3, SelectPane4, SelectPane5, SelectPane6, TerminalTaller, TerminalShorter, TerminalWider, TerminalNarrower, ] ); struct LumbridgeShell { model: ShellModel, timing: RenderTiming, runtime: Option, runtime_status: LiveRuntimeStatus, terminal: TerminalEngine, terminal_snapshot: TerminalSnapshot, last_runtime_sequence: u64, root_focus: FocusHandle, pane_focus: [FocusHandle; 6], } #[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_fault(&self) -> bool { matches!(self, Self::Fault(_)) } 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, Eq, PartialEq)] struct TerminalPaintRun { text: String, columns: u16, foreground: u32, background: u32, 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) -> 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); let mut background = terminal_color(cell.background); 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() } fn terminal_color(color: TerminalColor) -> u32 { match color { TerminalColor::Rgb { red, green, blue } => { (u32::from(red) << 16) | (u32::from(green) << 8) | u32::from(blue) } TerminalColor::Indexed(index) => indexed_terminal_color(index), TerminalColor::Named(named) => match named { TerminalNamedColor::Black => 0x1d2430, TerminalNamedColor::Red => 0xff6b6b, TerminalNamedColor::Green => 0x70d6a8, TerminalNamedColor::Yellow => 0xf1c76a, TerminalNamedColor::Blue => 0x68b5f8, TerminalNamedColor::Magenta => 0xc79bf2, TerminalNamedColor::Cyan => 0x63d5da, TerminalNamedColor::White => 0xdbe5f4, TerminalNamedColor::BrightBlack => 0x6d7a91, TerminalNamedColor::BrightRed => 0xff8b8b, TerminalNamedColor::BrightGreen => 0x93e6be, TerminalNamedColor::BrightYellow => 0xf8d98c, TerminalNamedColor::BrightBlue => 0x8bc8ff, TerminalNamedColor::BrightMagenta => 0xd9b4fb, TerminalNamedColor::BrightCyan => 0x86e7eb, TerminalNamedColor::BrightWhite | TerminalNamedColor::BrightForeground => 0xf4f8ff, TerminalNamedColor::Foreground => TEXT, TerminalNamedColor::Background => BG, TerminalNamedColor::Cursor => 0xf4f8ff, TerminalNamedColor::DimBlack => 0x121820, TerminalNamedColor::DimRed => 0x9f4a4a, TerminalNamedColor::DimGreen => 0x4e9878, TerminalNamedColor::DimYellow => 0xa88a49, TerminalNamedColor::DimBlue => 0x477fac, TerminalNamedColor::DimMagenta => 0x896ca4, TerminalNamedColor::DimCyan => 0x459397, TerminalNamedColor::DimWhite | TerminalNamedColor::DimForeground => MUTED, }, } } 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 } } } fn dim_color(color: u32) -> u32 { let dim = |channel: u32| channel * 13 / 20; (dim((color >> 16) & 0xff) << 16) | (dim((color >> 8) & 0xff) << 8) | dim(color & 0xff) } fn terminal_dimensions_for_window(window_size: Size) -> TerminalDimensions { let width = f32::from(window_size.width); let height = f32::from(window_size.height); let workspace_height = (height - APP_HEADER_HEIGHT - TAB_BAR_HEIGHT - APP_FOOTER_HEIGHT).max(0.0); let terminal_height = (workspace_height * 0.60 - TERMINAL_CHROME_HEIGHT).max(0.0); let terminal_width = (width - SIDEBAR_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( rows as u16, columns as u16, TERMINAL_CELL_WIDTH.round() as u16, TERMINAL_CELL_HEIGHT.round() as u16, ) .expect("geometry clamps terminal dimensions above zero") } 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() ) } } impl LumbridgeShell { fn new(window: &mut Window, cx: &mut Context) -> Self { let pane_focus = std::array::from_fn(|index| { cx.focus_handle() .tab_index(index as isize + 1) .tab_stop(true) }); let root_focus = cx.focus_handle(); window.focus(&pane_focus[0]); 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); 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 terminal_dimensions = terminal_dimensions_for_window(window.bounds().size); let terminal = TerminalEngine::new(TerminalEngineOptions { dimensions: terminal_dimensions, ..TerminalEngineOptions::default() }); let terminal_snapshot = terminal.snapshot(); let (runtime, runtime_status) = match start_live_runtime(terminal.dimensions()) { Ok(runtime) => (Some(runtime), LiveRuntimeStatus::Starting), Err(error) => (None, LiveRuntimeStatus::Fault(error.to_string())), }; cx.observe_window_bounds(window, |shell, window, cx| { let dimensions = terminal_dimensions_for_window(window.bounds().size); shell.resize_terminal(dimensions.rows(), dimensions.columns(), cx); }) .detach(); Self { model: ShellModel::with_external_output(LIVE_PANE) .expect("the live comparison pane is a terminal"), timing: RenderTiming::default(), runtime, runtime_status, terminal, terminal_snapshot, last_runtime_sequence: 0, root_focus, pane_focus, } } fn dispatch(&mut self, action: ShellAction) -> ActionOutcome { self.timing.mark_dispatch(); self.model.dispatch(action) } fn drain_runtime_events(&mut self) -> bool { if self.runtime_status.is_terminal() { return false; } let mut changed = false; for _ in 0..RUNTIME_DRAIN_LIMIT { let event = match self.runtime.as_ref().map(RuntimeActor::try_recv) { Some(Ok(Some(event))) => event, Some(Ok(None)) | None => break, Some(Err(RuntimeActorError::Disconnected)) => { self.runtime_status = LiveRuntimeStatus::Fault("runtime actor disconnected".to_owned()); changed = true; break; } Some(Err(error)) => { self.runtime_status = LiveRuntimeStatus::Fault(error.to_string()); changed = true; break; } }; match event { RuntimeEvent::Started { session_id, process_id, } => { self.runtime_status = LiveRuntimeStatus::Running { session_id: session_id.get(), process_id, }; changed = true; } RuntimeEvent::Output { sequence, bytes, .. } => { if sequence <= self.last_runtime_sequence { self.runtime_status = LiveRuntimeStatus::Fault(format!( "non-monotonic PTY output sequence {sequence}" )); changed = true; break; } self.last_runtime_sequence = sequence; let update = self.terminal.process(&bytes); for response in update.outbound { if let Err(error) = self.send_runtime_command(RuntimeCommand::Input(response)) { self.runtime_status = LiveRuntimeStatus::Fault(error.to_string()); return true; } } self.publish_terminal_snapshot(); changed = true; } RuntimeEvent::InputClosed { .. } => {} RuntimeEvent::Exited { status, .. } => { self.runtime_status = LiveRuntimeStatus::Exited(format!("PTY exited with code {}", status.code)); changed = true; break; } RuntimeEvent::Fault { operation, message, .. } => { self.runtime_status = LiveRuntimeStatus::Fault(format!("{operation:?}: {message}")); changed = true; break; } } } changed } fn send_runtime_command(&self, command: RuntimeCommand) -> Result<(), RuntimeActorError> { self.runtime .as_ref() .ok_or(RuntimeActorError::Disconnected)? .try_send(command) } fn publish_terminal_snapshot(&mut self) { let snapshot = self.terminal.snapshot(); let lines = snapshot.plain_rows(); self.terminal_snapshot = snapshot; self.dispatch(ShellAction::ReplaceExternalOutput { pane: LIVE_PANE, lines, }); } fn resize_terminal(&mut self, rows: u16, columns: u16, cx: &mut Context) { let dimensions = TerminalDimensions::new(rows, columns) .expect("resize actions always retain non-zero dimensions"); if !self.terminal.resize(dimensions) { return; } let pty_size = TerminalSize::new(rows, columns) .expect("terminal engine dimensions are valid PTY dimensions"); if let Err(error) = self.send_runtime_command(RuntimeCommand::Resize(pty_size)) { self.runtime_status = LiveRuntimeStatus::Fault(error.to_string()); } self.publish_terminal_snapshot(); cx.notify(); } fn select_pane(&mut self, pane: PaneId, window: &mut Window, cx: &mut Context) { self.dispatch(ShellAction::SelectPane(pane)); window.focus(&self.pane_focus[pane.index()]); cx.notify(); } fn move_focus( &mut self, direction: FocusDirection, window: &mut Window, cx: &mut Context, ) { let outcome = self.dispatch(ShellAction::MoveFocus(direction)); window.focus(&self.pane_focus[outcome.selected_pane.index()]); cx.notify(); } fn focus_left(&mut self, _: &FocusLeft, window: &mut Window, cx: &mut Context) { self.move_focus(FocusDirection::Left, window, cx); } fn focus_right(&mut self, _: &FocusRight, window: &mut Window, cx: &mut Context) { self.move_focus(FocusDirection::Right, window, cx); } fn focus_up(&mut self, _: &FocusUp, window: &mut Window, cx: &mut Context) { self.move_focus(FocusDirection::Up, window, cx); } fn focus_down(&mut self, _: &FocusDown, window: &mut Window, cx: &mut Context) { self.move_focus(FocusDirection::Down, window, cx); } fn open_palette(&mut self, _: &OpenPalette, _: &mut Window, cx: &mut Context) { self.dispatch(ShellAction::OpenCommandPalette); cx.notify(); } fn close_palette(&mut self, _: &ClosePalette, _: &mut Window, cx: &mut Context) { self.dispatch(ShellAction::CloseCommandPalette); cx.notify(); } fn terminal_taller(&mut self, _: &TerminalTaller, _: &mut Window, cx: &mut Context) { let dimensions = self.terminal.dimensions(); self.resize_terminal( dimensions.rows().saturating_add(TERMINAL_ROW_STEP), dimensions.columns(), cx, ); } fn terminal_shorter(&mut self, _: &TerminalShorter, _: &mut Window, cx: &mut Context) { let dimensions = self.terminal.dimensions(); self.resize_terminal( dimensions.rows().saturating_sub(TERMINAL_ROW_STEP).max(2), dimensions.columns(), cx, ); } fn terminal_wider(&mut self, _: &TerminalWider, _: &mut Window, cx: &mut Context) { let dimensions = self.terminal.dimensions(); self.resize_terminal( dimensions.rows(), dimensions.columns().saturating_add(TERMINAL_COLUMN_STEP), cx, ); } fn terminal_narrower(&mut self, _: &TerminalNarrower, _: &mut Window, cx: &mut Context) { let dimensions = self.terminal.dimensions(); self.resize_terminal( dimensions.rows(), dimensions .columns() .saturating_sub(TERMINAL_COLUMN_STEP) .max(20), cx, ); } fn on_key_down(&mut self, event: &KeyDownEvent, _window: &mut Window, cx: &mut Context) { if !self.model.command_palette().is_open() { if self.model.selected_pane() != LIVE_PANE || self.runtime_status.is_terminal() { return; } let modifiers = key_modifiers(event); let Some(event) = terminal_key_from_parts( event.keystroke.key.as_str(), event.keystroke.key_char.as_deref(), modifiers, ) else { return; }; let bytes = self.terminal.encode_key(&event); if bytes.is_empty() { return; } if let Err(error) = self.send_runtime_command(RuntimeCommand::Input(bytes)) { self.runtime_status = LiveRuntimeStatus::Fault(error.to_string()); } cx.notify(); return; } let mut query = self.model.command_palette().query().to_owned(); match event.keystroke.key.as_str() { "backspace" => { query.pop(); } "enter" => { self.dispatch(ShellAction::CloseCommandPalette); cx.notify(); return; } "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 pane_selector( pane: &PaneState, selected: bool, runtime_status: LiveRuntimeStatus, focus: FocusHandle, cx: &mut Context, ) -> gpui::AnyElement { let id = pane.id(); let needs_input = pane.needs_input(); let external = pane.output_source() == OutputSource::External; let label = if external { runtime_status.badge() } else { match pane.kind() { SurfaceKind::Terminal => pane.status().label(), _ => pane.fixture().badge, } }; let state_color = if needs_input || (external && runtime_status.is_fault()) { ATTENTION } else if matches!(pane.kind(), SurfaceKind::Terminal) { SUCCESS } else { ACCENT }; div() .id(("pane", id.index())) .track_focus(&focus) .tab_index(id.index() as isize + 1) .flex() .items_center() .justify_between() .gap_3() .h_full() .min_w(px(172.0)) .px_3() .bg(rgb(if selected { PANEL_ACTIVE } else { PANEL_ALT })) .border_1() .border_color(rgb(if selected || needs_input { state_color } else { BORDER_QUIET })) .rounded(px(5.0)) .on_click(cx.listener(move |shell, _, window, cx| { shell.select_pane(id, window, cx); })) .child( div() .flex() .flex_col() .min_w_0() .child( div() .truncate() .text_sm() .text_color(rgb(TEXT)) .child(if external { "Terminal · local shell" } else { pane.fixture().title }), ) .child( div() .mt_1() .truncate() .text_xs() .text_color(rgb(MUTED)) .child(pane.fixture().target.to_owned()), ), ) .child(div().text_xs().text_color(rgb(state_color)).child(label)) .into_any_element() } fn terminal_run(run: TerminalPaintRun) -> gpui::AnyElement { let cursor = run.cursor; div() .flex_none() .h(px(TERMINAL_CELL_HEIGHT)) .w(px(f32::from(run.columns) * TERMINAL_CELL_WIDTH)) .overflow_hidden() .whitespace_nowrap() .text_color(rgb(run.foreground)) .bg(rgb(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, |view| view.underline(), ) .when(run.style.contains(TerminalCellStyle::STRIKEOUT), |view| { view.line_through() }) .when(cursor == Some(TerminalCursorShape::Block), |view| { view.bg(rgb(TEXT)).text_color(rgb(BG)) }) .when(cursor == Some(TerminalCursorShape::HollowBlock), |view| { view.border_1().border_color(rgb(TEXT)) }) .when(cursor == Some(TerminalCursorShape::Underline), |view| { view.border_b_2().border_color(rgb(TEXT)) }) .when(cursor == Some(TerminalCursorShape::Beam), |view| { view.border_l_2().border_color(rgb(TEXT)) }) .child(run.text) .into_any_element() } fn terminal_view(&self) -> gpui::AnyElement { let rows = terminal_paint_rows(&self.terminal_snapshot) .into_iter() .map(|runs| { div() .flex() .h(px(TERMINAL_CELL_HEIGHT)) .flex_none() .children(runs.into_iter().map(Self::terminal_run)) }) .collect::>(); div() .flex() .flex_col() .size_full() .overflow_hidden() .bg(rgb(BG)) .font_family("monospace") .text_size(px(13.0)) .children(rows) .into_any_element() } fn work_surface(&self) -> gpui::AnyElement { let pane = self.model.pane(self.model.selected_pane()); let external = pane.output_source() == OutputSource::External; let status = if external { self.runtime_status.badge() } else { pane.fixture().badge }; let detail = if external { self.runtime_status.detail() } else { pane.fixture().target.to_owned() }; let surface_status = if external { format!( "{} · {}×{} · auto-fit", status, self.terminal.dimensions().columns(), self.terminal.dimensions().rows() ) } else { status.to_owned() }; let surface = match pane.kind() { SurfaceKind::Terminal => "TERMINAL", SurfaceKind::Markdown => "CONTEXT", SurfaceKind::Browser => "BROWSER", SurfaceKind::Review => "REVIEW", }; let tabs = ["TERMINAL", "BROWSER", "TOOLS", "CONTEXT", "GOAL", "REVIEW"] .into_iter() .map(|label| { div() .h_full() .flex() .items_center() .px_3() .text_xs() .text_color(rgb(if label == surface { ACCENT } else { MUTED })) .when(label == surface, |view| { view.border_b_2().border_color(rgb(ACCENT)) }) .child(label) }) .collect::>(); let content = if external { self.terminal_view() } 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(rgb(TEXT)) .children(pane.lines()[start..].iter().cloned()) .into_any_element() }; div() .flex() .flex_col() .size_full() .min_h_0() .overflow_hidden() .bg(rgb(PANEL)) .border_y_1() .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_3() .text_sm() .child(pane.fixture().title) .child(div().text_xs().text_color(rgb(MUTED)).child(detail)), ) .child( div() .text_xs() .text_color(rgb(SUCCESS)) .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( div() .flex_1() .min_h_0() .overflow_hidden() .p_3() .child(content), ) .into_any_element() } fn decision_region(&self) -> gpui::AnyElement { let choice = |label: &'static str, detail: &'static str| { div() .flex() .flex_col() .min_w(px(200.0)) .px_3() .py_2() .rounded(px(5.0)) .border_1() .border_color(rgb(BORDER)) .bg(rgb(PANEL_ALT)) .text_sm() .text_color(rgb(TEXT)) .child(label) .child(div().mt_1().text_xs().text_color(rgb(MUTED)).child(detail)) }; div() .flex() .flex_col() .size_full() .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("DECISION SHELF · answer, choices, approvals"), ) .child( div() .text_color(rgb(ATTENTION)) .child("LOCAL ANALYST OFF · suggestions inert"), ), ) .child( div() .flex() .items_center() .gap_2() .mt_2() .child(choice("Continue", "Keep the selected agent moving")) .child(choice("Review plan", "Inspect capability-scoped commands")) .child(choice("Ask…", "Refine before any action")), ) .into_any_element() } fn command_palette(&self) -> impl IntoElement { 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(rgb(PANEL_ALT)) .border_1() .border_color(rgb(ACCENT)) .rounded(px(8.0)) .child( div() .px_4() .py_3() .border_b_1() .border_color(rgb(BORDER)) .text_color(rgb(if query.is_empty() { MUTED } else { TEXT })) .child(prompt), ) .child( div() .flex() .flex_col() .p_2() .text_sm() .child( div() .px_3() .py_2() .rounded(px(5.0)) .bg(rgb(PANEL_ACTIVE)) .child("Focus next pane") .child( div() .mt_1() .text_xs() .text_color(rgb(MUTED)) .child("Workspace · navigation"), ), ) .child( div() .px_3() .py_2() .text_color(rgb(MUTED)) .child("Open attention request"), ) .child( div() .px_3() .py_2() .text_color(rgb(MUTED)) .child("Share selected pane to Buzz…"), ), ) .child( div() .flex() .justify_between() .px_4() .py_2() .border_t_1() .border_color(rgb(BORDER)) .text_xs() .text_color(rgb(MUTED)) .child("Enter to run") .child("Esc to close"), ), ) } } impl Render for LumbridgeShell { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let attention = self.model.pane(PaneId::ClaudeUi); let sidebar = div() .flex() .flex_col() .w(px(248.0)) .flex_none() .bg(rgb(PANEL)) .border_r_1() .border_color(rgb(BORDER_QUIET)) .child( div() .px_4() .pt_4() .pb_2() .text_xs() .text_color(rgb(ATTENTION)) .child("ATTENTION · 1"), ) .child( div() .mx_2() .mb_3() .px_3() .py_2() .rounded(px(5.0)) .bg(rgb(PANEL_ACTIVE)) .border_1() .border_color(rgb(ATTENTION)) .child( div() .text_sm() .text_color(rgb(TEXT)) .child(attention.fixture().title), ) .child( div() .mt_1() .text_xs() .text_color(rgb(MUTED)) .child("Waiting for a split decision"), ), ) .child( div() .px_4() .py_2() .text_xs() .text_color(rgb(MUTED)) .child("WORKTREES"), ) .children(WORKSPACES.into_iter().enumerate().map(|(index, name)| { div() .mx_2() .mb_1() .px_3() .py_2() .rounded(px(5.0)) .when(index == 0, |view| { view.bg(rgb(PANEL_ALT)).text_color(rgb(TEXT)) }) .when(index != 0, |view| view.text_color(rgb(MUTED))) .child(name) .when(index == 0, |view| { view.child( div() .mt_1() .text_xs() .text_color(rgb(MUTED)) .child("main · metal"), ) }) })) .child( div() .mt_3() .px_4() .py_2() .text_xs() .text_color(rgb(MUTED)) .child("RUNTIMES"), ) .child( div() .px_4() .py_1() .text_sm() .text_color(rgb(SUCCESS)) .child(format!("metal · {}", self.runtime_status.badge())), ) .child( div() .px_4() .py_1() .text_sm() .text_color(rgb(MUTED)) .child("amd-server · connected"), ) .child( div() .px_4() .py_1() .text_sm() .text_color(rgb(MUTED)) .child("spark-1 · sleeping"), ) .child(div().flex_1()) .child( div() .m_3() .p_3() .rounded(px(5.0)) .bg(rgb(PANEL_ALT)) .text_xs() .text_color(rgb(MUTED)) .child("Buzz · lumbridgecode") .child( div() .mt_1() .text_color(rgb(SUCCESS)) .child("connected · signed identity"), ), ); let tabs = div() .flex() .items_center() .h(px(38.0)) .flex_none() .px_2() .gap_1() .bg(rgb(PANEL)) .border_b_1() .border_color(rgb(BORDER_QUIET)) .child( div() .h_full() .flex() .items_center() .px_3() .border_b_2() .border_color(rgb(ACCENT)) .text_sm() .child("Agent workspace"), ) .child( div() .px_3() .text_sm() .text_color(rgb(MUTED)) .child("Architecture.md"), ) .child( div() .px_3() .text_sm() .text_color(rgb(MUTED)) .child("Review"), ) .child(div().flex_1()) .child( div() .px_3() .text_xs() .text_color(rgb(MUTED)) .child("1 interactive VT · 5 deterministic · 1 waiting"), ); let pane_selectors = self .model .panes() .iter() .map(|pane| { Self::pane_selector( pane, self.model.selected_pane() == pane.id(), self.runtime_status.clone(), self.pane_focus[pane.id().index()].clone(), cx, ) }) .collect::>(); let selected = self.model.pane(self.model.selected_pane()); let workspace_stack = div() .flex() .flex_col() .flex_1() .min_w_0() .min_h_0() .overflow_hidden() .child( div() .h(relative(0.20)) .flex_none() .flex() .flex_col() .min_h_0() .overflow_hidden() .px_3() .py_2() .bg(rgb(PANEL)) .child( div() .flex() .items_center() .justify_between() .text_xs() .child( div() .text_color(rgb(MUTED)) .child("CONTEXT · panes, agents, tools, goal"), ) .child( div() .text_color(rgb(if selected.needs_input() { ATTENTION } else { SUCCESS })) .child(if selected.needs_input() { "NEEDS INPUT · decision below" } else { "WORKSPACE HEALTHY" }), ), ) .child( div() .flex() .items_center() .gap_2() .mt_2() .flex_1() .min_h_0() .overflow_hidden() .children(pane_selectors), ), ) .child( div() .h(relative(0.60)) .flex_none() .min_h_0() .overflow_hidden() .child(self.work_surface()), ) .child( div() .h(relative(0.20)) .flex_none() .min_h_0() .overflow_hidden() .child(self.decision_region()), ); let counters = self.model.counters(); let footer_left = format!( "rev {} · VT rev {} · {} focus · {} VT snapshots · {} synthetic lines", self.model.revision(), self.terminal.revision(), counters.focus_moves, counters.external_snapshot_updates, counters.terminal_lines_appended ); let timing = self.timing.summary(); let root = div() .id("lumbridge-shell") .relative() .track_focus(&self.root_focus) .key_context("LumbridgeShell") .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::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(PaneId::CodexRuntime, window, cx); })) .on_action(cx.listener(|shell, _: &SelectPane2, window, cx| { shell.select_pane(PaneId::ClaudeUi, window, cx); })) .on_action(cx.listener(|shell, _: &SelectPane3, window, cx| { shell.select_pane(PaneId::PiDocs, window, cx); })) .on_action(cx.listener(|shell, _: &SelectPane4, window, cx| { shell.select_pane(PaneId::Architecture, window, cx); })) .on_action(cx.listener(|shell, _: &SelectPane5, window, cx| { shell.select_pane(PaneId::AcpPreview, window, cx); })) .on_action(cx.listener(|shell, _: &SelectPane6, window, cx| { shell.select_pane(PaneId::RuntimeReview, window, cx); })) .on_key_down(cx.listener(Self::on_key_down)) .flex() .flex_col() .size_full() .bg(rgb(BG)) .text_color(rgb(TEXT)) .child( div() .flex() .items_center() .h(px(48.0)) .flex_none() .px_4() .bg(rgb(PANEL_ALT)) .border_b_1() .border_color(rgb(BORDER_QUIET)) .child(div().text_lg().child("Lumbridge")) .child( div() .ml_3() .text_sm() .text_color(rgb(MUTED)) .child("Lumbridge Code / main"), ) .child(div().flex_1()) .child( div() .mr_4() .text_xs() .text_color(rgb(SUCCESS)) .child(format!("metal · {}", self.runtime_status.badge())), ) .child( div() .text_sm() .text_color(rgb(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_stack), ), ) .child( div() .flex() .items_center() .justify_between() .h(px(32.0)) .flex_none() .px_3() .bg(rgb(PANEL_ALT)) .border_t_1() .border_color(rgb(BORDER_QUIET)) .text_xs() .text_color(rgb(MUTED)) .child(footer_left) .child(timing) .child(FOOTER_RIGHT), ) .when(self.model.command_palette().is_open(), |view| { view.child(self.command_palette()) }); 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.starts_with('f') => { let number = function.strip_prefix('f')?.parse::().ok()?; TerminalKey::Function(number) } _ if !modifiers.contains(KeyModifiers::PLATFORM) => TerminalKey::Text(key_char?.to_owned()), _ => return None, }; Some(TerminalKeyEvent { key, modifiers }) } fn start_live_runtime(dimensions: TerminalDimensions) -> Result { let command = CommandConfig::new("/bin/sh") .map_err(RuntimeActorError::Start)? .args(["-c", LIVE_PTY_SCRIPT]); let pty_size = TerminalSize::new(dimensions.rows(), dimensions.columns()) .map_err(RuntimeActorError::Start)?; RuntimeActor::spawn( command, PtyOptions::new(pty_size), RuntimeActorOptions::default(), ) } fn main() { Application::new().run(|cx: &mut App| { cx.bind_keys([ KeyBinding::new("alt-left", FocusLeft, Some("LumbridgeShell")), KeyBinding::new("alt-right", FocusRight, Some("LumbridgeShell")), KeyBinding::new("alt-up", FocusUp, Some("LumbridgeShell")), KeyBinding::new("alt-down", FocusDown, Some("LumbridgeShell")), KeyBinding::new("secondary-k", OpenPalette, Some("LumbridgeShell")), KeyBinding::new("alt-1", SelectPane1, Some("LumbridgeShell")), KeyBinding::new("alt-2", SelectPane2, Some("LumbridgeShell")), KeyBinding::new("alt-3", SelectPane3, Some("LumbridgeShell")), KeyBinding::new("alt-4", SelectPane4, Some("LumbridgeShell")), KeyBinding::new("alt-5", SelectPane5, Some("LumbridgeShell")), KeyBinding::new("alt-6", SelectPane6, Some("LumbridgeShell")), KeyBinding::new("alt-shift-up", TerminalTaller, Some("LumbridgeShell")), KeyBinding::new("alt-shift-down", TerminalShorter, Some("LumbridgeShell")), KeyBinding::new("alt-shift-right", TerminalWider, Some("LumbridgeShell")), KeyBinding::new("alt-shift-left", TerminalNarrower, Some("LumbridgeShell")), ]); 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, indexed_terminal_color, terminal_dimensions_for_window, terminal_key_from_parts, terminal_paint_rows, }; 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 composed = terminal_key_from_parts("é", Some("é"), KeyModifiers::default()).unwrap(); assert_eq!(composed.key, TerminalKey::Text("é".into())); } #[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_the_middle_sixty_percent() { let dimensions = terminal_dimensions_for_window(size(px(1500.0), px(960.0))); assert_eq!(dimensions.rows(), 24); assert_eq!(dimensions.columns(), 145); } #[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()); assert!( rows[0] .iter() .any(|run| run.text == "AB" && run.columns == 2) ); assert!(rows[0].iter().any(|run| run.cursor.is_some())); } #[test] 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); } }