mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-12 18:18:10 -10:00
Compare commits
1
Commits
main
..
ci/tel-s6i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d9e761cd7 |
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.
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"schema_version": 6,
|
||||
"notice_version": 8,
|
||||
"sent_at": "2026-08-03T18:04:11Z",
|
||||
"install_id": "3f2a9c1e-0000-4000-8000-000000000001",
|
||||
"app_version": "0.9.4",
|
||||
"git_sha": "abcdef012345",
|
||||
"surface": "control-plane",
|
||||
"os": "macos",
|
||||
"arch": "aarch64",
|
||||
"libc": "none",
|
||||
"tty": true,
|
||||
"events": [
|
||||
{
|
||||
"event": "install_or_upgrade",
|
||||
"kind": "upgrade",
|
||||
"previous_version": "0.9.3-rc.1"
|
||||
},
|
||||
{
|
||||
"event": "session_start",
|
||||
"source": "fleet_worker",
|
||||
"install_age": "under_week"
|
||||
},
|
||||
{
|
||||
"event": "session_end",
|
||||
"duration_bucket": "1m_10m",
|
||||
"exit_class": "unclean",
|
||||
"cold_start_bucket": "250_1000",
|
||||
"providers": [
|
||||
"custom",
|
||||
"deepseek"
|
||||
],
|
||||
"counters": {
|
||||
"turns": 14,
|
||||
"tool_calls": 61,
|
||||
"fleet_dispatch": 0,
|
||||
"workflow_run": 0,
|
||||
"subagent_spawn": 2,
|
||||
"mcp_server_connected": 0,
|
||||
"memory_search": 0,
|
||||
"approval_modal_shown": 0,
|
||||
"approval_auto_allowed": 0,
|
||||
"command_palette_open": 3
|
||||
},
|
||||
"errors": {
|
||||
"auth_preflight_failed": 0,
|
||||
"provider_http_4xx": 0,
|
||||
"provider_http_5xx": 1,
|
||||
"tool_denied_by_policy": 0,
|
||||
"tool_timeout": 0,
|
||||
"network_error": 0
|
||||
},
|
||||
"turn_wall": {
|
||||
"lt_5s": 9,
|
||||
"5_30s": 4,
|
||||
"30_120s": 1,
|
||||
"gte_120s": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"event": "panic",
|
||||
"site": "crates/tui/src/tui/ui.rs:8801:17"
|
||||
},
|
||||
{
|
||||
"event": "product_usage",
|
||||
"counters": {
|
||||
"page_view": 1,
|
||||
"docs_view": 0,
|
||||
"install_copy": 0,
|
||||
"download": 0,
|
||||
"signup": 0,
|
||||
"login": 0,
|
||||
"session_create": 0,
|
||||
"session_resume": 0,
|
||||
"turn_submit": 0,
|
||||
"turn_complete": 0,
|
||||
"settings_open": 0,
|
||||
"integration_connect": 0,
|
||||
"error_shown": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"event": "operations_summary",
|
||||
"requests": 10,
|
||||
"errors": 1,
|
||||
"duration_ms_total": 1500,
|
||||
"duration_ms_max": 300,
|
||||
"probes": 2,
|
||||
"probes_failed": 0
|
||||
},
|
||||
{
|
||||
"event": "model_usage",
|
||||
"provider": "deepseek",
|
||||
"model": "other",
|
||||
"counters": {
|
||||
"turns_ok": 12,
|
||||
"turns_failed": 1,
|
||||
"turns_interrupted": 0,
|
||||
"input_tokens": 418230,
|
||||
"output_tokens": 9120,
|
||||
"cache_read_tokens": 380001,
|
||||
"cache_write_tokens": 0,
|
||||
"requests": 27,
|
||||
"reasoning_tokens": 2310,
|
||||
"generation_secs": 412
|
||||
},
|
||||
"turn_wall": {
|
||||
"lt_5s": 3,
|
||||
"5_30s": 8,
|
||||
"30_120s": 2,
|
||||
"gte_120s": 0
|
||||
},
|
||||
"context": {
|
||||
"lt_32k": 9,
|
||||
"32k_128k": 11,
|
||||
"128k_256k": 5,
|
||||
"gte_256k": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"event": "tool_usage",
|
||||
"tool": "read_file",
|
||||
"calls": 41,
|
||||
"errors": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -15,9 +15,9 @@ hostname is the only way in.
|
||||
|
||||
The current Engine source defaults usage analytics on under notice version 6, with a
|
||||
clear opt-out. Prior explicit declines remain off; a missing acceptance record
|
||||
is not treated as consent. Schema 4 carries `notice_version: 6`; schema 3
|
||||
is not treated as consent. Schema 6 carries `notice_version: 8` and schema 4 carries `notice_version: 6`; schema 3
|
||||
uses notice 5, and legacy schema 2 retains its original explicit
|
||||
`consent_version: 4` contract. Schema 1 and schema 4 batches are first-party
|
||||
`consent_version: 4` contract. Schema 1, 4 and 6 batches are first-party
|
||||
only in this source. Local validation is not proof this updated Worker has been deployed. A user who wants
|
||||
to contact nobody sets `telemetry_endpoint = ""`, which writes batches to
|
||||
`$CODEWHALE_HOME/telemetry/dryrun.jsonl` instead.
|
||||
@@ -38,7 +38,7 @@ require separate operator approval; none is established by the local tests.
|
||||
|
||||
Validated schema 3 batches carrying `notice_version: 5` and legacy schema 2
|
||||
batches carrying explicit `consent_version: 4` can reach the processor. Schema 1
|
||||
and schema 4 batches remain first-party only. Capture uses
|
||||
and schema 4 and 6 batches remain first-party only. Capture uses
|
||||
`/batch/`, fixed anonymous/no-geo controls, no incoming headers, no cookies,
|
||||
no redirects, no response parsing, no retries, and a 1.5-second timeout.
|
||||
PostHog failure is isolated from the successful Analytics Engine write.
|
||||
@@ -143,20 +143,21 @@ take the next free slot.
|
||||
| `blob8` | `tty` — `'true'` \| `'false'` |
|
||||
| `blob9` | `install_kind` (`install_or_upgrade` only) |
|
||||
| `blob10` | `previous_version` (`install_or_upgrade` only) |
|
||||
| `blob11` | `session_source` (`session_start` only) |
|
||||
| `blob12` | `duration_bucket` (`session_end` only) |
|
||||
| `blob13` | `exit_class` (`session_end` only) |
|
||||
| `blob11` | `session_source` (`session_start` only; schema 6 adds `fleet_worker`, `agent`, `scripted`) |
|
||||
| `blob12` | `duration_bucket` (`session_end`); `install_age` (`session_start`, schema 6 only: `new_today`, `under_week`, `older`, `''` for null or older schemas) |
|
||||
| `blob13` | `exit_class` (`session_end` only; schema 6 adds `unclean`) |
|
||||
| `blob14` | `cold_start_bucket` (`session_end` only; `''` on surfaces that do not measure it) |
|
||||
| `blob15` | `providers`, comma-joined, already sorted and deduplicated; the one `provider` on `model_usage` |
|
||||
| `blob16` | `panic_site` (`panic`) — a `crates/…` path or the literal `<dep>`; `model` (`model_usage`); `tool` (`tool_usage`) |
|
||||
| `blob17` | `sent_at` — the *batch* timestamp. Events carry none. |
|
||||
| `blob18` | `aggregate_counters` — closed JSON counts, for `product_usage` or `operations_summary` |
|
||||
| `blob19` | `schema_version` |
|
||||
| `blob20` | `privacy_version`: notice6 for v4, notice5 for v3, consent4 for v2, empty for v1; `blob19` keeps their meanings distinct |
|
||||
| `blob20` | `privacy_version`: notice8 for v6, notice7 for the v5 website funnel, notice6 for v4, notice5 for v3, consent4 for v2, empty for v1; `blob19` keeps their meanings distinct |
|
||||
| `double1..10` | `counters`: `turns`, `tool_calls`, `fleet_dispatch`, `workflow_run`, `subagent_spawn`, `mcp_server_connected`, `memory_search`, `approval_modal_shown`, `approval_auto_allowed`, `command_palette_open` |
|
||||
| `double11..16` | `errors`: `auth_preflight_failed`, `provider_http_4xx`, `provider_http_5xx`, `tool_denied_by_policy`, `tool_timeout`, `network_error` |
|
||||
| `double17..20` | `turn_wall`: `lt_5s`, `5_30s`, `30_120s`, `gte_120s` |
|
||||
| `double1..11` on `model_usage` | `turns_ok`, `turns_failed`, `turns_interrupted`, `input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_write_tokens`, then `turn_wall` `lt_5s`, `5_30s`, `30_120s`, `gte_120s` |
|
||||
| `double12..18` on `model_usage` | schema 6 only, `0` on v4 rows (`blob19` tells them apart): `requests`, `reasoning_tokens`, `generation_secs` (whole seconds, session sum), then input-size request counts `lt_32k`, `32k_128k`, `128k_256k`, `gte_256k` |
|
||||
| `double1..2` on `tool_usage` | `calls`, `errors` |
|
||||
|
||||
Columns not relevant to an event are `''` / `0`. `tty` is a blob because the 20
|
||||
|
||||
@@ -13,7 +13,7 @@ export const CAVEATS = [
|
||||
"Runtime install means first initialized enabled telemetry state, not all installations or new people. IDs rotate every 90 days and are removed on opt-out.",
|
||||
"Native success is observed model turns_ok, not a first-ever primary or useful task. Missing usage, same-process child activity and late cancellation can affect it.",
|
||||
"Dates are received batch dates rather than exact event times. Offline, opted-out and dropped deliveries are absent; ingest has no replay deduplication guarantee.",
|
||||
"Native IDs with an observed CI start are excluded in full because the aggregate schema cannot assign model counts to individual sessions.",
|
||||
"Native IDs with an observed CI, fleet worker, agent or scripted start are excluded in full because the aggregate schema cannot assign model counts to individual sessions.",
|
||||
];
|
||||
|
||||
function instant(value, field) {
|
||||
@@ -72,7 +72,7 @@ export function buildFunnelReport(input, { now = new Date() } = {}) {
|
||||
const installs = new Map(); const successes = new Map(); const started = new Set(); const ci = new Set();
|
||||
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
||||
for (const row of input.native_rows) {
|
||||
if (!uuid.test(row.install_id) || !NATIVE_SURFACES.includes(row.surface) || ![1, 2, 3, 4].includes(count(row.schema_version, "schema_version"))) throw new Error("invalid native identity, surface or version");
|
||||
if (!uuid.test(row.install_id) || !NATIVE_SURFACES.includes(row.surface) || ![1, 2, 3, 4, 6].includes(count(row.schema_version, "schema_version"))) throw new Error("invalid native identity, surface or version");
|
||||
const at = instant(fresh(row.received_at), "received_at");
|
||||
if (!within(at, from, to)) throw new Error("native row outside coverage");
|
||||
if (newest === null || at > newest) newest = at;
|
||||
@@ -82,11 +82,11 @@ export function buildFunnelReport(input, { now = new Date() } = {}) {
|
||||
if (!["install", "upgrade", "downgrade"].includes(row.install_kind)) throw new Error("invalid install kind");
|
||||
if (row.install_kind === "install" && (!installs.has(row.install_id) || at < installs.get(row.install_id))) installs.set(row.install_id, at);
|
||||
} else if (row.event === "session_start") {
|
||||
if (!["interactive", "resume", "fork", "api", "unknown", "ci"].includes(row.session_source)) throw new Error("invalid session source");
|
||||
if (row.session_source === "ci") ci.add(row.install_id);
|
||||
if (!["interactive", "resume", "fork", "api", "unknown", "ci", "fleet_worker", "agent", "scripted"].includes(row.session_source)) throw new Error("invalid session source");
|
||||
if (["ci", "fleet_worker", "agent", "scripted"].includes(row.session_source)) ci.add(row.install_id);
|
||||
started.add(row.install_id);
|
||||
} else if (row.event === "model_usage") {
|
||||
if (count(row.schema_version, "schema_version") !== 4) throw new Error("model usage requires v4");
|
||||
if (![4, 6].includes(count(row.schema_version, "schema_version"))) throw new Error("model usage requires v4 or v6");
|
||||
if (count(row.turns_ok, "turns_ok") > 0) {
|
||||
const observations = successes.get(row.install_id) ?? []; observations.push(at); successes.set(row.install_id, observations);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,14 @@
|
||||
*/
|
||||
|
||||
import type { Batch, Event } from "./schema";
|
||||
import { COUNTER_FIELDS, ERROR_FIELDS, MODEL_COUNTER_FIELDS, TURN_WALL_FIELDS } from "./schema";
|
||||
import {
|
||||
CONTEXT_FIELDS,
|
||||
COUNTER_FIELDS,
|
||||
ERROR_FIELDS,
|
||||
MODEL_COUNTER_FIELDS,
|
||||
MODEL_COUNTER_FIELDS_V6_EXTRA,
|
||||
TURN_WALL_FIELDS,
|
||||
} from "./schema";
|
||||
|
||||
/** The subset of `AnalyticsEngineDataset` this Worker uses. */
|
||||
export interface DataPointSink {
|
||||
@@ -58,7 +65,7 @@ export const BLOB_COLUMNS = [
|
||||
"install_kind", // blob9
|
||||
"previous_version", // blob10
|
||||
"session_source", // blob11
|
||||
"duration_bucket", // blob12
|
||||
"duration_bucket", // blob12 session_end; install_age on session_start (schema 6)
|
||||
"exit_class", // blob13
|
||||
"cold_start_bucket", // blob14
|
||||
"providers", // blob15 comma-joined, already sorted and deduplicated
|
||||
@@ -80,6 +87,19 @@ export const DOUBLE_COLUMNS = [
|
||||
...TURN_WALL_FIELDS,
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* `model_usage` doubles, in `double1..double20` order: the seven v4 counters,
|
||||
* the four turn-wall buckets, then (schema 6, appended so v4 queries keep their
|
||||
* meaning) `requests`, `reasoning_tokens`, `generation_secs` and the four
|
||||
* input-size buckets. v4 rows read 0 in `double12..double18`.
|
||||
*/
|
||||
export const MODEL_USAGE_DOUBLE_COLUMNS = [
|
||||
...MODEL_COUNTER_FIELDS,
|
||||
...TURN_WALL_FIELDS,
|
||||
...MODEL_COUNTER_FIELDS_V6_EXTRA,
|
||||
...CONTEXT_FIELDS,
|
||||
] as const;
|
||||
|
||||
const EMPTY_DOUBLES: number[] = DOUBLE_COLUMNS.map(() => 0);
|
||||
|
||||
/** Build the rows for one validated batch. */
|
||||
@@ -111,6 +131,7 @@ function toDataPoint(batch: Batch, event: Event): DataPoint {
|
||||
break;
|
||||
case "session_start":
|
||||
blobs[10] = event.source;
|
||||
blobs[11] = event.install_age ?? "";
|
||||
break;
|
||||
case "session_end":
|
||||
blobs[11] = event.duration_bucket;
|
||||
@@ -141,8 +162,10 @@ function toDataPoint(batch: Batch, event: Event): DataPoint {
|
||||
doubles = [
|
||||
...MODEL_COUNTER_FIELDS.map((field) => event.counters[field]),
|
||||
...TURN_WALL_FIELDS.map((field) => event.turn_wall[field]),
|
||||
...EMPTY_DOUBLES.slice(MODEL_COUNTER_FIELDS.length + TURN_WALL_FIELDS.length),
|
||||
...MODEL_COUNTER_FIELDS_V6_EXTRA.map((field) => event.counters[field] ?? 0),
|
||||
...CONTEXT_FIELDS.map((field) => event.context?.[field] ?? 0),
|
||||
];
|
||||
doubles = [...doubles, ...EMPTY_DOUBLES.slice(doubles.length)];
|
||||
break;
|
||||
case "tool_usage":
|
||||
blobs[15] = event.tool;
|
||||
|
||||
@@ -22,6 +22,9 @@ export const SCHEMA_VERSION = 4;
|
||||
/** Notice-v5 batches without `model_usage`, `tool_usage` or the `ci` source. */
|
||||
export const V3_SCHEMA_VERSION = 3;
|
||||
|
||||
/** Runtime schema 6: request aggregates, input-size buckets and exec labels. */
|
||||
export const V6_SCHEMA_VERSION = 6;
|
||||
|
||||
export const FUNNEL_SCHEMA_VERSION = 5;
|
||||
export const FUNNEL_NOTICE_VERSION = 7;
|
||||
export const FUNNEL_COUNTER_FIELDS = [
|
||||
@@ -34,6 +37,9 @@ export const FUNNEL_COUNTER_FIELDS = [
|
||||
/** Disclosed default-on policy, not a record of human acceptance. */
|
||||
export const NOTICE_VERSION = 6;
|
||||
|
||||
/** The notice version schema v6 batches carry. */
|
||||
export const V6_NOTICE_VERSION = 8;
|
||||
|
||||
/** The notice version schema v3 batches carry. */
|
||||
export const V3_NOTICE_VERSION = 5;
|
||||
|
||||
@@ -146,6 +152,13 @@ export const SESSION_SOURCES = [
|
||||
"ci",
|
||||
] as const;
|
||||
|
||||
/** Exec start labels and the install-age bucket that only schema 6 carries. */
|
||||
export const V6_ONLY_SESSION_SOURCES = ["fleet_worker", "agent", "scripted"] as const;
|
||||
export const V6_SESSION_SOURCES = [...SESSION_SOURCES, ...V6_ONLY_SESSION_SOURCES] as const;
|
||||
|
||||
/** `InstallAge` wire spellings; the field is also `null` when state is unreadable. */
|
||||
export const INSTALL_AGES = ["new_today", "under_week", "older"] as const;
|
||||
|
||||
/** `DurationBucket` wire spellings. */
|
||||
export const DURATION_BUCKETS = [
|
||||
"lt_1m",
|
||||
@@ -157,6 +170,9 @@ export const DURATION_BUCKETS = [
|
||||
/** `ExitClass::as_str`. */
|
||||
export const EXIT_CLASSES = ["clean", "signal", "panic", "error"] as const;
|
||||
|
||||
/** Schema 6 adds the class for a session recovered from a local checkpoint. */
|
||||
export const V6_EXIT_CLASSES = [...EXIT_CLASSES, "unclean"] as const;
|
||||
|
||||
/** `ColdStartBucket` wire spellings. */
|
||||
export const COLD_START_BUCKETS = [
|
||||
"lt_250",
|
||||
@@ -211,6 +227,21 @@ export const MODEL_COUNTER_FIELDS = [
|
||||
"cache_write_tokens",
|
||||
] as const;
|
||||
|
||||
/** Schema 6 appends these to `MODEL_COUNTER_FIELDS`; `generation_secs` is a whole-second session sum. */
|
||||
export const MODEL_COUNTER_FIELDS_V6_EXTRA = [
|
||||
"requests",
|
||||
"reasoning_tokens",
|
||||
"generation_secs",
|
||||
] as const;
|
||||
|
||||
export const V6_MODEL_COUNTER_FIELDS = [
|
||||
...MODEL_COUNTER_FIELDS,
|
||||
...MODEL_COUNTER_FIELDS_V6_EXTRA,
|
||||
] as const;
|
||||
|
||||
/** `ContextSize` wire spellings: request counts by input size, half-open at 32K, 128K and 256K. */
|
||||
export const CONTEXT_FIELDS = ["lt_32k", "32k_128k", "128k_256k", "gte_256k"] as const;
|
||||
|
||||
/** `event::TOOL_NAMES` plus the `mcp` and `other` buckets. */
|
||||
export const TOOL_NAMES = [
|
||||
"agent", "apply_patch", "authenticate", "bash", "code_execution", "create_goal",
|
||||
@@ -286,6 +317,13 @@ export const EVENT_FIELDS: Readonly<Record<string, readonly string[]>> = {
|
||||
funnel_usage: ["event", "counters"],
|
||||
};
|
||||
|
||||
/** The schema 6 key sets that differ from `EVENT_FIELDS`. */
|
||||
export const V6_EVENT_FIELDS: Readonly<Record<string, readonly string[]>> = {
|
||||
...EVENT_FIELDS,
|
||||
session_start: ["event", "source", "install_age"],
|
||||
model_usage: ["event", "provider", "model", "counters", "turn_wall", "context"],
|
||||
};
|
||||
|
||||
/** Every event discriminant, for the doc-match test. */
|
||||
export const EVENT_NAMES = Object.keys(EVENT_FIELDS);
|
||||
|
||||
@@ -430,7 +468,7 @@ export type FunnelCounters = Record<(typeof FUNNEL_COUNTER_FIELDS)[number], numb
|
||||
|
||||
export type Event =
|
||||
| { event: "install_or_upgrade"; kind: string; previous_version: string | null }
|
||||
| { event: "session_start"; source: string }
|
||||
| { event: "session_start"; source: string; install_age?: string | null }
|
||||
| {
|
||||
event: "session_end";
|
||||
duration_bucket: string;
|
||||
@@ -448,8 +486,10 @@ export type Event =
|
||||
event: "model_usage";
|
||||
provider: string;
|
||||
model: string;
|
||||
counters: Record<(typeof MODEL_COUNTER_FIELDS)[number], number>;
|
||||
counters: Record<(typeof MODEL_COUNTER_FIELDS)[number], number> &
|
||||
Partial<Record<(typeof MODEL_COUNTER_FIELDS_V6_EXTRA)[number], number>>;
|
||||
turn_wall: TurnWall;
|
||||
context?: Record<(typeof CONTEXT_FIELDS)[number], number>;
|
||||
}
|
||||
| { event: "tool_usage"; tool: string; calls: number; errors: number }
|
||||
| ({ event: "operations_summary" } & Record<(typeof OPERATIONS_FIELDS)[number], number>);
|
||||
@@ -546,13 +586,14 @@ function u32Map(
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateEvent(value: unknown, where: string): string | null {
|
||||
function validateEvent(value: unknown, where: string, v6 = false): string | null {
|
||||
if (!isPlainObject(value)) return `${where}: not an object`;
|
||||
const name = value.event;
|
||||
if (typeof name !== "string" || !Object.hasOwn(EVENT_FIELDS, name)) {
|
||||
const eventFields = v6 ? V6_EVENT_FIELDS : EVENT_FIELDS;
|
||||
if (typeof name !== "string" || !Object.hasOwn(eventFields, name)) {
|
||||
return `${where}: unknown event discriminant`;
|
||||
}
|
||||
const keyError = keysExactly(value, EVENT_FIELDS[name], `${where}(${name})`);
|
||||
const keyError = keysExactly(value, eventFields[name], `${where}(${name})`);
|
||||
if (keyError) return keyError;
|
||||
|
||||
switch (name) {
|
||||
@@ -575,8 +616,15 @@ function validateEvent(value: unknown, where: string): string | null {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case "session_start":
|
||||
return enumString(value.source, SESSION_SOURCES, `${where}.source`);
|
||||
case "session_start": {
|
||||
const sourceError = enumString(
|
||||
value.source,
|
||||
v6 ? V6_SESSION_SOURCES : SESSION_SOURCES,
|
||||
`${where}.source`,
|
||||
);
|
||||
if (sourceError || !v6 || value.install_age === null) return sourceError;
|
||||
return enumString(value.install_age, INSTALL_AGES, `${where}.install_age`);
|
||||
}
|
||||
case "session_end": {
|
||||
const bucketError = enumString(
|
||||
value.duration_bucket,
|
||||
@@ -586,7 +634,7 @@ function validateEvent(value: unknown, where: string): string | null {
|
||||
if (bucketError) return bucketError;
|
||||
const exitError = enumString(
|
||||
value.exit_class,
|
||||
EXIT_CLASSES,
|
||||
v6 ? V6_EXIT_CLASSES : EXIT_CLASSES,
|
||||
`${where}.exit_class`,
|
||||
);
|
||||
if (exitError) return exitError;
|
||||
@@ -638,9 +686,18 @@ function validateEvent(value: unknown, where: string): string | null {
|
||||
if (typeof value.model !== "string" || !MODEL_RE.test(value.model)) {
|
||||
return `${where}.model: not a catalogued model id`;
|
||||
}
|
||||
const counterError = v6
|
||||
? countMap(value.counters, V6_MODEL_COUNTER_FIELDS, `${where}.counters`, Number.MAX_SAFE_INTEGER) ??
|
||||
u32Map(
|
||||
{ requests: (value.counters as Record<string, unknown>).requests },
|
||||
["requests"],
|
||||
`${where}.counters`,
|
||||
)
|
||||
: countMap(value.counters, MODEL_COUNTER_FIELDS, `${where}.counters`, Number.MAX_SAFE_INTEGER);
|
||||
return (
|
||||
countMap(value.counters, MODEL_COUNTER_FIELDS, `${where}.counters`, Number.MAX_SAFE_INTEGER) ??
|
||||
u32Map(value.turn_wall, TURN_WALL_FIELDS, `${where}.turn_wall`)
|
||||
counterError ??
|
||||
u32Map(value.turn_wall, TURN_WALL_FIELDS, `${where}.turn_wall`) ??
|
||||
(v6 ? u32Map(value.context, CONTEXT_FIELDS, `${where}.context`) : null)
|
||||
);
|
||||
}
|
||||
case "tool_usage":
|
||||
@@ -675,15 +732,12 @@ export function validateBatch(value: unknown): Accepted | Rejection {
|
||||
|
||||
const legacy = value.schema_version === 1;
|
||||
const explicitConsent = value.schema_version === 2;
|
||||
const v3 = value.schema_version === V3_SCHEMA_VERSION;
|
||||
const v4 = value.schema_version === SCHEMA_VERSION;
|
||||
const funnel = value.schema_version === FUNNEL_SCHEMA_VERSION;
|
||||
const preUsage = value.schema_version !== SCHEMA_VERSION && !funnel;
|
||||
if (
|
||||
!legacy &&
|
||||
!explicitConsent &&
|
||||
value.schema_version !== SCHEMA_VERSION &&
|
||||
!funnel &&
|
||||
value.schema_version !== V3_SCHEMA_VERSION
|
||||
) {
|
||||
const v6 = value.schema_version === V6_SCHEMA_VERSION;
|
||||
const preUsage = legacy || explicitConsent || v3;
|
||||
if (!legacy && !explicitConsent && !v3 && !v4 && !funnel && !v6) {
|
||||
return { ok: false, reason: "batch.schema_version: unsupported" };
|
||||
}
|
||||
const fields = legacy ? LEGACY_ENVELOPE_FIELDS : explicitConsent ? V2_ENVELOPE_FIELDS : ENVELOPE_FIELDS;
|
||||
@@ -695,7 +749,7 @@ export function validateBatch(value: unknown): Accepted | Rejection {
|
||||
if (
|
||||
!legacy &&
|
||||
!explicitConsent &&
|
||||
value.notice_version !== (funnel ? FUNNEL_NOTICE_VERSION : preUsage ? V3_NOTICE_VERSION : NOTICE_VERSION)
|
||||
value.notice_version !== (funnel ? FUNNEL_NOTICE_VERSION : v6 ? V6_NOTICE_VERSION : preUsage ? V3_NOTICE_VERSION : NOTICE_VERSION)
|
||||
) {
|
||||
return { ok: false, reason: "batch.notice_version: current policy required" };
|
||||
}
|
||||
@@ -762,7 +816,7 @@ export function validateBatch(value: unknown): Accepted | Rejection {
|
||||
return { ok: false, reason: "events: operations_summary requires control-plane surface" };
|
||||
}
|
||||
}
|
||||
const eventError = validateEvent(event, `events[${index}]`);
|
||||
const eventError = validateEvent(event, `events[${index}]`, v6);
|
||||
if (eventError) return { ok: false, reason: eventError };
|
||||
}
|
||||
|
||||
|
||||
@@ -97,12 +97,12 @@ describe("additive website funnel contract", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps all four historical goldens accepted and runtime v4 first-party only", async () => {
|
||||
it("keeps the historical goldens and v6 accepted, with runtime v4 and v6 first-party only", async () => {
|
||||
const fetch = vi.fn(); vi.stubGlobal("fetch", fetch);
|
||||
for (const version of [1, 2, 3, 4]) {
|
||||
for (const version of [1, 2, 3, 4, 6]) {
|
||||
const batch = JSON.parse(readFileSync(new URL(`../../crates/telemetry/tests/golden/v${version}.json`, import.meta.url), "utf8"));
|
||||
expect(validateBatch(batch).ok, `v${version}`).toBe(true);
|
||||
if (version !== 4) continue;
|
||||
if (version !== 4 && version !== 6) continue;
|
||||
const { env, written } = harness();
|
||||
expect((await worker.fetch(postJson(batch), { ...env, POSTHOG_HOST: "https://us.i.posthog.com", POSTHOG_PROJECT_TOKEN: "phc_local_test_fixture", POSTHOG_IP_SAFE_EGRESS_VERIFIED: "true" })).status).toBe(204);
|
||||
expect(written).toHaveLength(batch.events.length);
|
||||
|
||||
@@ -4,8 +4,8 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import worker from "../src/index";
|
||||
import { INGEST_PATH } from "../src/route";
|
||||
import { BLOB_COLUMNS, DOUBLE_COLUMNS } from "../src/datapoint";
|
||||
import { MAX_BODY_BYTES, SURFACES } from "../src/schema";
|
||||
import { BLOB_COLUMNS, DOUBLE_COLUMNS, MODEL_USAGE_DOUBLE_COLUMNS } from "../src/datapoint";
|
||||
import { MAX_BODY_BYTES, SURFACES, validateBatch } from "../src/schema";
|
||||
import { goldenBatch, harness, post, postJson } from "./support";
|
||||
|
||||
describe("method and route", () => {
|
||||
@@ -473,3 +473,148 @@ describe("failing closed and quiet", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("schema 6", () => {
|
||||
const golden = (version: number): Record<string, any> =>
|
||||
JSON.parse(
|
||||
readFileSync(
|
||||
new URL(`../../crates/telemetry/tests/golden/v${version}.json`, import.meta.url),
|
||||
"utf8",
|
||||
),
|
||||
);
|
||||
const event = (batch: Record<string, any>, name: string) =>
|
||||
batch.events.find((item: Record<string, any>) => item.event === name);
|
||||
|
||||
it("accepts every historical golden and the v6 golden", () => {
|
||||
for (const version of [1, 2, 3, 4, 6]) {
|
||||
expect(validateBatch(golden(version)).ok, `v${version}`).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("answers 204 to the v4 and v6 goldens", async () => {
|
||||
for (const version of [4, 6]) {
|
||||
const { env, written } = harness();
|
||||
expect((await worker.fetch(postJson(golden(version)), env)).status).toBe(204);
|
||||
expect(written).toHaveLength(golden(version).events.length);
|
||||
}
|
||||
});
|
||||
|
||||
it("requires notice 8 on v6 and refuses the v4 notice", () => {
|
||||
const batch = golden(6);
|
||||
batch.notice_version = 6;
|
||||
expect(validateBatch(batch).ok).toBe(false);
|
||||
expect(validateBatch({ ...golden(4), notice_version: 8 }).ok).toBe(false);
|
||||
});
|
||||
|
||||
it("refuses a v4 batch carrying any v6 key or value", () => {
|
||||
const mutations: Array<(batch: Record<string, any>) => void> = [
|
||||
(batch) => { event(batch, "model_usage").context = event(golden(6), "model_usage").context; },
|
||||
(batch) => { event(batch, "model_usage").counters.requests = 1; },
|
||||
(batch) => { event(batch, "model_usage").counters.reasoning_tokens = 1; },
|
||||
(batch) => { event(batch, "model_usage").counters.generation_secs = 1; },
|
||||
(batch) => { event(batch, "session_start").install_age = "older"; },
|
||||
(batch) => { event(batch, "session_start").source = "fleet_worker"; },
|
||||
(batch) => { event(batch, "session_start").source = "agent"; },
|
||||
(batch) => { event(batch, "session_start").source = "scripted"; },
|
||||
(batch) => { event(batch, "session_end").exit_class = "unclean"; },
|
||||
];
|
||||
for (const [index, mutate] of mutations.entries()) {
|
||||
const batch = golden(4);
|
||||
mutate(batch);
|
||||
expect(validateBatch(batch).ok, `mutation ${index}`).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("refuses a v6 batch missing or adding a key", () => {
|
||||
const mutations: Array<(batch: Record<string, any>) => void> = [
|
||||
(batch) => { delete event(batch, "model_usage").context; },
|
||||
(batch) => { delete event(batch, "model_usage").context["32k_128k"]; },
|
||||
(batch) => { event(batch, "model_usage").context.gte_512k = 0; },
|
||||
(batch) => { delete event(batch, "model_usage").counters.requests; },
|
||||
(batch) => { delete event(batch, "model_usage").counters.reasoning_tokens; },
|
||||
(batch) => { delete event(batch, "model_usage").counters.generation_secs; },
|
||||
(batch) => { event(batch, "model_usage").counters.generation_ms = 1; },
|
||||
(batch) => { event(batch, "model_usage").model_family = "x"; },
|
||||
(batch) => { delete event(batch, "session_start").install_age; },
|
||||
(batch) => { event(batch, "session_start").extra = "x"; },
|
||||
(batch) => { event(batch, "session_end").counters.requests = 1; },
|
||||
];
|
||||
for (const [index, mutate] of mutations.entries()) {
|
||||
const batch = golden(6);
|
||||
mutate(batch);
|
||||
expect(validateBatch(batch).ok, `mutation ${index}`).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("refuses open values where v6 has a closed set or a count", () => {
|
||||
const mutations: Array<(batch: Record<string, any>) => void> = [
|
||||
(batch) => { event(batch, "session_start").install_age = "2026-11-01"; },
|
||||
(batch) => { event(batch, "session_start").install_age = "OLDER"; },
|
||||
(batch) => { event(batch, "session_start").source = "github-actions"; },
|
||||
(batch) => { event(batch, "session_end").exit_class = "killed"; },
|
||||
(batch) => { event(batch, "model_usage").counters.requests = 4294967296; },
|
||||
(batch) => { event(batch, "model_usage").counters.requests = 1.5; },
|
||||
(batch) => { event(batch, "model_usage").counters.generation_secs = -1; },
|
||||
(batch) => { event(batch, "model_usage").counters.reasoning_tokens = "7"; },
|
||||
(batch) => { event(batch, "model_usage").context.lt_32k = 4294967296; },
|
||||
(batch) => { event(batch, "model_usage").context.lt_32k = "1"; },
|
||||
(batch) => { event(batch, "model_usage").model = "/Users/someone/model"; },
|
||||
];
|
||||
for (const [index, mutate] of mutations.entries()) {
|
||||
const batch = golden(6);
|
||||
mutate(batch);
|
||||
expect(validateBatch(batch).ok, `mutation ${index}`).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts a null install_age and every v6 source and exit class", () => {
|
||||
for (const source of ["interactive", "resume", "fork", "api", "unknown", "ci", "fleet_worker", "agent", "scripted"]) {
|
||||
for (const age of ["new_today", "under_week", "older", null]) {
|
||||
const batch = golden(6);
|
||||
event(batch, "session_start").source = source;
|
||||
event(batch, "session_start").install_age = age;
|
||||
expect(validateBatch(batch).ok, `${source}/${age}`).toBe(true);
|
||||
}
|
||||
}
|
||||
for (const exit of ["clean", "signal", "panic", "error", "unclean"]) {
|
||||
const batch = golden(6);
|
||||
event(batch, "session_end").exit_class = exit;
|
||||
expect(validateBatch(batch).ok, exit).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("writes v6 model_usage after the unchanged eleven doubles, and leaves v4 rows zero there", async () => {
|
||||
expect(MODEL_USAGE_DOUBLE_COLUMNS).toHaveLength(18);
|
||||
const v6 = harness();
|
||||
await worker.fetch(postJson(golden(6)), v6.env);
|
||||
const row = v6.written.find((point) => point.blobs[0] === "model_usage")!;
|
||||
expect(row.doubles).toEqual([
|
||||
12, 1, 0, 418230, 9120, 380001, 0,
|
||||
3, 8, 2, 0,
|
||||
27, 2310, 412,
|
||||
9, 11, 5, 2,
|
||||
0, 0,
|
||||
]);
|
||||
expect(row.blobs[BLOB_COLUMNS.indexOf("schema_version")]).toBe("6");
|
||||
|
||||
const v4 = harness();
|
||||
await worker.fetch(postJson(golden(4)), v4.env);
|
||||
const old = v4.written.find((point) => point.blobs[0] === "model_usage")!;
|
||||
expect(old.doubles).toEqual([
|
||||
12, 1, 0, 418230, 9120, 380001, 0,
|
||||
3, 8, 2, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
]);
|
||||
});
|
||||
|
||||
it("writes install_age to blob12 of session_start rows only", async () => {
|
||||
const { env, written } = harness();
|
||||
await worker.fetch(postJson(golden(6)), env);
|
||||
const start = written.find((point) => point.blobs[0] === "session_start")!;
|
||||
expect(start.blobs[BLOB_COLUMNS.indexOf("session_source")]).toBe("fleet_worker");
|
||||
expect(start.blobs[BLOB_COLUMNS.indexOf("duration_bucket")]).toBe("under_week");
|
||||
const end = written.find((point) => point.blobs[0] === "session_end")!;
|
||||
expect(end.blobs[BLOB_COLUMNS.indexOf("exit_class")]).toBe("unclean");
|
||||
expect(end.blobs[BLOB_COLUMNS.indexOf("duration_bucket")]).toBe("1m_10m");
|
||||
});
|
||||
});
|
||||
@@ -49,6 +49,19 @@ describe("existing-pipeline owner funnel report", () => {
|
||||
expect(buildFunnelReport(input, { now: NOW }).native.seven_day_initialization_cohort.unavailable_reason).toContain("incomplete");
|
||||
});
|
||||
|
||||
it("excludes fleet worker, agent and scripted starts like CI, and accepts v6 model usage", () => {
|
||||
for (const source of ["fleet_worker", "agent", "scripted"]) {
|
||||
const input = receipt();
|
||||
input.native_rows.push({ ...native("session_start", "2026-10-04T00:00:00Z"), session_source: source });
|
||||
input.native_rows.push({ ...native("model_usage", "2026-10-04T00:00:00Z"), schema_version: 6 });
|
||||
const report = buildFunnelReport(input, { now: NOW });
|
||||
expect(report.native.excluded_ci_ids, source).toBe(1);
|
||||
expect(report.native.observed_completed_ids, source).toBe(0);
|
||||
}
|
||||
const input = receipt(); input.native_rows.push({ ...native("model_usage", "2026-10-04T00:00:00Z"), schema_version: 5 });
|
||||
expect(() => buildFunnelReport(input, { now: NOW })).toThrow(/version/);
|
||||
});
|
||||
|
||||
it("does not compute a cohort fraction from sampled native records", () => {
|
||||
const input = receipt(); input.native_rows[0].weight = 4;
|
||||
expect(buildFunnelReport(input, { now: NOW }).native.seven_day_initialization_cohort).toMatchObject({ fraction: null, sampled: true });
|
||||
|
||||
-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(),
|
||||
|
||||
@@ -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