Skip to content

feat(tui): add drag-to-resize sidebar width#2604

Merged
Hmbown merged 1 commit into
Hmbown:mainfrom
idling11:feature/sidebar-resize-drag
Jun 3, 2026
Merged

feat(tui): add drag-to-resize sidebar width#2604
Hmbown merged 1 commit into
Hmbown:mainfrom
idling11:feature/sidebar-resize-drag

Conversation

@idling11

@idling11 idling11 commented Jun 3, 2026

Copy link
Copy Markdown
Contributor
  • 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 #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_width workflow. The drag state is tracked in App, hit-tested in mouse_ui.rs, and persisted to disk on MouseUp using the established Settings::load() + save() pattern.

  • Handle rendering (ui.rs): the handle is painted after the sidebar render pass with three visual states (default/hover/drag); layout rects are stored on App each frame for hit-testing.
  • Mouse handling (mouse_ui.rs): handle_sidebar_resize_mouse manages Down/Drag/Up events; drag fires sidebar_resizing = true and updates sidebar_width_percent in real time, with clamp(10, 50) on the percentage.
  • Persistence (ui.rs + settings.rs): sidebar_width_dirty is set on MouseUp; the event loop checks the flag and calls Settings::load()update_sidebar_widthsave().

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_resizing can be left permanently true when the view-stack intercepts MouseUp while 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 — the handle_sidebar_resize_mouse function needs a recovery path for MouseUp when the view-stack is non-empty.

Important Files Changed

Filename Overview
crates/tui/src/tui/mouse_ui.rs Adds handle_sidebar_resize_mouse for Down/Drag/Up handling; the sidebar_resizing flag can get stuck true when a modal intercepts MouseUp while the view-stack is non-empty.
crates/tui/src/tui/ui.rs Renders the drag handle and stores layout rects for hit-testing; persistence is correctly wired to the MouseUp path.
crates/tui/src/tui/app.rs Adds 7 new state fields for drag-resize; initialisation is clean and consistent with existing field patterns.
crates/tui/src/settings.rs Adds update_sidebar_width helper that clamps to [10, 50] in memory; straightforward and consistent with the codebase pattern.
crates/tui/src/tui/ui/tests.rs Adds 5 focused unit tests covering handle-click, drag-percentage math, clamping, mouse-up finalisation, and non-handle click; all test the happy path correctly.

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.toml
Loading

Fix All in Codex Fix All in Claude Code Fix All in Cursor

Reviews (2): Last reviewed commit: "## `feat(tui): add drag-to-resize sideba..." | Re-trigger Greptile

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown

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 .github/APPROVED_CONTRIBUTORS will be closed automatically.

Please read CONTRIBUTING.md for the expected contribution shape. A maintainer can grant PR access by commenting /lgtm on a pull request.

@idling11 idling11 changed the title ## feat(tui): add drag-to-resize sidebar width feat(tui): add drag-to-resize sidebar width Jun 3, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread crates/tui/src/tui/ui.rs
Comment on lines +6453 to +6460
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);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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;

Comment thread crates/tui/src/tui/ui.rs
Comment on lines +2601 to +2608
// 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();
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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();
                        }
                    });
                }

Comment thread crates/tui/src/tui/ui.rs
Comment on lines +3011 to 3018
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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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
Comment on lines +48 to +84
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,
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Fix in Codex Fix in Claude Code Fix in Cursor

Comment thread crates/tui/src/tui/ui.rs
Comment on lines +3010 to +3017
// 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();
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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!

Fix in Codex Fix in Claude Code Fix in Cursor

Comment thread crates/tui/src/settings.rs
@idling11 idling11 force-pushed the feature/sidebar-resize-drag branch from 564a0ae to adf3fe2 Compare June 3, 2026 03:47
@Hmbown Hmbown merged commit 1781312 into Hmbown:main Jun 3, 2026
9 of 10 checks passed
idling11 added a commit to idling11/DeepSeek-TUI that referenced this pull request Jun 3, 2026
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
idling11 added a commit to idling11/DeepSeek-TUI that referenced this pull request Jun 3, 2026
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
idling11 added a commit to idling11/DeepSeek-TUI that referenced this pull request Jun 3, 2026
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
idling11 added a commit to idling11/DeepSeek-TUI that referenced this pull request Jun 3, 2026
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
idling11 added a commit to idling11/DeepSeek-TUI that referenced this pull request Jun 3, 2026
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
idling11 added a commit to idling11/DeepSeek-TUI that referenced this pull request Jun 3, 2026
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
idling11 added a commit to idling11/DeepSeek-TUI that referenced this pull request Jun 3, 2026
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
idling11 added a commit to idling11/DeepSeek-TUI that referenced this pull request Jun 3, 2026
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
idling11 added a commit to idling11/DeepSeek-TUI that referenced this pull request Jun 3, 2026
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
idling11 added a commit to idling11/DeepSeek-TUI that referenced this pull request Jun 3, 2026
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
idling11 added a commit to idling11/DeepSeek-TUI that referenced this pull request Jun 3, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: TUI sidebar width is not freely resizable — needs drag-to-resize support

2 participants