Skip to content

fix(tui): cache Work panel summary to survive transient mutex misses && fix compile error#2616

Closed
idling11 wants to merge 1 commit into
Hmbown:mainfrom
idling11:fix/sidebar-work-stale-cache-v2
Closed

fix(tui): cache Work panel summary to survive transient mutex misses && fix compile error#2616
idling11 wants to merge 1 commit into
Hmbown:mainfrom
idling11:fix/sidebar-work-stale-cache-v2

Conversation

@idling11
Copy link
Copy Markdown
Contributor

@idling11 idling11 commented 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 #2606

Greptile Summary

This PR fixes Work panel flickering in the TUI sidebar by caching the last successful SidebarWorkSummary in App and returning it as a fallback when the todos/plan_state mutexes are held by the engine, instead of resetting to "Work state updating…". It also adds SiliconflowCN as a distinct provider alongside the existing Siliconflow.

  • sidebar_work_summary is rewritten to use a three-tier fallback chain (fresh lock → cached snapshot → empty updating state), with live hunt/goal fields refreshed on every frame even during fallback. Four unit tests cover all branches.
  • SiliconflowCN is wired into the CLI enum, TUI config, ProvidersToml, env-var mapping, and documentation. Several duplicated match arms introduced in an earlier commit are also removed.
  • Code-formatting-only cleanups (long match arm line-wrapping) are spread across config.rs, client.rs, main.rs, and ui.rs.

Confidence Score: 4/5

Safe to merge after fixing the SiliconflowCN credential write/read key mismatch in config.rs; the sidebar caching change is correct.

The sidebar caching logic, tests, and App field addition are all correct. The SiliconflowCN wiring has one concrete defect: save_api_key_for and provider_config_key both use "siliconflow-CN" (hyphen) as the TOML table key, but ProvidersToml.siliconflow_CN (underscore) is what serde will read — so any credential written via codewhale auth set --provider siliconflow-cn is immediately orphaned and authentication for SiliconflowCN will always fall back to the default (empty) config.

crates/tui/src/config.rs — save_api_key_for and provider_config_key use the wrong separator for the SiliconflowCN TOML key.

Important Files Changed

