Skip to content

1828: feat(TUI): enable various plan rendering formats - #66

Open
martin-augment wants to merge 7 commits into
mainfrom
pr-1828-2026-06-09-02-59-55
Open

1828: feat(TUI): enable various plan rendering formats#66
martin-augment wants to merge 7 commits into
mainfrom
pr-1828-2026-06-09-02-59-55

Conversation

@martin-augment

Copy link
Copy Markdown
Owner

1828: To review by AI

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR extends the ballista-cli TUI with multi-format physical plan display and tab-based stage plan navigation. It introduces PhysicalFormat and StagePlanTab enums to support Default/Tree plan formats and Default/Tree/Metrics stage tabs respectively. The HTTP client now accepts optional plan format parameters, enabling selective plan retrieval. Key handling for both native and web builds triggers async plan loads on user demand. The JobPlansPopup and JobStagesPopup track selected formats and cache responses. Job configuration structures have been removed from Settings. UI renderers now display format-specific content with loading states, and footer bindings advertise new tab/format selection shortcuts. Agent policy files have been updated to prohibit issue linking and self-review.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pr-1828-2026-06-09-02-59-55

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude

claude Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code Review: feat(TUI): enable various plan rendering formats

Overview

This PR adds interactive tab switching for plan rendering formats in the Ballista TUI — users can now toggle between Default, Tree, and Metrics views for both job-level physical plans and stage-level plans. It also introduces a caching layer for fetched responses and removes the previous static job.stage.plan.tree config flag in favour of runtime key bindings.

The approach is sound and the feature is a meaningful UX improvement. Several issues need attention before merge.


Issues

Bug: StagePlanTab::Default sends ?plan_format= (empty string)

In two places the Default variant maps to Some(""):

ballista-cli/src/tui/ui/main/jobs/mod.rs:

StagePlanTab::Default => Some(""),

ballista-cli/src/tui/mod.rs:

StagePlanTab::Default => Some(""),

get_job_stages with Some("") builds the URL as ?plan_format= — an empty query parameter. Unless the server treats an empty value identically to no parameter, this is likely a bug. It should be None so that no query string is appended, mirroring the original no-format call.

Stale / dead config code

config.rs still contains the job.stage.plan.tree key in DEFAULT_CONFIG (line 54–58), and the web QueryString::parse() still parses ballista_job_stage_plan_tree from the URL (line 127–129) and emits job.stage.plan.tree into the built config string (line 145–148).

Since Settings no longer has a job field, this config is silently ignored at deserialisation, and format_tree ends up unused. Both the DEFAULT_CONFIG section and the query-string handling should be removed.

Fragile UiData::JobDetails routing

UiData::JobDetails(details) => {
    if details.physical_plan_tree.is_some() {
        if let Some(popup) = &mut self.job_plan_popup {
            popup.details.physical_plan_tree = details.physical_plan_tree;
        }
    } else {
        self.job_details = Some(details);
    }
}

Using the presence of physical_plan_tree to decide whether an event is a "load tree" response or a general details refresh is fragile. A dedicated UiData::JobPlanTree(String, Option<String>) variant would make the dispatch explicit and eliminate the risk of a future caller accidentally setting physical_plan_tree and triggering the wrong branch.

Dead-code annotations suggest incomplete wiring

Multiple places suppress dead-code warnings with no explanation:

  • #[allow(dead_code)] on StagePlanTab enum (stages.rs line 84)
  • #[allow(dead_code)] on JobStagesPopup::set_tab (stages.rs line 188)
  • #[allow(dead_code)] on UiData::JobStagesPlanData (event.rs line 42)

If these items are genuinely used only in one feature-flag branch, use #[cfg(...)] instead of silencing the lint. If they are placeholders for future work, they should not be merged until wired up, or the PR description should document the limitation explicitly.

#![allow(unfulfilled_lint_expectations)] in stages.rs

A file-level #![allow(unfulfilled_lint_expectations)] was added (line 17). This is a broad suppression on an unstable lint and it has no explanation. Either the #[expect(...)] attributes that triggered it should be fixed, or the attribute should be scoped as narrowly as possible.

Duplicate key handler logic

The is_plan_view() block inside handle_executors_key_event is copy-pasted nearly verbatim between the non-web path (around line 223) and the web path (around line 918). Even accounting for different return types (TuiResult<()> vs Option<WebKeyAsyncAction>), the match arms and borrow-splitting logic are identical. A small shared helper (possibly returning the fetch tuple) would eliminate drift between the two paths.

