Skip to content

Commit e447de7

Browse files
ilblackdragonclaude
andcommitted
feat(ux): complete UX overhaul — design system, onboarding, web polish
Phase 1 — Design System Foundation: - Unify CLI accent from cyan to emerald green (#34d399) with truecolor detection - Rename bold_cyan() → bold_accent(), add hint() for dim italic tips - Web: add typography scale (--text-xs to --text-3xl), replace 200+ hardcoded font-sizes - Web: spacing audit replacing hardcoded values with --space-* tokens - Web: motion system (easing/duration tokens) + prefers-reduced-motion support - Web: prefers-contrast: more media query for accessibility Phase 2 — First Impressions: - Boot screen progressive disclosure (3-4 lines vs 10, hint to ironclaw status) - Onboarding: auto-detect API keys (Anthropic/OpenAI/OpenRouter), skip interactive steps - Onboarding: minimal wordmark banner replacing ASCII art - Onboarding: dot-based step indicator (● ◉ ○) replacing progress bar - Onboarding: provider smart ordering (detected keys first with checkmarks) - Onboarding: warm completion card with key facts - Onboarding: --step flag for selective re-onboarding (deprecates --channels-only/--provider-only) Phase 3 — Web Chat & Interactions: - Welcome card with i18n suggestion chips - Code block syntax highlighting via highlight.js CDN - Turn cost SSE handler with token/cost badge - Streaming: reduced debounce 150ms→50ms, 10K force-flush safeguard - Connection indicator: amber disconnect, reconnection attempt counter - Skeleton loading states for threads/history - Tool card progress bar animation, icon pop on complete - Streaming cursor pulse, typing indicator dots - Refined approval card styling with keyboard hints - Thread sidebar: preview lines, active accent border - Input area: footer row, char count, send button glow - User messages: right-aligned chat bubbles with accent tint Phase 4 — Polish & Accessibility: - Doctor: grouped output (Core/Features/External sections) - REPL: redesigned /help with quick start + categorized commands - Web: aria-live on chat messages and toasts, aria-label on icon buttons - Web: focus-visible styles, touch targets, safe area padding - Web: mobile optimization (375px breakpoint, iOS zoom prevention) - i18n: new keys in en.js and zh-CN.js for welcome card, connection, messages Bug fixes during review: - Fix duplicate event listeners causing language switch and sidebar toggle to no-op - Fix sidebar collapse hiding toggle button (keep + and « visible when collapsed) - Restore message copy button that was accidentally removed [skip-regression-check] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1490841 commit e447de7

13 files changed

Lines changed: 1424 additions & 582 deletions

File tree

src/boot_screen.rs

Lines changed: 44 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
//! Boot screen displayed after all initialization completes.
22
//!
3-
//! Shows a polished ANSI-styled status panel summarizing the agent's runtime
4-
//! state: model, database, tool count, enabled features, active channels,
5-
//! and the gateway URL.
3+
//! Shows a compact ANSI-styled status panel with three tiers:
4+
//! - **Tier 1 (always):** Name + version, model + backend.
5+
//! - **Tier 2 (conditional):** Gateway URL, tunnel URL, non-default channels.
6+
//! - **Tier 3 (removed):** Database, tool count, features → use `ironclaw status`.
67
78
use crate::cli::fmt;
89

@@ -38,12 +39,19 @@ pub struct BootInfo {
3839
const KW: usize = 10;
3940

4041
/// Print the boot screen to stdout.
42+
///
43+
/// **Tier 1 (always):** Name + version, model + backend.
44+
/// **Tier 2 (conditional):** Gateway URL, tunnel URL, non-default channels.
45+
/// **Tier 3 (removed):** Database, tool count, features — use `ironclaw status`.
4146
pub fn print_boot_screen(info: &BootInfo) {
4247
let border = format!(" {}", fmt::separator(58));
4348

4449
println!();
4550
println!("{border}");
4651
println!();
52+
53+
// ── Tier 1: always shown ──────────────────────────────────────────
54+
4755
println!(
4856
" {}{}{} v{}",
4957
fmt::bold(),
@@ -53,7 +61,7 @@ pub fn print_boot_screen(info: &BootInfo) {
5361
);
5462
println!();
5563

56-
// Model line — complex (multiple styled parts), so manual formatting
64+
// Model line
5765
let model_display = if let Some(ref cheap) = info.cheap_model {
5866
format!(
5967
"{}{}{} {}cheap{} {}{}{}",
@@ -80,94 +88,10 @@ pub fn print_boot_screen(info: &BootInfo) {
8088
width = KW,
8189
);
8290

83-
// Database line
84-
let db_status = if info.db_connected {
85-
"connected"
86-
} else {
87-
"none"
88-
};
89-
let db_value = format!("{} ({})", info.db_backend, db_status);
90-
println!("{}", fmt::kv_line("database", &db_value, KW));
91-
92-
// Tools line
93-
let tools_value = format!("{} registered", info.tool_count);
94-
println!("{}", fmt::kv_line("tools", &tools_value, KW));
95-
96-
// Features line — complex (multiple items, some with warnings), so manual formatting
97-
let mut features = Vec::new();
98-
if info.embeddings_enabled {
99-
if let Some(ref provider) = info.embeddings_provider {
100-
features.push(format!("embeddings ({provider})"));
101-
} else {
102-
features.push("embeddings".to_string());
103-
}
104-
}
105-
if info.heartbeat_enabled {
106-
let mins = info.heartbeat_interval_secs / 60;
107-
features.push(format!("heartbeat ({mins}m)"));
108-
}
109-
match info.docker_status {
110-
crate::sandbox::detect::DockerStatus::Available => {
111-
features.push("sandbox".to_string());
112-
}
113-
crate::sandbox::detect::DockerStatus::NotInstalled => {
114-
features.push(format!(
115-
"{}sandbox (docker not installed){}",
116-
fmt::warning(),
117-
fmt::reset()
118-
));
119-
}
120-
crate::sandbox::detect::DockerStatus::NotRunning => {
121-
features.push(format!(
122-
"{}sandbox (docker not running){}",
123-
fmt::warning(),
124-
fmt::reset()
125-
));
126-
}
127-
crate::sandbox::detect::DockerStatus::Disabled => {
128-
// Don't show sandbox when disabled
129-
}
130-
}
131-
if info.claude_code_enabled {
132-
features.push("claude-code".to_string());
133-
}
134-
if info.routines_enabled {
135-
features.push("routines".to_string());
136-
}
137-
if info.skills_enabled {
138-
features.push("skills".to_string());
139-
}
140-
if !features.is_empty() {
141-
println!(
142-
" {}{:<width$}{} {}{}{}",
143-
fmt::dim(),
144-
"features",
145-
fmt::reset(),
146-
fmt::accent(),
147-
features.join(" "),
148-
fmt::reset(),
149-
width = KW,
150-
);
151-
}
152-
153-
// Channels line
154-
if !info.channels.is_empty() {
155-
let channels_value = info.channels.join(" ");
156-
println!(
157-
" {}{:<width$}{} {}{}{}",
158-
fmt::dim(),
159-
"channels",
160-
fmt::reset(),
161-
fmt::accent(),
162-
channels_value,
163-
fmt::reset(),
164-
width = KW,
165-
);
166-
}
91+
// ── Tier 2: conditional ───────────────────────────────────────────
16792

168-
// Gateway URL (highlighted)
93+
// Gateway URL
16994
if let Some(ref url) = info.gateway_url {
170-
println!();
17195
println!(
17296
" {}{:<width$}{} {}{}{}",
17397
fmt::dim(),
@@ -185,7 +109,7 @@ pub fn print_boot_screen(info: &BootInfo) {
185109
let provider_tag = info
186110
.tunnel_provider
187111
.as_deref()
188-
.map(|p| format!(" {}({}){}", fmt::dim(), p, fmt::reset()))
112+
.map(|p| format!(" {}({}){}", fmt::dim(), p, fmt::reset()))
189113
.unwrap_or_default();
190114
println!(
191115
" {}{:<width$}{} {}{}{}{}",
@@ -200,6 +124,28 @@ pub fn print_boot_screen(info: &BootInfo) {
200124
);
201125
}
202126

127+
// Non-default channels (skip if only the default set)
128+
let non_default: Vec<&str> = info
129+
.channels
130+
.iter()
131+
.filter(|c| !matches!(c.as_str(), "repl" | "gateway"))
132+
.map(|c| c.as_str())
133+
.collect();
134+
if !non_default.is_empty() {
135+
println!(
136+
" {}{:<width$}{} {}{}{}",
137+
fmt::dim(),
138+
"channels",
139+
fmt::reset(),
140+
fmt::accent(),
141+
non_default.join(" "),
142+
fmt::reset(),
143+
width = KW,
144+
);
145+
}
146+
147+
// ── Footer ────────────────────────────────────────────────────────
148+
203149
println!();
204150
println!("{border}");
205151

@@ -215,6 +161,13 @@ pub fn print_boot_screen(info: &BootInfo) {
215161
println!(" {}ready in {}{}", fmt::dim(), elapsed_str, fmt::reset());
216162
}
217163

164+
// Hint to run `ironclaw status` for full details
165+
println!(
166+
" {}Run `ironclaw status` for full system details.{}",
167+
fmt::hint(),
168+
fmt::reset()
169+
);
170+
218171
println!();
219172
}
220173

src/channels/repl.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -263,22 +263,29 @@ impl Default for ReplChannel {
263263

264264
fn print_help() {
265265
let h = fmt::bold();
266-
let c = fmt::bold_cyan();
266+
let c = fmt::bold_accent();
267267
let d = fmt::dim();
268268
let r = fmt::reset();
269+
let hi = fmt::hint();
269270

270271
println!();
271272
println!(" {h}IronClaw REPL{r}");
272273
println!();
274+
println!(" {h}Quick start{r}");
275+
println!(" {c}/new{r} {hi}Start a new thread{r}");
276+
println!(" {c}/compact{r} {hi}Compress context window{r}");
277+
println!(" {c}/quit{r} {hi}Exit{r}");
278+
println!();
279+
println!(" {h}All commands{r}");
273280
println!(
274-
" {h}Conversation{r} {c}/new{r} {c}/clear{r} {c}/compact{r} {c}/undo{r} {c}/redo{r} {c}/summarize{r} {c}/suggest{r}"
281+
" {d}Conversation{r} {c}/new{r} {c}/clear{r} {c}/compact{r} {c}/undo{r} {c}/redo{r} {c}/summarize{r} {c}/suggest{r}"
275282
);
276-
println!(" {h}Threads{r} {c}/thread{r} {c}/resume{r} {c}/list{r}");
277-
println!(" {h}Execution{r} {c}/interrupt{r} {d}(esc){r} {c}/cancel{r}");
283+
println!(" {d}Threads{r} {c}/thread{r} {c}/resume{r} {c}/list{r}");
284+
println!(" {d}Execution{r} {c}/interrupt{r} {d}(esc){r} {c}/cancel{r}");
278285
println!(
279-
" {h}System{r} {c}/tools{r} {c}/model{r} {c}/version{r} {c}/status{r} {c}/debug{r} {c}/heartbeat{r}"
286+
" {d}System{r} {c}/tools{r} {c}/model{r} {c}/version{r} {c}/status{r} {c}/debug{r} {c}/heartbeat{r}"
280287
);
281-
println!(" {h}Session{r} {c}/help{r} {c}/quit{r}");
288+
println!(" {d}Session{r} {c}/help{r} {c}/quit{r}");
282289
println!();
283290
}
284291

@@ -360,11 +367,11 @@ impl Channel for ReplChannel {
360367
"{}[debug]{} {}\u{203A}{} ",
361368
fmt::warning(),
362369
fmt::reset(),
363-
fmt::bold_cyan(),
370+
fmt::bold_accent(),
364371
fmt::reset()
365372
)
366373
} else {
367-
format!("{}\u{203A}{} ", fmt::bold_cyan(), fmt::reset())
374+
format!("{}\u{203A}{} ", fmt::bold_accent(), fmt::reset())
368375
};
369376

370377
match rl.readline(&prompt) {

0 commit comments

Comments
 (0)