Filename Overview
crates/tui/src/tui/sidebar.rs Core of the PR: rewrites sidebar_work_summary to cache the last successful SidebarWorkSummary in App, falling back to it when try_lock() misses. Logic and fallback chain are correct; live hunt/goal fields are refreshed even on cache fallback. Four unit tests cover the main scenarios.
crates/tui/src/config.rs Two functions (save_api_key_for and provider_config_key) use "siliconflow-CN" (hyphen) as the TOML key for SiliconflowCN, but ProvidersToml.siliconflow_CN (underscore) is what serde deserializes — the written credential is silently orphaned and never read back. The rest of the SiliconflowCN match-arm deduplication and formatting cleanups are correct.
crates/tui/src/tui/app.rs Adds cached_work_summary: Option<SidebarWorkSummary> field to App, initialized to None. Clean, minimal change to support the caching fix in sidebar.rs.
crates/config/src/lib.rs Adds siliconflow_CN: ProviderConfigToml field (with #[allow(non_snake_case)]) and splits SiliconflowCN from Siliconflow in get/get_mut. The field name siliconflow_CN (underscore) correctly reflects what serde will read from TOML, but callers in config.rs write the hyphenated form, causing the read/write mismatch flagged above.
crates/cli/src/lib.rs Adds SiliconflowCN to the CLI provider enum, slot function (returning "siliconflow-cn"), env-var list, and provider list. Straightforward additions with a matching test case.
crates/tui/src/tui/notifications.rs Removes a spurious leading `

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[render frame - sidebar_work_summary called] --> B{try_lock todos AND plan_state}
    B -- both succeed --> C[Build fresh SidebarWorkSummary\nstate_updating = false]
    C --> D[Store clone in app.cached_work_summary]
    D --> E[Return fresh summary]
    B -- either lock busy --> F{app.cached_work_summary is Some?}
    F -- yes --> G[Clone cached summary]
    G --> H[Overwrite live fields:\nquarry, token_budget,\nverdict, tokens_used]
    H --> I[Return updated cache\nstate_updating stays false]
    F -- no --> J[Return empty SidebarWorkSummary\nstate_updating = true]
    E --> K[Render Work panel]
    I --> K
    J --> K
Loading

Comments Outside Diff (1)

  1. crates/tui/src/tui/sidebar.rs, line 68 (link)

    P2 Double invocation of sidebar_work_summary per frame

    sidebar_work_summary(app) is called here just to compute has_useful_content(), and then called again at line 587 inside render_sidebar_work for actual rendering. Each call now acquires both mutexes, builds a full SidebarWorkSummary, and writes a clone() into the cache — so every frame where the Work panel is visible pays for two mutex acquisitions, two snapshot copies, and two heap allocations.

    This double-call pattern predates this PR, but the added clone() on every successful path makes it marginally more expensive. A straightforward fix would be to compute the summary once in render_sidebar_auto and pass it by reference to render_sidebar_work, eliminating the redundant invocation.

    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

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

Reviews (7): Last reviewed commit: "fix(tui): cache Work panel summary to su..." | Re-trigger Greptile

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 3, 2026

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.

Copy link
Copy Markdown
Contributor

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

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 introduces caching for the sidebar work summary in the TUI application to prevent the panel from being wiped when shared mutexes are busy. It adds a 'cached_work_summary' field to the 'App' struct, updates 'sidebar_work_summary' to fall back to this cache, and includes comprehensive unit tests. The feedback highlights two key improvements: first, changing the visibility of 'cached_work_summary' to 'pub(crate)' to avoid a potential compilation error (E0446) due to exposing a private type in a public interface; second, refactoring 'sidebar_work_summary' to consolidate the copying of live fields and eliminate redundant code across different execution paths.

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/app.rs
pub sidebar_hover_tooltip: Option<String>,
/// Cached Work panel summary — used as fallback when `try_lock()` on
/// the shared todo/plan mutexes fails during a frame render.
pub cached_work_summary: Option<SidebarWorkSummary>,
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

Since SidebarWorkSummary is defined as pub(crate) in sidebar.rs, declaring cached_work_summary as pub in the public App struct can lead to a compilation error (E0446: private type in public interface) if App is exported publicly from the tui crate. Changing the field visibility to pub(crate) resolves this safely, as it is only accessed within the tui crate.

Suggested change
pub cached_work_summary: Option<SidebarWorkSummary>,
pub(crate) cached_work_summary: Option<SidebarWorkSummary>,

Comment on lines +229 to 306
fn sidebar_work_summary(app: &mut App) -> SidebarWorkSummary {
// Try to build a fresh summary from the shared mutexes. When either
// lock is busy (engine is updating state in another tokio task),
// fall back to the last cached snapshot instead of wiping the panel.
let fresh = (|| {
let todos = app.todos.try_lock().ok()?;
let plan = app.plan_state.try_lock().ok()?;

let todos_snapshot = todos.snapshot();
let checklist_completion_pct = todos_snapshot.completion_pct;
let checklist_items: Vec<SidebarWorkChecklistItem> = todos_snapshot
.items
.into_iter()
.map(|item| SidebarWorkChecklistItem {
id: item.id,
content: item.content,
status: item.status,
})
.collect();

match app.plan_state.try_lock() {
Ok(plan) => {
if !plan.is_empty() {
summary.strategy_explanation = plan.explanation().map(str::to_string);
summary.strategy_steps = plan
.steps()
let (strategy_explanation, strategy_steps) = if plan.is_empty() {
(None, Vec::new())
} else {
(
plan.explanation().map(str::to_string),
plan.steps()
.iter()
.map(|step| SidebarWorkStrategyStep {
text: step.text.clone(),
status: step.status.clone(),
elapsed: step.elapsed_str(),
})
.collect();
}
}
Err(_) => {
summary.state_updating = true;
}
.collect(),
)
};

Some(SidebarWorkSummary {
goal_objective: app.hunt.quarry.clone(),
goal_token_budget: app.hunt.token_budget,
goal_completed: app.hunt.verdict == HuntVerdict::Hunted,
goal_started_at: app.hunt.started_at,
tokens_used: app.session.total_conversation_tokens,
checklist_completion_pct,
checklist_items,
strategy_explanation,
strategy_steps,
state_updating: false,
})
})();

if let Some(summary) = fresh {
app.cached_work_summary = Some(summary.clone());
return summary;
}

// Fall back to cached snapshot. Only emit "updating" when there is
// genuinely nothing to show (first render before any todos exist).
if let Some(ref cached) = app.cached_work_summary {
let mut summary = cached.clone();
// Keep hunt/goal fields live even when locks are busy.
summary.goal_objective = app.hunt.quarry.clone();
summary.goal_token_budget = app.hunt.token_budget;
summary.goal_completed = app.hunt.verdict == HuntVerdict::Hunted;
summary.goal_started_at = app.hunt.started_at;
summary.tokens_used = app.session.total_conversation_tokens;
return summary;
}

summary
SidebarWorkSummary {
goal_objective: app.hunt.quarry.clone(),
goal_token_budget: app.hunt.token_budget,
goal_completed: app.hunt.verdict == HuntVerdict::Hunted,
goal_started_at: app.hunt.started_at,
tokens_used: app.session.total_conversation_tokens,
state_updating: true,
..SidebarWorkSummary::default()
}
}
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 current implementation duplicates the copying of live app.hunt and app.session fields across three different code paths (the fresh closure, the cache fallback, and the default fallback). We can simplify this significantly by constructing/retrieving the base summary first, and then applying the live fields once at the end of the function. This improves maintainability and eliminates redundant code.

fn sidebar_work_summary(app: &mut App) -> SidebarWorkSummary {
    // Try to build a fresh summary from the shared mutexes. When either
    // lock is busy (engine is updating state in another tokio task),
    // fall back to the last cached snapshot instead of wiping the panel.
    let fresh = (|| {
        let todos = app.todos.try_lock().ok()?;
        let plan = app.plan_state.try_lock().ok()?;

        let todos_snapshot = todos.snapshot();
        let checklist_completion_pct = todos_snapshot.completion_pct;
        let checklist_items: Vec<SidebarWorkChecklistItem> = todos_snapshot
            .items
            .into_iter
            .map(|item| SidebarWorkChecklistItem {
                id: item.id,
                content: item.content,
                status: item.status,
            })
            .collect();

        let (strategy_explanation, strategy_steps) = if plan.is_empty() {
            (None, Vec::new())
        } else {
            (
                plan.explanation().map(str::to_string),
                plan.steps()
                    .iter()
                    .map(|step| SidebarWorkStrategyStep {
                        text: step.text.clone(),
                        status: step.status.clone(),
                        elapsed: step.elapsed_str(),
                    })
                    .collect(),
            )
        };

        Some(SidebarWorkSummary {
            checklist_completion_pct,
            checklist_items,
            strategy_explanation,
            strategy_steps,
            state_updating: false,
            ..SidebarWorkSummary::default()
        })
    })();

    let mut summary = if let Some(summary) = fresh {
        app.cached_work_summary = Some(summary.clone());
        summary
    } else if let Some(ref cached) = app.cached_work_summary {
        cached.clone()
    } else {
        SidebarWorkSummary {
            state_updating: true,
            ..SidebarWorkSummary::default()
        }
    };

    // Keep hunt/goal fields live on every frame, even during cache fallback.
    summary.goal_objective = app.hunt.quarry.clone();
    summary.goal_token_budget = app.hunt.token_budget;
    summary.goal_completed = app.hunt.verdict == HuntVerdict::Hunted;
    summary.goal_started_at = app.hunt.started_at;
    summary.tokens_used = app.session.total_conversation_tokens;

    summary
}

@idling11 idling11 force-pushed the fix/sidebar-work-stale-cache-v2 branch 6 times, most recently from 75739fc to 114c4d8 Compare June 3, 2026 05:02
@idling11
Copy link
Copy Markdown
Contributor Author

idling11 commented Jun 3, 2026

@Hmbown It seems the main branch has many compilation issues. My PR fixes some parts that weren't originally my work.

@idling11 idling11 force-pushed the fix/sidebar-work-stale-cache-v2 branch 4 times, most recently from 678d987 to 64738b0 Compare June 3, 2026 06:30
Comment thread crates/config/src/lib.rs
Comment on lines +222 to +223
#[allow(non_snake_case)]
pub siliconflow_CN: ProviderConfigToml,
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 Silent config drop for lowercase siliconflow_cn key

Every other provider in ProvidersToml uses all-lowercase snake_case (siliconflow, wanjie_ark, xiaomi_mimo, etc.). A user who follows that convention and writes [providers.siliconflow_cn] will silently receive an empty default config — serde silently ignores unknown fields and ProvidersToml.get(&ProviderKind::SiliconflowCN) returns &self.siliconflow_CN (which stays at Default). Adding #[serde(alias = "siliconflow_cn")] on the field would prevent the silent data loss without changing the primary documented key.

Fix in Codex Fix in Claude Code Fix in Cursor

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 idling11 force-pushed the fix/sidebar-work-stale-cache-v2 branch from 64738b0 to f134267 Compare June 3, 2026 06:34
@idling11 idling11 changed the title fix(tui): cache Work panel summary to survive transient mutex misses fix(tui): cache Work panel summary to survive transient mutex misses && fix compile error Jun 3, 2026
@Hmbown
Copy link
Copy Markdown
Owner

Hmbown commented Jun 3, 2026

Thanks @idling11 for both the Work-panel diagnosis/fix and the compile-break repair. I reproduced the compile failure on main and have already pulled the narrow SiliconFlow-CN repair into the v0.8.52 stabilization branch.

I am keeping the Work-panel cache piece separate for review so it does not get hidden inside the compile fix. Sorry main was in such a noisy state here; your report and PR made the failure much easier to unwind.

@Hmbown
Copy link
Copy Markdown
Owner

Hmbown commented Jun 3, 2026

Update: after a second pass for v0.8.52, I pulled the narrow Work-panel cache slice from this PR into #2626 as well, with the four focused sidebar regression tests. I kept the SiliconFlow provider wiring on the #2626 implementation path because that branch uses the shared [providers.siliconflow] table and provider-registry checks.

You are credited in the v0.8.52 changelog/community section for both #2606 and this PR. Once #2626 lands, I expect this PR to be superseded for the release-blocking pieces, while any broader Work-panel cleanup can stay separate. Thanks again, and sorry the release branch made your fix harder to review cleanly.

@Hmbown
Copy link
Copy Markdown
Owner

Hmbown commented Jun 3, 2026

Closing as superseded by #2626, which is now merged on main at c8ce2b8. The release branch includes the SiliconFlow-CN compile repair and the narrow Work-panel cache fix from this PR, with co-author/changelog credit for @idling11. Thanks again for the clear diagnosis; any broader Work-panel cleanup can stay as a separate follow-up if needed.

@Hmbown Hmbown closed this Jun 3, 2026
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.

Sidebar "Work" panel checklist status not updating after turn completes

2 participants