set_physical_format return type is semantically opaque

pub fn set_physical_format(&mut self, fmt: PhysicalFormat) -> Option<()>

Returning Option<()> to signal "a fetch is needed" is unusual. A bool (true = fetch needed) or a local enum (NeedsFetch / AlreadyCached) makes the caller's intent clearer.


Minor

  • physical_plan_tree: None is hardcoded in http_client.rs's get_job_details return value even when plan_format=tree is requested. The server presumably returns the tree plan in the physical_plan field, which the LoadJobPlanTree handler then moves manually (details.physical_plan_tree = details.physical_plan.take()). This works but the API shape is confusing — consider adding a physical_plan_tree field to JobDetailResponse so the HTTP deserialisation is self-documenting.

  • The footer [↑↓←→] Scroll, hint is now shown for the job_dot_popup and job_plan_popup, but not for the new stage-plan popup's scroll hint (only shown in the plan-open branch). Verify this is intentional.


Test Coverage

The only test changes are mechanical struct field additions (physical_plan_tree: None / Some(...)) to make existing tests compile. No new tests cover:

  • Tab switching and cache hit/miss behaviour in JobStagesPopup::set_tab
  • cache_plan_response updating self.stages only when the cached format is active
  • set_physical_format returning Some(())/None correctly
  • The UiData::JobDetails routing logic

Please add unit tests for the caching logic and the physical-format toggle — these are the core new behaviours and regressions would be silent without them.


Summary

The feature direction is good. The main blockers are the ?plan_format= empty-string bug, the stale config dead code, the fragile UiData::JobDetails routing, and the missing tests. The dead-code attributes and duplicate key-handler code should also be addressed before merging.

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

