Build interactive native workspace vertical slice
CI / rust (push) Successful in 1m43s

This commit is contained in:
2026-08-31 16:15:32 -07:00
parent de89b015bc
commit 27beb69ff8
19 changed files with 10535 additions and 200 deletions
+627 -86
View File
@@ -1,85 +1,445 @@
use std::collections::VecDeque;
use std::time::{Duration, Instant};
use gpui::{
App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
rgb, size,
App, Application, Bounds, Context, FocusHandle, KeyBinding, KeyDownEvent, Window, WindowBounds,
WindowOptions, actions, div, prelude::*, px, rgb, size,
};
use lumbridge_spike_model::{
FOOTER_CENTER, FOOTER_LEFT, FOOTER_RIGHT, PANES, PaneFixture, WORKSPACES,
ActionOutcome, FOOTER_RIGHT, FocusDirection, PaneId, PaneState, ShellAction, ShellModel,
SurfaceKind, WORKSPACES,
};
const BG: u32 = 0x0c0e13;
const PANEL: u32 = 0x121722;
const PANEL_ALT: u32 = 0x171d29;
const BORDER: u32 = 0x293244;
const TEXT: u32 = 0xd9e2f2;
const MUTED: u32 = 0x7f8ba3;
const ACCENT: u32 = 0x77bdfb;
const BG: u32 = 0x090c12;
const PANEL: u32 = 0x101620;
const PANEL_ALT: u32 = 0x151d29;
const PANEL_ACTIVE: u32 = 0x182334;
const BORDER: u32 = 0x263246;
const BORDER_QUIET: u32 = 0x1c2636;
const TEXT: u32 = 0xdbe5f4;
const MUTED: u32 = 0x8290a8;
const ACCENT: u32 = 0x68b5f8;
const ATTENTION: u32 = 0xf1b96a;
const SUCCESS: u32 = 0x70d6a8;
const TIMING_SAMPLE_LIMIT: usize = 256;
struct LumbridgeShell;
actions!(
lumbridge,
[
FocusLeft,
FocusRight,
FocusUp,
FocusDown,
OpenPalette,
ClosePalette,
SelectPane1,
SelectPane2,
SelectPane3,
SelectPane4,
SelectPane5,
SelectPane6,
]
);
fn pane_card(pane: &PaneFixture) -> impl IntoElement {
div()
.flex()
.flex_col()
.min_w_0()
.min_h_0()
.overflow_hidden()
.bg(rgb(PANEL))
.border_1()
.border_color(rgb(BORDER))
.rounded_md()
.child(
div()
.flex()
.items_center()
.justify_between()
.px_3()
.h(px(34.0))
.bg(rgb(PANEL_ALT))
.border_b_1()
.border_color(rgb(BORDER))
.text_sm()
.text_color(rgb(TEXT))
.child(pane.title)
.child(div().text_xs().text_color(rgb(ACCENT)).child(pane.badge)),
)
.child(
div()
.px_3()
.py_2()
.text_xs()
.text_color(rgb(MUTED))
.child(pane.target),
)
.child(
div()
.flex()
.flex_col()
.gap_1()
.px_3()
.pb_3()
.text_sm()
.text_color(rgb(TEXT))
.children(pane.lines.into_iter()),
struct LumbridgeShell {
model: ShellModel,
timing: RenderTiming,
root_focus: FocusHandle,
pane_focus: [FocusHandle; 6],
}
#[derive(Default)]
struct RenderTiming {
pending_dispatch: Option<Instant>,
dispatch_to_element_micros: VecDeque<u128>,
}
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()
)
}
}
impl LumbridgeShell {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let pane_focus = std::array::from_fn(|index| {
cx.focus_handle()
.tab_index(index as isize + 1)
.tab_stop(true)
});
let root_focus = cx.focus_handle();
window.focus(&pane_focus[0]);
cx.spawn(async move |this, cx| {
loop {
cx.background_executor()
.timer(Duration::from_millis(650))
.await;
if this
.update(cx, |shell, cx| {
shell.dispatch(ShellAction::SyntheticStreamTick);
cx.notify();
})
.is_err()
{
break;
}
}
})
.detach();
Self {
model: ShellModel::default(),
timing: RenderTiming::default(),
root_focus,
pane_focus,
}
}
fn dispatch(&mut self, action: ShellAction) -> ActionOutcome {
self.timing.mark_dispatch();
self.model.dispatch(action)
}
fn select_pane(&mut self, pane: PaneId, window: &mut Window, cx: &mut Context<Self>) {
self.dispatch(ShellAction::SelectPane(pane));
window.focus(&self.pane_focus[pane.index()]);
cx.notify();
}
fn move_focus(
&mut self,
direction: FocusDirection,
window: &mut Window,
cx: &mut Context<Self>,
) {
let outcome = self.dispatch(ShellAction::MoveFocus(direction));
window.focus(&self.pane_focus[outcome.selected_pane.index()]);
cx.notify();
}
fn focus_left(&mut self, _: &FocusLeft, window: &mut Window, cx: &mut Context<Self>) {
self.move_focus(FocusDirection::Left, window, cx);
}
fn focus_right(&mut self, _: &FocusRight, window: &mut Window, cx: &mut Context<Self>) {
self.move_focus(FocusDirection::Right, window, cx);
}
fn focus_up(&mut self, _: &FocusUp, window: &mut Window, cx: &mut Context<Self>) {
self.move_focus(FocusDirection::Up, window, cx);
}
fn focus_down(&mut self, _: &FocusDown, window: &mut Window, cx: &mut Context<Self>) {
self.move_focus(FocusDirection::Down, window, cx);
}
fn open_palette(&mut self, _: &OpenPalette, _: &mut Window, cx: &mut Context<Self>) {
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 on_key_down(&mut self, event: &KeyDownEvent, _window: &mut Window, cx: &mut Context<Self>) {
if !self.model.command_palette().is_open() {
return;
}
let mut query = self.model.command_palette().query().to_owned();
match event.keystroke.key.as_str() {
"backspace" => {
query.pop();
}
"enter" => {
self.dispatch(ShellAction::CloseCommandPalette);
cx.notify();
return;
}
_ => {
if let Some(character) = event.keystroke.key_char.as_deref()
&& !event.keystroke.modifiers.control
&& !event.keystroke.modifiers.platform
{
query.push_str(character);
} else {
return;
}
}
}
self.dispatch(ShellAction::SetCommandPaletteQuery(query));
cx.notify();
}
fn pane_card(
pane: &PaneState,
selected: bool,
focus: FocusHandle,
cx: &mut Context<Self>,
) -> gpui::AnyElement {
let id = pane.id();
let needs_input = pane.needs_input();
let label = match pane.kind() {
SurfaceKind::Terminal => pane.status().label(),
_ => pane.fixture().badge,
};
let state_color = if needs_input {
ATTENTION
} else if matches!(pane.kind(), SurfaceKind::Terminal) {
SUCCESS
} 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 }))
.border_1()
.border_color(rgb(if selected || needs_input {
state_color
} else {
BORDER
}))
.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))
.child(
div()
.flex()
.items_center()
.gap_2()
.child(
div()
.text_xs()
.text_color(rgb(MUTED))
.child(format!("{}", id.index() + 1)),
)
.child(pane.fixture().title),
)
.child(div().text_xs().text_color(rgb(state_color)).child(label)),
)
.child(
div()
.flex()
.items_center()
.justify_between()
.px_3()
.py_2()
.text_xs()
.text_color(rgb(MUTED))
.child(pane.fixture().target)
.when(selected, |view| view.child("FOCUSED")),
)
.child(
div()
.flex()
.flex_col()
.gap_1()
.px_3()
.pb_3()
.text_sm()
.text_color(rgb(TEXT))
.children(pane.lines()[line_start..].iter().cloned()),
)
.into_any_element()
}
fn command_palette(&self) -> impl IntoElement {
let query = self.model.command_palette().query();
let prompt = if query.is_empty() {
"Type a command…".to_owned()
} else {
query.to_owned()
};
div()
.absolute()
.inset_0()
.flex()
.justify_center()
.items_start()
.pt(px(92.0))
.bg(gpui::black().opacity(0.72))
.child(
div()
.w(px(620.0))
.overflow_hidden()
.bg(rgb(PANEL_ALT))
.border_1()
.border_color(rgb(ACCENT))
.rounded(px(8.0))
.child(
div()
.px_4()
.py_3()
.border_b_1()
.border_color(rgb(BORDER))
.text_color(rgb(if query.is_empty() { MUTED } else { TEXT }))
.child(prompt),
)
.child(
div()
.flex()
.flex_col()
.p_2()
.text_sm()
.child(
div()
.px_3()
.py_2()
.rounded(px(5.0))
.bg(rgb(PANEL_ACTIVE))
.child("Focus next pane")
.child(
div()
.mt_1()
.text_xs()
.text_color(rgb(MUTED))
.child("Workspace · navigation"),
),
)
.child(
div()
.px_3()
.py_2()
.text_color(rgb(MUTED))
.child("Open attention request"),
)
.child(
div()
.px_3()
.py_2()
.text_color(rgb(MUTED))
.child("Share selected pane to Buzz…"),
),
)
.child(
div()
.flex()
.justify_between()
.px_4()
.py_2()
.border_t_1()
.border_color(rgb(BORDER))
.text_xs()
.text_color(rgb(MUTED))
.child("Enter to run")
.child("Esc to close"),
),
)
}
}
impl Render for LumbridgeShell {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let attention = self.model.pane(PaneId::ClaudeUi);
let sidebar = div()
.flex()
.flex_col()
.w(px(220.0))
.w(px(248.0))
.flex_none()
.bg(rgb(PANEL))
.border_r_1()
.border_color(rgb(BORDER))
.border_color(rgb(BORDER_QUIET))
.child(
div()
.px_4()
.py_3()
.text_sm()
.pt_4()
.pb_2()
.text_xs()
.text_color(rgb(ATTENTION))
.child("ATTENTION · 1"),
)
.child(
div()
.mx_2()
.mb_3()
.px_3()
.py_2()
.rounded(px(5.0))
.bg(rgb(PANEL_ACTIVE))
.border_1()
.border_color(rgb(ATTENTION))
.child(
div()
.text_sm()
.text_color(rgb(TEXT))
.child(attention.fixture().title),
)
.child(
div()
.mt_1()
.text_xs()
.text_color(rgb(MUTED))
.child("Waiting for a split decision"),
),
)
.child(
div()
.px_4()
.py_2()
.text_xs()
.text_color(rgb(MUTED))
.child("WORKSPACES"),
.child("WORKTREES"),
)
.children(WORKSPACES.into_iter().enumerate().map(|(index, name)| {
div()
@@ -87,30 +447,130 @@ impl Render for LumbridgeShell {
.mb_1()
.px_3()
.py_2()
.rounded_md()
.rounded(px(5.0))
.when(index == 0, |view| {
view.bg(rgb(PANEL_ALT)).text_color(rgb(TEXT))
})
.when(index != 0, |view| view.text_color(rgb(MUTED)))
.child(name)
.when(index == 0, |view| {
view.child(
div()
.mt_1()
.text_xs()
.text_color(rgb(MUTED))
.child("main · metal"),
)
})
}))
.child(
div()
.mt_4()
.mt_3()
.px_4()
.py_2()
.text_xs()
.text_color(rgb(MUTED))
.child("HOSTS")
.child("RUNTIMES"),
)
.child(
div()
.px_4()
.py_1()
.text_sm()
.text_color(rgb(SUCCESS))
.child("metal · connected"),
)
.child(
div()
.px_4()
.py_1()
.text_sm()
.text_color(rgb(MUTED))
.child("amd-server · connected"),
)
.child(
div()
.px_4()
.py_1()
.text_sm()
.text_color(rgb(MUTED))
.child("spark-1 · sleeping"),
)
.child(div().flex_1())
.child(
div()
.m_3()
.p_3()
.rounded(px(5.0))
.bg(rgb(PANEL_ALT))
.text_xs()
.text_color(rgb(MUTED))
.child("Buzz · lumbridgecode")
.child(
div()
.mt_2()
.text_color(rgb(ACCENT))
.child("● metal · connected"),
)
.child(div().mt_2().child("● amd-server · connected"))
.child(div().mt_2().child("○ spark-1 · sleeping")),
.mt_1()
.text_color(rgb(SUCCESS))
.child("connected · signed identity"),
),
);
let tabs = div()
.flex()
.items_center()
.h(px(38.0))
.flex_none()
.px_2()
.gap_1()
.bg(rgb(PANEL))
.border_b_1()
.border_color(rgb(BORDER_QUIET))
.child(
div()
.h_full()
.flex()
.items_center()
.px_3()
.border_b_2()
.border_color(rgb(ACCENT))
.text_sm()
.child("Agent workspace"),
)
.child(
div()
.px_3()
.text_sm()
.text_color(rgb(MUTED))
.child("Architecture.md"),
)
.child(
div()
.px_3()
.text_sm()
.text_color(rgb(MUTED))
.child("Review"),
)
.child(div().flex_1())
.child(
div()
.px_3()
.text_xs()
.text_color(rgb(MUTED))
.child("6 surfaces · 3 remote · 1 waiting"),
);
let pane_cards = self
.model
.panes()
.iter()
.map(|pane| {
Self::pane_card(
pane,
self.model.selected_pane() == pane.id(),
self.pane_focus[pane.id().index()].clone(),
cx,
)
})
.collect::<Vec<_>>();
let grid = div()
.grid()
.grid_cols(3)
@@ -118,9 +578,48 @@ impl Render for LumbridgeShell {
.gap_2()
.p_2()
.size_full()
.children(PANES.iter().map(pane_card));
.children(pane_cards);
div()
let counters = self.model.counters();
let footer_left = format!(
"rev {} · {} focus moves · {} surface updates · {} lines",
self.model.revision(),
counters.focus_moves,
counters.surface_updates,
counters.terminal_lines_appended
);
let timing = self.timing.summary();
let root = div()
.id("lumbridge-shell")
.relative()
.track_focus(&self.root_focus)
.key_context("LumbridgeShell")
.on_action(cx.listener(Self::focus_left))
.on_action(cx.listener(Self::focus_right))
.on_action(cx.listener(Self::focus_up))
.on_action(cx.listener(Self::focus_down))
.on_action(cx.listener(Self::open_palette))
.on_action(cx.listener(Self::close_palette))
.on_action(cx.listener(|shell, _: &SelectPane1, window, cx| {
shell.select_pane(PaneId::CodexRuntime, window, cx);
}))
.on_action(cx.listener(|shell, _: &SelectPane2, window, cx| {
shell.select_pane(PaneId::ClaudeUi, window, cx);
}))
.on_action(cx.listener(|shell, _: &SelectPane3, window, cx| {
shell.select_pane(PaneId::PiDocs, window, cx);
}))
.on_action(cx.listener(|shell, _: &SelectPane4, window, cx| {
shell.select_pane(PaneId::Architecture, window, cx);
}))
.on_action(cx.listener(|shell, _: &SelectPane5, window, cx| {
shell.select_pane(PaneId::AcpPreview, window, cx);
}))
.on_action(cx.listener(|shell, _: &SelectPane6, window, cx| {
shell.select_pane(PaneId::RuntimeReview, window, cx);
}))
.on_key_down(cx.listener(Self::on_key_down))
.flex()
.flex_col()
.size_full()
@@ -130,63 +629,105 @@ impl Render for LumbridgeShell {
div()
.flex()
.items_center()
.justify_between()
.h(px(46.0))
.h(px(48.0))
.flex_none()
.px_4()
.bg(rgb(PANEL_ALT))
.border_b_1()
.border_color(rgb(BORDER))
.border_color(rgb(BORDER_QUIET))
.child(div().text_lg().child("Lumbridge"))
.child(
div()
.ml_3()
.text_sm()
.text_color(rgb(MUTED))
.child("UI spike · GPUI"),
.child("Lumbridge Code / main"),
)
.child(div().flex_1())
.child(
div()
.mr_4()
.text_xs()
.text_color(rgb(SUCCESS))
.child("metal · runtime online"),
)
.child(
div()
.text_sm()
.text_color(rgb(ACCENT))
.child("K Command Palette"),
.child("Ctrl/⌘ K · Commands"),
),
)
.child(div().flex().flex_1().min_h_0().child(sidebar).child(grid))
.child(
div().flex().flex_1().min_h_0().child(sidebar).child(
div()
.flex()
.flex_col()
.flex_1()
.min_w_0()
.child(tabs)
.child(grid),
),
)
.child(
div()
.flex()
.items_center()
.justify_between()
.h(px(30.0))
.h(px(32.0))
.flex_none()
.px_3()
.bg(rgb(PANEL_ALT))
.border_t_1()
.border_color(rgb(BORDER))
.border_color(rgb(BORDER_QUIET))
.text_xs()
.text_color(rgb(MUTED))
.child(FOOTER_LEFT)
.child(FOOTER_CENTER)
.child(footer_left)
.child(timing)
.child(FOOTER_RIGHT),
)
.when(self.model.command_palette().is_open(), |view| {
view.child(self.command_palette())
});
self.timing.observe_element_build();
root
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(1280.0), px(800.0)), cx);
cx.bind_keys([
KeyBinding::new("left", FocusLeft, Some("LumbridgeShell")),
KeyBinding::new("right", FocusRight, Some("LumbridgeShell")),
KeyBinding::new("up", FocusUp, Some("LumbridgeShell")),
KeyBinding::new("down", FocusDown, Some("LumbridgeShell")),
KeyBinding::new("h", FocusLeft, Some("LumbridgeShell")),
KeyBinding::new("l", FocusRight, Some("LumbridgeShell")),
KeyBinding::new("k", FocusUp, Some("LumbridgeShell")),
KeyBinding::new("j", FocusDown, Some("LumbridgeShell")),
KeyBinding::new("secondary-k", OpenPalette, Some("LumbridgeShell")),
KeyBinding::new("escape", ClosePalette, Some("LumbridgeShell")),
KeyBinding::new("1", SelectPane1, Some("LumbridgeShell")),
KeyBinding::new("2", SelectPane2, Some("LumbridgeShell")),
KeyBinding::new("3", SelectPane3, Some("LumbridgeShell")),
KeyBinding::new("4", SelectPane4, Some("LumbridgeShell")),
KeyBinding::new("5", SelectPane5, Some("LumbridgeShell")),
KeyBinding::new("6", SelectPane6, Some("LumbridgeShell")),
]);
let bounds = Bounds::centered(None, size(px(1500.0), px(960.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
titlebar: Some(gpui::TitlebarOptions {
title: Some("Lumbridge · GPUI spike".into()),
title: Some("Lumbridge · GPUI workspace".into()),
..Default::default()
}),
..Default::default()
},
|_, cx| cx.new(|_| LumbridgeShell),
|window, cx| cx.new(|cx| LumbridgeShell::new(window, cx)),
)
.expect("GPUI spike window should open");
.expect("GPUI workspace window should open");
cx.activate(true);
});
}