Skip to content

feat(sidebar): multi-line detail popover on truncated rows (#2694)#2734

Open
idling11 wants to merge 3 commits into
Hmbown:mainfrom
idling11:feat/sidebar-detail-popovers
Open

feat(sidebar): multi-line detail popover on truncated rows (#2694)#2734
idling11 wants to merge 3 commits into
Hmbown:mainfrom
idling11:feat/sidebar-detail-popovers

Conversation

@idling11
Copy link
Copy Markdown
Contributor

@idling11 idling11 commented Jun 4, 2026

Replace the single-line sidebar hover tooltip with a bordered, auto-wrapping popover that shows the full text of any truncated Work/Tasks/Agents row on mouse hover. Theme-safe styling uses DEEPSEEK_BLUE border on SURFACE_ELEVATED background.

Changes:

  • crates/tui/src/tui/app.rs:
    • Add SidebarHoverRow with row_y, full_text, detail, is_truncated
    • Extend SidebarHoverSection with rows: Vec<SidebarHoverRow> for per-row metadata
  • crates/tui/src/tui/sidebar.rs:
    • render_sidebar_section now builds rows from full_texts with per-line is_truncated detection based on content_area width
    • Update test struct literals with rows: vec![]
  • crates/tui/src/tui/mouse_ui.rs:
    • Hover detection prefers rows over flat lines. When a row has detail, the popover shows full_text + detail (newline-separated). Falls back to legacy lines for sections without row metadata.
  • crates/tui/src/tui/ui.rs:
    • Popover rendering: multi-line bordered block with auto-wrap, positioned below cursor, clamped to terminal bounds

Closes #2694

Summary

Testing

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --all-features
  • cargo test --workspace --all-features

Checklist

  • Updated docs or comments as needed
  • Added or updated tests where relevant
  • Verified TUI behavior manually if UI changes

Greptile Summary

This PR replaces the single-line sidebar hover tooltip with a bordered, auto-wrapping multi-line popover that displays the full text of truncated Work/Tasks/Agents rows. It introduces SidebarHoverRow for per-row metadata, builds row data inside render_sidebar_section, updates mouse hover detection to prefer the new rows path, and renders a DEEPSEEK_BLUE-bordered Paragraph block positioned below the cursor.

  • SidebarHoverRow adds row_y, full_text, detail, and is_truncated fields; SidebarHoverSection gains a parallel rows vector alongside the legacy lines field for backward compatibility.
  • The is_truncated detection in sidebar.rs is broken for the Work and Tasks panels: full_texts is derived from spans_to_text on already-rendered (pre-truncated) Line objects, so the truncation check always evaluates false and the popover never fires for goal/objective rows.
  • mouse_ui.rs fall-through logic, height estimation, ESC priority, and the detail: None stub were flagged in a prior review pass.

Confidence Score: 4/5

The popover renders and the new data structures are sound, but the truncation detection in the Work and Tasks panels will always return false for pre-truncated rows, meaning the visible-on-hover feature does not actually trigger for the rows it was designed for.

The callers of render_sidebar_section derive full_texts from spans_to_text on already-rendered, pre-truncated Line objects. Because content_width used by the panel functions equals content_area.width in render_sidebar_section, the is_truncated comparison always evaluates to false for goal, objective, and most task rows.

crates/tui/src/tui/sidebar.rs — the full_texts construction at the render_sidebar_work and render_sidebar_tasks call sites needs to capture original untruncated strings before truncate_line_to_width is applied.

Important Files Changed

Filename Overview
crates/tui/src/tui/app.rs Adds SidebarHoverRow struct and rows field to SidebarHoverSection; structs are well-defined and cleanly integrate with the existing hover state.
crates/tui/src/tui/sidebar.rs Builds rows from full_texts, but full_texts is derived from already-truncated rendered spans; is_truncated is always false for the key Work/Tasks rows, making the popover feature inoperative on those rows.
crates/tui/src/tui/mouse_ui.rs Adds rows-first detection path with correct fallback to legacy lines; non-truncated rows in a rows-populated section fall through to the lines path unnecessarily.
crates/tui/src/tui/ui.rs Replaces single-line tooltip with a bordered, auto-wrapping Paragraph popover; height estimation over-counts line lengths and the popup_width > 4 guard is dead code.

Comments Outside Diff (1)

  1. crates/tui/src/tui/mouse_ui.rs, line 308-342 (link)

    P2 Rows path falls through to legacy lines on non-truncated rows

    The comment says "Prefer per-row metadata (new) over flat lines (legacy)", but when the hovered row is found in section.rows with is_truncated = false, neither found is set nor do we break — so execution falls into the lines path for that same row. Today both paths use an identical UnicodeWidthStr width comparison so the observable behaviour is the same, but the intent is violated: a section that has rows populated should be handled exclusively by the rows path. Any future divergence between rows[i].is_truncated and the re-computed value in the lines branch (e.g. after a resize race or a detail-augmented text) will silently show or hide the wrong tooltip.

    Fix in Codex Fix in Claude Code Fix in Cursor

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

Reviews (4): Last reviewed commit: "style: cargo fmt --all" | Re-trigger Greptile

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 4, 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 a richer, multi-line popover for the sidebar hover state in the TUI, replacing the legacy flat-line tooltip format. It adds a new SidebarHoverRow struct to track per-row metadata (including absolute position, full text, truncation status, and extra details) and updates the mouse event handling and rendering logic to support this new format. The review feedback highlights two key areas for improvement: first, the popover is only shown if a row is truncated, which makes the extra detail field inaccessible for non-truncated rows; second, the height estimation for the popover uses byte length instead of display width and does not account for individual line lengths, which can cause layout issues, especially on smaller terminal screens where dimensions should be clamped.

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.

// Prefer per-row metadata (new) over flat lines (legacy).
if !section.rows.is_empty() {
for row in &section.rows {
if row.row_y == mouse.row && row.is_truncated {
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

If a row is not truncated (row.is_truncated is false) but has a detail populated (e.g., row.detail.is_some()), the popover will not be shown. This makes the detail completely inaccessible to the user for non-truncated rows. Consider showing the popover if the row is truncated OR if it has a detail.

Suggested change
if row.row_y == mouse.row && row.is_truncated {
if row.row_y == mouse.row && (row.is_truncated || row.detail.is_some()) {

Comment thread crates/tui/src/tui/ui.rs
Comment on lines +6771 to +6778
let popup_width = (72u16.min(size.width.saturating_sub(4))).max(30);

// Rough height: count newlines + estimate wrapped lines.
let raw_lines = tooltip_text.lines().count() as u16;
let est_wrapped = raw_lines
.saturating_add((tooltip_text.len() as u16).saturating_div(popup_width.max(1)));
let popup_content_height = est_wrapped.max(1).min(10);
let popup_height = popup_content_height + 2; // +2 for border
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

Using tooltip_text.len() (which returns the length in bytes) for height estimation will significantly overestimate the height for non-ASCII text (e.g., CJK characters which are 3 bytes each but only 2 columns wide). Additionally, dividing the total length by popup_width doesn't account for individual line lengths. \n\nAlso, if the terminal width is very small (e.g., < 30 columns), popup_width can exceed size.width, leading to out-of-bounds rendering. Clamping popup_width and popup_height to the terminal bounds prevents these layout issues.

                let popup_width = (72u16.min(size.width.saturating_sub(4))).max(30).min(size.width);\n\n                // Rough height: count newlines + estimate wrapped lines using display width.\n                let mut est_wrapped = 0u16;\n                for line in tooltip_text.lines() {\n                    let line_width = unicode_width::UnicodeWidthStr::width(line) as u16;\n                    let wrapped_count = line_width\n                        .saturating_add(popup_width.saturating_sub(1))\n                        .saturating_div(popup_width.max(1));\n                    est_wrapped = est_wrapped.saturating_add(wrapped_count.max(1));\n                }\n                let popup_content_height = est_wrapped.max(1).min(10);\n                let popup_height = (popup_content_height + 2).min(size.height); // +2 for border, clamped to terminal height

Replace the single-line sidebar hover tooltip with a bordered,
auto-wrapping popover that shows the full text of any truncated
Work/Tasks/Agents row on mouse hover. Theme-safe styling uses
`DEEPSEEK_BLUE` border on `SURFACE_ELEVATED` background.

Changes:
- `crates/tui/src/tui/app.rs`:
  - Add `SidebarHoverRow` with `row_y`, `full_text`, `detail`, `is_truncated`
  - Extend `SidebarHoverSection` with `rows: Vec<SidebarHoverRow>` for
    per-row metadata
- `crates/tui/src/tui/sidebar.rs`:
  - `render_sidebar_section` now builds `rows` from `full_texts` with
    per-line `is_truncated` detection based on content_area width
  - Update test struct literals with `rows: vec![]`
- `crates/tui/src/tui/mouse_ui.rs`:
  - Hover detection prefers `rows` over flat `lines`. When a row has
    `detail`, the popover shows `full_text + detail` (newline-separated).
    Falls back to legacy `lines` for sections without row metadata.
- `crates/tui/src/tui/ui.rs`:
  - Popover rendering: multi-line bordered block with auto-wrap,
    positioned below cursor, clamped to terminal bounds

Closes Hmbown#2694
@idling11 idling11 force-pushed the feat/sidebar-detail-popovers branch from 61fd6c9 to e151ec3 Compare June 4, 2026 03:16
Comment thread crates/tui/src/tui/ui.rs
let tooltip_area = Rect {
.min(size.height.saturating_sub(popup_height));

if popup_width > 4 {
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 popup_width > 4 guard is always true

popup_width is computed as (72u16.min(...)).max(30), so its minimum value is 30. The > 4 check can never be false and is dead code left over from the old text_width > 0 guard.

Suggested change
if popup_width > 4 {
{

Fix in Codex Fix in Claude Code Fix in Cursor

Comment thread crates/tui/src/tui/ui.rs
Comment on lines +6773 to +6777
// Rough height: count newlines + estimate wrapped lines.
let raw_lines = tooltip_text.lines().count() as u16;
let est_wrapped = raw_lines
.saturating_add((tooltip_text.len() as u16).saturating_div(popup_width.max(1)));
let popup_content_height = est_wrapped.clamp(1, 10);
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 Height estimation double-counts line lengths

tooltip_text.len() is the total byte length of the whole string. Dividing it by popup_width treats the entire text as one long unwrapped line and then adds that to the already-counted raw_lines, so every newline-separated line contributes its length twice. For a two-line text of 70 bytes each with popup_width = 72, the estimate becomes 2 + 1 = 3 even though both lines fit on one terminal row each. Per-line wrapping gives a tighter estimate.

Suggested change
// Rough height: count newlines + estimate wrapped lines.
let raw_lines = tooltip_text.lines().count() as u16;
let est_wrapped = raw_lines
.saturating_add((tooltip_text.len() as u16).saturating_div(popup_width.max(1)));
let popup_content_height = est_wrapped.clamp(1, 10);
// Rough height: sum per-line wrap estimates.
let popup_content_height = tooltip_text
.lines()
.map(|line| {
let w = unicode_width::UnicodeWidthStr::width(line).max(1) as u16;
w.saturating_add(popup_width - 1) / popup_width
})
.sum::<u16>()
.clamp(1, 10);

Fix in Codex Fix in Claude Code Fix in Cursor

Comment on lines +1930 to +1934
SidebarHoverRow {
row_y,
full_text: text.clone(),
detail: None,
is_truncated,
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 detail is always None, making the multi-line popover branch unreachable

The PR description highlights "full_text + detail (newline-separated)" as a key feature and mouse_ui.rs has the formatting branch for it, but every SidebarHoverRow is constructed here with detail: None. Until callers pass actual detail text, the format!("{}\n{}", row.full_text, detail) path in mouse_ui.rs is dead code. If the field is intentionally reserved for a follow-up, a TODO comment would make that explicit; if it was meant to be wired up in this PR, the connection is missing.

Fix in Codex Fix in Claude Code Fix in Cursor

@Hmbown
Copy link
Copy Markdown
Owner

Hmbown commented Jun 4, 2026

this is wonderful and so helpful - thank you @idling11 !!

- Add keyboard handler: pressing Esc when a sidebar popover is visible
  dismisses it and triggers a redraw.
- Add 2 unit tests verifying SidebarHoverRow truncation detection and
  sequential row_y positioning.
- Fix existing test struct literals missing the new `rows` field.
@idling11 idling11 force-pushed the feat/sidebar-detail-popovers branch from 8858c79 to ec1c24f Compare June 4, 2026 04:42
@idling11 idling11 closed this Jun 4, 2026
@idling11 idling11 reopened this Jun 4, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 4, 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.

Comment thread crates/tui/src/tui/ui.rs
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.

v0.9.0 Sidebar detail popovers: make Work/Tasks/Agents rows fully inspectable

2 participants