Skip to content

Commit 60bba58

Browse files
echobtfactorydroid
andauthored
refactor: centralize common utilities and remove duplicates (#430)
- Add truncate.rs module to cortex-common with centralized truncation utilities - Remove duplicate format_duration from cortex-tui/views/tasks.rs (use cortex_common::format_duration) - Remove duplicate strip_ansi_codes from cortex-cli/run_cmd.rs (use cortex_common::strip_ansi_codes) - Remove duplicate strip_ansi_codes from cortex-engine/agent/handler.rs (use cortex_common::strip_ansi_codes) - Remove duplicate truncate_id and truncate_text from cortex-tui/views/tasks.rs (use cortex_common::truncate_*) - Export new truncate utilities from cortex-common This reduces code duplication and centralizes common utilities for: - Duration formatting - ANSI code stripping - Text truncation with ellipsis (multiple variants) All changes maintain backward compatibility and the codebase compiles cleanly. Co-authored-by: Droid Agent <droid@factory.ai>
1 parent 0c2b75d commit 60bba58

9 files changed

Lines changed: 278 additions & 112 deletions

File tree

cortex-cli/src/run_cmd.rs

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const MAX_FILE_SIZE: u64 = 10 * 1024 * 1024;
3232

3333
use crate::styled_output::{print_success, print_warning};
3434
use cortex_common::resolve_model_alias;
35+
use cortex_common::strip_ansi_codes;
3536
use cortex_common::{resolve_model_with_info, warn_if_ambiguous_model};
3637
use cortex_engine::rollout::get_rollout_path;
3738
use cortex_engine::{Session, list_sessions};
@@ -315,36 +316,6 @@ fn should_use_colors() -> bool {
315316
io::stdout().is_terminal()
316317
}
317318

318-
/// Strip ANSI escape codes from a string.
319-
/// Use this when outputting to non-TTY destinations.
320-
fn strip_ansi_codes(s: &str) -> String {
321-
// Simple regex-free approach: remove ESC [ ... m sequences
322-
let mut result = String::with_capacity(s.len());
323-
let mut chars = s.chars().peekable();
324-
325-
while let Some(c) = chars.next() {
326-
if c == '\x1b' {
327-
// Check for CSI sequence
328-
if chars.peek() == Some(&'[') {
329-
chars.next(); // consume '['
330-
// Skip until we find the terminating character (letter)
331-
while let Some(&next) = chars.peek() {
332-
chars.next();
333-
if next.is_ascii_alphabetic() {
334-
break;
335-
}
336-
}
337-
} else {
338-
result.push(c);
339-
}
340-
} else {
341-
result.push(c);
342-
}
343-
}
344-
345-
result
346-
}
347-
348319
/// Get tool display information.
349320
fn get_tool_display(tool_name: &str) -> ToolDisplay {
350321
match tool_name.to_lowercase().as_str() {

cortex-common/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub mod signal_safety;
1818
pub mod subprocess_env;
1919
pub mod subprocess_output;
2020
pub mod text_sanitize;
21+
pub mod truncate;
2122

2223
#[cfg(feature = "cli")]
2324
pub mod config_override;
@@ -72,6 +73,10 @@ pub use subprocess_output::{
7273
pub use text_sanitize::{
7374
has_control_chars, normalize_code_fences, sanitize_control_chars, sanitize_for_terminal,
7475
};
76+
pub use truncate::{
77+
truncate_command, truncate_first_line, truncate_for_display, truncate_id, truncate_id_default,
78+
truncate_model_name, truncate_with_ellipsis, truncate_with_unicode_ellipsis,
79+
};
7580

7681
#[cfg(feature = "cli")]
7782
pub use config_override::{CliConfigOverrides, ConfigOverride};

cortex-common/src/truncate.rs

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
//! Text truncation utilities.
2+
//!
3+
//! Provides centralized truncation functions used across the codebase.
4+
5+
use std::borrow::Cow;
6+
7+
/// Truncates a string to a maximum length, adding ellipsis if truncated.
8+
///
9+
/// # Arguments
10+
/// * `s` - The string to truncate
11+
/// * `max_len` - Maximum length (including ellipsis)
12+
///
13+
/// # Returns
14+
/// The truncated string with "..." appended if truncation occurred.
15+
///
16+
/// # Examples
17+
/// ```
18+
/// use cortex_common::truncate::truncate_with_ellipsis;
19+
///
20+
/// assert_eq!(truncate_with_ellipsis("hello", 10), "hello");
21+
/// assert_eq!(truncate_with_ellipsis("hello world", 8), "hello...");
22+
/// ```
23+
pub fn truncate_with_ellipsis(s: &str, max_len: usize) -> Cow<'_, str> {
24+
if s.chars().count() <= max_len {
25+
Cow::Borrowed(s)
26+
} else {
27+
let truncated: String = s.chars().take(max_len.saturating_sub(3)).collect();
28+
Cow::Owned(format!("{}...", truncated))
29+
}
30+
}
31+
32+
/// Truncates a string to a maximum length, adding unicode ellipsis (…) if truncated.
33+
///
34+
/// # Arguments
35+
/// * `s` - The string to truncate
36+
/// * `max_len` - Maximum character count (including ellipsis)
37+
///
38+
/// # Returns
39+
/// The truncated string with "…" appended if truncation occurred.
40+
///
41+
/// # Examples
42+
/// ```
43+
/// use cortex_common::truncate::truncate_with_unicode_ellipsis;
44+
///
45+
/// assert_eq!(truncate_with_unicode_ellipsis("hello", 10), "hello");
46+
/// assert_eq!(truncate_with_unicode_ellipsis("hello world", 6), "hello…");
47+
/// ```
48+
pub fn truncate_with_unicode_ellipsis(s: &str, max_len: usize) -> Cow<'_, str> {
49+
let char_count = s.chars().count();
50+
if char_count <= max_len {
51+
Cow::Borrowed(s)
52+
} else {
53+
let truncated: String = s.chars().take(max_len.saturating_sub(1)).collect();
54+
Cow::Owned(format!("{}…", truncated))
55+
}
56+
}
57+
58+
/// Truncates an ID string to show first N characters with ellipsis.
59+
///
60+
/// Commonly used for displaying tool call IDs, session IDs, etc.
61+
///
62+
/// # Arguments
63+
/// * `id` - The ID string to truncate
64+
/// * `max_len` - Maximum length (default 10 if called with truncate_id_default)
65+
///
66+
/// # Returns
67+
/// The truncated ID with "…" appended if truncation occurred.
68+
pub fn truncate_id(id: &str, max_len: usize) -> Cow<'_, str> {
69+
truncate_with_unicode_ellipsis(id, max_len)
70+
}
71+
72+
/// Truncates an ID with default max length of 10.
73+
pub fn truncate_id_default(id: &str) -> Cow<'_, str> {
74+
truncate_id(id, 10)
75+
}
76+
77+
/// Truncates text to a maximum length, taking only the first line.
78+
///
79+
/// Useful for displaying task descriptions, messages, etc.
80+
///
81+
/// # Arguments
82+
/// * `text` - The text to truncate
83+
/// * `max_len` - Maximum character count (including ellipsis)
84+
///
85+
/// # Returns
86+
/// The first line truncated with "…" if it exceeds max_len.
87+
pub fn truncate_first_line(text: &str, max_len: usize) -> Cow<'_, str> {
88+
let first_line = text.lines().next().unwrap_or(text);
89+
truncate_with_unicode_ellipsis(first_line, max_len)
90+
}
91+
92+
/// Truncates a command string for display, preserving important parts.
93+
///
94+
/// # Arguments
95+
/// * `command` - The command to truncate
96+
/// * `max_len` - Maximum length
97+
///
98+
/// # Returns
99+
/// The truncated command with "..." appended if truncation occurred.
100+
pub fn truncate_command(command: &str, max_len: usize) -> Cow<'_, str> {
101+
if command.len() <= max_len {
102+
Cow::Borrowed(command)
103+
} else {
104+
// Take the first part up to max_len - 3 (for "...")
105+
let truncated = &command[..max_len.saturating_sub(3).min(command.len())];
106+
// Find last space to avoid cutting in middle of word
107+
if let Some(last_space) = truncated.rfind(' ') {
108+
Cow::Owned(format!("{}...", &truncated[..last_space]))
109+
} else {
110+
Cow::Owned(format!("{}...", truncated))
111+
}
112+
}
113+
}
114+
115+
/// Truncates a string for display in UI widgets.
116+
///
117+
/// # Arguments
118+
/// * `s` - The string to truncate
119+
/// * `max_len` - Maximum character width
120+
///
121+
/// # Returns
122+
/// The truncated string for display.
123+
pub fn truncate_for_display(s: &str, max_len: usize) -> Cow<'_, str> {
124+
truncate_with_ellipsis(s, max_len)
125+
}
126+
127+
/// Truncates a model name for display in compact spaces.
128+
///
129+
/// # Arguments
130+
/// * `name` - The model name
131+
/// * `max_len` - Maximum length
132+
///
133+
/// # Returns
134+
/// The truncated model name.
135+
pub fn truncate_model_name(name: &str, max_len: usize) -> Cow<'_, str> {
136+
if name.len() <= max_len {
137+
Cow::Borrowed(name)
138+
} else {
139+
// Try to keep the model family prefix if possible
140+
if let Some(slash_pos) = name.find('/') {
141+
let prefix = &name[..slash_pos + 1];
142+
if prefix.len() < max_len.saturating_sub(5) {
143+
let remaining = max_len - prefix.len() - 3;
144+
let suffix_start = name.len().saturating_sub(remaining);
145+
return Cow::Owned(format!("{}...{}", prefix, &name[suffix_start..]));
146+
}
147+
}
148+
truncate_with_ellipsis(name, max_len)
149+
}
150+
}
151+
152+
#[cfg(test)]
153+
mod tests {
154+
use super::*;
155+
156+
#[test]
157+
fn test_truncate_with_ellipsis_short() {
158+
assert_eq!(truncate_with_ellipsis("short", 10).as_ref(), "short");
159+
}
160+
161+
#[test]
162+
fn test_truncate_with_ellipsis_exact() {
163+
assert_eq!(truncate_with_ellipsis("exactlen", 8).as_ref(), "exactlen");
164+
}
165+
166+
#[test]
167+
fn test_truncate_with_ellipsis_long() {
168+
assert_eq!(
169+
truncate_with_ellipsis("this is a long string", 10).as_ref(),
170+
"this is..."
171+
);
172+
}
173+
174+
#[test]
175+
fn test_truncate_with_unicode_ellipsis() {
176+
assert_eq!(
177+
truncate_with_unicode_ellipsis("hello", 10).as_ref(),
178+
"hello"
179+
);
180+
assert_eq!(
181+
truncate_with_unicode_ellipsis("hello world", 6).as_ref(),
182+
"hello…"
183+
);
184+
}
185+
186+
#[test]
187+
fn test_truncate_id() {
188+
assert_eq!(truncate_id("bg-123456789", 10).as_ref(), "bg-123456…");
189+
assert_eq!(truncate_id("bg-1", 10).as_ref(), "bg-1");
190+
}
191+
192+
#[test]
193+
fn test_truncate_first_line() {
194+
assert_eq!(truncate_first_line("line1\nline2", 20).as_ref(), "line1");
195+
assert_eq!(
196+
truncate_first_line("very long first line", 10).as_ref(),
197+
"very long…"
198+
);
199+
}
200+
201+
#[test]
202+
fn test_truncate_command() {
203+
assert_eq!(truncate_command("ls -la", 20).as_ref(), "ls -la");
204+
assert_eq!(
205+
truncate_command("npm install --save-dev typescript", 20).as_ref(),
206+
"npm install..."
207+
);
208+
}
209+
210+
#[test]
211+
fn test_truncate_model_name() {
212+
assert_eq!(truncate_model_name("gpt-4", 10).as_ref(), "gpt-4");
213+
assert_eq!(
214+
truncate_model_name("anthropic/claude-3-opus-20240229", 20).as_ref(),
215+
"anthropic/...20240229"
216+
);
217+
}
218+
}

