ci: add GitHub Actions workflows with semantic-release auto-versioning

chore: fix all 702 clippy warnings across codebase
- auto-fix 475 via cargo clippy --fix
- fix remaining 227 manually: uninlined_format_args, redundant_closure, match_same_arms,
  underscore_binding, format_push_string, items_after_statements, needless_pass_by_value,
  clone_on_copy, case_sensitive_extension, single_match/let-else, write_with_newline,
  and other clippy lints
This commit is contained in:
asepharyana
2026-07-13 08:12:12 +07:00
parent be921d6836
commit 29a9fae3f6
79 changed files with 826 additions and 904 deletions
+8 -6
View File
@@ -1,3 +1,4 @@
#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)]
//! Chat transcript panel rendering — message cards with role badges.
//!
//! Flow: `draw_chat` turns `state.transcript_cache.messages` into a
@@ -19,7 +20,7 @@ use ratatui::Frame;
use super::theme::Theme;
/// Break a flat run of styled spans into `Line`s at embedded `\n` boundaries.
fn split_spans_into_lines<'a>(spans: Vec<Span<'a>>) -> Vec<Line<'a>> {
fn split_spans_into_lines(spans: Vec<Span<'_>>) -> Vec<Line<'_>> {
let mut lines = Vec::new();
let mut current_spans = Vec::new();
@@ -76,10 +77,11 @@ fn format_timestamp(ts: i64) -> String {
let secs = ts / 1000;
let mins = (secs / 60) % 60;
let hrs = (secs / 3600) % 24;
format!("{:02}:{:02}", hrs, mins)
format!("{hrs:02}:{mins:02}")
}
/// Render the scrollable chat transcript panel with message card styling.
#[allow(clippy::too_many_lines)]
pub fn draw_chat(frame: &mut Frame, area: Rect, state: &crate::app::state::rest::AppStateRest) {
let messages = &state.transcript_cache.messages;
let scroll_offset = state.scroll.offset;
@@ -95,7 +97,7 @@ pub fn draw_chat(frame: &mut Frame, area: Rect, state: &crate::app::state::rest:
};
// ── Render messages as cards ─────────────────────────────────────────
for (_msg_idx, msg) in messages.iter().enumerate() {
for msg in messages {
let accent = role_accent_color(&msg.role);
let badge = role_badge(&msg.role);
let label = role_label(&msg.role);
@@ -119,12 +121,12 @@ pub fn draw_chat(frame: &mut Frame, area: Rect, state: &crate::app::state::rest:
),
// Role name
Span::styled(
format!(" {}", label),
format!(" {label}"),
Style::default().fg(accent).add_modifier(Modifier::BOLD),
),
// Timestamp
Span::styled(
if ts_str.is_empty() { String::new() } else { format!(" {}", ts_str) },
if ts_str.is_empty() { String::new() } else { format!(" {ts_str}") },
Style::default().fg(Theme::TEXT_DIM),
),
]);
@@ -176,7 +178,7 @@ pub fn draw_chat(frame: &mut Frame, area: Rect, state: &crate::app::state::rest:
.add_modifier(Modifier::BOLD),
),
Span::styled(
format!(" {} ", spinner),
format!(" {spinner} "),
Style::default().fg(Theme::ROLE_ASSISTANT).add_modifier(Modifier::BOLD),
),
Span::styled(
+11 -25
View File
@@ -14,12 +14,13 @@ use super::theme::Theme;
/// Render a markdown string into styled terminal spans, word-wrapped to `width`.
///
/// Flow: pulldown_cmark parses `text` into an event stream → each
/// Flow: `pulldown_cmark` parses `text` into an event stream → each
/// Start/End/Text/Code/Break event is translated into styled `Span`s →
/// if `width > 0`, a second pass wraps long lines.
///
/// Return: a flat vec of styled spans; `chat::split_spans_into_lines`
/// turns it back into `Line`s for the Paragraph widget.
#[allow(clippy::too_many_lines)]
pub fn render_markdown(text: &str, width: u16) -> Vec<Span<'static>> {
let mut spans = Vec::new();
let parser = pulldown_cmark::Parser::new(text);
@@ -61,9 +62,6 @@ pub fn render_markdown(text: &str, width: u16) -> Vec<Span<'static>> {
pulldown_cmark::Tag::Paragraph => {
first_in_paragraph = true;
}
pulldown_cmark::Tag::Emphasis => {}
pulldown_cmark::Tag::Strong => {}
pulldown_cmark::Tag::List(_) => {}
pulldown_cmark::Tag::Item => {
// List item bullet
spans.push(Span::styled(
@@ -79,7 +77,7 @@ pub fn render_markdown(text: &str, width: u16) -> Vec<Span<'static>> {
// We push the URL as a tooltip-like suffix
// After the link text ends, we'll add the URL
spans.push(Span::styled(
format!("]({})", dest_url),
format!("]({dest_url})"),
Style::default().fg(Theme::TEXT_MUTED).add_modifier(Modifier::ITALIC),
));
}
@@ -111,14 +109,7 @@ pub fn render_markdown(text: &str, width: u16) -> Vec<Span<'static>> {
first_in_paragraph = true;
spans.push(Span::raw("\n\n"));
}
pulldown_cmark::TagEnd::Emphasis => {}
pulldown_cmark::TagEnd::Strong => {}
pulldown_cmark::TagEnd::List(_) => {}
pulldown_cmark::TagEnd::Item => {
spans.push(Span::raw("\n"));
}
pulldown_cmark::TagEnd::Link => {}
pulldown_cmark::TagEnd::BlockQuote(_) => {
pulldown_cmark::TagEnd::Item | pulldown_cmark::TagEnd::BlockQuote(_) => {
spans.push(Span::raw("\n"));
}
_ => {}
@@ -128,7 +119,7 @@ pub fn render_markdown(text: &str, width: u16) -> Vec<Span<'static>> {
let s = text.to_string();
if in_code_block {
spans.push(Span::styled(
format!(" {}", s),
format!(" {s}"),
Style::default().fg(Theme::ACCENT_TEAL).bg(Theme::CODE_BG),
));
} else if in_heading {
@@ -138,14 +129,9 @@ pub fn render_markdown(text: &str, width: u16) -> Vec<Span<'static>> {
3 => Theme::ACCENT_PURPLE,
_ => Theme::TEXT,
};
let prefix = match heading_level {
1 => " ",
2 => " ",
3 => " ",
_ => " ",
};
let prefix = " ";
spans.push(Span::styled(
format!("{}{}", prefix, s),
format!("{prefix}{s}"),
Style::default().fg(color).add_modifier(Modifier::BOLD),
));
} else {
@@ -160,7 +146,7 @@ pub fn render_markdown(text: &str, width: u16) -> Vec<Span<'static>> {
pulldown_cmark::Event::Code(text) => {
// Inline code with background
spans.push(Span::styled(
format!(" {} ", text),
format!(" {text} "),
Style::default()
.fg(Theme::ACCENT_TEAL)
.bg(Theme::CODE_BAR)
@@ -195,10 +181,10 @@ pub fn render_markdown(text: &str, width: u16) -> Vec<Span<'static>> {
spans_out.push(Span::styled(text_str.to_string(), style));
if !text_str.contains('\n') {
line_len += remaining;
} else {
if text_str.contains('\n') {
line_len = text_str.split('\n').next_back().unwrap_or("").len();
} else {
line_len += remaining;
}
}
spans = spans_out;
+39 -39
View File
@@ -1,3 +1,4 @@
#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)]
//! Top-level TUI render pipeline: layouts the terminal into chat / input
//! / status regions, dispatches overlay rendering with glassmorphism-style
//! centered panels, and floats toast notifications over the top-right corner.
@@ -124,6 +125,7 @@ fn render_main_panel(
/// - A top accent border strip (colored per variant)
/// - A title line with icon
/// - Content area with proper spacing
#[allow(clippy::too_many_lines)]
fn render_overlay(
frame: &mut Frame,
area: Rect,
@@ -172,12 +174,12 @@ fn render_overlay(
)),
Line::from(Span::styled(
format!(" Max tokens: {}",
state.settings.max_tokens.map(|v| v.to_string()).unwrap_or_else(|| "auto".to_string())),
state.settings.max_tokens.map_or_else(|| "auto".to_string(), |v| v.to_string())),
Style::default().fg(Theme::TEXT),
)),
Line::from(Span::styled(
format!(" Temperature: {}",
state.settings.temperature.map(|v| format!("{:.1}", v)).unwrap_or_else(|| "auto".to_string())),
state.settings.temperature.map_or_else(|| "auto".to_string(), |v| format!("{v:.1}"))),
Style::default().fg(Theme::TEXT),
)),
Line::from(Span::styled(
@@ -261,11 +263,11 @@ fn render_overlay(
input_text.as_str()
}
};
let masked = if !input_text.is_empty() {
let suffix = if input_text.len() > 8 { "****" } else { "" };
format!("{}{}", display, suffix)
} else {
let masked = if input_text.is_empty() {
display.to_string()
} else {
let suffix = if input_text.len() > 8 { "****" } else { "" };
format!("{display}{suffix}")
};
let lines = vec![
Line::from(Span::styled(
@@ -329,9 +331,9 @@ fn render_overlay(
let selected = i == current_idx;
lines.push(Line::from(Span::styled(
if selected {
format!("{} (active)", l)
format!("{l} (active)")
} else {
format!(" {}", l)
format!(" {l}")
},
if selected {
Style::default().fg(Theme::HIGHLIGHT).add_modifier(Modifier::BOLD)
@@ -386,7 +388,7 @@ fn render_overlay(
)),
Line::from(Span::raw("")),
Line::from(Span::styled(
format!(" Messages: {}", msg_count),
format!(" Messages: {msg_count}"),
Style::default().fg(Theme::INFO),
)),
Line::from(Span::styled(
@@ -427,7 +429,7 @@ fn render_overlay(
};
let preview: String = msg.content.chars().take(70).collect();
lines.push(Line::from(Span::styled(
format!(" [{}] {}", role_str, preview),
format!(" [{role_str}] {preview}"),
Style::default().fg(
if matches!(msg.role, crate::dto::chat::message::Role::User) {
Theme::INFO
@@ -484,7 +486,7 @@ fn render_overlay(
let (label, style) = match item {
crate::app::mode::learning::LearningItem::Pending { name, .. } => {
(
format!("{}[Pending] {}", prefix, name),
format!("{prefix}[Pending] {name}"),
if is_selected {
Style::default().fg(Theme::WARNING).bg(Theme::HIGHLIGHT_DIM)
.add_modifier(Modifier::BOLD)
@@ -496,7 +498,7 @@ fn render_overlay(
crate::app::mode::learning::LearningItem::Stored { name, lifecycle, .. } => {
let status = if lifecycle == "stale" { "Stale" } else { "Active" };
(
format!("{}[{}] {}", prefix, status, name),
format!("{prefix}[{status}] {name}"),
if is_selected {
Style::default().fg(Theme::TEXT).bg(Theme::HIGHLIGHT_DIM)
.add_modifier(Modifier::BOLD)
@@ -539,7 +541,7 @@ fn render_overlay(
" Name:", Style::default().fg(Theme::TEXT_DIM),
)));
right_lines.push(Line::from(Span::styled(
format!(" {}", name),
format!(" {name}"),
Style::default().fg(Theme::TEXT).add_modifier(Modifier::BOLD),
)));
right_lines.push(Line::from(Span::raw("")));
@@ -548,11 +550,11 @@ fn render_overlay(
Style::default().fg(Theme::WARNING),
)));
right_lines.push(Line::from(Span::styled(
format!(" Scope: {}", scope),
format!(" Scope: {scope}"),
Style::default().fg(Theme::TEXT),
)));
right_lines.push(Line::from(Span::styled(
format!(" Confidence: {}", confidence),
format!(" Confidence: {confidence}"),
Style::default().fg(Theme::TEXT),
)));
right_lines.push(Line::from(Span::raw("")));
@@ -561,7 +563,7 @@ fn render_overlay(
)));
for line in content.lines() {
right_lines.push(Line::from(Span::styled(
format!(" {}", line),
format!(" {line}"),
Style::default().fg(Theme::TEXT),
)));
}
@@ -578,7 +580,7 @@ fn render_overlay(
" Name:", Style::default().fg(Theme::TEXT_DIM),
)));
right_lines.push(Line::from(Span::styled(
format!(" {}", name),
format!(" {name}"),
Style::default().fg(Theme::TEXT).add_modifier(Modifier::BOLD),
)));
right_lines.push(Line::from(Span::raw("")));
@@ -588,15 +590,15 @@ fn render_overlay(
Theme::SUCCESS
};
right_lines.push(Line::from(Span::styled(
format!(" Status: {}", lifecycle),
format!(" Status: {lifecycle}"),
Style::default().fg(status_color),
)));
right_lines.push(Line::from(Span::styled(
format!(" Scope: {}", scope),
format!(" Scope: {scope}"),
Style::default().fg(Theme::TEXT),
)));
right_lines.push(Line::from(Span::styled(
format!(" Description: {}", description),
format!(" Description: {description}"),
Style::default().fg(Theme::TEXT),
)));
right_lines.push(Line::from(Span::raw("")));
@@ -605,7 +607,7 @@ fn render_overlay(
)));
for line in content.lines() {
right_lines.push(Line::from(Span::styled(
format!(" {}", line),
format!(" {line}"),
Style::default().fg(Theme::TEXT),
)));
}
@@ -635,7 +637,7 @@ fn render_overlay(
.border_style(Style::default().fg(Theme::INFO));
let runtime = state.session_runtime.as_ref();
let (tokens_in, tokens_out, api_calls, review_tokens, session_start) = runtime
.map(|r| {
.map_or((0, 0, 0, 0, 0), |r| {
(
r.usage.tokens_in,
r.usage.tokens_out,
@@ -643,18 +645,16 @@ fn render_overlay(
r.usage.review_tokens,
r.session_start,
)
})
.unwrap_or((0, 0, 0, 0, 0));
});
let (edit_count, lesson_count, review_count, consec_empty) = runtime
.map(|r| {
.map_or((0, 0, 0, 0), |r| {
(
r.edit_count,
r.lesson_count,
r.review_count,
r.consecutive_empty_reviews,
)
})
.unwrap_or((0, 0, 0, 0));
});
let elapsed_ms = chrono::Utc::now().timestamp_millis().saturating_sub(session_start);
let hours = elapsed_ms / 3_600_000;
let minutes = (elapsed_ms % 3_600_000) / 60_000;
@@ -669,19 +669,19 @@ fn render_overlay(
)),
Line::from(Span::raw("")),
Line::from(Span::styled(
format!(" Main agent: {} tokens", main_tokens),
format!(" Main agent: {main_tokens} tokens"),
Style::default().fg(Theme::TEXT),
)),
Line::from(Span::styled(
format!(" Self-learning: {} tokens", self_learning_total),
format!(" Self-learning: {self_learning_total} tokens"),
Style::default().fg(Theme::TEXT_MUTED),
)),
Line::from(Span::styled(
format!(" Total: {} tokens", total_tokens),
format!(" Total: {total_tokens} tokens"),
Style::default().fg(Theme::TEXT).add_modifier(Modifier::BOLD),
)),
Line::from(Span::styled(
format!(" API calls: {}", api_calls),
format!(" API calls: {api_calls}"),
Style::default().fg(Theme::TEXT),
)),
Line::from(Span::raw("")),
@@ -690,21 +690,21 @@ fn render_overlay(
Style::default().fg(Theme::INFO).add_modifier(Modifier::BOLD),
)),
Line::from(Span::styled(
format!(" Edits: {}", edit_count),
format!(" Edits: {edit_count}"),
Style::default().fg(Theme::TEXT),
)),
Line::from(Span::styled(
format!(" Reviews: {}", review_count),
format!(" Reviews: {review_count}"),
Style::default().fg(Theme::TEXT),
)),
Line::from(Span::styled(
format!(" Lessons: {}", lesson_count),
format!(" Lessons: {lesson_count}"),
Style::default().fg(Theme::TEXT_MUTED),
)),
Line::from(Span::styled(
format!(" Empty reviews: {}",
if consec_empty > 3 {
format!("{}", consec_empty)
format!("{consec_empty}")
} else {
consec_empty.to_string()
},
@@ -713,7 +713,7 @@ fn render_overlay(
)),
Line::from(Span::raw("")),
Line::from(Span::styled(
format!(" Session: {}h {}m {}s", hours, minutes, seconds),
format!(" Session: {hours}h {minutes}m {seconds}s"),
Style::default().fg(Theme::TEXT_DIM),
)),
];
@@ -757,7 +757,7 @@ fn render_overlay(
let is_selected = i == state.misc.selected_index;
let prefix = if is_selected { "" } else { " " };
let model_str = cfg.default_model.as_deref().unwrap_or("(any)");
let label = format!("{}{} ({})", prefix, name, model_str);
let label = format!("{prefix}{name} ({model_str})");
let style = if is_current {
Style::default().fg(Theme::HIGHLIGHT).add_modifier(Modifier::BOLD)
} else if is_selected {
@@ -842,7 +842,7 @@ fn render_input_bar(
} else {
Style::default().fg(Theme::TEXT)
};
let label = format!("{}{}", prefix, candidate);
let label = format!("{prefix}{candidate}");
lines.push(Line::from(Span::styled(label, style)));
}
let dropdown = Paragraph::new(lines).block(dropdown_block);
@@ -901,7 +901,7 @@ fn render_input_bar(
// ────────────────────────────────────────────────────────────────────────────
/// Render active toasts as a floating stack at top-right of the terminal.
/// Each toast auto-expires after its lifetime_ms. Max 4 visible at once.
/// Each toast auto-expires after its `lifetime_ms`. Max 4 visible at once.
///
/// Toasts are stacked vertically with a 1-line gap. Each has a colored
/// left border and a subtle background.
+8 -7
View File
@@ -1,3 +1,4 @@
#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)]
//! Status bar rendering for the TUI — modern segmented bar design.
//!
//! Flow: `draw_status_bar` reads live connection/turn state off
@@ -20,14 +21,15 @@ use super::theme::Theme;
/// Layout (left-to-right, space-filling):
/// LEFT: [zesdex] + status indicator (READY/PROG/NOAPI)
/// CENTER: spinner + optional contextual info
/// RIGHT: provider · model · ↑tokens_in ↓tokens_out
/// RIGHT: provider · model · ↑`tokens_in``tokens_out`
pub fn draw_status_bar(frame: &mut Frame, area: Rect, state: &crate::app::state::rest::AppStateRest) {
use ratatui::layout::{Constraint, Direction, Layout};
let spinner_frames = ["", "", "", "", "", "", "", "", "", ""];
// ── Agent status badge ────────────────────────────────────────────────
let (status_text, badge_bg, status_fg) = if state.turn_in_flight() {
let f = spinner_frames[(state.misc.tick_count as usize / 2) % spinner_frames.len()];
(format!(" {} PROG ", f), Theme::MODE_YOLO, Theme::BG)
(format!(" {f} PROG "), Theme::MODE_YOLO, Theme::BG)
} else if state.misc.api_connected {
(" READY ".to_string(), Theme::MODE_AUTO, Theme::BG)
} else {
@@ -61,7 +63,7 @@ pub fn draw_status_bar(frame: &mut Frame, area: Rect, state: &crate::app::state:
let total_chars: usize = rt.messages.iter()
.filter_map(|m| m.content.as_deref())
.map(|c| c.len())
.map(str::len)
.sum();
let current_tokens = total_chars / 4;
@@ -69,8 +71,8 @@ pub fn draw_status_bar(frame: &mut Frame, area: Rect, state: &crate::app::state:
if rt.usage.last_tokens_in > 0 || rt.usage.last_tokens_out > 0 {
parts.push(format!("{}{}", rt.usage.last_tokens_in, rt.usage.last_tokens_out));
}
let max_str = max_tokens.map(|v| v.to_string()).unwrap_or_else(|| "?".to_string());
parts.push(format!("{}/{}", current_tokens, max_str));
let max_str = max_tokens.map_or_else(|| "?".to_string(), |v| v.to_string());
parts.push(format!("{current_tokens}/{max_str}"));
parts.push(state.settings.provider.clone());
parts.push(state.settings.model.clone());
@@ -79,7 +81,7 @@ pub fn draw_status_bar(frame: &mut Frame, area: Rect, state: &crate::app::state:
let max_tokens = state.app_config.model_roles.values()
.find(|role| role.provider == state.settings.provider && role.model == state.settings.model)
.and_then(|role| role.context_window);
let max_str = max_tokens.map(|v| v.to_string()).unwrap_or_else(|| "?".to_string());
let max_str = max_tokens.map_or_else(|| "?".to_string(), |v| v.to_string());
format!(" 0/{} · {} · {} ", max_str, state.settings.provider, state.settings.model)
};
@@ -92,7 +94,6 @@ pub fn draw_status_bar(frame: &mut Frame, area: Rect, state: &crate::app::state:
));
// Render the bar using two columns
use ratatui::layout::{Constraint, Direction, Layout};
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
+6 -5
View File
@@ -55,6 +55,7 @@ fn is_company_pipeline(agents: &[crate::app::workflow::engine::WorkflowAgent]) -
}
/// Render the workflow status panel.
#[allow(clippy::too_many_lines)]
pub fn draw_workflow_panel(frame: &mut Frame, area: Rect, state: &crate::app::state::rest::AppStateRest) {
use ratatui::layout::{Constraint, Direction, Layout};
@@ -179,7 +180,7 @@ pub fn draw_workflow_panel(frame: &mut Frame, area: Rect, state: &crate::app::st
// Agent card header
card_lines.push(Line::from(vec![
Span::styled(
format!(" {} ", icon),
format!(" {icon} "),
Style::default().fg(color).add_modifier(Modifier::BOLD),
),
Span::styled(
@@ -187,7 +188,7 @@ pub fn draw_workflow_panel(frame: &mut Frame, area: Rect, state: &crate::app::st
Style::default().fg(Theme::TEXT).add_modifier(Modifier::BOLD),
),
Span::styled(
format!(" [{}]", label),
format!(" [{label}]"),
Style::default().fg(color),
),
Span::styled(
@@ -240,18 +241,18 @@ fn build_session_lines(state: &crate::app::state::rest::AppStateRest) -> Vec<Lin
]));
lines.push(Line::from(vec![
Span::styled(" Tool calls", Style::default().fg(Theme::TEXT_DIM)),
Span::styled(format!(" {}", tool_count), Style::default().fg(Theme::SUCCESS)),
Span::styled(format!(" {tool_count}"), Style::default().fg(Theme::SUCCESS)),
]));
if pending > 0 {
lines.push(Line::from(vec![
Span::styled(" Pending ", Style::default().fg(Theme::TEXT_DIM)),
Span::styled(format!(" {}", pending), Style::default().fg(Theme::WARNING)),
Span::styled(format!(" {pending}"), Style::default().fg(Theme::WARNING)),
]));
}
if bash_count > 0 {
lines.push(Line::from(vec![
Span::styled(" Bash jobs ", Style::default().fg(Theme::TEXT_DIM)),
Span::styled(format!(" {}", bash_count), Style::default().fg(Theme::WARNING)),
Span::styled(format!(" {bash_count}"), Style::default().fg(Theme::WARNING)),
]));
}
} else {