Replace the footer's placeholder usage with a real observation ledger
The footer showed invented percentages. It now shows what two harnesses actually report, or says it does not know. lumbridge-core gains an append-only per-profile UsageLedger and a projection that labels every derived value estimated, withholds a burn rate from a single sample, withholds a window fraction with no reported ceiling, withholds an exhaustion estimate that lands after the reset, and reports an expired window as rolled over rather than freezing its last percentage. A missing fact renders as missing, never as zero. (0012) lumbridge-harness is the impure side: processes, clocks, and untrusted wire text in, observations out. Three adapters: - Codex's account/rateLimits/read over the app-server's JSON-RPC stdio. The client cannot express a request outside a two-variant enum and answers every server-to-client request with -32601, so a harness asking Lumbridge for a credential is refused by construction. (0013) - Claude Code's session transcripts, as a byte-offset tail follower that reports nothing until the backlog is read to EOF — a partially-read backlog is indistinguishable from a burst of spend, and the first run against 20 MB reported forty-six billion tokens an hour. The parser models four counters, so the conversations in those files are not representable. (0014) - Claude Code's five-hour and seven-day subscription windows, via a bridge installed as its statusLine command. 0014 had claimed no such surface existed; it does, and the record is corrected in place rather than quietly edited. Lumbridge does not read the OAuth credential to call the account usage endpoint, which is what comparable tools do — AGENTS.md forbids it, and 0015 says so rather than leaving the gap unexplained. Also in here: a capability-check ordering fix in the workspace reducer, where the applied-request replay table was consulted before the capability check and so answered questions the caller had no right to ask; the GPUI spike wired to the live probes with per-harness gauges and provenance chips; and a launcher that matches its own window by PID, because GPUI sets WM_NAME but not _NET_WM_NAME and a title match never succeeded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7fe84f71e2
commit
ef52aa7ce2
Generated
+13
@@ -3043,6 +3043,17 @@ dependencies = [
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lumbridge-harness"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"lumbridge-core",
|
||||
"nix 0.28.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lumbridge-pty"
|
||||
version = "0.0.1"
|
||||
@@ -3065,6 +3076,8 @@ name = "lumbridge-spike-gpui"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"gpui",
|
||||
"lumbridge-core",
|
||||
"lumbridge-harness",
|
||||
"lumbridge-runtime",
|
||||
"lumbridge-spike-model",
|
||||
"lumbridge-storage",
|
||||
|
||||
@@ -9,6 +9,8 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
gpui = "0.2.2"
|
||||
lumbridge-core = { path = "../../crates/lumbridge-core" }
|
||||
lumbridge-harness = { path = "../../crates/lumbridge-harness" }
|
||||
lumbridge-runtime = { path = "../../crates/lumbridge-runtime" }
|
||||
lumbridge-spike-model = { path = "../ui-shell-model" }
|
||||
lumbridge-storage = { path = "../../crates/lumbridge-storage" }
|
||||
|
||||
+559
-101
@@ -1,4 +1,5 @@
|
||||
mod panel_registry;
|
||||
mod usage_feed;
|
||||
|
||||
use std::collections::{BTreeMap, VecDeque};
|
||||
use std::path::PathBuf;
|
||||
@@ -8,13 +9,14 @@ use gpui::{
|
||||
App, Application, Bounds, Context, FocusHandle, FontWeight, KeyBinding, KeyDownEvent, Pixels,
|
||||
Size, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, relative, rgb, size,
|
||||
};
|
||||
use lumbridge_core::UsageProvenance;
|
||||
use lumbridge_runtime::{
|
||||
CommandConfig, PtyOptions, RuntimeActorError, RuntimeActorOptions, RuntimeCommand,
|
||||
RuntimeEvent, RuntimeRegistry, RuntimeRegistryError, TerminalSize,
|
||||
};
|
||||
use lumbridge_spike_model::{
|
||||
ActionOutcome, FOOTER_RIGHT, OutputSource, PaneId as FixturePaneId, PaneStatus, ShellAction,
|
||||
ShellModel, SurfaceKind, WORKSPACES,
|
||||
ActionOutcome, OutputSource, PaneId as FixturePaneId, PaneStatus, ShellAction, ShellModel,
|
||||
SurfaceKind, WORKSPACES,
|
||||
};
|
||||
use lumbridge_storage::Store;
|
||||
use lumbridge_terminal::{
|
||||
@@ -24,6 +26,7 @@ use lumbridge_terminal::{
|
||||
};
|
||||
|
||||
use panel_registry::{PanelId, PanelKind, PanelRegistry, SeedPane};
|
||||
use usage_feed::{UsageFeed, UsageSegment};
|
||||
|
||||
const BG: u32 = 0x090c12;
|
||||
const PANEL: u32 = 0x101620;
|
||||
@@ -43,7 +46,7 @@ const WORKSPACE_SNAPSHOT_ID: &str = "lumbridge-code-gpui-spike";
|
||||
const SIDEBAR_WIDTH: f32 = 248.0;
|
||||
const APP_HEADER_HEIGHT: f32 = 48.0;
|
||||
const TAB_BAR_HEIGHT: f32 = 38.0;
|
||||
const APP_FOOTER_HEIGHT: f32 = 32.0;
|
||||
const APP_FOOTER_HEIGHT: f32 = 46.0;
|
||||
const TERMINAL_CONTENT_VERTICAL_INSET: f32 = 24.0;
|
||||
const TERMINAL_HORIZONTAL_INSET: f32 = 32.0;
|
||||
const TERMINAL_CELL_WIDTH: f32 = 8.4;
|
||||
@@ -84,10 +87,90 @@ struct LumbridgeShell {
|
||||
live_terminals: BTreeMap<PanelId, LiveTerminalState>,
|
||||
store: Option<Store>,
|
||||
persistence_status: String,
|
||||
usage: UsageFeed,
|
||||
/// Which surface tab each panel is showing. A panel is absent until the
|
||||
/// user picks something other than the surface it provides natively.
|
||||
surfaces: BTreeMap<PanelId, SurfaceTab>,
|
||||
/// Which decision-shelf choice each panel has selected. Selecting one is
|
||||
/// inert by design: it prepares nothing and runs nothing.
|
||||
shelf_choice: BTreeMap<PanelId, usize>,
|
||||
active_worktree: usize,
|
||||
add_panel_chooser_open: bool,
|
||||
root_focus: FocusHandle,
|
||||
}
|
||||
|
||||
/// The surfaces a pane can be viewed through.
|
||||
///
|
||||
/// A pane is the durable unit of work and its surface is a view over that work.
|
||||
/// Switching one never launches a process, moves the pane, or changes which
|
||||
/// agent owns the session — it only changes what is drawn.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum SurfaceTab {
|
||||
Terminal,
|
||||
Browser,
|
||||
Tools,
|
||||
Context,
|
||||
Goal,
|
||||
Review,
|
||||
}
|
||||
|
||||
impl SurfaceTab {
|
||||
const ALL: [Self; 6] = [
|
||||
Self::Terminal,
|
||||
Self::Browser,
|
||||
Self::Tools,
|
||||
Self::Context,
|
||||
Self::Goal,
|
||||
Self::Review,
|
||||
];
|
||||
|
||||
const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Terminal => "TERMINAL",
|
||||
Self::Browser => "BROWSER",
|
||||
Self::Tools => "TOOLS",
|
||||
Self::Context => "CONTEXT",
|
||||
Self::Goal => "GOAL",
|
||||
Self::Review => "REVIEW",
|
||||
}
|
||||
}
|
||||
|
||||
const fn ordinal(self) -> u64 {
|
||||
match self {
|
||||
Self::Terminal => 0,
|
||||
Self::Browser => 1,
|
||||
Self::Tools => 2,
|
||||
Self::Context => 3,
|
||||
Self::Goal => 4,
|
||||
Self::Review => 5,
|
||||
}
|
||||
}
|
||||
|
||||
/// The one surface a pane of this kind actually provides.
|
||||
const fn native_for(kind: SurfaceKind) -> Self {
|
||||
match kind {
|
||||
SurfaceKind::Terminal => Self::Terminal,
|
||||
SurfaceKind::Browser => Self::Browser,
|
||||
SurfaceKind::Markdown => Self::Context,
|
||||
SurfaceKind::Review => Self::Review,
|
||||
}
|
||||
}
|
||||
|
||||
/// Why this pane cannot show this surface. Stated plainly, because the
|
||||
/// product boundary says an unsupported surface is shown as unavailable
|
||||
/// rather than simulated.
|
||||
const fn unavailable_reason(self) -> &'static str {
|
||||
match self {
|
||||
Self::Terminal => "Only a terminal pane owns a live shell.",
|
||||
Self::Browser => "No isolated web engine is wired yet.",
|
||||
Self::Tools => "Tool calls arrive with the ACP client.",
|
||||
Self::Context => "Context projection is not built yet.",
|
||||
Self::Goal => "Goal tracking is not built yet.",
|
||||
Self::Review => "Review is not wired to Git yet.",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct PanelView {
|
||||
id: PanelId,
|
||||
@@ -488,6 +571,8 @@ impl LumbridgeShell {
|
||||
if this
|
||||
.update(cx, |shell, cx| {
|
||||
shell.dispatch(ShellAction::SyntheticStreamTick);
|
||||
shell.usage.advance();
|
||||
shell.usage.tick();
|
||||
cx.notify();
|
||||
})
|
||||
.is_err()
|
||||
@@ -554,6 +639,10 @@ impl LumbridgeShell {
|
||||
live_terminals,
|
||||
store,
|
||||
persistence_status,
|
||||
usage: UsageFeed::start(),
|
||||
surfaces: BTreeMap::new(),
|
||||
shelf_choice: BTreeMap::new(),
|
||||
active_worktree: 0,
|
||||
add_panel_chooser_open: false,
|
||||
root_focus,
|
||||
}
|
||||
@@ -778,6 +867,54 @@ impl LumbridgeShell {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Changes which surface a pane is viewed through.
|
||||
///
|
||||
/// This is a view change only. It never touches the PTY, the agent, or the
|
||||
/// pane's execution target, so a pane keeps running whatever it was running.
|
||||
fn select_surface(
|
||||
&mut self,
|
||||
pane: PanelId,
|
||||
tab: SurfaceTab,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.timing.mark_dispatch();
|
||||
self.surfaces.insert(pane, tab);
|
||||
if self.panels.select(pane) {
|
||||
self.persist_panels();
|
||||
}
|
||||
window.focus(&self.root_focus);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Records which suggestion the user picked. Deliberately does nothing else.
|
||||
fn select_shelf_choice(
|
||||
&mut self,
|
||||
pane: PanelId,
|
||||
index: usize,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.timing.mark_dispatch();
|
||||
if self.shelf_choice.get(&pane) == Some(&index) {
|
||||
self.shelf_choice.remove(&pane);
|
||||
} else {
|
||||
self.shelf_choice.insert(pane, index);
|
||||
}
|
||||
if self.panels.select(pane) {
|
||||
self.persist_panels();
|
||||
}
|
||||
window.focus(&self.root_focus);
|
||||
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>) {
|
||||
self.timing.mark_dispatch();
|
||||
if self.panels.select_attached_at(index) {
|
||||
@@ -1215,26 +1352,38 @@ impl LumbridgeShell {
|
||||
}
|
||||
},
|
||||
);
|
||||
let surface = match pane.kind {
|
||||
SurfaceKind::Terminal => "TERMINAL",
|
||||
SurfaceKind::Markdown => "CONTEXT",
|
||||
SurfaceKind::Browser => "BROWSER",
|
||||
SurfaceKind::Review => "REVIEW",
|
||||
};
|
||||
let tabs = ["TERMINAL", "BROWSER", "TOOLS", "CONTEXT", "GOAL", "REVIEW"]
|
||||
let native = SurfaceTab::native_for(pane.kind);
|
||||
let shown = self.surface_for(pane.id, pane.kind);
|
||||
let tabs = SurfaceTab::ALL
|
||||
.into_iter()
|
||||
.map(|label| {
|
||||
.map(|tab| {
|
||||
let active = tab == shown;
|
||||
let provided = tab == native;
|
||||
div()
|
||||
.id(("surface-tab", pane_id.get() * 16 + tab.ordinal()))
|
||||
.cursor_pointer()
|
||||
.h_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.px_3()
|
||||
.text_xs()
|
||||
.text_color(rgb(if label == surface { ACCENT } else { MUTED }))
|
||||
.when(label == surface, |view| {
|
||||
view.border_b_2().border_color(rgb(ACCENT))
|
||||
})
|
||||
.child(label)
|
||||
// Provided but unselected reads as available; unprovided
|
||||
// stays dim, so the tab strip shows what this pane can do
|
||||
// before you click rather than after.
|
||||
.text_color(rgb(if active {
|
||||
ACCENT
|
||||
} else if provided {
|
||||
TEXT
|
||||
} else {
|
||||
MUTED
|
||||
}))
|
||||
.when(active, |view| view.border_b_2().border_color(rgb(ACCENT)))
|
||||
.hover(|view| view.bg(rgb(PANEL_ACTIVE)).text_color(rgb(TEXT)))
|
||||
.child(tab.label())
|
||||
.on_click(cx.listener(move |shell, _, window, cx| {
|
||||
cx.stop_propagation();
|
||||
shell.select_surface(pane_id, tab, window, cx);
|
||||
}))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -1304,6 +1453,12 @@ impl LumbridgeShell {
|
||||
.border_color(rgb(BORDER))
|
||||
.text_xs()
|
||||
.text_color(rgb(if can_detach { MUTED } else { BORDER }))
|
||||
.when(can_detach, |view| {
|
||||
view.hover(|view| {
|
||||
view.border_color(rgb(ATTENTION))
|
||||
.text_color(rgb(ATTENTION))
|
||||
})
|
||||
})
|
||||
.child(if can_detach {
|
||||
"− DETACH"
|
||||
} else {
|
||||
@@ -1347,7 +1502,67 @@ impl LumbridgeShell {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// The surface a panel is currently being viewed through.
|
||||
fn surface_for(&self, id: PanelId, kind: SurfaceKind) -> SurfaceTab {
|
||||
self.surfaces
|
||||
.get(&id)
|
||||
.copied()
|
||||
.unwrap_or_else(|| SurfaceTab::native_for(kind))
|
||||
}
|
||||
|
||||
/// What a pane shows when asked for a surface it does not provide.
|
||||
///
|
||||
/// It says so, and it says what is still running underneath, because the
|
||||
/// pane's process identity does not change when the view does.
|
||||
fn unavailable_surface(&self, pane: &PanelView, shown: SurfaceTab) -> gpui::AnyElement {
|
||||
let native = SurfaceTab::native_for(pane.kind);
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.min_w_0()
|
||||
.min_h_0()
|
||||
.overflow_hidden()
|
||||
.bg(rgb(BG))
|
||||
.border_y_1()
|
||||
.border_color(rgb(BORDER))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.p_4()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(rgb(TEXT))
|
||||
.child(format!("{} unavailable", shown.label())),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(rgb(MUTED))
|
||||
.child(shown.unavailable_reason()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.mt_2()
|
||||
.text_xs()
|
||||
.text_color(rgb(SUCCESS))
|
||||
.child(format!(
|
||||
"{} is still this pane's live surface. Nothing was stopped.",
|
||||
native.label()
|
||||
)),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn pane_work_surface(&self, pane: &PanelView) -> gpui::AnyElement {
|
||||
let shown = self.surface_for(pane.id, pane.kind);
|
||||
if shown != SurfaceTab::native_for(pane.kind) {
|
||||
return self.unavailable_surface(pane, shown);
|
||||
}
|
||||
let external = pane.output_source == OutputSource::External;
|
||||
let content = if external {
|
||||
self.terminal_view(pane.id)
|
||||
@@ -1387,35 +1602,22 @@ impl LumbridgeShell {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn pane_decision_region(&self, pane: &PanelView) -> gpui::AnyElement {
|
||||
let choice = |label: &'static str, detail: &'static str, attention: bool| {
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.min_w_0()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.rounded(px(4.0))
|
||||
.border_1()
|
||||
.border_color(rgb(if attention { ATTENTION } else { BORDER }))
|
||||
.bg(rgb(PANEL_ALT))
|
||||
.text_xs()
|
||||
.text_color(rgb(TEXT))
|
||||
.child(label)
|
||||
.child(div().truncate().text_color(rgb(MUTED)).child(detail))
|
||||
};
|
||||
let choices = if pane.needs_input() {
|
||||
fn pane_decision_region(&self, pane: &PanelView, cx: &mut Context<Self>) -> gpui::AnyElement {
|
||||
let pane_id = pane.id;
|
||||
let picked = self.shelf_choice.get(&pane_id).copied();
|
||||
// Every choice names the capability it would need. None of them holds
|
||||
// that capability: choosing prepares, the command plane executes.
|
||||
let choices: [(&'static str, &'static str, &'static str, bool); 3] = if pane.needs_input() {
|
||||
[
|
||||
("Review request", "inspect scope", true),
|
||||
("Steer…", "edit response", false),
|
||||
("Dismiss", "leave inert", false),
|
||||
("Review request", "inspect scope", "needs Observe", true),
|
||||
("Steer…", "edit response", "needs Execute", false),
|
||||
("Dismiss", "leave inert", "inert", false),
|
||||
]
|
||||
} else {
|
||||
[
|
||||
("Continue", "keep moving", false),
|
||||
("Review plan", "inspect commands", false),
|
||||
("Ask…", "refine prompt", false),
|
||||
("Continue", "keep moving", "needs Execute", false),
|
||||
("Review plan", "inspect commands", "needs Observe", false),
|
||||
("Ask…", "refine prompt", "inert", false),
|
||||
]
|
||||
};
|
||||
div()
|
||||
@@ -1447,8 +1649,55 @@ impl LumbridgeShell {
|
||||
),
|
||||
)
|
||||
.child(div().flex().flex_col().gap_1().mt_2().children(
|
||||
choices.map(|(label, detail, attention)| choice(label, detail, attention)),
|
||||
choices.into_iter().enumerate().map(
|
||||
|(index, (label, detail, _capability, attention))| {
|
||||
let chosen = picked == Some(index);
|
||||
div()
|
||||
.id(("shelf-choice", pane_id.get() * 8 + index as u64))
|
||||
.cursor_pointer()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.min_w_0()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.rounded(px(4.0))
|
||||
.border_1()
|
||||
.border_color(rgb(if chosen {
|
||||
ACCENT
|
||||
} else if attention {
|
||||
ATTENTION
|
||||
} else {
|
||||
BORDER
|
||||
}))
|
||||
.bg(rgb(if chosen { PANEL_ACTIVE } else { PANEL_ALT }))
|
||||
.hover(|view| view.border_color(rgb(ACCENT)))
|
||||
.text_xs()
|
||||
.text_color(rgb(TEXT))
|
||||
.child(label)
|
||||
.child(div().truncate().text_color(rgb(MUTED)).child(detail))
|
||||
.on_click(cx.listener(move |shell, _, window, cx| {
|
||||
cx.stop_propagation();
|
||||
shell.select_shelf_choice(pane_id, index, window, cx);
|
||||
}))
|
||||
},
|
||||
),
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
.mt_2()
|
||||
.text_xs()
|
||||
.text_color(rgb(if picked.is_some() { ACCENT } else { MUTED }))
|
||||
.child(picked.map_or_else(
|
||||
|| "Pick one to see what it would need".to_owned(),
|
||||
|index| {
|
||||
format!(
|
||||
"{} · {} · nothing has run",
|
||||
choices[index].0, choices[index].2
|
||||
)
|
||||
},
|
||||
)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -1461,6 +1710,7 @@ impl LumbridgeShell {
|
||||
let pane_id = pane.id;
|
||||
div()
|
||||
.id(("workspace-panel", pane_id.get()))
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(move |shell, _, window, cx| {
|
||||
shell.select_pane(pane_id, window, cx);
|
||||
}))
|
||||
@@ -1494,7 +1744,7 @@ impl LumbridgeShell {
|
||||
.flex_none()
|
||||
.min_h_0()
|
||||
.overflow_hidden()
|
||||
.child(self.pane_decision_region(pane)),
|
||||
.child(self.pane_decision_region(pane, cx)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -1795,6 +2045,209 @@ impl LumbridgeShell {
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/// Attached panels whose agent is blocked on a human answer.
|
||||
///
|
||||
/// The count is derived rather than declared: a sidebar that claims one
|
||||
/// pane needs input while no pane is blocked is the same class of untruth
|
||||
/// as an invented usage number.
|
||||
fn attention_panels(&self) -> Vec<PanelView> {
|
||||
self.panels
|
||||
.attached_ids()
|
||||
.into_iter()
|
||||
.map(|id| self.panel_view(id))
|
||||
.filter(PanelView::needs_input)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Where sessions are owned, and what each host is actually doing.
|
||||
fn runtime_rows(&self) -> Vec<RuntimeRow> {
|
||||
let running = self
|
||||
.live_terminals
|
||||
.values()
|
||||
.filter(|terminal| matches!(terminal.status, LiveRuntimeStatus::Running { .. }))
|
||||
.count();
|
||||
let total = self.live_terminals.len();
|
||||
let local = RuntimeRow {
|
||||
label: "metal",
|
||||
detail: format!("{running}/{total} live PTYs"),
|
||||
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
|
||||
// belongs beside them: it is the reason the footer can or cannot answer.
|
||||
let adapter = RuntimeRow {
|
||||
label: "usage adapter",
|
||||
detail: format!(
|
||||
"{} · {}/{} profiles reporting",
|
||||
self.usage.probe_status(),
|
||||
self.usage.reporting_profile_count(),
|
||||
self.usage.declared_profile_count()
|
||||
),
|
||||
tone: if self.usage.reporting_profile_count() == 0 {
|
||||
MUTED
|
||||
} else {
|
||||
SUCCESS
|
||||
},
|
||||
};
|
||||
vec![local, remote, adapter]
|
||||
}
|
||||
|
||||
/// The standing of every harness, always, regardless of what is selected.
|
||||
///
|
||||
/// The old footer answered only for the selected pane, so the moment you
|
||||
/// focused a plain shell your quota vanished. This strip keeps every
|
||||
/// harness on screen and leads with what is left, because that is the
|
||||
/// question. The selected harness expands with its rate and its trust.
|
||||
fn footer_usage_zone(&self) -> impl IntoElement {
|
||||
let active = self
|
||||
.panels
|
||||
.panel(self.panels.selected())
|
||||
.and_then(|panel| self.usage.profile_for_seed(panel.seed))
|
||||
.cloned();
|
||||
let segments = self.usage.strip();
|
||||
let orphan = active.is_none();
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_4()
|
||||
.when(orphan, |view| {
|
||||
view.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_color(rgb(MUTED))
|
||||
.child("no harness on this pane"),
|
||||
)
|
||||
})
|
||||
.children(segments.into_iter().map(|segment| {
|
||||
let selected = active.as_ref() == Some(&segment.id);
|
||||
usage_segment(segment, selected)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// The provenance of a value is carried by the colour of its meter, so the
|
||||
/// strip reads at a glance without a legend on every segment.
|
||||
const fn provenance_color(provenance: UsageProvenance) -> u32 {
|
||||
match provenance {
|
||||
UsageProvenance::ProviderReported => SUCCESS,
|
||||
UsageProvenance::HarnessReported => ACCENT,
|
||||
UsageProvenance::LocallyMeasured => TEXT,
|
||||
UsageProvenance::Estimated => ATTENTION,
|
||||
UsageProvenance::Unavailable => MUTED,
|
||||
}
|
||||
}
|
||||
|
||||
/// A quota meter. The filled part is spent; the empty part is what is left,
|
||||
/// which is the way round an engineer reads it.
|
||||
fn usage_meter(consumed_permille: Option<u64>, color: u32) -> impl IntoElement {
|
||||
let Some(permille) = consumed_permille else {
|
||||
// No capsule. A full-length empty gauge reads as "plenty left" from
|
||||
// across the room, which is the opposite of the truth. A hairline is
|
||||
// visibly not a measurement.
|
||||
return div()
|
||||
.w(px(80.0))
|
||||
.h(px(2.0))
|
||||
.flex_none()
|
||||
.bg(rgb(BORDER))
|
||||
.into_any_element();
|
||||
};
|
||||
let clamped = u16::try_from(permille.min(1_000)).unwrap_or(1_000);
|
||||
div()
|
||||
.w(px(80.0))
|
||||
.h(px(8.0))
|
||||
.flex_none()
|
||||
.rounded(px(4.0))
|
||||
.bg(rgb(BORDER_QUIET))
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
div()
|
||||
.h_full()
|
||||
.w(relative(f32::from(clamped) / 1_000.0))
|
||||
.bg(rgb(color)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn usage_segment(segment: UsageSegment, selected: bool) -> impl IntoElement {
|
||||
let color = provenance_color(segment.provenance);
|
||||
let name_color = if selected { TEXT } else { MUTED };
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.when(selected, |view| {
|
||||
view.px_2().py_1().rounded(px(4.0)).bg(rgb(PANEL_ACTIVE))
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_color(rgb(name_color))
|
||||
.child(segment.label),
|
||||
)
|
||||
.child(usage_meter(segment.consumed_permille, color))
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_color(rgb(if segment.headline.is_some() {
|
||||
TEXT
|
||||
} else {
|
||||
MUTED
|
||||
}))
|
||||
.child(segment.headline.unwrap_or_else(|| "no reading".to_owned())),
|
||||
)
|
||||
.when_some(segment.reset, |view, reset| {
|
||||
view.child(div().flex_none().text_color(rgb(MUTED)).child(reset))
|
||||
})
|
||||
// Only the harness you are looking at spends footer width on its rate
|
||||
// and its trust. The rest stay one glance wide.
|
||||
.when(selected, |view| {
|
||||
view.child(footer_separator())
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_color(rgb(provenance_color(segment.burn_provenance)))
|
||||
.child(segment.burn),
|
||||
)
|
||||
.child(provenance_chip(&segment.trust, segment.provenance))
|
||||
})
|
||||
}
|
||||
|
||||
/// One host or endpoint row in the sidebar's runtime list.
|
||||
struct RuntimeRow {
|
||||
label: &'static str,
|
||||
detail: String,
|
||||
tone: u32,
|
||||
}
|
||||
|
||||
fn footer_separator() -> impl IntoElement {
|
||||
div().text_color(rgb(BORDER)).child("│")
|
||||
}
|
||||
|
||||
/// The trust marker. Its colour is the provenance, never the value.
|
||||
fn provenance_chip(text: &str, provenance: UsageProvenance) -> impl IntoElement {
|
||||
let color = match provenance {
|
||||
UsageProvenance::ProviderReported => SUCCESS,
|
||||
UsageProvenance::HarnessReported => ACCENT,
|
||||
UsageProvenance::LocallyMeasured => TEXT,
|
||||
UsageProvenance::Estimated => ATTENTION,
|
||||
UsageProvenance::Unavailable => MUTED,
|
||||
};
|
||||
div()
|
||||
.px_2()
|
||||
.py(px(1.0))
|
||||
.rounded(px(3.0))
|
||||
.border_1()
|
||||
.border_color(rgb(color))
|
||||
.text_color(rgb(color))
|
||||
.child(text.to_owned())
|
||||
}
|
||||
|
||||
impl Render for LumbridgeShell {
|
||||
@@ -1827,6 +2280,7 @@ impl Render for LumbridgeShell {
|
||||
"{running_runtime_count}/{} LIVE PTYS",
|
||||
self.live_terminals.len()
|
||||
);
|
||||
let attention_panels = self.attention_panels();
|
||||
let sidebar = div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
@@ -1841,11 +2295,30 @@ impl Render for LumbridgeShell {
|
||||
.pt_4()
|
||||
.pb_2()
|
||||
.text_xs()
|
||||
.text_color(rgb(ATTENTION))
|
||||
.child("ATTENTION · 1"),
|
||||
.text_color(rgb(if attention_panels.is_empty() {
|
||||
MUTED
|
||||
} else {
|
||||
ATTENTION
|
||||
}))
|
||||
.child(format!("ATTENTION · {}", attention_panels.len())),
|
||||
)
|
||||
.child(
|
||||
.when(attention_panels.is_empty(), |view| {
|
||||
view.child(
|
||||
div()
|
||||
.mx_2()
|
||||
.mb_3()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.text_xs()
|
||||
.text_color(rgb(MUTED))
|
||||
.child("No pane is waiting on you"),
|
||||
)
|
||||
})
|
||||
.children(attention_panels.into_iter().map(|pane| {
|
||||
let id = pane.id;
|
||||
div()
|
||||
.id(("attention-panel", id.get()))
|
||||
.cursor_pointer()
|
||||
.mx_2()
|
||||
.mb_3()
|
||||
.px_3()
|
||||
@@ -1854,20 +2327,19 @@ impl Render for LumbridgeShell {
|
||||
.bg(rgb(PANEL_ACTIVE))
|
||||
.border_1()
|
||||
.border_color(rgb(ATTENTION))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(rgb(TEXT))
|
||||
.child("Claude Code · UI"),
|
||||
)
|
||||
.hover(|view| view.bg(rgb(PANEL_ALT)))
|
||||
.child(div().text_sm().text_color(rgb(TEXT)).child(pane.title))
|
||||
.child(
|
||||
div()
|
||||
.mt_1()
|
||||
.text_xs()
|
||||
.text_color(rgb(MUTED))
|
||||
.child("Waiting for a split decision"),
|
||||
),
|
||||
)
|
||||
.child(pane.target),
|
||||
)
|
||||
.on_click(cx.listener(move |shell, _, window, cx| {
|
||||
shell.select_pane(id, window, cx);
|
||||
}))
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.px_4()
|
||||
@@ -1877,18 +2349,20 @@ impl Render for LumbridgeShell {
|
||||
.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(index == 0, |view| {
|
||||
view.bg(rgb(PANEL_ALT)).text_color(rgb(TEXT))
|
||||
})
|
||||
.when(index != 0, |view| view.text_color(rgb(MUTED)))
|
||||
.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(index == 0, |view| {
|
||||
.when(active, |view| {
|
||||
view.child(
|
||||
div()
|
||||
.mt_1()
|
||||
@@ -1897,6 +2371,9 @@ impl Render for LumbridgeShell {
|
||||
.child("main · metal"),
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(move |shell, _, window, cx| {
|
||||
shell.select_worktree(index, window, cx);
|
||||
}))
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
@@ -1907,30 +2384,13 @@ impl Render for LumbridgeShell {
|
||||
.text_color(rgb(MUTED))
|
||||
.child("RUNTIMES"),
|
||||
)
|
||||
.child(
|
||||
.children(self.runtime_rows().into_iter().map(|row| {
|
||||
div()
|
||||
.px_4()
|
||||
.py_1()
|
||||
.text_sm()
|
||||
.text_color(rgb(SUCCESS))
|
||||
.child(format!("metal · {runtime_summary}")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.px_4()
|
||||
.py_1()
|
||||
.text_sm()
|
||||
.text_color(rgb(MUTED))
|
||||
.child("amd-server · connected"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.px_4()
|
||||
.py_1()
|
||||
.text_sm()
|
||||
.text_color(rgb(MUTED))
|
||||
.child("spark-1 · sleeping"),
|
||||
)
|
||||
.child(div().text_sm().text_color(rgb(row.tone)).child(row.label))
|
||||
.child(div().text_xs().text_color(rgb(MUTED)).child(row.detail))
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.px_4()
|
||||
@@ -1959,6 +2419,7 @@ impl Render for LumbridgeShell {
|
||||
.rounded(px(4.0))
|
||||
.border_1()
|
||||
.border_color(rgb(BORDER_QUIET))
|
||||
.hover(|view| view.border_color(rgb(ACCENT)).text_color(rgb(TEXT)))
|
||||
.text_xs()
|
||||
.text_color(rgb(MUTED))
|
||||
.child(format!("↪ {title}"))
|
||||
@@ -1994,6 +2455,8 @@ impl Render for LumbridgeShell {
|
||||
.bg(rgb(PANEL))
|
||||
.border_b_1()
|
||||
.border_color(rgb(BORDER_QUIET))
|
||||
// One tab, because there is one workspace. Two more used to sit
|
||||
// here looking like navigation and doing nothing.
|
||||
.child(
|
||||
div()
|
||||
.h_full()
|
||||
@@ -2003,21 +2466,7 @@ impl Render for LumbridgeShell {
|
||||
.border_b_2()
|
||||
.border_color(rgb(ACCENT))
|
||||
.text_sm()
|
||||
.child("Agent workspace"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.px_3()
|
||||
.text_sm()
|
||||
.text_color(rgb(MUTED))
|
||||
.child("Architecture.md"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.px_3()
|
||||
.text_sm()
|
||||
.text_color(rgb(MUTED))
|
||||
.child("Review"),
|
||||
.child(WORKSPACES[self.active_worktree]),
|
||||
)
|
||||
.child(div().flex_1())
|
||||
.child(div().px_3().text_xs().text_color(rgb(MUTED)).child(format!(
|
||||
@@ -2145,7 +2594,7 @@ impl Render for LumbridgeShell {
|
||||
.ml_3()
|
||||
.text_sm()
|
||||
.text_color(rgb(MUTED))
|
||||
.child("Lumbridge Code / main"),
|
||||
.child(format!("{} / main", WORKSPACES[self.active_worktree])),
|
||||
)
|
||||
.child(div().flex_1())
|
||||
.child(
|
||||
@@ -2180,17 +2629,26 @@ impl Render for LumbridgeShell {
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.h(px(32.0))
|
||||
.h(px(APP_FOOTER_HEIGHT))
|
||||
.flex_none()
|
||||
.px_3()
|
||||
.gap_4()
|
||||
.bg(rgb(PANEL_ALT))
|
||||
.border_t_1()
|
||||
.border_color(rgb(BORDER_QUIET))
|
||||
.text_xs()
|
||||
.text_color(rgb(MUTED))
|
||||
.child(footer_left)
|
||||
.child(timing)
|
||||
.child(FOOTER_RIGHT),
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.flex_none()
|
||||
.min_w_0()
|
||||
.child(div().truncate().child(footer_left))
|
||||
.child(div().truncate().child(timing)),
|
||||
)
|
||||
.child(div().flex_1())
|
||||
.child(self.footer_usage_zone()),
|
||||
)
|
||||
.when(self.model.command_palette().is_open(), |view| {
|
||||
view.child(self.command_palette())
|
||||
|
||||
@@ -0,0 +1,555 @@
|
||||
//! Footer usage for the spike, fed by real probes through the real ledger.
|
||||
//!
|
||||
//! Profiles are *declared*; numbers are not. A profile exists as soon as the
|
||||
//! shell knows a pane is running a harness, so the footer can name the account
|
||||
//! it cannot read. Observations only ever come from a [`UsageProbe`]. A harness
|
||||
//! with no adapter yet therefore renders the honest-gap path under its own
|
||||
//! name — "Claude Code · Anthropic · … usage unavailable" — rather than
|
||||
//! borrowing another pane's number or showing a zero.
|
||||
//!
|
||||
//! The only adapter that exists today is the Codex quota probe. It is started
|
||||
//! at launch unless `LUMBRIDGE_CODEX_PROBE=0` is set, and if the `codex` binary
|
||||
//! is absent the probe simply fails to start and its profiles keep rendering as
|
||||
//! unavailable.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use lumbridge_core::{
|
||||
AccountProfile, AccountProfileId, FooterUsage, UsageLedger, UsageProjection, UsageProvenance,
|
||||
format_duration_ms,
|
||||
};
|
||||
use lumbridge_harness::claude::{ClaudeCodeProbe, ClaudeCodeProbeOptions};
|
||||
use lumbridge_harness::codex::{CodexProbe, CodexProbeOptions};
|
||||
use lumbridge_harness::{MonotonicWallClock, ProbeHealth, UsageProbe};
|
||||
|
||||
use crate::panel_registry::SeedPane;
|
||||
|
||||
/// Harnesses the seeded panes run, whether or not an adapter exists for them.
|
||||
struct DeclaredProfile {
|
||||
seed: SeedPane,
|
||||
id: &'static str,
|
||||
harness: &'static str,
|
||||
provider: &'static str,
|
||||
model: &'static str,
|
||||
account: &'static str,
|
||||
}
|
||||
|
||||
/// The Codex pane maps onto the probe's own primary-window profile, so a live
|
||||
/// reading lands under the pane that produced it. The rest are declarations
|
||||
/// with no adapter behind them yet.
|
||||
const DECLARED: &[DeclaredProfile] = &[
|
||||
DeclaredProfile {
|
||||
seed: SeedPane::CodexRuntime,
|
||||
id: "codex-app-server-primary",
|
||||
harness: "Codex",
|
||||
provider: "ChatGPT",
|
||||
model: "primary window",
|
||||
account: "subscription",
|
||||
},
|
||||
// The Claude pane maps to the five-hour window, not the token total: it is
|
||||
// the limit that actually stops work, and it is what "how much do I have
|
||||
// left" means. The transcript total is still declared by the probe and
|
||||
// still shows in the strip once it reports.
|
||||
DeclaredProfile {
|
||||
seed: SeedPane::ClaudeUi,
|
||||
id: "claude-code-five-hour",
|
||||
harness: "Claude Code",
|
||||
provider: "Anthropic",
|
||||
model: "five-hour window",
|
||||
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 {
|
||||
ledger: UsageLedger,
|
||||
clock: MonotonicWallClock,
|
||||
profiles: BTreeMap<AccountProfileId, AccountProfile>,
|
||||
seeds: Vec<(SeedPane, AccountProfileId)>,
|
||||
probes: Vec<Box<dyn UsageProbe>>,
|
||||
health: BTreeMap<AccountProfileId, ProbeHealth>,
|
||||
probe_status: String,
|
||||
}
|
||||
|
||||
impl UsageFeed {
|
||||
pub(crate) fn start() -> Self {
|
||||
let mut profiles = BTreeMap::new();
|
||||
let mut seeds = Vec::new();
|
||||
for declared in DECLARED {
|
||||
let Ok(profile) = AccountProfile::new(
|
||||
declared.id,
|
||||
declared.harness,
|
||||
declared.provider,
|
||||
declared.model,
|
||||
declared.account,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
seeds.push((declared.seed, profile.id().clone()));
|
||||
profiles.insert(profile.id().clone(), profile);
|
||||
}
|
||||
|
||||
let mut feed = Self {
|
||||
ledger: UsageLedger::new(),
|
||||
clock: MonotonicWallClock::start(),
|
||||
profiles,
|
||||
seeds,
|
||||
probes: Vec::new(),
|
||||
health: BTreeMap::new(),
|
||||
probe_status: "no usage adapter running".to_owned(),
|
||||
};
|
||||
feed.start_codex_probe();
|
||||
feed.start_claude_probe();
|
||||
feed
|
||||
}
|
||||
|
||||
/// Starts the Codex quota probe unless the user opted out.
|
||||
///
|
||||
/// 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
|
||||
/// when no adapter can run.
|
||||
fn start_codex_probe(&mut self) {
|
||||
if std::env::var_os("LUMBRIDGE_CODEX_PROBE").is_some_and(|value| value == "0") {
|
||||
self.probe_status = "codex probe disabled".to_owned();
|
||||
return;
|
||||
}
|
||||
match CodexProbe::start(CodexProbeOptions::default()) {
|
||||
Ok(probe) => {
|
||||
for profile in probe.profiles() {
|
||||
self.profiles
|
||||
.entry(profile.id().clone())
|
||||
.or_insert_with(|| profile.clone());
|
||||
self.health
|
||||
.insert(profile.id().clone(), ProbeHealth::Starting);
|
||||
}
|
||||
self.probes.push(Box::new(probe));
|
||||
self.probe_status = "codex probe starting".to_owned();
|
||||
}
|
||||
Err(error) => {
|
||||
self.probe_status = format!("codex probe unavailable · {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts the Claude Code transcript probe unless the user opted out.
|
||||
///
|
||||
/// This one reads files rather than launching anything, so it has no
|
||||
/// harness to fail to find — a missing projects directory simply reports
|
||||
/// nothing until Claude Code creates it.
|
||||
fn start_claude_probe(&mut self) {
|
||||
if std::env::var_os("LUMBRIDGE_CLAUDE_PROBE").is_some_and(|value| value == "0") {
|
||||
return;
|
||||
}
|
||||
match ClaudeCodeProbe::start(ClaudeCodeProbeOptions::default()) {
|
||||
Ok(probe) => {
|
||||
for profile in probe.profiles() {
|
||||
self.profiles
|
||||
.entry(profile.id().clone())
|
||||
.or_insert_with(|| profile.clone());
|
||||
self.health
|
||||
.insert(profile.id().clone(), ProbeHealth::Starting);
|
||||
}
|
||||
self.probes.push(Box::new(probe));
|
||||
}
|
||||
Err(error) => {
|
||||
self.probe_status = format!("claude probe unavailable · {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Which declared profile a seeded panel belongs to.
|
||||
///
|
||||
/// Panels created at runtime are plain shells with no harness, so they map
|
||||
/// to nothing rather than borrowing another pane's account.
|
||||
pub(crate) fn profile_for_seed(&self, seed: Option<SeedPane>) -> Option<&AccountProfileId> {
|
||||
let seed = seed?;
|
||||
self.seeds
|
||||
.iter()
|
||||
.find(|(candidate, _)| *candidate == seed)
|
||||
.map(|(_, id)| id)
|
||||
}
|
||||
|
||||
/// Drains every probe into the ledger. Returns whether anything changed.
|
||||
pub(crate) fn tick(&mut self) -> bool {
|
||||
let mut changed = false;
|
||||
for probe in &mut self.probes {
|
||||
let outcome = probe.poll();
|
||||
let health = outcome.health();
|
||||
for profile in probe.profiles() {
|
||||
if self.health.insert(profile.id().clone(), health) != Some(health) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
for observation in outcome.into_observations() {
|
||||
// A rejected observation is a real signal, not a crash: the
|
||||
// ledger refuses anything that would move a profile's stream
|
||||
// backwards. Dropping it preserves the append-only invariant.
|
||||
if self.ledger.record(observation).is_ok() {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
self.probe_status = self.summarize_health();
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
fn summarize_health(&self) -> String {
|
||||
if self.probes.is_empty() {
|
||||
return "no usage adapter running".to_owned();
|
||||
}
|
||||
let ready = self
|
||||
.health
|
||||
.values()
|
||||
.filter(|health| matches!(health, ProbeHealth::Ready))
|
||||
.count();
|
||||
let faulted = self
|
||||
.health
|
||||
.values()
|
||||
.filter(|health| health.is_faulted())
|
||||
.count();
|
||||
if faulted > 0 {
|
||||
return format!("{} probes · {faulted} faulted", self.probes.len());
|
||||
}
|
||||
format!("{} probes · {ready} ready", self.probes.len())
|
||||
}
|
||||
|
||||
pub(crate) fn probe_status(&self) -> &str {
|
||||
&self.probe_status
|
||||
}
|
||||
|
||||
fn projection(&self, id: &AccountProfileId) -> UsageProjection {
|
||||
self.ledger.project(id, self.clock.last_emitted_ms())
|
||||
}
|
||||
|
||||
/// Advances the feed's read clock. Kept separate from [`Self::tick`] so
|
||||
/// rendering never mutates the clock mid-frame.
|
||||
pub(crate) fn advance(&mut self) {
|
||||
let _ = self.clock.now_ms();
|
||||
}
|
||||
|
||||
/// Every harness the footer should account for, in a stable order.
|
||||
///
|
||||
/// The seeded panes come first so the strip does not reorder itself as
|
||||
/// readings arrive, then any probe profile that has actually reported. A
|
||||
/// probe profile with nothing to say — Codex's secondary window on an
|
||||
/// account that has none — is left out rather than shown as an empty rail,
|
||||
/// because the strip is for harnesses the user is running, not for every
|
||||
/// row a probe could theoretically fill.
|
||||
pub(crate) fn strip(&self) -> Vec<UsageSegment> {
|
||||
let mut ordered: Vec<&AccountProfileId> = self.seeds.iter().map(|(_, id)| id).collect();
|
||||
for id in self.profiles.keys() {
|
||||
if !ordered.contains(&id) && self.projection(id).is_available() {
|
||||
ordered.push(id);
|
||||
}
|
||||
}
|
||||
let mut segments: Vec<UsageSegment> = ordered
|
||||
.into_iter()
|
||||
.filter_map(|id| self.segment(id))
|
||||
.collect();
|
||||
// Two windows on one account would otherwise both read "CODEX". A
|
||||
// strip that names two different quotas the same thing is worse than
|
||||
// a longer label.
|
||||
let duplicated: Vec<String> = segments
|
||||
.iter()
|
||||
.filter(|segment| {
|
||||
segments
|
||||
.iter()
|
||||
.filter(|other| other.label == segment.label)
|
||||
.count()
|
||||
> 1
|
||||
})
|
||||
.map(|segment| segment.label.clone())
|
||||
.collect();
|
||||
for segment in &mut segments {
|
||||
if duplicated.contains(&segment.label) {
|
||||
segment.label = format!("{} {}", segment.label, segment.model.to_uppercase());
|
||||
}
|
||||
}
|
||||
segments
|
||||
}
|
||||
|
||||
fn segment(&self, id: &AccountProfileId) -> Option<UsageSegment> {
|
||||
let profile = self.profiles.get(id)?;
|
||||
let projection = self.projection(id);
|
||||
let footer = FooterUsage::new(profile, &projection);
|
||||
let consumed_permille = projection.consumed_permille();
|
||||
Some(UsageSegment {
|
||||
id: id.clone(),
|
||||
label: profile.harness().to_uppercase(),
|
||||
model: profile.model().to_owned(),
|
||||
consumed_permille,
|
||||
// "How much do I have left" is the question an engineer actually
|
||||
// asks. Consumption stays available in the expanded detail.
|
||||
// With a ceiling, lead with what is left. Without one, the honest
|
||||
// headline is what was spent — a profile that reports real
|
||||
// consumption but no quota must not read as "no reading".
|
||||
//
|
||||
// Show a decimal only when the value actually has one. Codex
|
||||
// reports whole percents, so "81.0% left" would claim a tenth of a
|
||||
// percent of resolution that no one measured.
|
||||
headline: consumed_permille
|
||||
.map(|permille| {
|
||||
let left = 1_000_u64.saturating_sub(permille);
|
||||
if left % 10 == 0 {
|
||||
format!("{}% left", left / 10)
|
||||
} else {
|
||||
format!("{}.{}% left", left / 10, left % 10)
|
||||
}
|
||||
})
|
||||
.or_else(|| {
|
||||
let consumed = projection.consumed()?;
|
||||
let unit = projection.unit()?;
|
||||
Some(format!("{} used", unit.format_amount(consumed)))
|
||||
}),
|
||||
// A profile that reports spend but no ceiling and no reset should
|
||||
// say so where the reset would go, rather than leaving a silent
|
||||
// gap that reads as "we just haven't shown it yet".
|
||||
reset: projection.resets_in_ms().map_or_else(
|
||||
|| {
|
||||
(projection.is_available() && projection.limit().is_none())
|
||||
.then(|| "no quota reported".to_owned())
|
||||
},
|
||||
|remaining| Some(format!("resets {}", format_duration_ms(remaining))),
|
||||
),
|
||||
burn: footer.burn(),
|
||||
burn_provenance: projection.burn_provenance(),
|
||||
trust: footer.trust(),
|
||||
provenance: projection.provenance(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Profiles that have produced at least one usable reading.
|
||||
pub(crate) fn reporting_profile_count(&self) -> usize {
|
||||
self.profiles
|
||||
.keys()
|
||||
.filter(|id| self.projection(id).is_available())
|
||||
.count()
|
||||
}
|
||||
|
||||
pub(crate) fn declared_profile_count(&self) -> usize {
|
||||
self.profiles.len()
|
||||
}
|
||||
|
||||
pub(crate) fn shutdown(&mut self) {
|
||||
for probe in &mut self.probes {
|
||||
probe.shutdown();
|
||||
}
|
||||
self.probes.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for UsageFeed {
|
||||
fn drop(&mut self) {
|
||||
self.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/// One harness's standing in the footer strip.
|
||||
///
|
||||
/// `remaining` and `reset` are `None` when there is nothing to report. That is
|
||||
/// deliberately not an empty string: the renderer has to decide what a gap
|
||||
/// looks like rather than printing a blank where a number belongs.
|
||||
pub(crate) struct UsageSegment {
|
||||
pub(crate) id: AccountProfileId,
|
||||
pub(crate) label: String,
|
||||
pub(crate) model: String,
|
||||
pub(crate) consumed_permille: Option<u64>,
|
||||
/// What to lead with: how much is left when a ceiling is known, how much
|
||||
/// was spent when it is not, and nothing at all when there is no reading.
|
||||
pub(crate) headline: Option<String>,
|
||||
pub(crate) reset: Option<String>,
|
||||
pub(crate) burn: String,
|
||||
pub(crate) burn_provenance: UsageProvenance,
|
||||
pub(crate) trust: String,
|
||||
pub(crate) provenance: UsageProvenance,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{DECLARED, UsageFeed};
|
||||
use crate::panel_registry::SeedPane;
|
||||
use lumbridge_core::UsageProvenance;
|
||||
|
||||
/// 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.
|
||||
fn declared_only_feed() -> UsageFeed {
|
||||
// SAFETY-FREE: this only sets environment variables for this process.
|
||||
unsafe {
|
||||
std::env::set_var("LUMBRIDGE_CODEX_PROBE", "0");
|
||||
std::env::set_var("LUMBRIDGE_CLAUDE_PROBE", "0");
|
||||
}
|
||||
UsageFeed::start()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_declared_harness_with_no_adapter_shows_a_gap_under_its_own_name() {
|
||||
let feed = declared_only_feed();
|
||||
let segment = feed
|
||||
.strip()
|
||||
.into_iter()
|
||||
.find(|segment| segment.label.starts_with("CLAUDE CODE"))
|
||||
.expect("Claude Code is declared");
|
||||
assert!(
|
||||
segment.headline.is_none(),
|
||||
"a harness with no adapter must not show a number"
|
||||
);
|
||||
assert_eq!(segment.trust, "no usage source");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panels_without_a_seed_have_no_profile() {
|
||||
let feed = declared_only_feed();
|
||||
assert!(feed.profile_for_seed(None).is_none());
|
||||
assert!(
|
||||
feed.profile_for_seed(Some(SeedPane::Architecture))
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nothing_reports_until_a_probe_produces_a_reading() {
|
||||
let feed = declared_only_feed();
|
||||
assert_eq!(feed.declared_profile_count(), DECLARED.len());
|
||||
assert_eq!(
|
||||
feed.reporting_profile_count(),
|
||||
0,
|
||||
"declaring a profile must not imply a reading"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_codex_pane_maps_onto_the_probe_profile_id() {
|
||||
let feed = declared_only_feed();
|
||||
let id = feed
|
||||
.profile_for_seed(Some(SeedPane::CodexRuntime))
|
||||
.expect("the Codex pane declares a profile");
|
||||
assert_eq!(
|
||||
id.as_str(),
|
||||
"codex-app-server-primary",
|
||||
"a live reading must land under the pane that produced it"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_strip_lists_every_declared_harness_even_with_no_readings() {
|
||||
let feed = declared_only_feed();
|
||||
let strip = feed.strip();
|
||||
assert_eq!(strip.len(), DECLARED.len());
|
||||
let labels: Vec<&str> = strip.iter().map(|segment| segment.label.as_str()).collect();
|
||||
assert_eq!(labels, ["CODEX", "CLAUDE CODE", "PI"]);
|
||||
for segment in &strip {
|
||||
assert!(
|
||||
segment.headline.is_none() && segment.reset.is_none(),
|
||||
"a harness with no adapter must report no figure at all"
|
||||
);
|
||||
assert!(!segment.provenance.is_available());
|
||||
assert_eq!(segment.provenance, UsageProvenance::Unavailable);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fractional_reading_keeps_its_decimal() {
|
||||
use lumbridge_core::{UsageObservation, UsageUnit, UsageWindow};
|
||||
|
||||
let mut feed = declared_only_feed();
|
||||
let id = feed
|
||||
.profile_for_seed(Some(SeedPane::CodexRuntime))
|
||||
.expect("declared")
|
||||
.clone();
|
||||
feed.advance();
|
||||
let now_ms = feed.clock.last_emitted_ms();
|
||||
feed.ledger
|
||||
.record(
|
||||
UsageObservation::counted(
|
||||
id.clone(),
|
||||
UsageUnit::WindowPermille,
|
||||
185,
|
||||
UsageProvenance::ProviderReported,
|
||||
now_ms.saturating_sub(1_000),
|
||||
)
|
||||
.expect("available")
|
||||
.with_limit(1_000)
|
||||
.expect("a positive limit")
|
||||
.with_window(UsageWindow::until(now_ms + 3_600_000))
|
||||
.expect("inside the window"),
|
||||
)
|
||||
.expect("ordered");
|
||||
let segment = feed
|
||||
.strip()
|
||||
.into_iter()
|
||||
.find(|segment| segment.id == id)
|
||||
.expect("present");
|
||||
assert_eq!(segment.headline.as_deref(), Some("81.5% left"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_segment_reports_what_is_left_not_what_was_used() {
|
||||
use lumbridge_core::{UsageObservation, UsageUnit, UsageWindow};
|
||||
|
||||
let mut feed = declared_only_feed();
|
||||
let id = feed
|
||||
.profile_for_seed(Some(SeedPane::CodexRuntime))
|
||||
.expect("the Codex pane declares a profile")
|
||||
.clone();
|
||||
// The feed reads a real wall clock, so the fixture has to sit inside a
|
||||
// window that is still open now rather than at an arbitrary epoch.
|
||||
feed.advance();
|
||||
let now_ms = feed.clock.last_emitted_ms();
|
||||
feed.ledger
|
||||
.record(
|
||||
UsageObservation::counted(
|
||||
id.clone(),
|
||||
UsageUnit::WindowPermille,
|
||||
180,
|
||||
UsageProvenance::ProviderReported,
|
||||
now_ms.saturating_sub(1_000),
|
||||
)
|
||||
.expect("available")
|
||||
.with_limit(1_000)
|
||||
.expect("a positive limit")
|
||||
.with_window(UsageWindow::until(now_ms + 3_600_000))
|
||||
.expect("inside the window"),
|
||||
)
|
||||
.expect("ordered");
|
||||
|
||||
let segment = feed
|
||||
.strip()
|
||||
.into_iter()
|
||||
.find(|segment| segment.id == id)
|
||||
.expect("the Codex segment is present");
|
||||
assert_eq!(
|
||||
segment.headline.as_deref(),
|
||||
Some("82% left"),
|
||||
"a whole-percent source must not render a tenth it never measured"
|
||||
);
|
||||
assert_eq!(segment.consumed_permille, Some(180));
|
||||
assert_eq!(
|
||||
segment.reset.as_deref(),
|
||||
Some("resets 1h 0m"),
|
||||
"a windowed reading reports its reset"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_segment_with_no_reading_offers_no_figure_to_render() {
|
||||
let feed = declared_only_feed();
|
||||
for segment in feed.strip() {
|
||||
assert!(segment.headline.is_none());
|
||||
assert!(segment.reset.is_none());
|
||||
assert!(
|
||||
!segment.burn.contains("/hr"),
|
||||
"a rate needs readings this segment does not have"
|
||||
);
|
||||
assert_eq!(segment.consumed_permille, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user