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
+618 -5
View File
@@ -1,3 +1,70 @@
//! Deterministic state and fixtures shared by the GPUI and Floem spikes.
//!
//! UI candidates translate framework input into [`ShellAction`] and render the
//! resulting [`ShellModel`]. This makes their interaction traces comparable.
use std::fmt;
pub const GRID_ROWS: usize = 2;
pub const GRID_COLUMNS: usize = 3;
pub const DEFAULT_TERMINAL_LINE_LIMIT: usize = 64;
/// Stable identity used in traces and accessibility identifiers.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[repr(u8)]
pub enum PaneId {
CodexRuntime = 0,
ClaudeUi = 1,
PiDocs = 2,
Architecture = 3,
AcpPreview = 4,
RuntimeReview = 5,
}
impl PaneId {
pub const ALL: [Self; GRID_ROWS * GRID_COLUMNS] = [
Self::CodexRuntime,
Self::ClaudeUi,
Self::PiDocs,
Self::Architecture,
Self::AcpPreview,
Self::RuntimeReview,
];
#[must_use]
pub const fn index(self) -> usize {
self as usize
}
#[must_use]
pub const fn stable_name(self) -> &'static str {
match self {
Self::CodexRuntime => "codex-runtime",
Self::ClaudeUi => "claude-ui",
Self::PiDocs => "pi-docs",
Self::Architecture => "architecture",
Self::AcpPreview => "acp-preview",
Self::RuntimeReview => "runtime-review",
}
}
#[must_use]
pub const fn row(self) -> usize {
self.index() / GRID_COLUMNS
}
#[must_use]
pub const fn column(self) -> usize {
self.index() % GRID_COLUMNS
}
}
impl fmt::Display for PaneId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.stable_name())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SurfaceKind {
Terminal,
@@ -6,21 +73,45 @@ pub enum SurfaceKind {
Review,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PaneStatus {
Working,
NeedsInput,
Streaming,
Ready,
}
impl PaneStatus {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Working => "WORKING",
Self::NeedsInput => "NEEDS INPUT",
Self::Streaming => "STREAMING",
Self::Ready => "READY",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PaneFixture {
pub id: PaneId,
pub title: &'static str,
pub badge: &'static str,
pub target: &'static str,
pub kind: SurfaceKind,
pub status: PaneStatus,
pub lines: [&'static str; 4],
}
pub const PANES: [PaneFixture; 6] = [
PaneFixture {
id: PaneId::CodexRuntime,
title: "Codex · runtime",
badge: "WORKING",
target: "metal · Tailscale SSH",
kind: SurfaceKind::Terminal,
status: PaneStatus::Working,
lines: [
"$ cargo nextest run -p lumbridge-runtime",
"PASS remote::reconnect_replays_output",
@@ -29,10 +120,12 @@ pub const PANES: [PaneFixture; 6] = [
],
},
PaneFixture {
id: PaneId::ClaudeUi,
title: "Claude Code · UI",
badge: "NEEDS INPUT",
target: "MacBook Air · local",
kind: SurfaceKind::Terminal,
status: PaneStatus::NeedsInput,
lines: [
"$ bacon clippy",
"finished in 0.42s",
@@ -41,10 +134,12 @@ pub const PANES: [PaneFixture; 6] = [
],
},
PaneFixture {
id: PaneId::PiDocs,
title: "Pi · docs",
badge: "STREAMING",
target: "amd-server · OpenSSH",
kind: SurfaceKind::Terminal,
status: PaneStatus::Streaming,
lines: [
"$ cargo watch --why",
"ACP session resumed at event 1842",
@@ -53,10 +148,12 @@ pub const PANES: [PaneFixture; 6] = [
],
},
PaneFixture {
id: PaneId::Architecture,
title: "Architecture.md",
badge: "MARKDOWN",
target: "lumbridge-code · worktree",
kind: SurfaceKind::Markdown,
status: PaneStatus::Ready,
lines: [
"## Remote session path",
"The destination runtime owns the PTY.",
@@ -65,10 +162,12 @@ pub const PANES: [PaneFixture; 6] = [
],
},
PaneFixture {
id: PaneId::AcpPreview,
title: "Preview · ACP docs",
badge: "BROWSER",
target: "isolated system web engine",
kind: SurfaceKind::Browser,
status: PaneStatus::Ready,
lines: [
"https://agentclientprotocol.com",
"Content process: sandboxed",
@@ -77,10 +176,12 @@ pub const PANES: [PaneFixture; 6] = [
],
},
PaneFixture {
id: PaneId::RuntimeReview,
title: "Changes · lumbridge-runtime",
badge: "REVIEW",
target: "metal · worktree remote-runtime",
kind: SurfaceKind::Review,
status: PaneStatus::Ready,
lines: [
"+ 184 remote transport",
"+ 96 SQLite migrations",
@@ -97,26 +198,538 @@ pub const WORKSPACES: [&str; 5] = [
"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";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PaneState {
fixture: PaneFixture,
status: PaneStatus,
status_before_input: Option<PaneStatus>,
lines: Vec<String>,
synthetic_line_sequence: u64,
}
impl PaneState {
#[must_use]
pub const fn id(&self) -> PaneId {
self.fixture.id
}
#[must_use]
pub const fn fixture(&self) -> &PaneFixture {
&self.fixture
}
#[must_use]
pub const fn kind(&self) -> SurfaceKind {
self.fixture.kind
}
#[must_use]
pub const fn status(&self) -> PaneStatus {
self.status
}
#[must_use]
pub const fn needs_input(&self) -> bool {
matches!(self.status, PaneStatus::NeedsInput)
}
#[must_use]
pub fn lines(&self) -> &[String] {
&self.lines
}
#[must_use]
pub const fn synthetic_line_sequence(&self) -> u64 {
self.synthetic_line_sequence
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct CommandPaletteState {
open: bool,
query: String,
}
impl CommandPaletteState {
#[must_use]
pub const fn is_open(&self) -> bool {
self.open
}
#[must_use]
pub fn query(&self) -> &str {
&self.query
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FocusDirection {
Left,
Right,
Up,
Down,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ShellAction {
MoveFocus(FocusDirection),
SelectPane(PaneId),
SetNeedsInput { pane: PaneId, needs_input: bool },
OpenCommandPalette,
CloseCommandPalette,
SetCommandPaletteQuery(String),
SyntheticStreamTick,
}
/// Counters prove both candidates replayed the same workload. Framework
/// adapters may use the event sequence and revision for explicitly labelled
/// timing stages; these counters do not imply pixels reached the display.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct MeasurementCounters {
pub events_dispatched: u64,
pub state_changes: u64,
pub focus_moves: u64,
pub blocked_focus_moves: u64,
pub direct_selections: u64,
pub needs_input_changes: u64,
pub palette_changes: u64,
pub synthetic_ticks: u64,
/// Total surfaces invalidated by the deterministic six-surface workload.
pub surface_updates: u64,
pub terminal_lines_appended: u64,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ActionOutcome {
pub event_sequence: u64,
pub revision: u64,
pub changed: bool,
pub selected_pane: PaneId,
pub terminal_lines_appended: usize,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct InvalidTerminalLineLimit;
impl fmt::Display for InvalidTerminalLineLimit {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("terminal line limit must be greater than zero")
}
}
impl std::error::Error for InvalidTerminalLineLimit {}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ShellModel {
panes: [PaneState; GRID_ROWS * GRID_COLUMNS],
selected_pane: PaneId,
command_palette: CommandPaletteState,
terminal_line_limit: usize,
synthetic_tick: u64,
revision: u64,
counters: MeasurementCounters,
}
impl Default for ShellModel {
fn default() -> Self {
Self::with_terminal_line_limit(DEFAULT_TERMINAL_LINE_LIMIT)
.expect("the default terminal line limit is non-zero")
}
}
impl ShellModel {
pub fn with_terminal_line_limit(limit: usize) -> Result<Self, InvalidTerminalLineLimit> {
if limit == 0 {
return Err(InvalidTerminalLineLimit);
}
let panes = PANES.map(|fixture| {
let mut lines = fixture.lines.map(String::from).to_vec();
if fixture.kind == SurfaceKind::Terminal && lines.len() > limit {
lines.drain(..lines.len() - limit);
}
PaneState {
fixture,
status: fixture.status,
status_before_input: (fixture.status == PaneStatus::NeedsInput)
.then_some(PaneStatus::Working),
lines,
synthetic_line_sequence: 0,
}
});
Ok(Self {
panes,
selected_pane: PaneId::CodexRuntime,
command_palette: CommandPaletteState::default(),
terminal_line_limit: limit,
synthetic_tick: 0,
revision: 0,
counters: MeasurementCounters::default(),
})
}
#[must_use]
pub fn panes(&self) -> &[PaneState; GRID_ROWS * GRID_COLUMNS] {
&self.panes
}
#[must_use]
pub fn pane(&self, id: PaneId) -> &PaneState {
&self.panes[id.index()]
}
#[must_use]
pub const fn selected_pane(&self) -> PaneId {
self.selected_pane
}
#[must_use]
pub const fn command_palette(&self) -> &CommandPaletteState {
&self.command_palette
}
#[must_use]
pub const fn terminal_line_limit(&self) -> usize {
self.terminal_line_limit
}
#[must_use]
pub const fn synthetic_tick(&self) -> u64 {
self.synthetic_tick
}
#[must_use]
pub const fn revision(&self) -> u64 {
self.revision
}
#[must_use]
pub const fn counters(&self) -> MeasurementCounters {
self.counters
}
pub fn dispatch(&mut self, action: ShellAction) -> ActionOutcome {
self.counters.events_dispatched += 1;
let mut changed = false;
let mut appended = 0;
match action {
ShellAction::MoveFocus(direction) => {
if let Some(next) = focus_neighbor(self.selected_pane, direction) {
self.selected_pane = next;
self.counters.focus_moves += 1;
changed = true;
} else {
self.counters.blocked_focus_moves += 1;
}
}
ShellAction::SelectPane(pane) => {
if self.selected_pane != pane {
self.selected_pane = pane;
self.counters.direct_selections += 1;
changed = true;
}
}
ShellAction::SetNeedsInput { pane, needs_input } => {
let pane = &mut self.panes[pane.index()];
if needs_input && !pane.needs_input() {
pane.status_before_input = Some(pane.status);
pane.status = PaneStatus::NeedsInput;
self.counters.needs_input_changes += 1;
changed = true;
} else if !needs_input && pane.needs_input() {
pane.status = pane.status_before_input.take().unwrap_or(PaneStatus::Ready);
self.counters.needs_input_changes += 1;
changed = true;
}
}
ShellAction::OpenCommandPalette => {
if !self.command_palette.open {
self.command_palette.open = true;
self.command_palette.query.clear();
self.counters.palette_changes += 1;
changed = true;
}
}
ShellAction::CloseCommandPalette => {
if self.command_palette.open {
self.command_palette.open = false;
self.command_palette.query.clear();
self.counters.palette_changes += 1;
changed = true;
}
}
ShellAction::SetCommandPaletteQuery(query) => {
if self.command_palette.open && self.command_palette.query != query {
self.command_palette.query = query;
self.counters.palette_changes += 1;
changed = true;
}
}
ShellAction::SyntheticStreamTick => {
self.synthetic_tick += 1;
self.counters.synthetic_ticks += 1;
for pane in &mut self.panes {
pane.synthetic_line_sequence += 1;
if pane.kind() != SurfaceKind::Terminal {
continue;
}
pane.lines.push(format!(
"[tick {:06}] {} · synthetic line {:06}",
self.synthetic_tick,
pane.id(),
pane.synthetic_line_sequence
));
if pane.lines.len() > self.terminal_line_limit {
pane.lines.remove(0);
}
appended += 1;
}
self.counters.surface_updates += self.panes.len() as u64;
self.counters.terminal_lines_appended += appended as u64;
changed = true;
}
}
if changed {
self.revision += 1;
self.counters.state_changes += 1;
}
ActionOutcome {
event_sequence: self.counters.events_dispatched,
revision: self.revision,
changed,
selected_pane: self.selected_pane,
terminal_lines_appended: appended,
}
}
}
#[must_use]
pub const fn focus_neighbor(pane: PaneId, direction: FocusDirection) -> Option<PaneId> {
let row = pane.row();
let column = pane.column();
let next = match direction {
FocusDirection::Left if column > 0 => Some(pane.index() - 1),
FocusDirection::Right if column + 1 < GRID_COLUMNS => Some(pane.index() + 1),
FocusDirection::Up if row > 0 => Some(pane.index() - GRID_COLUMNS),
FocusDirection::Down if row + 1 < GRID_ROWS => Some(pane.index() + GRID_COLUMNS),
_ => None,
};
match next {
Some(index) => Some(PaneId::ALL[index]),
None => None,
}
}
#[cfg(test)]
mod tests {
use super::{PANES, SurfaceKind};
use super::{
FocusDirection, GRID_COLUMNS, GRID_ROWS, PANES, PaneId, PaneStatus, ShellAction,
ShellModel, SurfaceKind, focus_neighbor,
};
#[test]
fn comparison_fixture_has_six_mixed_surfaces() {
assert_eq!(PANES.len(), 6);
fn fixture_has_stable_ids_and_mixed_surfaces() {
assert_eq!(PANES.len(), GRID_ROWS * GRID_COLUMNS);
assert_eq!(PANES.map(|pane| pane.id), PaneId::ALL);
assert_eq!(PaneId::CodexRuntime.stable_name(), "codex-runtime");
assert_eq!(PaneId::RuntimeReview.to_string(), "runtime-review");
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)
.filter(|p| p.kind == SurfaceKind::Terminal)
.count(),
3
);
}
#[test]
fn initial_state_has_one_explicit_needs_input_pane() {
let model = ShellModel::default();
assert_eq!(model.selected_pane(), PaneId::CodexRuntime);
assert_eq!(model.revision(), 0);
assert!(!model.command_palette().is_open());
assert_eq!(
model
.panes()
.iter()
.filter(|pane| pane.needs_input())
.map(|pane| pane.id())
.collect::<Vec<_>>(),
vec![PaneId::ClaudeUi]
);
}
#[test]
fn neighbors_match_two_by_three_grid_without_wrapping() {
assert_eq!(
focus_neighbor(PaneId::CodexRuntime, FocusDirection::Right),
Some(PaneId::ClaudeUi)
);
assert_eq!(
focus_neighbor(PaneId::ClaudeUi, FocusDirection::Down),
Some(PaneId::AcpPreview)
);
assert_eq!(
focus_neighbor(PaneId::RuntimeReview, FocusDirection::Up),
Some(PaneId::PiDocs)
);
assert_eq!(
focus_neighbor(PaneId::Architecture, FocusDirection::Left),
None
);
assert_eq!(focus_neighbor(PaneId::PiDocs, FocusDirection::Right), None);
assert_eq!(
focus_neighbor(PaneId::CodexRuntime, FocusDirection::Up),
None
);
}
#[test]
fn focus_updates_selection_revision_and_blocked_counter() {
let mut model = ShellModel::default();
let blocked = model.dispatch(ShellAction::MoveFocus(FocusDirection::Left));
assert!(!blocked.changed);
assert_eq!((blocked.event_sequence, blocked.revision), (1, 0));
let moved = model.dispatch(ShellAction::MoveFocus(FocusDirection::Right));
assert!(moved.changed);
assert_eq!(moved.selected_pane, PaneId::ClaudeUi);
assert_eq!(moved.revision, 1);
model.dispatch(ShellAction::MoveFocus(FocusDirection::Down));
assert_eq!(model.selected_pane(), PaneId::AcpPreview);
assert_eq!(model.counters().focus_moves, 2);
assert_eq!(model.counters().blocked_focus_moves, 1);
}
#[test]
fn direct_selection_is_idempotent_and_counted_separately() {
let mut model = ShellModel::default();
assert!(
!model
.dispatch(ShellAction::SelectPane(PaneId::CodexRuntime))
.changed
);
assert!(
model
.dispatch(ShellAction::SelectPane(PaneId::RuntimeReview))
.changed
);
assert_eq!(model.selected_pane(), PaneId::RuntimeReview);
assert_eq!(model.counters().direct_selections, 1);
assert_eq!(model.counters().events_dispatched, 2);
assert_eq!(model.counters().state_changes, 1);
}
#[test]
fn needs_input_restores_previous_status() {
let mut model = ShellModel::default();
model.dispatch(ShellAction::SetNeedsInput {
pane: PaneId::PiDocs,
needs_input: true,
});
assert_eq!(model.pane(PaneId::PiDocs).status(), PaneStatus::NeedsInput);
assert!(
!model
.dispatch(ShellAction::SetNeedsInput {
pane: PaneId::PiDocs,
needs_input: true,
})
.changed
);
model.dispatch(ShellAction::SetNeedsInput {
pane: PaneId::PiDocs,
needs_input: false,
});
assert_eq!(model.pane(PaneId::PiDocs).status(), PaneStatus::Streaming);
model.dispatch(ShellAction::SetNeedsInput {
pane: PaneId::ClaudeUi,
needs_input: false,
});
assert_eq!(model.pane(PaneId::ClaudeUi).status(), PaneStatus::Working);
assert_eq!(model.counters().needs_input_changes, 3);
}
#[test]
fn command_palette_has_explicit_lifecycle() {
let mut model = ShellModel::default();
assert!(
!model
.dispatch(ShellAction::SetCommandPaletteQuery("pane".into()))
.changed
);
model.dispatch(ShellAction::OpenCommandPalette);
model.dispatch(ShellAction::SetCommandPaletteQuery("pane: next".into()));
assert!(model.command_palette().is_open());
assert_eq!(model.command_palette().query(), "pane: next");
model.dispatch(ShellAction::CloseCommandPalette);
assert!(!model.command_palette().is_open());
assert_eq!(model.command_palette().query(), "");
assert_eq!(model.counters().palette_changes, 3);
}
#[test]
fn ticks_append_predictable_lines_to_terminals_only() {
let mut model = ShellModel::default();
let markdown = model.pane(PaneId::Architecture).lines().to_vec();
let outcome = model.dispatch(ShellAction::SyntheticStreamTick);
assert_eq!(outcome.terminal_lines_appended, 3);
assert_eq!(model.synthetic_tick(), 1);
assert_eq!(
model.pane(PaneId::CodexRuntime).lines().last().unwrap(),
"[tick 000001] codex-runtime · synthetic line 000001"
);
assert_eq!(model.pane(PaneId::Architecture).lines(), markdown);
assert!(
model
.panes()
.iter()
.all(|pane| pane.synthetic_line_sequence() == 1)
);
assert_eq!(model.counters().synthetic_ticks, 1);
assert_eq!(model.counters().surface_updates, 6);
assert_eq!(model.counters().terminal_lines_appended, 3);
}
#[test]
fn terminal_scrollback_is_bounded_below_fixture_size() {
let mut model = ShellModel::with_terminal_line_limit(2).unwrap();
assert_eq!(model.pane(PaneId::CodexRuntime).lines().len(), 2);
for _ in 0..10 {
model.dispatch(ShellAction::SyntheticStreamTick);
}
for pane in model
.panes()
.iter()
.filter(|pane| pane.kind() == SurfaceKind::Terminal)
{
assert_eq!(pane.lines().len(), 2);
assert_eq!(pane.synthetic_line_sequence(), 10);
assert!(pane.lines()[0].starts_with("[tick 000009]"));
assert!(pane.lines()[1].starts_with("[tick 000010]"));
}
assert!(ShellModel::with_terminal_line_limit(0).is_err());
}
#[test]
fn identical_action_replays_produce_identical_models_and_counters() {
let actions = [
ShellAction::MoveFocus(FocusDirection::Right),
ShellAction::MoveFocus(FocusDirection::Down),
ShellAction::OpenCommandPalette,
ShellAction::SetCommandPaletteQuery("workspace".into()),
ShellAction::CloseCommandPalette,
ShellAction::SetNeedsInput {
pane: PaneId::CodexRuntime,
needs_input: true,
},
ShellAction::SyntheticStreamTick,
ShellAction::SyntheticStreamTick,
];
let mut gpui = ShellModel::default();
let mut floem = ShellModel::default();
let gpui_outcomes = actions
.iter()
.cloned()
.map(|action| gpui.dispatch(action))
.collect::<Vec<_>>();
let floem_outcomes = actions
.iter()
.cloned()
.map(|action| floem.dispatch(action))
.collect::<Vec<_>>();
assert_eq!(gpui_outcomes, floem_outcomes);
assert_eq!(gpui, floem);
assert_eq!(gpui.counters().events_dispatched, actions.len() as u64);
assert_eq!(gpui.counters().surface_updates, 12);
assert_eq!(gpui.counters().terminal_lines_appended, 6);
}
}