This commit is contained in:
Generated
+60
@@ -1616,6 +1616,18 @@ dependencies = [
|
||||
"zune-inflate",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||
|
||||
[[package]]
|
||||
name = "fallible-streaming-iterator"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "1.9.0"
|
||||
@@ -2961,6 +2973,17 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.38.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1d20bef17f513b9b3004532233187769cd072d790971f4e4da0e346eb6401e8"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.15"
|
||||
@@ -3013,6 +3036,13 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "lumbridge-core"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lumbridge-pty"
|
||||
version = "0.0.1"
|
||||
@@ -3037,13 +3067,24 @@ dependencies = [
|
||||
"gpui",
|
||||
"lumbridge-runtime",
|
||||
"lumbridge-spike-model",
|
||||
"lumbridge-storage",
|
||||
"lumbridge-terminal",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lumbridge-spike-model"
|
||||
version = "0.0.1"
|
||||
|
||||
[[package]]
|
||||
name = "lumbridge-storage"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"lumbridge-core",
|
||||
"rusqlite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lumbridge-terminal"
|
||||
version = "0.0.1"
|
||||
@@ -4464,6 +4505,19 @@ version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.40.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"libsqlite3-sys",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.12.0"
|
||||
@@ -6028,6 +6082,12 @@ dependencies = [
|
||||
"sval_serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
|
||||
@@ -11,6 +11,9 @@ publish = false
|
||||
gpui = "0.2.2"
|
||||
lumbridge-runtime = { path = "../../crates/lumbridge-runtime" }
|
||||
lumbridge-spike-model = { path = "../ui-shell-model" }
|
||||
lumbridge-storage = { path = "../../crates/lumbridge-storage" }
|
||||
lumbridge-terminal = { path = "../../crates/lumbridge-terminal" }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
|
||||
[workspace]
|
||||
|
||||
+555
-110
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,379 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use lumbridge_spike_model::PaneId as FixturePaneId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub(crate) struct PanelId(u64);
|
||||
|
||||
impl PanelId {
|
||||
pub(crate) const fn get(self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub(crate) enum PanelKind {
|
||||
Terminal,
|
||||
Browser,
|
||||
Markdown,
|
||||
Review,
|
||||
}
|
||||
|
||||
impl PanelKind {
|
||||
pub(crate) const ALL: [Self; 4] = [Self::Terminal, Self::Browser, Self::Markdown, Self::Review];
|
||||
|
||||
pub(crate) const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Terminal => "Terminal",
|
||||
Self::Browser => "Browser",
|
||||
Self::Markdown => "Markdown",
|
||||
Self::Review => "Review",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn description(self) -> &'static str {
|
||||
match self {
|
||||
Self::Terminal => "A real local shell with its own runtime actor",
|
||||
Self::Browser => "An isolated system-web-engine surface",
|
||||
Self::Markdown => "Local notes, plans, and architecture",
|
||||
Self::Review => "Repository changes and approval boundaries",
|
||||
}
|
||||
}
|
||||
|
||||
const fn default_target(self) -> &'static str {
|
||||
match self {
|
||||
Self::Terminal => "metal · local runtime",
|
||||
Self::Browser => "isolated system web engine",
|
||||
Self::Markdown => "lumbridge-code · local document",
|
||||
Self::Review => "lumbridge-code · working tree",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub(crate) enum SeedPane {
|
||||
CodexRuntime,
|
||||
ClaudeUi,
|
||||
PiDocs,
|
||||
Architecture,
|
||||
AcpPreview,
|
||||
RuntimeReview,
|
||||
}
|
||||
|
||||
impl SeedPane {
|
||||
pub(crate) const fn fixture_id(self) -> FixturePaneId {
|
||||
match self {
|
||||
Self::CodexRuntime => FixturePaneId::CodexRuntime,
|
||||
Self::ClaudeUi => FixturePaneId::ClaudeUi,
|
||||
Self::PiDocs => FixturePaneId::PiDocs,
|
||||
Self::Architecture => FixturePaneId::Architecture,
|
||||
Self::AcpPreview => FixturePaneId::AcpPreview,
|
||||
Self::RuntimeReview => FixturePaneId::RuntimeReview,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub(crate) struct WorkspacePanel {
|
||||
pub(crate) id: PanelId,
|
||||
pub(crate) kind: PanelKind,
|
||||
pub(crate) title: String,
|
||||
pub(crate) target: String,
|
||||
pub(crate) attached: bool,
|
||||
pub(crate) seed: Option<SeedPane>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub(crate) struct PanelRegistry {
|
||||
schema_version: u32,
|
||||
panels: Vec<WorkspacePanel>,
|
||||
selected: PanelId,
|
||||
next_id: u64,
|
||||
}
|
||||
|
||||
impl Default for PanelRegistry {
|
||||
fn default() -> Self {
|
||||
Self::seeded()
|
||||
}
|
||||
}
|
||||
|
||||
impl PanelRegistry {
|
||||
pub(crate) fn seeded() -> Self {
|
||||
let seeds = [
|
||||
(
|
||||
PanelKind::Terminal,
|
||||
"Codex · runtime",
|
||||
"metal · Tailscale SSH",
|
||||
SeedPane::CodexRuntime,
|
||||
),
|
||||
(
|
||||
PanelKind::Terminal,
|
||||
"Claude Code · UI",
|
||||
"MacBook Air · local",
|
||||
SeedPane::ClaudeUi,
|
||||
),
|
||||
(
|
||||
PanelKind::Terminal,
|
||||
"Pi · docs",
|
||||
"amd-server · OpenSSH",
|
||||
SeedPane::PiDocs,
|
||||
),
|
||||
(
|
||||
PanelKind::Markdown,
|
||||
"Architecture.md",
|
||||
"lumbridge-code · worktree",
|
||||
SeedPane::Architecture,
|
||||
),
|
||||
(
|
||||
PanelKind::Browser,
|
||||
"Preview · ACP docs",
|
||||
"isolated system web engine",
|
||||
SeedPane::AcpPreview,
|
||||
),
|
||||
(
|
||||
PanelKind::Review,
|
||||
"Changes · lumbridge-runtime",
|
||||
"metal · worktree remote-runtime",
|
||||
SeedPane::RuntimeReview,
|
||||
),
|
||||
];
|
||||
let panels = seeds
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, (kind, title, target, seed))| WorkspacePanel {
|
||||
id: PanelId(index as u64 + 1),
|
||||
kind,
|
||||
title: title.to_owned(),
|
||||
target: target.to_owned(),
|
||||
attached: index < 5,
|
||||
seed: Some(seed),
|
||||
})
|
||||
.collect();
|
||||
Self {
|
||||
schema_version: 1,
|
||||
panels,
|
||||
selected: PanelId(1),
|
||||
next_id: 7,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_json(json: &str) -> Result<Self, String> {
|
||||
let registry: Self = serde_json::from_str(json).map_err(|error| error.to_string())?;
|
||||
registry.validate()?;
|
||||
Ok(registry)
|
||||
}
|
||||
|
||||
pub(crate) fn to_json(&self) -> Result<String, String> {
|
||||
serde_json::to_string(self).map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
fn validate(&self) -> Result<(), String> {
|
||||
if self.schema_version != 1 {
|
||||
return Err(format!(
|
||||
"unsupported panel snapshot version {}",
|
||||
self.schema_version
|
||||
));
|
||||
}
|
||||
if self.panels.is_empty() || self.attached_count() == 0 {
|
||||
return Err("a workspace must retain one attached panel".to_owned());
|
||||
}
|
||||
let ids = self
|
||||
.panels
|
||||
.iter()
|
||||
.map(|panel| panel.id)
|
||||
.collect::<BTreeSet<_>>();
|
||||
if ids.len() != self.panels.len() || ids.iter().any(|id| id.get() == 0) {
|
||||
return Err("panel IDs must be unique and non-zero".to_owned());
|
||||
}
|
||||
if self
|
||||
.panel(self.selected)
|
||||
.is_none_or(|panel| !panel.attached)
|
||||
{
|
||||
return Err("the selected panel must be attached".to_owned());
|
||||
}
|
||||
let max_id = ids.iter().map(|id| id.get()).max().unwrap_or(0);
|
||||
if self.next_id <= max_id {
|
||||
return Err("the next panel ID must exceed every persisted ID".to_owned());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn panels(&self) -> &[WorkspacePanel] {
|
||||
&self.panels
|
||||
}
|
||||
|
||||
pub(crate) fn panel(&self, id: PanelId) -> Option<&WorkspacePanel> {
|
||||
self.panels.iter().find(|panel| panel.id == id)
|
||||
}
|
||||
|
||||
pub(crate) fn selected(&self) -> PanelId {
|
||||
self.selected
|
||||
}
|
||||
|
||||
pub(crate) fn attached_ids(&self) -> Vec<PanelId> {
|
||||
self.panels
|
||||
.iter()
|
||||
.filter(|panel| panel.attached)
|
||||
.map(|panel| panel.id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn detached_ids(&self) -> Vec<PanelId> {
|
||||
self.panels
|
||||
.iter()
|
||||
.filter(|panel| !panel.attached)
|
||||
.map(|panel| panel.id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn attached_count(&self) -> usize {
|
||||
self.panels.iter().filter(|panel| panel.attached).count()
|
||||
}
|
||||
|
||||
pub(crate) fn select(&mut self, id: PanelId) -> bool {
|
||||
if self.panel(id).is_none_or(|panel| !panel.attached) || self.selected == id {
|
||||
return false;
|
||||
}
|
||||
self.selected = id;
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn select_attached_at(&mut self, index: usize) -> bool {
|
||||
self.attached_ids()
|
||||
.get(index)
|
||||
.copied()
|
||||
.is_some_and(|id| self.select(id))
|
||||
}
|
||||
|
||||
pub(crate) fn move_horizontal(&mut self, delta: isize) -> bool {
|
||||
let attached = self.attached_ids();
|
||||
let Some(position) = attached.iter().position(|id| *id == self.selected) else {
|
||||
return false;
|
||||
};
|
||||
let next = position as isize + delta;
|
||||
if next < 0 || next >= attached.len() as isize {
|
||||
return false;
|
||||
}
|
||||
self.select(attached[next as usize])
|
||||
}
|
||||
|
||||
pub(crate) fn create(&mut self, kind: PanelKind) -> PanelId {
|
||||
let id = PanelId(self.next_id);
|
||||
self.next_id += 1;
|
||||
let sequence = self
|
||||
.panels
|
||||
.iter()
|
||||
.filter(|panel| panel.kind == kind)
|
||||
.count()
|
||||
+ 1;
|
||||
let title = match kind {
|
||||
PanelKind::Terminal => format!("Terminal · {sequence}"),
|
||||
PanelKind::Browser => format!("Browser · {sequence}"),
|
||||
PanelKind::Markdown => format!("Notes · {sequence}"),
|
||||
PanelKind::Review => format!("Review · {sequence}"),
|
||||
};
|
||||
let selected_position = self
|
||||
.panels
|
||||
.iter()
|
||||
.position(|panel| panel.id == self.selected)
|
||||
.expect("selected panel exists");
|
||||
self.panels.insert(
|
||||
selected_position + 1,
|
||||
WorkspacePanel {
|
||||
id,
|
||||
kind,
|
||||
title,
|
||||
target: kind.default_target().to_owned(),
|
||||
attached: true,
|
||||
seed: None,
|
||||
},
|
||||
);
|
||||
self.selected = id;
|
||||
id
|
||||
}
|
||||
|
||||
pub(crate) fn detach(&mut self, id: PanelId) -> bool {
|
||||
if self.attached_count() == 1 || self.panel(id).is_none_or(|panel| !panel.attached) {
|
||||
return false;
|
||||
}
|
||||
let attached_before = self.attached_ids();
|
||||
let detached_position = attached_before
|
||||
.iter()
|
||||
.position(|candidate| *candidate == id)
|
||||
.expect("attached panel appears in attached IDs");
|
||||
self.panels
|
||||
.iter_mut()
|
||||
.find(|panel| panel.id == id)
|
||||
.expect("panel exists")
|
||||
.attached = false;
|
||||
if self.selected == id {
|
||||
let remaining = self.attached_ids();
|
||||
self.selected = remaining[detached_position.min(remaining.len() - 1)];
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn attach(&mut self, id: PanelId) -> bool {
|
||||
let Some(panel) = self.panels.iter_mut().find(|panel| panel.id == id) else {
|
||||
return false;
|
||||
};
|
||||
if panel.attached {
|
||||
return false;
|
||||
}
|
||||
panel.attached = true;
|
||||
self.selected = id;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{PanelId, PanelKind, PanelRegistry};
|
||||
|
||||
#[test]
|
||||
fn dynamic_panels_receive_monotonic_ids_and_insert_beside_selection() {
|
||||
let mut registry = PanelRegistry::seeded();
|
||||
let browser = registry.create(PanelKind::Browser);
|
||||
let terminal = registry.create(PanelKind::Terminal);
|
||||
assert_eq!(browser, PanelId(7));
|
||||
assert_eq!(terminal, PanelId(8));
|
||||
assert_eq!(registry.selected(), terminal);
|
||||
assert_eq!(registry.panels()[2].id, terminal);
|
||||
assert_eq!(registry.panels()[3].id, browser);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identity_and_detached_state_round_trip_without_reuse() {
|
||||
let mut registry = PanelRegistry::seeded();
|
||||
let created = registry.create(PanelKind::Markdown);
|
||||
assert!(registry.detach(created));
|
||||
let json = registry.to_json().unwrap();
|
||||
let mut restored = PanelRegistry::from_json(&json).unwrap();
|
||||
assert!(restored.detached_ids().contains(&created));
|
||||
assert_eq!(restored.create(PanelKind::Review), PanelId(8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detach_preserves_panel_and_reattach_restores_same_identity() {
|
||||
let mut registry = PanelRegistry::seeded();
|
||||
let id = registry.create(PanelKind::Terminal);
|
||||
assert!(registry.detach(id));
|
||||
assert!(registry.panel(id).is_some());
|
||||
assert!(registry.attach(id));
|
||||
assert_eq!(registry.selected(), id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_or_empty_snapshots_are_rejected() {
|
||||
assert!(PanelRegistry::from_json("{}").is_err());
|
||||
assert!(
|
||||
PanelRegistry::from_json(
|
||||
r#"{"schema_version":1,"panels":[],"selected":1,"next_id":2}"#
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user