diff --git a/CHANGELOG.md b/CHANGELOG.md index 32015b0b..2a832f43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.3.13] - 2026-02-04 + +### Fixed +- Restore an in-app scrollbar for the transcript view + ## [0.3.12] - 2026-02-04 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index dd39fefa..4f66a4aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -674,7 +674,7 @@ dependencies = [ [[package]] name = "deepseek-tui" -version = "0.3.12" +version = "0.3.13" dependencies = [ "anyhow", "arboard", diff --git a/Cargo.toml b/Cargo.toml index 75a8425f..9f6e0775 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deepseek-tui" -version = "0.3.12" +version = "0.3.13" edition = "2024" description = "Unofficial DeepSeek CLI - Just run 'deepseek' to start chatting" license = "MIT" diff --git a/src/tui/widgets/mod.rs b/src/tui/widgets/mod.rs index bfadbe50..542f6ef5 100644 --- a/src/tui/widgets/mod.rs +++ b/src/tui/widgets/mod.rs @@ -21,14 +21,19 @@ use unicode_width::UnicodeWidthStr; pub struct ChatWidget { content_area: Rect, + scrollbar_area: Option, lines: Vec>, + total_lines: usize, + visible_lines: usize, + top: usize, } impl ChatWidget { pub fn new(app: &mut App, area: Rect) -> Self { - let content_area = area; - + let mut content_area = area; + let visible_lines = content_area.height as usize; let render_options = app.transcript_render_options(); + app.transcript_cache.ensure( &app.history, content_area.width.max(1), @@ -36,8 +41,26 @@ impl ChatWidget { render_options, ); - let total_lines = app.transcript_cache.total_lines(); - let visible_lines = content_area.height as usize; + let mut total_lines = app.transcript_cache.total_lines(); + let mut scrollbar_area = None; + + if total_lines > visible_lines && content_area.width > 1 { + scrollbar_area = Some(Rect { + x: content_area.x + content_area.width.saturating_sub(1), + y: content_area.y, + width: 1, + height: content_area.height, + }); + content_area.width = content_area.width.saturating_sub(1).max(1); + app.transcript_cache.ensure( + &app.history, + content_area.width.max(1), + app.history_version, + render_options, + ); + total_lines = app.transcript_cache.total_lines(); + } + let line_meta = app.transcript_cache.line_meta(); if app.pending_scroll_delta != 0 { @@ -75,7 +98,11 @@ impl ChatWidget { Self { content_area, + scrollbar_area, lines, + total_lines, + visible_lines, + top, } } } @@ -84,6 +111,10 @@ impl Renderable for ChatWidget { fn render(&self, _area: Rect, buf: &mut Buffer) { let paragraph = Paragraph::new(self.lines.clone()); paragraph.render(self.content_area, buf); + + if let Some(area) = self.scrollbar_area { + render_scrollbar(buf, area, self.total_lines, self.visible_lines, self.top); + } } fn desired_height(&self, _width: u16) -> u16 { @@ -91,6 +122,53 @@ impl Renderable for ChatWidget { } } +fn render_scrollbar( + buf: &mut Buffer, + area: Rect, + total_lines: usize, + visible_lines: usize, + top: usize, +) { + if area.width == 0 || area.height == 0 || total_lines == 0 { + return; + } + + let height = area.height as usize; + let track_style = Style::default().fg(palette::TEXT_DIM); + let thumb_style = Style::default().fg(palette::DEEPSEEK_SKY); + + for row in 0..height { + if let Some(cell) = buf.cell_mut((area.x, area.y + row as u16)) { + cell.set_symbol("│").set_style(track_style); + } + } + + if total_lines <= visible_lines { + return; + } + + let thumb_height = ((visible_lines as f32 / total_lines as f32) * height as f32) + .ceil() + .clamp(1.0, height as f32) as usize; + let max_top = total_lines.saturating_sub(visible_lines); + let thumb_top = if max_top == 0 { + 0 + } else { + let available = height.saturating_sub(thumb_height); + let ratio = top as f32 / max_top as f32; + (ratio * available as f32).round() as usize + }; + + for row in thumb_top..thumb_top.saturating_add(thumb_height) { + if row >= height { + break; + } + if let Some(cell) = buf.cell_mut((area.x, area.y + row as u16)) { + cell.set_symbol("█").set_style(thumb_style); + } + } +} + pub struct ComposerWidget<'a> { app: &'a App, prompt: &'a str,