From 3ffa9ab0cfd0be8cc849acff9ef3f4d403b0d5ae Mon Sep 17 00:00:00 2001 From: badMade <106821302+badMade@users.noreply.github.com> Date: Mon, 13 Apr 2026 03:04:21 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Hide=20terminal?= =?UTF-8?q?=20cursor=20during=20CLI=20spinner=20animation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 💡 What: Hides the terminal cursor (`\x1b[?25l`) while the `Spinner` is active and restores it (`\x1b[?25h`) when the spinner finishes, fails, or goes out of scope (via the `Drop` trait). 🎯 Why: Prevents the terminal cursor from jumping around or rendering alongside the spinner frames during long-running tasks, creating a smoother and more polished CLI experience. The `Drop` implementation ensures safety so the cursor is not permanently hidden if the CLI crashes or is interrupted. 📸 Before/After: Visual noise during "🦀 Thinking..." is reduced. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- .Jules/palette.md | 3 +++ .../108fa52a890542199d833cd93ed0429f.json | 8 +++++++ .../200c7e63f6f04a57ac6211a90e4e35d0.json | 9 +++++++ .../59b491f8b3b2439a957e0c680cd193bc.json | 9 +++++++ .../d5662f5a103444a9984129c1c8b01597.json | 9 +++++++ .../sessions/session-1775386832313-0.jsonl | 2 -- .../sessions/session-1775386842352-0.jsonl | 2 -- .../sessions/session-1775386852257-0.jsonl | 2 -- .../sessions/session-1775386853666-0.jsonl | 2 -- rust/crates/rusty-claude-cli/src/render.rs | 24 ++++++++++++++++--- 10 files changed, 59 insertions(+), 11 deletions(-) create mode 100644 .Jules/palette.md create mode 100644 .port_sessions/108fa52a890542199d833cd93ed0429f.json create mode 100644 .port_sessions/200c7e63f6f04a57ac6211a90e4e35d0.json create mode 100644 .port_sessions/59b491f8b3b2439a957e0c680cd193bc.json create mode 100644 .port_sessions/d5662f5a103444a9984129c1c8b01597.json delete mode 100644 rust/.claw/sessions/session-1775386832313-0.jsonl delete mode 100644 rust/.claw/sessions/session-1775386842352-0.jsonl delete mode 100644 rust/.claw/sessions/session-1775386852257-0.jsonl delete mode 100644 rust/.claw/sessions/session-1775386853666-0.jsonl diff --git a/.Jules/palette.md b/.Jules/palette.md new file mode 100644 index 0000000000..a6132bd4a6 --- /dev/null +++ b/.Jules/palette.md @@ -0,0 +1,3 @@ +## $(date +%Y-%m-%d) - [CLI Cursor UX Fix] +**Learning:** This app is a Rust CLI, not a web frontend. Standard web accessibility rules (ARIA labels, DOM structure) do not apply. UX improvements here involve terminal manipulation (using `crossterm` for ANSI output, cursor hiding, colors, text alignment). Hiding the terminal cursor during a `Spinner` animation prevents the cursor from awkwardly jumping or rendering alongside spinner frames. +**Action:** When implementing CLI UX changes that modify terminal state (like hiding a cursor), ALWAYS implement a `Drop` trait to ensure the state (like cursor visibility) is predictably restored if the process is interrupted or panics. diff --git a/.port_sessions/108fa52a890542199d833cd93ed0429f.json b/.port_sessions/108fa52a890542199d833cd93ed0429f.json new file mode 100644 index 0000000000..153dc91faf --- /dev/null +++ b/.port_sessions/108fa52a890542199d833cd93ed0429f.json @@ -0,0 +1,8 @@ +{ + "session_id": "108fa52a890542199d833cd93ed0429f", + "messages": [ + "review MCP tool" + ], + "input_tokens": 3, + "output_tokens": 13 +} \ No newline at end of file diff --git a/.port_sessions/200c7e63f6f04a57ac6211a90e4e35d0.json b/.port_sessions/200c7e63f6f04a57ac6211a90e4e35d0.json new file mode 100644 index 0000000000..b901e8a706 --- /dev/null +++ b/.port_sessions/200c7e63f6f04a57ac6211a90e4e35d0.json @@ -0,0 +1,9 @@ +{ + "session_id": "200c7e63f6f04a57ac6211a90e4e35d0", + "messages": [ + "review MCP tool", + "review MCP tool" + ], + "input_tokens": 6, + "output_tokens": 32 +} \ No newline at end of file diff --git a/.port_sessions/59b491f8b3b2439a957e0c680cd193bc.json b/.port_sessions/59b491f8b3b2439a957e0c680cd193bc.json new file mode 100644 index 0000000000..ead1a0232c --- /dev/null +++ b/.port_sessions/59b491f8b3b2439a957e0c680cd193bc.json @@ -0,0 +1,9 @@ +{ + "session_id": "59b491f8b3b2439a957e0c680cd193bc", + "messages": [ + "review MCP tool", + "review MCP tool" + ], + "input_tokens": 6, + "output_tokens": 32 +} \ No newline at end of file diff --git a/.port_sessions/d5662f5a103444a9984129c1c8b01597.json b/.port_sessions/d5662f5a103444a9984129c1c8b01597.json new file mode 100644 index 0000000000..82393d76a0 --- /dev/null +++ b/.port_sessions/d5662f5a103444a9984129c1c8b01597.json @@ -0,0 +1,9 @@ +{ + "session_id": "d5662f5a103444a9984129c1c8b01597", + "messages": [ + "review MCP tool", + "review MCP tool" + ], + "input_tokens": 6, + "output_tokens": 32 +} \ No newline at end of file diff --git a/rust/.claw/sessions/session-1775386832313-0.jsonl b/rust/.claw/sessions/session-1775386832313-0.jsonl deleted file mode 100644 index 81f7a9d22b..0000000000 --- a/rust/.claw/sessions/session-1775386832313-0.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"created_at_ms":1775386832313,"session_id":"session-1775386832313-0","type":"session_meta","updated_at_ms":1775386832313,"version":1} -{"message":{"blocks":[{"text":"status --help","type":"text"}],"role":"user"},"type":"message"} diff --git a/rust/.claw/sessions/session-1775386842352-0.jsonl b/rust/.claw/sessions/session-1775386842352-0.jsonl deleted file mode 100644 index 4a678ace1a..0000000000 --- a/rust/.claw/sessions/session-1775386842352-0.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"created_at_ms":1775386842352,"session_id":"session-1775386842352-0","type":"session_meta","updated_at_ms":1775386842352,"version":1} -{"message":{"blocks":[{"text":"doctor --help","type":"text"}],"role":"user"},"type":"message"} diff --git a/rust/.claw/sessions/session-1775386852257-0.jsonl b/rust/.claw/sessions/session-1775386852257-0.jsonl deleted file mode 100644 index fa8cb0320f..0000000000 --- a/rust/.claw/sessions/session-1775386852257-0.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"created_at_ms":1775386852257,"session_id":"session-1775386852257-0","type":"session_meta","updated_at_ms":1775386852257,"version":1} -{"message":{"blocks":[{"text":"doctor --help","type":"text"}],"role":"user"},"type":"message"} diff --git a/rust/.claw/sessions/session-1775386853666-0.jsonl b/rust/.claw/sessions/session-1775386853666-0.jsonl deleted file mode 100644 index d2bd3033ed..0000000000 --- a/rust/.claw/sessions/session-1775386853666-0.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"created_at_ms":1775386853666,"session_id":"session-1775386853666-0","type":"session_meta","updated_at_ms":1775386853666,"version":1} -{"message":{"blocks":[{"text":"status --help","type":"text"}],"role":"user"},"type":"message"} diff --git a/rust/crates/rusty-claude-cli/src/render.rs b/rust/crates/rusty-claude-cli/src/render.rs index fd5a6f1c59..4b16288f55 100644 --- a/rust/crates/rusty-claude-cli/src/render.rs +++ b/rust/crates/rusty-claude-cli/src/render.rs @@ -1,7 +1,7 @@ use std::fmt::Write as FmtWrite; use std::io::{self, Write}; -use crossterm::cursor::{MoveToColumn, RestorePosition, SavePosition}; +use crossterm::cursor::{Hide, MoveToColumn, RestorePosition, SavePosition, Show}; use crossterm::style::{Color, Print, ResetColor, SetForegroundColor, Stylize}; use crossterm::terminal::{Clear, ClearType}; use crossterm::{execute, queue}; @@ -47,6 +47,16 @@ impl Default for ColorTheme { #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct Spinner { frame_index: usize, + cursor_hidden: bool, +} + +impl Drop for Spinner { + fn drop(&mut self) { + if self.cursor_hidden { + let mut out = io::stdout(); + let _ = execute!(out, Show); + } + } } impl Spinner { @@ -63,6 +73,10 @@ impl Spinner { theme: &ColorTheme, out: &mut impl Write, ) -> io::Result<()> { + if !self.cursor_hidden { + queue!(out, Hide)?; + self.cursor_hidden = true; + } let frame = Self::FRAMES[self.frame_index % Self::FRAMES.len()]; self.frame_index += 1; queue!( @@ -85,13 +99,15 @@ impl Spinner { out: &mut impl Write, ) -> io::Result<()> { self.frame_index = 0; + self.cursor_hidden = false; execute!( out, MoveToColumn(0), Clear(ClearType::CurrentLine), SetForegroundColor(theme.spinner_done), Print(format!("✔ {label}\n")), - ResetColor + ResetColor, + Show )?; out.flush() } @@ -103,13 +119,15 @@ impl Spinner { out: &mut impl Write, ) -> io::Result<()> { self.frame_index = 0; + self.cursor_hidden = false; execute!( out, MoveToColumn(0), Clear(ClearType::CurrentLine), SetForegroundColor(theme.spinner_failed), Print(format!("✘ {label}\n")), - ResetColor + ResetColor, + Show )?; out.flush() } From e71eaab600042133e40dbd5bb0bc835b175ed0fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 22:44:35 +0000 Subject: [PATCH 2/3] fix: preserve spinner cursor recovery on write errors Agent-Logs-Url: https://github.com/badMade/claw-code/sessions/56bca0b3-24cf-4c30-a1e4-19d7b17df2c5 Co-authored-by: badMade <106821302+badMade@users.noreply.github.com> --- rust/crates/rusty-claude-cli/src/render.rs | 63 +++++++++++++++++++--- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/rust/crates/rusty-claude-cli/src/render.rs b/rust/crates/rusty-claude-cli/src/render.rs index 4b16288f55..0c31b7d9b9 100644 --- a/rust/crates/rusty-claude-cli/src/render.rs +++ b/rust/crates/rusty-claude-cli/src/render.rs @@ -99,8 +99,7 @@ impl Spinner { out: &mut impl Write, ) -> io::Result<()> { self.frame_index = 0; - self.cursor_hidden = false; - execute!( + let show_result = execute!( out, MoveToColumn(0), Clear(ClearType::CurrentLine), @@ -108,7 +107,11 @@ impl Spinner { Print(format!("✔ {label}\n")), ResetColor, Show - )?; + ); + if show_result.is_ok() { + self.cursor_hidden = false; + } + show_result?; out.flush() } @@ -119,8 +122,7 @@ impl Spinner { out: &mut impl Write, ) -> io::Result<()> { self.frame_index = 0; - self.cursor_hidden = false; - execute!( + let show_result = execute!( out, MoveToColumn(0), Clear(ClearType::CurrentLine), @@ -128,7 +130,11 @@ impl Spinner { Print(format!("✘ {label}\n")), ResetColor, Show - )?; + ); + if show_result.is_ok() { + self.cursor_hidden = false; + } + show_result?; out.flush() } } @@ -927,6 +933,7 @@ fn strip_ansi(input: &str) -> String { #[cfg(test)] mod tests { use super::{strip_ansi, MarkdownStreamState, Spinner, TerminalRenderer}; + use std::io::{self, Write}; #[test] fn renders_markdown_with_styling_and_lists() { @@ -1081,4 +1088,48 @@ mod tests { let output = String::from_utf8_lossy(&out); assert!(output.contains("Working")); } + + struct AlwaysFailWriter; + + impl Write for AlwaysFailWriter { + fn write(&mut self, _buf: &[u8]) -> io::Result { + Err(io::Error::other("write failed")) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + + #[test] + fn spinner_finish_error_keeps_cursor_hidden_for_drop_recovery() { + let terminal_renderer = TerminalRenderer::new(); + let mut spinner = Spinner::new(); + let mut out = Vec::new(); + spinner + .tick("Working", terminal_renderer.color_theme(), &mut out) + .expect("tick succeeds"); + + let mut failing_out = AlwaysFailWriter; + let result = spinner.finish("Done", terminal_renderer.color_theme(), &mut failing_out); + + assert!(result.is_err()); + assert!(spinner.cursor_hidden); + } + + #[test] + fn spinner_fail_error_keeps_cursor_hidden_for_drop_recovery() { + let terminal_renderer = TerminalRenderer::new(); + let mut spinner = Spinner::new(); + let mut out = Vec::new(); + spinner + .tick("Working", terminal_renderer.color_theme(), &mut out) + .expect("tick succeeds"); + + let mut failing_out = AlwaysFailWriter; + let result = spinner.fail("Done", terminal_renderer.color_theme(), &mut failing_out); + + assert!(result.is_err()); + assert!(spinner.cursor_hidden); + } } From 495a33df9c45e3db1e1a9b989ca0b8a66e988d1c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 04:16:04 +0000 Subject: [PATCH 3/3] chore: remove local port session artifacts from PR Agent-Logs-Url: https://github.com/badMade/claw-code/sessions/e0dbbfc8-a13d-4979-912a-62bab47f0163 Co-authored-by: badMade <106821302+badMade@users.noreply.github.com> --- .gitignore | 1 + .port_sessions/108fa52a890542199d833cd93ed0429f.json | 8 -------- .port_sessions/200c7e63f6f04a57ac6211a90e4e35d0.json | 9 --------- .port_sessions/59b491f8b3b2439a957e0c680cd193bc.json | 9 --------- .port_sessions/d5662f5a103444a9984129c1c8b01597.json | 9 --------- 5 files changed, 1 insertion(+), 35 deletions(-) delete mode 100644 .port_sessions/108fa52a890542199d833cd93ed0429f.json delete mode 100644 .port_sessions/200c7e63f6f04a57ac6211a90e4e35d0.json delete mode 100644 .port_sessions/59b491f8b3b2439a957e0c680cd193bc.json delete mode 100644 .port_sessions/d5662f5a103444a9984129c1c8b01597.json diff --git a/.gitignore b/.gitignore index 47adce46c8..de3bbd12e3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ __pycache__/ archive/ .omx/ .clawd-agents/ +.port_sessions/ # Rust build artifacts (also in rust/.gitignore) target/ diff --git a/.port_sessions/108fa52a890542199d833cd93ed0429f.json b/.port_sessions/108fa52a890542199d833cd93ed0429f.json deleted file mode 100644 index 153dc91faf..0000000000 --- a/.port_sessions/108fa52a890542199d833cd93ed0429f.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "session_id": "108fa52a890542199d833cd93ed0429f", - "messages": [ - "review MCP tool" - ], - "input_tokens": 3, - "output_tokens": 13 -} \ No newline at end of file diff --git a/.port_sessions/200c7e63f6f04a57ac6211a90e4e35d0.json b/.port_sessions/200c7e63f6f04a57ac6211a90e4e35d0.json deleted file mode 100644 index b901e8a706..0000000000 --- a/.port_sessions/200c7e63f6f04a57ac6211a90e4e35d0.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "session_id": "200c7e63f6f04a57ac6211a90e4e35d0", - "messages": [ - "review MCP tool", - "review MCP tool" - ], - "input_tokens": 6, - "output_tokens": 32 -} \ No newline at end of file diff --git a/.port_sessions/59b491f8b3b2439a957e0c680cd193bc.json b/.port_sessions/59b491f8b3b2439a957e0c680cd193bc.json deleted file mode 100644 index ead1a0232c..0000000000 --- a/.port_sessions/59b491f8b3b2439a957e0c680cd193bc.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "session_id": "59b491f8b3b2439a957e0c680cd193bc", - "messages": [ - "review MCP tool", - "review MCP tool" - ], - "input_tokens": 6, - "output_tokens": 32 -} \ No newline at end of file diff --git a/.port_sessions/d5662f5a103444a9984129c1c8b01597.json b/.port_sessions/d5662f5a103444a9984129c1c8b01597.json deleted file mode 100644 index 82393d76a0..0000000000 --- a/.port_sessions/d5662f5a103444a9984129c1c8b01597.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "session_id": "d5662f5a103444a9984129c1c8b01597", - "messages": [ - "review MCP tool", - "review MCP tool" - ], - "input_tokens": 6, - "output_tokens": 32 -} \ No newline at end of file