feat(tui): add drag-to-resize sidebar width#2604
Conversation
|
Thanks @idling11 for taking the time to contribute. This repository is currently observing a maintainer-managed contribution gate in dry-run mode, so this pull request is staying open. When enforcement is enabled, pull requests from contributors who are not listed in Please read |
feat(tui): add drag-to-resize sidebar widthThere was a problem hiding this comment.
Code Review
This pull request implements a draggable resize handle for the TUI sidebar, allowing users to dynamically adjust and persist the sidebar width percentage. The review feedback highlights a potential panic from direct buffer indexing, a layout logic issue where a hardcoded minimum width constraint conflicts with the percentage limits, and performance concerns regarding synchronous file I/O blocking the main async event loop.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| let buf = f.buffer_mut(); | ||
| for row in handle_rect.y..handle_rect.y.saturating_add(handle_rect.height) { | ||
| if row < buf.area().height { | ||
| buf[(handle_rect.x, row)] | ||
| .set_char('│') | ||
| .set_style(handle_style); | ||
| } | ||
| } |
There was a problem hiding this comment.
Using direct indexing buf[(handle_rect.x, row)] can panic if handle_rect.x is out of bounds (e.g., during rapid terminal resizing or layout miscalculations). Using buf.get_mut(handle_rect.x, row) is safer and more idiomatic, as it returns an Option<&mut Cell> and avoids any potential panics.
let buf = f.buffer_mut();
for row in handle_rect.y..handle_rect.y.saturating_add(handle_rect.height) {
if let Some(cell) = buf.get_mut(handle_rect.x, row) {
cell.set_char('│')
.set_style(handle_style);
}
}| } | ||
| MouseEventKind::Drag(MouseButton::Left) if app.sidebar_resizing => { | ||
| let delta = app.sidebar_resize_anchor_x as i32 - mouse.column as i32; | ||
| let new_width = (app.sidebar_resize_anchor_width as i32 + delta).max(24) as u16; |
There was a problem hiding this comment.
The hardcoded .max(24) constraint on new_width prevents the sidebar from being resized down to the minimum 10% on standard terminal widths (e.g., on a 120-column screen, 24 columns is 20%, so the user can never reach 10%). Additionally, if the sidebar was previously smaller than 24 columns, starting a drag will cause a sudden jump to 24 columns. Consider using .max(0) or .max(1) and letting the percentage clamp (10% to 50%) handle the minimum width constraint naturally.
| let new_width = (app.sidebar_resize_anchor_width as i32 + delta).max(24) as u16; | |
| let new_width = (app.sidebar_resize_anchor_width as i32 + delta).max(0) as u16; |
| // Persist sidebar width when the user finishes a drag-to-resize. | ||
| if app.sidebar_width_dirty { | ||
| app.sidebar_width_dirty = false; | ||
| if let Ok(mut settings) = Settings::load() { | ||
| settings.update_sidebar_width(app.sidebar_width_percent); | ||
| let _ = settings.save(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Performing synchronous file I/O (Settings::load() and settings.save()) directly on the main async event loop thread can block the executor and cause noticeable UI stuttering or lag when the user finishes dragging. Consider offloading this blocking operation to a background thread using tokio::task::spawn_blocking.
// Persist sidebar width when the user finishes a drag-to-resize.
if app.sidebar_width_dirty {
app.sidebar_width_dirty = false;
let percent = app.sidebar_width_percent;
tokio::task::spawn_blocking(move || {
if let Ok(mut settings) = Settings::load() {
settings.update_sidebar_width(percent);
let _ = settings.save();
}
});
}| if app.sidebar_width_dirty { | ||
| app.sidebar_width_dirty = false; | ||
| if let Ok(mut settings) = Settings::load() { | ||
| settings.update_sidebar_width(app.sidebar_width_percent); | ||
| let _ = settings.save(); | ||
| } | ||
| } | ||
| continue; |
There was a problem hiding this comment.
Performing synchronous file I/O (Settings::load() and settings.save()) directly on the main async event loop thread can block the executor and cause noticeable UI stuttering or lag when the user finishes dragging. Consider offloading this blocking operation to a background thread using tokio::task::spawn_blocking.
// Persist sidebar width when the user finishes a drag-to-resize.
if app.sidebar_width_dirty {
app.sidebar_width_dirty = false;
let percent = app.sidebar_width_percent;
tokio::task::spawn_blocking(move || {
if let Ok(mut settings) = Settings::load() {
settings.update_sidebar_width(percent);
let _ = settings.save();
}
});
}- **What:** Add interactive mouse-drag resize support between the chat area and sidebar.
Previously sidebar width could only be adjusted via `/config sidebar_width <percent>`, requiring a typed percentage and guesswork.
- **How:**
- Render a **1-column draggable handle** (`│`) on the left edge of the sidebar with three visual states:
- **default** — dim slate background
- **hover** — orange (warning) background
- **drag** — blue (accent) background
- On `MouseDown` on the handle → record anchor position (`sidebar_resize_anchor_x`, `sidebar_resize_anchor_width`).
- On `Drag` → compute delta, convert to percentage (10–50% range), update `sidebar_width_percent` in real time.
- On `MouseUp` → persist the new width to `~/.deepseek/settings.toml` via `Settings::save()`.
- Store `last_sidebar_area` / `last_sidebar_handle_area` / `sidebar_resize_total_width` for mouse hit-testing and percentage calculation.
- **Files changed:**
| File | Change |
|------|--------|
| `crates/tui/src/tui/app.rs` | Add 7 state fields for resize drag |
| `crates/tui/src/tui/ui.rs` | Render handle, store layout info, persist on drag-end |
| `crates/tui/src/tui/mouse_ui.rs` | `handle_sidebar_resize_mouse` — Down/Drag/Up logic |
| `crates/tui/src/settings.rs` | `update_sidebar_width()` helper |
| `crates/tui/src/tui/ui/tests.rs` | 5 unit tests |
- **Tests:** 5 new tests covering handle click, drag percentage calculation, 10–50% clamp, mouse-up finalization, and non-handle click.
- **Verification:** `cargo fmt --all` ✅ | `cargo check` ✅ | `cargo test -p codewhale-tui -- ui::tests` (337 tests) ✅
Closes Hmbown#2602
| fn handle_sidebar_resize_mouse(app: &mut App, mouse: MouseEvent) -> bool { | ||
| let Some(handle) = app.last_sidebar_handle_area else { | ||
| return false; | ||
| }; | ||
|
|
||
| let hit = mouse.column == handle.x | ||
| && mouse.row >= handle.y | ||
| && mouse.row < handle.y.saturating_add(handle.height); | ||
|
|
||
| match mouse.kind { | ||
| MouseEventKind::Down(MouseButton::Left) if hit => { | ||
| app.sidebar_resizing = true; | ||
| app.sidebar_resize_anchor_x = mouse.column; | ||
| app.sidebar_resize_anchor_width = app.last_sidebar_area.map(|a| a.width).unwrap_or(28); | ||
| app.needs_redraw = true; | ||
| true | ||
| } | ||
| MouseEventKind::Drag(MouseButton::Left) if app.sidebar_resizing => { | ||
| let delta = app.sidebar_resize_anchor_x as i32 - mouse.column as i32; | ||
| let new_width = (app.sidebar_resize_anchor_width as i32 + delta).max(24) as u16; | ||
| let total = app.sidebar_resize_total_width.max(1); | ||
| let new_pct = ((new_width as u32 * 100) / total as u32).clamp(10, 50) as u16; | ||
| if new_pct != app.sidebar_width_percent { | ||
| app.sidebar_width_percent = new_pct; | ||
| app.needs_redraw = true; | ||
| } | ||
| true | ||
| } | ||
| MouseEventKind::Up(MouseButton::Left) if app.sidebar_resizing => { | ||
| app.sidebar_resizing = false; | ||
| app.sidebar_width_dirty = true; | ||
| app.needs_redraw = true; | ||
| true | ||
| } | ||
| _ => false, | ||
| } | ||
| } |
There was a problem hiding this comment.
Stale handle rect when sidebar is hidden
last_sidebar_handle_area and last_sidebar_area are only written inside if let Some(sidebar_area) = sidebar_area { … } in the render function, so they are never cleared when sidebar_focus == SidebarFocus::Hidden or when the terminal shrinks below SIDEBAR_VISIBLE_MIN_WIDTH. A click at the coordinates that the handle occupied before it disappeared will still pass the hit-test here and begin a resize drag against an invisible sidebar, silently mutating sidebar_width_percent with no visual feedback.
Fix: clear both fields (and abort any active drag) when the sidebar is not rendered:
// in the render fn, in the else branch where sidebar is absent
app.last_sidebar_handle_area = None;
app.last_sidebar_area = None;
app.sidebar_resizing = false;Then the early-return let Some(handle) = app.last_sidebar_handle_area else { return false; } will correctly bail out.
| // Persist sidebar width when the user finishes a drag-to-resize. | ||
| if app.sidebar_width_dirty { | ||
| app.sidebar_width_dirty = false; | ||
| if let Ok(mut settings) = Settings::load() { | ||
| settings.update_sidebar_width(app.sidebar_width_percent); | ||
| let _ = settings.save(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Dead persistence check in the key-event branch
sidebar_width_dirty is only ever set to true inside handle_sidebar_resize_mouse on a MouseUp event. When a mouse-up fires, the first copy of this block (around line 2602) clears the flag and saves before continue-ing to the next iteration. By the time a key event is processed (this branch), the flag is always false. This block is unreachable in practice and just adds noise; it can be removed.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
564a0ae to
adf3fe2
Compare
The sidebar Work panel reads checklist data from `app.todos` (`tokio::sync::Mutex`) via non-blocking `try_lock()`. When the engine holds the lock (executing `checklist_write` in a tokio task), the panel silently resets to "Work state updating..." and discards all prior data. The Tasks panel does not suffer from this because it reads plain App fields (no lock needed). Fix: cache the last successful `SidebarWorkSummary` in `App` and return it when `try_lock()` fails. Live hunt/goal fields (quarry, verdict, cycle count, token usage) are still refreshed from the current App state on every frame, even during cache fallback. - Make `SidebarWorkSummary`, `SidebarWorkChecklistItem`, `SidebarWorkStrategyStep` pub(crate) so App can store them. - Add `cached_work_summary: Option<SidebarWorkSummary>` to App. - Rewrite `sidebar_work_summary(&mut App)` to use a fallback chain: fresh locks → cache → state_updating empty state. - Add 4 unit tests covering cache populate, lock-busy fallback, no-cache+lock fallback, and live-field refresh during fallback. Closes Hmbown#2604
The sidebar Work panel reads checklist data from `app.todos` (`tokio::sync::Mutex`) via non-blocking `try_lock()`. When the engine holds the lock (executing `checklist_write` in a tokio task), the panel silently resets to "Work state updating..." and discards all prior data. The Tasks panel does not suffer from this because it reads plain App fields (no lock needed). Fix: cache the last successful `SidebarWorkSummary` in `App` and return it when `try_lock()` fails. Live hunt/goal fields (quarry, verdict, cycle count, token usage) are still refreshed from the current App state on every frame, even during cache fallback. - Make `SidebarWorkSummary`, `SidebarWorkChecklistItem`, `SidebarWorkStrategyStep` pub(crate) so App can store them. - Add `cached_work_summary: Option<SidebarWorkSummary>` to App. - Rewrite `sidebar_work_summary(&mut App)` to use a fallback chain: fresh locks → cache → state_updating empty state. - Add 4 unit tests covering cache populate, lock-busy fallback, no-cache+lock fallback, and live-field refresh during fallback. Closes Hmbown#2604
The sidebar Work panel reads checklist data from `app.todos` (`tokio::sync::Mutex`) via non-blocking `try_lock()`. When the engine holds the lock (executing `checklist_write` in a tokio task), the panel silently resets to "Work state updating..." and discards all prior data. The Tasks panel does not suffer from this because it reads plain App fields (no lock needed). Fix: cache the last successful `SidebarWorkSummary` in `App` and return it when `try_lock()` fails. Live hunt/goal fields (quarry, verdict, cycle count, token usage) are still refreshed from the current App state on every frame, even during cache fallback. - Make `SidebarWorkSummary`, `SidebarWorkChecklistItem`, `SidebarWorkStrategyStep` pub(crate) so App can store them. - Add `cached_work_summary: Option<SidebarWorkSummary>` to App. - Rewrite `sidebar_work_summary(&mut App)` to use a fallback chain: fresh locks → cache → state_updating empty state. - Add 4 unit tests covering cache populate, lock-busy fallback, no-cache+lock fallback, and live-field refresh during fallback. Closes Hmbown#2604
The sidebar Work panel reads checklist data from `app.todos` (`tokio::sync::Mutex`) via non-blocking `try_lock()`. When the engine holds the lock (executing `checklist_write` in a tokio task), the panel silently resets to "Work state updating..." and discards all prior data. The Tasks panel does not suffer from this because it reads plain App fields (no lock needed). Fix: cache the last successful `SidebarWorkSummary` in `App` and return it when `try_lock()` fails. Live hunt/goal fields (quarry, verdict, cycle count, token usage) are still refreshed from the current App state on every frame, even during cache fallback. - Make `SidebarWorkSummary`, `SidebarWorkChecklistItem`, `SidebarWorkStrategyStep` pub(crate) so App can store them. - Add `cached_work_summary: Option<SidebarWorkSummary>` to App. - Rewrite `sidebar_work_summary(&mut App)` to use a fallback chain: fresh locks → cache → state_updating empty state. - Add 4 unit tests covering cache populate, lock-busy fallback, no-cache+lock fallback, and live-field refresh during fallback. Closes Hmbown#2604
The sidebar Work panel reads checklist data from `app.todos` (`tokio::sync::Mutex`) via non-blocking `try_lock()`. When the engine holds the lock (executing `checklist_write` in a tokio task), the panel silently resets to "Work state updating..." and discards all prior data. The Tasks panel does not suffer from this because it reads plain App fields (no lock needed). Fix: cache the last successful `SidebarWorkSummary` in `App` and return it when `try_lock()` fails. Live hunt/goal fields (quarry, verdict, cycle count, token usage) are still refreshed from the current App state on every frame, even during cache fallback. - Make `SidebarWorkSummary`, `SidebarWorkChecklistItem`, `SidebarWorkStrategyStep` pub(crate) so App can store them. - Add `cached_work_summary: Option<SidebarWorkSummary>` to App. - Rewrite `sidebar_work_summary(&mut App)` to use a fallback chain: fresh locks → cache → state_updating empty state. - Add 4 unit tests covering cache populate, lock-busy fallback, no-cache+lock fallback, and live-field refresh during fallback. Closes Hmbown#2604
The sidebar Work panel reads checklist data from `app.todos` (`tokio::sync::Mutex`) via non-blocking `try_lock()`. When the engine holds the lock (executing `checklist_write` in a tokio task), the panel silently resets to "Work state updating..." and discards all prior data. The Tasks panel does not suffer from this because it reads plain App fields (no lock needed). Fix: cache the last successful `SidebarWorkSummary` in `App` and return it when `try_lock()` fails. Live hunt/goal fields (quarry, verdict, cycle count, token usage) are still refreshed from the current App state on every frame, even during cache fallback. - Make `SidebarWorkSummary`, `SidebarWorkChecklistItem`, `SidebarWorkStrategyStep` pub(crate) so App can store them. - Add `cached_work_summary: Option<SidebarWorkSummary>` to App. - Rewrite `sidebar_work_summary(&mut App)` to use a fallback chain: fresh locks → cache → state_updating empty state. - Add 4 unit tests covering cache populate, lock-busy fallback, no-cache+lock fallback, and live-field refresh during fallback. Closes Hmbown#2604
The sidebar Work panel reads checklist data from `app.todos` (`tokio::sync::Mutex`) via non-blocking `try_lock()`. When the engine holds the lock (executing `checklist_write` in a tokio task), the panel silently resets to "Work state updating..." and discards all prior data. The Tasks panel does not suffer from this because it reads plain App fields (no lock needed). Fix: cache the last successful `SidebarWorkSummary` in `App` and return it when `try_lock()` fails. Live hunt/goal fields (quarry, verdict, cycle count, token usage) are still refreshed from the current App state on every frame, even during cache fallback. - Make `SidebarWorkSummary`, `SidebarWorkChecklistItem`, `SidebarWorkStrategyStep` pub(crate) so App can store them. - Add `cached_work_summary: Option<SidebarWorkSummary>` to App. - Rewrite `sidebar_work_summary(&mut App)` to use a fallback chain: fresh locks → cache → state_updating empty state. - Add 4 unit tests covering cache populate, lock-busy fallback, no-cache+lock fallback, and live-field refresh during fallback. Closes Hmbown#2604
The sidebar Work panel reads checklist data from `app.todos` (`tokio::sync::Mutex`) via non-blocking `try_lock()`. When the engine holds the lock (executing `checklist_write` in a tokio task), the panel silently resets to "Work state updating..." and discards all prior data. The Tasks panel does not suffer from this because it reads plain App fields (no lock needed). Fix: cache the last successful `SidebarWorkSummary` in `App` and return it when `try_lock()` fails. Live hunt/goal fields (quarry, verdict, cycle count, token usage) are still refreshed from the current App state on every frame, even during cache fallback. - Make `SidebarWorkSummary`, `SidebarWorkChecklistItem`, `SidebarWorkStrategyStep` pub(crate) so App can store them. - Add `cached_work_summary: Option<SidebarWorkSummary>` to App. - Rewrite `sidebar_work_summary(&mut App)` to use a fallback chain: fresh locks → cache → state_updating empty state. - Add 4 unit tests covering cache populate, lock-busy fallback, no-cache+lock fallback, and live-field refresh during fallback. Closes Hmbown#2604
The sidebar Work panel reads checklist data from `app.todos` (`tokio::sync::Mutex`) via non-blocking `try_lock()`. When the engine holds the lock (executing `checklist_write` in a tokio task), the panel silently resets to "Work state updating..." and discards all prior data. The Tasks panel does not suffer from this because it reads plain App fields (no lock needed). Fix: cache the last successful `SidebarWorkSummary` in `App` and return it when `try_lock()` fails. Live hunt/goal fields (quarry, verdict, cycle count, token usage) are still refreshed from the current App state on every frame, even during cache fallback. - Make `SidebarWorkSummary`, `SidebarWorkChecklistItem`, `SidebarWorkStrategyStep` pub(crate) so App can store them. - Add `cached_work_summary: Option<SidebarWorkSummary>` to App. - Rewrite `sidebar_work_summary(&mut App)` to use a fallback chain: fresh locks → cache → state_updating empty state. - Add 4 unit tests covering cache populate, lock-busy fallback, no-cache+lock fallback, and live-field refresh during fallback. Closes Hmbown#2604
The sidebar Work panel reads checklist data from `app.todos` (`tokio::sync::Mutex`) via non-blocking `try_lock()`. When the engine holds the lock (executing `checklist_write` in a tokio task), the panel silently resets to "Work state updating..." and discards all prior data. The Tasks panel does not suffer from this because it reads plain App fields (no lock needed). Fix: cache the last successful `SidebarWorkSummary` in `App` and return it when `try_lock()` fails. Live hunt/goal fields (quarry, verdict, cycle count, token usage) are still refreshed from the current App state on every frame, even during cache fallback. - Make `SidebarWorkSummary`, `SidebarWorkChecklistItem`, `SidebarWorkStrategyStep` pub(crate) so App can store them. - Add `cached_work_summary: Option<SidebarWorkSummary>` to App. - Rewrite `sidebar_work_summary(&mut App)` to use a fallback chain: fresh locks → cache → state_updating empty state. - Add 4 unit tests covering cache populate, lock-busy fallback, no-cache+lock fallback, and live-field refresh during fallback. Closes Hmbown#2604
The sidebar Work panel reads checklist data from `app.todos` (`tokio::sync::Mutex`) via non-blocking `try_lock()`. When the engine holds the lock (executing `checklist_write` in a tokio task), the panel silently resets to "Work state updating..." and discards all prior data. The Tasks panel does not suffer from this because it reads plain App fields (no lock needed). Fix: cache the last successful `SidebarWorkSummary` in `App` and return it when `try_lock()` fails. Live hunt/goal fields (quarry, verdict, cycle count, token usage) are still refreshed from the current App state on every frame, even during cache fallback. - Make `SidebarWorkSummary`, `SidebarWorkChecklistItem`, `SidebarWorkStrategyStep` pub(crate) so App can store them. - Add `cached_work_summary: Option<SidebarWorkSummary>` to App. - Rewrite `sidebar_work_summary(&mut App)` to use a fallback chain: fresh locks → cache → state_updating empty state. - Add 4 unit tests covering cache populate, lock-busy fallback, no-cache+lock fallback, and live-field refresh during fallback. Closes Hmbown#2604
What: Add interactive mouse-drag resize support between the chat area and sidebar.
Previously sidebar width could only be adjusted via
/config sidebar_width <percent>, requiring a typed percentage and guesswork.How:
│) on the left edge of the sidebar with three visual states:MouseDownon the handle → record anchor position (sidebar_resize_anchor_x,sidebar_resize_anchor_width).Drag→ compute delta, convert to percentage (10–50% range), updatesidebar_width_percentin real time.MouseUp→ persist the new width to~/.deepseek/settings.tomlviaSettings::save().last_sidebar_area/last_sidebar_handle_area/sidebar_resize_total_widthfor mouse hit-testing and percentage calculation.Files changed:
crates/tui/src/tui/app.rscrates/tui/src/tui/ui.rscrates/tui/src/tui/mouse_ui.rshandle_sidebar_resize_mouse— Down/Drag/Up logiccrates/tui/src/settings.rsupdate_sidebar_width()helpercrates/tui/src/tui/ui/tests.rsTests: 5 new tests covering handle click, drag percentage calculation, 10–50% clamp, mouse-up finalization, and non-handle click.
Verification:
cargo fmt --all✅ |cargo check✅ |cargo test -p codewhale-tui -- ui::tests(337 tests) ✅Closes #2602
Greptile Summary
This PR adds interactive mouse-drag resizing of the TUI sidebar via a 1-column
│handle rendered on the sidebar's left edge, replacing the previous keyboard-only/config sidebar_widthworkflow. The drag state is tracked inApp, hit-tested inmouse_ui.rs, and persisted to disk onMouseUpusing the establishedSettings::load()+save()pattern.ui.rs): the handle is painted after the sidebar render pass with three visual states (default/hover/drag); layout rects are stored onAppeach frame for hit-testing.mouse_ui.rs):handle_sidebar_resize_mousemanages Down/Drag/Up events; drag firessidebar_resizing = trueand updatessidebar_width_percentin real time, withclamp(10, 50)on the percentage.ui.rs+settings.rs):sidebar_width_dirtyis set onMouseUp; the event loop checks the flag and callsSettings::load()→update_sidebar_width→save().Confidence Score: 4/5
Safe to merge with one fix: the stuck-resize bug when a modal opens during a drag should be addressed before shipping.
The drag-resize logic, percentage calculation, and persistence path are all correct and well-tested. The one gap is that
sidebar_resizingcan be left permanentlytruewhen the view-stack interceptsMouseUpwhile a modal is open — any subsequent drag will then silently continue from a stale anchor. The scenario is narrow but reproducible whenever a keyboard shortcut opens a modal mid-drag.crates/tui/src/tui/mouse_ui.rs— thehandle_sidebar_resize_mousefunction needs a recovery path forMouseUpwhen the view-stack is non-empty.Important Files Changed
handle_sidebar_resize_mousefor Down/Drag/Up handling; thesidebar_resizingflag can get stucktruewhen a modal interceptsMouseUpwhile the view-stack is non-empty.update_sidebar_widthhelper that clamps to [10, 50] in memory; straightforward and consistent with the codebase pattern.Sequence Diagram
sequenceDiagram participant User participant EventLoop participant handle_sidebar_resize_mouse participant App participant Settings User->>EventLoop: MouseDown on handle col EventLoop->>handle_sidebar_resize_mouse: mouse event handle_sidebar_resize_mouse->>App: "sidebar_resizing=true, anchor_x/width set" App-->>EventLoop: "needs_redraw=true" User->>EventLoop: MouseDrag (left/right) EventLoop->>handle_sidebar_resize_mouse: mouse event handle_sidebar_resize_mouse->>App: compute delta → update sidebar_width_percent (clamped 10-50%) App-->>EventLoop: "needs_redraw=true" User->>EventLoop: MouseUp EventLoop->>handle_sidebar_resize_mouse: mouse event handle_sidebar_resize_mouse->>App: "sidebar_resizing=false, sidebar_width_dirty=true" EventLoop->>Settings: load() from disk Settings-->>EventLoop: settings struct EventLoop->>Settings: update_sidebar_width(app.sidebar_width_percent) EventLoop->>Settings: save() to ~/.deepseek/settings.tomlReviews (2): Last reviewed commit: "## `feat(tui): add drag-to-resize sideba..." | Re-trigger Greptile