use std::sync::Arc;
use std::time::Instant;
use abstracttui::prelude::*;
use abstracttui::widgets::Bitmap;
use crate::transcript::Fold;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Phase {
Idle,
Starting,
Running,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Conn {
Unknown,
Ok,
Down(String, bool),
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Workflow {
pub bundle_id: String,
pub flow_id: String,
pub name: String,
pub description: String,
}
impl Workflow {
pub fn supports_gating(&self) -> bool {
self.bundle_id.contains("multiagent-coding") || self.flow_id.contains("multiagent-cod")
}
pub fn label(&self) -> String {
if self.name.is_empty() {
format!("{}:{}", self.bundle_id, self.flow_id)
} else {
self.name.clone()
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReasoningProbe {
pub provider: String,
pub model: String,
pub supported: Option<bool>,
pub levels: Vec<String>,
pub source: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ProviderInfo {
pub name: String,
pub models: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ToolInfo {
pub name: String,
pub description: String,
pub toolset: String,
pub tier: Option<String>,
pub approval: Option<String>,
pub risk_rank: Option<u8>,
pub served_disabled: bool,
pub enable_gate: String,
pub why_disabled: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionState {
Waiting,
Running,
Failed,
Done,
Unknown,
}
impl SessionState {
pub fn label(self) -> &'static str {
match self {
SessionState::Waiting => "waiting on you",
SessionState::Running => "running",
SessionState::Failed => "failed",
SessionState::Done => "done",
SessionState::Unknown => "—",
}
}
pub fn rank(self) -> u8 {
match self {
SessionState::Waiting => 0,
SessionState::Running => 1,
SessionState::Failed => 2,
SessionState::Done => 3,
SessionState::Unknown => 4,
}
}
pub fn is_live(self) -> bool {
matches!(self, SessionState::Waiting | SessionState::Running)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionRow {
pub id: String,
pub state: SessionState,
pub last_at: String,
pub turns: usize,
pub first_run: String,
pub prompt: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SessionIndex {
Unfetched,
Loading,
Loaded {
rows: Vec<SessionRow>,
truncated: bool,
labeled: usize,
},
Failed(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QuitVerb {
Pause,
Cancel,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum QuitState {
#[default]
None,
Choosing,
Delivering {
verb: QuitVerb,
run_id: String,
gen: u64,
},
Acked {
verb: QuitVerb,
run_id: String,
},
Failed {
verb: QuitVerb,
run_id: String,
definitive: bool,
error: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerbAck {
pub verb: QuitVerb,
pub run_id: String,
pub ok: bool,
pub definitive: bool,
pub error: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PendingAttachment {
pub path: String,
pub name: String,
pub size: u64,
pub uploaded: Option<(String, serde_json::Value)>,
}
impl From<&ToolInfo> for crate::tool_policy::ToolClass {
fn from(t: &ToolInfo) -> Self {
crate::tool_policy::ToolClass {
name: t.name.clone(),
approval: t.approval.clone(),
tier: t.tier.clone(),
served_disabled: t.served_disabled,
enable_gate: t.enable_gate.clone(),
risk_rank: t.risk_rank,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SkillInfo {
pub name: String,
pub description: String,
pub trust: String,
pub blocked: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct McpServer {
pub name: String,
pub url: String,
pub description: String,
pub auth_required: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CacheInfo {
pub provider: String,
pub model: String,
pub supported: bool,
pub mode: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct GpuSample {
pub util_pct: f64,
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub enum GpuMeter {
#[default]
Off,
Pending,
Ready(GpuSample),
Unsupported(String),
Error(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct HostContracts {
pub model_residency: bool,
pub host_state: bool,
pub session_caches: bool,
pub modality_labels: Vec<(String, String)>,
}
impl HostContracts {
pub fn label_for<'a>(&'a self, task: &'a str) -> &'a str {
self.modality_labels
.iter()
.find(|(t, _)| t == task)
.map(|(_, l)| l.as_str())
.unwrap_or(task)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ResidencyRow {
pub runtime_id: String,
pub task: String,
pub provider: String,
pub model: String,
pub source: String,
pub resident: Option<bool>,
pub state: String,
pub locked: bool,
pub lockable: Option<bool>,
pub modalities: Vec<String>,
pub size_bytes: Option<u64>,
pub size_vram_bytes: Option<u64>,
pub est_weights_bytes: Option<u64>,
pub cache_bytes: Option<u64>,
pub context_length: Option<u64>,
pub calibrated_context_length: Option<u64>,
pub context_calibrated: bool,
pub is_default: bool,
pub loaded_at: String,
pub last_used_at: String,
}
impl ResidencyRow {
pub fn display_size(&self) -> Option<(u64, bool)> {
self.display_size_source().map(|(b, est, _)| (b, est))
}
pub fn display_size_source(&self) -> Option<(u64, bool, &'static str)> {
self.size_bytes
.map(|b| (b, false, "reported by the model server (size_bytes)"))
.or(self
.size_vram_bytes
.map(|b| (b, false, "reported by the model server (size_vram_bytes)")))
.or(self
.est_weights_bytes
.map(|b| (b, true, "estimated on-disk weight size (est_weights_bytes)")))
}
pub fn is_resident(&self) -> bool {
self.resident == Some(true)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LockAction {
Unlock,
Lock {
adopt: bool,
},
Refused(&'static str),
}
pub fn lock_action(r: &ResidencyRow) -> LockAction {
if r.locked {
return LockAction::Unlock;
}
if !r.is_resident() {
return LockAction::Refused(match r.resident {
Some(false) => "not resident — only a loaded model can be locked",
_ => "residency unknown — r refreshes before locking",
});
}
if r.lockable == Some(false) {
return LockAction::Refused("this gateway reports the model as not lockable");
}
LockAction::Lock {
adopt: r.source == "provider_server",
}
}
pub fn unload_refusal(r: &ResidencyRow) -> Option<&'static str> {
match r.resident {
Some(false) => Some("not resident — there is nothing to unload"),
_ => None,
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SessionCacheRow {
pub key: String,
pub provider: String,
pub model: String,
pub session_id: String,
pub bytes: Option<u64>,
pub token_count: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct HostFacts {
pub ts: String,
pub host_name: String,
pub ram_total: Option<u64>,
pub ram_used: Option<u64>,
pub ram_available: Option<u64>,
pub ram_percent: Option<f64>,
pub process_rss: Option<u64>,
pub device_backend: String,
pub device_allocated: Option<u64>,
pub device_total: Option<u64>,
pub device_free: Option<u64>,
pub device_host_in_use: Option<u64>,
pub device_wired_limit: Option<u64>,
pub gpu_supported: bool,
pub gpu_util_pct: Option<f64>,
pub models: Vec<ResidencyRow>,
pub caches: Vec<SessionCacheRow>,
pub totals: Vec<(String, u64)>,
pub degraded: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeviceScope {
AllProcesses,
Process,
}
impl DeviceScope {
pub fn label(self) -> &'static str {
match self {
DeviceScope::AllProcesses => "all processes",
DeviceScope::Process => "this process only",
}
}
}
impl HostFacts {
pub fn accelerator_figure(&self) -> Option<(u64, Option<u64>, DeviceScope)> {
let ceiling = |c: Option<u64>| c.filter(|t| *t > 0);
if let Some(used) = self.device_host_in_use {
return Some((
used,
ceiling(self.device_wired_limit).or_else(|| ceiling(self.device_total)),
DeviceScope::AllProcesses,
));
}
self.device_allocated
.map(|used| (used, ceiling(self.device_total), DeviceScope::Process))
}
pub fn device_meter(&self) -> Option<(u64, u64, DeviceScope)> {
self.accelerator_figure()
.and_then(|(used, ceiling, scope)| ceiling.map(|t| (used, t, scope)))
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub enum HostState {
#[default]
Idle,
Pending,
Ready(HostFacts),
Stale(HostFacts),
Unsupported(String),
Error(String),
}
pub const IMAGE_ENTRY_CAP: usize = 32;
#[derive(Clone)]
pub struct ImageEntry {
pub artifact_id: String,
pub bitmap: Option<Arc<Bitmap>>,
pub error: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueuedPrompt {
pub id: u64,
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PendingSteer {
pub armed_at_root: String,
pub armed_while_starting: bool,
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct GoalState {
pub text: String,
pub run_id: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RunOutcome {
#[default]
None,
Success,
StoppedShort,
Failed,
Cancelled,
}
impl RunOutcome {
pub fn for_conclusion(failed: bool, cancelled: bool, stopped_short: bool) -> Self {
if failed {
RunOutcome::Failed
} else if cancelled {
RunOutcome::Cancelled
} else if stopped_short {
RunOutcome::StoppedShort
} else {
RunOutcome::Success
}
}
pub fn holds_the_queue(self) -> bool {
matches!(
self,
RunOutcome::Failed | RunOutcome::Cancelled | RunOutcome::StoppedShort
)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct SessionTotals {
pub input_tokens: u64,
pub output_tokens: u64,
pub total_tokens: u64,
pub runs: u64,
}
#[derive(Clone, Copy)]
pub struct Store {
pub fold: Signal<Fold>,
pub phase: Signal<Phase>,
pub conn: Signal<Conn>,
pub session_id: Signal<String>,
pub run_id: Signal<String>,
pub workflow: Signal<Workflow>,
pub workflows: Signal<Vec<Workflow>>,
pub provider: Signal<String>,
pub model: Signal<String>,
pub gating_mode: Signal<String>,
pub review_mode: Signal<bool>,
pub review_rounds: Signal<u32>,
pub max_iterations: Signal<u32>,
pub reasoning: Signal<String>,
pub reasoning_probe: Signal<Option<ReasoningProbe>>,
pub providers: Signal<Vec<ProviderInfo>>,
pub tools: Signal<Vec<ToolInfo>>,
pub tools_error: Signal<String>,
pub disabled_tools: Signal<Vec<String>>,
pub camera_seed_pending: Signal<bool>,
pub skills_catalog: Signal<Vec<SkillInfo>>,
pub skills_error: Signal<String>,
pub selected_skills: Signal<Vec<String>>,
pub mcp_servers: Signal<Vec<McpServer>>,
pub mcp_note: Signal<String>,
pub cache: Signal<Option<CacheInfo>>,
pub gpu: Signal<GpuMeter>,
pub host_state: Signal<HostState>,
pub host_contracts: Signal<Option<HostContracts>>,
pub host_estimate: Signal<Option<(String, String)>>,
pub context_window: Signal<u64>,
pub last_call_rate: Signal<Option<f64>>,
pub default_route: Signal<(String, String)>,
pub images: Signal<Vec<ImageEntry>>,
pub totals: Signal<SessionTotals>,
pub run_started: Signal<Option<Instant>>,
pub elapsed_secs: Signal<u64>,
pub notices: Signal<Vec<String>>,
pub last_esc: Signal<Option<Instant>>,
pub animation: Signal<u8>,
pub show_details: Signal<bool>,
pub expanded_tools: Signal<Vec<String>>,
pub session_index: Signal<SessionIndex>,
pub paused: Signal<bool>,
pub pending_attachments: Signal<Vec<PendingAttachment>>,
pub max_attachment_bytes: Signal<u64>,
pub quit_state: Signal<QuitState>,
pub verb_ack: Signal<Option<VerbAck>>,
pub run_status_probe: Signal<Option<(String, String)>>,
pub history_loading: Signal<bool>,
pub history_cursor: Signal<Option<String>>,
pub older_turns: Signal<usize>,
pub restoring: Signal<bool>,
pub restore_failed: Signal<Option<String>>,
pub restore_progress: Signal<Option<(usize, usize)>>,
pub paste_undo: Signal<Option<(String, Vec<String>)>>,
pub preview: Signal<Option<crate::preview::PreviewState>>,
pub preview_seq: Signal<u64>,
pub accepted_tier: Signal<String>,
pub tool_overrides: Signal<Vec<(String, String)>>,
pub workspace_mode: Signal<String>,
pub workspace_allowed: Signal<Vec<String>>,
pub queue: Signal<Vec<QueuedPrompt>>,
pub queue_paused: Signal<bool>,
pub pending_steer: Signal<Option<PendingSteer>>,
pub last_outcome: Signal<RunOutcome>,
pub queue_next_id: Signal<u64>,
pub composer_seed: Signal<Option<String>>,
pub steer_restore: Signal<Option<String>>,
pub goal: Signal<Option<GoalState>>,
pub goal_workflows: Signal<Vec<Workflow>>,
pub focus: Signal<crate::convo::Focus>,
pub convos: Signal<Vec<crate::convo::EntityConvo>>,
pub entities: Signal<Vec<crate::entities::EntityInfo>>,
pub entities_as_of: Signal<String>,
pub entities_loading: Signal<bool>,
pub entities_error: Signal<String>,
pub entity_cards: Signal<Vec<(String, crate::entities::EntityCard)>>,
pub mcp_info: Signal<crate::entities::McpRegistryInfo>,
}
impl Store {
pub fn create(cx: Scope) -> Store {
Store {
fold: cx.signal(Fold::new()),
phase: cx.signal(Phase::Idle),
animation: cx.signal(0u8),
conn: cx.signal(Conn::Unknown),
session_id: cx.signal(String::new()),
run_id: cx.signal(String::new()),
workflow: cx.signal(Workflow::default()),
workflows: cx.signal(Vec::new()),
provider: cx.signal(String::new()),
model: cx.signal(String::new()),
gating_mode: cx.signal(String::new()),
review_mode: cx.signal(crate::cli::DEFAULT_REVIEW_MODE),
review_rounds: cx.signal(crate::cli::DEFAULT_REVIEW_ROUNDS),
max_iterations: cx.signal(0u32),
reasoning: cx.signal(String::new()),
reasoning_probe: cx.signal(None),
providers: cx.signal(Vec::new()),
tools: cx.signal(Vec::new()),
tools_error: cx.signal(String::new()),
disabled_tools: cx.signal(Vec::new()),
camera_seed_pending: cx.signal(false),
skills_catalog: cx.signal(Vec::new()),
skills_error: cx.signal(String::new()),
selected_skills: cx.signal(Vec::new()),
mcp_servers: cx.signal(Vec::new()),
mcp_note: cx.signal(String::new()),
cache: cx.signal(None),
gpu: cx.signal(GpuMeter::Off),
host_state: cx.signal(HostState::Idle),
host_contracts: cx.signal(None),
host_estimate: cx.signal(None),
context_window: cx.signal(0),
last_call_rate: cx.signal(None),
default_route: cx.signal((String::new(), String::new())),
images: cx.signal(Vec::new()),
totals: cx.signal(SessionTotals::default()),
run_started: cx.signal(None),
elapsed_secs: cx.signal(0),
notices: cx.signal(Vec::new()),
last_esc: cx.signal(None),
show_details: cx.signal(false),
expanded_tools: cx.signal(Vec::new()),
session_index: cx.signal(SessionIndex::Unfetched),
paused: cx.signal(false),
quit_state: cx.signal(QuitState::None),
verb_ack: cx.signal(None),
run_status_probe: cx.signal(None),
history_loading: cx.signal(false),
history_cursor: cx.signal(None),
older_turns: cx.signal(0),
restoring: cx.signal(false),
restore_failed: cx.signal(None),
restore_progress: cx.signal(None),
pending_attachments: cx.signal(Vec::new()),
max_attachment_bytes: cx.signal(0),
paste_undo: cx.signal(None),
preview: cx.signal(None),
preview_seq: cx.signal(0),
accepted_tier: cx.signal(String::new()),
tool_overrides: cx.signal(Vec::new()),
workspace_mode: cx.signal(String::new()),
workspace_allowed: cx.signal(Vec::new()),
queue: cx.signal(Vec::new()),
queue_paused: cx.signal(false),
pending_steer: cx.signal(None),
last_outcome: cx.signal(RunOutcome::None),
queue_next_id: cx.signal(1),
composer_seed: cx.signal(None),
steer_restore: cx.signal(None),
goal: cx.signal(None),
goal_workflows: cx.signal(Vec::new()),
focus: cx.signal(crate::convo::Focus::Agent),
convos: cx.signal(Vec::new()),
entities: cx.signal(Vec::new()),
entities_as_of: cx.signal(String::new()),
entities_loading: cx.signal(false),
entities_error: cx.signal(String::new()),
entity_cards: cx.signal(Vec::new()),
mcp_info: cx.signal(Default::default()),
}
}
pub fn notify(&self, text: impl Into<String>) {
let text = text.into();
self.notices.update(|n| n.push(text));
}
pub fn mint_queue_id(&self) -> u64 {
let id = self.queue_next_id.get_untracked();
self.queue_next_id.set(id.wrapping_add(1));
id
}
pub fn reset_steer_lane(&self) -> Option<PendingSteer> {
let dropped = self.pending_steer.get_untracked();
self.pending_steer.set(None);
self.last_outcome.set(RunOutcome::None);
dropped
}
pub fn effective_user_disabled(inventory: &[ToolInfo], disabled: &[String]) -> usize {
disabled
.iter()
.filter(|d| {
inventory
.iter()
.any(|t| t.name == **d && !t.served_disabled)
})
.count()
}
pub fn grantable_tool_names(&self) -> Vec<String> {
let disabled = self.disabled_tools.get_untracked();
self.tools.with_untracked(|inv| {
inv.iter()
.filter(|t| !t.served_disabled)
.map(|t| t.name.clone())
.filter(|n| !disabled.contains(n))
.collect()
})
}
pub fn tool_classes(&self) -> Vec<crate::tool_policy::ToolClass> {
self.tools.with_untracked(|inv| {
inv.iter()
.map(crate::tool_policy::ToolClass::from)
.collect()
})
}
pub fn image_for(&self, artifact_id: &str) -> Option<ImageEntry> {
self.images
.with(|imgs| imgs.iter().find(|e| e.artifact_id == artifact_id).cloned())
}
pub fn upsert_image(&self, entry: ImageEntry) {
self.images.update(|imgs| {
match imgs.iter_mut().find(|e| e.artifact_id == entry.artifact_id) {
Some(slot) => {
if entry.bitmap.is_some() || slot.bitmap.is_none() {
*slot = entry;
}
}
None => imgs.push(entry),
}
if imgs.len() > IMAGE_ENTRY_CAP {
let overflow = imgs.len() - IMAGE_ENTRY_CAP;
imgs.drain(..overflow);
}
});
}
}
#[cfg(test)]
mod tests {
#[test]
fn only_a_finished_turn_releases_the_queue() {
use super::RunOutcome as O;
assert_eq!(O::for_conclusion(false, false, false), O::Success);
assert_eq!(O::for_conclusion(false, false, true), O::StoppedShort);
assert_eq!(O::for_conclusion(true, false, false), O::Failed);
assert_eq!(O::for_conclusion(false, true, false), O::Cancelled);
assert_eq!(O::for_conclusion(true, true, true), O::Failed);
assert!(!O::Success.holds_the_queue());
assert!(O::StoppedShort.holds_the_queue());
assert!(O::Failed.holds_the_queue());
assert!(O::Cancelled.holds_the_queue());
}
use super::*;
use abstracttui::widgets::Bitmap;
#[test]
fn queue_lane_mints_ids_and_steer_reset_leaves_the_queue_alone() {
let (root, ()) = abstracttui::reactive::create_root(|cx| {
let store = Store::create(cx);
let a = store.mint_queue_id();
let b = store.mint_queue_id();
assert_ne!(a, b, "ids are unique");
store.queue.update(|q| {
q.push(QueuedPrompt {
id: a,
text: "one".into(),
});
q.push(QueuedPrompt {
id: b,
text: "two".into(),
});
});
store.queue_paused.set(true);
store.pending_steer.set(Some(PendingSteer {
armed_at_root: "r1".into(),
armed_while_starting: true,
text: "buffered".into(),
}));
store.last_outcome.set(RunOutcome::Failed);
let dropped = store.reset_steer_lane();
assert_eq!(dropped.map(|p| p.text).as_deref(), Some("buffered"));
assert_eq!(
store.queue.with_untracked(|q| q.len()),
2,
"the queue survives a steer-lane reset"
);
assert!(store.queue_paused.get_untracked(), "pause flag untouched");
assert!(store.pending_steer.get_untracked().is_none());
assert_eq!(store.last_outcome.get_untracked(), RunOutcome::None);
assert!(store.mint_queue_id() > b);
});
root.dispose();
}
#[test]
fn image_upsert_replaces_by_artifact_id_and_keeps_good_bitmaps() {
let (root, ()) = abstracttui::reactive::create_root(|cx| {
let store = Store::create(cx);
let entry = |id: &str, bitmap: Option<Arc<Bitmap>>, error: &str| ImageEntry {
artifact_id: id.into(),
bitmap,
error: error.into(),
};
let bitmap = || {
Some(Arc::new(Bitmap::new(
1,
1,
abstracttui::prelude::Rgba::BLACK,
)))
};
store.upsert_image(entry("a1", None, "image fetch failed: timeout"));
store.upsert_image(entry("a2", None, ""));
store.upsert_image(entry("a1", bitmap(), ""));
assert_eq!(
store.images.with_untracked(|v| v.len()),
2,
"upsert never grows"
);
let a1 = store.image_for("a1").expect("entry exists");
assert!(a1.bitmap.is_some(), "success replaced the error entry");
assert!(a1.error.is_empty());
store.upsert_image(entry("a1", None, "image fetch failed: 503"));
let a1 = store.image_for("a1").expect("entry exists");
assert!(
a1.bitmap.is_some(),
"a good bitmap survives a transient re-fetch error"
);
assert_eq!(store.images.with_untracked(|v| v.len()), 2);
store.upsert_image(entry("a1", bitmap(), ""));
assert_eq!(store.images.with_untracked(|v| v.len()), 2);
store.upsert_image(entry("a3", None, "decode failed"));
let a3 = store.image_for("a3").expect("entry exists");
assert!(a3.bitmap.is_none());
assert_eq!(a3.error, "decode failed");
});
root.dispose();
}
#[test]
fn image_list_is_capped_evicting_oldest_first() {
let (root, ()) = abstracttui::reactive::create_root(|cx| {
let store = Store::create(cx);
for i in 0..(IMAGE_ENTRY_CAP + 8) {
store.upsert_image(ImageEntry {
artifact_id: format!("art-{i}"),
bitmap: None,
error: String::new(),
});
}
assert_eq!(
store.images.with_untracked(|v| v.len()),
IMAGE_ENTRY_CAP,
"the list never exceeds the cap"
);
assert!(
store.image_for("art-0").is_none(),
"the oldest entry evicted"
);
assert!(
store
.image_for(&format!("art-{}", IMAGE_ENTRY_CAP + 7))
.is_some(),
"the newest entry survives"
);
store.upsert_image(ImageEntry {
artifact_id: format!("art-{}", IMAGE_ENTRY_CAP + 7),
bitmap: None,
error: "retry".into(),
});
assert_eq!(store.images.with_untracked(|v| v.len()), IMAGE_ENTRY_CAP);
});
root.dispose();
}
#[test]
fn gpu_meter_defaults_off() {
let (root, ()) = abstracttui::reactive::create_root(|cx| {
let store = Store::create(cx);
assert_eq!(store.gpu.get_untracked(), GpuMeter::Off);
});
root.dispose();
}
}