Copy link
Copy Markdown

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 support for rendering physical plan trees and stage plans (Default, Tree, and Metrics formats) asynchronously within the Ballista CLI TUI. The changes include updates to the HTTP client, event handling, domain models, and UI rendering to fetch and cache these plans. The review feedback highlights several potential race conditions where asynchronous responses could be applied to the wrong popup if a user switches views while a request is in flight, suggesting that the job_id be passed and verified. Additionally, the reviewer recommends mapping the default plan tab to None instead of an empty string to avoid unnecessary empty query parameters in API requests.

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 on lines +345 to +347
if let Some(p) = &mut self.job_plan_popup {
p.details.physical_plan_tree = details.physical_plan;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

There is a potential race condition here. If the user closes the popup or opens a popup for a different job while the async get_job_details request is in flight, the returned tree plan will be applied to the wrong popup. We should verify that the popup's job_id matches the job_id of the fetched plan.

                        if let Some(p) = &mut self.job_plan_popup {
                            if p.details.job_id == job_id {
                                p.details.physical_plan_tree = details.physical_plan;
                            }
                        }

Comment on lines +859 to +861
if let Some(popup) = &mut self.job_plan_popup {
popup.details.physical_plan_tree = details.physical_plan_tree;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Similar to the non-web path, there is a potential race condition where the returned tree plan is applied to the wrong popup if the user switched popups while the async request was in flight. We should verify that the popup's job_id matches the job_id of the fetched plan.

Suggested change
if let Some(popup) = &mut self.job_plan_popup {
popup.details.physical_plan_tree = details.physical_plan_tree;
}
if let Some(popup) = &mut self.job_plan_popup {
if popup.details.job_id == details.job_id {
popup.details.physical_plan_tree = details.physical_plan_tree;
}
}

Comment on lines +872 to +875
UiData::JobStagesPlanData(tab, stages) => {
if let Some(popup) = &mut self.job_stages_popup {
popup.cache_plan_response(tab, stages);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

There is a potential race condition where the returned stage plan is cached in the wrong popup if the user switched popups while the async request was in flight. We should include the job_id in UiData::JobStagesPlanData and verify that the popup's job_id matches before caching.

            UiData::JobStagesPlanData(job_id, tab, stages) => {
                if let Some(popup) = &mut self.job_stages_popup {
                    if popup.job_id == job_id {
                        popup.cache_plan_response(tab, stages);
                    }
                }
            }

Comment on lines +41 to +42
#[allow(dead_code)]
JobStagesPlanData(StagePlanTab, JobStagesResponse),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

To prevent race conditions when caching stage plans, we should include the job_id in the JobStagesPlanData variant so that the receiver can verify it belongs to the currently active popup.

Suggested change
#[allow(dead_code)]
JobStagesPlanData(StagePlanTab, JobStagesResponse),
#[allow(dead_code)]
JobStagesPlanData(String, StagePlanTab, JobStagesResponse),

Comment on lines +134 to +136
app.send_event(Event::DataLoaded {
data: UiData::JobStagesPlanData(tab, stages),
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Pass the job_id along with the JobStagesPlanData event to allow the receiver to verify and prevent race conditions.

Suggested change
app.send_event(Event::DataLoaded {
data: UiData::JobStagesPlanData(tab, stages),
})
app.send_event(Event::DataLoaded {
data: UiData::JobStagesPlanData(job_id.to_string(), tab, stages),
})

stages
.stages
.sort_by_key(|s| s.id.parse::<u64>().unwrap_or(u64::MAX));
send_data(UiData::JobStagesPlanData(tab, stages), tx).await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Pass the job_id along with the JobStagesPlanData event in the web path to prevent race conditions.

                        send_data(UiData::JobStagesPlanData(job_id, tab, stages), tx).await;

Comment on lines +115 to +119
let fmt = match tab {
StagePlanTab::Default => Some(""),
StagePlanTab::Tree => Some("tree"),
StagePlanTab::Metrics => Some("metrics"),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Instead of mapping StagePlanTab::Default to Some("") which appends an empty query parameter ?plan_format=, we can map it to None so that no query parameter is appended at all, which is cleaner and more standard.

Suggested change
let fmt = match tab {
StagePlanTab::Default => Some(""),
StagePlanTab::Tree => Some("tree"),
StagePlanTab::Metrics => Some("metrics"),
};
let fmt = match tab {
StagePlanTab::Default => None,
StagePlanTab::Tree => Some("tree"),
StagePlanTab::Metrics => Some("metrics"),
};

Comment on lines +360 to +364
let fmt = match tab {
StagePlanTab::Default => Some(""),
StagePlanTab::Tree => Some("tree"),
StagePlanTab::Metrics => Some("metrics"),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Map StagePlanTab::Default to None in the web path as well to avoid appending an empty query parameter.

                let fmt = match tab {
                    StagePlanTab::Default => None,
                    StagePlanTab::Tree => Some("tree"),
                    StagePlanTab::Metrics => Some("metrics"),
                };

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d1e0caa. Configure here.


pub fn set_plan_view(&mut self) {
self.details_view = StageDetailsView::Plan;
self.details_view = StageDetailsView::Plan(StagePlanTab::Default);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale plan after reopening view

Medium Severity

After switching stage plans to tree or metrics, leaving plan view and opening it again with p sets the active tab to default but leaves stages on the last fetched format, so the UI can show the wrong plan text until the user presses d, t, or m.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d1e0caa. Configure here.

StagePlanTab::Default => Some(""),
StagePlanTab::Tree => Some("tree"),
StagePlanTab::Metrics => Some("metrics"),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Empty default plan_format query

Low Severity

Default stage plan reloads pass Some("") into get_job_stages, which builds ?plan_format= instead of omitting the parameter or using default. The scheduler expects default, tree, or metrics, so a default-tab fetch can fail when the default cache is missing.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d1e0caa. Configure here.

@augmentcode

augmentcode Bot commented Jun 9, 2026

Copy link
Copy Markdown
🤖 Augment PR Summary

Summary: This PR extends the Ballista CLI TUI/web UI to support additional plan rendering formats.

Changes:

  • Adds a physical-plan render mode toggle (default vs tree) in the job plan popup, including caching of the tree-rendered plan.
  • Adds stage plan tabs for default, tree, and metrics, plus a per-tab cache to avoid refetching.
  • Plumbs new async actions/events so the web UI can request tree/metrics formats on demand.
  • Extends HttpClient methods to accept an optional plan_format query parameter for job details and job stages endpoints.
  • Updates key handling and footer hints to advertise the new plan-format keybindings.
  • Removes the previous config-driven stage plan tree toggle in favor of interactive switching.

Technical Notes: Plan-format selection is implemented via the scheduler REST API’s plan_format query parameter and UI-side caching to minimize network requests.

🤖 Was this summary useful? React with 👍 or 👎

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review completed. 2 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

@@ -57,8 +39,6 @@ pub struct Settings {
pub data_reload_interval_ms: u64,
/// How often to refresh the UI. In millis.
pub repaint_interval_ms: u64,

@augmentcode augmentcode Bot Jun 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In ballista-cli/src/tui/infrastructure/config.rs, Settings no longer has job, but DEFAULT_CONFIG (lines 44-58) and the web query-string config builder still emit a job: section, so try_deserialize() will likely fail with an unknown-field error at startup. Consider updating/removing those config sources to match the new Settings schema.

Severity: high

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

use crate::tui::domain::jobs::stages::StagePlanTab;

let fmt = match tab {
StagePlanTab::Default => Some(""),

@augmentcode augmentcode Bot Jun 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

StagePlanTab::Default => Some("") will construct a ?plan_format= query if it ever triggers a fetch, but the scheduler deserializes plan_format into an enum and an empty value is likely rejected as an unknown variant. Consider representing default by omitting the query parameter (or using default) to avoid 400s on stage-plan loads.

Severity: medium

Other Locations
  • ballista-cli/src/tui/mod.rs:361

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
ballista-cli/src/tui/ui/main/jobs/mod.rs (1)

115-119: ⚡ Quick win

Use None for default plan format instead of Some("").

On Line 116, Some("") sends ?plan_format= while other default stage loads use None (Line 89). Using one representation avoids inconsistent server/cache behavior.

Suggested fix
 let fmt = match tab {
-    StagePlanTab::Default => Some(""),
+    StagePlanTab::Default => None,
     StagePlanTab::Tree => Some("tree"),
     StagePlanTab::Metrics => Some("metrics"),
 };

Please apply the same mapping change in ballista-cli/src/tui/mod.rs (Line 361).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ballista-cli/src/tui/ui/main/jobs/mod.rs` around lines 115 - 119, Change the
default plan format mapping to use None instead of Some(""): in the match that
assigns fmt for StagePlanTab (the arm StagePlanTab::Default currently returns
Some(""), change it to None), and apply the identical change to the other
occurrence where fmt is set for the same StagePlanTab variants in the TUI module
(the other match that maps StagePlanTab::Default to Some("") should be changed
to None as well) so both places consistently omit the plan_format parameter.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ballista-cli/src/tui/app.rs`:
- Around line 858-875: Async responses must be correlated by job_id before
mutating popups: when handling UiData::JobDetails ensure you only set
popup.details.physical_plan_tree if self.job_plan_popup exists and its job_id
matches the JobDetails' job_id (otherwise set self.job_details); similarly,
change UiData::JobStagesPlanData to carry job_id and in the match check that if
let Some(popup) = &mut self.job_stages_popup { popup.job_id == job_id &&
popup.cache_plan_response(tab, stages) } so you only cache for the matching
JobStagesPopup; update all emission sites of UiData::JobStagesPlanData to
include the job_id.

In `@ballista-cli/src/tui/domain/jobs/stages.rs`:
- Around line 166-170: set_plan_view currently sets details_view to
StagePlanTab::Default and resets scroll positions but doesn't clear or reset the
plan payload, so a previously-loaded Tree/Metrics payload can be shown under the
Default tab; update the set_plan_view method to also reset the stored plan data
(e.g. clear or set self.stages / the plan payload field to the default/empty
payload for the Default tab) so reopening the plan view cannot display stale
non-default content, keeping the existing resets of
plan_vertical_scroll_position and plan_horizontal_scroll_position.

In `@ballista-cli/src/tui/event.rs`:
- Around line 41-42: The variant JobStagesPlanData currently holds only
(StagePlanTab, JobStagesResponse) which allows a delayed response to overwrite
state for a different job; change the enum variant to include the originating
job identifier (e.g., JobStagesPlanData(JobId, StagePlanTab,
JobStagesResponse)), update all creators/emitters to supply the job_id, and
update all pattern matches/consumers (places that destructure JobStagesPlanData
and any response-matching logic) to compare the incoming job_id against the
currently active job before applying the payload so stale responses are ignored.

In `@ballista-cli/src/tui/ui/footer.rs`:
- Around line 41-42: Update the footer hint to match the actual key handling for
the job-dot popup: locate the two pushes that build current_view_key_bindings
(the Span::from lines) and change the horizontal-arrows hint to only show
vertical scroll keys (e.g., replace "[↑↓←→] Scroll, " with "[↑↓] Scroll, " or
similar) so the displayed keys reflect the implemented up/down-only scrolling
for the dot popup.

---

Nitpick comments:
In `@ballista-cli/src/tui/ui/main/jobs/mod.rs`:
- Around line 115-119: Change the default plan format mapping to use None
instead of Some(""): in the match that assigns fmt for StagePlanTab (the arm
StagePlanTab::Default currently returns Some(""), change it to None), and apply
the identical change to the other occurrence where fmt is set for the same
StagePlanTab variants in the TUI module (the other match that maps
StagePlanTab::Default to Some("") should be changed to None as well) so both
places consistently omit the plan_format parameter.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: eb0cf81d-d04b-471b-a956-836db99f5b41

📥 Commits

Reviewing files that changed from the base of the PR and between 370838f and d1e0caa.

📒 Files selected for processing (15)
  • .cursor/rules.md
  • AGENTS.md
  • CLAUDE.md
  • ballista-cli/src/tui/app.rs
  • ballista-cli/src/tui/domain/jobs.rs
  • ballista-cli/src/tui/domain/jobs/stages.rs
  • ballista-cli/src/tui/event.rs
  • ballista-cli/src/tui/http_client.rs
  • ballista-cli/src/tui/infrastructure/config.rs
  • ballista-cli/src/tui/mod.rs
  • ballista-cli/src/tui/ui/footer.rs
  • ballista-cli/src/tui/ui/main/jobs/job_plan_popup.rs
  • ballista-cli/src/tui/ui/main/jobs/mod.rs
  • ballista-cli/src/tui/ui/main/mod.rs
  • ballista-cli/src/tui/ui/mod.rs
💤 Files with no reviewable changes (1)
  • ballista-cli/src/tui/infrastructure/config.rs

Comment on lines +858 to +875
if details.physical_plan_tree.is_some() {
if let Some(popup) = &mut self.job_plan_popup {
popup.details.physical_plan_tree = details.physical_plan_tree;
}
} else {
self.job_details = Some(details);
}
}
UiData::JobStagesGraph(graph) => {
self.job_dot_popup = Some(graph);
}
UiData::JobStagesData(job_id, stages) => {
self.job_stages_popup = Some(JobStagesPopup::new(job_id, stages));
}
UiData::JobStagesPlanData(tab, stages) => {
if let Some(popup) = &mut self.job_stages_popup {
popup.cache_plan_response(tab, stages);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Guard async popup updates with job_id correlation.

Out-of-order async responses can update the wrong popup state. JobDetails tree updates and JobStagesPlanData cache writes currently apply to whichever popup is open, without verifying job identity.

Proposed fix
 UiData::JobDetails(details) => {
     if details.physical_plan_tree.is_some() {
-        if let Some(popup) = &mut self.job_plan_popup {
+        if let Some(popup) = &mut self.job_plan_popup
+            && popup.details.job_id == details.job_id
+        {
             popup.details.physical_plan_tree = details.physical_plan_tree;
         }
     } else {
         self.job_details = Some(details);
     }
 }
-UiData::JobStagesPlanData(tab, stages) => {
-    if let Some(popup) = &mut self.job_stages_popup {
+UiData::JobStagesPlanData(job_id, tab, stages) => {
+    if let Some(popup) = &mut self.job_stages_popup
+        && popup.job_id == job_id
+    {
         popup.cache_plan_response(tab, stages);
     }
 }

This also requires extending the UiData::JobStagesPlanData payload at emission sites to include job_id.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ballista-cli/src/tui/app.rs` around lines 858 - 875, Async responses must be
correlated by job_id before mutating popups: when handling UiData::JobDetails
ensure you only set popup.details.physical_plan_tree if self.job_plan_popup
exists and its job_id matches the JobDetails' job_id (otherwise set
self.job_details); similarly, change UiData::JobStagesPlanData to carry job_id
and in the match check that if let Some(popup) = &mut self.job_stages_popup {
popup.job_id == job_id && popup.cache_plan_response(tab, stages) } so you only
cache for the matching JobStagesPopup; update all emission sites of
UiData::JobStagesPlanData to include the job_id.

Comment on lines 166 to 170
pub fn set_plan_view(&mut self) {
self.details_view = StageDetailsView::Plan;
self.details_view = StageDetailsView::Plan(StagePlanTab::Default);
self.plan_vertical_scroll_position = 0;
self.plan_horizontal_scroll_position = 0;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Default plan tab can display stale non-default payload.

On Line 167, set_plan_view switches to StagePlanTab::Default but leaves self.stages untouched. If Tree/Metrics was previously loaded, reopening plan view can show the wrong content under the default tab.

Suggested fix
 pub fn set_plan_view(&mut self) {
     self.details_view = StageDetailsView::Plan(StagePlanTab::Default);
+    if let Some(default_resp) = self.plan_cache.default.as_ref() {
+        self.stages = default_resp.clone();
+    }
     self.plan_vertical_scroll_position = 0;
     self.plan_horizontal_scroll_position = 0;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ballista-cli/src/tui/domain/jobs/stages.rs` around lines 166 - 170,
set_plan_view currently sets details_view to StagePlanTab::Default and resets
scroll positions but doesn't clear or reset the plan payload, so a
previously-loaded Tree/Metrics payload can be shown under the Default tab;
update the set_plan_view method to also reset the stored plan data (e.g. clear
or set self.stages / the plan payload field to the default/empty payload for the
Default tab) so reopening the plan view cannot display stale non-default
content, keeping the existing resets of plan_vertical_scroll_position and
plan_horizontal_scroll_position.

Comment on lines +41 to +42
#[allow(dead_code)]
JobStagesPlanData(StagePlanTab, JobStagesResponse),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

JobStagesPlanData is missing job_id, enabling cross-job stale-response overwrite.

Line 42 carries only tab + payload. If a delayed async response arrives after the user switches jobs, the active popup can cache stages for the wrong job.

Contract fix
-    JobStagesPlanData(StagePlanTab, JobStagesResponse),
+    JobStagesPlanData(String, StagePlanTab, JobStagesResponse),
- data: UiData::JobStagesPlanData(tab, stages),
+ data: UiData::JobStagesPlanData(job_id.to_owned(), tab, stages),
- UiData::JobStagesPlanData(tab, stages) => {
-     if let Some(popup) = &mut self.job_stages_popup {
-         popup.cache_plan_response(tab, stages);
-     }
- }
+ UiData::JobStagesPlanData(job_id, tab, stages) => {
+     if let Some(popup) = &mut self.job_stages_popup
+         && popup.job_id == job_id
+     {
+         popup.cache_plan_response(tab, stages);
+     }
+ }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ballista-cli/src/tui/event.rs` around lines 41 - 42, The variant
JobStagesPlanData currently holds only (StagePlanTab, JobStagesResponse) which
allows a delayed response to overwrite state for a different job; change the
enum variant to include the originating job identifier (e.g.,
JobStagesPlanData(JobId, StagePlanTab, JobStagesResponse)), update all
creators/emitters to supply the job_id, and update all pattern matches/consumers
(places that destructure JobStagesPlanData and any response-matching logic) to
compare the incoming job_id against the currently active job before applying the
payload so stale responses are ignored.

Comment on lines +41 to 42
current_view_key_bindings.push(Span::from("[↑↓←→] Scroll, "));
current_view_key_bindings.push(Span::from("[Esc] Close popup, "));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix job-dot footer hint to match actual key handling.

The footer shows horizontal scroll keys for the dot popup, but the handler only supports up/down scrolling.

Proposed fix
-    current_view_key_bindings.push(Span::from("[↑↓←→] Scroll, "));
+    current_view_key_bindings.push(Span::from("[↑↓] Scroll, "));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
current_view_key_bindings.push(Span::from("[↑↓←→] Scroll, "));
current_view_key_bindings.push(Span::from("[Esc] Close popup, "));
current_view_key_bindings.push(Span::from("[↑↓] Scroll, "));
current_view_key_bindings.push(Span::from("[Esc] Close popup, "));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ballista-cli/src/tui/ui/footer.rs` around lines 41 - 42, Update the footer
hint to match the actual key handling for the job-dot popup: locate the two
pushes that build current_view_key_bindings (the Span::from lines) and change
the horizontal-arrows hint to only show vertical scroll keys (e.g., replace
"[↑↓←→] Scroll, " with "[↑↓] Scroll, " or similar) so the displayed keys reflect
the implemented up/down-only scrolling for the dot popup.

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.

3 participants