diff --git a/Cargo.toml b/Cargo.toml index 26f7491e9..0244c3a95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,6 +138,7 @@ sse-stream = "0.2.3" which = "8.0" similar = "2.5" urlencoding = "2.1" +oxc = { version = "0.138.0", default-features = false, features = ["ast_visit", "codegen", "semantic", "transformer"] } # Tauri (desktop only) tauri = { version = "2.11", features = ["unstable", "macos-private-api", "tray-icon"] } diff --git a/scripts/audit-theme-colors.mjs b/scripts/audit-theme-colors.mjs index 0bdc7c58e..96acc7741 100644 --- a/scripts/audit-theme-colors.mjs +++ b/scripts/audit-theme-colors.mjs @@ -869,7 +869,7 @@ function audit(options) { incrementMap(tokenColorCounts, color); } else if (exceptionFile) { incrementMap(exceptionColorCounts, color); - } else { + } else if (colorDomain === 'appUi') { componentColorOccurrences += 1; incrementMap(componentColorCounts, color); addToSetMap(componentColorFiles, color, relativePath); diff --git a/scripts/core-boundaries/rules/feature-rules.mjs b/scripts/core-boundaries/rules/feature-rules.mjs index b3ca6b64c..98f4505ad 100644 --- a/scripts/core-boundaries/rules/feature-rules.mjs +++ b/scripts/core-boundaries/rules/feature-rules.mjs @@ -55,7 +55,7 @@ export const optionalDependencyFeatureOwnerRules = [ ownerFeatures: ['mcp', 'miniapp-runtime', 'remote-connect', 'remote-ssh-concrete'], }, { depName: 'bitfun-agent-runtime', ownerFeatures: ['deep-research'] }, - { depName: 'bitfun-product-domains', ownerFeatures: ['function-agents', 'miniapp-runtime'] }, + { depName: 'bitfun-product-domains', ownerFeatures: ['canvas-runtime', 'function-agents', 'miniapp-runtime'] }, { depName: 'bitfun-runtime-ports', ownerFeatures: ['remote-connect'] }, { depName: 'bitfun-services-core', @@ -73,6 +73,7 @@ export const optionalDependencyFeatureOwnerRules = [ { depName: 'mac_address', ownerFeatures: ['remote-connect'] }, { depName: 'md5', ownerFeatures: ['remote-connect'] }, { depName: 'notify', ownerFeatures: ['file-watch'] }, + { depName: 'oxc', ownerFeatures: ['canvas-runtime'] }, { depName: 'qrcode', ownerFeatures: ['remote-connect'] }, { depName: 'rand', ownerFeatures: ['mcp', 'remote-connect', 'remote-ssh-concrete'] }, { depName: 'reqwest', ownerFeatures: ['announcement', 'browser-control', 'debug-log', 'mcp', 'miniapp-runtime', 'remote-connect', 'review-platform', 'web-tools'] }, @@ -83,7 +84,7 @@ export const optionalDependencyFeatureOwnerRules = [ { depName: 'rustls', ownerFeatures: ['remote-connect'] }, { depName: 'rustls-native-certs', ownerFeatures: ['remote-connect'] }, { depName: 'schannel', ownerFeatures: ['remote-connect'] }, - { depName: 'sha2', ownerFeatures: ['remote-connect', 'remote-ssh'] }, + { depName: 'sha2', ownerFeatures: ['canvas-runtime', 'remote-connect', 'remote-ssh'] }, { depName: 'shellexpand', ownerFeatures: ['remote-ssh-concrete'] }, { depName: 'sse-stream', ownerFeatures: ['mcp'] }, { depName: 'ssh_config', ownerFeatures: ['remote-ssh-concrete', 'ssh_config'] }, @@ -91,8 +92,8 @@ export const optionalDependencyFeatureOwnerRules = [ { depName: 'thiserror', ownerFeatures: ['browser-control', 'git', 'remote-ssh-concrete', 'review-platform', 'web-tools', 'workspace-search'] }, { depName: 'tokio-tungstenite', ownerFeatures: ['remote-connect'] }, { depName: 'tokio-util', ownerFeatures: ['remote-ssh'] }, - { depName: 'urlencoding', ownerFeatures: ['remote-connect'] }, - { depName: 'uuid', ownerFeatures: ['miniapp-runtime', 'remote-connect', 'remote-ssh-concrete'] }, + { depName: 'urlencoding', ownerFeatures: ['canvas-runtime', 'remote-connect'] }, + { depName: 'uuid', ownerFeatures: ['canvas-runtime', 'miniapp-runtime', 'remote-connect', 'remote-ssh-concrete'] }, { depName: 'which', ownerFeatures: ['miniapp-runtime', 'remote-connect', 'workspace-search'] }, { depName: 'x25519-dalek', ownerFeatures: ['remote-connect'] }, ], @@ -161,6 +162,7 @@ export const ownerCrateFeatureAssemblyRules = [ 'computer-use', 'image-analysis', 'miniapp', + 'canvas', 'agent-control', ], }, @@ -170,6 +172,7 @@ export const ownerCrateFeatureAssemblyRules = [ requiredProductFullFeatures: [ 'announcement', 'browser-control', + 'canvas-runtime', 'debug-log', 'deep-research', 'file-watch', diff --git a/scripts/core-boundaries/self-test.mjs b/scripts/core-boundaries/self-test.mjs index 855d6ee30..a20e66bb8 100644 --- a/scripts/core-boundaries/self-test.mjs +++ b/scripts/core-boundaries/self-test.mjs @@ -3014,6 +3014,8 @@ export function runManifestParserSelfTest({ 'bitfun-product-domains = \\{ path = "\\.\\.\\/\\.\\.\\/contracts\\/product-domains", default-features = false, optional = true \\}', 'dep:bitfun-ai-adapters', 'ai-adapter-runtime', + 'canvas-runtime', + 'bitfun-services-integrations\\/canvas-runtime', 'bitfun-services-integrations\\/function-agents', 'bitfun-services-integrations\\/miniapp-runtime', 'dep:bitfun-product-capabilities', diff --git a/scripts/theme-color-governance-baseline.json b/scripts/theme-color-governance-baseline.json index 4162df32c..f09e7c461 100644 --- a/scripts/theme-color-governance-baseline.json +++ b/scripts/theme-color-governance-baseline.json @@ -87,7 +87,7 @@ "max": 0 }, "colorDomainContracts.registeredUnique": { - "max": 13 + "max": 14 }, "colorDomainContracts.missingRegisteredUnique": { "max": 0 @@ -179,6 +179,12 @@ "colorDomainNearPairs.generatedWidget.nearTotal": { "max": 0 }, + "colorDomainNearPairs.bitfunCanvas.indistinguishableTotal": { + "max": 0 + }, + "colorDomainNearPairs.bitfunCanvas.nearTotal": { + "max": 0 + }, "colorDomainNearPairs.boundaryFallback.indistinguishableTotal": { "max": 0 }, @@ -257,6 +263,12 @@ "colorDomainScopes.generatedWidget.uniqueColors": { "max": 0 }, + "colorDomainScopes.bitfunCanvas.occurrences": { + "max": 0 + }, + "colorDomainScopes.bitfunCanvas.uniqueColors": { + "max": 0 + }, "colorDomainScopes.boundaryFallback.occurrences": { "max": 18 }, diff --git a/scripts/theme-css-var-contract.mjs b/scripts/theme-css-var-contract.mjs index 497017c87..d186140d6 100644 --- a/scripts/theme-css-var-contract.mjs +++ b/scripts/theme-css-var-contract.mjs @@ -21,6 +21,7 @@ export const CONTRACT_VAR_DEFINITION_PATH_PARTS = [ 'component-library/styles', 'infrastructure/theme', 'src/mobile-web/src/theme/presets', + 'tools/bitfun-canvas/runtime/styles', 'tools/generative-widget/themePayload.ts', ]; @@ -68,6 +69,11 @@ export const COLOR_DOMAIN_RULES = [ label: 'Generated widget', pathParts: ['tools/generative-widget'], }, + { + key: 'bitfunCanvas', + label: 'BitFun Canvas', + pathParts: ['tools/bitfun-canvas'], + }, { key: 'boundaryFallback', label: 'Boundary fallback', @@ -157,6 +163,12 @@ export const COLOR_DOMAIN_CONTRACTS = [ reason: 'Generated widgets run in an isolated iframe boundary and need an explicit payload instead of scraping host CSS variables.', mergePolicy: 'Keep payload variables canonical; keep legacy aliases in iframe fallback until widget consumers no longer read them.', }, + { + key: 'bitfunCanvas', + owner: 'src/web-ui/src/tools/bitfun-canvas', + reason: 'BitFun Canvas renders generated TSX inside a dedicated iframe runtime with an SDK palette that must stay isolated from app chrome tokens.', + mergePolicy: 'Keep Canvas iframe and SDK colors in the Canvas runtime contract; promote only reusable host chrome roles to shared app tokens.', + }, { key: 'boundaryFallback', owner: 'src/web-ui/src/shared/theme/themeBoundaryFallbacks.ts', @@ -292,6 +304,11 @@ export const SURFACE_TOKEN_RENAME_CONTRACTS = [ ]; export const DYNAMIC_VAR_FAMILY_CONTRACTS = [ + { + prefix: '--bitfun-canvas-', + owner: 'src/web-ui/src/tools/bitfun-canvas/runtime/canvasRuntimeInstaller.ts; src/web-ui/src/tools/bitfun-canvas/runtime/styles/canvas-runtime.scss', + reason: 'BitFun Canvas iframe runtime receives host theme values through a scoped CSS variable family that must stay isolated from app root tokens.', + }, { prefix: '--blur-', owner: 'src/web-ui/src/infrastructure/theme/core/ThemeService.ts', diff --git a/src/apps/desktop/Cargo.toml b/src/apps/desktop/Cargo.toml index 5f5131060..eb90d59ff 100644 --- a/src/apps/desktop/Cargo.toml +++ b/src/apps/desktop/Cargo.toml @@ -20,6 +20,8 @@ serde_json = { workspace = true } [dependencies] # Internal crates bitfun-core = { path = "../../crates/assembly/core", default-features = false, features = ["product-full"] } +bitfun-product-domains = { path = "../../crates/contracts/product-domains", default-features = false } +bitfun-services-integrations = { path = "../../crates/services/services-integrations", default-features = false, features = ["canvas-runtime"] } bitfun-agent-tools = { path = "../../crates/execution/tool-contracts" } bitfun-transport = { path = "../../crates/adapters/transport", features = ["tauri-adapter"] } bitfun-webdriver = { path = "../../crates/adapters/webdriver" } diff --git a/src/apps/desktop/src/api/canvas_api.rs b/src/apps/desktop/src/api/canvas_api.rs new file mode 100644 index 000000000..46d765dce --- /dev/null +++ b/src/apps/desktop/src/api/canvas_api.rs @@ -0,0 +1,243 @@ +//! Canvas artifact Tauri commands. + +use crate::api::app_state::AppState; +use crate::api::session_storage_path::desktop_effective_session_storage_path; +use bitfun_product_domains::canvas::{ + parse_canvas_artifact_ref, CanvasDiagnostic, CanvasDiagnosticCategory, + CanvasDiagnosticSeverity, CanvasRevision, CanvasSnapshot, CanvasState, CanvasStoragePort, +}; +use bitfun_services_integrations::canvas::CanvasService; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::BTreeMap; +use tauri::State; + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CanvasStateRequest { + pub artifact_reference: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub workspace_path: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub remote_connection_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub remote_ssh_host: Option, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SaveCanvasStateRequest { + pub artifact_reference: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub source_revision_seen: Option, + #[serde(default)] + pub values: BTreeMap, + pub updated_at: i64, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub workspace_path: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub remote_connection_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub remote_ssh_host: Option, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ReportCanvasRuntimeErrorRequest { + pub artifact_reference: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub source_revision_seen: Option, + pub message: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub stack: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub workspace_path: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub remote_connection_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub remote_ssh_host: Option, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CanvasStateResponse { + pub state: Option, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CanvasArtifactResponse { + pub canvas: CanvasSnapshot, + pub artifact_reference: String, +} + +#[tauri::command] +pub async fn load_canvas_artifact( + state: State<'_, AppState>, + request: CanvasStateRequest, +) -> Result { + let reference = parse_canvas_artifact_ref(&request.artifact_reference) + .map_err(|error| format!("Invalid Canvas artifact reference: {:?}", error))?; + let service = canvas_service_for_request(&state, request_workspace(&request), &request).await?; + let snapshot = service + .load_snapshot(reference.session_id, reference.canvas_id) + .await + .map_err(|error| error.message)?; + Ok(CanvasArtifactResponse { + canvas: snapshot, + artifact_reference: request.artifact_reference, + }) +} + +#[tauri::command] +pub async fn load_canvas_state( + state: State<'_, AppState>, + request: CanvasStateRequest, +) -> Result { + let reference = parse_canvas_artifact_ref(&request.artifact_reference) + .map_err(|error| format!("Invalid Canvas artifact reference: {:?}", error))?; + let service = canvas_service_for_request(&state, request_workspace(&request), &request).await?; + let state = service + .load_state(reference.session_id, reference.canvas_id) + .await + .map_err(|error| error.message)?; + Ok(CanvasStateResponse { state }) +} + +#[tauri::command] +pub async fn save_canvas_state( + state: State<'_, AppState>, + request: SaveCanvasStateRequest, +) -> Result { + let reference = parse_canvas_artifact_ref(&request.artifact_reference) + .map_err(|error| format!("Invalid Canvas artifact reference: {:?}", error))?; + let service = canvas_service_for_save_request(&state, &request).await?; + let canvas_state = CanvasState { + canvas_id: reference.canvas_id, + source_revision_seen: request.source_revision_seen.map(CanvasRevision::new), + values: request.values, + updated_at: request.updated_at, + schema_version: bitfun_product_domains::canvas::CANVAS_CURRENT_STATE_SCHEMA_VERSION, + }; + let saved = service + .save_state(reference.session_id, canvas_state) + .await + .map_err(|error| error.message)?; + Ok(CanvasStateResponse { state: Some(saved) }) +} + +#[tauri::command] +pub async fn report_canvas_runtime_error( + state: State<'_, AppState>, + request: ReportCanvasRuntimeErrorRequest, +) -> Result { + let reference = parse_canvas_artifact_ref(&request.artifact_reference) + .map_err(|error| format!("Invalid Canvas artifact reference: {:?}", error))?; + let service = canvas_service_for_runtime_error_request(&state, &request).await?; + let message = if let Some(name) = request.name.as_deref().filter(|value| !value.is_empty()) { + format!("{}: {}", name, request.message) + } else { + request.message.clone() + }; + let mut diagnostic = CanvasDiagnostic { + severity: CanvasDiagnosticSeverity::Error, + category: CanvasDiagnosticCategory::Runtime, + message, + code: Some("canvas.runtime.error".to_string()), + line: None, + column: None, + suggested_fix: Some( + "Open the Canvas source and fix the runtime exception, then update the Canvas." + .to_string(), + ), + }; + if let Some(source_revision) = request.source_revision_seen.as_deref() { + diagnostic + .message + .push_str(&format!(" (source revision: {})", source_revision)); + } + if let Some(stack) = request.stack.as_deref().filter(|value| !value.is_empty()) { + diagnostic.message.push_str("\n"); + diagnostic.message.push_str(stack); + } + let snapshot = service + .report_runtime_diagnostic(reference.session_id, reference.canvas_id, diagnostic) + .await + .map_err(|error| error.message)?; + Ok(CanvasArtifactResponse { + canvas: snapshot, + artifact_reference: request.artifact_reference, + }) +} + +fn request_workspace(request: &CanvasStateRequest) -> Option<&str> { + request.workspace_path.as_deref() +} + +async fn canvas_service_for_runtime_error_request( + state: &AppState, + request: &ReportCanvasRuntimeErrorRequest, +) -> Result { + canvas_service_for_workspace( + state, + request.workspace_path.as_deref(), + request.remote_connection_id.as_deref(), + request.remote_ssh_host.as_deref(), + ) + .await +} + +async fn canvas_service_for_save_request( + state: &AppState, + request: &SaveCanvasStateRequest, +) -> Result { + canvas_service_for_workspace( + state, + request.workspace_path.as_deref(), + request.remote_connection_id.as_deref(), + request.remote_ssh_host.as_deref(), + ) + .await +} + +async fn canvas_service_for_request( + state: &AppState, + workspace_path: Option<&str>, + request: &CanvasStateRequest, +) -> Result { + canvas_service_for_workspace( + state, + workspace_path, + request.remote_connection_id.as_deref(), + request.remote_ssh_host.as_deref(), + ) + .await +} + +async fn canvas_service_for_workspace( + state: &AppState, + workspace_path: Option<&str>, + remote_connection_id: Option<&str>, + remote_ssh_host: Option<&str>, +) -> Result { + let workspace = match workspace_path { + Some(path) if !path.trim().is_empty() => path.trim().to_string(), + _ => state + .workspace_path + .read() + .await + .as_ref() + .map(|path| path.to_string_lossy().to_string()) + .ok_or_else(|| "No active workspace is available for Canvas state".to_string())?, + }; + let sessions_dir = desktop_effective_session_storage_path( + state, + &workspace, + remote_connection_id, + remote_ssh_host, + ) + .await; + Ok(CanvasService::persistent(sessions_dir)) +} diff --git a/src/apps/desktop/src/api/mod.rs b/src/apps/desktop/src/api/mod.rs index 090f2d400..d4a3ad68a 100644 --- a/src/apps/desktop/src/api/mod.rs +++ b/src/apps/desktop/src/api/mod.rs @@ -7,6 +7,7 @@ pub mod app_state; pub mod browser_api; pub mod browser_control_api; pub mod btw_api; +pub mod canvas_api; pub mod clipboard_file_api; pub mod commands; pub mod computer_use_api; diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index a285aee45..3af54853b 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -1190,6 +1190,10 @@ pub async fn run() { api::miniapp_api::miniapp_runtime_status, api::miniapp_api::miniapp_worker_call, api::miniapp_api::miniapp_host_call, + api::canvas_api::load_canvas_artifact, + api::canvas_api::load_canvas_state, + api::canvas_api::report_canvas_runtime_error, + api::canvas_api::save_canvas_state, api::miniapp_api::miniapp_worker_stop, api::miniapp_api::miniapp_worker_list_running, api::miniapp_api::miniapp_install_deps, diff --git a/src/crates/assembly/core/Cargo.toml b/src/crates/assembly/core/Cargo.toml index a793dc5b1..0b22527ef 100644 --- a/src/crates/assembly/core/Cargo.toml +++ b/src/crates/assembly/core/Cargo.toml @@ -156,6 +156,7 @@ schannel = "0.1" default = ["product-full"] product-full = [ "ai-adapter-runtime", + "canvas-runtime", "dep:chrono-tz", "dep:cron", "dep:dashmap", @@ -190,6 +191,10 @@ product-domains = [ "bitfun-services-integrations/miniapp-runtime", "bitfun-product-domains/product-full", ] +canvas-runtime = [ + "product-domains", + "bitfun-services-integrations/canvas-runtime", +] runtime-services = [] service-integrations = [ "dep:aes-gcm", @@ -216,5 +221,21 @@ ssh-remote = [ [dev-dependencies] tempfile = { workspace = true } +[[test]] +name = "context_profile" +required-features = ["product-full"] + +[[test]] +name = "git_contracts" +required-features = ["service-integrations"] + +[[test]] +name = "product_assembly" +required-features = ["product-full"] + +[[test]] +name = "remote_mcp_streamable_http" +required-features = ["service-integrations"] + [build-dependencies] sha2 = { workspace = true } diff --git a/src/crates/assembly/core/builtin_skills/bitfun-canvas/SKILL.md b/src/crates/assembly/core/builtin_skills/bitfun-canvas/SKILL.md new file mode 100644 index 000000000..45d8e6af7 --- /dev/null +++ b/src/crates/assembly/core/builtin_skills/bitfun-canvas/SKILL.md @@ -0,0 +1,108 @@ +--- +name: bitfun-canvas +description: >- + A BitFun Canvas is a live React app that the user can open beside the chat. + You MUST use a canvas when the agent produces a standalone analytical artifact + — quantitative analyses, billing investigations, security audits, architecture + reviews, data-heavy content, timelines, charts, tables, interactive + explorations, repeatable tools, or any response that benefits from visual + layout. Especially prefer a canvas when presenting structured tool or service + results where the data is the deliverable — render it in a rich canvas rather + than dumping it into a markdown table or code block. If you catch yourself + about to write a markdown table, stop and use a canvas instead. You MUST also + read this skill whenever you create, edit, or debug a BitFun Canvas artifact. +metadata: + surfaces: + - ide +--- +A canvas is a single TSX source artifact that BitFun compiles so the user can open it beside the chat. Follow the workflow below in order. + +## Workflow + +### 1. Decide whether to use a canvas + +The trigger is **user intent**, not response shape. Ask: would the user benefit from viewing this output as its **own standalone artifact**, separate from the chat? If the output is a means to an end (a drafted message, a code fix, a dashboard in another tool), skip the canvas. + +**Use a canvas when the agent produces new standalone analytical output:** +- Quantitative analyses and metrics breakdowns (e.g. "send 500 requests and tell me how many fail") +- Billing or account investigations that surface structured findings from database queries +- Security audits or architecture reviews with categorized findings +- Cross-system data analyses and overlap reports +- Structured data from tools or services where the data IS the deliverable +- Financial analyses, margin decompositions, usage trend reports +- Tables with more than a handful of rows that the user asked to see + +**Do NOT use a canvas when:** +- The user asks for work in a **specific tool** — "create a Datadog dashboard" means give them a Datadog dashboard, not a canvas +- The user has a **specific deliverable** — "draft a support response", "fix this code", "make this PR" +- The user is **working within an existing artifact** — improving an HTML dashboard, editing an existing file +- The user is doing **targeted debugging** or active development, even if structured findings emerge along the way +- Short factual answers, one-off file edits, or quick clarifying questions +- Tools are queried as an **intermediate step** for a different deliverable (e.g. querying Stripe to draft a support reply) + +### 2. Write the canvas + +**Location.** BitFun Canvases are session-scoped artifacts created with `CreateCanvas`. For a new canvas, always call `CreateCanvas` with a concise title and complete TSX source; do not stop after telling the user what the source would be or showing code in chat. For small targeted revisions, use `PatchCanvas` with exact unique text replacements. Use `ReadCanvas` first when you do not have the latest source in context. Use `UpdateCanvas` only for large rewrites that need a complete replacement TSX source. + +**File rules:** +- Exactly one TSX source per canvas. Never create helper files, style files, or supporting modules. +- Import **only** from `bitfun/canvas`. No relative imports, no npm packages, no Node built-ins. +- Default-export the top-level component. +- Embed all data inline. **No `fetch()`, no network calls.** +- Do not locally redeclare names imported from `bitfun/canvas` such as `Grid`, `Code`, `Stack`, `Text`, `Row`, or `Card`. Use the SDK component directly, or choose a distinct helper name such as `LayerGrid` or `InlineCodeText`. + +**Never render empty states.** A canvas exists to show real content. If a section, chart, table, or component has no data to display, **omit it** — do not render it with placeholder text ("Add header here", "TODO", "Example"), a "No data" message, an empty array, zeroed rows, or an empty chart frame. If the entire canvas would be empty because you don't have the underlying data, do not produce a canvas — tell the user what's missing and ask for it instead. + +**Label every plot.** Charts and tables must be self-describing — a reader looking at the canvas alone should know exactly what they're seeing. For every plot include: +- A title naming the **specific metric** (not "Metrics" — "API error rate by service"). +- **Axis labels with units** on both axes (e.g. "Date", "Latency (ms)"). +- A **legend** when more than one series is shown, with the exact series names from the source data. +- The **source and time range** in a small caption (e.g. "Source: Datadog · last 7 days"). If a value is a transformation (mean, p95, normalized, smoothed), say so in the label. + +**Component discovery:** prefer built-in `bitfun/canvas` components over hand-rolled markup. The full public surface (components, hooks, prop types, tokens) is declared in `sdk/index.d.ts` next to this skill and its sibling `.d.ts` files — read them when you need exact exports, prop shapes, or hook signatures rather than guessing. Referencing an export that does not exist is the most common runtime error. + +Apply the Canvas generation policy below as you write, and complete its pre-delivery self-check (section 6) before returning the canvas. + +## Design guidance + +Be creative. The SDK gives you expressive building blocks — use them in whatever combination best serves the content. But avoid slop: no gradients, no emojis, no box-shadows, no rainbow coloring. BitFun canvases are flat, minimal, and purposeful. + +### Visual hierarchy + +Not everything deserves equal treatment. Primary content gets more space, larger headings, and accent color. Supporting content stays compact. Squint test: blur your eyes — can you tell what matters? + +**Color.** All colors from `useHostTheme()` tokens — read its JSDoc in the SDK declarations for the return shape and usage pattern. No hardcoded hex. Use accent color deliberately, not on everything. + +### Slop patterns — forbidden + +These specific patterns produce low-quality output. If 2+ are present, redesign. + +- **Gradients** — no `linear-gradient`, `radial-gradient`, `background-clip: text`. +- **Emojis** — no emoji as icons, status indicators, bullets, or section markers. +- **Box shadows** — no `box-shadow`. Flat surfaces only. +- **Wall of identical cards** — every section wrapped in the same card style with no variation. Mix open sections with cards. +- **Rainbow coloring** — a different color on every element. Most elements are neutral; color is used sparingly with purpose. +- **Giant text** — font sizes above H1 (24px), or bold text stuffed in CardHeader. +- **Decorative borders** — colored borders on every element. Borders are structural (subtle stroke tokens), not decorative. + +### Pre-delivery self-check + +Before returning canvas code, verify: +1. Does the layout have visual hierarchy? One thing should stand out. +2. Is there variety in the composition? Not just a single column of uniform blocks. +3. Slop check: scan for the forbidden patterns above. + +## Introducing the canvas + +Whenever you mention a canvas to the user — one you created, updated, or want them to open — **always** include the `bitfun-canvas://...` artifact reference returned by the Canvas tool. Use the artifact title or a short descriptive label near the reference; do not refer to a canvas by name alone without the reference. + +When you create a canvas, add a short note in your chat response telling the user they can open it beside the chat, with that `bitfun-canvas://...` reference: + +- **First canvas** — include one sentence explaining what a canvas is. +- **Unsolicited canvas** — if the user didn't ask for a canvas, include one sentence explaining why you chose it over plain text. + +Both can apply at once; one or two sentences total is enough. Skip the intro for subsequent canvases unless you are mentioning that canvas again (still include the artifact reference). + +## Troubleshooting + +If a canvas appears blank or missing, first inspect the Canvas tool result diagnostics and runtime diagnostics. `CreateCanvas`, `PatchCanvas`, and `UpdateCanvas` save the source first, then compile it. If compilation or policy validation fails, the tool returns diagnostics and preserves the previous last-known-good compiled payload when one exists. Runtime errors are reported back to the host as Canvas diagnostics; open the source view, fix the exception, then call `PatchCanvas` for a small exact edit or `UpdateCanvas` for a full rewrite. diff --git a/src/crates/assembly/core/builtin_skills/bitfun-canvas/sdk/callout-tone-icons.d.ts b/src/crates/assembly/core/builtin_skills/bitfun-canvas/sdk/callout-tone-icons.d.ts new file mode 100644 index 000000000..6888a2e27 --- /dev/null +++ b/src/crates/assembly/core/builtin_skills/bitfun-canvas/sdk/callout-tone-icons.d.ts @@ -0,0 +1,11 @@ +import type { JSX } from "react"; +type CalloutToneIconGlyph = "info" | "warning" | "circles-check" | "exclamation-circle"; +type CalloutToneForIcon = "info" | "success" | "warning" | "danger" | "neutral"; +/** Maps `Callout` tone to the same status icons used by `BitFun UI` toasts. */ +export declare const calloutToneIconGlyph: Record, CalloutToneIconGlyph>; +export declare function CalloutToneIcon({ tone, color }: { + tone: CalloutToneForIcon; + color: string; +}): JSX.Element; +export {}; +//# sourceMappingURL=callout-tone-icons.d.ts.map \ No newline at end of file diff --git a/src/crates/assembly/core/builtin_skills/bitfun-canvas/sdk/canvas-tokens.d.ts b/src/crates/assembly/core/builtin_skills/bitfun-canvas/sdk/canvas-tokens.d.ts new file mode 100644 index 000000000..d8ebe8096 --- /dev/null +++ b/src/crates/assembly/core/builtin_skills/bitfun-canvas/sdk/canvas-tokens.d.ts @@ -0,0 +1,98 @@ +/** + * Design-token contract for `bitfun/canvas`. + * + * Canvas uses host-provided BitFun semantic CSS variables instead of declaring + * an independent hardcoded color palette. Treat exported colors as token + * references or host-resolved strings; do not depend on concrete hex values. + */ +export interface CanvasPalette { + readonly foreground: string; + readonly foregroundSecondary: string; + readonly foregroundTertiary: string; + readonly foregroundQuaternary: string; + readonly editor: string; + readonly chrome: string; + readonly sidebar: string; + readonly elevated: string; + readonly fillPrimary: string; + readonly fillSecondary: string; + readonly fillTertiary: string; + readonly fillQuaternary: string; + readonly strokePrimary: string; + readonly strokeSecondary: string; + readonly strokeTertiary: string; + readonly strokeFocused: string; + readonly accent: string; + readonly buttonBackground: string; + readonly buttonForeground: string; + readonly buttonHoverBackground: string; + readonly link: string; + readonly diffInsertedLine: string; + readonly diffRemovedLine: string; + readonly diffStripAdded: string; + readonly diffStripRemoved: string; +} +export interface CanvasHostThemeOverrides { + readonly primary?: string; + readonly editorBackground?: string; + readonly editorForeground?: string; +} +export type Color = "gray" | "purple" | "green" | "yellow" | "cyan" | "pink" | "blue" | "orange"; +export type CategoryPalette = Readonly>; +export declare const canvasPaletteDark: CanvasPalette; +export declare const canvasPaletteLight: CanvasPalette; +export declare function applyWorkbenchSurfaces(palette: CanvasPalette, surfaces: Pick): CanvasPalette; +export declare function applyPrimaryColor(palette: CanvasPalette, primary: string): CanvasPalette; +export declare const categoryPaletteDark: CategoryPalette; +export declare const categoryPaletteLight: CategoryPalette; +/** Legacy `colorPalette` name kept for back-compat; prefer `useHostTheme().category`. */ +export declare const colorPalette: CategoryPalette; +export declare const usageColorSequence: readonly Color[]; +export declare const chartColorSequence: readonly string[]; +export interface CanvasTokens { + readonly bg: { + readonly editor: string; + readonly chrome: string; + readonly elevated: string; + }; + readonly text: { + readonly primary: string; + readonly secondary: string; + readonly tertiary: string; + readonly quaternary: string; + readonly link: string; + readonly onAccent: string; + }; + readonly stroke: { + readonly primary: string; + readonly secondary: string; + readonly tertiary: string; + readonly focused: string; + }; + readonly fill: { + readonly primary: string; + readonly secondary: string; + readonly tertiary: string; + readonly quaternary: string; + }; + readonly accent: { + readonly primary: string; + readonly control: string; + readonly controlHover: string; + }; + readonly diff: { + readonly insertedLine: string; + readonly removedLine: string; + readonly stripAdded: string; + readonly stripRemoved: string; + }; + readonly category: CategoryPalette; +} +/** Semantic colors for components. Spacing and radius live in `theme.ts`. */ +export declare const canvasTokens: CanvasTokens; +export declare const canvasTokensLight: CanvasTokens; +export declare function buildHostTokens(kind: string, overrides?: CanvasHostThemeOverrides): { + tokens: CanvasTokens; + palette: CanvasPalette; +}; +//# sourceMappingURL=canvas-tokens.d.ts.map diff --git a/src/crates/assembly/core/builtin_skills/bitfun-canvas/sdk/chart-primitives.d.ts b/src/crates/assembly/core/builtin_skills/bitfun-canvas/sdk/chart-primitives.d.ts new file mode 100644 index 000000000..bcf643962 --- /dev/null +++ b/src/crates/assembly/core/builtin_skills/bitfun-canvas/sdk/chart-primitives.d.ts @@ -0,0 +1,272 @@ +/** + * Chart primitives for `bitfun/canvas` — multi-series, stacked, and pie charts + * rendered as pure inline SVG with zero external dependencies. + * + * Distilled from the portal-website Highcharts analytics charting layer. + */ +import type { CSSProperties, JSX } from "react"; +/** + * Semantic tone for a chart series or slice. Mirrors the tone vocabulary + * used by `Stat`, `Pill`, `Table`, and other SDK primitives so colors + * match across a canvas — e.g. a `Stat tone="success"` and a + * `ChartSeries tone="success"` render in the same green. + * + * Omit `tone` to let the chart auto-assign a distinct color from the + * chart palette; supply `tone` only when the value carries semantic + * meaning that should match other tonal elements on the page. + */ +export type ChartTone = "success" | "danger" | "warning" | "info" | "neutral"; +/** A single labeled value, used by `PieChart`. */ +export type ChartDataPoint = { + label: string; + /** Non-negative numeric value. */ + value: number; +}; +/** + * A named data series for `BarChart` and `LineChart`. + * The `data` array aligns by index with the parent component's `categories`. + * If `tone` is omitted, a color is auto-assigned from the chart palette. + */ +export type ChartSeries = { + name: string; + data: number[]; + tone?: ChartTone; +}; +/** + * A dashed marker line drawn across the plot at a fixed value — for targets, + * SLOs, budgets, means, or limits. Drawn horizontally on line / vertical-bar + * charts and vertically on `horizontal` bar charts; either way it marks the + * value axis and is folded into the auto domain so it stays on-canvas. + */ +export type ChartReferenceLine = { + /** Position on the value axis, in the same units as the series data. */ + value: number; + /** Short label drawn in a chip at the line's end. */ + label?: string; + /** Line color. Omit for a muted neutral; set to match other tonal elements. */ + tone?: ChartTone; +}; +/** + * Shared value-axis controls (the y-axis, or the x-axis on `horizontal` bar + * charts). By default the axis starts at zero; override to zoom into a tight + * range. On stacked / normalized bars these are ignored — those always start + * at zero. + */ +type ValueAxisProps = { + /** + * Start the value axis at zero. Defaults to `true`. Set `false` to + * auto-fit the axis to the data range — useful for tightly-clustered + * series (e.g. uptime 99.0–99.9%) that a zero baseline would flatten. + */ + beginAtZero?: boolean; + /** Explicit axis minimum. Overrides `beginAtZero`. */ + yMin?: number; + /** Explicit axis maximum. */ + yMax?: number; + /** Horizontal marker lines for targets / thresholds / means. */ + referenceLines?: ChartReferenceLine[]; +}; +export type BarChartProps = ValueAxisProps & { + /** Category labels along the independent axis. */ + categories: string[]; + /** One or more data series. Values align by index with `categories`. */ + series: ChartSeries[]; + height?: number; + /** Stack series on top of each other instead of grouping side-by-side. */ + stacked?: boolean; + /** Render horizontal bars instead of vertical columns. */ + horizontal?: boolean; + /** Show as 100% stacked (implies `stacked`). */ + normalized?: boolean; + /** Suffix for value labels (e.g. "%", " ms"). */ + valueSuffix?: string; + /** Prefix for value labels (e.g. "$"). Ignored in `normalized` mode. */ + valuePrefix?: string; + /** + * Print each bar's value as a label. Defaults to auto: on for a single + * series with ≤8 categories, off otherwise. Set `true` to force labels on + * (e.g. grouped multi-series), or `false` to force them off. No effect on + * `stacked` / `normalized` charts — use the hover tooltip there. + */ + showValues?: boolean; + style?: CSSProperties; +}; +export type LineChartProps = ValueAxisProps & { + categories: string[]; + series: ChartSeries[]; + height?: number; + /** Fill the area under each line with a soft tint. */ + fill?: boolean; + /** Suffix for value labels (e.g. "%", " ms"). */ + valueSuffix?: string; + /** Prefix for value labels (e.g. "$"). */ + valuePrefix?: string; + /** Print the value next to every data point (≤20 categories). */ + showValues?: boolean; + /** Draw a vertical guide through the cursor while hovering. Defaults to `true`. */ + showHoverGuide?: boolean; + style?: CSSProperties; +}; +export type PieChartProps = { + data: Array; + /** Diameter in px. Defaults to 200. */ + size?: number; + /** Render as a donut with the summed total shown in the hollow center. */ + donut?: boolean; + style?: CSSProperties; +}; +/** + * Multi-series bar/column chart with optional stacking and normalization. + * Distilled from the portal-website Highcharts analytics charts. + * + * Pass `categories` for x-axis labels and one or more `series` whose `data` + * arrays align by index. With a single series you get simple bars; with + * multiple series the default is grouped (side-by-side) — set `stacked` for + * stacked columns or `normalized` for 100%-stacked share-mode. + * + * Colors are auto-assigned from the chart palette. With a **single series**, + * each bar gets a different color by category (so a chart of 5 categories + * shows 5 colors out of the box). With **multiple series**, each series gets + * its own color. A legend appears when there are 2+ series. + * + * For semantic coloring, pass `tone` on a series — it maps to the same + * palette entries used by `Stat`, `Pill`, and `Table` so your chart matches + * tonal elements elsewhere on the page. + * + * @example + * ```tsx + * // Simple single-series + * + * + * // Stacked multi-series (like portal AI commit chart) + * + * + * // Semantic tones — "accepted" renders in the same green as + * // elsewhere on the page. + * + * + * // Mark a target with a reference line. Grouped/single bars also accept + * // `yMin` / `yMax` to frame the axis (stacked bars stay zero-based). + * + * ``` + */ +export declare function BarChart({ categories, series, height, stacked, horizontal, normalized, valueSuffix, valuePrefix, showValues, beginAtZero, yMin, yMax, referenceLines, style }: BarChartProps): JSX.Element; +/** + * Multi-series line chart with optional area fill. Distilled from the + * portal-website Highcharts analytics charts. + * + * Each series draws a polyline with dot markers at each data point. + * Set `fill` to shade the area under every line. Hover over any category + * column to see a tooltip with all series values and a vertical cursor guide. + * + * This is **not** a time-series component — it does not parse dates. + * Pass pre-formatted date strings as `categories` if plotting over time. + * + * Colors are auto-assigned from the chart palette. For semantic coloring, + * pass `tone` on a series — it maps to the same palette entries used by + * `Stat`, `Pill`, and `Table`. + * + * @example + * ```tsx + * // Single line + * + * + * // Multi-series with area fill + * + * + * // Semantic tones — "errors" renders in the same red as a + * // elsewhere on the page. + * + * + * // Zoom into a tight range and mark an SLO. `beginAtZero={false}` + * // auto-fits the axis; `referenceLines` draws the target. + * + * ``` + */ +export declare function LineChart({ categories, series, height, fill, valueSuffix, valuePrefix, showValues, showHoverGuide, beginAtZero, yMin, yMax, referenceLines, style }: LineChartProps): JSX.Element; +/** + * Pie (or donut) chart with hover highlighting. Distilled from the + * portal-website Highcharts analytics charts. + * + * Unlike `BarChart` and `LineChart`, `PieChart` takes a flat `data` array of + * `{ label, value }` points — each slice is its own category. Colors are + * auto-assigned from the chart palette; pass `tone` on a point to give a + * slice a semantic color that matches other tonal elements on the page. + * + * Hovering a slice expands it outward and dims the others; hovering a legend + * item does the same. A tooltip with value and percentage appears below the + * chart. Set `donut` for a hollow center that shows the summed total. + * + * **Do not** use for bar-style comparisons — use `BarChart` instead. + * + * @example + * ```tsx + * // Basic pie + * + * + * // Donut with semantic tones + * + * ``` + */ +export declare function PieChart({ data, size, donut, style }: PieChartProps): JSX.Element; +export {}; +//# sourceMappingURL=chart-primitives.d.ts.map \ No newline at end of file diff --git a/src/crates/assembly/core/builtin_skills/bitfun-canvas/sdk/collapsible-section.d.ts b/src/crates/assembly/core/builtin_skills/bitfun-canvas/sdk/collapsible-section.d.ts new file mode 100644 index 000000000..87b0ed909 --- /dev/null +++ b/src/crates/assembly/core/builtin_skills/bitfun-canvas/sdk/collapsible-section.d.ts @@ -0,0 +1,67 @@ +/** + * Borderless disclosure row — chevron + structured header (title, optional + * leading slot, count, trailing slot) with a body that toggles open. Distilled + * from baby-glass `ContextTreeRow`'s lightweight list-row chrome (no card + * border, no background fill). + * + * For a bordered surface that collapses, use `` instead. + */ +import { type CSSProperties, type JSX, type ReactNode } from "react"; +export type CollapsibleSectionProps = { + /** Plain-text title rendered next to the disclosure chevron. */ + title: string; + /** + * Optional leading visual (e.g. ``) shown between the chevron and + * the title. + */ + leading?: ReactNode; + /** + * Optional small count rendered after the title (e.g. number of children). + */ + count?: number; + /** + * Optional trailing node, right-aligned (e.g. token readout, badge, button). + * Rendered with `t.text.tertiary` color hint via the wrapper; the slot can + * override. + */ + trailing?: ReactNode; + /** Body shown when expanded. */ + children?: ReactNode; + /** When true, the section starts expanded (uncontrolled). */ + defaultOpen?: boolean; + style?: CSSProperties; +}; +/** + * Borderless collapsible row with a structured header. Starts closed unless + * `defaultOpen` is set. + * + * Compose with `` in the `leading` slot for a colored category icon, + * and put a token readout / pill / button in `trailing`. Body content is + * indented under the row so nested `CollapsibleSection`s read as a tree. + * + * For a bordered, card-shaped collapsible surface, use `` + * instead — `CollapsibleSection` has no border or background and is meant to + * sit in a list of similar rows. + * + * @example + * ```tsx + * // Basic + * + * Messages go here. + * + * + * // With a colored category swatch + count + trailing token readout + * } + * trailing={12.3k} + * > + * + * Search results. + * + * + * ``` + */ +export declare function CollapsibleSection({ title, leading, count, trailing, children, defaultOpen, style }: CollapsibleSectionProps): JSX.Element; +//# sourceMappingURL=collapsible-section.d.ts.map \ No newline at end of file diff --git a/src/crates/assembly/core/builtin_skills/bitfun-canvas/sdk/dag-layout.d.ts b/src/crates/assembly/core/builtin_skills/bitfun-canvas/sdk/dag-layout.d.ts new file mode 100644 index 000000000..722e1a01a --- /dev/null +++ b/src/crates/assembly/core/builtin_skills/bitfun-canvas/sdk/dag-layout.d.ts @@ -0,0 +1,102 @@ +/** + * Pure layout math for directed acyclic graphs. Returns positioned node + * coordinates, edge anchor points, rank bounding boxes, and back-edge flags. + * Rendering is the caller's responsibility. + * + * Handles cycles gracefully: back-edges are detected via DFS, excluded from + * ranking, and flagged in the output so the caller can render them differently + * (e.g. dashed arcs). + */ +export type DAGLayoutOptions = { + /** Nodes to lay out. Only `id` is required. */ + nodes: Array<{ + id: string; + }>; + /** Directed edges. */ + edges: Array<{ + from: string; + to: string; + }>; + /** Flow direction. Default `"vertical"` (top-to-bottom). */ + direction?: "vertical" | "horizontal"; + /** Node box width in px. Default 160. */ + nodeWidth?: number; + /** Node box height in px. Default 40. */ + nodeHeight?: number; + /** Gap between ranks (layers) in px. Default 64. */ + rankGap?: number; + /** Gap between sibling nodes in the same rank in px. Default 48. */ + nodeGap?: number; + /** Padding around the bounding box in px. Default 24. */ + padding?: number; +}; +export type DAGLayoutNode = { + id: string; + /** Left edge of the node box. */ + x: number; + /** Top edge of the node box. */ + y: number; + /** Layer index (0 = root). */ + rank: number; + /** Position within the rank (0-indexed). */ + order: number; +}; +export type DAGLayoutEdge = { + from: string; + to: string; + /** Suggested source anchor point (center of the outgoing side). */ + sourceX: number; + sourceY: number; + /** Suggested target anchor point (center of the incoming side). */ + targetX: number; + targetY: number; + /** True when this edge was identified as a back-edge (part of a cycle). */ + isBackEdge: boolean; +}; +export type DAGLayoutRank = { + /** Rank index (0 = root). */ + rank: number; + /** Left edge of the rank bounding box. */ + x: number; + /** Top edge of the rank bounding box. */ + y: number; + /** Width of the rank bounding box. */ + width: number; + /** Height of the rank bounding box. */ + height: number; + /** Node ids in this rank, in order. */ + nodeIds: string[]; +}; +export type DAGLayoutResult = { + nodes: DAGLayoutNode[]; + edges: DAGLayoutEdge[]; + /** Bounding box per rank — useful for drawing layer bands. */ + ranks: DAGLayoutRank[]; + /** The direction used for this layout. */ + direction: "vertical" | "horizontal"; + /** Total width of the bounding box. */ + width: number; + /** Total height of the bounding box. */ + height: number; +}; +/** + * Compute a hierarchical layout for a directed graph. + * + * Returns node positions, edge anchor points, rank bounding boxes, and + * back-edge flags. The caller handles all rendering. + * + * @example + * ```ts + * const layout = computeDAGLayout({ + * nodes: [{ id: "a" }, { id: "b" }, { id: "c" }], + * edges: [{ from: "a", to: "b" }, { from: "b", to: "c" }], + * }); + * + * // layout.nodes[i].x / .y → position your own SVG/HTML elements + * // layout.edges[i].sourceX/Y, targetX/Y → draw lines between them + * // layout.edges[i].isBackEdge → style cycle edges differently + * // layout.ranks[i] → draw layer bands behind each rank + * ``` + */ +export declare function computeDAGLayout(options: DAGLayoutOptions): DAGLayoutResult; +//# sourceMappingURL=dag-layout.d.ts.map \ No newline at end of file diff --git a/src/crates/assembly/core/builtin_skills/bitfun-canvas/sdk/diff-view.d.ts b/src/crates/assembly/core/builtin_skills/bitfun-canvas/sdk/diff-view.d.ts new file mode 100644 index 000000000..8457bcd8a --- /dev/null +++ b/src/crates/assembly/core/builtin_skills/bitfun-canvas/sdk/diff-view.d.ts @@ -0,0 +1,130 @@ +/** + * Low-level diff primitives for the canvas SDK. + * + * The canvas diff surface is intentionally minimal — two components that + * compose with the generic `Card` / `CardHeader` / `CardBody` / `Pill` / + * `Text` primitives to build any diff layout an agent can imagine: + * + * - `DiffView` — a monospaced, syntax-highlighted unified diff renderer. + * Drops into any container (a `Card`, a table cell, a bare layout, + * nothing at all). No card chrome, no header, no path display. Pass + * `path` to auto-detect the language for highlighting, or `language` + * to override. + * + * - `DiffStats` — the canonical `+N` green / `-N` red glyph pair. Use + * it anywhere a small "added/deleted" summary makes sense — in a + * `CardHeader`'s `trailing` slot, next to a filename in a file tree, + * inside a status row, etc. + * + * For file-level metadata the preferred composition is to use the + * generic `Card` family: + * + * ```tsx + * + * }> + * src/utils.ts + * + * + * + * + * + * ``` + */ +import type { CSSProperties, JSX } from "react"; +export type DiffStatsProps = { + additions?: number; + deletions?: number; + style?: CSSProperties; +}; +/** + * Inline `+N` / `-N` glyph pair. Green additions, red deletions, with + * tabular numerals so columns of stats line up. Renders nothing when + * both counts are zero. + * + * @example + * ```tsx + * }> + * src/utils.ts + * + * + * + * Refactor pass + * + * + * ``` + */ +export declare function DiffStats({ additions, deletions, style }: DiffStatsProps): JSX.Element | null; +export type DiffLineType = "added" | "removed" | "unchanged"; +export type DiffLineData = { + type: DiffLineType; + content: string; + lineNumber?: number; +}; +export type DiffViewProps = { + lines: DiffLineData[]; + /** + * File path used to infer the syntax-highlighting language from the + * extension (e.g. `"src/utils.ts"` → `typescript`). The most ergonomic + * way to enable highlighting — pass the same path you show in the + * enclosing card header. Unknown extensions silently render as plain + * text. + * + * If both `path` and `language` are provided, `language` wins. + */ + path?: string; + /** + * Explicit language override for syntax highlighting (e.g. + * `"typescript"`, `"python"`, `"tsx"`). Use this when no file path is + * available, when the path's extension is misleading, or when the + * content is a snippet rather than a real file. Accepts common + * aliases (`ts`, `py`, `rs`, `md`, etc.). Unknown languages silently + * fall back to plain text. + * + * Highlighting is applied per line, so multi-line constructs (block + * comments, template literals) may not colorize perfectly across line + * boundaries. For typical diff-sized inputs this is fine. + */ + language?: string; + /** Show line numbers in the gutter. Default `true`. */ + showLineNumbers?: boolean; + /** Color line numbers green/red for added/removed lines. Default `true`. */ + coloredLineNumbers?: boolean; + /** Show a 3px accent strip on the left edge for changed lines. Default `true`. */ + showAccentStrip?: boolean; + style?: CSSProperties; +}; +/** + * Unified diff body renderer with monospaced type, colored line + * backgrounds, line-number gutter, accent strip, and optional Shiki + * syntax highlighting. + * + * `DiffView` does not provide any surrounding chrome — place it inside + * a `Card` + `CardBody` (with `padding: 0`) when you want the standard + * bordered "file diff" look, or drop it anywhere else if you want the + * bare renderer. + * + * Pass `path` to enable syntax highlighting from the file extension. + * + * @example + * ```tsx + * + * }> + * src/utils.ts + * + * + * + * + * + * ``` + */ +export declare function DiffView({ lines, path, language, showLineNumbers, coloredLineNumbers, showAccentStrip, style }: DiffViewProps): JSX.Element; +//# sourceMappingURL=diff-view.d.ts.map \ No newline at end of file diff --git a/src/crates/assembly/core/builtin_skills/bitfun-canvas/sdk/form-primitives.d.ts b/src/crates/assembly/core/builtin_skills/bitfun-canvas/sdk/form-primitives.d.ts new file mode 100644 index 000000000..7a6be8ebb --- /dev/null +++ b/src/crates/assembly/core/builtin_skills/bitfun-canvas/sdk/form-primitives.d.ts @@ -0,0 +1,195 @@ +/** + * Form primitives for `bitfun/canvas`. Provides themed, controlled form controls + * for interactive canvas apps with persistent state. All `onChange` callbacks + * receive the **value directly** (not a DOM event), so they pair naturally + * with `useCanvasState`: + * + * ```tsx + * const [name, setName] = useCanvasState("name", ""); + * + * ``` + */ +import { type CSSProperties, type JSX, type ReactNode } from "react"; +export type TextInputProps = { + value?: string; + /** Called with the new string value on every keystroke. */ + onChange?: (value: string) => void; + placeholder?: string; + disabled?: boolean; + type?: "text" | "email" | "password" | "number" | "url" | "search"; + style?: CSSProperties; +}; +/** + * Single-line text input (28px height). Use for names, titles, search + * queries, and short text fields. + * + * `onChange` receives the **string value**, not a DOM event — this pairs + * directly with `useCanvasState` setters. + * + * @example + * ```tsx + * const [name, setName] = useCanvasState("name", ""); + * + * + * ``` + */ +export declare function TextInput({ value, onChange, placeholder, disabled, type, style }: TextInputProps): JSX.Element; +export type TextAreaProps = { + value?: string; + /** Called with the new string value on every keystroke. */ + onChange?: (value: string) => void; + placeholder?: string; + disabled?: boolean; + /** Minimum visible rows. Defaults to 3. */ + rows?: number; + style?: CSSProperties; +}; +/** + * Multi-line text input that auto-resizes to fit its content. + * Use for notes, descriptions, comments, and multi-line text fields. + * + * The textarea grows as the user types. Set `rows` for the minimum visible + * height (defaults to 3). Override with `style={{ height: "100px" }}` for a + * fixed size. + * + * @example + * ```tsx + * const [notes, setNotes] = useCanvasState("notes", ""); + * + *