Spike native shells and define local-first integrations
CI / rust (push) Successful in 1m40s

This commit is contained in:
2026-08-31 15:42:26 -07:00
parent a703b5a5ee
commit aaa1cb1fa9
28 changed files with 14809 additions and 14 deletions
+37
View File
@@ -0,0 +1,37 @@
# Native UI spikes
These disposable applications render the same fixture through GPUI and Floem.
They are a decision instrument, not product code. The root workspace excludes
this nested workspace so normal Lumbridge CI does not download or compile both
UI frameworks.
The GPUI spike uses published `gpui 0.2.2`. The Floem spike pins upstream commit
`778bb5f2aa08429e579ee2e6ac97e84fbf18b618`; the crates.io `floem 0.2.0` package
lags the current API substantially enough that comparing it to current GPUI
would not be representative.
Both spikes must preserve the same information architecture:
- workspace/sidebar and remote host state;
- two rows of three busy surfaces;
- local and remote terminal/agent panes;
- native Markdown editor/preview and browser placeholders;
- connection, harness, usage, and burn context in the footer.
Build independently:
```bash
cargo build --release --manifest-path spikes/gpui-shell/Cargo.toml
cargo build --release --manifest-path spikes/floem-shell/Cargo.toml
```
Each candidate is an independent Cargo workspace. GPUI pins `taffy 0.9.0`
while current Floem requires `taffy 0.9.2`; putting them in one comparison
workspace creates an artificial resolver conflict and would let one candidate's
dependency decisions distort the other candidate's build.
The comparison records release build time, binary size, startup, idle RSS,
six-pane streaming frame time, key-to-present latency, accessibility/IME,
window behavior, browser-child integration, packaging, dependency count, and
license closure on macOS, Ubuntu, and Omarchy. A build is not adoption: GPUI's
complete dependency-license closure remains a hard gate.
+4680
View File
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "lumbridge-spike-floem"
description = "Floem implementation of the Lumbridge native shell spike"
version = "0.0.1"
edition = "2024"
rust-version = "1.94"
license = "Apache-2.0"
publish = false
[dependencies]
floem = { git = "https://github.com/lapce/floem", rev = "778bb5f2aa08429e579ee2e6ac97e84fbf18b618", default-features = false, features = ["vger"] }
lumbridge-spike-model = { path = "../ui-shell-model" }
[workspace]
+163
View File
@@ -0,0 +1,163 @@
use floem::{Application, kurbo::Size, peniko::Color, prelude::*, window::WindowConfig};
use lumbridge_spike_model::{
FOOTER_CENTER, FOOTER_LEFT, FOOTER_RIGHT, PANES, PaneFixture, WORKSPACES,
};
const BG: Color = Color::from_rgb8(12, 14, 19);
const PANEL: Color = Color::from_rgb8(18, 23, 34);
const PANEL_ALT: Color = Color::from_rgb8(23, 29, 41);
const BORDER: Color = Color::from_rgb8(41, 50, 68);
const TEXT: Color = Color::from_rgb8(217, 226, 242);
const MUTED: Color = Color::from_rgb8(127, 139, 163);
const ACCENT: Color = Color::from_rgb8(119, 189, 251);
fn pane_card(pane: PaneFixture) -> impl IntoView {
let header = Stack::horizontal((
Label::new(pane.title).style(|s| s.flex_grow(1.0).color(TEXT)),
Label::new(pane.badge).style(|s| s.font_size(11.0).color(ACCENT)),
))
.style(|s| {
s.items_center()
.height(34.0)
.padding_horiz(10.0)
.background(PANEL_ALT)
.border_bottom(1.0)
.border_color(BORDER)
});
let lines =
Stack::vertical(pane.lines.map(|line| {
Label::new(line).style(|s| s.font_size(12.0).color(TEXT).margin_bottom(5.0))
}));
Stack::vertical((
header,
Label::new(pane.target).style(|s| s.font_size(11.0).color(MUTED).padding(10.0)),
lines.style(|s| s.padding_horiz(10.0).padding_bottom(10.0)),
))
.style(|s| {
s.flex_basis(0)
.flex_grow(1.0)
.min_width(0.0)
.min_height(0.0)
.margin(4.0)
.background(PANEL)
.border(1.0)
.border_color(BORDER)
.border_radius(6.0)
})
}
fn app_view() -> impl IntoView {
let sidebar_items = Stack::vertical(WORKSPACES.map(|name| {
Label::new(name).style(move |s| {
s.width_full()
.padding_vert(8.0)
.padding_horiz(12.0)
.margin_bottom(3.0)
.color(if name == "Lumbridge Code" {
TEXT
} else {
MUTED
})
.apply_if(name == "Lumbridge Code", |s| {
s.background(PANEL_ALT).border_radius(6.0)
})
})
}));
let sidebar = Stack::vertical((
Label::new("WORKSPACES").style(|s| s.font_size(11.0).color(MUTED).padding(12.0)),
sidebar_items.style(|s| s.padding_horiz(8.0)),
Label::new("HOSTS").style(|s| {
s.font_size(11.0)
.color(MUTED)
.padding(12.0)
.margin_top(12.0)
}),
Label::new("● metal · connected").style(|s| s.color(ACCENT).padding_horiz(14.0)),
Label::new("● amd-server · connected")
.style(|s| s.color(MUTED).padding_horiz(14.0).margin_top(8.0)),
Label::new("○ spark-1 · sleeping")
.style(|s| s.color(MUTED).padding_horiz(14.0).margin_top(8.0)),
))
.style(|s| {
s.width(220.0)
.height_full()
.flex_shrink(0.0)
.background(PANEL)
.border_right(1.0)
.border_color(BORDER)
});
let top_row = Stack::horizontal((
pane_card(PANES[0]),
pane_card(PANES[1]),
pane_card(PANES[2]),
))
.style(|s| s.flex_basis(0).flex_grow(1.0).min_height(0.0).width_full());
let bottom_row = Stack::horizontal((
pane_card(PANES[3]),
pane_card(PANES[4]),
pane_card(PANES[5]),
))
.style(|s| s.flex_basis(0).flex_grow(1.0).min_height(0.0).width_full());
let panes = Stack::vertical((top_row, bottom_row)).style(|s| {
s.flex_basis(0)
.flex_grow(1.0)
.min_width(0.0)
.height_full()
.padding(4.0)
});
let header = Stack::horizontal((
Label::new("Lumbridge").style(|s| s.font_size(18.0).color(TEXT).flex_grow(1.0)),
Label::new("UI spike · Floem").style(|s| s.color(MUTED).flex_grow(1.0)),
Label::new("⌘K Command Palette").style(|s| s.color(ACCENT)),
))
.style(|s| {
s.height(46.0)
.padding_horiz(14.0)
.items_center()
.background(PANEL_ALT)
.border_bottom(1.0)
.border_color(BORDER)
});
let body = Stack::horizontal((sidebar, panes))
.style(|s| s.flex_basis(0).flex_grow(1.0).min_height(0.0));
let footer = Stack::horizontal((
Label::new(FOOTER_LEFT).style(|s| s.flex_grow(1.0)),
Label::new(FOOTER_CENTER).style(|s| s.flex_grow(1.0)),
Label::new(FOOTER_RIGHT),
))
.style(|s| {
s.height(30.0)
.padding_horiz(10.0)
.items_center()
.font_size(11.0)
.color(MUTED)
.background(PANEL_ALT)
.border_top(1.0)
.border_color(BORDER)
});
Stack::vertical((header, body, footer))
.style(|s| s.width_full().height_full().background(BG))
.window_title(|| "Lumbridge · Floem spike".to_owned())
}
fn main() {
Application::new()
.window(
|_| app_view(),
Some(
WindowConfig::default()
.size(Size::new(1280.0, 800.0))
.min_size(Size::new(900.0, 600.0))
.title("Lumbridge · Floem spike"),
),
)
.run();
}
+7195
View File
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "lumbridge-spike-gpui"
description = "GPUI implementation of the Lumbridge native shell spike"
version = "0.0.1"
edition = "2024"
rust-version = "1.94"
license = "Apache-2.0"
publish = false
[dependencies]
gpui = "0.2.2"
lumbridge-spike-model = { path = "../ui-shell-model" }
[workspace]
+192
View File
@@ -0,0 +1,192 @@
use gpui::{
App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
rgb, size,
};
use lumbridge_spike_model::{
FOOTER_CENTER, FOOTER_LEFT, FOOTER_RIGHT, PANES, PaneFixture, 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;
struct LumbridgeShell;
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()),
)
}
impl Render for LumbridgeShell {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let sidebar = div()
.flex()
.flex_col()
.w(px(220.0))
.flex_none()
.bg(rgb(PANEL))
.border_r_1()
.border_color(rgb(BORDER))
.child(
div()
.px_4()
.py_3()
.text_sm()
.text_color(rgb(MUTED))
.child("WORKSPACES"),
)
.children(WORKSPACES.into_iter().enumerate().map(|(index, name)| {
div()
.mx_2()
.mb_1()
.px_3()
.py_2()
.rounded_md()
.when(index == 0, |view| {
view.bg(rgb(PANEL_ALT)).text_color(rgb(TEXT))
})
.when(index != 0, |view| view.text_color(rgb(MUTED)))
.child(name)
}))
.child(
div()
.mt_4()
.px_4()
.text_xs()
.text_color(rgb(MUTED))
.child("HOSTS")
.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")),
);
let grid = div()
.grid()
.grid_cols(3)
.grid_rows(2)
.gap_2()
.p_2()
.size_full()
.children(PANES.iter().map(pane_card));
div()
.flex()
.flex_col()
.size_full()
.bg(rgb(BG))
.text_color(rgb(TEXT))
.child(
div()
.flex()
.items_center()
.justify_between()
.h(px(46.0))
.flex_none()
.px_4()
.bg(rgb(PANEL_ALT))
.border_b_1()
.border_color(rgb(BORDER))
.child(div().text_lg().child("Lumbridge"))
.child(
div()
.text_sm()
.text_color(rgb(MUTED))
.child("UI spike · GPUI"),
)
.child(
div()
.text_sm()
.text_color(rgb(ACCENT))
.child("⌘K Command Palette"),
),
)
.child(div().flex().flex_1().min_h_0().child(sidebar).child(grid))
.child(
div()
.flex()
.items_center()
.justify_between()
.h(px(30.0))
.flex_none()
.px_3()
.bg(rgb(PANEL_ALT))
.border_t_1()
.border_color(rgb(BORDER))
.text_xs()
.text_color(rgb(MUTED))
.child(FOOTER_LEFT)
.child(FOOTER_CENTER)
.child(FOOTER_RIGHT),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(1280.0), px(800.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
titlebar: Some(gpui::TitlebarOptions {
title: Some("Lumbridge · GPUI spike".into()),
..Default::default()
}),
..Default::default()
},
|_, cx| cx.new(|_| LumbridgeShell),
)
.expect("GPUI spike window should open");
cx.activate(true);
});
}
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "lumbridge-spike-model"
version = "0.0.1"
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "lumbridge-spike-model"
description = "Identical fixture data for Lumbridge native UI comparisons"
version = "0.0.1"
edition = "2024"
rust-version = "1.94"
license = "Apache-2.0"
publish = false
[lib]
path = "src/lib.rs"
[workspace]
+122
View File
@@ -0,0 +1,122 @@
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SurfaceKind {
Terminal,
Markdown,
Browser,
Review,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PaneFixture {
pub title: &'static str,
pub badge: &'static str,
pub target: &'static str,
pub kind: SurfaceKind,
pub lines: [&'static str; 4],
}
pub const PANES: [PaneFixture; 6] = [
PaneFixture {
title: "Codex · runtime",
badge: "WORKING",
target: "metal · Tailscale SSH",
kind: SurfaceKind::Terminal,
lines: [
"$ cargo nextest run -p lumbridge-runtime",
"PASS remote::reconnect_replays_output",
"PASS pty::resize_preserves_cursor",
"agent is editing 3 files…",
],
},
PaneFixture {
title: "Claude Code · UI",
badge: "NEEDS INPUT",
target: "MacBook Air · local",
kind: SurfaceKind::Terminal,
lines: [
"$ bacon clippy",
"finished in 0.42s",
"Which split should receive focus?",
"[Approve] [Steer] [Cancel]",
],
},
PaneFixture {
title: "Pi · docs",
badge: "STREAMING",
target: "amd-server · OpenSSH",
kind: SurfaceKind::Terminal,
lines: [
"$ cargo watch --why",
"ACP session resumed at event 1842",
"writing remote-session.md",
"",
],
},
PaneFixture {
title: "Architecture.md",
badge: "MARKDOWN",
target: "lumbridge-code · worktree",
kind: SurfaceKind::Markdown,
lines: [
"## Remote session path",
"The destination runtime owns the PTY.",
"The laptop may disconnect and reattach.",
"SQLite remains local to each machine.",
],
},
PaneFixture {
title: "Preview · ACP docs",
badge: "BROWSER",
target: "isolated system web engine",
kind: SurfaceKind::Browser,
lines: [
"https://agentclientprotocol.com",
"Content process: sandboxed",
"Runtime bridge: no privileged access",
"Open in external browser ↗",
],
},
PaneFixture {
title: "Changes · lumbridge-runtime",
badge: "REVIEW",
target: "metal · worktree remote-runtime",
kind: SurfaceKind::Review,
lines: [
"+ 184 remote transport",
"+ 96 SQLite migrations",
" 12 obsolete scaffold",
"6 files · tests passing",
],
},
];
pub const WORKSPACES: [&str; 5] = [
"Lumbridge Code",
"Runtime / metal",
"UI spikes",
"ACP adapters",
"Usage telemetry",
];
pub const FOOTER_LEFT: &str = "6 panes · 3 remote · 1 needs input";
pub const FOOTER_CENTER: &str = "Codex · ChatGPT subscription · 62% window remaining";
pub const FOOTER_RIGHT: &str = "burn 8.4%/hr · resets in 2h 14m";
#[cfg(test)]
mod tests {
use super::{PANES, SurfaceKind};
#[test]
fn comparison_fixture_has_six_mixed_surfaces() {
assert_eq!(PANES.len(), 6);
assert!(PANES.iter().any(|pane| pane.kind == SurfaceKind::Markdown));
assert!(PANES.iter().any(|pane| pane.kind == SurfaceKind::Browser));
assert_eq!(
PANES
.iter()
.filter(|pane| pane.kind == SurfaceKind::Terminal)
.count(),
3
);
}
}