cortex-engine/src/agent/handler.rs

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use super::{AgentEvent, AgentProfile, RiskLevel, ToolPermission};
44
use crate::client::types::{Message, MessageContent, MessageRole, ToolCall};
55
use crate::error::{CortexError, Result};
6+
use cortex_common::strip_ansi_codes;
67
use std::path::Path;
78
use tokio::sync::mpsc;
89

@@ -182,31 +183,6 @@ impl MessageFilter for NonEmptyFilter {
182183
}
183184
}
184185

185-
/// Strip ANSI escape codes.
186-
fn strip_ansi_codes(text: &str) -> String {
187-
let mut result = String::with_capacity(text.len());
188-
let mut in_escape = false;
189-
let chars = text.chars().peekable();
190-
191-
for c in chars {
192-
if c == '\x1b' {
193-
in_escape = true;
194-
continue;
195-
}
196-
197-
if in_escape {
198-
if c == 'm' {
199-
in_escape = false;
200-
}
201-
continue;
202-
}
203-
204-
result.push(c);
205-
}
206-
207-
result
208-
}
209-
210186
#[cfg(test)]
211187
mod tests {
212188
use super::*;

cortex-gui/src-tauri/src/lib.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1470,7 +1470,11 @@ pub fn run() {
14701470
RunEvent::Ready => {
14711471
info!("Application ready");
14721472
}
1473-
RunEvent::WindowEvent { label, event: tauri::WindowEvent::Destroyed, .. } => {
1473+
RunEvent::WindowEvent {
1474+
label,
1475+
event: tauri::WindowEvent::Destroyed,
1476+
..
1477+
} => {
14741478
// Exit app when all windows are closed
14751479
let windows = app.webview_windows();
14761480
if windows.is_empty() || (windows.len() == 1 && windows.contains_key(&label)) {

cortex-gui/src-tauri/src/mcp/tools.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -373,14 +373,20 @@ async fn capture_linux_screenshot<R: Runtime>(
373373
}
374374

375375
// Try scrot (common lightweight option)
376-
if let Ok(status) = crate::process_utils::command("scrot").args(["-u", output_path]).status() {
376+
if let Ok(status) = crate::process_utils::command("scrot")
377+
.args(["-u", output_path])
378+
.status()
379+
{
377380
if status.success() {
378381
return Ok(());
379382
}
380383
}
381384

382385
// Try maim (another common option) - capture full screen since we can't get window handle
383-
if let Ok(status) = crate::process_utils::command("maim").args([output_path]).status() {
386+
if let Ok(status) = crate::process_utils::command("maim")
387+
.args([output_path])
388+
.status()
389+
{
384390
if status.success() {
385391
return Ok(());
386392
}

0 commit comments

Comments
 (0)