mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-12 18:18:10 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f56b9b9cc |
No files matched your search
@@ -86,6 +86,7 @@ use codewhale_skill_install::install::{
|
||||
pub mod dsh;
|
||||
mod place;
|
||||
mod stage;
|
||||
mod summary;
|
||||
mod tarball;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -93,6 +94,7 @@ 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
|
||||
|
||||
@@ -0,0 +1,865 @@
|
||||
//! 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
@@ -2,16 +2,6 @@
|
||||
|
||||
## 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,7 +57,6 @@ 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
@@ -1,276 +0,0 @@
|
||||
//! 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,12 +59,10 @@ 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,14 +366,6 @@ 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
@@ -1,360 +0,0 @@
|
||||
---
|
||||
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
@@ -1,118 +0,0 @@
|
||||
//! 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,7 +31,6 @@ mod approval;
|
||||
mod artifacts;
|
||||
mod atmosphere;
|
||||
mod attention;
|
||||
mod chart;
|
||||
mod chrome;
|
||||
mod display;
|
||||
mod habitat;
|
||||
@@ -105,7 +104,6 @@ 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