mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-12 18:18:10 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef02f41a8c | ||
|
|
b0361216c2 |
No files matched your search
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **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).
|
||||
- **Blank environment variables:** exporting a variable empty (for example `CODEWHALE_MODEL=` from a compose `${VAR:-}` template) no longer hides its legacy alias. `DEEPSEEK_MODEL`, `DEEPSEEK_BASE_URL`, `KIMI_BASE_URL`, `DEEPSEEK_FORCE_HTTP1`, `DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS` and the other provider alias variables now apply as they do when the primary is unset. Thanks [@luochen211](https://github.com/luochen211) (PR 7315).
|
||||
- **Qwen Code as an external agent:** Codewhale now finds an installed Qwen Code CLI (`qwen`, 0.6.1 or newer) and can run it as a session over ACP with `qwen --acp`. `GET /v1/agents/external` lists it, an older install is reported as needing `npm i -g @qwen-code/qwen-code@latest`, and sign-in stays inside Qwen Code (`/auth`). Thanks [@MannXo](https://github.com/MannXo) (PR 7293).
|
||||
- **MCP and language servers that send requests mid-call:** a tool call no longer fails with "neither a result nor an error" when an MCP server sends a `ping` or other request that reuses the call's id while the call is running, and a language server's `workspace/configuration` or similar request no longer ends a pending LSP request. Codewhale now answers `ping`, answers other server requests with "Method not found" (it never advertised them), keeps waiting for the real reply, and a refused answer over Streamable HTTP no longer drops the connection to legacy SSE. Thanks [@luochen211](https://github.com/luochen211) (PR 7319).
|
||||
- **Qwen Code as an external agent:** Codewhale now finds an installed Qwen Code CLI (`qwen`, 0.6.1 or newer) and can run it as a session over ACP with `qwen --acp`. `GET /v1/agents/external` lists it, an older install is reported as needing `npm i -g @qwen-code/qwen-code@latest`, and sign-in stays inside Qwen Code (`/auth`). Thanks [@MannXo](https://github.com/MannXo) (#7293).
|
||||
- **README and contributor credit:** the repository front page now opens the way the website does: the line "The model can be anyone's. The agent should be yours.", one sentence on what Codewhale is, the install command and one picture, with reference detail moved into the linked guides; the 18 translated READMEs follow the same structure and say they are machine-drafted. The contributor wall and the website now count the same 433 people (407 GitHub accounts and the commit authors whose email is not linked to an account), where the website had counted accounts only. `DEPENDENCY_SECURITY.md` moved into `docs/dependency-maintenance.md`, and `.github/SUPPORT.md` says where to ask for help.
|
||||
|
||||
@@ -3115,6 +3115,10 @@ fn write_exec_stdout(text: &str) -> Result<()> {
|
||||
.and_then(|()| stdout.flush())
|
||||
{
|
||||
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => {
|
||||
let _ = codewhale_telemetry::report_termination(
|
||||
codewhale_telemetry::ExitClass::Signal,
|
||||
codewhale_telemetry::SIGNAL_FLUSH_TIMEOUT,
|
||||
);
|
||||
// SAFETY: restores the default disposition and re-raises the
|
||||
// signal the write would have delivered without SIG_IGN.
|
||||
#[cfg(unix)]
|
||||
|
||||
@@ -373,4 +373,39 @@ mod tests {
|
||||
};
|
||||
assert_eq!(handle.persist_local(), FlushOutcome::TimedOut);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hung_writer_cannot_hold_a_termination_send_past_its_deadline() {
|
||||
let (tx, rx) = sync_channel::<Message>(buffer::MAX_EVENTS + 1);
|
||||
let handle = Handle {
|
||||
tx,
|
||||
pending: Arc::new(AtomicUsize::new(0)),
|
||||
closing: AtomicBool::new(false),
|
||||
};
|
||||
let started = Instant::now();
|
||||
let outcome = handle.shutdown(Duration::from_millis(50));
|
||||
assert_eq!(outcome, FlushOutcome::TimedOut);
|
||||
assert!(started.elapsed() < Duration::from_secs(5));
|
||||
assert!(matches!(rx.try_recv(), Ok(Message::Shutdown(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_second_termination_send_does_not_wait() {
|
||||
let (tx, _rx) = sync_channel::<Message>(buffer::MAX_EVENTS + 1);
|
||||
let handle = Handle {
|
||||
tx,
|
||||
pending: Arc::new(AtomicUsize::new(0)),
|
||||
closing: AtomicBool::new(false),
|
||||
};
|
||||
assert_eq!(
|
||||
handle.shutdown(Duration::from_millis(20)),
|
||||
FlushOutcome::TimedOut
|
||||
);
|
||||
let started = Instant::now();
|
||||
assert_eq!(
|
||||
handle.shutdown(Duration::from_secs(30)),
|
||||
FlushOutcome::TimedOut
|
||||
);
|
||||
assert!(started.elapsed() < Duration::from_secs(5));
|
||||
}
|
||||
}
|
||||
+119
-2
@@ -46,8 +46,8 @@ pub mod notice;
|
||||
mod tests;
|
||||
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
use std::time::Duration;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub use actor::{BATCH_MAX_BYTES, BATCH_MAX_EVENTS, FlushOutcome};
|
||||
pub use counters::{Counter, ErrorCounter, SessionCounters, TurnOutcome, TurnTokens};
|
||||
@@ -69,11 +69,26 @@ pub use event::{
|
||||
/// handshake would hold a user's terminal past exit.
|
||||
pub const SHUTDOWN_FLUSH_TIMEOUT: Duration = Duration::from_secs(3);
|
||||
|
||||
/// How long the send attempt on a terminating signal or a broken pipe may hold
|
||||
/// the process.
|
||||
///
|
||||
/// Shorter than [`SHUTDOWN_FLUSH_TIMEOUT`] so that, run alongside the runtime's
|
||||
/// two second signal drain, it fits inside the two second grace a supervisor
|
||||
/// gives SIGTERM before it escalates.
|
||||
pub const SIGNAL_FLUSH_TIMEOUT: Duration = Duration::from_millis(1500);
|
||||
|
||||
const COLD_START_UNSET: u64 = u64::MAX;
|
||||
|
||||
static COLD_START_MS: AtomicU64 = AtomicU64::new(COLD_START_UNSET);
|
||||
|
||||
/// Everything a write path needs once the process is armed.
|
||||
struct Armed {
|
||||
handle: actor::Handle,
|
||||
root: std::path::PathBuf,
|
||||
surface: Surface,
|
||||
started: Instant,
|
||||
exit_class: AtomicU8,
|
||||
session_end_recorded: AtomicBool,
|
||||
}
|
||||
|
||||
/// The one gate. Unset means every write path is a hard no-op.
|
||||
@@ -120,7 +135,10 @@ pub fn init(consent: TelemetryConsent) {
|
||||
let _ = ARMED.set(Armed {
|
||||
handle: actor::Handle::spawn(context),
|
||||
root: root.clone(),
|
||||
surface: consent.surface(),
|
||||
started: Instant::now(),
|
||||
exit_class: AtomicU8::new(ExitClass::Clean.as_u8()),
|
||||
session_end_recorded: AtomicBool::new(false),
|
||||
});
|
||||
|
||||
record_install_or_upgrade(&root);
|
||||
@@ -266,6 +284,105 @@ pub fn exit_class() -> ExitClass {
|
||||
})
|
||||
}
|
||||
|
||||
/// Report how long the interactive event loop took to start. The terminal UI is
|
||||
/// the only caller; every other surface leaves the cold-start bucket absent.
|
||||
pub fn set_cold_start_ms(millis: u64) {
|
||||
let _ = COLD_START_MS.compare_exchange(
|
||||
COLD_START_UNSET,
|
||||
millis,
|
||||
Ordering::Relaxed,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
}
|
||||
|
||||
/// Build `session_end` from what this process actually accumulated.
|
||||
///
|
||||
/// The exit class is read from the process-wide atomic and never derived from
|
||||
/// an exit code. Building the event writes nothing; an unarmed process reports a
|
||||
/// zero duration and nothing reaches a sink.
|
||||
#[must_use]
|
||||
pub fn session_end_event() -> Event {
|
||||
let counters = session_counters();
|
||||
let cold_start = COLD_START_MS.load(Ordering::Relaxed);
|
||||
Event::SessionEnd {
|
||||
duration_bucket: DurationBucket::from_secs(
|
||||
ARMED
|
||||
.get()
|
||||
.map_or(0, |armed| armed.started.elapsed().as_secs()),
|
||||
),
|
||||
exit_class: exit_class(),
|
||||
cold_start_bucket: (cold_start != COLD_START_UNSET)
|
||||
.then(|| ColdStartBucket::from_millis(cold_start)),
|
||||
providers: counters.providers(),
|
||||
counters: counters.counters(),
|
||||
errors: counters.errors(),
|
||||
turn_wall: counters.turn_wall(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Queue this session's usage rows and `session_end` once.
|
||||
///
|
||||
/// Whichever of the clean path, the signal path or a broken pipe gets here
|
||||
/// first reports; later callers do nothing, so one session never ends twice.
|
||||
/// A no-op when unarmed.
|
||||
pub fn record_session_end() {
|
||||
let Some(armed) = ARMED.get() else {
|
||||
return;
|
||||
};
|
||||
if armed.session_end_recorded.swap(true, Ordering::AcqRel) {
|
||||
return;
|
||||
}
|
||||
for event in session_counters().usage_events() {
|
||||
record(event);
|
||||
}
|
||||
record(session_end_event());
|
||||
}
|
||||
|
||||
/// Close the session synchronously, for a path that cannot wait for the writer.
|
||||
///
|
||||
/// States `class`, then appends the usage rows and `session_end` to the local
|
||||
/// buffer with the same lock-free-for-the-caller write as [`record_blocking`].
|
||||
/// A no-op when unarmed, and a no-op for the events when the session already
|
||||
/// ended.
|
||||
pub fn record_session_end_blocking(class: ExitClass) {
|
||||
let Some(armed) = ARMED.get() else {
|
||||
return;
|
||||
};
|
||||
set_exit_class(class);
|
||||
if armed.session_end_recorded.swap(true, Ordering::AcqRel) {
|
||||
return;
|
||||
}
|
||||
for event in session_counters().usage_events() {
|
||||
record_blocking(event);
|
||||
}
|
||||
record_blocking(session_end_event());
|
||||
}
|
||||
|
||||
/// One bounded network attempt for a process that is being terminated.
|
||||
///
|
||||
/// Runs the ordinary flush, so consent, the opt-out tombstone and the run kill
|
||||
/// switch are re-checked immediately beforehand and a person who has opted out
|
||||
/// sends nothing. Short CLI commands stay disk-only. Never retried, and the
|
||||
/// caller is released after `deadline` whether or not the request finished.
|
||||
#[must_use]
|
||||
pub fn flush_for_termination(deadline: Duration) -> FlushOutcome {
|
||||
let Some(armed) = ARMED.get() else {
|
||||
return FlushOutcome::Empty;
|
||||
};
|
||||
if armed.surface == Surface::Cli {
|
||||
return FlushOutcome::Buffered;
|
||||
}
|
||||
armed.handle.shutdown(deadline)
|
||||
}
|
||||
|
||||
/// Record the session end and make the one bounded send attempt, for a process
|
||||
/// that is about to be ended by a signal. Returns [`FlushOutcome::Empty`] when
|
||||
/// unarmed.
|
||||
pub fn report_termination(class: ExitClass, deadline: Duration) -> FlushOutcome {
|
||||
record_session_end_blocking(class);
|
||||
flush_for_termination(deadline)
|
||||
}
|
||||
|
||||
/// Final flush, then stop the writer thread.
|
||||
///
|
||||
/// Returns [`FlushOutcome::Empty`] when unarmed.
|
||||
|
||||
@@ -1434,6 +1434,21 @@ fn completed_wipe_blocks_identity_and_state_recreation() {
|
||||
|
||||
// ------------------------------------------------------------ unarmed gate --
|
||||
|
||||
#[test]
|
||||
fn termination_reporting_is_a_noop_when_unarmed() {
|
||||
assert!(!crate::is_armed());
|
||||
crate::record_session_end();
|
||||
crate::record_session_end_blocking(ExitClass::Signal);
|
||||
assert_eq!(
|
||||
crate::report_termination(ExitClass::Signal, Duration::from_millis(10)),
|
||||
crate::FlushOutcome::Empty
|
||||
);
|
||||
assert_eq!(
|
||||
crate::flush_for_termination(Duration::from_millis(10)),
|
||||
crate::FlushOutcome::Empty
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_blocking_is_a_noop_when_unarmed() {
|
||||
// The process panic hook is installed before the command line is parsed, so
|
||||
|
||||
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **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).
|
||||
- **Blank environment variables:** exporting a variable empty (for example `CODEWHALE_MODEL=` from a compose `${VAR:-}` template) no longer hides its legacy alias. `DEEPSEEK_MODEL`, `DEEPSEEK_BASE_URL`, `KIMI_BASE_URL`, `DEEPSEEK_FORCE_HTTP1`, `DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS` and the other provider alias variables now apply as they do when the primary is unset. Thanks [@luochen211](https://github.com/luochen211) (PR 7315).
|
||||
- **Qwen Code as an external agent:** Codewhale now finds an installed Qwen Code CLI (`qwen`, 0.6.1 or newer) and can run it as a session over ACP with `qwen --acp`. `GET /v1/agents/external` lists it, an older install is reported as needing `npm i -g @qwen-code/qwen-code@latest`, and sign-in stays inside Qwen Code (`/auth`). Thanks [@MannXo](https://github.com/MannXo) (PR 7293).
|
||||
- **MCP and language servers that send requests mid-call:** a tool call no longer fails with "neither a result nor an error" when an MCP server sends a `ping` or other request that reuses the call's id while the call is running, and a language server's `workspace/configuration` or similar request no longer ends a pending LSP request. Codewhale now answers `ping`, answers other server requests with "Method not found" (it never advertised them), keeps waiting for the real reply, and a refused answer over Streamable HTTP no longer drops the connection to legacy SSE. Thanks [@luochen211](https://github.com/luochen211) (PR 7319).
|
||||
- **Qwen Code as an external agent:** Codewhale now finds an installed Qwen Code CLI (`qwen`, 0.6.1 or newer) and can run it as a session over ACP with `qwen --acp`. `GET /v1/agents/external` lists it, an older install is reported as needing `npm i -g @qwen-code/qwen-code@latest`, and sign-in stays inside Qwen Code (`/auth`). Thanks [@MannXo](https://github.com/MannXo) (#7293).
|
||||
- **README and contributor credit:** the repository front page now opens the way the website does: the line "The model can be anyone's. The agent should be yours.", one sentence on what Codewhale is, the install command and one picture, with reference detail moved into the linked guides; the 18 translated READMEs follow the same structure and say they are machine-drafted. The contributor wall and the website now count the same 433 people (407 GitHub accounts and the commit authors whose email is not linked to an account), where the website had counted accounts only. `DEPENDENCY_SECURITY.md` moved into `docs/dependency-maintenance.md`, and `.github/SUPPORT.md` says where to ask for help.
|
||||
|
||||
+8
-60
@@ -862,6 +862,10 @@ fn spawn_signal_cleanup_task() {
|
||||
let mut signals = TerminatingSignals::register();
|
||||
tokio::spawn(async move {
|
||||
let exit_code = signals.wait().await;
|
||||
codewhale_telemetry::record_session_end_blocking(codewhale_telemetry::ExitClass::Signal);
|
||||
let telemetry_send = tokio::task::spawn_blocking(|| {
|
||||
codewhale_telemetry::flush_for_termination(codewhale_telemetry::SIGNAL_FLUSH_TIMEOUT)
|
||||
});
|
||||
// Persistence and a serving Runtime API share a bounded window to
|
||||
// flush queued saves and end open streams with a typed `stream.end`.
|
||||
// A second signal skips both waits.
|
||||
@@ -870,6 +874,9 @@ fn spawn_signal_cleanup_task() {
|
||||
tokio::join!(
|
||||
codewhale_app_server::runtime_api::drain_for_signal_exit(),
|
||||
crate::tui::persistence_actor::flush_for_signal_exit(),
|
||||
async {
|
||||
let _ = telemetry_send.await;
|
||||
},
|
||||
)
|
||||
} => {}
|
||||
_ = signals.wait() => {}
|
||||
@@ -885,66 +892,11 @@ fn spawn_signal_cleanup_task() {
|
||||
#[cfg(unix)]
|
||||
codewhale_runtime::process_tree::kill_contained_trees_for_exit();
|
||||
crate::tui::ui::emergency_restore_terminal();
|
||||
// Nothing async survives the `exit` below, so this is the last
|
||||
// chance to say how the session ended. `record_blocking` is one
|
||||
// `O_APPEND` write with no lock: taking the compaction lock here
|
||||
// would let a second Codewhale process sharing CODEWHALE_HOME hang
|
||||
// Ctrl-C, and the second-signal short-circuit below has to stay
|
||||
// reachable. A no-op unless this process was armed.
|
||||
//
|
||||
// The class is stated, not derived: `RunTerminationReason::Canceled`
|
||||
// also exits 130, so `exit_code` cannot tell a signal from an
|
||||
// Esc-cancelled turn.
|
||||
record_signal_session_end();
|
||||
}
|
||||
std::process::exit(exit_code);
|
||||
});
|
||||
}
|
||||
|
||||
/// When this process's armed telemetry session began. Set once, at arming, and
|
||||
/// read from both the ordinary teardown and the signal path.
|
||||
static TELEMETRY_SESSION_START: std::sync::OnceLock<std::time::Instant> =
|
||||
std::sync::OnceLock::new();
|
||||
|
||||
/// Build `session_end` from what this process actually accumulated.
|
||||
///
|
||||
/// The exit class is read from the process-wide atomic and never derived from
|
||||
/// an exit code: `RunTerminationReason::Canceled` maps to 130, the same value
|
||||
/// the SIGINT path uses, so a code-based derivation would report every
|
||||
/// Esc-cancelled turn as a signal.
|
||||
///
|
||||
/// The cold-start bucket is `None` unless the interactive event loop actually
|
||||
/// began, which is what keeps it absent rather than invented on the surfaces
|
||||
/// that have no event loop.
|
||||
fn telemetry_session_end() -> codewhale_telemetry::Event {
|
||||
let counters = codewhale_telemetry::session_counters();
|
||||
codewhale_telemetry::Event::SessionEnd {
|
||||
duration_bucket: codewhale_telemetry::DurationBucket::from_secs(
|
||||
TELEMETRY_SESSION_START
|
||||
.get()
|
||||
.map_or(0, |start| start.elapsed().as_secs()),
|
||||
),
|
||||
exit_class: codewhale_telemetry::exit_class(),
|
||||
cold_start_bucket: codewhale_runtime::startup_trace::cold_start_ms()
|
||||
.map(codewhale_telemetry::ColdStartBucket::from_millis),
|
||||
providers: counters.providers(),
|
||||
counters: counters.counters(),
|
||||
errors: counters.errors(),
|
||||
turn_wall: counters.turn_wall(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Close the session synchronously, from the signal handler.
|
||||
///
|
||||
/// A no-op unless this process was armed.
|
||||
fn record_signal_session_end() {
|
||||
codewhale_telemetry::set_exit_class(codewhale_telemetry::ExitClass::Signal);
|
||||
for event in codewhale_telemetry::session_counters().usage_events() {
|
||||
codewhale_telemetry::record_blocking(event);
|
||||
}
|
||||
codewhale_telemetry::record_blocking(telemetry_session_end());
|
||||
}
|
||||
|
||||
/// Terminating-signal streams, registered up front and awaited later.
|
||||
///
|
||||
/// Splitting registration from the await is the point: the OS disposition
|
||||
@@ -2514,7 +2466,6 @@ fn arm_telemetry_with_setup(
|
||||
return;
|
||||
};
|
||||
codewhale_telemetry::init(consent.with_config_path(Some(store.path().to_path_buf())));
|
||||
let _ = TELEMETRY_SESSION_START.set(std::time::Instant::now());
|
||||
codewhale_telemetry::record(codewhale_telemetry::Event::SessionStart { source });
|
||||
}
|
||||
|
||||
@@ -2580,10 +2531,7 @@ async fn finish_telemetry(outcome: &Result<()>, surface: codewhale_telemetry::Su
|
||||
{
|
||||
codewhale_telemetry::set_exit_class(codewhale_telemetry::ExitClass::Error);
|
||||
}
|
||||
for event in codewhale_telemetry::session_counters().usage_events() {
|
||||
codewhale_telemetry::record(event);
|
||||
}
|
||||
codewhale_telemetry::record(telemetry_session_end());
|
||||
codewhale_telemetry::record_session_end();
|
||||
if surface == codewhale_telemetry::Surface::Cli {
|
||||
let persistence = codewhale_telemetry::persist_local_blocking();
|
||||
logging::info(format!(
|
||||
|
||||
@@ -227,7 +227,7 @@ fn the_session_source_distinguishes_resume_and_fork_from_a_fresh_launch() {
|
||||
|
||||
#[test]
|
||||
fn a_session_end_built_without_arming_writes_nothing() {
|
||||
let event = telemetry_session_end();
|
||||
let event = codewhale_telemetry::session_end_event();
|
||||
assert!(matches!(
|
||||
event,
|
||||
codewhale_telemetry::Event::SessionEnd { .. }
|
||||
|
||||
@@ -1418,6 +1418,9 @@ pub async fn run_tui(
|
||||
// than through it. Only this path calls it, which is what keeps the
|
||||
// cold-start bucket absent on surfaces with no event loop.
|
||||
codewhale_runtime::startup_trace::mark_cold_start();
|
||||
if let Some(millis) = codewhale_runtime::startup_trace::cold_start_ms() {
|
||||
codewhale_telemetry::set_cold_start_ms(millis);
|
||||
}
|
||||
let result = run_event_loop(
|
||||
&mut terminal,
|
||||
&mut app,
|
||||
|
||||
+15
-4
@@ -126,7 +126,7 @@ claim one.
|
||||
## When anything is sent, and where
|
||||
|
||||
Nothing is collected or sent when a persistent opt-out or run-scoped kill
|
||||
switch is in force, or when privacy state is unreadable. TUI and `exec` sessions have exactly one network flush point: an attempt
|
||||
switch is in force, or when privacy state is unreadable. TUI and `exec` sessions have one network flush point at a normal end: an attempt
|
||||
during shutdown, bounded at three seconds. Short CLI commands such as `config`,
|
||||
`doctor`, and `auth` do not wait for that network request. They record
|
||||
`session_end`, seal the event to the local buffer with a much shorter bound, and
|
||||
@@ -134,10 +134,21 @@ return; a configured endpoint sends those buffered events with the next
|
||||
interactive shutdown flush. An explicitly empty endpoint finalizes its local
|
||||
dry-run batch immediately.
|
||||
|
||||
A session that is ended early makes one more bounded attempt, so its usage is
|
||||
not lost with it. When a terminating signal (interrupt, terminate, hang-up, or
|
||||
console close) reaches a TUI, `exec` or `serve` session, and when `exec` ends
|
||||
because the program reading its output has closed the pipe, the session records
|
||||
`session_end` with exit class `signal`, then makes one send attempt bounded at
|
||||
one and a half seconds, alongside the process's own shutdown work. A second
|
||||
signal ends the process at once without waiting. This is the same single `POST`
|
||||
of the same fields, never retried. Short CLI commands stay local-only on this
|
||||
path too.
|
||||
|
||||
There is no startup flush, mid-session flush, per-turn flush, or per-tool-call
|
||||
flush. Every network shutdown flush re-resolves your setting from disk
|
||||
flush. Every network flush re-resolves your setting from disk
|
||||
immediately beforehand, so `codewhale config set telemetry false` written from
|
||||
another terminal stops the flush of a session that is already running.
|
||||
another terminal stops the flush of a session that is already running, including
|
||||
one that is being terminated.
|
||||
|
||||
A flush is **one `POST`** to the resolved endpoint — by default
|
||||
`https://telemetry.codewhale.net/v1/telemetry`. The request carries a
|
||||
@@ -317,7 +328,7 @@ The workhorse. Everything a session accumulated ships here, once.
|
||||
|
||||
**`duration_bucket`** — `chrono` delta from `app.session_started_at` (`crates/tui/src/tui/app.rs:1889`). Half-open, seconds: `lt_1m` (`d < 60`), `1m_10m` (`60 ≤ d < 600`), `10m_60m` (`600 ≤ d < 3600`), `gt_60m` (`d ≥ 3600`).
|
||||
|
||||
**`exit_class`** — `clean | signal | panic | error`. **Derived from an explicit `AtomicU8`, never from an exit code.** `RunTerminationReason::Canceled` maps to exit 130 (`crates/engine/src/core/runtime_contract/termination.rs:53`), the same value the signal task uses (`crates/tui/src/lib.rs:682`, 128+SIGINT), so a code-based derivation would report every Esc-cancelled turn as a signal. The atomic is set by the panic hook (`crates/tui/src/lib.rs:1582`), by the signal task (`:678-696`) before `std::process::exit`, and on the clean path from `RunTerminationReason::is_success()` (`crates/engine/src/core/runtime_contract/termination.rs:44-46`) — `error` otherwise. Exit codes remain insufficient: an approval-required exit (3) and a terminal exec failure (1) need their actual termination classification.
|
||||
**`exit_class`** — `clean | signal | panic | error`. **Derived from an explicit `AtomicU8`, never from an exit code.** `RunTerminationReason::Canceled` maps to exit 130 (`crates/engine/src/core/runtime_contract/termination.rs:53`), the same value the signal task uses (`crates/tui/src/lib.rs:682`, 128+SIGINT), so a code-based derivation would report every Esc-cancelled turn as a signal. The atomic is set by the panic hook (`crates/tui/src/lib.rs:1582`), by the signal task when the first signal arrives, by `exec` when its output pipe closes, and on the clean path from `RunTerminationReason::is_success()` (`crates/engine/src/core/runtime_contract/termination.rs:44-46`) — `error` otherwise. Exit codes remain insufficient: an approval-required exit (3) and a terminal exec failure (1) need their actual termination classification.
|
||||
|
||||
**`cold_start_bucket`** — from `startup_trace::elapsed_ms()`, which reads `PROCESS_START` directly and is independent of the startup summary's buffer clear (`crates/runtime/src/startup_trace.rs:39-45`). Boundaries: `lt_250`, `250_1000`, `1000_3000`, `gte_3000`. Absent on non-TUI surfaces.
|
||||
|
||||
|
||||
-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(),
|
||||
|
||||
@@ -259,8 +259,8 @@
|
||||
},
|
||||
{
|
||||
"path": "docs/TELEMETRY.md",
|
||||
"sha256": "0ac65dbd0e1d9d196d1c4421116852c3ab6a855062c375c25b09db993084c563",
|
||||
"content": "# Codewhale product telemetry\n\n> 阅读简体中文版:[zh_hans/TELEMETRY.md](zh_hans/TELEMETRY.md)\n\n**Usage analytics are on by default in the current source.** The first\ninteractive launch gives a localized, nonblocking disclosure naming **Codewhale\nand PostHog**, with a direct route to turn counting off. Notice version `6`\ndescribes this policy; displaying it is not recorded as human acceptance.\nMissing an older acceptance record does not turn a fresh installation off.\nExisting explicit opt-outs remain off across upgrades, and unreadable privacy\nstate fails closed. Environment and CLI kill switches still take precedence.\n\nRead the disclosure with `codewhale config telemetry`. Turn usage off in\n`/settings` or run `codewhale config set telemetry false`. Turning it back on\nin Settings or with `codewhale config set telemetry true` updates the existing\ndurable preference and privacy records for new sessions. The compatibility\ncommand `codewhale config telemetry --accept-notice 5` remains available, but\nis not required for a fresh installation. Collection never includes work content or credentials.\n\nThe PostHog sink is optional and inert until the ingest operator explicitly\nconfigures a project token and approved regional host, and records staging\nproof that the actual egress path does not forward the original client IP.\nSource readiness does not establish a deployment, an activated processor,\nan egress receipt, or a retention setting.\n\nCodewhale does not collect conversations, code, prompts, files, file/repo/branch\nnames, model content, or credentials. It sends no per-turn or per-tool timeline.\nIt does send the closed, aggregate schema below: version and platform classes,\nsession duration/outcome, feature/error counters, and a random install id that\nrotates every 90 days.\n\n**There is now a real endpoint.** An enabled session sends its batches to the\nfirst-party ingest service at `https://telemetry.codewhale.net/v1/telemetry`,\nwhich is the shipped default for `telemetry_endpoint`. What that service is,\nwhat it stores, and what it structurally cannot store is spelled out in \"What\nthe endpoint does\" below.\n\n**To send nothing anywhere, turn telemetry off** (see \"Turning it off\"). To stay\nenabled but contact nobody, set `telemetry_endpoint = \"\"`: batches are then\nappended to `$CODEWHALE_HOME/telemetry/dryrun.jsonl` on your own machine, byte\nfor byte what the server would have received, and no HTTP client is ever\nconstructed. That file is how you audit this document against reality.\n\nThis document is the schema. It is not a summary of the schema: a test in\n`crates/telemetry` parses the field names out of this file and asserts set\nequality against the structs the serializer actually uses, so a field that is\nhere and not in the code — or in the code and not here — fails the build.\n\nLocal saved sessions and `codewhale metrics` remain available with telemetry\ndisabled and require no account or hosted reporting service. Disabling telemetry\ndoes not disable local usage receipts.\n\n## Turning it off\n\nThere are two off switches and they do different things. Both stop collection\ncompletely; only one of them erases anything.\n\n```sh\ncodewhale config set telemetry false # opt out: stops collection and erases state\nCODEWHALE_TELEMETRY=0 codewhale # kill switch: stops collection, erases nothing\nDO_NOT_TRACK=1 codewhale # the same kill switch, by the common convention\ncodewhale --telemetry false # the same kill switch, for one command\n```\n\n**`telemetry = false` in the config file is the opt-out.** It is a floor:\n`--telemetry true` and `CODEWHALE_TELEMETRY=1` both lose to it, because a\nsetting you can undo by accident from a wrapper script is not a setting. It\ndeletes the random install id, truncates every buffered event and every dry-run\nrecord, and writes a tombstone. Appends, identity/state writes, and delivery all\nshare the wipe's ordering lock, so once opt-out returns no pre-opt-out write or\nPOST remains in flight. If any part of that wipe fails, the tombstone is still\nthere and the buffer is undrainable — a failed wipe fails closed. Every later\nrun re-asserts the same tombstone for as long as the setting stands, so it\nsurvives. Turning telemetry back on in `/settings` records that explicit preference\nin both existing privacy registers; a fresh launch can then clear the tombstone. Nothing buffered before that point is\never sent.\n\n**The environment variable and the flag are kill switches, not opt-outs.**\nTelemetry is off for the run, nothing is written, nothing is sent — and\nnothing on disk is touched or deleted. That is deliberate: a harness or agent\nthat sets `CODEWHALE_TELEMETRY=0` for one command must not silently discard the\ninstall id and the dry-run records of the person who owns the machine. If you\nwant the erasing kind, use the config file.\n\n`CODEWHALE_TELEMELine truncated
|
||||
"sha256": "486afaa17e4ec77776aeb1e0c1c6da1fddd91cba05738d813c90deec54e7caa2",
|
||||
"content": "# Codewhale product telemetry\n\n> 阅读简体中文版:[zh_hans/TELEMETRY.md](zh_hans/TELEMETRY.md)\n\n**Usage analytics are on by default in the current source.** The first\ninteractive launch gives a localized, nonblocking disclosure naming **Codewhale\nand PostHog**, with a direct route to turn counting off. Notice version `6`\ndescribes this policy; displaying it is not recorded as human acceptance.\nMissing an older acceptance record does not turn a fresh installation off.\nExisting explicit opt-outs remain off across upgrades, and unreadable privacy\nstate fails closed. Environment and CLI kill switches still take precedence.\n\nRead the disclosure with `codewhale config telemetry`. Turn usage off in\n`/settings` or run `codewhale config set telemetry false`. Turning it back on\nin Settings or with `codewhale config set telemetry true` updates the existing\ndurable preference and privacy records for new sessions. The compatibility\ncommand `codewhale config telemetry --accept-notice 5` remains available, but\nis not required for a fresh installation. Collection never includes work content or credentials.\n\nThe PostHog sink is optional and inert until the ingest operator explicitly\nconfigures a project token and approved regional host, and records staging\nproof that the actual egress path does not forward the original client IP.\nSource readiness does not establish a deployment, an activated processor,\nan egress receipt, or a retention setting.\n\nCodewhale does not collect conversations, code, prompts, files, file/repo/branch\nnames, model content, or credentials. It sends no per-turn or per-tool timeline.\nIt does send the closed, aggregate schema below: version and platform classes,\nsession duration/outcome, feature/error counters, and a random install id that\nrotates every 90 days.\n\n**There is now a real endpoint.** An enabled session sends its batches to the\nfirst-party ingest service at `https://telemetry.codewhale.net/v1/telemetry`,\nwhich is the shipped default for `telemetry_endpoint`. What that service is,\nwhat it stores, and what it structurally cannot store is spelled out in \"What\nthe endpoint does\" below.\n\n**To send nothing anywhere, turn telemetry off** (see \"Turning it off\"). To stay\nenabled but contact nobody, set `telemetry_endpoint = \"\"`: batches are then\nappended to `$CODEWHALE_HOME/telemetry/dryrun.jsonl` on your own machine, byte\nfor byte what the server would have received, and no HTTP client is ever\nconstructed. That file is how you audit this document against reality.\n\nThis document is the schema. It is not a summary of the schema: a test in\n`crates/telemetry` parses the field names out of this file and asserts set\nequality against the structs the serializer actually uses, so a field that is\nhere and not in the code — or in the code and not here — fails the build.\n\nLocal saved sessions and `codewhale metrics` remain available with telemetry\ndisabled and require no account or hosted reporting service. Disabling telemetry\ndoes not disable local usage receipts.\n\n## Turning it off\n\nThere are two off switches and they do different things. Both stop collection\ncompletely; only one of them erases anything.\n\n```sh\ncodewhale config set telemetry false # opt out: stops collection and erases state\nCODEWHALE_TELEMETRY=0 codewhale # kill switch: stops collection, erases nothing\nDO_NOT_TRACK=1 codewhale # the same kill switch, by the common convention\ncodewhale --telemetry false # the same kill switch, for one command\n```\n\n**`telemetry = false` in the config file is the opt-out.** It is a floor:\n`--telemetry true` and `CODEWHALE_TELEMETRY=1` both lose to it, because a\nsetting you can undo by accident from a wrapper script is not a setting. It\ndeletes the random install id, truncates every buffered event and every dry-run\nrecord, and writes a tombstone. Appends, identity/state writes, and delivery all\nshare the wipe's ordering lock, so once opt-out returns no pre-opt-out write or\nPOST remains in flight. If any part of that wipe fails, the tombstone is still\nthere and the buffer is undrainable — a failed wipe fails closed. Every later\nrun re-asserts the same tombstone for as long as the setting stands, so it\nsurvives. Turning telemetry back on in `/settings` records that explicit preference\nin both existing privacy registers; a fresh launch can then clear the tombstone. Nothing buffered before that point is\never sent.\n\n**The environment variable and the flag are kill switches, not opt-outs.**\nTelemetry is off for the run, nothing is written, nothing is sent — and\nnothing on disk is touched or deleted. That is deliberate: a harness or agent\nthat sets `CODEWHALE_TELEMETRY=0` for one command must not silently discard the\ninstall id and the dry-run records of the person who owns the machine. If you\nwant the erasing kind, use the config file.\n\n`CODEWHALE_TELEMELine truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/public-surface-facts.json",
|
||||
|
||||
@@ -35,6 +35,7 @@ export const RELEASE_CONTRIBUTORS: string[] = [
|
||||
"@luochen211",
|
||||
"@gannp",
|
||||
"@Felixkw12",
|
||||
"@MannXo",
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -42,7 +43,7 @@ export const RELEASE_CONTRIBUTORS: string[] = [
|
||||
* one. scripts/check-contributor-credit.py requires them here now; the release
|
||||
* cut moves them into RELEASE_CONTRIBUTORS with that release's changelog block.
|
||||
*/
|
||||
export const UNRELEASED_CONTRIBUTORS: string[] = ["@luochen211", "@Lstarsky0", "@SamhandsomeLee", "@gaord", "@MannXo"];
|
||||
export const UNRELEASED_CONTRIBUTORS: string[] = ["@luochen211", "@Lstarsky0", "@SamhandsomeLee", "@gaord"];
|
||||
|
||||
/**
|
||||
* Contributors who helped with reports, reproductions, and verification.
|
||||
|
||||
Reference in new issue
Block a user