This commit is contained in:
+552
-121
@@ -2,8 +2,8 @@ use std::collections::VecDeque;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, FocusHandle, KeyBinding, KeyDownEvent, Window, WindowBounds,
|
||||
WindowOptions, actions, div, prelude::*, px, rgb, size,
|
||||
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,
|
||||
@@ -14,8 +14,9 @@ use lumbridge_spike_model::{
|
||||
ShellModel, SurfaceKind, WORKSPACES,
|
||||
};
|
||||
use lumbridge_terminal::{
|
||||
KeyModifiers, TerminalDimensions, TerminalEngine, TerminalEngineOptions, TerminalKey,
|
||||
TerminalKeyEvent,
|
||||
KeyModifiers, TerminalCellStyle, TerminalColor, TerminalCursorShape, TerminalDimensions,
|
||||
TerminalEngine, TerminalEngineOptions, TerminalKey, TerminalKeyEvent, TerminalNamedColor,
|
||||
TerminalSnapshot,
|
||||
};
|
||||
|
||||
const BG: u32 = 0x090c12;
|
||||
@@ -34,6 +35,14 @@ 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;
|
||||
|
||||
@@ -65,6 +74,7 @@ struct LumbridgeShell {
|
||||
runtime: Option<RuntimeActor>,
|
||||
runtime_status: LiveRuntimeStatus,
|
||||
terminal: TerminalEngine,
|
||||
terminal_snapshot: TerminalSnapshot,
|
||||
last_runtime_sequence: u64,
|
||||
root_focus: FocusHandle,
|
||||
pane_focus: [FocusHandle; 6],
|
||||
@@ -123,6 +133,171 @@ struct RenderTiming {
|
||||
dispatch_to_element_micros: VecDeque<u128>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
struct TerminalPaintRun {
|
||||
text: String,
|
||||
columns: u16,
|
||||
foreground: u32,
|
||||
background: u32,
|
||||
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) -> 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);
|
||||
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<Pixels>) -> 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());
|
||||
@@ -207,12 +382,23 @@ impl LumbridgeShell {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let terminal = TerminalEngine::new(TerminalEngineOptions::default());
|
||||
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"),
|
||||
@@ -220,6 +406,7 @@ impl LumbridgeShell {
|
||||
runtime,
|
||||
runtime_status,
|
||||
terminal,
|
||||
terminal_snapshot,
|
||||
last_runtime_sequence: 0,
|
||||
root_focus,
|
||||
pane_focus,
|
||||
@@ -316,7 +503,9 @@ impl LumbridgeShell {
|
||||
}
|
||||
|
||||
fn publish_terminal_snapshot(&mut self) {
|
||||
let lines = self.terminal.snapshot().plain_rows();
|
||||
let snapshot = self.terminal.snapshot();
|
||||
let lines = snapshot.plain_rows();
|
||||
self.terminal_snapshot = snapshot;
|
||||
self.dispatch(ShellAction::ReplaceExternalOutput {
|
||||
pane: LIVE_PANE,
|
||||
lines,
|
||||
@@ -474,11 +663,10 @@ impl LumbridgeShell {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn pane_card(
|
||||
fn pane_selector(
|
||||
pane: &PaneState,
|
||||
selected: bool,
|
||||
runtime_status: LiveRuntimeStatus,
|
||||
terminal_dimensions: TerminalDimensions,
|
||||
focus: FocusHandle,
|
||||
cx: &mut Context<Self>,
|
||||
) -> gpui::AnyElement {
|
||||
@@ -500,147 +688,296 @@ impl LumbridgeShell {
|
||||
} else {
|
||||
ACCENT
|
||||
};
|
||||
let line_start = pane.lines().len().saturating_sub(12);
|
||||
|
||||
div()
|
||||
.id(("pane", id.index()))
|
||||
.track_focus(&focus)
|
||||
.tab_index(id.index() as isize + 1)
|
||||
.flex()
|
||||
.flex_col()
|
||||
.min_w_0()
|
||||
.min_h_0()
|
||||
.overflow_hidden()
|
||||
.bg(rgb(if selected { PANEL_ACTIVE } else { PANEL }))
|
||||
.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
|
||||
BORDER_QUIET
|
||||
}))
|
||||
.rounded(px(5.0))
|
||||
.when(selected, |view| view.border_2())
|
||||
.on_click(cx.listener(move |shell, _, window, cx| {
|
||||
shell.select_pane(id, window, cx);
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.px_3()
|
||||
.h(px(36.0))
|
||||
.flex_none()
|
||||
.bg(rgb(if selected { PANEL_ACTIVE } else { PANEL_ALT }))
|
||||
.border_b_1()
|
||||
.border_color(rgb(BORDER_QUIET))
|
||||
.text_sm()
|
||||
.text_color(rgb(TEXT))
|
||||
.flex_col()
|
||||
.min_w_0()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(rgb(MUTED))
|
||||
.child(format!("{}", id.index() + 1)),
|
||||
)
|
||||
.truncate()
|
||||
.text_sm()
|
||||
.text_color(rgb(TEXT))
|
||||
.child(if external {
|
||||
"Shell · VT session"
|
||||
"Terminal · local shell"
|
||||
} else {
|
||||
pane.fixture().title
|
||||
}),
|
||||
)
|
||||
.child(div().text_xs().text_color(rgb(state_color)).child(label)),
|
||||
.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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
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()
|
||||
.py_2()
|
||||
.text_xs()
|
||||
.text_color(rgb(MUTED))
|
||||
.child(if external {
|
||||
format!(
|
||||
"{} · {}×{} · Alt+Shift+arrows resize",
|
||||
runtime_status.detail(),
|
||||
terminal_dimensions.columns(),
|
||||
terminal_dimensions.rows()
|
||||
)
|
||||
} else {
|
||||
pane.fixture().target.to_owned()
|
||||
})
|
||||
.when(selected, |view| view.child("FOCUSED")),
|
||||
.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),
|
||||
),
|
||||
)
|
||||
.when(external && selected, |view| {
|
||||
view.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.h(px(26.0))
|
||||
.flex_none()
|
||||
.px_3()
|
||||
.border_y_1()
|
||||
.border_color(rgb(BORDER_QUIET))
|
||||
.bg(rgb(PANEL_ALT))
|
||||
.text_xs()
|
||||
.child(div().text_color(rgb(ACCENT)).child("TERMINAL"))
|
||||
.child(div().text_color(rgb(MUTED)).child("BROWSER"))
|
||||
.child(div().text_color(rgb(MUTED)).child("TOOLS"))
|
||||
.child(div().text_color(rgb(MUTED)).child("CONTEXT"))
|
||||
.child(div().text_color(rgb(MUTED)).child("GOAL"))
|
||||
.child(div().text_color(rgb(MUTED)).child("REVIEW")),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.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()
|
||||
.gap_1()
|
||||
.px_3()
|
||||
.pb_3()
|
||||
.text_sm()
|
||||
.text_color(rgb(TEXT))
|
||||
.children(pane.lines()[line_start..].iter().cloned()),
|
||||
.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")),
|
||||
)
|
||||
.when(external && selected, |view| {
|
||||
view.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.h(px(38.0))
|
||||
.flex_none()
|
||||
.px_3()
|
||||
.border_t_1()
|
||||
.border_color(rgb(BORDER_QUIET))
|
||||
.bg(rgb(PANEL_ALT))
|
||||
.text_xs()
|
||||
.child(
|
||||
div()
|
||||
.text_color(rgb(MUTED))
|
||||
.child("LOCAL ANALYST · OFF · suggestions only"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.text_color(rgb(TEXT))
|
||||
.child("Continue")
|
||||
.child("Review plan")
|
||||
.child("Ask…"),
|
||||
),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -899,29 +1236,91 @@ impl Render for LumbridgeShell {
|
||||
.child("1 interactive VT · 5 deterministic · 1 waiting"),
|
||||
);
|
||||
|
||||
let pane_cards = self
|
||||
let pane_selectors = self
|
||||
.model
|
||||
.panes()
|
||||
.iter()
|
||||
.map(|pane| {
|
||||
Self::pane_card(
|
||||
Self::pane_selector(
|
||||
pane,
|
||||
self.model.selected_pane() == pane.id(),
|
||||
self.runtime_status.clone(),
|
||||
self.terminal.dimensions(),
|
||||
self.pane_focus[pane.id().index()].clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let grid = div()
|
||||
.grid()
|
||||
.grid_cols(3)
|
||||
.grid_rows(2)
|
||||
.gap_2()
|
||||
.p_2()
|
||||
let selected = self.model.pane(self.model.selected_pane());
|
||||
let workspace_stack = div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.children(pane_cards);
|
||||
.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!(
|
||||
@@ -1014,7 +1413,7 @@ impl Render for LumbridgeShell {
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.child(tabs)
|
||||
.child(grid),
|
||||
.child(workspace_stack),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -1141,7 +1540,11 @@ fn main() {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{KeyModifiers, TerminalKey, terminal_key_from_parts};
|
||||
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() {
|
||||
@@ -1158,4 +1561,32 @@ mod tests {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user