mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
* feat(ux): complete UX overhaul — design system, boot screen, onboarding, web polish Shared design system: CSS custom properties for spacing, typography, transitions, and color tokens used across web UI and boot screen. Boot screen: compact feature-tags line showing enabled subsystems (db, tools, routines, heartbeat, skills, sandbox, embeddings) at a glance. Downgrade startup info logs (libSQL, webhook, workspace seed) to debug level since the boot screen now covers this. Onboarding wizard: model picker with live API fetch, provider-aware auth flow, improved error recovery and progress display. Web UI: ARIA attributes, welcome card, streaming debounce, connection status banner, skeleton loaders, send cooldown. CLI: doctor command enhancements, status command cleanup, REPL banner consolidation, shared fmt module. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat(ux): Apple-level design refinements — spring physics, glass morphism, chat polish Merge staging theme support (dark/light/system toggle) and layer UX polish on top: spring-physics motion, glass morphism depth, chat experience improvements, and responsive mobile refinements. Design system: - Restore and extend design token system (spacing, typography, timing, easing) with legacy aliases for theme compatibility - Add shadow tiers, accent glow, glass morphism, spring easing tokens - Tokens defined in both dark (:root) and light ([data-theme="light"]) Micro-interactions (Phase 2): - Spring-overshoot message entry animation (slideUp) - Spring-scale button press on all interactive buttons - Tab crossfade animation, tool card smooth accordion (max-height) - Modal scale(0.95) + blur(8px) entry, toast spring slide - Sidebar width crossfade, card hover lift Visual depth (Phase 3): - Tab bar glass morphism + surface highlight + sliding indicator - Active tab accent background pill - Assistant message accent left border, user message bubble tail - Floating input area (rounded + shadow + margin) Chat polish (Phase 4): - Smooth streaming cursor (cursorPulse), message hover timestamps - Time separators (Today/Yesterday/date) - Textarea smooth auto-expand, send button glow Settings & forms (Phase 5): - iOS-style toggle switches for boolean settings - Input focus glow, save feedback spring animation - Welcome card with gradient background + proper spacing - Sticky settings group headers with glass backdrop Accessibility & mobile (Phase 6): - Animated focus ring, prefers-reduced-motion global kill-switch - Touch target audit (44px min), mobile bottom-sheet modals - Mobile bottom tab bar, toast redesign (icon + border + countdown) - Thread hover translateX, badge in_progress pulse Bug fixes: - Gateway/TEE popover z-index (tab-bar z-index: 200, popovers 500) - Connection lost banner as fixed top bar instead of flex child - Sidebar collapse keeps toggle + new thread buttons visible - Downgrade noisy startup logs (db, webhook, vector) to debug - Remove green dot pulse animation on connected status - Deduplicate confirm-modal in HTML, add tab-indicator div Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat(web): mobile layout improvements — sidebar toggle, settings drill-down, tab bar polish - Fix mobile sidebar toggle: use expanded-mobile class instead of collapsed, add backdrop overlay, auto-close on thread select, outside-click dismiss - Settings: replace cramped horizontal tabs with drill-down navigation (category list → detail view → back button) - Bottom tab bar: add glass morphism, hide theme toggle, flip tab indicator to top edge - Keep thread toggle button visible in collapsed 36px sidebar strip Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat(repl): interactive approval selector and transient status lines - Replace ASCII-art approval box with clean horizontal rule card - Add inquire-based interactive selector for tool approvals (↑↓ + Enter) - Selector runs directly from send_status via spawn_blocking, with stdin_locked flag to prevent readline from competing for stdin - Transient thinking/tool-started lines: each replaces the previous, all erased before final output (no clutter left in scrollback) - Esc in selector sends denial so agent never gets stuck Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: widen TurnCost token fields to u64 and remove unused variable - Change input_tokens/output_tokens from u32 to u64 in StatusUpdate::TurnCost, SseEvent::TurnCost, and the thread_ops emit site to avoid truncation on large conversations - Remove unused _routine_engine_for_loop binding in agent_loop.rs Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * chore: reduce startup log noise — demote info to debug Demote routine startup messages (builder, WASM tools, tunnel, WASM channels) from info to debug so the default log output stays clean. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(web): allow CDN scripts in CSP connect-src directive Add cdn.jsdelivr.net and cdnjs.cloudflare.com to connect-src so the browser can fetch marked.js and DOMPurify without CSP violations. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * style: fix cargo fmt in repl.rs Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(web): gate turn_cost SSE handler on current thread Prevents cost badge from attaching to the wrong message when switching threads or receiving events from background threads. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * ci: retrigger CI * fix: add missing extension_manager to webhook EngineContext The webhook trigger path added in #736 was missing the extension_manager field introduced by #1453. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * chore: ignore RUSTSEC-2026-0049 rustls-webpki CRL advisory Low impact — requires compromised CA to exploit. Tracked for upstream rustls-webpki upgrade. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(routines): use fields.join for cron normalization Use split_whitespace fields instead of re-trimming the original string to avoid preserving extra internal whitespace in cron expressions. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat(repl): Apple-style approval card — clean vertical flow - Drop verbose tool description (the command IS the decision surface) - Unified vertical pipe layout: ◆ header → │ params → │ selector - Selector options show keyboard shortcuts inline: Approve (y) - Compact help message, answered state uses └ to close the flow - No horizontal rules, no blank-line padding — just breathing room Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor(repl): replace inquire with crossterm for approval selector Drop the inquire dependency (which pulled in crossterm 0.25, duplicating the existing 0.28). The 3-option approval selector is now built directly with crossterm raw mode — same UX, zero new dependencies. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * chore(deps): upgrade crossterm 0.28 → 0.29, eliminate duplication termimad (via crokey) uses crossterm 0.29. Upgrading our direct dependency from 0.28 to 0.29 collapses to a single crossterm version in the dependency tree. Also migrated termimad::crossterm:: references to the direct crossterm import. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address review comments — box_top off-by-one, smart_truncate overflow, mobile theme toggle - Fix box_top() fill calculation: was off-by-one, producing boxes 1 char too wide (fmt.rs) - Fix smart_truncate(): account for "..." in the budget so output never exceeds max_chars (repl.rs) - Move theme toggle to settings sidebar on mobile instead of display:none, so mobile users can still switch themes (style.css, index.html, app.js) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * style: cargo fmt repl.rs Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address review — retry duplication, CSP connect-src, deny color - Remove failed message before retry to prevent duplicate user messages - Revert connect-src to 'self' — CDN hosts only need script-src - Use red for Deny confirmation in REPL approval selector Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
348 lines
10 KiB
Rust
348 lines
10 KiB
Rust
//! Boot screen displayed after all initialization completes.
|
|
//!
|
|
//! Shows a compact ANSI-styled status panel with three tiers:
|
|
//! - **Tier 1 (always):** Name + version, model + backend.
|
|
//! - **Tier 2 (conditional):** Gateway URL, tunnel URL, non-default channels.
|
|
//! - **Tier 3 (removed):** Database, tool count, features → use `ironclaw status`.
|
|
|
|
use crate::cli::fmt;
|
|
|
|
/// All displayable fields for the boot screen.
|
|
pub struct BootInfo {
|
|
pub version: String,
|
|
pub agent_name: String,
|
|
pub llm_backend: String,
|
|
pub llm_model: String,
|
|
pub cheap_model: Option<String>,
|
|
pub db_backend: String,
|
|
pub db_connected: bool,
|
|
pub tool_count: usize,
|
|
pub gateway_url: Option<String>,
|
|
pub embeddings_enabled: bool,
|
|
pub embeddings_provider: Option<String>,
|
|
pub heartbeat_enabled: bool,
|
|
pub heartbeat_interval_secs: u64,
|
|
pub sandbox_enabled: bool,
|
|
pub docker_status: crate::sandbox::detect::DockerStatus,
|
|
pub claude_code_enabled: bool,
|
|
pub routines_enabled: bool,
|
|
pub skills_enabled: bool,
|
|
pub channels: Vec<String>,
|
|
/// Public URL from a managed tunnel (e.g., "https://abc.ngrok.io").
|
|
pub tunnel_url: Option<String>,
|
|
/// Provider name for the managed tunnel (e.g., "ngrok").
|
|
pub tunnel_provider: Option<String>,
|
|
/// Time elapsed during startup. Shown at the bottom when present.
|
|
pub startup_elapsed: Option<std::time::Duration>,
|
|
}
|
|
|
|
const KW: usize = 10;
|
|
|
|
/// Print the boot screen to stdout.
|
|
///
|
|
/// **Tier 1 (always):** Name + version, model + backend.
|
|
/// **Tier 2 (conditional):** Gateway URL, tunnel URL, non-default channels.
|
|
/// **Tier 3 (removed):** Database, tool count, features — use `ironclaw status`.
|
|
pub fn print_boot_screen(info: &BootInfo) {
|
|
let border = format!(" {}", fmt::separator(58));
|
|
|
|
println!();
|
|
println!("{border}");
|
|
println!();
|
|
|
|
// ── Tier 1: always shown ──────────────────────────────────────────
|
|
|
|
println!(
|
|
" {}{}{} v{}",
|
|
fmt::bold(),
|
|
info.agent_name,
|
|
fmt::reset(),
|
|
info.version
|
|
);
|
|
println!();
|
|
|
|
// Model line
|
|
let model_display = if let Some(ref cheap) = info.cheap_model {
|
|
format!(
|
|
"{}{}{} {}cheap{} {}{}{}",
|
|
fmt::accent(),
|
|
info.llm_model,
|
|
fmt::reset(),
|
|
fmt::dim(),
|
|
fmt::reset(),
|
|
fmt::accent(),
|
|
cheap,
|
|
fmt::reset(),
|
|
)
|
|
} else {
|
|
format!("{}{}{}", fmt::accent(), info.llm_model, fmt::reset())
|
|
};
|
|
println!(
|
|
" {}{:<width$}{} {model_display} {}via {}{}",
|
|
fmt::dim(),
|
|
"model",
|
|
fmt::reset(),
|
|
fmt::dim(),
|
|
info.llm_backend,
|
|
fmt::reset(),
|
|
width = KW,
|
|
);
|
|
|
|
// ── Tier 2: conditional ───────────────────────────────────────────
|
|
|
|
// Gateway URL
|
|
if let Some(ref url) = info.gateway_url {
|
|
println!(
|
|
" {}{:<width$}{} {}{}{}",
|
|
fmt::dim(),
|
|
"gateway",
|
|
fmt::reset(),
|
|
fmt::link(),
|
|
url,
|
|
fmt::reset(),
|
|
width = KW,
|
|
);
|
|
}
|
|
|
|
// Tunnel URL
|
|
if let Some(ref url) = info.tunnel_url {
|
|
let provider_tag = info
|
|
.tunnel_provider
|
|
.as_deref()
|
|
.map(|p| format!(" {}({}){}", fmt::dim(), p, fmt::reset()))
|
|
.unwrap_or_default();
|
|
println!(
|
|
" {}{:<width$}{} {}{}{}{}",
|
|
fmt::dim(),
|
|
"tunnel",
|
|
fmt::reset(),
|
|
fmt::link(),
|
|
url,
|
|
fmt::reset(),
|
|
provider_tag,
|
|
width = KW,
|
|
);
|
|
}
|
|
|
|
// Non-default channels (skip if only the default set)
|
|
let non_default: Vec<&str> = info
|
|
.channels
|
|
.iter()
|
|
.filter(|c| !matches!(c.as_str(), "repl" | "gateway"))
|
|
.map(|c| c.as_str())
|
|
.collect();
|
|
if !non_default.is_empty() {
|
|
println!(
|
|
" {}{:<width$}{} {}{}{}",
|
|
fmt::dim(),
|
|
"channels",
|
|
fmt::reset(),
|
|
fmt::accent(),
|
|
non_default.join(" "),
|
|
fmt::reset(),
|
|
width = KW,
|
|
);
|
|
}
|
|
|
|
// ── Tier 3: compact feature tags ──────────────────────────────────
|
|
|
|
let mut tags: Vec<String> = Vec::new();
|
|
|
|
// Database
|
|
if info.db_connected {
|
|
tags.push(format!("db:{}", info.db_backend));
|
|
}
|
|
|
|
// Tool count
|
|
if info.tool_count > 0 {
|
|
tags.push(format!("tools:{}", info.tool_count));
|
|
}
|
|
|
|
// Routines
|
|
if info.routines_enabled {
|
|
tags.push("routines".to_string());
|
|
}
|
|
|
|
// Heartbeat with interval
|
|
if info.heartbeat_enabled {
|
|
let interval = if info.heartbeat_interval_secs >= 3600
|
|
&& info.heartbeat_interval_secs.is_multiple_of(3600)
|
|
{
|
|
format!("{}h", info.heartbeat_interval_secs / 3600)
|
|
} else if info.heartbeat_interval_secs >= 60
|
|
&& info.heartbeat_interval_secs.is_multiple_of(60)
|
|
{
|
|
format!("{}m", info.heartbeat_interval_secs / 60)
|
|
} else {
|
|
format!("{}s", info.heartbeat_interval_secs)
|
|
};
|
|
tags.push(format!("heartbeat:{interval}"));
|
|
}
|
|
|
|
// Skills
|
|
if info.skills_enabled {
|
|
tags.push("skills".to_string());
|
|
}
|
|
|
|
// Sandbox / Docker
|
|
if info.sandbox_enabled {
|
|
let suffix = match info.docker_status {
|
|
crate::sandbox::detect::DockerStatus::Available => "",
|
|
crate::sandbox::detect::DockerStatus::NotRunning => ":stopped",
|
|
_ => ":unavail",
|
|
};
|
|
tags.push(format!("sandbox{suffix}"));
|
|
}
|
|
|
|
// Embeddings
|
|
if info.embeddings_enabled {
|
|
if let Some(ref provider) = info.embeddings_provider {
|
|
tags.push(format!("embeddings:{provider}"));
|
|
} else {
|
|
tags.push("embeddings".to_string());
|
|
}
|
|
}
|
|
|
|
// Claude Code bridge
|
|
if info.claude_code_enabled {
|
|
tags.push("claude-code".to_string());
|
|
}
|
|
|
|
if !tags.is_empty() {
|
|
println!(
|
|
" {}{:<width$}{} {}",
|
|
fmt::dim(),
|
|
"features",
|
|
fmt::reset(),
|
|
tags.join(" "),
|
|
width = KW,
|
|
);
|
|
}
|
|
|
|
// ── Footer ────────────────────────────────────────────────────────
|
|
|
|
println!();
|
|
println!("{border}");
|
|
|
|
// Startup elapsed
|
|
if let Some(elapsed) = info.startup_elapsed {
|
|
let millis = elapsed.as_millis();
|
|
let elapsed_str = if millis < 1000 {
|
|
format!("{millis}ms")
|
|
} else {
|
|
let secs = elapsed.as_secs_f64();
|
|
format!("{secs:.1}s")
|
|
};
|
|
println!(" {}ready in {}{}", fmt::dim(), elapsed_str, fmt::reset());
|
|
}
|
|
|
|
// Hint to run `ironclaw status` for full details
|
|
println!(
|
|
" {}Run `ironclaw status` for full system details.{}",
|
|
fmt::hint(),
|
|
fmt::reset()
|
|
);
|
|
|
|
println!();
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::sandbox::detect::DockerStatus;
|
|
|
|
#[test]
|
|
fn test_print_boot_screen_full() {
|
|
let info = BootInfo {
|
|
version: "0.2.0".to_string(),
|
|
agent_name: "ironclaw".to_string(),
|
|
llm_backend: "nearai".to_string(),
|
|
llm_model: "claude-3-5-sonnet-20241022".to_string(),
|
|
cheap_model: Some("gpt-4o-mini".to_string()),
|
|
db_backend: "libsql".to_string(),
|
|
db_connected: true,
|
|
tool_count: 24,
|
|
gateway_url: Some("http://127.0.0.1:3001/?token=abc123".to_string()),
|
|
embeddings_enabled: true,
|
|
embeddings_provider: Some("openai".to_string()),
|
|
heartbeat_enabled: true,
|
|
heartbeat_interval_secs: 1800,
|
|
sandbox_enabled: true,
|
|
docker_status: DockerStatus::Available,
|
|
claude_code_enabled: false,
|
|
routines_enabled: true,
|
|
skills_enabled: true,
|
|
channels: vec![
|
|
"repl".to_string(),
|
|
"gateway".to_string(),
|
|
"telegram".to_string(),
|
|
],
|
|
tunnel_url: Some("https://abc123.ngrok.io".to_string()),
|
|
tunnel_provider: Some("ngrok".to_string()),
|
|
startup_elapsed: None,
|
|
};
|
|
// Should not panic
|
|
print_boot_screen(&info);
|
|
}
|
|
|
|
#[test]
|
|
fn test_print_boot_screen_minimal() {
|
|
let info = BootInfo {
|
|
version: "0.2.0".to_string(),
|
|
agent_name: "ironclaw".to_string(),
|
|
llm_backend: "nearai".to_string(),
|
|
llm_model: "gpt-4o".to_string(),
|
|
cheap_model: None,
|
|
db_backend: "none".to_string(),
|
|
db_connected: false,
|
|
tool_count: 5,
|
|
gateway_url: None,
|
|
embeddings_enabled: false,
|
|
embeddings_provider: None,
|
|
heartbeat_enabled: false,
|
|
heartbeat_interval_secs: 0,
|
|
sandbox_enabled: false,
|
|
docker_status: DockerStatus::Disabled,
|
|
claude_code_enabled: false,
|
|
routines_enabled: false,
|
|
skills_enabled: false,
|
|
channels: vec![],
|
|
tunnel_url: None,
|
|
tunnel_provider: None,
|
|
startup_elapsed: None,
|
|
};
|
|
// Should not panic
|
|
print_boot_screen(&info);
|
|
}
|
|
|
|
#[test]
|
|
fn test_print_boot_screen_no_features() {
|
|
let info = BootInfo {
|
|
version: "0.1.0".to_string(),
|
|
agent_name: "test".to_string(),
|
|
llm_backend: "openai".to_string(),
|
|
llm_model: "gpt-4o".to_string(),
|
|
cheap_model: None,
|
|
db_backend: "postgres".to_string(),
|
|
db_connected: true,
|
|
tool_count: 10,
|
|
gateway_url: None,
|
|
embeddings_enabled: false,
|
|
embeddings_provider: None,
|
|
heartbeat_enabled: false,
|
|
heartbeat_interval_secs: 0,
|
|
sandbox_enabled: false,
|
|
docker_status: DockerStatus::Disabled,
|
|
claude_code_enabled: false,
|
|
routines_enabled: false,
|
|
skills_enabled: false,
|
|
channels: vec!["repl".to_string()],
|
|
tunnel_url: None,
|
|
tunnel_provider: None,
|
|
startup_elapsed: None,
|
|
};
|
|
// Should not panic
|
|
print_boot_screen(&info);
|
|
}
|
|
}
|