mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-12 18:18:10 -10:00
Compare commits
1
Commits
main
..
ci/cw-8161
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d692be3473 |
No files matched your search
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- **Plugin lifecycle from the shell:** `codewhale plugins` now has `install <spec>`, `show <name>`, `update <name>` and `uninstall <name>` (each with `--json`) alongside `status`, `restage`, `trust`, `enable`, `disable` and `revoke`. `plugins install` uses the same reviewed installer as `codewhale install`, `show` prints the capability review without prompting or trusting, `update` refreshes from the recorded source and reports either the new version or `already current`, and `uninstall` disables an enabled bundle first and asks in a terminal unless `--yes` is passed (non-interactive and `--json` runs require `--yes`). (CW-8161)
|
||||
|
||||
- **Renaming sessions with Chinese titles:** the session picker no longer rejects a Chinese, Japanese, Korean or emoji title of more than about 33 characters as "Title must be 1–100 characters". It now counts characters, as the message says and as `/rename` and the Runtime API already did, so any title up to 100 characters is accepted. Thanks [@luochen211](https://github.com/luochen211) (PR 7322).
|
||||
- **Non-ASCII file names:** `/diff`, the `@git` and `@diff` mentions and the Runtime API workspace diff now show paths with Chinese, emoji and other non-ASCII characters as written instead of as quoted octal escapes, and the turn context no longer lists the workspace root as an empty "Active paths" entry. Thanks [@luochen211](https://github.com/luochen211) (PR 7317).
|
||||
- **Session names after a crash, and keep-forever snapshot retention:** a session name saved right after a crash that cut off the previous session-index line is no longer lost, because the new entry now starts on its own line; and a very large `[snapshots] max_age_days` (such as `9223372036854775807` for "keep forever") no longer wraps around and deletes every undo snapshot at session start. Thanks [@luochen211](https://github.com/luochen211) (PR 7316).
|
||||
|
||||
@@ -234,10 +234,10 @@ enum Commands {
|
||||
#[command(about = "Install a plugin bundle without trusting or enabling it")]
|
||||
Install(TuiPassthroughArgs),
|
||||
#[command(
|
||||
about = "Inspect, review, trust, enable, disable or revoke plugin bundles",
|
||||
about = "Install, inspect, review, trust, enable, disable, update, uninstall or revoke plugin bundles",
|
||||
alias = "plugin",
|
||||
override_usage = "codewhale plugins <COMMAND>",
|
||||
after_help = "Commands:\n status [NAME] [--json] Inspect plugin status and diagnostics\n list [NAME] [--json] Alias of status\n restage <NAME> [--json] Restore an eligible reviewed plugin snapshot\n trust <NAME> [--json] Show the capability review; in a terminal, then ask\n trust <NAME> --review-token T [--yes] [--json] Trust exactly the content the bundle review token names\n enable <NAME> [--json] Enable a trusted plugin\n disable <NAME> [--json] Disable a plugin, keeping its trust\n revoke <NAME> [--json] Revoke trust\n\nThe bundle review token ties trust to exact content; it does not show that anyone read the review,\nand it is not the host-service review token. A --yes trust is recorded as the command line. When\nCODEWHALE_AGENT_PROJECT marks the process, as it does every command the agent runs, trust and enable\nare the agent's, for that project only; that mark is a label the agent could remove in Full Access."
|
||||
after_help = "Commands:\n install <SPEC> [--json] Install a plugin bundle without trusting or enabling it\n status [NAME] [--json] Inspect plugin status and diagnostics\n list [NAME] [--json] Alias of status\n show <NAME> [--json] Show the capability review without prompting or trusting\n restage <NAME> [--json] Restore an eligible reviewed plugin snapshot\n trust <NAME> [--json] Show the capability review; in a terminal, then ask\n trust <NAME> --review-token T [--yes] [--json] Trust exactly the content the bundle review token names\n enable <NAME> [--json] Enable a trusted plugin\n disable <NAME> [--json] Disable a plugin, keeping its trust\n update <NAME> [--json] Update an installed plugin bundle from its recorded source\n uninstall <NAME> [--yes] [--json] Disable if needed and remove an installed plugin bundle\n revoke <NAME> [--json] Revoke trust\n\nThe bundle review token ties trust to exact content; it does not show that anyone read the review,\nand it is not the host-service review token. A --yes trust is recorded as the command line. When\nCODEWHALE_AGENT_PROJECT marks the process, as it does every command the agent runs, trust and enable\nare the agent's, for that project only; that mark is a label the agent could remove in Full Access."
|
||||
)]
|
||||
Plugins(TuiPassthroughArgs),
|
||||
/// Bootstrap MCP config and/or skills directories.
|
||||
|
||||
@@ -95,11 +95,21 @@ pub async fn execute(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn install_from_cli(
|
||||
pub fn check_network_outcome(outcome: &PluginMutationOutcome) -> Result<()> {
|
||||
match outcome {
|
||||
PluginMutationOutcome::NeedsApproval(host) => {
|
||||
bail!("Network approval required for {host}; use /network allow {host}, then retry")
|
||||
}
|
||||
PluginMutationOutcome::NetworkDenied(host) => bail!("Network access denied for {host}"),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn install_from_cli_receipt(
|
||||
source: String,
|
||||
config: codewhale_runtime::config::Config,
|
||||
mut registry: PluginRegistry,
|
||||
) -> Result<()> {
|
||||
) -> Result<PluginMutationReceipt> {
|
||||
let runtime = tokio::runtime::Handle::current();
|
||||
let policy = crate::activation::extension_host_policy_enabled();
|
||||
let receipt = tokio::task::spawn_blocking(move || {
|
||||
@@ -119,6 +129,16 @@ pub async fn install_from_cli(
|
||||
))
|
||||
})
|
||||
.await??;
|
||||
check_network_outcome(&receipt.outcome)?;
|
||||
Ok(receipt)
|
||||
}
|
||||
|
||||
pub async fn install_from_cli(
|
||||
source: String,
|
||||
config: codewhale_runtime::config::Config,
|
||||
registry: PluginRegistry,
|
||||
) -> Result<()> {
|
||||
let receipt = install_from_cli_receipt(source, config, registry).await?;
|
||||
match receipt.outcome {
|
||||
PluginMutationOutcome::Installed => {
|
||||
println!("Installed {} (disabled, untrusted).", receipt.name);
|
||||
@@ -128,10 +148,6 @@ pub async fn install_from_cli(
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
PluginMutationOutcome::NeedsApproval(host) => {
|
||||
bail!("Network approval required for {host}; use /network allow {host}, then retry")
|
||||
}
|
||||
PluginMutationOutcome::NetworkDenied(host) => bail!("Network access denied for {host}"),
|
||||
_ => bail!("Unexpected plugin install outcome"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- **Plugin lifecycle from the shell:** `codewhale plugins` now has `install <spec>`, `show <name>`, `update <name>` and `uninstall <name>` (each with `--json`) alongside `status`, `restage`, `trust`, `enable`, `disable` and `revoke`. `plugins install` uses the same reviewed installer as `codewhale install`, `show` prints the capability review without prompting or trusting, `update` refreshes from the recorded source and reports either the new version or `already current`, and `uninstall` disables an enabled bundle first and asks in a terminal unless `--yes` is passed (non-interactive and `--json` runs require `--yes`). (CW-8161)
|
||||
|
||||
- **Renaming sessions with Chinese titles:** the session picker no longer rejects a Chinese, Japanese, Korean or emoji title of more than about 33 characters as "Title must be 1–100 characters". It now counts characters, as the message says and as `/rename` and the Runtime API already did, so any title up to 100 characters is accepted. Thanks [@luochen211](https://github.com/luochen211) (PR 7322).
|
||||
- **Non-ASCII file names:** `/diff`, the `@git` and `@diff` mentions and the Runtime API workspace diff now show paths with Chinese, emoji and other non-ASCII characters as written instead of as quoted octal escapes, and the turn context no longer lists the workspace root as an empty "Active paths" entry. Thanks [@luochen211](https://github.com/luochen211) (PR 7317).
|
||||
- **Session names after a crash, and keep-forever snapshot retention:** a session name saved right after a crash that cut off the previous session-index line is no longer lost, because the new entry now starts on its own line; and a very large `[snapshots] max_age_days` (such as `9223372036854775807` for "keep forever") no longer wraps around and deletes every undo snapshot at session start. Thanks [@luochen211](https://github.com/luochen211) (PR 7316).
|
||||
|
||||
@@ -278,6 +278,42 @@ fn report(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn block_on_mutation<F: std::future::Future>(future: F) -> Result<F::Output> {
|
||||
match tokio::runtime::Handle::try_current() {
|
||||
Ok(handle) => Ok(handle.block_on(future)),
|
||||
Err(_) => Ok(tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()?
|
||||
.block_on(future)),
|
||||
}
|
||||
}
|
||||
|
||||
fn manifest_version_at(path: &Path) -> Option<String> {
|
||||
let manifest_path = codewhale_plugins::agent_plugin::resolve_manifest_path(path)?;
|
||||
codewhale_plugins::manifest::PluginManifest::validate_from_path(&manifest_path)
|
||||
.ok()
|
||||
.map(|validated| validated.manifest.plugin.version)
|
||||
}
|
||||
|
||||
pub(crate) fn show(
|
||||
registry: &PluginRegistry,
|
||||
locale: Locale,
|
||||
name: &str,
|
||||
json: bool,
|
||||
console: &mut dyn Console,
|
||||
) -> Result<()> {
|
||||
let marked = console.agent_marked();
|
||||
let plugin = find(registry, locale, name)?;
|
||||
if json {
|
||||
console.print(&serde_json::to_string_pretty(&review_json(
|
||||
registry, plugin, locale, marked,
|
||||
))?);
|
||||
} else {
|
||||
console.print(&review_text(registry, plugin, locale, marked));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) struct TrustRequest {
|
||||
pub name: String,
|
||||
pub review_token: Option<String>,
|
||||
@@ -315,13 +351,10 @@ pub(crate) fn trust(
|
||||
"`--yes` only skips the confirmation prompt. Trust also needs --review-token, the bundle review token that names the exact content to trust; to see it, {review_again}"
|
||||
);
|
||||
}
|
||||
show(registry, locale, &name, json, console)?;
|
||||
if json {
|
||||
console.print(&serde_json::to_string_pretty(&review_json(
|
||||
registry, plugin, locale, marked,
|
||||
))?);
|
||||
return Ok(());
|
||||
}
|
||||
console.print(&review_text(registry, plugin, locale, marked));
|
||||
if !console.interactive() {
|
||||
console.print(&format!(
|
||||
"\nNot trusted. The bundle review token ties trust to exactly the content shown above; it does not record that anyone read this review. A trust made with it is recorded as {}. To trust, run:\n {}",
|
||||
@@ -358,7 +391,7 @@ pub(crate) fn trust(
|
||||
"Trust needs confirmation: pass --yes with the bundle review token, or run in a terminal to be shown the review and asked"
|
||||
);
|
||||
}
|
||||
console.print(&review_text(registry, plugin, locale, marked));
|
||||
show(registry, locale, &name, false, console)?;
|
||||
if !console.confirm("\nTrust this bundle with exactly the capabilities above?") {
|
||||
console.print("Not trusted.");
|
||||
return Ok(());
|
||||
@@ -370,6 +403,150 @@ pub(crate) fn trust(
|
||||
report(registry, locale, &name, "trusted", json, console)
|
||||
}
|
||||
|
||||
pub(crate) fn update(
|
||||
registry: &mut Arc<PluginRegistry>,
|
||||
locale: Locale,
|
||||
name: &str,
|
||||
network: &codewhale_net::network_policy::NetworkPolicy,
|
||||
json: bool,
|
||||
console: &mut dyn Console,
|
||||
) -> Result<()> {
|
||||
use codewhale_plugins::mutation::{
|
||||
PluginMutationContext, PluginMutationOutcome, PluginMutationRequest, check_network_outcome,
|
||||
execute,
|
||||
};
|
||||
let before = find(registry, locale, name)?.clone();
|
||||
let selector = before.name().to_string();
|
||||
let ctx = PluginMutationContext {
|
||||
network,
|
||||
max_size: codewhale_plugins::install::DEFAULT_MAX_SIZE_BYTES,
|
||||
};
|
||||
let receipt = block_on_mutation(execute(
|
||||
PluginMutationRequest::Update { selector },
|
||||
&ctx,
|
||||
Arc::make_mut(registry),
|
||||
))?
|
||||
.map_err(|error| failed(locale, &format!("{error:#}")))?;
|
||||
check_network_outcome(&receipt.outcome)?;
|
||||
match receipt.outcome {
|
||||
PluginMutationOutcome::Updated => {
|
||||
*registry = registry.rediscover_for_workspace(registry.workspace());
|
||||
let version = registry
|
||||
.get(&receipt.name)
|
||||
.map(|plugin| plugin.manifest.plugin.version.clone())
|
||||
.or_else(|| receipt.path.as_deref().and_then(manifest_version_at))
|
||||
.unwrap_or_default();
|
||||
if json {
|
||||
let mut record = registry
|
||||
.get(&receipt.name)
|
||||
.map(crate::plugin_cli_record)
|
||||
.unwrap_or_else(|| {
|
||||
serde_json::json!({
|
||||
"name": text(&receipt.name),
|
||||
"version": text(&version),
|
||||
"path": receipt.path.as_ref().map(|p| text(&p.to_string_lossy())),
|
||||
"content_hash": receipt.installed_content_hash.or(receipt.content_hash),
|
||||
})
|
||||
});
|
||||
if let Some(object) = record.as_object_mut() {
|
||||
object.insert("action".to_string(), "updated".into());
|
||||
}
|
||||
console.print(&serde_json::to_string_pretty(&record)?);
|
||||
} else {
|
||||
let action = if version.is_empty() {
|
||||
"updated".to_string()
|
||||
} else {
|
||||
format!("updated to {}", text(&version))
|
||||
};
|
||||
console.print(
|
||||
&tr(locale, MessageId::CmdPluginBundleMutationSuccess)
|
||||
.replace("{name}", &text(&receipt.name))
|
||||
.replace("{action}", &action),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
PluginMutationOutcome::NoChange => {
|
||||
if json {
|
||||
let mut record = crate::plugin_cli_record(&before);
|
||||
if let Some(object) = record.as_object_mut() {
|
||||
object.insert("action".to_string(), "already_current".into());
|
||||
}
|
||||
console.print(&serde_json::to_string_pretty(&record)?);
|
||||
} else {
|
||||
console.print(
|
||||
&tr(locale, MessageId::CmdPluginBundleMutationSuccess)
|
||||
.replace("{name}", &text(&receipt.name))
|
||||
.replace("{action}", "already current"),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
_ => bail!("Unexpected plugin update outcome"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn uninstall(
|
||||
registry: &mut Arc<PluginRegistry>,
|
||||
locale: Locale,
|
||||
name: &str,
|
||||
yes: bool,
|
||||
json: bool,
|
||||
console: &mut dyn Console,
|
||||
) -> Result<()> {
|
||||
use codewhale_plugins::mutation::{PluginMutationContext, PluginMutationRequest, execute};
|
||||
use codewhale_plugins::types::PluginScope;
|
||||
let plugin = find(registry, locale, name)?.clone();
|
||||
let plugin_name = plugin.name().to_string();
|
||||
let version = plugin.manifest.plugin.version.clone();
|
||||
if !yes {
|
||||
if json || !console.interactive() {
|
||||
bail!("Uninstall needs confirmation: pass --yes, or run in a terminal to be asked");
|
||||
}
|
||||
let prompt = if version.is_empty() {
|
||||
format!("Remove {}?", text(&plugin_name))
|
||||
} else {
|
||||
format!("Remove {} {}?", text(&plugin_name), text(&version))
|
||||
};
|
||||
if !console.confirm(&prompt) {
|
||||
console.print("Not removed.");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
if plugin.scope == PluginScope::User && plugin.enabled {
|
||||
Arc::make_mut(registry)
|
||||
.disable(&plugin_name)
|
||||
.map_err(|error| failed(locale, &error))?;
|
||||
}
|
||||
let network = codewhale_net::network_policy::NetworkPolicy::default();
|
||||
let ctx = PluginMutationContext {
|
||||
network: &network,
|
||||
max_size: codewhale_plugins::install::DEFAULT_MAX_SIZE_BYTES,
|
||||
};
|
||||
let receipt = block_on_mutation(execute(
|
||||
PluginMutationRequest::Uninstall {
|
||||
selector: plugin_name,
|
||||
},
|
||||
&ctx,
|
||||
Arc::make_mut(registry),
|
||||
))?
|
||||
.map_err(|error| failed(locale, &format!("{error:#}")))?;
|
||||
if json {
|
||||
console.print(&serde_json::to_string_pretty(&serde_json::json!({
|
||||
"name": text(&receipt.name),
|
||||
"version": text(&version),
|
||||
"action": "uninstalled",
|
||||
}))?);
|
||||
} else {
|
||||
console.print(
|
||||
&tr(locale, MessageId::CmdPluginBundleMutationSuccess)
|
||||
.replace("{name}", &text(&receipt.name))
|
||||
.replace("{action}", "uninstalled"),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn enable(
|
||||
registry: &mut Arc<PluginRegistry>,
|
||||
locale: Locale,
|
||||
|
||||
@@ -1507,3 +1507,258 @@ fn headless_marked_trust_needs_an_install_from_a_folder_in_the_project() {
|
||||
Some(PluginApprover::command_line(Some(registry.workspace())))
|
||||
);
|
||||
}
|
||||
|
||||
fn install_temp_home_plugin(
|
||||
root: &Path,
|
||||
name: &str,
|
||||
version: &str,
|
||||
) -> (
|
||||
std::path::PathBuf,
|
||||
std::sync::Arc<codewhale_plugins::PluginRegistry>,
|
||||
) {
|
||||
let source = root.join("src").join(name);
|
||||
fs::create_dir_all(source.join("skills/hello")).unwrap();
|
||||
fs::write(
|
||||
source.join("plugin.toml"),
|
||||
format!(
|
||||
"schema_version = 1\n[plugin]\nname = {name:?}\nversion = {version:?}\n[skills]\npath = \"skills\"\n"
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
source.join("skills/hello/SKILL.md"),
|
||||
"---\nname: hello\ndescription: hi\n---\nbody\n",
|
||||
)
|
||||
.unwrap();
|
||||
let (app, _temp) = create_test_app(root);
|
||||
let mut registry = app.plugin_registry.clone();
|
||||
let network = codewhale_net::network_policy::NetworkPolicy::default();
|
||||
let ctx = codewhale_plugins::mutation::PluginMutationContext {
|
||||
network: &network,
|
||||
max_size: codewhale_plugins::install::DEFAULT_MAX_SIZE_BYTES,
|
||||
};
|
||||
let parsed = codewhale_plugins::install::PluginInstallSource::parse(
|
||||
source.to_str().expect("utf-8 path"),
|
||||
)
|
||||
.unwrap();
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(codewhale_plugins::mutation::execute(
|
||||
codewhale_plugins::mutation::PluginMutationRequest::Install { source: parsed },
|
||||
&ctx,
|
||||
std::sync::Arc::make_mut(&mut registry),
|
||||
))
|
||||
.unwrap();
|
||||
let registry = registry.rediscover_for_workspace(registry.workspace());
|
||||
(source, registry)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headless_show_prints_review_without_prompting_or_trusting() {
|
||||
let _lock = crate::test_support::lock_test_env();
|
||||
let root = TempDir::new().unwrap();
|
||||
let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", root.path().join("home"));
|
||||
write_mcp_review_bundle(root.path());
|
||||
let (mut app, _temp) = create_test_app(root.path());
|
||||
let Some(AppAction::OpenCommandReview { content, .. }) =
|
||||
plugins_with_kimi_home_override(&mut app, Some("trust review-mcp"), None).action
|
||||
else {
|
||||
panic!("trust opens a confirmation control");
|
||||
};
|
||||
let registry = app.plugin_registry.clone();
|
||||
let token = registry.get("review-mcp").unwrap().review_token();
|
||||
|
||||
let mut interactive = ScriptedConsole {
|
||||
interactive: true,
|
||||
answer: true,
|
||||
..ScriptedConsole::default()
|
||||
};
|
||||
headless::show(®istry, Locale::En, "review-mcp", false, &mut interactive).unwrap();
|
||||
assert!(interactive.prompts.is_empty());
|
||||
assert!(interactive.printed[0].starts_with(&content));
|
||||
assert!(interactive.printed[0].ends_with(&format!("Bundle review token: {token}")));
|
||||
assert!(!registry.get("review-mcp").unwrap().trusted());
|
||||
|
||||
let mut json_console = ScriptedConsole::default();
|
||||
headless::show(®istry, Locale::En, "review-mcp", true, &mut json_console).unwrap();
|
||||
let parsed: serde_json::Value = serde_json::from_str(&json_console.printed[0]).unwrap();
|
||||
assert_eq!(parsed["action"], "review");
|
||||
assert_eq!(parsed["trusted_by_this_command"], false);
|
||||
assert_eq!(parsed["bundle_review_token"], token);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headless_update_reports_current_or_new_version_and_network_approval() {
|
||||
let _lock = crate::test_support::lock_test_env();
|
||||
let root = TempDir::new().unwrap();
|
||||
let home = root.path().join("home");
|
||||
let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &home);
|
||||
let (source, mut registry) = install_temp_home_plugin(root.path(), "demo", "1.0.0");
|
||||
let network = codewhale_net::network_policy::NetworkPolicy::default();
|
||||
|
||||
let mut console = ScriptedConsole::default();
|
||||
headless::update(
|
||||
&mut registry,
|
||||
Locale::En,
|
||||
"demo",
|
||||
&network,
|
||||
false,
|
||||
&mut console,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
console.printed.last().unwrap().contains("already current"),
|
||||
"{:?}",
|
||||
console.printed
|
||||
);
|
||||
|
||||
fs::write(
|
||||
source.join("plugin.toml"),
|
||||
"schema_version = 1\n[plugin]\nname = \"demo\"\nversion = \"1.1.0\"\n[skills]\npath = \"skills\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
headless::update(
|
||||
&mut registry,
|
||||
Locale::En,
|
||||
"demo",
|
||||
&network,
|
||||
false,
|
||||
&mut console,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
console.printed.last().unwrap().contains("updated to 1.1.0"),
|
||||
"{:?}",
|
||||
console.printed
|
||||
);
|
||||
assert_eq!(
|
||||
registry.get("demo").unwrap().manifest.plugin.version,
|
||||
"1.1.0"
|
||||
);
|
||||
|
||||
fs::write(
|
||||
home.join("plugins/demo/.installed-from"),
|
||||
serde_json::json!({
|
||||
"version": 2,
|
||||
"spec": "https://example.com/demo.tar.gz",
|
||||
"source_checksum": "",
|
||||
"content_hash": "",
|
||||
"plugin_name": "demo"
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
let err = headless::update(
|
||||
&mut registry,
|
||||
Locale::En,
|
||||
"demo",
|
||||
&network,
|
||||
false,
|
||||
&mut console,
|
||||
)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(
|
||||
err.contains(
|
||||
"Network approval required for example.com; use /network allow example.com, then retry"
|
||||
),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headless_uninstall_disables_enabled_plugin_first_and_removes_it() {
|
||||
let _lock = crate::test_support::lock_test_env();
|
||||
let root = TempDir::new().unwrap();
|
||||
let home = root.path().join("home");
|
||||
let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &home);
|
||||
let (_source, mut registry) = install_temp_home_plugin(root.path(), "demo", "1.0.0");
|
||||
let token = registry.get("demo").unwrap().review_token();
|
||||
let mut console = ScriptedConsole::default();
|
||||
headless_trust_named(
|
||||
&mut registry,
|
||||
"demo",
|
||||
Some(&token),
|
||||
true,
|
||||
false,
|
||||
&mut console,
|
||||
)
|
||||
.unwrap();
|
||||
headless::enable(&mut registry, Locale::En, "demo", false, &mut console).unwrap();
|
||||
assert!(registry.get("demo").unwrap().enabled);
|
||||
let installed_dir = home.join("plugins/demo");
|
||||
assert!(installed_dir.exists());
|
||||
|
||||
let piped_err = headless::uninstall(
|
||||
&mut registry,
|
||||
Locale::En,
|
||||
"demo",
|
||||
false,
|
||||
false,
|
||||
&mut console,
|
||||
)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(piped_err.contains("--yes"), "{piped_err}");
|
||||
assert!(installed_dir.exists());
|
||||
|
||||
let mut json_interactive = ScriptedConsole {
|
||||
interactive: true,
|
||||
answer: true,
|
||||
..ScriptedConsole::default()
|
||||
};
|
||||
let json_err = headless::uninstall(
|
||||
&mut registry,
|
||||
Locale::En,
|
||||
"demo",
|
||||
false,
|
||||
true,
|
||||
&mut json_interactive,
|
||||
)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(json_err.contains("--yes"), "{json_err}");
|
||||
assert!(json_interactive.prompts.is_empty());
|
||||
assert!(installed_dir.exists());
|
||||
|
||||
let mut declined = ScriptedConsole {
|
||||
interactive: true,
|
||||
answer: false,
|
||||
..ScriptedConsole::default()
|
||||
};
|
||||
headless::uninstall(
|
||||
&mut registry,
|
||||
Locale::En,
|
||||
"demo",
|
||||
false,
|
||||
false,
|
||||
&mut declined,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(declined.prompts.len(), 1);
|
||||
assert_eq!(declined.prompts[0].0, "Remove demo 1.0.0?");
|
||||
assert!(installed_dir.exists());
|
||||
assert!(registry.get("demo").unwrap().enabled);
|
||||
|
||||
let mut accepted = ScriptedConsole {
|
||||
interactive: true,
|
||||
answer: true,
|
||||
..ScriptedConsole::default()
|
||||
};
|
||||
headless::uninstall(
|
||||
&mut registry,
|
||||
Locale::En,
|
||||
"demo",
|
||||
false,
|
||||
false,
|
||||
&mut accepted,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(accepted.prompts.len(), 1);
|
||||
assert_eq!(accepted.prompts[0].0, "Remove demo 1.0.0?");
|
||||
assert!(!installed_dir.exists());
|
||||
let rediscovered = registry.rediscover_for_workspace(registry.workspace());
|
||||
assert!(rediscovered.get("demo").is_none());
|
||||
}
|
||||
+206
-11
@@ -147,7 +147,9 @@ enum Commands {
|
||||
SessionDiagnostics(SessionDiagnosticsArgs),
|
||||
#[command(about = "Install a local, GitHub or version-pinned npm plugin bundle")]
|
||||
Install { source: String },
|
||||
#[command(about = "Inspect, review, trust, enable, disable or revoke plugin bundles")]
|
||||
#[command(
|
||||
about = "Install, inspect, review, trust, enable, disable, update, uninstall or revoke plugin bundles"
|
||||
)]
|
||||
Plugins {
|
||||
#[command(subcommand)]
|
||||
command: PluginsCommand,
|
||||
@@ -278,6 +280,40 @@ enum PluginsCommand {
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
#[command(about = "Show a plugin's capability review without prompting or trusting")]
|
||||
Show {
|
||||
#[arg(value_name = "NAME", help = "Plugin bundle name")]
|
||||
name: String,
|
||||
#[arg(long, help = "Print the review as JSON")]
|
||||
json: bool,
|
||||
},
|
||||
#[command(
|
||||
about = "Install a local, GitHub or version-pinned npm plugin bundle without trusting or enabling it"
|
||||
)]
|
||||
Install {
|
||||
#[arg(value_name = "SPEC")]
|
||||
spec: String,
|
||||
#[arg(long, help = "Print the result as JSON")]
|
||||
json: bool,
|
||||
},
|
||||
#[command(about = "Update an installed user-scope plugin bundle from its recorded source")]
|
||||
Update {
|
||||
#[arg(value_name = "NAME", help = "Plugin bundle name")]
|
||||
name: String,
|
||||
#[arg(long, help = "Print the result as JSON")]
|
||||
json: bool,
|
||||
},
|
||||
#[command(
|
||||
about = "Uninstall an installed user-scope plugin bundle (disables it first if enabled)"
|
||||
)]
|
||||
Uninstall {
|
||||
#[arg(value_name = "NAME", help = "Plugin bundle name")]
|
||||
name: String,
|
||||
#[arg(long, short = 'y', help = "Skip the confirmation prompt")]
|
||||
yes: bool,
|
||||
#[arg(long, help = "Print the result as JSON; requires --yes")]
|
||||
json: bool,
|
||||
},
|
||||
#[command(about = "Restore an eligible reviewed plugin snapshot")]
|
||||
Restage {
|
||||
#[arg(value_name = "NAME")]
|
||||
@@ -2468,7 +2504,7 @@ fn telemetry_command_is_read_only(command: Option<&Commands>) -> bool {
|
||||
|| matches!(
|
||||
command,
|
||||
Some(Commands::Plugins {
|
||||
command: PluginsCommand::Status { .. }
|
||||
command: PluginsCommand::Status { .. } | PluginsCommand::Show { .. }
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -2803,15 +2839,37 @@ async fn run_async_main_dispatch(
|
||||
)
|
||||
.await
|
||||
}
|
||||
Commands::Plugins { command } => tokio::task::spawn_blocking(move || {
|
||||
let _config = load_structural_config_from_cli(&cli)?;
|
||||
let workspace = crate::commands::groups::plugins::headless::agent_project()
|
||||
.unwrap_or_else(|| resolve_workspace(&cli));
|
||||
let registry = plugin_discovery.inspect_registry_for_workspace(&workspace);
|
||||
run_plugins_cli(command, registry)
|
||||
})
|
||||
.await
|
||||
.context("Plugin command failed to finish")?,
|
||||
Commands::Plugins { command } => match command {
|
||||
PluginsCommand::Install { spec, json } => {
|
||||
let config = load_config_from_cli(&cli)?;
|
||||
let default_workspace = resolve_workspace(&cli);
|
||||
let workspace = crate::commands::groups::plugins::headless::agent_project()
|
||||
.unwrap_or_else(|| default_workspace.clone());
|
||||
let registry = if workspace == default_workspace {
|
||||
(*plugin_registry).clone()
|
||||
} else {
|
||||
(*plugin_discovery.inspect_registry_for_workspace(&workspace)).clone()
|
||||
};
|
||||
run_plugins_install_cli(
|
||||
spec,
|
||||
json,
|
||||
config,
|
||||
registry,
|
||||
&plugin_discovery,
|
||||
&workspace,
|
||||
)
|
||||
.await
|
||||
}
|
||||
command => tokio::task::spawn_blocking(move || {
|
||||
let config = load_structural_config_from_cli(&cli)?;
|
||||
let workspace = crate::commands::groups::plugins::headless::agent_project()
|
||||
.unwrap_or_else(|| resolve_workspace(&cli));
|
||||
let registry = plugin_discovery.inspect_registry_for_workspace(&workspace);
|
||||
run_plugins_cli(command, registry, &config)
|
||||
})
|
||||
.await
|
||||
.context("Plugin command failed to finish")?,
|
||||
},
|
||||
Commands::Setup(args) => {
|
||||
let config = load_config_from_cli(&cli)?;
|
||||
let workspace = resolve_workspace(&cli);
|
||||
@@ -9433,9 +9491,47 @@ fn print_plugin_cli_diagnostic(diagnostic: &codewhale_plugins::types::PluginDiag
|
||||
);
|
||||
}
|
||||
|
||||
async fn run_plugins_install_cli(
|
||||
spec: String,
|
||||
json: bool,
|
||||
config: Config,
|
||||
registry: codewhale_plugins::PluginRegistry,
|
||||
plugin_discovery: &Arc<codewhale_plugins::PluginDiscoveryContext>,
|
||||
workspace: &Path,
|
||||
) -> Result<()> {
|
||||
if !json {
|
||||
return codewhale_plugins::mutation::install_from_cli(spec, config, registry).await;
|
||||
}
|
||||
let receipt =
|
||||
codewhale_plugins::mutation::install_from_cli_receipt(spec, config, registry).await?;
|
||||
let refreshed = plugin_discovery.inspect_registry_for_workspace(workspace);
|
||||
let record = refreshed
|
||||
.get(&receipt.name)
|
||||
.map(|plugin| {
|
||||
let mut value = plugin_cli_record(plugin);
|
||||
if let Some(object) = value.as_object_mut() {
|
||||
object.insert("action".to_string(), "installed".into());
|
||||
}
|
||||
value
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
serde_json::json!({
|
||||
"name": plugin_cli_text(&receipt.name),
|
||||
"action": "installed",
|
||||
"path": receipt.path.as_ref().map(|p| plugin_cli_text(&p.to_string_lossy())),
|
||||
"content_hash": receipt.installed_content_hash.or(receipt.content_hash),
|
||||
"trust": "untrusted",
|
||||
"enabled": false,
|
||||
})
|
||||
});
|
||||
println!("{}", serde_json::to_string_pretty(&record)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_plugins_cli(
|
||||
command: PluginsCommand,
|
||||
mut registry: Arc<codewhale_plugins::PluginRegistry>,
|
||||
config: &Config,
|
||||
) -> Result<()> {
|
||||
use crate::commands::groups::plugins::headless;
|
||||
use codewhale_localization::{MessageId, resolve_locale, tr};
|
||||
@@ -9525,6 +9621,39 @@ fn run_plugins_cli(
|
||||
}
|
||||
}
|
||||
}
|
||||
PluginsCommand::Show { name, json } => headless::show(
|
||||
®istry,
|
||||
locale,
|
||||
&name,
|
||||
json,
|
||||
&mut headless::ProcessConsole,
|
||||
)?,
|
||||
PluginsCommand::Install { .. } => {
|
||||
unreachable!("plugins install is dispatched before spawn_blocking")
|
||||
}
|
||||
PluginsCommand::Update { name, json } => {
|
||||
let network = config
|
||||
.network
|
||||
.clone()
|
||||
.map(codewhale_net::network_policy::NetworkPolicy::from)
|
||||
.unwrap_or_default();
|
||||
headless::update(
|
||||
&mut registry,
|
||||
locale,
|
||||
&name,
|
||||
&network,
|
||||
json,
|
||||
&mut headless::ProcessConsole,
|
||||
)?;
|
||||
}
|
||||
PluginsCommand::Uninstall { name, yes, json } => headless::uninstall(
|
||||
&mut registry,
|
||||
locale,
|
||||
&name,
|
||||
yes,
|
||||
json,
|
||||
&mut headless::ProcessConsole,
|
||||
)?,
|
||||
PluginsCommand::Trust {
|
||||
name,
|
||||
review_token,
|
||||
@@ -21017,6 +21146,72 @@ mod mcp_add_arg_tests {
|
||||
#[cfg(test)]
|
||||
mod config_stay_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod plugins_cli_tests {
|
||||
use super::*;
|
||||
use clap::Parser;
|
||||
|
||||
#[test]
|
||||
fn plugins_cli_parses_install_uninstall_update_and_show() {
|
||||
let install = Cli::try_parse_from([
|
||||
"codewhale",
|
||||
"plugins",
|
||||
"install",
|
||||
"github:example/plugin",
|
||||
"--json",
|
||||
])
|
||||
.expect("parse plugins install");
|
||||
assert!(matches!(
|
||||
install.command,
|
||||
Some(Commands::Plugins {
|
||||
command: PluginsCommand::Install {
|
||||
ref spec,
|
||||
json: true,
|
||||
}
|
||||
}) if spec == "github:example/plugin"
|
||||
));
|
||||
|
||||
let uninstall =
|
||||
Cli::try_parse_from(["codewhale", "plugins", "uninstall", "demo", "-y", "--json"])
|
||||
.expect("parse plugins uninstall");
|
||||
assert!(matches!(
|
||||
uninstall.command,
|
||||
Some(Commands::Plugins {
|
||||
command: PluginsCommand::Uninstall {
|
||||
ref name,
|
||||
yes: true,
|
||||
json: true,
|
||||
}
|
||||
}) if name == "demo"
|
||||
));
|
||||
|
||||
let update = Cli::try_parse_from(["codewhale", "plugins", "update", "demo", "--json"])
|
||||
.expect("parse plugins update");
|
||||
assert!(matches!(
|
||||
update.command,
|
||||
Some(Commands::Plugins {
|
||||
command: PluginsCommand::Update {
|
||||
ref name,
|
||||
json: true,
|
||||
}
|
||||
}) if name == "demo"
|
||||
));
|
||||
|
||||
let show = Cli::try_parse_from(["codewhale", "plugins", "show", "demo", "--json"])
|
||||
.expect("parse plugins show");
|
||||
assert!(matches!(
|
||||
show.command,
|
||||
Some(Commands::Plugins {
|
||||
command: PluginsCommand::Show {
|
||||
ref name,
|
||||
json: true,
|
||||
}
|
||||
}) if name == "demo"
|
||||
));
|
||||
assert!(telemetry_command_is_read_only(show.command.as_ref()));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod doctor_secret_scan_tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
+30
-1
@@ -145,7 +145,8 @@ None of these is offered to you proactively. Add the one that fits the job.
|
||||
|
||||
## Sources
|
||||
|
||||
`codewhale install <spec>` and `/plugin install <spec>` use the same installer.
|
||||
`codewhale install <spec>`, `codewhale plugins install <spec>`, and
|
||||
`/plugin install <spec>` use the same installer.
|
||||
The shell entry does not start a model or require model credentials. Both keep
|
||||
bundles disabled and untrusted until you explicitly review and enable them.
|
||||
|
||||
@@ -248,6 +249,34 @@ matching and the plugin goes inactive until you review again.
|
||||
- `uninstall` refuses enabled plugins (disable first), deletes the bundle
|
||||
directory, and removes its persisted trust/enablement entry.
|
||||
|
||||
## Shell CLI
|
||||
|
||||
Run the full plugin lifecycle from a shell without starting a session:
|
||||
|
||||
```sh
|
||||
codewhale plugins install <spec> [--json]
|
||||
codewhale plugins show <name> [--json]
|
||||
codewhale plugins status [name] [--json]
|
||||
codewhale plugins trust <name> [--review-token TOKEN --yes] [--json]
|
||||
codewhale plugins enable <name> [--json]
|
||||
codewhale plugins disable <name> [--json]
|
||||
codewhale plugins update <name> [--json]
|
||||
codewhale plugins uninstall <name> [--yes] [--json]
|
||||
codewhale plugins revoke <name> [--json]
|
||||
codewhale plugins restage <name> [--json]
|
||||
```
|
||||
|
||||
- `install` uses the same installer as `codewhale install <spec>` and leaves the
|
||||
bundle disabled and untrusted.
|
||||
- `show` prints the same capability review as `trust` without prompting or
|
||||
changing trust state.
|
||||
- `update` re-reads the recorded source and reports `updated to <version>` or
|
||||
`already current` (or the network-approval message when a host needs
|
||||
approval).
|
||||
- `uninstall` disables an enabled plugin first, then removes it. In a terminal
|
||||
it asks `Remove <name> <version>? [y/N]` unless `--yes` is given; without a
|
||||
terminal or with `--json`, `--yes` is required.
|
||||
|
||||
## Safety rules
|
||||
|
||||
- Every install carries an `.installed-from` provenance marker. The installer
|
||||
|
||||
+24
-1
@@ -85,7 +85,8 @@ Codewhale 不会凭空编造远程插件 URL;缺失的插件只会从可用目
|
||||
|
||||
`/plugin install <spec>` 的常用来源如下。完整来源和限制见
|
||||
[英文来源说明](../PLUGINS.md#sources),包括指定版本的 GitHub 与 npm tarball;
|
||||
`codewhale install <spec>` 是使用同一安装器的 shell 入口,不需要模型凭据。
|
||||
`codewhale install <spec>` 和 `codewhale plugins install <spec>` 是使用同一安装器的
|
||||
shell 入口,不需要模型凭据。
|
||||
|
||||
```text
|
||||
/plugin install ./path/to/bundle # local directory (copied)
|
||||
@@ -164,6 +165,28 @@ Claude 的 `http` 传输映射为 Streamable HTTP。
|
||||
- `uninstall` 拒绝卸载已启用的插件(先停用),删除插件包目录,
|
||||
并移除它持久化的信任/启用记录。
|
||||
|
||||
## 命令行(Shell CLI)
|
||||
|
||||
无需启动会话,也可在命令行完成插件的完整生命周期管理:
|
||||
|
||||
```sh
|
||||
codewhale plugins install <spec> [--json]
|
||||
codewhale plugins show <name> [--json]
|
||||
codewhale plugins status [name] [--json]
|
||||
codewhale plugins trust <name> [--review-token TOKEN --yes] [--json]
|
||||
codewhale plugins enable <name> [--json]
|
||||
codewhale plugins disable <name> [--json]
|
||||
codewhale plugins update <name> [--json]
|
||||
codewhale plugins uninstall <name> [--yes] [--json]
|
||||
codewhale plugins revoke <name> [--json]
|
||||
codewhale plugins restage <name> [--json]
|
||||
```
|
||||
|
||||
- `install` 与 `codewhale install <spec>` 使用同一安装器,安装后保持停用且未受信任。
|
||||
- `show` 打印与 `trust` 相同的能力审查内容,从不询问确认,也不会授予信任。
|
||||
- `update` 会重读记录的来源,并报告 `updated to <version>` 或 `already current`(若主机需要审批,则按 `install` 同样的方式提示网络审批错误)。
|
||||
- `uninstall` 会先停用已启用的插件,再卸载它。在终端中未传 `--yes` 时会询问 `Remove <name> <version>? [y/N]`;非终端或使用 `--json` 时必须传入 `--yes`。
|
||||
|
||||
## 安全规则
|
||||
|
||||
- 每次安装都会带一个 `.installed-from` 来源标记。缺少这个标记的插件包,
|
||||
|
||||
-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(),
|
||||
|
||||
@@ -339,8 +339,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/PLUGINS.md",
|
||||
"sha256": "8e6aec4cc4dbb82d2dc40e7d99ce857acdaf88902622bba748ec559858351a90",
|
||||
"content": "# Installing plugins\n\n> 阅读简体中文版:[zh_hans/PLUGINS.md](zh_hans/PLUGINS.md)。\n\nTo create a bundle, start with [Write your first Codewhale plugin](PLUGIN_AUTHORING.md)\nand its runnable Skills example.\n\nThis is the walkthrough for the `/plugin install` on-ramp (v0.9.4, #5182).\n[PLUGIN_BUNDLES.md](PLUGIN_BUNDLES.md) remains the contract for the bundle\nformat (`plugin.json`, compatible `kimi.plugin.json` or\n`.claude-plugin/plugin.json`, or legacy `plugin.toml`), discovery, validation, and\nthe trust/enable lifecycle — this document covers how bits get onto disk in\nthe first place.\n\nEverything you add to Codewhale is a plugin. A skill, an MCP connector and a hook\nare each a plugin, or part of one, and each has a kind: Skill, Connector, Hook,\nCommand. A skill you set up by hand is a plugin of the kind Skill, a server you\nconfigure is a plugin of the kind Connector, and a hook you configure is a plugin\nof the kind Hook. They are still managed where they are today: Extensions →\nSkills, MCP and Hooks in the terminal, and Customize › Skills, Connectors and\nHooks on the desktop. Their files, trust rules and commands (`/skills`, `/mcp`,\n`/hooks`) do not change. One Plugins list that shows every kind together is\nplanned for 0.10.3 and is not in this build; today the kind labels appear on the\nparts inside a plugin bundle's detail. \"Connection\" is the word for a model\nprovider or another agent; it is not a kind of plugin.\n\n## Choose what to add\n\n| What you need | Use | Starting point |\n| --- | --- | --- |\n| Reusable instructions that work through your existing tools | A plugin that adds a skill | `/skills`, then `/skill <name>`; [Skills](SKILLS.md) |\n| A reviewed bundle of workflows, tools, commands or agent profiles | A plugin bundle | `/plugin marketplace list`; [first-party catalog](https://github.com/codewhale-hq/codewhale-plugin-marketplace) |\n| Tools or context from a particular local process or remote service | A plugin that adds an MCP connector | `/mcp`; [MCP setup](MCP.md) |\n\nA plugin can combine skills, Markdown commands, Fleet agent profiles, hooks,\nMCP servers and AI-provider declarations. Tools come from an MCP server or an\nexperimental native extension; there is no generic application-UI component in\nthe bundle manifest. Computer Use's Mac helper and Chromewhale's browser\nextension have their own setup after plugin review.\n\nAn MCP connector is one of two kinds: a local process over stdio, or a remote URL over\nStreamable HTTP. Legacy SSE is available when the server still supports it.\nUser-configured remote servers can use environment-backed credentials or the\nhost's OAuth login. Plugin-contributed remote servers require explicit\nenvironment-backed credentials when authentication is needed; their interactive\nOAuth path is not supported. Installing a plugin does not connect an\naccount or establish that a service works.\n\nA local stdio server runs with your operating-system user's access. Requested\nfilesystem roots in a plugin manifest do not sandbox that process.\n\nFor example, configure [Cloudflare's public documentation server](https://developers.cloudflare.com/changelog/post/2026-10-22-cloudflare-mcp-servers-retire-sse/)\ndirectly, or install the `cloudflare-docs` plugin when you also want its skill:\n\n```sh\ncodewhale mcp add cloudflare-docs --url https://docs.mcp.cloudflare.com/mcp\ncodewhale mcp connect cloudflare-docs\ncodewhale mcp tools cloudflare-docs\n```\n\nChoose one setup for that server. `connect` is a diagnostic connection; it does\nnot attach tools to an already running session. In an existing terminal session,\nrun `/mcp reload` after changing configuration.\n\n## Bundle compatibility\n\n| Format | Supported boundary |\n| --- | --- |\n| Native `plugin.json` or legacy `plugin.toml` | Reviewed declarative adapters above; [manifest contract](PLUGIN_BUNDLES.md) |\n| Claude `.claude-plugin/plugin.json` | Skills, commands, agent profiles and MCP subset; hooks, LSP, custom MCP paths and plugin-root expansion are rejected; [compatibility details](CLAUDE_PLUGIN_COMPAT.md) |\n| `kimi.plugin.json` | Compatible skills, commands, agents and MCP declarations; unsupported runtime fields fail closed |\n| Local DSH package | Explicit preview and exact-hash import through `/plugin import dsh`; experimental native runtime, not blanket DSH compatibility or release qualification; [current evidence and limits](DSH_PLUGIN_COMPAT.md) |\n\nNative TypeScript extensions require `extension_host = true` under `[features]`\nand fresh capability review. LSP, requested filesystem roots and lifecycle mutation\nremain inventory-only. Current DSH adapters include work beyond the published\nv0.10.2 CLI; third-party DSH plugins' complete workflows are not established by\nan import succeeding. An imported DSH plugin's UI is served only on the seats the Engine admits ([details](DSH_PLUGIN_COMPAT.md#plugin-ui-dshclient)). Other applicationsLine truncated
|
||||
"sha256": "6b3003da7fe56e593abc7c9cf0fd3b940b52b2f3881b5c071ade55468d9dbb29",
|
||||
"content": "# Installing plugins\n\n> 阅读简体中文版:[zh_hans/PLUGINS.md](zh_hans/PLUGINS.md)。\n\nTo create a bundle, start with [Write your first Codewhale plugin](PLUGIN_AUTHORING.md)\nand its runnable Skills example.\n\nThis is the walkthrough for the `/plugin install` on-ramp (v0.9.4, #5182).\n[PLUGIN_BUNDLES.md](PLUGIN_BUNDLES.md) remains the contract for the bundle\nformat (`plugin.json`, compatible `kimi.plugin.json` or\n`.claude-plugin/plugin.json`, or legacy `plugin.toml`), discovery, validation, and\nthe trust/enable lifecycle — this document covers how bits get onto disk in\nthe first place.\n\nEverything you add to Codewhale is a plugin. A skill, an MCP connector and a hook\nare each a plugin, or part of one, and each has a kind: Skill, Connector, Hook,\nCommand. A skill you set up by hand is a plugin of the kind Skill, a server you\nconfigure is a plugin of the kind Connector, and a hook you configure is a plugin\nof the kind Hook. They are still managed where they are today: Extensions →\nSkills, MCP and Hooks in the terminal, and Customize › Skills, Connectors and\nHooks on the desktop. Their files, trust rules and commands (`/skills`, `/mcp`,\n`/hooks`) do not change. One Plugins list that shows every kind together is\nplanned for 0.10.3 and is not in this build; today the kind labels appear on the\nparts inside a plugin bundle's detail. \"Connection\" is the word for a model\nprovider or another agent; it is not a kind of plugin.\n\n## Choose what to add\n\n| What you need | Use | Starting point |\n| --- | --- | --- |\n| Reusable instructions that work through your existing tools | A plugin that adds a skill | `/skills`, then `/skill <name>`; [Skills](SKILLS.md) |\n| A reviewed bundle of workflows, tools, commands or agent profiles | A plugin bundle | `/plugin marketplace list`; [first-party catalog](https://github.com/codewhale-hq/codewhale-plugin-marketplace) |\n| Tools or context from a particular local process or remote service | A plugin that adds an MCP connector | `/mcp`; [MCP setup](MCP.md) |\n\nA plugin can combine skills, Markdown commands, Fleet agent profiles, hooks,\nMCP servers and AI-provider declarations. Tools come from an MCP server or an\nexperimental native extension; there is no generic application-UI component in\nthe bundle manifest. Computer Use's Mac helper and Chromewhale's browser\nextension have their own setup after plugin review.\n\nAn MCP connector is one of two kinds: a local process over stdio, or a remote URL over\nStreamable HTTP. Legacy SSE is available when the server still supports it.\nUser-configured remote servers can use environment-backed credentials or the\nhost's OAuth login. Plugin-contributed remote servers require explicit\nenvironment-backed credentials when authentication is needed; their interactive\nOAuth path is not supported. Installing a plugin does not connect an\naccount or establish that a service works.\n\nA local stdio server runs with your operating-system user's access. Requested\nfilesystem roots in a plugin manifest do not sandbox that process.\n\nFor example, configure [Cloudflare's public documentation server](https://developers.cloudflare.com/changelog/post/2026-10-22-cloudflare-mcp-servers-retire-sse/)\ndirectly, or install the `cloudflare-docs` plugin when you also want its skill:\n\n```sh\ncodewhale mcp add cloudflare-docs --url https://docs.mcp.cloudflare.com/mcp\ncodewhale mcp connect cloudflare-docs\ncodewhale mcp tools cloudflare-docs\n```\n\nChoose one setup for that server. `connect` is a diagnostic connection; it does\nnot attach tools to an already running session. In an existing terminal session,\nrun `/mcp reload` after changing configuration.\n\n## Bundle compatibility\n\n| Format | Supported boundary |\n| --- | --- |\n| Native `plugin.json` or legacy `plugin.toml` | Reviewed declarative adapters above; [manifest contract](PLUGIN_BUNDLES.md) |\n| Claude `.claude-plugin/plugin.json` | Skills, commands, agent profiles and MCP subset; hooks, LSP, custom MCP paths and plugin-root expansion are rejected; [compatibility details](CLAUDE_PLUGIN_COMPAT.md) |\n| `kimi.plugin.json` | Compatible skills, commands, agents and MCP declarations; unsupported runtime fields fail closed |\n| Local DSH package | Explicit preview and exact-hash import through `/plugin import dsh`; experimental native runtime, not blanket DSH compatibility or release qualification; [current evidence and limits](DSH_PLUGIN_COMPAT.md) |\n\nNative TypeScript extensions require `extension_host = true` under `[features]`\nand fresh capability review. LSP, requested filesystem roots and lifecycle mutation\nremain inventory-only. Current DSH adapters include work beyond the published\nv0.10.2 CLI; third-party DSH plugins' complete workflows are not established by\nan import succeeding. An imported DSH plugin's UI is served only on the seats the Engine admits ([details](DSH_PLUGIN_COMPAT.md#plugin-ui-dshclient)). Other applicationsLine truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/PLUGIN_BUNDLES.md",
|
||||
|
||||
Reference in new issue
Block a user