Files
lumbridge-code/apps/lumbridge/src/main.rs
T
Metal AgentandClaude Opus 5 3a8a100ea5 Make the attention count real, and make it carry a source
The sidebar had said ATTENTION · 1 since the first commit. It was reading a
fixture's needs_input flag that no live pane ever set, so the number was frozen
at whatever the demo data said.

Attention is now built from observed signals, and every signal carries an
AttentionSource. The rule, enforced by is_countable on the source rather than by
a filter at the call site: a guess may draw a row and sort it, but may not
increment the count. That is decision 0012's provenance rule applied to a
different claim, for the same reason — a wrong count teaches people to ignore
the number, and the number is the whole point of the section.

RuntimeObserved is the only source that produces signals today, from process
exits and runtime faults. The two ACP kinds are declared and never constructed,
so there is a shape for the ACP client to fill and nobody is tempted to
approximate "asked you a question" by watching output for a question mark.

RuntimeEvent::Exited carries a u32 code that was being formatted into a sentence
and discarded; AttentionKind::Finished keeps it, which is why a row can say
"Exited with code 42" instead of "needs attention".

Verified live: exiting a pane with code 42 produces ATTENTION · 1, a row reading
"Exited with code 42 · observed", a dimmed pane banner offering restart, and a
live-PTY count that drops from 3/3 to 2/3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 23:38:55 -07:00

3417 lines
125 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<PanelId>,
live_terminals: BTreeMap<PanelId, LiveTerminalState>,
store: Option<Store>,
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<PanelId, SurfaceTab>,
/// Which decision-shelf choice each panel has selected. Selecting one is
/// inert by design: it prepares nothing and runs nothing.
shelf_choice: BTreeMap<PanelId, usize>,
add_panel_chooser_open: bool,
/// The pane a terminate has been asked for but not yet confirmed.
pending_terminate: Option<PanelId>,
/// Why the last keystroke went nowhere, if it did.
input_gap: Option<String>,
/// What is waiting on a human, and how we know.
attention: Attention<PanelId>,
/// 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<String>,
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<u32>,
},
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<Instant>,
dispatch_to_element_micros: VecDeque<u128>,
}
#[derive(Clone, PartialEq)]
struct TerminalPaintRun {
text: String,
columns: u16,
foreground: Rgba,
background: Rgba,
style: TerminalCellStyle,
cursor: Option<TerminalCursorShape>,
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<Vec<TerminalPaintRun>> {
(0..snapshot.dimensions.rows())
.map(|row| {
let mut runs: Vec<TerminalPaintRun> = 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<Pixels>,
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<Pixels>) -> 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<usize> {
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::<Vec<_>>();
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<PathBuf> {
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<Store>, 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>) -> 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::<Vec<_>>();
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<Self>) {
let mut changed = false;
let panes = self.live_terminals.keys().copied().collect::<Vec<_>>();
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<Self>) {
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>) {
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>,
) {
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>,
) {
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>) {
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<Self>) {
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>) {
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>) {
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>) {
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<Self>) {
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<Self>) {
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<Self>) {
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>) {
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>,
) {
self.detach_panel(self.panels.selected(), window, cx);
}
fn move_focus(&mut self, delta: isize, window: &mut Window, cx: &mut Context<Self>) {
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>) {
self.move_focus(-1, window, cx);
}
fn focus_right(&mut self, _: &FocusRight, window: &mut Window, cx: &mut Context<Self>) {
self.move_focus(1, window, cx);
}
fn focus_up(&mut self, _: &FocusUp, window: &mut Window, cx: &mut Context<Self>) {
self.move_focus(-1, window, cx);
}
fn focus_down(&mut self, _: &FocusDown, window: &mut Window, cx: &mut Context<Self>) {
self.move_focus(1, window, cx);
}
fn open_palette(&mut self, _: &OpenPalette, _: &mut Window, cx: &mut Context<Self>) {
self.add_panel_chooser_open = false;
self.dispatch(ShellAction::OpenCommandPalette);
cx.notify();
}
fn close_palette(&mut self, _: &ClosePalette, _: &mut Window, cx: &mut Context<Self>) {
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<Self>) {
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<Self>) {
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<Self>) {
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<Self>) {
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<Self>) {
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<Self>) -> 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::<Vec<_>>();
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<Self>,
) -> 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::<Vec<_>>();
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<gpui::AnyElement> {
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<gpui::AnyElement> {
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<Self>) -> 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<Self>) -> 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<Self>,
) -> 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<Self>) -> 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::<Vec<_>>();
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::<Vec<_>>();
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("14 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<AttentionRow> {
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<RuntimeRow> {
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<Self>,
) -> 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<Self>) -> 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<Self>,
) -> 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<String> = 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::<Vec<_>>()
})
}
}
/// 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<u64>,
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<Self>) -> 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::<Vec<_>>();
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::<Vec<_>>();
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::<u64>();
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<TerminalKeyEvent> {
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::<u8>().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<TerminalScroll> {
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<SeedPane>) -> &'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<PanelId>,
pane: PanelId,
seed: Option<SeedPane>,
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);
}
}