This commit is contained in:
+2
-1
@@ -1,4 +1,6 @@
|
||||
/target/
|
||||
/spikes/target/
|
||||
/spikes/*/target/
|
||||
/.idea/
|
||||
/.zed/
|
||||
/.vscode/
|
||||
@@ -11,4 +13,3 @@
|
||||
# Upstream research checkouts live beside this repository, never inside it.
|
||||
/Research/
|
||||
/research-checkouts/
|
||||
|
||||
|
||||
Generated
+1438
File diff suppressed because it is too large
Load Diff
+8
-3
@@ -1,11 +1,17 @@
|
||||
[workspace]
|
||||
members = ["apps/lumbridge", "crates/lumbridge-core"]
|
||||
members = [
|
||||
"apps/lumbridge",
|
||||
"crates/lumbridge-buzz",
|
||||
"crates/lumbridge-core",
|
||||
"crates/lumbridge-storage",
|
||||
]
|
||||
exclude = ["spikes"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.0.1"
|
||||
edition = "2024"
|
||||
rust-version = "1.85"
|
||||
rust-version = "1.94"
|
||||
license = "Apache-2.0"
|
||||
repository = "https://git.karti.ai/lumbridge-public/lumbridge-code"
|
||||
|
||||
@@ -15,4 +21,3 @@ unsafe_code = "forbid"
|
||||
[workspace.lints.clippy]
|
||||
all = "warn"
|
||||
pedantic = "warn"
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ contracts before committing to a large implementation.
|
||||
profiles for OpenAI, Anthropic, Gemini, Groq, Cerebras, DeepSeek, and more.
|
||||
- Usage history, burn rate, reset windows, and forecasts with visible data
|
||||
provenance instead of invented precision.
|
||||
- Optional first-class Buzz channels, messages, agents, and confirmed redacted
|
||||
pane sharing without making Buzz a requirement.
|
||||
|
||||
Start with [the product spec](docs/PRODUCT_SPEC.md),
|
||||
[architecture](docs/ARCHITECTURE.md), [research map](docs/RESEARCH.md), and
|
||||
@@ -51,3 +53,10 @@ cargo run -p lumbridge
|
||||
|
||||
See [the testing strategy](docs/TESTING.md) for fake harnesses, ACP replay,
|
||||
terminal conformance, UI driving, recovery, performance, and packaging tests.
|
||||
|
||||
The accepted local/remote boundary is recorded in
|
||||
[decision 0003](docs/decisions/0003-local-data-and-remote-sessions.md). The two
|
||||
native shell candidates live in [`spikes/`](spikes/), with results tracked in
|
||||
[the UI scorecard](docs/UI_SPIKE_SCORECARD.md).
|
||||
The signed-protocol and pane-sharing boundary is in
|
||||
[the Buzz integration design](docs/BUZZ_INTEGRATION.md).
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "lumbridge-buzz"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
buzz-sdk = { git = "https://github.com/block/buzz.git", rev = "cb3144999bebc4939cb15b2200b373281d493b52" }
|
||||
nostr = "0.44"
|
||||
thiserror = "2"
|
||||
uuid = "1"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,209 @@
|
||||
//! Credential-free Buzz protocol preparation for Lumbridge.
|
||||
//!
|
||||
//! This crate validates the user's pane-sharing decision and delegates event
|
||||
//! construction to the upstream Buzz SDK. Signing, uploading, and publishing
|
||||
//! belong to platform adapters that can resolve an OS credential-store handle.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use nostr::EventBuilder;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// A locally configured Buzz identity without private key material.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct BuzzAccount {
|
||||
/// Lumbridge-local stable identifier.
|
||||
pub id: String,
|
||||
/// Human-readable account label.
|
||||
pub label: String,
|
||||
/// Buzz relay/server URL.
|
||||
pub relay_url: String,
|
||||
/// Public Nostr identity in hexadecimal form.
|
||||
pub public_key: String,
|
||||
/// Opaque reference resolved by Keychain or Secret Service.
|
||||
pub secret_store_handle: String,
|
||||
}
|
||||
|
||||
/// Metadata produced after an approved screenshot has been uploaded.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct UploadedPaneImage {
|
||||
/// Publicly retrievable attachment URL returned by Buzz storage.
|
||||
pub url: String,
|
||||
/// MIME type, normally `image/png` or `image/webp`.
|
||||
pub media_type: String,
|
||||
/// Lower-case SHA-256 digest of the uploaded bytes.
|
||||
pub sha256: String,
|
||||
/// Exact byte length of the upload.
|
||||
pub size_bytes: u64,
|
||||
/// Optional pixel dimensions.
|
||||
pub dimensions: Option<(u32, u32)>,
|
||||
}
|
||||
|
||||
impl UploadedPaneImage {
|
||||
fn imeta(&self) -> Vec<String> {
|
||||
let mut tag = vec![
|
||||
"imeta".to_owned(),
|
||||
format!("url {}", self.url),
|
||||
format!("m {}", self.media_type),
|
||||
format!("x {}", self.sha256),
|
||||
format!("size {}", self.size_bytes),
|
||||
];
|
||||
if let Some((width, height)) = self.dimensions {
|
||||
tag.push(format!("dim {width}x{height}"));
|
||||
}
|
||||
tag
|
||||
}
|
||||
}
|
||||
|
||||
/// The explicit human approval associated with a pane share.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ShareApproval {
|
||||
/// Destination selected in the preview UI.
|
||||
pub channel_id: Uuid,
|
||||
/// Final caption visible in the preview UI.
|
||||
pub caption: String,
|
||||
/// Whether redaction has run for this exact captured image.
|
||||
pub redaction_reviewed: bool,
|
||||
/// Whether the user confirmed the exact image, caption, and destination.
|
||||
pub user_confirmed: bool,
|
||||
}
|
||||
|
||||
/// Errors that prevent preparation of a pane share.
|
||||
#[derive(Debug)]
|
||||
pub enum ShareError {
|
||||
/// Redaction review is mandatory before sharing a pane.
|
||||
RedactionNotReviewed,
|
||||
/// A user must confirm the final preview and destination.
|
||||
NotConfirmed,
|
||||
/// The uploaded attachment metadata is malformed.
|
||||
InvalidAttachment(&'static str),
|
||||
/// Buzz rejected the message fields.
|
||||
Buzz(buzz_sdk::SdkError),
|
||||
}
|
||||
|
||||
impl fmt::Display for ShareError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::RedactionNotReviewed => formatter.write_str("pane redaction was not reviewed"),
|
||||
Self::NotConfirmed => formatter.write_str("pane share was not confirmed"),
|
||||
Self::InvalidAttachment(reason) => {
|
||||
write!(formatter, "invalid pane attachment: {reason}")
|
||||
}
|
||||
Self::Buzz(error) => write!(formatter, "Buzz rejected pane share: {error}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ShareError {}
|
||||
|
||||
impl From<buzz_sdk::SdkError> for ShareError {
|
||||
fn from(error: buzz_sdk::SdkError) -> Self {
|
||||
Self::Buzz(error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Build an unsigned Buzz message only after the share safety gate passes.
|
||||
///
|
||||
/// The returned builder must be signed by an identity obtained from the OS
|
||||
/// credential store and then published by the Buzz transport adapter.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error when review/confirmation is missing, attachment metadata is
|
||||
/// unsafe, or the Buzz SDK rejects the event.
|
||||
pub fn prepare_pane_share(
|
||||
approval: &ShareApproval,
|
||||
image: &UploadedPaneImage,
|
||||
) -> Result<EventBuilder, ShareError> {
|
||||
if !approval.redaction_reviewed {
|
||||
return Err(ShareError::RedactionNotReviewed);
|
||||
}
|
||||
if !approval.user_confirmed {
|
||||
return Err(ShareError::NotConfirmed);
|
||||
}
|
||||
if !matches!(image.media_type.as_str(), "image/png" | "image/webp") {
|
||||
return Err(ShareError::InvalidAttachment("unsupported image type"));
|
||||
}
|
||||
if image.sha256.len() != 64 || !image.sha256.bytes().all(|byte| byte.is_ascii_hexdigit()) {
|
||||
return Err(ShareError::InvalidAttachment(
|
||||
"SHA-256 must be 64 hexadecimal characters",
|
||||
));
|
||||
}
|
||||
if image.size_bytes == 0 {
|
||||
return Err(ShareError::InvalidAttachment("image is empty"));
|
||||
}
|
||||
|
||||
let content = if approval.caption.trim().is_empty() {
|
||||
format!("", image.url)
|
||||
} else {
|
||||
format!(
|
||||
"{}\n\n",
|
||||
approval.caption.trim(),
|
||||
image.url
|
||||
)
|
||||
};
|
||||
buzz_sdk::build_message(
|
||||
approval.channel_id,
|
||||
&content,
|
||||
None,
|
||||
&[],
|
||||
false,
|
||||
&[image.imeta()],
|
||||
)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use nostr::{Keys, Kind};
|
||||
|
||||
use super::{ShareApproval, ShareError, UploadedPaneImage, prepare_pane_share};
|
||||
|
||||
fn image() -> UploadedPaneImage {
|
||||
UploadedPaneImage {
|
||||
url: "https://buzz.example/upload/pane.png".into(),
|
||||
media_type: "image/png".into(),
|
||||
sha256: "a".repeat(64),
|
||||
size_bytes: 42_000,
|
||||
dimensions: Some((1_720, 1_400)),
|
||||
}
|
||||
}
|
||||
|
||||
fn approval() -> ShareApproval {
|
||||
ShareApproval {
|
||||
channel_id: uuid::uuid!("5088492f-f83b-41e1-b2cd-95cc741b4521"),
|
||||
caption: "Lumbridge GPUI spike".into(),
|
||||
redaction_reviewed: true,
|
||||
user_confirmed: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requires_review_and_confirmation() {
|
||||
let mut decision = approval();
|
||||
decision.redaction_reviewed = false;
|
||||
assert!(matches!(
|
||||
prepare_pane_share(&decision, &image()),
|
||||
Err(ShareError::RedactionNotReviewed)
|
||||
));
|
||||
|
||||
decision.redaction_reviewed = true;
|
||||
decision.user_confirmed = false;
|
||||
assert!(matches!(
|
||||
prepare_pane_share(&decision, &image()),
|
||||
Err(ShareError::NotConfirmed)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_a_signable_buzz_stream_message() {
|
||||
let event = prepare_pane_share(&approval(), &image())
|
||||
.expect("approved share should build")
|
||||
.sign_with_keys(&Keys::generate())
|
||||
.expect("test identity should sign");
|
||||
|
||||
assert_eq!(event.kind, Kind::Custom(9));
|
||||
assert!(event.content.contains("Lumbridge GPUI spike"));
|
||||
assert!(event.content.contains("pane.png"));
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,56 @@
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// The transport Lumbridge uses to reach a user-owned remote machine.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum RemoteTransport {
|
||||
/// The system OpenSSH client, including the user's SSH config, `ProxyJump`,
|
||||
/// agent, and a Tailscale `MagicDNS` name or tailnet IP when supplied.
|
||||
OpenSsh,
|
||||
/// The Tailscale CLI's SSH proxy and host-key verification path.
|
||||
TailscaleSsh,
|
||||
}
|
||||
|
||||
impl RemoteTransport {
|
||||
#[must_use]
|
||||
pub const fn storage_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::OpenSsh => "openssh",
|
||||
Self::TailscaleSsh => "tailscale-ssh",
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn from_storage_name(value: &str) -> Option<Self> {
|
||||
match value {
|
||||
"openssh" => Some(Self::OpenSsh),
|
||||
"tailscale-ssh" => Some(Self::TailscaleSsh),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A connection profile. It contains routing metadata, never credentials.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct RemoteHost {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
pub hostname: String,
|
||||
pub username: Option<String>,
|
||||
pub port: Option<u16>,
|
||||
pub transport: RemoteTransport,
|
||||
}
|
||||
|
||||
/// Where a workspace's processes and PTYs are owned.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum ExecutionTarget {
|
||||
Local,
|
||||
Remote {
|
||||
host_id: String,
|
||||
remote_path: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Platform {
|
||||
MacOs,
|
||||
@@ -64,7 +114,7 @@ impl ProductStatus {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Platform, ProductStatus, UsageProvenance};
|
||||
use super::{Platform, ProductStatus, RemoteTransport, UsageProvenance};
|
||||
|
||||
#[test]
|
||||
fn scaffold_status_is_explicit() {
|
||||
@@ -76,4 +126,14 @@ mod tests {
|
||||
fn usage_can_be_explicitly_unavailable() {
|
||||
assert_eq!(UsageProvenance::Unavailable, UsageProvenance::Unavailable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_transport_storage_names_are_stable() {
|
||||
for transport in [RemoteTransport::OpenSsh, RemoteTransport::TailscaleSsh] {
|
||||
assert_eq!(
|
||||
RemoteTransport::from_storage_name(transport.storage_name()),
|
||||
Some(transport)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "lumbridge-storage"
|
||||
description = "Local SQLite persistence for Lumbridge"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
lumbridge-core = { path = "../lumbridge-core" }
|
||||
rusqlite = { version = "0.40.2", default-features = false, features = ["bundled"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,284 @@
|
||||
//! Local, device-owned persistence for Lumbridge.
|
||||
//!
|
||||
//! This database stores workspace metadata and remote routing profiles. API
|
||||
//! keys, subscription credentials, SSH private keys, and Tailscale credentials
|
||||
//! are intentionally outside its contract.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use lumbridge_core::{RemoteHost, RemoteTransport};
|
||||
use rusqlite::{Connection, OptionalExtension, params};
|
||||
|
||||
const SCHEMA: &str = r"
|
||||
CREATE TABLE IF NOT EXISTS app_meta (
|
||||
key TEXT PRIMARY KEY NOT NULL,
|
||||
value TEXT NOT NULL
|
||||
) STRICT;
|
||||
|
||||
INSERT INTO app_meta (key, value) VALUES ('schema_version', '2')
|
||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS remote_hosts (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
hostname TEXT NOT NULL,
|
||||
username TEXT,
|
||||
port INTEGER CHECK (port IS NULL OR port BETWEEN 1 AND 65535),
|
||||
transport TEXT NOT NULL CHECK (transport IN ('openssh', 'tailscale-ssh')),
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workspaces (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
repository_path TEXT,
|
||||
remote_host_id TEXT REFERENCES remote_hosts(id) ON DELETE RESTRICT,
|
||||
remote_path TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHECK (
|
||||
(remote_host_id IS NULL AND remote_path IS NULL)
|
||||
OR (remote_host_id IS NOT NULL AND remote_path IS NOT NULL)
|
||||
)
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS panes (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
kind TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
position INTEGER NOT NULL,
|
||||
remote_session_id TEXT,
|
||||
state_json TEXT NOT NULL DEFAULT '{}',
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) STRICT;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS panes_workspace_position
|
||||
ON panes(workspace_id, position);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS buzz_accounts (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
relay_url TEXT NOT NULL,
|
||||
public_key TEXT NOT NULL,
|
||||
secret_store_handle TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workspace_buzz_channels (
|
||||
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
account_id TEXT NOT NULL REFERENCES buzz_accounts(id) ON DELETE CASCADE,
|
||||
channel_id TEXT NOT NULL,
|
||||
channel_name TEXT NOT NULL,
|
||||
PRIMARY KEY (workspace_id, account_id, channel_id)
|
||||
) STRICT;
|
||||
";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum StorageError {
|
||||
Database(rusqlite::Error),
|
||||
UnknownTransport(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for StorageError {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Database(error) => write!(formatter, "SQLite error: {error}"),
|
||||
Self::UnknownTransport(value) => write!(formatter, "unknown remote transport: {value}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for StorageError {}
|
||||
|
||||
impl From<rusqlite::Error> for StorageError {
|
||||
fn from(error: rusqlite::Error) -> Self {
|
||||
Self::Database(error)
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, StorageError>;
|
||||
|
||||
/// The one `SQLite` connection owned by a runtime instance.
|
||||
pub struct Store {
|
||||
connection: Connection,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
/// Open or create a local database and apply forward-compatible migrations.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error when the file cannot be opened or its schema cannot be
|
||||
/// initialized.
|
||||
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
|
||||
Self::from_connection(Connection::open(path)?)
|
||||
}
|
||||
|
||||
/// Create an isolated store for tests and short-lived tools.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error when `SQLite` cannot initialize the in-memory database.
|
||||
pub fn open_in_memory() -> Result<Self> {
|
||||
Self::from_connection(Connection::open_in_memory()?)
|
||||
}
|
||||
|
||||
fn from_connection(connection: Connection) -> Result<Self> {
|
||||
connection.execute_batch("PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;")?;
|
||||
let mode: String =
|
||||
connection.query_row("PRAGMA journal_mode = WAL", [], |row| row.get(0))?;
|
||||
if mode == "wal" {
|
||||
connection.execute_batch("PRAGMA synchronous = NORMAL;")?;
|
||||
}
|
||||
connection.execute_batch(SCHEMA)?;
|
||||
Ok(Self { connection })
|
||||
}
|
||||
|
||||
/// Return the currently installed schema version.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the metadata table cannot be read.
|
||||
pub fn schema_version(&self) -> Result<u32> {
|
||||
let raw: String = self.connection.query_row(
|
||||
"SELECT value FROM app_meta WHERE key = 'schema_version'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
raw.parse()
|
||||
.map_err(|_| StorageError::Database(rusqlite::Error::InvalidQuery))
|
||||
}
|
||||
|
||||
/// Insert or update a credential-free remote host profile.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if `SQLite` rejects the profile.
|
||||
pub fn save_remote_host(&self, host: &RemoteHost) -> Result<()> {
|
||||
self.connection.execute(
|
||||
r"
|
||||
INSERT INTO remote_hosts (id, label, hostname, username, port, transport)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
label = excluded.label,
|
||||
hostname = excluded.hostname,
|
||||
username = excluded.username,
|
||||
port = excluded.port,
|
||||
transport = excluded.transport,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
",
|
||||
params![
|
||||
host.id,
|
||||
host.label,
|
||||
host.hostname,
|
||||
host.username,
|
||||
host.port,
|
||||
host.transport.storage_name()
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load one remote host profile by stable identifier.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error when the row is malformed or cannot be read.
|
||||
pub fn remote_host(&self, id: &str) -> Result<Option<RemoteHost>> {
|
||||
let row = self
|
||||
.connection
|
||||
.query_row(
|
||||
"SELECT id, label, hostname, username, port, transport FROM remote_hosts WHERE id = ?1",
|
||||
[id],
|
||||
|row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, String>(2)?,
|
||||
row.get::<_, Option<String>>(3)?,
|
||||
row.get::<_, Option<u16>>(4)?,
|
||||
row.get::<_, String>(5)?,
|
||||
))
|
||||
},
|
||||
)
|
||||
.optional()?;
|
||||
|
||||
row.map(|(id, label, hostname, username, port, transport)| {
|
||||
let transport = RemoteTransport::from_storage_name(&transport)
|
||||
.ok_or_else(|| StorageError::UnknownTransport(transport.clone()))?;
|
||||
Ok(RemoteHost {
|
||||
id,
|
||||
label,
|
||||
hostname,
|
||||
username,
|
||||
port,
|
||||
transport,
|
||||
})
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn contains_column(&self, table: &str, column: &str) -> Result<bool> {
|
||||
let mut statement = self
|
||||
.connection
|
||||
.prepare(&format!("PRAGMA table_info({table})"))?;
|
||||
let names = statement.query_map([], |row| row.get::<_, String>(1))?;
|
||||
for name in names {
|
||||
if name? == column {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use lumbridge_core::{RemoteHost, RemoteTransport};
|
||||
|
||||
use super::Store;
|
||||
|
||||
#[test]
|
||||
fn initializes_schema_and_round_trips_remote_host() {
|
||||
let store = Store::open_in_memory().expect("store should initialize");
|
||||
assert_eq!(store.schema_version().expect("schema version"), 2);
|
||||
|
||||
let host = RemoteHost {
|
||||
id: "metal".into(),
|
||||
label: "metal · Jarvis".into(),
|
||||
hostname: "desk-1-metal-jarvis.tail.example".into(),
|
||||
username: Some("engineer".into()),
|
||||
port: None,
|
||||
transport: RemoteTransport::TailscaleSsh,
|
||||
};
|
||||
store.save_remote_host(&host).expect("host should save");
|
||||
assert_eq!(
|
||||
store.remote_host("metal").expect("host should load"),
|
||||
Some(host)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn credential_columns_do_not_exist() {
|
||||
let store = Store::open_in_memory().expect("store should initialize");
|
||||
for forbidden in ["password", "private_key", "api_key", "auth_token"] {
|
||||
assert!(
|
||||
!store
|
||||
.contains_column("remote_hosts", forbidden)
|
||||
.expect("schema query")
|
||||
);
|
||||
}
|
||||
for forbidden in ["private_key", "secret_key", "nsec", "auth_token"] {
|
||||
assert!(
|
||||
!store
|
||||
.contains_column("buzz_accounts", forbidden)
|
||||
.expect("schema query")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+57
-6
@@ -8,7 +8,8 @@ Lumbridge should be two cooperating Rust processes:
|
||||
native desktop UI
|
||||
| local authenticated IPC
|
||||
Lumbridge session runtime
|
||||
|-- PTYs and process trees
|
||||
|-- local PTYs and process trees
|
||||
|-- SSH/Tailscale transport to remote Lumbridge runtimes
|
||||
|-- ACP clients and adapters
|
||||
|-- repositories and worktrees
|
||||
|-- usage/event ledger
|
||||
@@ -30,13 +31,18 @@ messages should be real from the beginning.
|
||||
- `lumbridge-harness`: manifests, launch profiles, hooks, PTY fallback adapters.
|
||||
- `lumbridge-provider`: BYOK providers and provider-neutral usage records.
|
||||
- `lumbridge-storage`: SQLite migrations, event log, snapshots, retention.
|
||||
- `lumbridge-remote`: OpenSSH/Tailscale command construction, framed stdio,
|
||||
handshake, reconnect, heartbeat, and remote-runtime discovery.
|
||||
- `lumbridge-secrets`: Keychain/libsecret adapters and redaction.
|
||||
- `lumbridge-git`: repositories, worktrees, diffs, status, conflict state.
|
||||
- `lumbridge-buzz`: credential-free Buzz event/broker preparation and pane-share
|
||||
safety gates; platform adapters own signing and transport.
|
||||
- `lumbridge-ui`: native desktop state and rendering.
|
||||
- `lumbridge`: installable application entry point.
|
||||
|
||||
Only `lumbridge-core` and the entry point exist in the scaffold. New crates are
|
||||
added after their architecture spike passes.
|
||||
The scaffold currently contains `lumbridge-core`, `lumbridge-storage`,
|
||||
`lumbridge-buzz`, and the entry point. Larger runtime crates are added after
|
||||
their architecture spikes pass.
|
||||
|
||||
## Terminal path
|
||||
|
||||
@@ -57,13 +63,46 @@ Do not lock the project to a webview or to Zed's private implementation details
|
||||
before a measured spike. Compare:
|
||||
|
||||
1. GPUI for a Zed-like native model and excellent text-heavy interaction.
|
||||
2. Iced/wgpu plus a dedicated terminal renderer for stable Rust portability.
|
||||
3. Tauri only as a delivery-speed baseline, not the assumed winner.
|
||||
2. Floem for an independent native Rust model with existing editor primitives.
|
||||
|
||||
The winner must render six busy panes smoothly, keep input latency low, support
|
||||
IME/accessibility, package on macOS and both Linux targets, and avoid a license
|
||||
or upstream-stability trap.
|
||||
|
||||
## Local-first and remote session path
|
||||
|
||||
SQLite is device-local. It stores workspace metadata, pane layouts, event and
|
||||
usage history, remote routing profiles, and small snapshots. It never stores SSH
|
||||
private keys, Tailscale credentials, provider API keys, or subscription tokens.
|
||||
Secrets remain in the OS credential store or in the user's existing SSH agent.
|
||||
|
||||
A remote pane is not a local PTY wrapped around a long-lived `ssh` process. Its
|
||||
durable owner is a per-user `lumbridge-runtime` on the destination:
|
||||
|
||||
```text
|
||||
MacBook Lumbridge UI/runtime
|
||||
|
|
||||
| ssh host lumbridge remote connect --stdio
|
||||
| or: tailscale ssh host lumbridge remote connect --stdio
|
||||
v
|
||||
remote per-user Lumbridge runtime -- Unix socket -- PTYs, agents, worktrees
|
||||
|
|
||||
`-- remote SQLite + chunked scrollback on that machine
|
||||
```
|
||||
|
||||
The SSH child carries a versioned framed protocol over stdio. Normal OpenSSH
|
||||
remains the default because it honors the user's config, agent, host keys,
|
||||
ProxyJump, and Tailscale addresses. `tailscale ssh` is an explicit transport for
|
||||
users who want Tailscale's SSH proxy and host-key path. Lumbridge does not
|
||||
configure a tailnet, weaken ACLs, copy SSH keys, or require a listening TCP port.
|
||||
|
||||
The remote runtime assigns a stable session ID before acknowledging a launch.
|
||||
On network loss the local pane becomes disconnected, the remote PTY continues,
|
||||
and reconnect resumes from the last acknowledged output sequence. A second
|
||||
authorized Lumbridge installation can attach to the same remote session after
|
||||
the remote runtime arbitrates input ownership. Collaborative simultaneous input
|
||||
is not part of the first release.
|
||||
|
||||
## Harness integration
|
||||
|
||||
Each harness is described by a versioned manifest: executable discovery, launch
|
||||
@@ -88,6 +127,14 @@ usually expose token counts but cost still depends on cached tokens, reasoning,
|
||||
tool calls, and current pricing. Adapters normalize facts without erasing their
|
||||
source or uncertainty.
|
||||
|
||||
## Buzz collaboration
|
||||
|
||||
Buzz channel messages, replies, agents, and attachments use the upstream Rust
|
||||
SDK's signed Nostr semantics. SQLite stores a public identity and opaque
|
||||
credential-store handle, never the private identity key. Pane images cross the
|
||||
network only after local capture, redaction preview, explicit destination, and
|
||||
confirmation. See `BUZZ_INTEGRATION.md` for the contract and test plan.
|
||||
|
||||
## Persistence
|
||||
|
||||
SQLite in WAL mode stores metadata, commands/events, normalized usage, and small
|
||||
@@ -95,6 +142,11 @@ snapshots. Large scrollback chunks and binary attachments use content-addressed
|
||||
files. A write-ahead event is committed before an external mutation is reported
|
||||
as accepted. Startup replays incomplete operations and reconciles live children.
|
||||
|
||||
Default data roots are `~/Library/Application Support/ai.karti.lumbridge/` on
|
||||
macOS and `${XDG_DATA_HOME:-~/.local/share}/lumbridge/` on Linux. Backups and
|
||||
exports are explicit; Lumbridge does not synchronize the database through a
|
||||
hidden hosted account.
|
||||
|
||||
## Security
|
||||
|
||||
- macOS secrets: Keychain; Linux secrets: Secret Service/libsecret, with an
|
||||
@@ -113,4 +165,3 @@ Shared contracts cover PTY, process tree, notifications, secret store, paths,
|
||||
autostart, updater, and packaging. macOS uses `forkpty`/process groups and native
|
||||
Keychain. Ubuntu and Omarchy use Unix PTYs, cgroups/systemd scopes when available,
|
||||
and Secret Service. Omarchy is treated as Arch Linux, not as a separate kernel.
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# Buzz integration
|
||||
|
||||
## Product boundary
|
||||
|
||||
Buzz is Lumbridge's first-class collaboration surface, not its control plane.
|
||||
Lumbridge remains useful offline, owns its local workspace state, and never
|
||||
requires a Buzz account to open a terminal or run an agent.
|
||||
|
||||
The initial integration provides:
|
||||
|
||||
- connect an existing Buzz identity using an OS credential-store reference;
|
||||
- bind one or more Buzz channels to a Lumbridge workspace;
|
||||
- read and post channel messages and replies from a collaboration pane;
|
||||
- surface Buzz agents attached to a bound channel without pretending they are
|
||||
local processes;
|
||||
- share an approved pane image to an explicitly selected channel.
|
||||
|
||||
## Protocol shape
|
||||
|
||||
The pinned upstream is Block's Apache-2.0 Buzz repository at commit
|
||||
`cb3144999bebc4939cb15b2200b373281d493b52`. `buzz-sdk` constructs typed Nostr
|
||||
events and broker actions but deliberately owns neither identity keys nor
|
||||
network connections. Lumbridge follows that separation:
|
||||
|
||||
```text
|
||||
Lumbridge collaboration pane
|
||||
| typed local command
|
||||
lumbridge-buzz
|
||||
| Buzz SDK event/broker action
|
||||
identity signer + Buzz transport adapter
|
||||
| signed WebSocket / HTTPS upload
|
||||
Buzz relay and attachment storage
|
||||
```
|
||||
|
||||
The exact revision stays pinned until an audited upgrade. We use Buzz's signed
|
||||
protocol and SDK types rather than a parallel webhook format.
|
||||
|
||||
## Identity and local data
|
||||
|
||||
SQLite stores the account label, relay URL, public key, and an opaque secret
|
||||
store handle. The Nostr private key stays in macOS Keychain or Linux Secret
|
||||
Service. It must never enter SQLite, command arguments, crash reports, pane
|
||||
history, logs, or Git configuration.
|
||||
|
||||
An installed Buzz CLI identity may be imported only through an explicit user
|
||||
flow that transfers it into the OS credential store. Lumbridge must not search
|
||||
the filesystem for keys. Buzz's current NIP-AB device-pairing tooling is marked
|
||||
for interoperability testing upstream, so it is not a production login promise.
|
||||
|
||||
## Pane-image sharing
|
||||
|
||||
Sharing is a user-visible state machine:
|
||||
|
||||
1. Capture the selected pane only, excluding application chrome by default.
|
||||
2. Detect likely secrets and render a local preview with proposed redactions.
|
||||
3. Let the user adjust redactions, caption, and destination channel.
|
||||
4. Require confirmation of that exact preview and destination.
|
||||
5. Encode PNG or WebP, hash it, upload with the authenticated Buzz attachment
|
||||
flow, and receive a canonical URL.
|
||||
6. Build a kind-9 Buzz message with Markdown plus an `imeta` tag, sign it using
|
||||
the OS-held identity, and publish it.
|
||||
7. Persist only the resulting event ID, attachment metadata, destination, and
|
||||
local audit outcome. Never persist the unredacted capture.
|
||||
|
||||
Any change to the pixels, caption, or destination invalidates confirmation. A
|
||||
failed upload or publish is retryable but never reported as sent.
|
||||
|
||||
## Agent integration
|
||||
|
||||
Buzz agents and Lumbridge harnesses are different entities. A Buzz agent may be
|
||||
shown in the collaboration pane and may exchange messages through Buzz's broker
|
||||
contract. A local or remote Lumbridge harness remains owned by the Lumbridge
|
||||
runtime and communicates through ACP or a supervised PTY. Linking the two later
|
||||
requires an explicit capability grant, visible identity, and revocation path.
|
||||
|
||||
## Test plan
|
||||
|
||||
- deterministic unit tests for channel IDs, content limits, media metadata, and
|
||||
the review/confirmation gate;
|
||||
- signature tests with generated fixture identities only;
|
||||
- integration tests against a disposable local Buzz relay and attachment store;
|
||||
- disconnect, duplicate-delivery, cursor-resume, and publish-retry tests;
|
||||
- rendered tests proving destination and redactions remain visible at confirm;
|
||||
- a real-account smoke test only in a manually enabled, non-CI test profile.
|
||||
+29
-2
@@ -21,6 +21,24 @@ The product should feel faster and calmer as concurrency rises.
|
||||
5. Review file and Git changes by workspace or worktree.
|
||||
6. Understand usage, rate-limit windows, burn rate, and likely exhaustion time.
|
||||
7. Resume the entire workspace after the UI or machine restarts.
|
||||
8. Work from a laptop while selected panes and agents run durably on a
|
||||
user-owned machine reached through SSH or Tailscale.
|
||||
9. Follow a project's Buzz channel and intentionally share a redacted pane image
|
||||
without leaving the workspace.
|
||||
|
||||
## Local and remote workspaces
|
||||
|
||||
Every Lumbridge installation is useful on its own and owns its local settings,
|
||||
history, usage observations, and workspace views. A pane can execute locally or
|
||||
on a saved remote host. Remote hosts may be normal SSH destinations, entries in
|
||||
the user's SSH config, Tailscale MagicDNS names, or tailnet IPs.
|
||||
|
||||
For durable remote work, a per-user Lumbridge runtime on the destination owns
|
||||
the PTY and process tree. The desktop app connects to that runtime over an SSH
|
||||
stdio channel and can later reconnect without moving the process to a cloud
|
||||
service. The same remote workspace may be opened from another authorized
|
||||
Lumbridge installation, but local UI state is not silently merged between
|
||||
devices.
|
||||
|
||||
## Initial harnesses
|
||||
|
||||
@@ -64,13 +82,23 @@ harness-reported, locally measured, estimated, or unavailable. Lumbridge must
|
||||
not scrape browser cookies or reverse-engineer private account APIs to manufacture
|
||||
an exact remaining balance. When only local observations exist, the UI says so.
|
||||
|
||||
## Buzz collaboration
|
||||
|
||||
Buzz is optional and deeply integrated: an engineer can connect an existing
|
||||
identity, bind channels to workspaces, read and send messages, see channel
|
||||
agents, and share a pane image. Sharing always previews the exact capture,
|
||||
redactions, caption, and destination before upload. Buzz identity secrets stay
|
||||
in the operating-system credential store.
|
||||
|
||||
## Non-goals for the first release
|
||||
|
||||
- A new foundation-model training or inference service.
|
||||
- A hosted account or mandatory Lumbridge cloud.
|
||||
- Replacing every coding harness with one Lumbridge-owned agent loop.
|
||||
- Windows support.
|
||||
- Mobile control, collaborative cloud sessions, or remote execution.
|
||||
- Mobile control or collaborative hosted sessions.
|
||||
- Arbitrary third-party remote compute provisioning; the first release connects
|
||||
only to machines and SSH/Tailscale access the user already controls.
|
||||
- Bundling third-party subscriptions or reselling model tokens.
|
||||
|
||||
## Experience principles
|
||||
@@ -81,4 +109,3 @@ an exact remaining balance. When only local observations exist, the UI says so.
|
||||
- Approval and security boundaries stay visible.
|
||||
- Local and offline workflows remain useful.
|
||||
- Estimates are useful only when their uncertainty is honest.
|
||||
|
||||
|
||||
+8
-1
@@ -4,9 +4,13 @@
|
||||
|
||||
- Capture upstream snapshots, licenses, relevant modules, and architectural notes.
|
||||
- Benchmark UI candidates with six animated terminal panes.
|
||||
- Prove OpenSSH and `tailscale ssh` stdio transports against a disposable remote
|
||||
runtime, including disconnect and replay.
|
||||
- Spike PTY correctness and process-tree cleanup on macOS, Ubuntu, and Omarchy.
|
||||
- Connect the official ACP Rust SDK to two contrasting harnesses.
|
||||
- Audit actual usage/quota surfaces for every launch provider.
|
||||
- Prove Buzz SDK message, attachment, cursor, reconnect, and duplicate-delivery
|
||||
behavior against a disposable local relay.
|
||||
- Decide terminal core, UI stack, IPC transport, and schema evolution policy.
|
||||
|
||||
Exit: recorded decisions with working spikes and measured results.
|
||||
@@ -14,8 +18,12 @@ Exit: recorded decisions with working spikes and measured results.
|
||||
## Phase 1 — terminal workspace alpha
|
||||
|
||||
- Durable runtime, local IPC, workspaces, tabs, splits, shells, scrollback.
|
||||
- Saved SSH/Tailscale hosts and durable remote panes through a per-user remote
|
||||
runtime; reconnect from a second Lumbridge installation.
|
||||
- Session restore, crash recovery, command palette, keybindings, notifications.
|
||||
- Repository/worktree creation and basic Git status/diff.
|
||||
- Optional Buzz collaboration pane, workspace channel binding, and confirmed
|
||||
redacted pane-image sharing.
|
||||
- macOS, Ubuntu, and Omarchy development packages.
|
||||
|
||||
Exit: Lumbridge is worth using as a terminal multiplexer without AI features.
|
||||
@@ -44,4 +52,3 @@ Exit: usage displays are useful, auditable, and honest about uncertainty.
|
||||
- Signed/notarized macOS releases, `.deb`, AppImage, and Arch package.
|
||||
- Delta updater with signed manifests and rollback.
|
||||
- Accessibility, performance, soak, migration, and recovery qualification.
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Native UI spike scorecard
|
||||
|
||||
The GPUI and Floem programs under `spikes/` consume the same six-pane fixture.
|
||||
No product framework decision is accepted until both rows contain measurements
|
||||
from macOS and Linux and the hard gates pass.
|
||||
|
||||
| Criterion | Hard gate | GPUI | Floem |
|
||||
|---|---:|---:|---:|
|
||||
| Builds on macOS Apple Silicon | yes | pending | pending |
|
||||
| Builds on Ubuntu | yes | pending | pending |
|
||||
| Builds on Omarchy/Arch | yes | pending | pending |
|
||||
| Dependency/license closure permits Apache-2.0 distribution | yes | pending | pending |
|
||||
| Keyboard navigation + AccessKit tree | yes | pending | pending |
|
||||
| IME and composed Unicode input | yes | pending | pending |
|
||||
| Isolated system browser child | yes | pending | pending |
|
||||
| Cold startup, p50/p95 | record | pending | pending |
|
||||
| Idle RSS / six-stream RSS | record | pending | pending |
|
||||
| Key-to-present p50/p95 | record | pending | pending |
|
||||
| Six-stream frame p95/p99 | record | pending | pending |
|
||||
| Release binary/package size | record | pending | pending |
|
||||
| Native menu/window/clipboard/drag-and-drop | review | pending | pending |
|
||||
| API clarity and maintenance burden | review | pending | pending |
|
||||
|
||||
The current programs establish dependency, build, launch, and static layout
|
||||
baselines. The next iteration adds deterministic streaming, input timestamps,
|
||||
accessibility identifiers, a native Markdown editor, and one Wry browser child.
|
||||
@@ -0,0 +1,25 @@
|
||||
# 0003: Data is local; remote sessions live on the user's remote machine
|
||||
|
||||
Status: accepted for implementation.
|
||||
|
||||
Each Lumbridge installation owns a local SQLite database and content-addressed
|
||||
data directory. There is no mandatory Lumbridge account, hosted database, or
|
||||
silent multi-device synchronization. Credentials remain in platform credential
|
||||
stores, SSH agents, or upstream harnesses rather than SQLite.
|
||||
|
||||
Lumbridge supports local work and user-owned remote machines as equal execution
|
||||
targets. OpenSSH is the default transport and may reach a Tailscale MagicDNS name
|
||||
or tailnet address. An explicit `tailscale ssh` transport is also supported.
|
||||
Lumbridge consumes existing connectivity and authorization; it does not manage
|
||||
tailnet ACLs or private keys.
|
||||
|
||||
Durability belongs where the process runs. A remote per-user Lumbridge runtime
|
||||
owns remote PTYs, agents, worktrees, event history, and scrollback. Desktop apps
|
||||
connect over a framed SSH stdio stream and reconnect by stable session ID. This
|
||||
allows a MacBook to close or change networks without terminating work on metal
|
||||
or amd-server.
|
||||
|
||||
Another authorized Lumbridge installation may discover and attach to that remote
|
||||
workspace. Its device-local window layout and preferences remain local. The
|
||||
remote runtime arbitrates a single input owner in the first release; real-time
|
||||
collaborative editing is a separate future capability.
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_path=${LUMBRIDGE_REPO:-/home/metal/Desktop/Lumbridge Code/lumbridge}
|
||||
window_title='Lumbridge · Bacon'
|
||||
|
||||
if [[ ! -d "$repo_path/.git" ]]; then
|
||||
echo "Lumbridge repository not found at $repo_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export DISPLAY=${DISPLAY:-:0}
|
||||
export XAUTHORITY=${XAUTHORITY:-/run/user/1000/gdm/Xauthority}
|
||||
|
||||
for command_name in gnome-terminal wmctrl; do
|
||||
if ! command -v "$command_name" >/dev/null 2>&1; then
|
||||
echo "required command not found: $command_name" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
existing_id=$(wmctrl -l | awk -v title="$window_title" 'index($0, title) { print $1; exit }')
|
||||
if [[ -z "$existing_id" ]]; then
|
||||
gnome-terminal \
|
||||
--title="$window_title" \
|
||||
-- bash -lc "cd '$repo_path' && source /home/metal/.cargo/env && exec bacon" &
|
||||
|
||||
for _attempt in $(seq 1 50); do
|
||||
existing_id=$(wmctrl -l | awk -v title="$window_title" 'index($0, title) { print $1; exit }')
|
||||
[[ -n "$existing_id" ]] && break
|
||||
sleep 0.1
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -z "$existing_id" ]]; then
|
||||
echo "Bacon terminal did not become visible" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Gigabyte G34WQC is 3440x1440 at X=0. Leave room for GNOME's top bar.
|
||||
wmctrl -i -r "$existing_id" -b remove,maximized_vert,maximized_horz
|
||||
wmctrl -i -r "$existing_id" -e 0,0,0,1720,1400
|
||||
wmctrl -i -a "$existing_id"
|
||||
echo "Bacon is running in window $existing_id on the left half of the Gigabyte display."
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: lumbridge-development
|
||||
description: Build, test, review, benchmark, or document Lumbridge, the native Rust local-first IDE, terminal multiplexer, and agent workspace.
|
||||
---
|
||||
|
||||
# Lumbridge Development
|
||||
|
||||
Work from the Lumbridge repository root. Read `AGENTS.md`, then only the product
|
||||
documents relevant to the task:
|
||||
|
||||
- architecture or persistence: `docs/ARCHITECTURE.md` and `docs/decisions/`;
|
||||
- product behavior: `docs/PRODUCT_SPEC.md`;
|
||||
- tests: `docs/TESTING.md`;
|
||||
- native UI work: `docs/UI_OPTIONS.md` and `docs/UI_SPIKE_SCORECARD.md`;
|
||||
- Buzz work: `docs/BUZZ_INTEGRATION.md`.
|
||||
|
||||
Preserve these product boundaries:
|
||||
|
||||
- The application shell, terminal, editor, runtime, and state model are native
|
||||
Rust. Do not introduce Electron, React, TypeScript, or a webview shell.
|
||||
- Data is local-first. SQLite contains metadata and history, never API keys,
|
||||
subscription tokens, SSH private keys, Buzz private keys, or Tailscale keys.
|
||||
- Remote PTYs live in a per-user runtime on the user's remote machine. OpenSSH
|
||||
and Tailscale are transports over access the user already configured; do not
|
||||
change tailnet ACLs, copy credentials, or open public listeners.
|
||||
- macOS, Ubuntu, and Omarchy/Arch are release targets. A Linux-only success is
|
||||
not cross-platform proof.
|
||||
- Usage and quota values always retain provenance and uncertainty.
|
||||
|
||||
## Development loop
|
||||
|
||||
Before editing, inspect Git status and preserve unrelated work. Use:
|
||||
|
||||
```bash
|
||||
bacon
|
||||
cargo xtest
|
||||
./scripts/ci.sh
|
||||
```
|
||||
|
||||
Root CI must remain independent from experimental UI dependencies. GPUI and
|
||||
Floem live in separate workspaces under `spikes/` and consume the same
|
||||
`ui-shell-model` fixture. Record only observed results in the scorecard; do not
|
||||
select a framework until both pass the hard gates.
|
||||
|
||||
For Buzz integration, use its Apache-2.0 Rust SDK and signed protocol semantics
|
||||
rather than inventing a webhook dialect. Test against a local relay and fixture
|
||||
identity. Pane screenshots or transcripts require a visible preview, redaction,
|
||||
explicit destination, and user confirmation before upload.
|
||||
|
||||
Add tests at the lowest deterministic layer first, then platform or rendered
|
||||
tests where behavior crosses a real boundary. Before handoff, run the relevant
|
||||
spike build plus `./scripts/ci.sh`, `git diff --check`, and report anything not
|
||||
validated on all target systems.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Lumbridge Development"
|
||||
short_description: "Build and validate the Lumbridge native IDE"
|
||||
default_prompt: "Use $lumbridge-development to continue building and validating Lumbridge."
|
||||
@@ -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.
|
||||
Generated
+4680
File diff suppressed because it is too large
Load Diff
@@ -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]
|
||||
@@ -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();
|
||||
}
|
||||
Generated
+7195
File diff suppressed because it is too large
Load Diff
@@ -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]
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
Generated
+7
@@ -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"
|
||||
@@ -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]
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user