feat(sidebar): multi-line detail popover on truncated rows (#2694)#2734
feat(sidebar): multi-line detail popover on truncated rows (#2694)#2734idling11 wants to merge 3 commits into
Conversation
|
Thanks @idling11 for taking the time to contribute. This repository is currently observing a maintainer-managed contribution gate in dry-run mode, so this pull request is staying open. When enforcement is enabled, pull requests from contributors who are not listed in Please read |
There was a problem hiding this comment.
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 §ion.rows { | ||
| if row.row_y == mouse.row && row.is_truncated { |
There was a problem hiding this comment.
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.
| if row.row_y == mouse.row && row.is_truncated { | |
| if row.row_y == mouse.row && (row.is_truncated || row.detail.is_some()) { |
| 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 |
There was a problem hiding this comment.
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 heightReplace 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
61fd6c9 to
e151ec3
Compare
| let tooltip_area = Rect { | ||
| .min(size.height.saturating_sub(popup_height)); | ||
|
|
||
| if popup_width > 4 { |
| // 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); |
There was a problem hiding this comment.
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.
| // 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); |
| SidebarHoverRow { | ||
| row_y, | ||
| full_text: text.clone(), | ||
| detail: None, | ||
| is_truncated, |
There was a problem hiding this comment.
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.
|
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.
8858c79 to
ec1c24f
Compare
|
Thanks @idling11 for taking the time to contribute. This repository is currently observing a maintainer-managed contribution gate in dry-run mode, so this pull request is staying open. When enforcement is enabled, pull requests from contributors who are not listed in Please read |
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_BLUEborder onSURFACE_ELEVATEDbackground.Changes:
crates/tui/src/tui/app.rs:SidebarHoverRowwithrow_y,full_text,detail,is_truncatedSidebarHoverSectionwithrows: Vec<SidebarHoverRow>for per-row metadatacrates/tui/src/tui/sidebar.rs:render_sidebar_sectionnow buildsrowsfromfull_textswith per-lineis_truncateddetection based on content_area widthrows: vec![]crates/tui/src/tui/mouse_ui.rs:rowsover flatlines. When a row hasdetail, the popover showsfull_text + detail(newline-separated). Falls back to legacylinesfor sections without row metadata.crates/tui/src/tui/ui.rs:Closes #2694
Summary
Testing
cargo fmt --all -- --checkcargo clippy --workspace --all-targets --all-featurescargo test --workspace --all-featuresChecklist
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
SidebarHoverRowfor per-row metadata, builds row data insiderender_sidebar_section, updates mouse hover detection to prefer the new rows path, and renders aDEEPSEEK_BLUE-borderedParagraphblock positioned below the cursor.SidebarHoverRowaddsrow_y,full_text,detail, andis_truncatedfields;SidebarHoverSectiongains a parallelrowsvector alongside the legacylinesfield for backward compatibility.is_truncateddetection insidebar.rsis broken for the Work and Tasks panels:full_textsis derived fromspans_to_texton already-rendered (pre-truncated)Lineobjects, so the truncation check always evaluatesfalseand the popover never fires for goal/objective rows.mouse_ui.rsfall-through logic, height estimation, ESC priority, and thedetail: Nonestub 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
SidebarHoverRowstruct androwsfield toSidebarHoverSection; structs are well-defined and cleanly integrate with the existing hover state.rowsfromfull_texts, butfull_textsis derived from already-truncated rendered spans;is_truncatedis always false for the key Work/Tasks rows, making the popover feature inoperative on those rows.lines; non-truncated rows in a rows-populated section fall through to thelinespath unnecessarily.Paragraphpopover; height estimation over-counts line lengths and thepopup_width > 4guard is dead code.Comments Outside Diff (1)
crates/tui/src/tui/mouse_ui.rs, line 308-342 (link)lineson non-truncated rowsThe comment says "Prefer per-row metadata (new) over flat lines (legacy)", but when the hovered row is found in
section.rowswithis_truncated = false, neitherfoundis set nor do we break — so execution falls into thelinespath for that same row. Today both paths use an identicalUnicodeWidthStrwidth comparison so the observable behaviour is the same, but the intent is violated: a section that hasrowspopulated should be handled exclusively by the rows path. Any future divergence betweenrows[i].is_truncatedand the re-computed value in thelinesbranch (e.g. after a resize race or a detail-augmented text) will silently show or hide the wrong tooltip.Reviews (4): Last reviewed commit: "style: cargo fmt --all" | Re-trigger Greptile