mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-13 04:29:24 -10:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d4920057b | ||
|
|
622a493078 |
No files matched your search
Generated
-34
@@ -907,7 +907,6 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"toml 1.1.6+spec-1.1.0",
|
||||
"webbrowser",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -1150,7 +1149,6 @@ dependencies = [
|
||||
"sha2 0.11.1",
|
||||
"tempfile",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -1221,7 +1219,6 @@ dependencies = [
|
||||
"codewhale-util",
|
||||
"codewhale-workflow",
|
||||
"codewhale-workflow-js",
|
||||
"cucumber",
|
||||
"dirs 7.0.0",
|
||||
"encoding_rs",
|
||||
"fd-lock",
|
||||
@@ -1240,12 +1237,10 @@ dependencies = [
|
||||
"objc2-foundation",
|
||||
"parking_lot",
|
||||
"portable-pty",
|
||||
"pretty_assertions",
|
||||
"proc-macro2",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"ring",
|
||||
"rio-vt",
|
||||
"rmcp",
|
||||
"rusqlite",
|
||||
"rustls",
|
||||
@@ -1261,9 +1256,7 @@ dependencies = [
|
||||
"tar",
|
||||
"tempfile",
|
||||
"thiserror 2.0.21",
|
||||
"tiny_http",
|
||||
"tokio",
|
||||
"tokio-tungstenite 0.30.0",
|
||||
"tokio-util",
|
||||
"toml 1.1.6+spec-1.1.0",
|
||||
"toml_edit 0.25.15+spec-1.1.0",
|
||||
@@ -1616,7 +1609,6 @@ dependencies = [
|
||||
"colored",
|
||||
"dirs 7.0.0",
|
||||
"fd-lock",
|
||||
"futures-util",
|
||||
"ignore",
|
||||
"jsonwebtoken",
|
||||
"libc",
|
||||
@@ -1878,15 +1870,12 @@ dependencies = [
|
||||
"objc2-foundation",
|
||||
"parking_lot",
|
||||
"portable-pty",
|
||||
"pretty_assertions",
|
||||
"ratatui",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"rio-vt",
|
||||
"rquickjs",
|
||||
"rusqlite",
|
||||
"rustls",
|
||||
"schemars 1.2.2",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1896,7 +1885,6 @@ dependencies = [
|
||||
"tempfile",
|
||||
"tiny_http",
|
||||
"tokio",
|
||||
"tokio-tungstenite 0.30.0",
|
||||
"tokio-util",
|
||||
"toml 1.1.6+spec-1.1.0",
|
||||
"toml_edit 0.25.15+spec-1.1.0",
|
||||
@@ -2472,12 +2460,6 @@ dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -5679,16 +5661,6 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||
|
||||
[[package]]
|
||||
name = "pretty_assertions"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
|
||||
dependencies = [
|
||||
"diff",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
@@ -8843,12 +8815,6 @@ dependencies = [
|
||||
"hashlink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.3"
|
||||
|
||||
@@ -23,6 +23,5 @@ reqwest = { workspace = true, features = ["blocking"] }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tempfile.workspace = true
|
||||
toml.workspace = true
|
||||
webbrowser = "1.0"
|
||||
zeroize = "1.8.2"
|
||||
@@ -180,28 +180,6 @@ impl ModelRegistry {
|
||||
self.models.clone()
|
||||
}
|
||||
|
||||
/// Returns whether a selector is known only outside the selected provider.
|
||||
///
|
||||
/// This is rejection metadata, never route authority: callers may use it
|
||||
/// to reject a clearly foreign model, but must not use the matching row to
|
||||
/// select a provider or credential slot.
|
||||
#[must_use]
|
||||
pub fn is_known_for_other_provider(
|
||||
&self,
|
||||
requested: &str,
|
||||
selected_provider: ProviderKind,
|
||||
) -> bool {
|
||||
let known_here = self
|
||||
.models
|
||||
.iter()
|
||||
.any(|model| model.provider == selected_provider && model_matches(model, requested));
|
||||
!known_here
|
||||
&& self
|
||||
.models
|
||||
.iter()
|
||||
.any(|model| model.provider != selected_provider && model_matches(model, requested))
|
||||
}
|
||||
|
||||
/// Resolves a user-requested model name to a concrete [`ModelInfo`].
|
||||
///
|
||||
/// Resolution follows this priority order:
|
||||
|
||||
@@ -434,19 +434,6 @@ impl ConfigToml {
|
||||
}
|
||||
}
|
||||
|
||||
/// The unquoted contents of an extras key that holds a TOML string.
|
||||
///
|
||||
/// [`ConfigToml::get_value`] renders extras through `toml::Value::to_string`,
|
||||
/// which re-applies TOML quoting — and switches to a single-quoted literal
|
||||
/// string whenever the payload contains a `"`. A JSON blob written with
|
||||
/// [`ConfigToml::set_value`] therefore comes back as `'[{"a":1}]'` and no
|
||||
/// longer parses as JSON (#4727). Callers that stored structured text want
|
||||
/// the payload, not its TOML rendering.
|
||||
#[must_use]
|
||||
pub fn get_raw_string(&self, key: &str) -> Option<&str> {
|
||||
self.extras.get(key).and_then(toml::Value::as_str)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_display_value(&self, key: &str) -> Option<String> {
|
||||
if let Some(alias) = self.root_alias_key(key) {
|
||||
|
||||
@@ -22,10 +22,6 @@ use crate::route::{
|
||||
WireModelId,
|
||||
};
|
||||
|
||||
/// Provider catalog endpoint used by Models.dev.
|
||||
pub const MODELS_DEV_API_URL: &str = "https://models.dev/api.json";
|
||||
/// Provider-agnostic model metadata endpoint used by Models.dev.
|
||||
pub const MODELS_DEV_MODELS_URL: &str = "https://models.dev/models.json";
|
||||
/// Combined `{ models, providers }` endpoint used by Models.dev.
|
||||
pub const MODELS_DEV_CATALOG_URL: &str = "https://models.dev/catalog.json";
|
||||
|
||||
|
||||
@@ -522,20 +522,6 @@ impl OfferingPricing {
|
||||
None
|
||||
}
|
||||
|
||||
/// Per-million price for one canonical class, when published.
|
||||
#[must_use]
|
||||
pub fn price_per_million(&self, class: TokenClass) -> Option<f64> {
|
||||
match class {
|
||||
TokenClass::Input => self.input_per_million,
|
||||
TokenClass::Output => self.output_per_million,
|
||||
TokenClass::CacheRead => self.cache_read_per_million,
|
||||
TokenClass::CacheWrite => self.cache_write_per_million,
|
||||
TokenClass::Reasoning => self.reasoning_per_million,
|
||||
TokenClass::InputAudio => self.input_audio_per_million,
|
||||
TokenClass::OutputAudio => self.output_audio_per_million,
|
||||
}
|
||||
}
|
||||
|
||||
/// Classes this turn actually used that carry no published price.
|
||||
///
|
||||
/// Non-empty means [`Self::estimate_cost`] fails closed for this usage; the
|
||||
|
||||
@@ -24,4 +24,3 @@ serde_json = { workspace = true, features = ["preserve_order"] }
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
uuid.workspace = true
|
||||
tokio = { workspace = true, features = ["macros", "rt", "time"] }
|
||||
@@ -60,10 +60,6 @@ impl Thread {
|
||||
pub fn leaf_id(&self) -> Option<&str> {
|
||||
self.leaf_id.as_deref()
|
||||
}
|
||||
|
||||
pub fn set_leaf(&mut self, leaf: Option<String>) {
|
||||
self.leaf_id = leaf;
|
||||
}
|
||||
}
|
||||
|
||||
/// Ephemeral session within a thread (one engine lifetime / one turn's
|
||||
|
||||
@@ -203,14 +203,9 @@ codewhale-util = { path = "../util", version = "0.10.3", features = ["test-suppo
|
||||
codewhale-models = { path = "../models", version = "0.10.3", features = [
|
||||
"test-support",
|
||||
] }
|
||||
cucumber = "0.23.0"
|
||||
wiremock = "0.6"
|
||||
tiny_http = "0.12"
|
||||
pretty_assertions = "1.4"
|
||||
rio-vt = "0.5.1"
|
||||
rusqlite.workspace = true
|
||||
schemars = { version = "1.2.1", features = ["derive", "preserve_order"] }
|
||||
tokio-tungstenite = "0.30"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
@@ -1363,36 +1363,6 @@ fn auto_route_usage_has_reported_data(usage: &codewhale_models::Usage) -> bool {
|
||||
|| usage.server_tool_use.is_some()
|
||||
}
|
||||
|
||||
/// Stable, persistence-safe identity for one classifier response. The raw
|
||||
/// provider response id is hashed with the frozen dispatch route and instant;
|
||||
/// neither it nor any custom route label crosses into telemetry/persistence.
|
||||
pub fn auto_route_usage_source_id(route: &EffectiveRouteEnvelope, response_id: &str) -> String {
|
||||
use sha2::{Digest as _, Sha256};
|
||||
|
||||
let mut digest = Sha256::new();
|
||||
let dispatched_at = route.dispatched_at.to_rfc3339();
|
||||
for part in [
|
||||
b"codewhale:auto-route-classifier:v1".as_slice(),
|
||||
route.provider.as_str().as_bytes(),
|
||||
route.provider_identity.as_bytes(),
|
||||
route.model.as_bytes(),
|
||||
route
|
||||
.endpoint_fingerprint
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.as_bytes(),
|
||||
dispatched_at.as_bytes(),
|
||||
response_id.as_bytes(),
|
||||
] {
|
||||
digest.update((part.len() as u64).to_le_bytes());
|
||||
digest.update(part);
|
||||
}
|
||||
format!(
|
||||
"auto-router:{}",
|
||||
codewhale_runtime::hashing::hex_bytes(digest.finalize())
|
||||
)
|
||||
}
|
||||
|
||||
fn auto_route_attempt_from_response(
|
||||
_request_route: EffectiveRouteEnvelope,
|
||||
response: &MessageResponse,
|
||||
|
||||
@@ -135,8 +135,7 @@ pub const MATRIX_BORDER_RGB: (u8, u8, u8) = (0, 204, 0); // #00CC00
|
||||
// whale's ivory ink on both sides. This is the charcoal alternative to the
|
||||
// terminal's navy "Underwater" default in 0.10.0. The
|
||||
// 0.10.0 action pair uses glacial blue on charcoal and deep ocean blue on
|
||||
// paper. The GPUI desktop does not paint these values: its theme is the
|
||||
// separate `GPUI_*` / `GPUI_LIGHT_*` set below.
|
||||
// paper.
|
||||
//
|
||||
// Every pair audited by `contrast::theme_contrast_violations` clears its
|
||||
// floor: body roles clear 4.5:1 on all four surfaces, hint/dim and the
|
||||
|
||||
@@ -91,16 +91,6 @@ pub fn codewhale_home_is_explicit() -> bool {
|
||||
path_env("CODEWHALE_HOME").is_some()
|
||||
}
|
||||
|
||||
/// Return the legacy `DEEPSEEK_HOME` compatibility override, if configured.
|
||||
///
|
||||
/// New state must use [`codewhale_home`]. This resolver exists only for readers
|
||||
/// whose persisted format still explicitly supports the legacy environment
|
||||
/// alias.
|
||||
#[must_use]
|
||||
pub fn legacy_deepseek_home_override() -> Option<PathBuf> {
|
||||
path_env("DEEPSEEK_HOME")
|
||||
}
|
||||
|
||||
/// Resolve the user's platform home, preferring `HOME` before `USERPROFILE`.
|
||||
///
|
||||
/// The explicit environment order makes CLI, state, config, and secret paths
|
||||
|
||||
@@ -86,7 +86,6 @@ use codewhale_skill_install::install::{
|
||||
pub mod dsh;
|
||||
mod place;
|
||||
mod stage;
|
||||
mod summary;
|
||||
mod tarball;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -94,7 +93,6 @@ mod tests;
|
||||
|
||||
use place::{ensure_target_within_plugins_dir, finalize_install, plugin_target_path};
|
||||
use stage::stage_local_copy;
|
||||
pub use summary::{PluginSummary, PreviousInstall, SummaryClause, SummaryInput, build_summary};
|
||||
use tarball::stage_tarball;
|
||||
|
||||
/// Default per-bundle size cap. Mirrors the skill installer; the runtime
|
||||
|
||||
@@ -1,865 +0,0 @@
|
||||
//! One-line install summary shared by every install surface.
|
||||
//!
|
||||
//! A pure builder: everything it needs arrives in [`SummaryInput`], nothing is
|
||||
//! read from disk, the registry or the environment. Each clause carries a
|
||||
//! stable message id so a surface can localize the clause while the English
|
||||
//! text here stays the fallback.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use super::dsh::DshConversion;
|
||||
use crate::host_services::{HostService, HostServiceReview, PluginApprover, approver_label};
|
||||
use crate::manifest::PluginInventory;
|
||||
|
||||
pub const MSG_HEAD: &str = "plugin.summary.head";
|
||||
pub const MSG_STATE_ON: &str = "plugin.summary.state_on";
|
||||
pub const MSG_STATE_OFF: &str = "plugin.summary.state_off";
|
||||
pub const MSG_APPROVER: &str = "plugin.summary.approver";
|
||||
pub const MSG_ADDS: &str = "plugin.summary.adds";
|
||||
pub const MSG_ADDS_NOTHING_NEW: &str = "plugin.summary.adds_nothing_new";
|
||||
pub const MSG_NOW_ALSO_ADDS: &str = "plugin.summary.now_also_adds";
|
||||
pub const MSG_WONT_RUN: &str = "plugin.summary.wont_run";
|
||||
pub const MSG_NEEDS_EXTENSION_HOST: &str = "plugin.summary.needs_extension_host";
|
||||
pub const MSG_NEEDS_NODE: &str = "plugin.summary.needs_node";
|
||||
pub const MSG_REPLACES: &str = "plugin.summary.replaces";
|
||||
pub const MSG_DOWNGRADED: &str = "plugin.summary.downgraded";
|
||||
pub const MSG_SERVICES: &str = "plugin.summary.services";
|
||||
pub const MSG_SERVICES_NOW_ALSO: &str = "plugin.summary.services_now_also";
|
||||
pub const MSG_SERVICES_WITHHELD: &str = "plugin.summary.services_withheld";
|
||||
pub const MSG_SERVICES_UNOFFERED: &str = "plugin.summary.services_unoffered";
|
||||
pub const MSG_SERVICES_PAUSE: &str = "plugin.summary.services_pause";
|
||||
pub const MSG_PROVIDER_NEW_SESSION: &str = "plugin.summary.provider_new_session";
|
||||
|
||||
const HOST_LIST_LIMIT: usize = 3;
|
||||
const VERSION_GATE_REASON: &str = "incompatible-version";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SummaryClause {
|
||||
pub id: &'static str,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PluginSummary {
|
||||
pub clauses: Vec<SummaryClause>,
|
||||
}
|
||||
|
||||
impl PluginSummary {
|
||||
#[must_use]
|
||||
pub fn line(&self) -> String {
|
||||
let mut clauses = self.clauses.iter();
|
||||
let Some(head) = clauses.next() else {
|
||||
return String::new();
|
||||
};
|
||||
let rest: Vec<&str> = clauses.map(|clause| clause.text.as_str()).collect();
|
||||
if rest.is_empty() {
|
||||
head.text.clone()
|
||||
} else {
|
||||
format!("{}: {}", head.text, rest.join("; "))
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn ids(&self) -> Vec<&'static str> {
|
||||
self.clauses.iter().map(|clause| clause.id).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PreviousInstall<'a> {
|
||||
pub name: &'a str,
|
||||
pub version: &'a str,
|
||||
pub source: Option<&'a str>,
|
||||
pub inventory: &'a PluginInventory,
|
||||
pub host_services: &'a [HostService],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SummaryInput<'a> {
|
||||
pub name: &'a str,
|
||||
pub version: &'a str,
|
||||
pub source: Option<&'a str>,
|
||||
pub enabled: bool,
|
||||
pub approver: Option<&'a PluginApprover>,
|
||||
pub inventory: &'a PluginInventory,
|
||||
pub dsh: Option<&'a DshConversion>,
|
||||
pub host_services: Option<&'a HostServiceReview>,
|
||||
pub extension_host_on: bool,
|
||||
pub previous: Option<PreviousInstall<'a>>,
|
||||
pub downgrade_allowed: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
struct Capabilities {
|
||||
skills: usize,
|
||||
local_mcp: usize,
|
||||
remote_mcp: usize,
|
||||
commands: usize,
|
||||
agents: usize,
|
||||
hooks: usize,
|
||||
lsp: usize,
|
||||
native: usize,
|
||||
providers: usize,
|
||||
panels: usize,
|
||||
filesystem_roots: BTreeSet<String>,
|
||||
network_hosts: BTreeSet<String>,
|
||||
lifecycle_mutation: bool,
|
||||
}
|
||||
|
||||
impl Capabilities {
|
||||
fn from_inventory(inventory: &PluginInventory) -> Self {
|
||||
let split = inventory.stdio_mcp_servers + inventory.remote_mcp_servers;
|
||||
let (local_mcp, remote_mcp) = if split == 0 {
|
||||
(inventory.mcp_servers, 0)
|
||||
} else {
|
||||
(
|
||||
inventory.stdio_mcp_servers + inventory.mcp_servers.saturating_sub(split),
|
||||
inventory.remote_mcp_servers,
|
||||
)
|
||||
};
|
||||
Self {
|
||||
skills: inventory.skills,
|
||||
local_mcp,
|
||||
remote_mcp,
|
||||
commands: inventory.commands,
|
||||
agents: inventory.agents,
|
||||
hooks: inventory.hooks,
|
||||
lsp: inventory.lsp,
|
||||
native: inventory.native,
|
||||
providers: inventory.providers,
|
||||
panels: 0,
|
||||
filesystem_roots: inventory.filesystem_roots.iter().cloned().collect(),
|
||||
network_hosts: inventory.network_hosts.iter().cloned().collect(),
|
||||
lifecycle_mutation: inventory.lifecycle_mutation,
|
||||
}
|
||||
}
|
||||
|
||||
fn with_dsh(mut self, dsh: &DshConversion) -> Self {
|
||||
self.skills = self.skills.max(dsh.skills.len());
|
||||
self.local_mcp = self.local_mcp.max(dsh.local_servers.len());
|
||||
self.remote_mcp = self.remote_mcp.max(dsh.remote_servers.len());
|
||||
self.native = self.native.max(dsh.native_rows.len());
|
||||
if dsh.requires_native && self.native == 0 {
|
||||
self.native = 1;
|
||||
}
|
||||
self.panels = self.panels.max(dsh.client_closures.len());
|
||||
self.network_hosts.extend(dsh.network_hosts.iter().cloned());
|
||||
self
|
||||
}
|
||||
|
||||
fn since(&self, old: &Self) -> Self {
|
||||
let more = |new: usize, old: usize| new.saturating_sub(old);
|
||||
Self {
|
||||
skills: more(self.skills, old.skills),
|
||||
local_mcp: more(self.local_mcp, old.local_mcp),
|
||||
remote_mcp: more(self.remote_mcp, old.remote_mcp),
|
||||
commands: more(self.commands, old.commands),
|
||||
agents: more(self.agents, old.agents),
|
||||
hooks: more(self.hooks, old.hooks),
|
||||
lsp: more(self.lsp, old.lsp),
|
||||
native: more(self.native, old.native),
|
||||
providers: more(self.providers, old.providers),
|
||||
panels: more(self.panels, old.panels),
|
||||
filesystem_roots: self
|
||||
.filesystem_roots
|
||||
.difference(&old.filesystem_roots)
|
||||
.cloned()
|
||||
.collect(),
|
||||
network_hosts: self
|
||||
.network_hosts
|
||||
.difference(&old.network_hosts)
|
||||
.cloned()
|
||||
.collect(),
|
||||
lifecycle_mutation: self.lifecycle_mutation && !old.lifecycle_mutation,
|
||||
}
|
||||
}
|
||||
|
||||
fn phrases(&self) -> Vec<String> {
|
||||
let mut phrases = Vec::new();
|
||||
let mut count = |n: usize, one: &str, many: &str| {
|
||||
if n > 0 {
|
||||
phrases.push(format!("{n} {}", if n == 1 { one } else { many }));
|
||||
}
|
||||
};
|
||||
count(self.skills, "skill", "skills");
|
||||
count(self.local_mcp, "local MCP server", "local MCP servers");
|
||||
count(self.remote_mcp, "remote MCP server", "remote MCP servers");
|
||||
count(self.commands, "command", "commands");
|
||||
count(self.agents, "agent profile", "agent profiles");
|
||||
count(self.hooks, "hook", "hooks");
|
||||
count(self.lsp, "language server", "language servers");
|
||||
count(self.native, "native extension", "native extensions");
|
||||
count(self.providers, "provider", "providers");
|
||||
count(self.panels, "panel", "panels");
|
||||
if !self.filesystem_roots.is_empty() {
|
||||
phrases.push(format!("file access ({})", limited(&self.filesystem_roots)));
|
||||
}
|
||||
if !self.network_hosts.is_empty() {
|
||||
phrases.push(format!("network access ({})", limited(&self.network_hosts)));
|
||||
}
|
||||
if self.lifecycle_mutation {
|
||||
phrases.push("permission to change plugins".to_string());
|
||||
}
|
||||
phrases
|
||||
}
|
||||
}
|
||||
|
||||
fn limited(items: &BTreeSet<String>) -> String {
|
||||
let shown: Vec<&str> = items
|
||||
.iter()
|
||||
.take(HOST_LIST_LIMIT)
|
||||
.map(String::as_str)
|
||||
.collect();
|
||||
let mut text = shown.join(", ");
|
||||
if items.len() > HOST_LIST_LIMIT {
|
||||
text.push_str(&format!(" and {} more", items.len() - HOST_LIST_LIMIT));
|
||||
}
|
||||
text
|
||||
}
|
||||
|
||||
fn and_list(items: &[String]) -> String {
|
||||
match items {
|
||||
[] => String::new(),
|
||||
[only] => only.clone(),
|
||||
[init @ .., last] => format!("{} and {last}", init.join(", ")),
|
||||
}
|
||||
}
|
||||
|
||||
fn rows(n: usize) -> String {
|
||||
format!("{n} {}", if n == 1 { "row" } else { "rows" })
|
||||
}
|
||||
|
||||
fn quoted(peers: &[crate::host_services::ReviewPeer]) -> String {
|
||||
peers
|
||||
.iter()
|
||||
.map(|peer| format!("`{}`", peer.name))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
fn push(clauses: &mut Vec<SummaryClause>, id: &'static str, text: String) {
|
||||
clauses.push(SummaryClause { id, text });
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn build_summary(input: &SummaryInput<'_>) -> PluginSummary {
|
||||
let mut clauses = Vec::new();
|
||||
|
||||
let mut head = input.name.to_string();
|
||||
if !input.version.is_empty() {
|
||||
head.push(' ');
|
||||
head.push_str(input.version);
|
||||
}
|
||||
if let Some(source) = input.source.filter(|source| !source.is_empty()) {
|
||||
head.push_str(&format!(" from {source}"));
|
||||
}
|
||||
push(&mut clauses, MSG_HEAD, head);
|
||||
|
||||
if input.enabled {
|
||||
push(&mut clauses, MSG_STATE_ON, "on".to_string());
|
||||
} else {
|
||||
push(&mut clauses, MSG_STATE_OFF, "off".to_string());
|
||||
}
|
||||
push(
|
||||
&mut clauses,
|
||||
MSG_APPROVER,
|
||||
approver_label(input.approver).to_string(),
|
||||
);
|
||||
|
||||
if let Some(previous) = &input.previous {
|
||||
if input.downgrade_allowed && previous.version != input.version {
|
||||
push(
|
||||
&mut clauses,
|
||||
MSG_DOWNGRADED,
|
||||
format!("downgraded from {}", previous.version),
|
||||
);
|
||||
}
|
||||
if previous.source != input.source {
|
||||
let from = previous
|
||||
.source
|
||||
.filter(|source| !source.is_empty())
|
||||
.map_or_else(|| "an unrecorded source".to_string(), str::to_string);
|
||||
push(
|
||||
&mut clauses,
|
||||
MSG_REPLACES,
|
||||
format!("replaces {} from {from}", previous.name),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut now = Capabilities::from_inventory(input.inventory);
|
||||
if let Some(dsh) = input.dsh {
|
||||
now = now.with_dsh(dsh);
|
||||
}
|
||||
match &input.previous {
|
||||
None => {
|
||||
let phrases = now.phrases();
|
||||
if !phrases.is_empty() {
|
||||
push(
|
||||
&mut clauses,
|
||||
MSG_ADDS,
|
||||
format!("adds {}", and_list(&phrases)),
|
||||
);
|
||||
}
|
||||
}
|
||||
Some(previous) => {
|
||||
let added = now
|
||||
.since(&Capabilities::from_inventory(previous.inventory))
|
||||
.phrases();
|
||||
if added.is_empty() {
|
||||
push(
|
||||
&mut clauses,
|
||||
MSG_ADDS_NOTHING_NEW,
|
||||
"adds nothing new".to_string(),
|
||||
);
|
||||
} else {
|
||||
push(
|
||||
&mut clauses,
|
||||
MSG_NOW_ALSO_ADDS,
|
||||
format!("now also adds {}", and_list(&added)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(dsh) = input.dsh {
|
||||
let skipped: Vec<_> = dsh
|
||||
.outcomes
|
||||
.iter()
|
||||
.filter(|outcome| outcome.needs_manual_port())
|
||||
.collect();
|
||||
if !skipped.is_empty() {
|
||||
let gated = skipped
|
||||
.iter()
|
||||
.filter(|outcome| outcome.reason.starts_with(VERSION_GATE_REASON))
|
||||
.count();
|
||||
let manual = skipped.len() - gated;
|
||||
let reason = match (gated, manual) {
|
||||
(0, _) => "each needs a manual port".to_string(),
|
||||
(_, 0) => "refused by the host version gate".to_string(),
|
||||
_ => format!(
|
||||
"{gated} refused by the host version gate, {manual} with no portable form"
|
||||
),
|
||||
};
|
||||
push(
|
||||
&mut clauses,
|
||||
MSG_WONT_RUN,
|
||||
format!("won't run: {} ({reason})", rows(skipped.len())),
|
||||
);
|
||||
}
|
||||
if dsh.requires_node {
|
||||
push(
|
||||
&mut clauses,
|
||||
MSG_NEEDS_NODE,
|
||||
"needs Node.js for its local servers".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if now.native > 0 && !input.extension_host_on {
|
||||
push(
|
||||
&mut clauses,
|
||||
MSG_NEEDS_EXTENSION_HOST,
|
||||
"needs the extension host (off)".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(review) = input.host_services {
|
||||
let granted = review.grants();
|
||||
let previously: &[HostService] = input
|
||||
.previous
|
||||
.as_ref()
|
||||
.map_or(&[], |previous| previous.host_services);
|
||||
let names = |services: Vec<HostService>| -> Vec<String> {
|
||||
services
|
||||
.into_iter()
|
||||
.map(|service| service.name().to_string())
|
||||
.collect()
|
||||
};
|
||||
match &input.previous {
|
||||
None => {
|
||||
if !granted.is_empty() {
|
||||
let mut text =
|
||||
format!("uses host services {}", and_list(&names(granted.clone())));
|
||||
let mut peers: Vec<_> = review
|
||||
.lines
|
||||
.iter()
|
||||
.filter(|line| line.offered)
|
||||
.flat_map(|line| line.shared_with.iter())
|
||||
.collect();
|
||||
peers.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
peers.dedup_by(|a, b| a.id == b.id);
|
||||
if !peers.is_empty() {
|
||||
let peers: Vec<_> = peers.into_iter().cloned().collect();
|
||||
text.push_str(&format!(" (shared with {})", quoted(&peers)));
|
||||
}
|
||||
push(&mut clauses, MSG_SERVICES, text);
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
let fresh: Vec<_> = granted
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|service| !previously.contains(service))
|
||||
.collect();
|
||||
if !fresh.is_empty() {
|
||||
push(
|
||||
&mut clauses,
|
||||
MSG_SERVICES_NOW_ALSO,
|
||||
format!("now also uses host services {}", and_list(&names(fresh))),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
let withheld: Vec<String> = review
|
||||
.lines
|
||||
.iter()
|
||||
.filter(|line| line.offered && !line.withheld_by.is_empty())
|
||||
.map(|line| {
|
||||
format!(
|
||||
"{} ({} is turned on without it)",
|
||||
line.service.name(),
|
||||
quoted(&line.withheld_by)
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
if !withheld.is_empty() {
|
||||
push(
|
||||
&mut clauses,
|
||||
MSG_SERVICES_WITHHELD,
|
||||
format!("off for now: {}", withheld.join(", ")),
|
||||
);
|
||||
}
|
||||
let unoffered: Vec<String> = review
|
||||
.lines
|
||||
.iter()
|
||||
.filter(|line| !line.offered)
|
||||
.map(|line| line.service.name().to_string())
|
||||
.collect();
|
||||
if !unoffered.is_empty() {
|
||||
push(
|
||||
&mut clauses,
|
||||
MSG_SERVICES_UNOFFERED,
|
||||
format!(
|
||||
"this Codewhale does not offer {} to plugins",
|
||||
and_list(&unoffered)
|
||||
),
|
||||
);
|
||||
}
|
||||
if !granted.is_empty() {
|
||||
push(
|
||||
&mut clauses,
|
||||
MSG_SERVICES_PAUSE,
|
||||
"services pause for every plugin in its host until the host is idle".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if now.providers > 0 {
|
||||
push(
|
||||
&mut clauses,
|
||||
MSG_PROVIDER_NEW_SESSION,
|
||||
"provider changes apply in a new session".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
PluginSummary { clauses }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::host_services::{
|
||||
AgentApproval, AgentPosture, HostServiceLine, PersonVia, PluginCohort, ReviewPeer,
|
||||
};
|
||||
use crate::install::dsh::DshOutcome;
|
||||
|
||||
fn inventory() -> PluginInventory {
|
||||
PluginInventory::default()
|
||||
}
|
||||
|
||||
fn input<'a>(inventory: &'a PluginInventory, approver: &'a PluginApprover) -> SummaryInput<'a> {
|
||||
SummaryInput {
|
||||
name: "notes",
|
||||
version: "1.2.0",
|
||||
source: Some("github:acme/notes"),
|
||||
enabled: true,
|
||||
approver: Some(approver),
|
||||
inventory,
|
||||
dsh: None,
|
||||
host_services: None,
|
||||
extension_host_on: true,
|
||||
previous: None,
|
||||
downgrade_allowed: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn person() -> PluginApprover {
|
||||
PluginApprover::person(PersonVia::Terminal)
|
||||
}
|
||||
|
||||
fn conversion() -> DshConversion {
|
||||
DshConversion {
|
||||
plugin_name: "dsh-whale-widget".into(),
|
||||
source_package: Some("dsh-whale-widget".into()),
|
||||
source_version: Some("0.4.2".into()),
|
||||
manifest_sha256: "0".repeat(64),
|
||||
layers: Vec::new(),
|
||||
outcomes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
skills: vec!["balance".into()],
|
||||
remote_servers: vec!["account".into()],
|
||||
local_servers: vec!["bridge".into(), "helper".into()],
|
||||
network_hosts: vec!["api.example.com".into()],
|
||||
requires_node: false,
|
||||
requires_native: false,
|
||||
native_rows: Vec::new(),
|
||||
client_closures: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn skipped(kind: &str, reason: &str) -> DshOutcome {
|
||||
DshOutcome {
|
||||
row: Some("row".into()),
|
||||
package: Some("pkg".into()),
|
||||
kind: kind.into(),
|
||||
outcome: "skipped".into(),
|
||||
reason: reason.into(),
|
||||
layer: None,
|
||||
patch: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn text_of(summary: &PluginSummary, id: &str) -> Option<String> {
|
||||
summary
|
||||
.clauses
|
||||
.iter()
|
||||
.find(|clause| clause.id == id)
|
||||
.map(|clause| clause.text.clone())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_plain_plugin_names_source_state_approver_and_adds() {
|
||||
let inventory = PluginInventory {
|
||||
skills: 2,
|
||||
mcp_servers: 1,
|
||||
stdio_mcp_servers: 1,
|
||||
hooks: 1,
|
||||
network_hosts: vec!["api.example.com".into()],
|
||||
..inventory()
|
||||
};
|
||||
let approver = person();
|
||||
let summary = build_summary(&input(&inventory, &approver));
|
||||
assert_eq!(
|
||||
summary.line(),
|
||||
"notes 1.2.0 from github:acme/notes: on; approved by you; adds 2 skills, 1 local MCP server, 1 hook and network access (api.example.com)"
|
||||
);
|
||||
assert_eq!(
|
||||
summary.ids(),
|
||||
vec![MSG_HEAD, MSG_STATE_ON, MSG_APPROVER, MSG_ADDS]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_off_plugin_and_each_approver_kind_read_distinctly() {
|
||||
let inventory = inventory();
|
||||
let agent = PluginApprover::Agent(AgentApproval {
|
||||
posture: AgentPosture::AutoReview,
|
||||
workspace: "/work".into(),
|
||||
thread_id: None,
|
||||
turn_id: None,
|
||||
tool_call_id: None,
|
||||
});
|
||||
let mut request = input(&inventory, &agent);
|
||||
request.enabled = false;
|
||||
request.source = None;
|
||||
let summary = build_summary(&request);
|
||||
assert_eq!(
|
||||
summary.line(),
|
||||
"notes 1.2.0: off; approved by the agent in Auto-Review"
|
||||
);
|
||||
let mut request = input(&inventory, &agent);
|
||||
request.approver = None;
|
||||
assert!(
|
||||
build_summary(&request)
|
||||
.line()
|
||||
.contains("approver not recorded")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_dsh_package_counts_conversion_fields_and_panels() {
|
||||
let mut dsh = conversion();
|
||||
dsh.requires_node = true;
|
||||
dsh.client_closures = vec![crate::install::dsh::DshClientClosure {
|
||||
package: "dsh-whale-widget".into(),
|
||||
root: String::new(),
|
||||
entry: "source/client.js".into(),
|
||||
platform: None,
|
||||
inject: Vec::new(),
|
||||
host_provided: Vec::new(),
|
||||
files: Default::default(),
|
||||
bytes: 10,
|
||||
}];
|
||||
let inventory = inventory();
|
||||
let approver = person();
|
||||
let mut request = input(&inventory, &approver);
|
||||
request.dsh = Some(&dsh);
|
||||
let summary = build_summary(&request);
|
||||
assert_eq!(
|
||||
text_of(&summary, MSG_ADDS).unwrap(),
|
||||
"adds 1 skill, 2 local MCP servers, 1 remote MCP server, 1 panel and network access (api.example.com)"
|
||||
);
|
||||
assert_eq!(
|
||||
text_of(&summary, MSG_NEEDS_NODE).unwrap(),
|
||||
"needs Node.js for its local servers"
|
||||
);
|
||||
assert!(text_of(&summary, MSG_WONT_RUN).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_refused_and_version_refused_rows_are_counted_with_reasons() {
|
||||
let mut dsh = conversion();
|
||||
dsh.outcomes = vec![
|
||||
skipped("foreign", "no portable representation"),
|
||||
skipped("mcp", "needs a manual port"),
|
||||
DshOutcome {
|
||||
outcome: "converted".into(),
|
||||
..skipped("mcp", "")
|
||||
},
|
||||
];
|
||||
let inventory = inventory();
|
||||
let approver = person();
|
||||
let mut request = input(&inventory, &approver);
|
||||
request.dsh = Some(&dsh);
|
||||
assert_eq!(
|
||||
text_of(&build_summary(&request), MSG_WONT_RUN).unwrap(),
|
||||
"won't run: 2 rows (each needs a manual port)"
|
||||
);
|
||||
|
||||
dsh.outcomes = vec![skipped(
|
||||
"patch",
|
||||
"incompatible-version: this bundle requires DSH `^0.1.0-rc.6`",
|
||||
)];
|
||||
let mut request = input(&inventory, &approver);
|
||||
request.dsh = Some(&dsh);
|
||||
assert_eq!(
|
||||
text_of(&build_summary(&request), MSG_WONT_RUN).unwrap(),
|
||||
"won't run: 1 row (refused by the host version gate)"
|
||||
);
|
||||
|
||||
dsh.outcomes
|
||||
.push(skipped("foreign", "no portable representation"));
|
||||
let mut request = input(&inventory, &approver);
|
||||
request.dsh = Some(&dsh);
|
||||
assert_eq!(
|
||||
text_of(&build_summary(&request), MSG_WONT_RUN).unwrap(),
|
||||
"won't run: 2 rows (1 refused by the host version gate, 1 with no portable form)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_native_rows_need_the_extension_host_only_while_it_is_off() {
|
||||
let mut dsh = conversion();
|
||||
dsh.requires_native = true;
|
||||
dsh.native_rows = vec!["worker".into()];
|
||||
let inventory = inventory();
|
||||
let approver = person();
|
||||
let mut request = input(&inventory, &approver);
|
||||
request.dsh = Some(&dsh);
|
||||
request.extension_host_on = false;
|
||||
let off = build_summary(&request);
|
||||
assert_eq!(
|
||||
text_of(&off, MSG_NEEDS_EXTENSION_HOST).unwrap(),
|
||||
"needs the extension host (off)"
|
||||
);
|
||||
assert!(
|
||||
text_of(&off, MSG_ADDS)
|
||||
.unwrap()
|
||||
.contains("1 native extension")
|
||||
);
|
||||
request.extension_host_on = true;
|
||||
assert!(text_of(&build_summary(&request), MSG_NEEDS_EXTENSION_HOST).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_update_names_only_what_changed() {
|
||||
let before = PluginInventory {
|
||||
skills: 2,
|
||||
..inventory()
|
||||
};
|
||||
let after = PluginInventory {
|
||||
skills: 2,
|
||||
hooks: 1,
|
||||
network_hosts: vec!["api.example.com".into()],
|
||||
..inventory()
|
||||
};
|
||||
let approver = person();
|
||||
let mut request = input(&after, &approver);
|
||||
request.previous = Some(PreviousInstall {
|
||||
name: "notes",
|
||||
version: "1.1.0",
|
||||
source: Some("github:acme/notes"),
|
||||
inventory: &before,
|
||||
host_services: &[],
|
||||
});
|
||||
let summary = build_summary(&request);
|
||||
assert_eq!(
|
||||
text_of(&summary, MSG_NOW_ALSO_ADDS).unwrap(),
|
||||
"now also adds 1 hook and network access (api.example.com)"
|
||||
);
|
||||
assert!(text_of(&summary, MSG_ADDS).is_none());
|
||||
assert!(text_of(&summary, MSG_REPLACES).is_none());
|
||||
assert!(text_of(&summary, MSG_DOWNGRADED).is_none());
|
||||
|
||||
request.previous = Some(PreviousInstall {
|
||||
name: "notes",
|
||||
version: "1.1.0",
|
||||
source: Some("github:acme/notes"),
|
||||
inventory: &after,
|
||||
host_services: &[],
|
||||
});
|
||||
assert_eq!(
|
||||
text_of(&build_summary(&request), MSG_ADDS_NOTHING_NEW).unwrap(),
|
||||
"adds nothing new"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_source_change_and_downgrade_are_stated() {
|
||||
let inventory = inventory();
|
||||
let approver = person();
|
||||
let mut request = input(&inventory, &approver);
|
||||
request.previous = Some(PreviousInstall {
|
||||
name: "notes",
|
||||
version: "1.4.0",
|
||||
source: Some("github:old/notes"),
|
||||
inventory: &inventory,
|
||||
host_services: &[],
|
||||
});
|
||||
request.downgrade_allowed = true;
|
||||
let summary = build_summary(&request);
|
||||
assert_eq!(
|
||||
text_of(&summary, MSG_REPLACES).unwrap(),
|
||||
"replaces notes from github:old/notes"
|
||||
);
|
||||
assert_eq!(
|
||||
text_of(&summary, MSG_DOWNGRADED).unwrap(),
|
||||
"downgraded from 1.4.0"
|
||||
);
|
||||
request.downgrade_allowed = false;
|
||||
assert!(text_of(&build_summary(&request), MSG_DOWNGRADED).is_none());
|
||||
request.previous.as_mut().unwrap().source = None;
|
||||
assert_eq!(
|
||||
text_of(&build_summary(&request), MSG_REPLACES).unwrap(),
|
||||
"replaces notes from an unrecorded source"
|
||||
);
|
||||
}
|
||||
|
||||
fn review(lines: Vec<HostServiceLine>) -> HostServiceReview {
|
||||
HostServiceReview::new("user/0/notes", "c", "k", &PluginCohort::Reviewed, lines)
|
||||
}
|
||||
|
||||
fn peer(name: &str) -> ReviewPeer {
|
||||
ReviewPeer {
|
||||
id: format!("user/0/{name}"),
|
||||
name: name.into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_host_services_state_sharing_withheld_and_the_pause() {
|
||||
let inventory = inventory();
|
||||
let approver = person();
|
||||
let review = review(vec![
|
||||
HostServiceLine {
|
||||
service: HostService::Workspace,
|
||||
offered: true,
|
||||
shared_with: vec![peer("other")],
|
||||
withheld_by: Vec::new(),
|
||||
},
|
||||
HostServiceLine {
|
||||
service: HostService::Network,
|
||||
offered: true,
|
||||
shared_with: Vec::new(),
|
||||
withheld_by: vec![peer("legacy")],
|
||||
},
|
||||
HostServiceLine {
|
||||
service: HostService::Remote,
|
||||
offered: false,
|
||||
shared_with: Vec::new(),
|
||||
withheld_by: Vec::new(),
|
||||
},
|
||||
]);
|
||||
let mut request = input(&inventory, &approver);
|
||||
request.host_services = Some(&review);
|
||||
let summary = build_summary(&request);
|
||||
assert_eq!(
|
||||
text_of(&summary, MSG_SERVICES).unwrap(),
|
||||
"uses host services workspace and network (shared with `other`)"
|
||||
);
|
||||
assert_eq!(
|
||||
text_of(&summary, MSG_SERVICES_WITHHELD).unwrap(),
|
||||
"off for now: network (`legacy` is turned on without it)"
|
||||
);
|
||||
assert_eq!(
|
||||
text_of(&summary, MSG_SERVICES_UNOFFERED).unwrap(),
|
||||
"this Codewhale does not offer remote to plugins"
|
||||
);
|
||||
assert_eq!(
|
||||
text_of(&summary, MSG_SERVICES_PAUSE).unwrap(),
|
||||
"services pause for every plugin in its host until the host is idle"
|
||||
);
|
||||
|
||||
let before = [HostService::Workspace];
|
||||
let prior = PluginInventory::default();
|
||||
request.previous = Some(PreviousInstall {
|
||||
name: "notes",
|
||||
version: "1.1.0",
|
||||
source: Some("github:acme/notes"),
|
||||
inventory: &prior,
|
||||
host_services: &before,
|
||||
});
|
||||
let update = build_summary(&request);
|
||||
assert_eq!(
|
||||
text_of(&update, MSG_SERVICES_NOW_ALSO).unwrap(),
|
||||
"now also uses host services network"
|
||||
);
|
||||
assert!(text_of(&update, MSG_SERVICES).is_none());
|
||||
|
||||
let empty = self::review(Vec::new());
|
||||
request.previous = None;
|
||||
request.host_services = Some(&empty);
|
||||
assert!(text_of(&build_summary(&request), MSG_SERVICES_PAUSE).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_provider_declarations_say_they_apply_in_a_new_session() {
|
||||
let inventory = PluginInventory {
|
||||
providers: 1,
|
||||
..inventory()
|
||||
};
|
||||
let approver = person();
|
||||
let summary = build_summary(&input(&inventory, &approver));
|
||||
assert_eq!(
|
||||
text_of(&summary, MSG_PROVIDER_NEW_SESSION).unwrap(),
|
||||
"provider changes apply in a new session"
|
||||
);
|
||||
assert!(text_of(&summary, MSG_ADDS).unwrap().contains("1 provider"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_long_host_lists_are_shortened() {
|
||||
let inventory = PluginInventory {
|
||||
network_hosts: ["a.io", "b.io", "c.io", "d.io", "e.io"]
|
||||
.map(String::from)
|
||||
.to_vec(),
|
||||
..inventory()
|
||||
};
|
||||
let approver = person();
|
||||
assert_eq!(
|
||||
text_of(&build_summary(&input(&inventory, &approver)), MSG_ADDS).unwrap(),
|
||||
"adds network access (a.io, b.io, c.io and 2 more)"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,6 @@ description = "Codewhale sandboxed Python REPL runtime"
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
codewhale-dependencies = { path = "../dependencies", version = "0.10.3" }
|
||||
codewhale-runtime = { path = "../runtime", version = "0.10.3" }
|
||||
|
||||
@@ -80,7 +80,6 @@ uuid.workspace = true
|
||||
wait-timeout = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
futures-util = "0.3"
|
||||
ring = "0.17"
|
||||
wiremock = "0.6"
|
||||
codewhale-models = { path = "../models", version = "0.10.3", features = [
|
||||
|
||||
@@ -13,9 +13,6 @@
|
||||
//! Sampling and hashing are not the same promise, and a notice that implies
|
||||
//! them when neither is true is worse than no notice.
|
||||
|
||||
/// Headline shown above [`NOTICE_BODY`].
|
||||
pub const NOTICE_HEADLINE: &str = "Codewhale usage reporting";
|
||||
|
||||
/// The notice itself.
|
||||
///
|
||||
/// Wrapped at 72 columns so it renders unchanged in the native responsive
|
||||
|
||||
@@ -197,11 +197,7 @@ codewhale-models = { path = "../models", version = "0.10.3", features = [
|
||||
cucumber = "0.23.0"
|
||||
wiremock = "0.6"
|
||||
tiny_http = "0.12"
|
||||
pretty_assertions = "1.4"
|
||||
rio-vt = "0.5.1"
|
||||
rusqlite.workspace = true
|
||||
schemars = { version = "1.2.1", features = ["derive", "preserve_order"] }
|
||||
tokio-tungstenite = "0.30"
|
||||
codewhale-app-server = { path = "../app-server", version = "0.10.3", features = ["test-support"] }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
|
||||
@@ -1285,7 +1285,6 @@ pub enum SidebarRowAction {
|
||||
Command(String),
|
||||
/// Put a destructive command in the composer instead of executing it.
|
||||
/// The user confirms with Enter or cancels by editing/clearing the draft.
|
||||
#[allow(dead_code)] // destructive confirm path; mouse_ui already matches it (TUI-DOG-008)
|
||||
PrefillCommand(String),
|
||||
/// Select the persistent Agents panel. This is deliberately a navigation
|
||||
/// action rather than a modal: the Subagents summary is a group door, so
|
||||
|
||||
@@ -85,7 +85,7 @@ impl BacktrackState {
|
||||
/// `true` whenever the user has armed or opened backtrack. The UI uses
|
||||
/// this to skip the prime hint once the overlay is up and to know
|
||||
/// whether arrow keys should drive selection.
|
||||
#[allow(dead_code)] // helper exposed for future UI consumers + tests.
|
||||
#[cfg(test)]
|
||||
#[must_use]
|
||||
pub fn is_active(&self) -> bool {
|
||||
!matches!(self.phase, BacktrackPhase::Inactive)
|
||||
@@ -94,7 +94,7 @@ impl BacktrackState {
|
||||
/// `true` only when the overlay is open and Left/Right should step
|
||||
/// through prior user messages. `Primed` is intentionally excluded —
|
||||
/// during the prime window arrows still scroll the transcript.
|
||||
#[allow(dead_code)] // helper exposed for future UI consumers + tests.
|
||||
#[cfg(test)]
|
||||
#[must_use]
|
||||
pub fn is_selecting(&self) -> bool {
|
||||
matches!(self.phase, BacktrackPhase::Selecting { .. })
|
||||
|
||||
@@ -169,7 +169,6 @@ impl LiveTranscriptOverlay {
|
||||
|
||||
/// Return the overlay to live-tail mode (used when backtrack is
|
||||
/// confirmed or canceled). Re-arms sticky-tail so streaming resumes.
|
||||
#[allow(dead_code)] // exposed for callers that retain an overlay across a backtrack cancel; current UI just pops the view.
|
||||
pub fn set_tail_mode(&mut self) {
|
||||
self.mode = Mode::Tail;
|
||||
self.sticky_to_bottom.set(true);
|
||||
@@ -177,7 +176,7 @@ impl LiveTranscriptOverlay {
|
||||
}
|
||||
|
||||
/// For tests + UI: current mode.
|
||||
#[allow(dead_code)] // currently consumed only by tests; kept public for symmetry with `set_*` setters.
|
||||
#[cfg(test)]
|
||||
#[must_use]
|
||||
pub fn mode(&self) -> Mode {
|
||||
self.mode
|
||||
|
||||
@@ -469,8 +469,6 @@ impl WorkflowPlan {
|
||||
}
|
||||
}
|
||||
|
||||
pub type WorkflowIr = WorkflowPlan;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PhasePlan {
|
||||
pub name: String,
|
||||
@@ -494,8 +492,6 @@ pub struct Phase {
|
||||
pub tasks: Vec<Task>,
|
||||
}
|
||||
|
||||
pub type WorkflowPhase = Phase;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FailurePolicy {
|
||||
@@ -524,9 +520,6 @@ pub struct Task {
|
||||
pub timeout_secs: Option<u64>,
|
||||
}
|
||||
|
||||
pub type WorkflowTask = Task;
|
||||
pub type WorkflowRole = AgentType;
|
||||
|
||||
/// The workflow wire's agent type. The serialized spellings are the canonical
|
||||
/// Codewhale role vocabulary (founder, 2026-09-02); the pre-rename JS
|
||||
/// spellings stay accepted as aliases so checked-in and saved workflows keep
|
||||
|
||||
@@ -65,10 +65,6 @@ impl FleetRoleMap {
|
||||
self.roles.get(&key).map(String::as_str)
|
||||
}
|
||||
|
||||
pub fn contains_role(&self, role: &str) -> bool {
|
||||
self.get(role).is_some()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.roles.is_empty()
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"_comment": "Ceiling for dead-code suppressions across crates/, counting both `#[allow(dead_code)]` and `#[expect(dead_code)]`. `total` is the sum and is the ratcheted figure. It may go down freely; raising it needs a reviewer to say why in the PR. Regenerate with: python3 scripts/check-dead-code-budget.py --update",
|
||||
"_issue": "https://github.com/codewhale-hq/CodeWhale/issues/4785",
|
||||
"_expect_blind_spot": "Until #6241 this gate counted only the `allow` spelling, so rewriting an allow as an expect lowered the number without removing any dead code. The ceiling was re-based to the true combined count when that was fixed; it is not a regression.",
|
||||
"total": 121,
|
||||
"allow_total": 89,
|
||||
"total": 116,
|
||||
"allow_total": 84,
|
||||
"expect_total": 32,
|
||||
"per_crate": {
|
||||
"app-server": {
|
||||
@@ -55,7 +55,7 @@
|
||||
"expect": 0
|
||||
},
|
||||
"tui": {
|
||||
"allow": 36,
|
||||
"allow": 31,
|
||||
"expect": 20
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -2,6 +2,16 @@
|
||||
|
||||
## Unreleased — 0.2.0
|
||||
|
||||
- Add data displays: `DataSparkline`, `DataBars` and `DataTrend` (a braille
|
||||
line chart). The words carry every value; the ink draws in from a caller
|
||||
progress (`MotionStep::drawn`) and is at its end state under reduced or
|
||||
still motion. Bars glide from their previous values.
|
||||
- Add `DataDisplay`, the same displays as JSON an agent can send:
|
||||
`DataDisplay::from_json` validates it (finite numbers, at most 1024 points
|
||||
or 24 bars, labels up to 120 characters), `describe` says the same facts as
|
||||
plain text, and `DATA_DISPLAY_SCHEMA` is its tool schema. Try
|
||||
`cargo run --example data`.
|
||||
|
||||
- Reexport the shared avatar contract from Codewhale Protocol. Rendering and
|
||||
artwork stay in this library. Protocol 0.10.3 registry proof must precede
|
||||
this package publication.
|
||||
|
||||
+1
@@ -57,6 +57,7 @@ Gallery names are the exact names accepted by the gallery example; the
|
||||
| Engine `agent_roster.rs::render_agent_roster`, `widgets/workflow_panel.rs::row_receipt_text`, `gate_receipts.rs`; GPUI `usage.rs::TurnRow`, `workbar.rs::Readout`, `working_context.rs::ExecutionReceipt` | `Receipt`, `ReceiptTable`, `ReceiptValue`, `Cost`, formatting helpers | Primitive/composite | `receipt-row`, `receipt-table`, `receipt-table-compact`, `receipt-table-minimal`, `receipt-table-clipped` |
|
||||
| Engine `diff_render.rs::BoundedDiffRender`, `render_diff_bounded`, `history/file_mutation.rs`; GPUI `review.rs::DiffRowKind`, `workspace/render/review.rs::ReviewRows` | `Diff`, `DiffLine`, `DiffKind`, `DiffGutter`, `DiffHighlight`, `parse_unified` | Primitive | `diff`, `diff-wrapped`, `diff-no-numbers`, `diff-highlighted`, `review-scene` |
|
||||
| Engine `widgets/workflow_panel.rs::WorkflowPanel`, `WorkflowPanelRow`, `history/checklist.rs`; GPUI `plans.rs`, `workspace/render/workers.rs` | `WorkflowTree`, `TreeNode`, `TreeState`, `CountBar` | Primitive/composite | `workflow-tree`, `workflow-tree-selected`, `workflow-tree-clipped`, `workflow-tree-long`, `count-bars` |
|
||||
| Kit-native; data from the host or an agent's JSON (CW-8202) | `DataSparkline`, `DataBars`, `DataBarItem`, `DataTrend`, `DataDisplay`, `DataDisplayView`, `DataWords`, `DATA_DISPLAY_SCHEMA`, `MotionStep::drawn` | Primitive | `data-sparkline`, `data-bars`, `data-bars-growing`, `data-trend`, `data-trend-drawing`, `data-from-agent` |
|
||||
| Engine `approval/view.rs::ApprovalView`, `approval/elevation.rs::ElevationView`, `auto_review.rs`; GPUI `workspace/render/conversation.rs::Workspace::render_attention`, `workspace/render/review.rs::Workspace::render_restore_confirmation` | `ApprovalCard`, `ApprovalSubject`, `ApprovalState`, `ApprovalChoice`, `ApprovalPaint`, `DecisionBand`, `DecisionBandPlan`, `DecisionBandAction`, `DecisionBandSave`, `ReviewVerdict`, `ReviewAggregate` | Composite | `approval-command`, `approval-outside`, `approval-patch`, `approval-elevation`, `approval-clipped`, `approval-spoofed`, `approval-native-band`, `approval-native-band-collapsed`, `review-verdicts`, `review-aggregate` |
|
||||
| GPUI `workspace/render/conversation.rs::Workspace::render_attention`, `workspace/render/files.rs::Workspace::render_artifact_column` | `AttentionQueue`, `AttentionItem`, `ArtifactShelf`, `Artifact` | Composite | `attention-queue`, `attention-focused`, `attention-narrow`, `attention-empty`, `artifact`, `artifact-shelf`, `artifact-narrow`, `artifact-unknown`, `artifact-empty` |
|
||||
| Engine `views/mod.rs::ConfigView`, `ConfigRow`, `ConfigView::render_setting_detail`; GPUI `settings.rs::Spec`, `Applies`, `workspace/settings/mod.rs::Settings` | `SettingRow`, `SettingDetail`, `SettingWords` | Composite | `setting-row`, `setting-detail` |
|
||||
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
//! Live data displays: `cargo run --example data`.
|
||||
//!
|
||||
//! A sparkline takes a new value every 400 ms, the trend draws in, and the
|
||||
//! bars glide to new values every three seconds. r replays, m switches
|
||||
//! between full and reduced motion, q or Escape exits.
|
||||
//!
|
||||
//! `--json FILE` shows one agent-sent [`DataDisplay`] instead.
|
||||
//! `--text` prints the final frame and each display's plain description.
|
||||
//! `--svg DIR` writes frames at fixed draw-in points.
|
||||
|
||||
use std::{
|
||||
io,
|
||||
path::PathBuf,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use codewhale_ratatui::{
|
||||
DataBarItem, DataDisplay, DataWords, MotionEasing, MotionMode, MotionPolicy, MotionStep,
|
||||
MotionTiming, Paint, Role, Theme,
|
||||
testing::{self, Profile},
|
||||
};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
|
||||
};
|
||||
use ratatui::{Terminal, backend::CrosstermBackend, buffer::Buffer, layout::Rect};
|
||||
|
||||
const TICK: Duration = Duration::from_millis(400);
|
||||
const RESHUFFLE: Duration = Duration::from_secs(3);
|
||||
const DRAW_IN: MotionTiming =
|
||||
MotionTiming::space(Duration::from_millis(1_400), MotionEasing::EaseOut);
|
||||
const GLIDE: MotionTiming = MotionTiming::PANEL;
|
||||
|
||||
struct Noise(u64);
|
||||
|
||||
impl Noise {
|
||||
fn next(&mut self) -> f64 {
|
||||
self.0 = self
|
||||
.0
|
||||
.wrapping_mul(6_364_136_223_846_793_005)
|
||||
.wrapping_add(1);
|
||||
(self.0 >> 33) as f64 / f64::from(1u32 << 31)
|
||||
}
|
||||
}
|
||||
|
||||
struct Board {
|
||||
noise: Noise,
|
||||
requests: Vec<f64>,
|
||||
latency: Vec<f64>,
|
||||
bars: DataDisplay,
|
||||
bars_before: DataDisplay,
|
||||
draw: MotionStep,
|
||||
glide: MotionStep,
|
||||
agent: Option<DataDisplay>,
|
||||
}
|
||||
|
||||
fn crate_bars(noise: &mut Noise) -> DataDisplay {
|
||||
DataDisplay::Bars {
|
||||
label: "Tests by crate".into(),
|
||||
unit: "tests".into(),
|
||||
items: ["engine", "tui", "runtime", "protocol", "config"]
|
||||
.into_iter()
|
||||
.map(|name| DataBarItem::new(name, (40.0 + noise.next() * 400.0).round()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
impl Board {
|
||||
fn new(agent: Option<DataDisplay>) -> Self {
|
||||
let mut noise = Noise(7);
|
||||
let latency = (0..96)
|
||||
.map(|i| 80.0 + 45.0 * (f64::from(i) / 9.0).sin() + noise.next() * 30.0)
|
||||
.collect();
|
||||
let requests = (0..40).map(|_| 20.0 + noise.next() * 60.0).collect();
|
||||
let bars = crate_bars(&mut noise);
|
||||
Self {
|
||||
noise,
|
||||
requests,
|
||||
latency,
|
||||
bars_before: DataDisplay::Bars {
|
||||
label: String::new(),
|
||||
unit: String::new(),
|
||||
items: Vec::new(),
|
||||
},
|
||||
bars,
|
||||
draw: MotionStep::at_rest(0.0),
|
||||
glide: MotionStep::at_rest(1.0),
|
||||
agent,
|
||||
}
|
||||
}
|
||||
|
||||
fn replay(&mut self, now: Instant, policy: MotionPolicy) {
|
||||
self.draw = MotionStep::at_rest(0.0);
|
||||
self.draw.go(1.0, now, DRAW_IN, policy);
|
||||
}
|
||||
|
||||
fn tick(&mut self) {
|
||||
let last = self.requests.last().copied().unwrap_or(50.0);
|
||||
let next = (last + (self.noise.next() - 0.5) * 30.0).clamp(5.0, 120.0);
|
||||
self.requests.push(next.round());
|
||||
if self.requests.len() > 200 {
|
||||
self.requests.remove(0);
|
||||
}
|
||||
}
|
||||
|
||||
fn reshuffle(&mut self, now: Instant, policy: MotionPolicy) {
|
||||
self.bars_before = std::mem::replace(&mut self.bars, crate_bars(&mut self.noise));
|
||||
self.glide = MotionStep::at_rest(0.0);
|
||||
self.glide.go(1.0, now, GLIDE, policy);
|
||||
}
|
||||
|
||||
fn paint(&self, area: Rect, buf: &mut Buffer, theme: &Theme, draw: f32, glide: f32) {
|
||||
buf.set_style(area, theme.bg(Role::Background));
|
||||
let inner = Rect::new(
|
||||
area.x + 2,
|
||||
area.y + 1,
|
||||
area.width.saturating_sub(4),
|
||||
area.height.saturating_sub(2),
|
||||
);
|
||||
if let Some(agent) = &self.agent {
|
||||
agent.view().drawn(draw).paint(inner, buf, theme);
|
||||
return;
|
||||
}
|
||||
let row =
|
||||
|y: u16, h: u16| Rect::new(inner.x, inner.y + y, inner.width, h).intersection(inner);
|
||||
let requests = DataDisplay::Sparkline {
|
||||
label: "Requests".into(),
|
||||
unit: "rpm".into(),
|
||||
values: self.requests.clone(),
|
||||
};
|
||||
let latency = DataDisplay::Trend {
|
||||
label: "Latency".into(),
|
||||
unit: "ms".into(),
|
||||
values: self.latency.clone(),
|
||||
rows: Some(9),
|
||||
};
|
||||
requests.view().drawn(draw).paint(row(0, 1), buf, theme);
|
||||
latency.view().drawn(draw).paint(row(2, 9), buf, theme);
|
||||
self.bars
|
||||
.view()
|
||||
.from(&self.bars_before)
|
||||
.drawn(glide.min(draw))
|
||||
.paint(row(12, 6), buf, theme);
|
||||
}
|
||||
|
||||
fn displays(&self) -> Vec<DataDisplay> {
|
||||
if let Some(agent) = &self.agent {
|
||||
return vec![agent.clone()];
|
||||
}
|
||||
vec![
|
||||
DataDisplay::Sparkline {
|
||||
label: "Requests".into(),
|
||||
unit: "rpm".into(),
|
||||
values: self.requests.clone(),
|
||||
},
|
||||
DataDisplay::Trend {
|
||||
label: "Latency".into(),
|
||||
unit: "ms".into(),
|
||||
values: self.latency.clone(),
|
||||
rows: Some(9),
|
||||
},
|
||||
self.bars.clone(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let value = |flag: &str| {
|
||||
args.iter()
|
||||
.position(|a| a == flag)
|
||||
.and_then(|i| args.get(i + 1))
|
||||
.cloned()
|
||||
};
|
||||
let agent = match value("--json") {
|
||||
Some(path) => {
|
||||
let json = std::fs::read_to_string(&path)?;
|
||||
match DataDisplay::from_json(&json) {
|
||||
Ok(shown) => Some(shown),
|
||||
Err(e) => {
|
||||
eprintln!("{path}: {e}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let board = Board::new(agent);
|
||||
|
||||
if args.iter().any(|a| a == "--text") {
|
||||
let buf = testing::render(84, 20, |area, buf| {
|
||||
board.paint(area, buf, &Profile::DarkTrue.theme(), 1.0, 1.0);
|
||||
});
|
||||
println!("{}", testing::text(&buf));
|
||||
for shown in board.displays() {
|
||||
println!("{}", shown.describe(&DataWords::default()));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(dir) = value("--svg") {
|
||||
let dir = PathBuf::from(dir);
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
let theme = Profile::DarkTrue.theme();
|
||||
for (i, at) in [0.2_f32, 0.5, 0.8, 1.0].into_iter().enumerate() {
|
||||
let buf = testing::render(84, 20, |area, buf| board.paint(area, buf, &theme, at, at));
|
||||
let path = dir.join(format!("data-{i}.svg"));
|
||||
std::fs::write(&path, testing::svg(&buf, &theme))?;
|
||||
println!("{}", path.display());
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
run(board)
|
||||
}
|
||||
|
||||
fn run(mut board: Board) -> io::Result<()> {
|
||||
enable_raw_mode()?;
|
||||
let mut out = io::stdout();
|
||||
execute!(out, EnterAlternateScreen)?;
|
||||
let mut terminal = Terminal::new(CrosstermBackend::new(out))?;
|
||||
let theme = Theme::detect().tui();
|
||||
let mut mode = MotionMode::Full;
|
||||
let mut policy = MotionPolicy::new(mode, &theme);
|
||||
let start = Instant::now();
|
||||
board.replay(start, policy);
|
||||
let (mut next_tick, mut next_shuffle) = (start + TICK, start + RESHUFFLE);
|
||||
|
||||
let result = loop {
|
||||
let now = Instant::now();
|
||||
if now >= next_tick {
|
||||
board.tick();
|
||||
next_tick = now + TICK;
|
||||
}
|
||||
if now >= next_shuffle {
|
||||
board.reshuffle(now, policy);
|
||||
next_shuffle = now + RESHUFFLE;
|
||||
}
|
||||
let (draw, glide) = (
|
||||
board.draw.drawn(now, policy),
|
||||
board.glide.drawn(now, policy),
|
||||
);
|
||||
terminal.draw(|f| board.paint(f.area(), f.buffer_mut(), &theme, draw, glide))?;
|
||||
|
||||
let due = [
|
||||
board.draw.next_frame_at(now, policy),
|
||||
board.glide.next_frame_at(now, policy),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.chain([next_tick, next_shuffle])
|
||||
.min()
|
||||
.unwrap_or(next_tick);
|
||||
if event::poll(due.saturating_duration_since(Instant::now()))?
|
||||
&& let Event::Key(key) = event::read()?
|
||||
&& key.kind != KeyEventKind::Release
|
||||
{
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => break Ok(()),
|
||||
KeyCode::Char('r') => board.replay(Instant::now(), policy),
|
||||
KeyCode::Char('m') => {
|
||||
mode = if mode == MotionMode::Full {
|
||||
MotionMode::Reduced
|
||||
} else {
|
||||
MotionMode::Full
|
||||
};
|
||||
policy = MotionPolicy::new(mode, &theme);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
result
|
||||
}
|
||||
+1261
File diff suppressed because it is too large.
Load diff
@@ -59,10 +59,12 @@ pub use tabs::*;
|
||||
pub use toggle::*;
|
||||
|
||||
// Package Display.
|
||||
mod chart;
|
||||
mod diff;
|
||||
mod progress;
|
||||
mod receipt;
|
||||
mod tree;
|
||||
pub use chart::*;
|
||||
pub use diff::*;
|
||||
pub use progress::*;
|
||||
pub use receipt::*;
|
||||
|
||||
@@ -366,6 +366,14 @@ impl MotionStep {
|
||||
}
|
||||
}
|
||||
|
||||
/// Progress for a draw-in, in `0..=1`: eased where `policy` lets space
|
||||
/// move, the target where it does not. Feed it to a data display's
|
||||
/// `drawn`.
|
||||
#[must_use]
|
||||
pub fn drawn(&self, now: Instant, policy: MotionPolicy) -> f32 {
|
||||
self.shown(now, policy, MotionChannel::Space)
|
||||
}
|
||||
|
||||
/// A column between `from` and `to`: slide a marker, nudge a panel.
|
||||
#[must_use]
|
||||
pub fn offset(&self, now: Instant, policy: MotionPolicy, from: u16, to: u16) -> u16 {
|
||||
|
||||
+360
@@ -0,0 +1,360 @@
|
||||
---
|
||||
source: src/components/chart.rs
|
||||
assertion_line: 1242
|
||||
expression: "testing::snapshot(12, |area, buf, theme|\n{\n let row = |y: u16, h: u16|\n { Rect::new(area.x, area.y + y, area.width, h).intersection(area) };\n DataSparkline::new(\"Requests\",\n &values).unit(\"rpm\").paint(row(0, 1), buf, theme);\n DataBars::new(\"Tokens by model\",\n &items).unit(\"tokens\").paint(row(2, 5), buf, theme);\n DataTrend::new(\"Latency\",\n &values).unit(\"ms\").rows(5).paint(row(7, 5), buf, theme);\n})"
|
||||
---
|
||||
== dark-truecolor · 40x12
|
||||
Requests ▁▃▅▄▅▆▇███▆▅▄▃▂▁ now 45.6 rpm
|
||||
|
||||
Tokens by model
|
||||
opus ██████████████████ 12.4k tokens
|
||||
sonnet ███████████▌ 7900 tokens
|
||||
haiku ████▌ 3100 tokens
|
||||
cache 0 tokens
|
||||
Latency now 45.6 ms
|
||||
69.7 ms │ ⡠⠒⠑⠒⠒⠢⣀ ⡀⡠⠔⠉⠉⢢⡀
|
||||
│⢀⠔⠁ ⢇ ⢀⠔⠉ ⠈⠑⢄
|
||||
│⠁ ⠑⠢⡀ ⡠⠊
|
||||
17.2 ms │ ⠈⠱⣀⣀⠤⠊⠊
|
||||
---
|
||||
r0 c0..8 fg=Foreground bg=Background bold
|
||||
r0 c8..10 bg=Background
|
||||
r0 c10..26 fg=Accent bg=Background
|
||||
r0 c26..28 bg=Background
|
||||
r0 c28..32 fg=Muted bg=Background
|
||||
r0 c32..40 fg=Foreground bg=Background
|
||||
r1 c0..40 bg=Background
|
||||
r2 c0..15 fg=Foreground bg=Background bold
|
||||
r2 c15..40 bg=Background
|
||||
r3 c0..6 fg=Foreground bg=Background
|
||||
r3 c6..8 bg=Background
|
||||
r3 c8..26 fg=Accent bg=Background
|
||||
r3 c26..28 bg=Background
|
||||
r3 c28..40 fg=Muted bg=Background
|
||||
r4 c0..6 fg=Foreground bg=Background
|
||||
r4 c6..8 bg=Background
|
||||
r4 c8..20 fg=Accent bg=Background
|
||||
r4 c20..28 bg=Background
|
||||
r4 c28..39 fg=Muted bg=Background
|
||||
r4 c39..40 bg=Background
|
||||
r5 c0..6 fg=Foreground bg=Background
|
||||
r5 c6..8 bg=Background
|
||||
r5 c8..13 fg=Accent bg=Background
|
||||
r5 c13..28 bg=Background
|
||||
r5 c28..39 fg=Muted bg=Background
|
||||
r5 c39..40 bg=Background
|
||||
r6 c0..6 fg=Foreground bg=Background
|
||||
r6 c6..28 bg=Background
|
||||
r6 c28..36 fg=Muted bg=Background
|
||||
r6 c36..40 bg=Background
|
||||
r7 c0..7 fg=Foreground bg=Background bold
|
||||
r7 c7..9 bg=Background
|
||||
r7 c9..13 fg=Muted bg=Background
|
||||
r7 c13..20 fg=Foreground bg=Background
|
||||
r7 c20..40 bg=Background
|
||||
r8 c0..8 fg=Muted bg=Background
|
||||
r8 c8..9 fg=Dim bg=Background
|
||||
r8 c9..11 bg=Background
|
||||
r8 c11..18 fg=Accent bg=Background
|
||||
r8 c18..31 bg=Background
|
||||
r8 c31..38 fg=Accent bg=Background
|
||||
r8 c38..40 bg=Background
|
||||
r9 c0..8 fg=Muted bg=Background
|
||||
r9 c8..9 fg=Dim bg=Background
|
||||
r9 c9..12 fg=Accent bg=Background
|
||||
r9 c12..18 bg=Background
|
||||
r9 c18..19 fg=Accent bg=Background
|
||||
r9 c19..29 bg=Background
|
||||
r9 c29..32 fg=Accent bg=Background
|
||||
r9 c32..37 bg=Background
|
||||
r9 c37..40 fg=Accent bg=Background
|
||||
r10 c0..8 fg=Muted bg=Background
|
||||
r10 c8..9 fg=Dim bg=Background
|
||||
r10 c9..10 fg=Accent bg=Background
|
||||
r10 c10..19 bg=Background
|
||||
r10 c19..22 fg=Accent bg=Background
|
||||
r10 c22..28 bg=Background
|
||||
r10 c28..30 fg=Accent bg=Background
|
||||
r10 c30..40 bg=Background
|
||||
r11 c0..8 fg=Muted bg=Background
|
||||
r11 c8..9 fg=Dim bg=Background
|
||||
r11 c9..21 bg=Background
|
||||
r11 c21..28 fg=Accent bg=Background
|
||||
r11 c28..40 bg=Background
|
||||
|
||||
== dark-truecolor · 80x12
|
||||
Requests ▂▁▁▁▁▁▂▂▂▃▃▄▅▆▆▆▇▇████▇▇▆▆▅▅ now 45.6 rpm min 17.2 rpm max 69.7 rpm
|
||||
|
||||
Tokens by model
|
||||
opus ██████████████████████████████████████████████████████████ 12.4k tokens
|
||||
sonnet █████████████████████████████████████ 7900 tokens
|
||||
haiku ██████████████▌ 3100 tokens
|
||||
cache 0 tokens
|
||||
Latency now 45.6 ms min 17.2 ms max 69.7 ms
|
||||
69.7 ms │ ⣀⠤⠒⠊⠑⠒⠒⠒⠒⠒⠒⠢⠤⢄⣀ ⣀ ⣀⡠⠤⠒⠒⠉⠉⠉⠉⠢⢄⣀⣀
|
||||
│ ⣀⠤⠒⠉ ⠑⠤⣀ ⢀⡠⠒⠉ ⠉ ⠉⠑⠒⠤⣀
|
||||
│⠉ ⠉⠑⠒⠤⢄⣀ ⡠⠔⠊⠁
|
||||
17.2 ms │ ⠉⠑⢄⣀⣀⣀⣀⣀⠤⠤⠒⠊⠒⠊⠉
|
||||
---
|
||||
r0 c0..8 fg=Foreground bg=Background bold
|
||||
r0 c8..10 bg=Background
|
||||
r0 c10..38 fg=Accent bg=Background
|
||||
r0 c38..40 bg=Background
|
||||
r0 c40..44 fg=Muted bg=Background
|
||||
r0 c44..52 fg=Foreground bg=Background
|
||||
r0 c52..54 bg=Background
|
||||
r0 c54..58 fg=Muted bg=Background
|
||||
r0 c58..66 fg=Foreground bg=Background
|
||||
r0 c66..68 bg=Background
|
||||
r0 c68..72 fg=Muted bg=Background
|
||||
r0 c72..80 fg=Foreground bg=Background
|
||||
r1 c0..80 bg=Background
|
||||
r2 c0..15 fg=Foreground bg=Background bold
|
||||
r2 c15..80 bg=Background
|
||||
r3 c0..6 fg=Foreground bg=Background
|
||||
r3 c6..8 bg=Background
|
||||
r3 c8..66 fg=Accent bg=Background
|
||||
r3 c66..68 bg=Background
|
||||
r3 c68..80 fg=Muted bg=Background
|
||||
r4 c0..6 fg=Foreground bg=Background
|
||||
r4 c6..8 bg=Background
|
||||
r4 c8..45 fg=Accent bg=Background
|
||||
r4 c45..68 bg=Background
|
||||
r4 c68..79 fg=Muted bg=Background
|
||||
r4 c79..80 bg=Background
|
||||
r5 c0..6 fg=Foreground bg=Background
|
||||
r5 c6..8 bg=Background
|
||||
r5 c8..23 fg=Accent bg=Background
|
||||
r5 c23..68 bg=Background
|
||||
r5 c68..79 fg=Muted bg=Background
|
||||
r5 c79..80 bg=Background
|
||||
r6 c0..6 fg=Foreground bg=Background
|
||||
r6 c6..68 bg=Background
|
||||
r6 c68..76 fg=Muted bg=Background
|
||||
r6 c76..80 bg=Background
|
||||
r7 c0..7 fg=Foreground bg=Background bold
|
||||
r7 c7..9 bg=Background
|
||||
r7 c9..13 fg=Muted bg=Background
|
||||
r7 c13..20 fg=Foreground bg=Background
|
||||
r7 c20..22 bg=Background
|
||||
r7 c22..26 fg=Muted bg=Background
|
||||
r7 c26..33 fg=Foreground bg=Background
|
||||
r7 c33..35 bg=Background
|
||||
r7 c35..39 fg=Muted bg=Background
|
||||
r7 c39..46 fg=Foreground bg=Background
|
||||
r7 c46..80 bg=Background
|
||||
r8 c0..8 fg=Muted bg=Background
|
||||
r8 c8..9 fg=Dim bg=Background
|
||||
r8 c9..14 bg=Background
|
||||
r8 c14..29 fg=Accent bg=Background
|
||||
r8 c29..60 bg=Background
|
||||
r8 c60..61 fg=Accent bg=Background
|
||||
r8 c61..62 bg=Background
|
||||
r8 c62..75 fg=Accent bg=Background
|
||||
r8 c75..80 bg=Background
|
||||
r9 c0..8 fg=Muted bg=Background
|
||||
r9 c8..9 fg=Dim bg=Background
|
||||
r9 c9..10 bg=Background
|
||||
r9 c10..14 fg=Accent bg=Background
|
||||
r9 c14..29 bg=Background
|
||||
r9 c29..32 fg=Accent bg=Background
|
||||
r9 c32..56 bg=Background
|
||||
r9 c56..60 fg=Accent bg=Background
|
||||
r9 c60..61 bg=Background
|
||||
r9 c61..62 fg=Accent bg=Background
|
||||
r9 c62..75 bg=Background
|
||||
r9 c75..80 fg=Accent bg=Background
|
||||
r10 c0..8 fg=Muted bg=Background
|
||||
r10 c8..9 fg=Dim bg=Background
|
||||
r10 c9..10 fg=Accent bg=Background
|
||||
r10 c10..32 bg=Background
|
||||
r10 c32..38 fg=Accent bg=Background
|
||||
r10 c38..53 bg=Background
|
||||
r10 c53..57 fg=Accent bg=Background
|
||||
r10 c57..80 bg=Background
|
||||
r11 c0..8 fg=Muted bg=Background
|
||||
r11 c8..9 fg=Dim bg=Background
|
||||
r11 c9..38 bg=Background
|
||||
r11 c38..53 fg=Accent bg=Background
|
||||
r11 c53..80 bg=Background
|
||||
|
||||
== dark-truecolor · 120x12
|
||||
Requests ▄▅▆▆▇██▇██▇▇▇▇▅▅▄▄▃▃▂▁▁▁▁▁▂▂▂▃▃▄▅▆▆▆▇▇████▇▇▆▆▅▅ now 45.6 rpm min 17.2 rpm max 69.7 rpm
|
||||
|
||||
Tokens by model
|
||||
opus ██████████████████████████████████████████████████████████████████████████████████████████████████ 12.4k tokens
|
||||
sonnet ██████████████████████████████████████████████████████████████▍ 7900 tokens
|
||||
haiku ████████████████████████▌ 3100 tokens
|
||||
cache 0 tokens
|
||||
Latency now 45.6 ms min 17.2 ms max 69.7 ms
|
||||
69.7 ms │ ⢀⣀⠤⠤⠒⠒⠊⠉⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠢⠤⠤⠤⣀⣀ ⣀⡀ ⣀⣀⠤⠤⠒⠒⠒⠊⠉⠉⠉⠉⠉⠉⠒⠤⢄⣀⣀⣀
|
||||
│ ⢀⣀⠤⠔⠒⠉⠁ ⠉⠢⠤⣀⣀ ⣀⡠⠤⠒⠊⠉ ⠈⠉ ⠉⠉⠑⠒⠒⠤⠤⣀
|
||||
│⠉⠁ ⠉⠉⠒⠒⠢⠤⢄⣀⣀ ⢀⣀⠤⠔⠊⠉
|
||||
17.2 ms │ ⠉⠉⠑⠢⢄⣀⣀⣀⣀⣀⣀⣀⡠⠤⠤⠤⠒⠒⠊⠑⠒⠒⠉⠁
|
||||
---
|
||||
r0 c0..8 fg=Foreground bg=Background bold
|
||||
r0 c8..10 bg=Background
|
||||
r0 c10..58 fg=Accent bg=Background
|
||||
r0 c58..60 bg=Background
|
||||
r0 c60..64 fg=Muted bg=Background
|
||||
r0 c64..72 fg=Foreground bg=Background
|
||||
r0 c72..74 bg=Background
|
||||
r0 c74..78 fg=Muted bg=Background
|
||||
r0 c78..86 fg=Foreground bg=Background
|
||||
r0 c86..88 bg=Background
|
||||
r0 c88..92 fg=Muted bg=Background
|
||||
r0 c92..100 fg=Foreground bg=Background
|
||||
r0 c100..120 bg=Background
|
||||
r1 c0..120 bg=Background
|
||||
r2 c0..15 fg=Foreground bg=Background bold
|
||||
r2 c15..120 bg=Background
|
||||
r3 c0..6 fg=Foreground bg=Background
|
||||
r3 c6..8 bg=Background
|
||||
r3 c8..106 fg=Accent bg=Background
|
||||
r3 c106..108 bg=Background
|
||||
r3 c108..120 fg=Muted bg=Background
|
||||
r4 c0..6 fg=Foreground bg=Background
|
||||
r4 c6..8 bg=Background
|
||||
r4 c8..71 fg=Accent bg=Background
|
||||
r4 c71..108 bg=Background
|
||||
r4 c108..119 fg=Muted bg=Background
|
||||
r4 c119..120 bg=Background
|
||||
r5 c0..6 fg=Foreground bg=Background
|
||||
r5 c6..8 bg=Background
|
||||
r5 c8..33 fg=Accent bg=Background
|
||||
r5 c33..108 bg=Background
|
||||
r5 c108..119 fg=Muted bg=Background
|
||||
r5 c119..120 bg=Background
|
||||
r6 c0..6 fg=Foreground bg=Background
|
||||
r6 c6..108 bg=Background
|
||||
r6 c108..116 fg=Muted bg=Background
|
||||
r6 c116..120 bg=Background
|
||||
r7 c0..7 fg=Foreground bg=Background bold
|
||||
r7 c7..9 bg=Background
|
||||
r7 c9..13 fg=Muted bg=Background
|
||||
r7 c13..20 fg=Foreground bg=Background
|
||||
r7 c20..22 bg=Background
|
||||
r7 c22..26 fg=Muted bg=Background
|
||||
r7 c26..33 fg=Foreground bg=Background
|
||||
r7 c33..35 bg=Background
|
||||
r7 c35..39 fg=Muted bg=Background
|
||||
r7 c39..46 fg=Foreground bg=Background
|
||||
r7 c46..120 bg=Background
|
||||
r8 c0..8 fg=Muted bg=Background
|
||||
r8 c8..9 fg=Dim bg=Background
|
||||
r8 c9..16 bg=Background
|
||||
r8 c16..40 fg=Accent bg=Background
|
||||
r8 c40..89 bg=Background
|
||||
r8 c89..91 fg=Accent bg=Background
|
||||
r8 c91..92 bg=Background
|
||||
r8 c92..112 fg=Accent bg=Background
|
||||
r8 c112..120 bg=Background
|
||||
r9 c0..8 fg=Muted bg=Background
|
||||
r9 c8..9 fg=Dim bg=Background
|
||||
r9 c9..10 bg=Background
|
||||
r9 c10..17 fg=Accent bg=Background
|
||||
r9 c17..40 bg=Background
|
||||
r9 c40..45 fg=Accent bg=Background
|
||||
r9 c45..83 bg=Background
|
||||
r9 c83..89 fg=Accent bg=Background
|
||||
r9 c89..90 bg=Background
|
||||
r9 c90..92 fg=Accent bg=Background
|
||||
r9 c92..112 bg=Background
|
||||
r9 c112..120 fg=Accent bg=Background
|
||||
r10 c0..8 fg=Muted bg=Background
|
||||
r10 c8..9 fg=Dim bg=Background
|
||||
r10 c9..11 fg=Accent bg=Background
|
||||
r10 c11..45 bg=Background
|
||||
r10 c45..54 fg=Accent bg=Background
|
||||
r10 c54..77 bg=Background
|
||||
r10 c77..83 fg=Accent bg=Background
|
||||
r10 c83..120 bg=Background
|
||||
r11 c0..8 fg=Muted bg=Background
|
||||
r11 c8..9 fg=Dim bg=Background
|
||||
r11 c9..54 bg=Background
|
||||
r11 c54..78 fg=Accent bg=Background
|
||||
r11 c78..120 bg=Background
|
||||
|
||||
== no-color · 40x12
|
||||
Requests ▁▃▅▄▅▆▇███▆▅▄▃▂▁ now 45.6 rpm
|
||||
|
||||
Tokens by model
|
||||
opus ██████████████████ 12.4k tokens
|
||||
sonnet ███████████▌ 7900 tokens
|
||||
haiku ████▌ 3100 tokens
|
||||
cache 0 tokens
|
||||
Latency now 45.6 ms
|
||||
69.7 ms │ ⡠⠒⠑⠒⠒⠢⣀ ⡀⡠⠔⠉⠉⢢⡀
|
||||
│⢀⠔⠁ ⢇ ⢀⠔⠉ ⠈⠑⢄
|
||||
│⠁ ⠑⠢⡀ ⡠⠊
|
||||
17.2 ms │ ⠈⠱⣀⣀⠤⠊⠊
|
||||
|
||||
== no-color · 80x12
|
||||
Requests ▂▁▁▁▁▁▂▂▂▃▃▄▅▆▆▆▇▇████▇▇▆▆▅▅ now 45.6 rpm min 17.2 rpm max 69.7 rpm
|
||||
|
||||
Tokens by model
|
||||
opus ██████████████████████████████████████████████████████████ 12.4k tokens
|
||||
sonnet █████████████████████████████████████ 7900 tokens
|
||||
haiku ██████████████▌ 3100 tokens
|
||||
cache 0 tokens
|
||||
Latency now 45.6 ms min 17.2 ms max 69.7 ms
|
||||
69.7 ms │ ⣀⠤⠒⠊⠑⠒⠒⠒⠒⠒⠒⠢⠤⢄⣀ ⣀ ⣀⡠⠤⠒⠒⠉⠉⠉⠉⠢⢄⣀⣀
|
||||
│ ⣀⠤⠒⠉ ⠑⠤⣀ ⢀⡠⠒⠉ ⠉ ⠉⠑⠒⠤⣀
|
||||
│⠉ ⠉⠑⠒⠤⢄⣀ ⡠⠔⠊⠁
|
||||
17.2 ms │ ⠉⠑⢄⣀⣀⣀⣀⣀⠤⠤⠒⠊⠒⠊⠉
|
||||
|
||||
== no-color · 120x12
|
||||
Requests ▄▅▆▆▇██▇██▇▇▇▇▅▅▄▄▃▃▂▁▁▁▁▁▂▂▂▃▃▄▅▆▆▆▇▇████▇▇▆▆▅▅ now 45.6 rpm min 17.2 rpm max 69.7 rpm
|
||||
|
||||
Tokens by model
|
||||
opus ██████████████████████████████████████████████████████████████████████████████████████████████████ 12.4k tokens
|
||||
sonnet ██████████████████████████████████████████████████████████████▍ 7900 tokens
|
||||
haiku ████████████████████████▌ 3100 tokens
|
||||
cache 0 tokens
|
||||
Latency now 45.6 ms min 17.2 ms max 69.7 ms
|
||||
69.7 ms │ ⢀⣀⠤⠤⠒⠒⠊⠉⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠢⠤⠤⠤⣀⣀ ⣀⡀ ⣀⣀⠤⠤⠒⠒⠒⠊⠉⠉⠉⠉⠉⠉⠒⠤⢄⣀⣀⣀
|
||||
│ ⢀⣀⠤⠔⠒⠉⠁ ⠉⠢⠤⣀⣀ ⣀⡠⠤⠒⠊⠉ ⠈⠉ ⠉⠉⠑⠒⠒⠤⠤⣀
|
||||
│⠉⠁ ⠉⠉⠒⠒⠢⠤⢄⣀⣀ ⢀⣀⠤⠔⠊⠉
|
||||
17.2 ms │ ⠉⠉⠑⠢⢄⣀⣀⣀⣀⣀⣀⣀⡠⠤⠤⠤⠒⠒⠊⠑⠒⠒⠉⠁
|
||||
|
||||
== ascii · 40x12
|
||||
Requests _.---=####=--.__ now 45.6 rpm
|
||||
|
||||
Tokens by model
|
||||
opus ################## 12.4k tokens
|
||||
sonnet ############ 7900 tokens
|
||||
haiku ##### 3100 tokens
|
||||
cache 0 tokens
|
||||
Latency now 45.6 ms
|
||||
69.7 ms | ....... .....:.
|
||||
|... : ... ...
|
||||
|. ... ..
|
||||
17.2 ms | .:.....
|
||||
|
||||
== ascii · 80x12
|
||||
Requests _________..--===########==-- now 45.6 rpm min 17.2 rpm max 69.7 rpm
|
||||
|
||||
Tokens by model
|
||||
opus ########################################################## 12.4k tokens
|
||||
sonnet ##################################### 7900 tokens
|
||||
haiku ############### 3100 tokens
|
||||
cache 0 tokens
|
||||
Latency now 45.6 ms min 17.2 ms max 69.7 ms
|
||||
69.7 ms | ............... . .............
|
||||
| .... ... .... . .....
|
||||
|. ...... ....
|
||||
17.2 ms | ...............
|
||||
|
||||
== ascii · 120x12
|
||||
Requests --==##########----.._________..--===########==-- now 45.6 rpm min 17.2 rpm max 69.7 rpm
|
||||
|
||||
Tokens by model
|
||||
opus ################################################################################################## 12.4k tokens
|
||||
sonnet ############################################################### 7900 tokens
|
||||
haiku ######################### 3100 tokens
|
||||
cache 0 tokens
|
||||
Latency now 45.6 ms min 17.2 ms max 69.7 ms
|
||||
69.7 ms | ........................ .. ....................
|
||||
| ....... ..... ...... .. ........
|
||||
|.. ......... ......
|
||||
17.2 ms | ........................
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
//! Gallery: data displays fully drawn, mid draw-in, empty and from agent JSON.
|
||||
|
||||
use ratatui::{buffer::Buffer, layout::Rect};
|
||||
|
||||
use super::Entry;
|
||||
use crate::{DataBarItem, DataBars, DataDisplay, DataSparkline, DataTrend, Paint, Theme};
|
||||
|
||||
fn series() -> Vec<f64> {
|
||||
(0..64)
|
||||
.map(|i| 120.0 + 60.0 * (f64::from(i) / 6.0).sin() + f64::from((i * 7) % 11) * 3.0)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn items() -> Vec<DataBarItem> {
|
||||
vec![
|
||||
DataBarItem::new("engine", 412.0),
|
||||
DataBarItem::new("tui", 268.0),
|
||||
DataBarItem::new("runtime", 131.0),
|
||||
DataBarItem::new("protocol", 44.0),
|
||||
]
|
||||
}
|
||||
|
||||
fn row(area: Rect, y: u16, h: u16) -> Rect {
|
||||
Rect::new(area.x, area.y.saturating_add(y), area.width, h).intersection(area)
|
||||
}
|
||||
|
||||
fn sparklines(area: Rect, buf: &mut Buffer, theme: &Theme) {
|
||||
let values = series();
|
||||
DataSparkline::new("Requests", &values)
|
||||
.unit("rpm")
|
||||
.paint(row(area, 0, 1), buf, theme);
|
||||
DataSparkline::new("Requests", &values)
|
||||
.unit("rpm")
|
||||
.drawn(0.45)
|
||||
.paint(row(area, 1, 1), buf, theme);
|
||||
DataSparkline::new("Requests", &[]).paint(row(area, 2, 1), buf, theme);
|
||||
}
|
||||
|
||||
fn bars(area: Rect, buf: &mut Buffer, theme: &Theme) {
|
||||
let items = items();
|
||||
DataBars::new("Tests by crate", &items).paint(row(area, 0, 5), buf, theme);
|
||||
}
|
||||
|
||||
fn bars_growing(area: Rect, buf: &mut Buffer, theme: &Theme) {
|
||||
let items = items();
|
||||
DataBars::new("Tests by crate", &items)
|
||||
.previous(&[300.0, 300.0, 300.0, 300.0])
|
||||
.drawn(0.5)
|
||||
.paint(row(area, 0, 5), buf, theme);
|
||||
}
|
||||
|
||||
fn trend(area: Rect, buf: &mut Buffer, theme: &Theme) {
|
||||
let values = series();
|
||||
DataTrend::new("Latency", &values)
|
||||
.unit("ms")
|
||||
.rows(7)
|
||||
.paint(area, buf, theme);
|
||||
}
|
||||
|
||||
fn trend_drawing(area: Rect, buf: &mut Buffer, theme: &Theme) {
|
||||
let values = series();
|
||||
DataTrend::new("Latency", &values)
|
||||
.unit("ms")
|
||||
.rows(7)
|
||||
.drawn(0.6)
|
||||
.paint(area, buf, theme);
|
||||
}
|
||||
|
||||
fn from_agent(area: Rect, buf: &mut Buffer, theme: &Theme) {
|
||||
let shown = DataDisplay::from_json(
|
||||
r#"{"kind":"bars","label":"Tokens this turn","unit":"tokens",
|
||||
"items":[{"label":"read","value":18200},{"label":"wrote","value":2400},
|
||||
{"label":"cached","value":61000}]}"#,
|
||||
)
|
||||
.expect("gallery fixture is a valid data display");
|
||||
shown.view().paint(area, buf, theme);
|
||||
}
|
||||
|
||||
pub(crate) fn entries() -> Vec<Entry> {
|
||||
vec![
|
||||
Entry {
|
||||
name: "data-sparkline",
|
||||
width: 80,
|
||||
height: 3,
|
||||
draw: sparklines,
|
||||
},
|
||||
Entry {
|
||||
name: "data-bars",
|
||||
width: 60,
|
||||
height: 5,
|
||||
draw: bars,
|
||||
},
|
||||
Entry {
|
||||
name: "data-bars-growing",
|
||||
width: 60,
|
||||
height: 5,
|
||||
draw: bars_growing,
|
||||
},
|
||||
Entry {
|
||||
name: "data-trend",
|
||||
width: 72,
|
||||
height: 7,
|
||||
draw: trend,
|
||||
},
|
||||
Entry {
|
||||
name: "data-trend-drawing",
|
||||
width: 72,
|
||||
height: 7,
|
||||
draw: trend_drawing,
|
||||
},
|
||||
Entry {
|
||||
name: "data-from-agent",
|
||||
width: 60,
|
||||
height: 4,
|
||||
draw: from_agent,
|
||||
},
|
||||
]
|
||||
}
|
||||
+2
@@ -31,6 +31,7 @@ mod approval;
|
||||
mod artifacts;
|
||||
mod atmosphere;
|
||||
mod attention;
|
||||
mod chart;
|
||||
mod chrome;
|
||||
mod display;
|
||||
mod habitat;
|
||||
@@ -104,6 +105,7 @@ pub fn entries() -> Vec<Entry> {
|
||||
lists::entries(),
|
||||
chrome::entries(),
|
||||
display::entries(),
|
||||
chart::entries(),
|
||||
approval::entries(),
|
||||
motion::entries(),
|
||||
workspace::entries(),
|
||||
|
||||
Reference in new issue
Block a user