Stop the shell asserting things that are not true
The footer was rebuilt on a real ledger two commits ago. The rest of the shell was never audited the same way, and a multi-agent pass over it found the same class of defect everywhere else: - A sidebar card reading "Buzz · lumbridgecode / connected · signed identity" in the success colour. lumbridge-buzz is not a dependency of this binary. - A saved host "amd-server", and a WORKTREES section with five entries and a working selector, backed by a lumbridge-git crate that does not exist. - A first-run workspace of six panes announcing "Codex · runtime / metal · Tailscale SSH", "Claude Code · UI / MacBook Air · local" and a Pi pane on a saved host. Every one of them was a /bin/sh, and the machine names were this developer's. - A declared "Pi · spark-1 · laguna-s-2.1" usage profile with no probe of any kind behind it. Declaring a profile promises the gap is real; that one could never be filled. - FOOTER_CENTER = "Codex · ChatGPT subscription · 62% window remaining", rendered by the Floem shell. Decision 0013 names that exact form as the thing that must never be shown. - The header's PTY count painted green unconditionally, so "0/5 LIVE PTYS" read as success. runtime_rows already had the right rule three hundred lines away. - A browser panel describing itself as "An isolated system-web-engine surface" on the chooser screen where you pick it. There is no web engine in this build. First run is now three real local shells, and a pane claims a harness when one has actually been launched into it. The seed mapping stays for when that is possible. Also removes the only unsafe block in the shell: a test set LUMBRIDGE_*_PROBE through the environment, which needs unsafe under edition 2024 and silently disabled both probes for every other test in the binary. Replaced with UsageFeedOptions passed to start_with. Clippy pedantic on the spike goes 79 -> 15 against root CI's -D warnings, so graduating it into the workspace is not gated on a warning cleanup. The four remaining too_many_lines are the render split, which the sidebar work needs to do anyway. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
219c674aea
commit
834b73e831
@@ -12,8 +12,7 @@ use floem::{
|
|||||||
window::WindowConfig,
|
window::WindowConfig,
|
||||||
};
|
};
|
||||||
use lumbridge_spike_model::{
|
use lumbridge_spike_model::{
|
||||||
FOOTER_CENTER, FOOTER_RIGHT, FocusDirection, PaneId, PaneState, ShellAction, ShellModel,
|
FOOTER_FIXTURE_NOTICE, FocusDirection, PaneId, PaneState, ShellAction, ShellModel, SurfaceKind,
|
||||||
SurfaceKind, WORKSPACES,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const BG: Color = Color::from_rgb8(9, 12, 18);
|
const BG: Color = Color::from_rgb8(9, 12, 18);
|
||||||
@@ -132,22 +131,16 @@ fn render_pane_card(pane: PaneState, selected: bool, model: RwSignal<ShellModel>
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn sidebar() -> impl IntoView {
|
fn sidebar() -> impl IntoView {
|
||||||
let worktrees = Stack::from_iter(WORKSPACES.into_iter().enumerate().map(|(index, name)| {
|
// The worktree list this shell used to render came from a five-entry
|
||||||
Stack::vertical((
|
// fixture naming this developer's machines, backed by no git integration.
|
||||||
Label::new(name),
|
// The GPUI shell's copy was deleted in the same pass; this one renders the
|
||||||
Label::new(if index == 0 { "main · metal" } else { "" })
|
// workspace it is actually open on.
|
||||||
.style(|s| s.font_size(10.0).color(MUTED).margin_top(2.0)),
|
let worktrees = Stack::vertical((Label::new("Lumbridge Code"),)).style(|s| {
|
||||||
))
|
s.flex_col()
|
||||||
.style(move |s| {
|
.padding_horiz(8.0)
|
||||||
s.width_full()
|
.padding_vert(7.0)
|
||||||
.padding_vert(7.0)
|
.color(TEXT)
|
||||||
.padding_horiz(10.0)
|
});
|
||||||
.margin_bottom(3.0)
|
|
||||||
.color(if index == 0 { TEXT } else { MUTED })
|
|
||||||
.apply_if(index == 0, |s| s.background(PANEL_ALT).border_radius(5.0))
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
.style(|s| s.flex_col().padding_horiz(8.0));
|
|
||||||
|
|
||||||
Stack::vertical((
|
Stack::vertical((
|
||||||
Label::new("ATTENTION · 1").style(|s| s.font_size(10.0).color(ATTENTION).padding(12.0)),
|
Label::new("ATTENTION · 1").style(|s| s.font_size(10.0).color(ATTENTION).padding(12.0)),
|
||||||
@@ -434,8 +427,7 @@ fn app_view() -> impl IntoView {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.style(|s| s.flex_grow(1.0)),
|
.style(|s| s.flex_grow(1.0)),
|
||||||
Label::new(FOOTER_CENTER).style(|s| s.flex_grow(1.0)),
|
Label::new(FOOTER_FIXTURE_NOTICE).style(|s| s.flex_grow(1.0)),
|
||||||
Label::new(FOOTER_RIGHT),
|
|
||||||
))
|
))
|
||||||
.style(|s| {
|
.style(|s| {
|
||||||
s.height(32.0)
|
s.height(32.0)
|
||||||
|
|||||||
+112
-91
@@ -16,7 +16,7 @@ use lumbridge_runtime::{
|
|||||||
};
|
};
|
||||||
use lumbridge_spike_model::{
|
use lumbridge_spike_model::{
|
||||||
ActionOutcome, OutputSource, PaneId as FixturePaneId, PaneStatus, ShellAction, ShellModel,
|
ActionOutcome, OutputSource, PaneId as FixturePaneId, PaneStatus, ShellAction, ShellModel,
|
||||||
SurfaceKind, WORKSPACES,
|
SurfaceKind,
|
||||||
};
|
};
|
||||||
use lumbridge_storage::Store;
|
use lumbridge_storage::Store;
|
||||||
use lumbridge_terminal::{
|
use lumbridge_terminal::{
|
||||||
@@ -28,16 +28,63 @@ use lumbridge_terminal::{
|
|||||||
use panel_registry::{PanelId, PanelKind, PanelRegistry, SeedPane};
|
use panel_registry::{PanelId, PanelKind, PanelRegistry, SeedPane};
|
||||||
use usage_feed::{UsageFeed, UsageSegment};
|
use usage_feed::{UsageFeed, UsageSegment};
|
||||||
|
|
||||||
|
// A colour is read as six hex digits, so `0x8b_c8ff` would be harder to check
|
||||||
|
// against a design token than `0x8bc8ff`, not easier. Scoped to the palette and
|
||||||
|
// the two terminal colour tables; every other literal keeps the lint.
|
||||||
|
#[allow(
|
||||||
|
clippy::unreadable_literal,
|
||||||
|
reason = "six-digit colour hex reads whole"
|
||||||
|
)]
|
||||||
const BG: u32 = 0x090c12;
|
const BG: u32 = 0x090c12;
|
||||||
|
#[allow(
|
||||||
|
clippy::unreadable_literal,
|
||||||
|
reason = "six-digit colour hex reads whole"
|
||||||
|
)]
|
||||||
const PANEL: u32 = 0x101620;
|
const PANEL: u32 = 0x101620;
|
||||||
|
#[allow(
|
||||||
|
clippy::unreadable_literal,
|
||||||
|
reason = "six-digit colour hex reads whole"
|
||||||
|
)]
|
||||||
const PANEL_ALT: u32 = 0x151d29;
|
const PANEL_ALT: u32 = 0x151d29;
|
||||||
|
#[allow(
|
||||||
|
clippy::unreadable_literal,
|
||||||
|
reason = "six-digit colour hex reads whole"
|
||||||
|
)]
|
||||||
const PANEL_ACTIVE: u32 = 0x182334;
|
const PANEL_ACTIVE: u32 = 0x182334;
|
||||||
|
#[allow(
|
||||||
|
clippy::unreadable_literal,
|
||||||
|
reason = "six-digit colour hex reads whole"
|
||||||
|
)]
|
||||||
const BORDER: u32 = 0x263246;
|
const BORDER: u32 = 0x263246;
|
||||||
|
#[allow(
|
||||||
|
clippy::unreadable_literal,
|
||||||
|
reason = "six-digit colour hex reads whole"
|
||||||
|
)]
|
||||||
const BORDER_QUIET: u32 = 0x1c2636;
|
const BORDER_QUIET: u32 = 0x1c2636;
|
||||||
|
#[allow(
|
||||||
|
clippy::unreadable_literal,
|
||||||
|
reason = "six-digit colour hex reads whole"
|
||||||
|
)]
|
||||||
const TEXT: u32 = 0xdbe5f4;
|
const TEXT: u32 = 0xdbe5f4;
|
||||||
|
#[allow(
|
||||||
|
clippy::unreadable_literal,
|
||||||
|
reason = "six-digit colour hex reads whole"
|
||||||
|
)]
|
||||||
const MUTED: u32 = 0x8290a8;
|
const MUTED: u32 = 0x8290a8;
|
||||||
|
#[allow(
|
||||||
|
clippy::unreadable_literal,
|
||||||
|
reason = "six-digit colour hex reads whole"
|
||||||
|
)]
|
||||||
const ACCENT: u32 = 0x68b5f8;
|
const ACCENT: u32 = 0x68b5f8;
|
||||||
|
#[allow(
|
||||||
|
clippy::unreadable_literal,
|
||||||
|
reason = "six-digit colour hex reads whole"
|
||||||
|
)]
|
||||||
const ATTENTION: u32 = 0xf1b96a;
|
const ATTENTION: u32 = 0xf1b96a;
|
||||||
|
#[allow(
|
||||||
|
clippy::unreadable_literal,
|
||||||
|
reason = "six-digit colour hex reads whole"
|
||||||
|
)]
|
||||||
const SUCCESS: u32 = 0x70d6a8;
|
const SUCCESS: u32 = 0x70d6a8;
|
||||||
const TIMING_SAMPLE_LIMIT: usize = 256;
|
const TIMING_SAMPLE_LIMIT: usize = 256;
|
||||||
const RUNTIME_POLL_INTERVAL: Duration = Duration::from_millis(16);
|
const RUNTIME_POLL_INTERVAL: Duration = Duration::from_millis(16);
|
||||||
@@ -94,7 +141,6 @@ struct LumbridgeShell {
|
|||||||
/// Which decision-shelf choice each panel has selected. Selecting one is
|
/// Which decision-shelf choice each panel has selected. Selecting one is
|
||||||
/// inert by design: it prepares nothing and runs nothing.
|
/// inert by design: it prepares nothing and runs nothing.
|
||||||
shelf_choice: BTreeMap<PanelId, usize>,
|
shelf_choice: BTreeMap<PanelId, usize>,
|
||||||
active_worktree: usize,
|
|
||||||
add_panel_chooser_open: bool,
|
add_panel_chooser_open: bool,
|
||||||
root_focus: FocusHandle,
|
root_focus: FocusHandle,
|
||||||
}
|
}
|
||||||
@@ -339,6 +385,34 @@ fn terminal_paint_rows(snapshot: &TerminalSnapshot) -> Vec<Vec<TerminalPaintRun>
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(
|
||||||
|
clippy::unreadable_literal,
|
||||||
|
reason = "six-digit colour hex reads whole"
|
||||||
|
)]
|
||||||
|
/// Narrows an already-clamped dimension.
|
||||||
|
///
|
||||||
|
/// Every caller clamps into `2.0..=u16::MAX` first, so this is a narrowing of a
|
||||||
|
/// value known to fit — but `as` would silently produce garbage if a caller ever
|
||||||
|
/// stopped clamping, and a NaN would become zero. This saturates instead, which
|
||||||
|
/// is why the `with_cell_size` expect below is honest rather than hopeful.
|
||||||
|
fn clamp_to_u16(value: f32) -> u16 {
|
||||||
|
if value.is_nan() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
#[allow(
|
||||||
|
clippy::cast_possible_truncation,
|
||||||
|
clippy::cast_sign_loss,
|
||||||
|
reason = "clamped into u16 range on the line above"
|
||||||
|
)]
|
||||||
|
{
|
||||||
|
value.clamp(0.0, f32::from(u16::MAX)) as u16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(
|
||||||
|
clippy::unreadable_literal,
|
||||||
|
reason = "six-digit colour hex reads whole"
|
||||||
|
)]
|
||||||
fn terminal_color(color: TerminalColor) -> u32 {
|
fn terminal_color(color: TerminalColor) -> u32 {
|
||||||
match color {
|
match color {
|
||||||
TerminalColor::Rgb { red, green, blue } => {
|
TerminalColor::Rgb { red, green, blue } => {
|
||||||
@@ -377,6 +451,10 @@ fn terminal_color(color: TerminalColor) -> u32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(
|
||||||
|
clippy::unreadable_literal,
|
||||||
|
reason = "six-digit colour hex reads whole"
|
||||||
|
)]
|
||||||
fn indexed_terminal_color(index: u8) -> u32 {
|
fn indexed_terminal_color(index: u8) -> u32 {
|
||||||
const ANSI: [u32; 16] = [
|
const ANSI: [u32; 16] = [
|
||||||
0x1d2430, 0xff6b6b, 0x70d6a8, 0xf1c76a, 0x68b5f8, 0xc79bf2, 0x63d5da, 0xdbe5f4, 0x6d7a91,
|
0x1d2430, 0xff6b6b, 0x70d6a8, 0xf1c76a, 0x68b5f8, 0xc79bf2, 0x63d5da, 0xdbe5f4, 0x6d7a91,
|
||||||
@@ -427,10 +505,10 @@ fn terminal_dimensions_for_window(
|
|||||||
.floor()
|
.floor()
|
||||||
.clamp(20.0, f32::from(u16::MAX));
|
.clamp(20.0, f32::from(u16::MAX));
|
||||||
TerminalDimensions::with_cell_size(
|
TerminalDimensions::with_cell_size(
|
||||||
rows as u16,
|
clamp_to_u16(rows),
|
||||||
columns as u16,
|
clamp_to_u16(columns),
|
||||||
TERMINAL_CELL_WIDTH.round() as u16,
|
clamp_to_u16(TERMINAL_CELL_WIDTH.round()),
|
||||||
TERMINAL_CELL_HEIGHT.round() as u16,
|
clamp_to_u16(TERMINAL_CELL_HEIGHT.round()),
|
||||||
)
|
)
|
||||||
.expect("geometry clamps terminal dimensions above zero")
|
.expect("geometry clamps terminal dimensions above zero")
|
||||||
}
|
}
|
||||||
@@ -513,7 +591,7 @@ fn load_panel_registry() -> (Option<Store>, PanelRegistry, String) {
|
|||||||
let Some(path) = workspace_database_path() else {
|
let Some(path) = workspace_database_path() else {
|
||||||
return (
|
return (
|
||||||
None,
|
None,
|
||||||
PanelRegistry::seeded(),
|
PanelRegistry::first_run(),
|
||||||
"layout memory-only · no data directory".to_owned(),
|
"layout memory-only · no data directory".to_owned(),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -522,7 +600,7 @@ fn load_panel_registry() -> (Option<Store>, PanelRegistry, String) {
|
|||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
None,
|
None,
|
||||||
PanelRegistry::seeded(),
|
PanelRegistry::first_run(),
|
||||||
format!("layout memory-only · {error}"),
|
format!("layout memory-only · {error}"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -531,7 +609,7 @@ fn load_panel_registry() -> (Option<Store>, PanelRegistry, String) {
|
|||||||
Err(error) => {
|
Err(error) => {
|
||||||
return (
|
return (
|
||||||
None,
|
None,
|
||||||
PanelRegistry::seeded(),
|
PanelRegistry::first_run(),
|
||||||
format!("layout memory-only · {error}"),
|
format!("layout memory-only · {error}"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -541,18 +619,18 @@ fn load_panel_registry() -> (Option<Store>, PanelRegistry, String) {
|
|||||||
Ok(panels) => (Some(store), panels, "layout restored · SQLite".to_owned()),
|
Ok(panels) => (Some(store), panels, "layout restored · SQLite".to_owned()),
|
||||||
Err(error) => (
|
Err(error) => (
|
||||||
Some(store),
|
Some(store),
|
||||||
PanelRegistry::seeded(),
|
PanelRegistry::first_run(),
|
||||||
format!("invalid layout ignored · {error}"),
|
format!("invalid layout ignored · {error}"),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
Ok(None) => (
|
Ok(None) => (
|
||||||
Some(store),
|
Some(store),
|
||||||
PanelRegistry::seeded(),
|
PanelRegistry::first_run(),
|
||||||
"layout ready · SQLite".to_owned(),
|
"layout ready · SQLite".to_owned(),
|
||||||
),
|
),
|
||||||
Err(error) => (
|
Err(error) => (
|
||||||
Some(store),
|
Some(store),
|
||||||
PanelRegistry::seeded(),
|
PanelRegistry::first_run(),
|
||||||
format!("layout read failed · {error}"),
|
format!("layout read failed · {error}"),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -642,7 +720,6 @@ impl LumbridgeShell {
|
|||||||
usage: UsageFeed::start(),
|
usage: UsageFeed::start(),
|
||||||
surfaces: BTreeMap::new(),
|
surfaces: BTreeMap::new(),
|
||||||
shelf_choice: BTreeMap::new(),
|
shelf_choice: BTreeMap::new(),
|
||||||
active_worktree: 0,
|
|
||||||
add_panel_chooser_open: false,
|
add_panel_chooser_open: false,
|
||||||
root_focus,
|
root_focus,
|
||||||
}
|
}
|
||||||
@@ -655,7 +732,7 @@ impl LumbridgeShell {
|
|||||||
let result = self
|
let result = self
|
||||||
.panels
|
.panels
|
||||||
.to_json()
|
.to_json()
|
||||||
.map_err(|error| error.to_owned())
|
.map_err(|error| error.clone())
|
||||||
.and_then(|json| {
|
.and_then(|json| {
|
||||||
store
|
store
|
||||||
.save_workspace_snapshot(WORKSPACE_SNAPSHOT_ID, &json)
|
.save_workspace_snapshot(WORKSPACE_SNAPSHOT_ID, &json)
|
||||||
@@ -908,13 +985,6 @@ impl LumbridgeShell {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_worktree(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.timing.mark_dispatch();
|
|
||||||
self.active_worktree = index.min(WORKSPACES.len().saturating_sub(1));
|
|
||||||
window.focus(&self.root_focus);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn select_pane_at(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
|
fn select_pane_at(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.timing.mark_dispatch();
|
self.timing.mark_dispatch();
|
||||||
if self.panels.select_attached_at(index) {
|
if self.panels.select_attached_at(index) {
|
||||||
@@ -1204,7 +1274,7 @@ impl LumbridgeShell {
|
|||||||
|| run.style.contains(TerminalCellStyle::DOTTED_UNDERLINE)
|
|| run.style.contains(TerminalCellStyle::DOTTED_UNDERLINE)
|
||||||
|| run.style.contains(TerminalCellStyle::DASHED_UNDERLINE)
|
|| run.style.contains(TerminalCellStyle::DASHED_UNDERLINE)
|
||||||
|| run.hyperlink,
|
|| run.hyperlink,
|
||||||
|view| view.underline(),
|
gpui::Styled::underline,
|
||||||
)
|
)
|
||||||
.when(run.style.contains(TerminalCellStyle::STRIKEOUT), |view| {
|
.when(run.style.contains(TerminalCellStyle::STRIKEOUT), |view| {
|
||||||
view.line_through()
|
view.line_through()
|
||||||
@@ -1247,7 +1317,7 @@ impl LumbridgeShell {
|
|||||||
"BROWSER",
|
"BROWSER",
|
||||||
vec![
|
vec![
|
||||||
"about:blank".to_owned(),
|
"about:blank".to_owned(),
|
||||||
"Content process: isolated".to_owned(),
|
"No web engine is embedded in this build.".to_owned(),
|
||||||
"Choose a URL or open externally ↗".to_owned(),
|
"Choose a URL or open externally ↗".to_owned(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1256,7 +1326,7 @@ impl LumbridgeShell {
|
|||||||
"MARKDOWN",
|
"MARKDOWN",
|
||||||
vec![
|
vec![
|
||||||
"# Untitled note".to_owned(),
|
"# Untitled note".to_owned(),
|
||||||
"".to_owned(),
|
String::new(),
|
||||||
"Local-first workspace document.".to_owned(),
|
"Local-first workspace document.".to_owned(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1264,7 +1334,7 @@ impl LumbridgeShell {
|
|||||||
SurfaceKind::Review,
|
SurfaceKind::Review,
|
||||||
"REVIEW",
|
"REVIEW",
|
||||||
vec![
|
vec![
|
||||||
"Working tree ready for review.".to_owned(),
|
"No working tree is loaded.".to_owned(),
|
||||||
"No approval has been requested.".to_owned(),
|
"No approval has been requested.".to_owned(),
|
||||||
"0 files selected".to_owned(),
|
"0 files selected".to_owned(),
|
||||||
],
|
],
|
||||||
@@ -2069,18 +2139,10 @@ impl LumbridgeShell {
|
|||||||
.count();
|
.count();
|
||||||
let total = self.live_terminals.len();
|
let total = self.live_terminals.len();
|
||||||
let local = RuntimeRow {
|
let local = RuntimeRow {
|
||||||
label: "metal",
|
label: "this machine",
|
||||||
detail: format!("{running}/{total} live PTYs"),
|
detail: format!("{running}/{total} live PTYs"),
|
||||||
tone: if running == 0 { MUTED } else { SUCCESS },
|
tone: if running == 0 { MUTED } else { SUCCESS },
|
||||||
};
|
};
|
||||||
// Remote runtimes are a Phase 1 deliverable. Until the SSH stdio
|
|
||||||
// transport exists, the only honest status for a saved host is that
|
|
||||||
// nothing is connected to it.
|
|
||||||
let remote = RuntimeRow {
|
|
||||||
label: "amd-server",
|
|
||||||
detail: "saved host · no runtime connected".to_owned(),
|
|
||||||
tone: MUTED,
|
|
||||||
};
|
|
||||||
// The usage adapter is neither a host nor an endpoint, but its health
|
// The usage adapter is neither a host nor an endpoint, but its health
|
||||||
// belongs beside them: it is the reason the footer can or cannot answer.
|
// belongs beside them: it is the reason the footer can or cannot answer.
|
||||||
let adapter = RuntimeRow {
|
let adapter = RuntimeRow {
|
||||||
@@ -2097,7 +2159,7 @@ impl LumbridgeShell {
|
|||||||
SUCCESS
|
SUCCESS
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
vec![local, remote, adapter]
|
vec![local, adapter]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The standing of every harness, always, regardless of what is selected.
|
/// The standing of every harness, always, regardless of what is selected.
|
||||||
@@ -2373,41 +2435,6 @@ impl Render for LumbridgeShell {
|
|||||||
shell.select_pane(id, window, cx);
|
shell.select_pane(id, window, cx);
|
||||||
}))
|
}))
|
||||||
}))
|
}))
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.px_4()
|
|
||||||
.py_2()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(rgb(MUTED))
|
|
||||||
.child("WORKTREES"),
|
|
||||||
)
|
|
||||||
.children(WORKSPACES.into_iter().enumerate().map(|(index, name)| {
|
|
||||||
let active = index == self.active_worktree;
|
|
||||||
div()
|
|
||||||
.id(("worktree", index as u64))
|
|
||||||
.cursor_pointer()
|
|
||||||
.mx_2()
|
|
||||||
.mb_1()
|
|
||||||
.px_3()
|
|
||||||
.py_2()
|
|
||||||
.rounded(px(5.0))
|
|
||||||
.when(active, |view| view.bg(rgb(PANEL_ALT)).text_color(rgb(TEXT)))
|
|
||||||
.when(!active, |view| view.text_color(rgb(MUTED)))
|
|
||||||
.hover(|view| view.bg(rgb(PANEL_ACTIVE)).text_color(rgb(TEXT)))
|
|
||||||
.child(name)
|
|
||||||
.when(active, |view| {
|
|
||||||
view.child(
|
|
||||||
div()
|
|
||||||
.mt_1()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(rgb(MUTED))
|
|
||||||
.child("main · metal"),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.on_click(cx.listener(move |shell, _, window, cx| {
|
|
||||||
shell.select_worktree(index, window, cx);
|
|
||||||
}))
|
|
||||||
}))
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.mt_3()
|
.mt_3()
|
||||||
@@ -2460,23 +2487,7 @@ impl Render for LumbridgeShell {
|
|||||||
shell.attach_panel(pane, window, cx);
|
shell.attach_panel(pane, window, cx);
|
||||||
}))
|
}))
|
||||||
}))
|
}))
|
||||||
.child(div().flex_1())
|
.child(div().flex_1());
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.m_3()
|
|
||||||
.p_3()
|
|
||||||
.rounded(px(5.0))
|
|
||||||
.bg(rgb(PANEL_ALT))
|
|
||||||
.text_xs()
|
|
||||||
.text_color(rgb(MUTED))
|
|
||||||
.child("Buzz · lumbridgecode")
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.mt_1()
|
|
||||||
.text_color(rgb(SUCCESS))
|
|
||||||
.child("connected · signed identity"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
let tabs = div()
|
let tabs = div()
|
||||||
.flex()
|
.flex()
|
||||||
@@ -2499,7 +2510,7 @@ impl Render for LumbridgeShell {
|
|||||||
.border_b_2()
|
.border_b_2()
|
||||||
.border_color(rgb(ACCENT))
|
.border_color(rgb(ACCENT))
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.child(WORKSPACES[self.active_worktree]),
|
.child("Lumbridge Code"),
|
||||||
)
|
)
|
||||||
.child(div().flex_1())
|
.child(div().flex_1())
|
||||||
.child(div().px_3().text_xs().text_color(rgb(MUTED)).child(format!(
|
.child(div().px_3().text_xs().text_color(rgb(MUTED)).child(format!(
|
||||||
@@ -2627,15 +2638,21 @@ impl Render for LumbridgeShell {
|
|||||||
.ml_3()
|
.ml_3()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.text_color(rgb(MUTED))
|
.text_color(rgb(MUTED))
|
||||||
.child(format!("{} / main", WORKSPACES[self.active_worktree])),
|
.child("Lumbridge Code"),
|
||||||
)
|
)
|
||||||
.child(div().flex_1())
|
.child(div().flex_1())
|
||||||
|
// The same tone rule runtime_rows applies: nothing live is
|
||||||
|
// not a success, and "0/5 LIVE PTYS" in green said it was.
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.mr_4()
|
.mr_4()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(rgb(SUCCESS))
|
.text_color(rgb(if running_runtime_count == 0 {
|
||||||
.child(format!("metal · {runtime_summary}")),
|
MUTED
|
||||||
|
} else {
|
||||||
|
SUCCESS
|
||||||
|
}))
|
||||||
|
.child(runtime_summary),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -2921,6 +2938,10 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[allow(
|
||||||
|
clippy::unreadable_literal,
|
||||||
|
reason = "six-digit colour hex reads whole"
|
||||||
|
)]
|
||||||
fn xterm_color_cube_and_grayscale_are_deterministic() {
|
fn xterm_color_cube_and_grayscale_are_deterministic() {
|
||||||
assert_eq!(indexed_terminal_color(16), 0x000000);
|
assert_eq!(indexed_terminal_color(16), 0x000000);
|
||||||
assert_eq!(indexed_terminal_color(231), 0xffffff);
|
assert_eq!(indexed_terminal_color(231), 0xffffff);
|
||||||
|
|||||||
@@ -36,18 +36,26 @@ impl PanelKind {
|
|||||||
pub(crate) const fn description(self) -> &'static str {
|
pub(crate) const fn description(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Terminal => "A real local shell with its own runtime actor",
|
Self::Terminal => "A real local shell with its own runtime actor",
|
||||||
Self::Browser => "An isolated system-web-engine surface",
|
// Not a web engine. There is no browser in this build; the panel
|
||||||
|
// renders a placeholder surface and says so.
|
||||||
|
Self::Browser => "A placeholder surface. No web engine is embedded yet",
|
||||||
Self::Markdown => "Local notes, plans, and architecture",
|
Self::Markdown => "Local notes, plans, and architecture",
|
||||||
Self::Review => "Repository changes and approval boundaries",
|
Self::Review => "Repository changes and approval boundaries",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What a newly created panel is pointed at.
|
||||||
|
///
|
||||||
|
/// Every one of these used to name a specific machine — this developer's,
|
||||||
|
/// as it happens. A panel's target is whatever it was actually launched
|
||||||
|
/// against, and a new panel has been launched against nothing but the
|
||||||
|
/// local shell.
|
||||||
const fn default_target(self) -> &'static str {
|
const fn default_target(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Terminal => "metal · local runtime",
|
Self::Terminal => "local shell",
|
||||||
Self::Browser => "isolated system web engine",
|
Self::Browser => "no surface attached",
|
||||||
Self::Markdown => "lumbridge-code · local document",
|
Self::Markdown => "local document",
|
||||||
Self::Review => "lumbridge-code · working tree",
|
Self::Review => "working tree",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,49 +104,26 @@ pub(crate) struct PanelRegistry {
|
|||||||
|
|
||||||
impl Default for PanelRegistry {
|
impl Default for PanelRegistry {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::seeded()
|
Self::first_run()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PanelRegistry {
|
impl PanelRegistry {
|
||||||
pub(crate) fn seeded() -> Self {
|
/// The workspace a first run opens with.
|
||||||
|
///
|
||||||
|
/// Three real local shells and nothing else. This used to seed six panes
|
||||||
|
/// announcing "Codex · runtime / metal · Tailscale SSH", "Claude Code · UI
|
||||||
|
/// / `MacBook` Air · local" and a Pi pane on a saved host — none of which
|
||||||
|
/// existed. Every one was a `/bin/sh`. A pane claims a harness when one has
|
||||||
|
/// actually been launched into it, and until then it is a terminal.
|
||||||
|
///
|
||||||
|
/// Three, not one: `visible_panel_count` branches at 1100 px and 2800 px,
|
||||||
|
/// so a single-panel default renders one pane on an ultrawide.
|
||||||
|
pub(crate) fn first_run() -> Self {
|
||||||
let seeds = [
|
let seeds = [
|
||||||
(
|
(PanelKind::Terminal, "Terminal 1", "local shell", None),
|
||||||
PanelKind::Terminal,
|
(PanelKind::Terminal, "Terminal 2", "local shell", None),
|
||||||
"Codex · runtime",
|
(PanelKind::Terminal, "Terminal 3", "local shell", None),
|
||||||
"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
|
let panels = seeds
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -149,7 +134,7 @@ impl PanelRegistry {
|
|||||||
title: title.to_owned(),
|
title: title.to_owned(),
|
||||||
target: target.to_owned(),
|
target: target.to_owned(),
|
||||||
attached: index < 5,
|
attached: index < 5,
|
||||||
seed: Some(seed),
|
seed,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Self {
|
Self {
|
||||||
@@ -335,7 +320,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dynamic_panels_receive_monotonic_ids_and_insert_beside_selection() {
|
fn dynamic_panels_receive_monotonic_ids_and_insert_beside_selection() {
|
||||||
let mut registry = PanelRegistry::seeded();
|
let mut registry = PanelRegistry::first_run();
|
||||||
let browser = registry.create(PanelKind::Browser);
|
let browser = registry.create(PanelKind::Browser);
|
||||||
let terminal = registry.create(PanelKind::Terminal);
|
let terminal = registry.create(PanelKind::Terminal);
|
||||||
assert_eq!(browser, PanelId(7));
|
assert_eq!(browser, PanelId(7));
|
||||||
@@ -347,7 +332,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn identity_and_detached_state_round_trip_without_reuse() {
|
fn identity_and_detached_state_round_trip_without_reuse() {
|
||||||
let mut registry = PanelRegistry::seeded();
|
let mut registry = PanelRegistry::first_run();
|
||||||
let created = registry.create(PanelKind::Markdown);
|
let created = registry.create(PanelKind::Markdown);
|
||||||
assert!(registry.detach(created));
|
assert!(registry.detach(created));
|
||||||
let json = registry.to_json().unwrap();
|
let json = registry.to_json().unwrap();
|
||||||
@@ -358,7 +343,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn detach_preserves_panel_and_reattach_restores_same_identity() {
|
fn detach_preserves_panel_and_reattach_restores_same_identity() {
|
||||||
let mut registry = PanelRegistry::seeded();
|
let mut registry = PanelRegistry::first_run();
|
||||||
let id = registry.create(PanelKind::Terminal);
|
let id = registry.create(PanelKind::Terminal);
|
||||||
assert!(registry.detach(id));
|
assert!(registry.detach(id));
|
||||||
assert!(registry.panel(id).is_some());
|
assert!(registry.panel(id).is_some());
|
||||||
|
|||||||
@@ -34,9 +34,15 @@ struct DeclaredProfile {
|
|||||||
account: &'static str,
|
account: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The Codex pane maps onto the probe's own primary-window profile, so a live
|
/// Only harnesses with a real adapter behind them.
|
||||||
/// reading lands under the pane that produced it. The rest are declarations
|
///
|
||||||
/// with no adapter behind them yet.
|
/// A third entry declared "Pi · spark-1 · laguna-s-2.1 · self-hosted" with no
|
||||||
|
/// probe of any kind, so the strip carried a permanent empty rail for a harness
|
||||||
|
/// the user was not running. Declaring a profile is a promise that the gap is
|
||||||
|
/// real; declaring one nothing can ever fill is just a fabricated row.
|
||||||
|
///
|
||||||
|
/// The seed mapping stays: it is how a live reading lands under the pane that
|
||||||
|
/// produced it, once a harness can actually be launched into a pane.
|
||||||
const DECLARED: &[DeclaredProfile] = &[
|
const DECLARED: &[DeclaredProfile] = &[
|
||||||
DeclaredProfile {
|
DeclaredProfile {
|
||||||
seed: SeedPane::CodexRuntime,
|
seed: SeedPane::CodexRuntime,
|
||||||
@@ -58,14 +64,6 @@ const DECLARED: &[DeclaredProfile] = &[
|
|||||||
model: "five-hour window",
|
model: "five-hour window",
|
||||||
account: "subscription",
|
account: "subscription",
|
||||||
},
|
},
|
||||||
DeclaredProfile {
|
|
||||||
seed: SeedPane::PiDocs,
|
|
||||||
id: "spark-1-local",
|
|
||||||
harness: "Pi",
|
|
||||||
provider: "spark-1",
|
|
||||||
model: "laguna-s-2.1",
|
|
||||||
account: "self-hosted",
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
pub(crate) struct UsageFeed {
|
pub(crate) struct UsageFeed {
|
||||||
@@ -78,8 +76,45 @@ pub(crate) struct UsageFeed {
|
|||||||
probe_status: String,
|
probe_status: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Which probes to start.
|
||||||
|
///
|
||||||
|
/// Passed in rather than read from the environment inside the constructor:
|
||||||
|
/// a test that wanted a probe-free feed used to set a process-wide environment
|
||||||
|
/// variable, which required `unsafe` and silently disabled both probes for
|
||||||
|
/// every other test in the binary.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub(crate) struct UsageFeedOptions {
|
||||||
|
pub(crate) codex: bool,
|
||||||
|
pub(crate) claude: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UsageFeedOptions {
|
||||||
|
/// Reads the documented opt-out switches. The only place the environment
|
||||||
|
/// is consulted, so a caller can always ask for something else.
|
||||||
|
fn from_env() -> Self {
|
||||||
|
let enabled = |name: &str| std::env::var_os(name).is_none_or(|value| value != "0");
|
||||||
|
Self {
|
||||||
|
codex: enabled("LUMBRIDGE_CODEX_PROBE"),
|
||||||
|
claude: enabled("LUMBRIDGE_CLAUDE_PROBE"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Declares every profile and starts nothing.
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) const fn none() -> Self {
|
||||||
|
Self {
|
||||||
|
codex: false,
|
||||||
|
claude: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl UsageFeed {
|
impl UsageFeed {
|
||||||
pub(crate) fn start() -> Self {
|
pub(crate) fn start() -> Self {
|
||||||
|
Self::start_with(UsageFeedOptions::from_env())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn start_with(options: UsageFeedOptions) -> Self {
|
||||||
let mut profiles = BTreeMap::new();
|
let mut profiles = BTreeMap::new();
|
||||||
let mut seeds = Vec::new();
|
let mut seeds = Vec::new();
|
||||||
for declared in DECLARED {
|
for declared in DECLARED {
|
||||||
@@ -105,8 +140,8 @@ impl UsageFeed {
|
|||||||
health: BTreeMap::new(),
|
health: BTreeMap::new(),
|
||||||
probe_status: "no usage adapter running".to_owned(),
|
probe_status: "no usage adapter running".to_owned(),
|
||||||
};
|
};
|
||||||
feed.start_codex_probe();
|
feed.start_codex_probe(options.codex);
|
||||||
feed.start_claude_probe();
|
feed.start_claude_probe(options.claude);
|
||||||
feed
|
feed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,8 +150,8 @@ impl UsageFeed {
|
|||||||
/// A failure here is not an error state for the shell: the profiles stay
|
/// A failure here is not an error state for the shell: the profiles stay
|
||||||
/// declared and render as unavailable, which is exactly what they should do
|
/// declared and render as unavailable, which is exactly what they should do
|
||||||
/// when no adapter can run.
|
/// when no adapter can run.
|
||||||
fn start_codex_probe(&mut self) {
|
fn start_codex_probe(&mut self, enabled: bool) {
|
||||||
if std::env::var_os("LUMBRIDGE_CODEX_PROBE").is_some_and(|value| value == "0") {
|
if !enabled {
|
||||||
self.probe_status = "codex probe disabled".to_owned();
|
self.probe_status = "codex probe disabled".to_owned();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -143,8 +178,12 @@ impl UsageFeed {
|
|||||||
/// This one reads files rather than launching anything, so it has no
|
/// This one reads files rather than launching anything, so it has no
|
||||||
/// harness to fail to find — a missing projects directory simply reports
|
/// harness to fail to find — a missing projects directory simply reports
|
||||||
/// nothing until Claude Code creates it.
|
/// nothing until Claude Code creates it.
|
||||||
fn start_claude_probe(&mut self) {
|
fn start_claude_probe(&mut self, enabled: bool) {
|
||||||
if std::env::var_os("LUMBRIDGE_CLAUDE_PROBE").is_some_and(|value| value == "0") {
|
if !enabled {
|
||||||
|
// Say so, rather than leaving whatever the Codex path last wrote.
|
||||||
|
// A disabled probe that reports nothing about itself is
|
||||||
|
// indistinguishable from one that failed to start.
|
||||||
|
self.probe_status = "claude probe disabled".to_owned();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match ClaudeCodeProbe::start(ClaudeCodeProbeOptions::default()) {
|
match ClaudeCodeProbe::start(ClaudeCodeProbeOptions::default()) {
|
||||||
@@ -415,19 +454,14 @@ pub(crate) struct UsageSegment {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{DECLARED, UsageFeed};
|
use super::{DECLARED, UsageFeed, UsageFeedOptions};
|
||||||
use crate::panel_registry::SeedPane;
|
use crate::panel_registry::SeedPane;
|
||||||
use lumbridge_core::UsageProvenance;
|
use lumbridge_core::UsageProvenance;
|
||||||
|
|
||||||
/// Never starts a probe, so the test cannot depend on an installed harness,
|
/// Never starts a probe, so the test cannot depend on an installed harness,
|
||||||
/// on the user's transcripts, or on a status-line bridge being present.
|
/// on the user's transcripts, or on a status-line bridge being present.
|
||||||
fn declared_only_feed() -> UsageFeed {
|
fn declared_only_feed() -> UsageFeed {
|
||||||
// SAFETY-FREE: this only sets environment variables for this process.
|
UsageFeed::start_with(UsageFeedOptions::none())
|
||||||
unsafe {
|
|
||||||
std::env::set_var("LUMBRIDGE_CODEX_PROBE", "0");
|
|
||||||
std::env::set_var("LUMBRIDGE_CLAUDE_PROBE", "0");
|
|
||||||
}
|
|
||||||
UsageFeed::start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -485,7 +519,7 @@ mod tests {
|
|||||||
let strip = feed.strip();
|
let strip = feed.strip();
|
||||||
assert_eq!(strip.len(), DECLARED.len());
|
assert_eq!(strip.len(), DECLARED.len());
|
||||||
let labels: Vec<&str> = strip.iter().map(|segment| segment.label.as_str()).collect();
|
let labels: Vec<&str> = strip.iter().map(|segment| segment.label.as_str()).collect();
|
||||||
assert_eq!(labels, ["CODEX", "CLAUDE CODE", "PI"]);
|
assert_eq!(labels, ["CODEX", "CLAUDE CODE"]);
|
||||||
for segment in &strip {
|
for segment in &strip {
|
||||||
assert!(
|
assert!(
|
||||||
segment.headline.is_none() && segment.reset.is_none(),
|
segment.headline.is_none() && segment.reset.is_none(),
|
||||||
|
|||||||
@@ -198,25 +198,15 @@ pub const PANES: [PaneFixture; 6] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
pub const WORKSPACES: [&str; 5] = [
|
/// The one footer string the frozen Floem shell renders.
|
||||||
"Lumbridge Code",
|
|
||||||
"Runtime / metal",
|
|
||||||
"UI spikes",
|
|
||||||
"ACP adapters",
|
|
||||||
"Usage telemetry",
|
|
||||||
];
|
|
||||||
/// Static footer strings retained only by the Floem comparison shell.
|
|
||||||
///
|
///
|
||||||
/// These are layout fixtures, not usage. They are not a usage source and must
|
/// It replaces three that read like data — a pane census, "Codex · ChatGPT
|
||||||
/// not be rendered as one: the GPUI shell now derives its footer from
|
/// subscription · 62% window remaining", and "burn 8.4%/hr · resets in 2h 14m".
|
||||||
/// `lumbridge_core::UsageLedger`, where every value carries a provenance and a
|
/// Decision 0013 names that exact form as the thing that must never be shown,
|
||||||
/// missing fact renders as missing. Delete these once Floem consumes the ledger
|
/// and a comparison shell rendering a fabricated quota beside a real one is
|
||||||
/// or the candidate is retired. See decision 0012.
|
/// worse than a comparison shell with no footer numbers at all. The GPUI shell
|
||||||
pub const FOOTER_LEFT: &str = "6 panes · 3 remote · 1 needs input";
|
/// derives its footer from `lumbridge_core::UsageLedger`. See decision 0012.
|
||||||
/// See [`FOOTER_LEFT`]: a layout fixture, not a usage reading.
|
pub const FOOTER_FIXTURE_NOTICE: &str = "layout fixture · no usage source";
|
||||||
pub const FOOTER_CENTER: &str = "Codex · ChatGPT subscription · 62% window remaining";
|
|
||||||
/// See [`FOOTER_LEFT`]: a layout fixture, not a usage reading.
|
|
||||||
pub const FOOTER_RIGHT: &str = "burn 8.4%/hr · resets in 2h 14m";
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct PaneState {
|
pub struct PaneState {
|
||||||
|
|||||||
Reference in New Issue
Block a user