feat(tui): show persisted guidance on rejected policy chunks - #2908
feat(tui): show persisted guidance on rejected policy chunks#2908vyncint wants to merge 4 commits into
Conversation
The reviewer's rejection note is stored in PolicyChunk.rejection_reason and already reaches the TUI in GetDraftPolicyResponse.chunks, but openshell-tui never read the field. The note was dropped at the last step, so a reviewer had no way to recall why a chunk had been rejected. Render it in two places, following the truncate-in-list / full-in-popup convention in the TUI development guide: a truncated, dimmed suffix on the list row, and a "Guidance:" line in the detail popup. Gate the accessor on status == "rejected" rather than on the field alone. Approving a chunk passes None for the reason and the gateway writes the field only when Some, so a chunk that was rejected and later approved still carries the old note. Reading the field unconditionally would surface a stale rejection on an approved rule. Part of NVIDIA#1098, Definition of Done item "Rejected chunks show persisted guidance". The other items on that issue are untouched. Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
|
All contributors have signed the DCO ✍️ ✅ |
|
I have read the DCO document and I hereby sign the DCO. |
johntmyers
left a comment
There was a problem hiding this comment.
gator-agent
PR Review Status
This focused TUI change is project-valid under #1098, but the detail view does not keep the full persisted guidance reachable when an accepted rejection reason wraps past the fixed popup height. The direct user-facing TUI behavior also needs a corresponding Fern documentation update before pipeline handoff.
Action required: make long guidance reachable in the detail UI, add the deterministic render coverage described inline, and document the rejected-guidance UX under docs/.
Blocking findings:
GATOR-30f8375a-01: long persisted rejection guidance is clipped in the fixed, non-scrollable detail popup.
Carried findings:
- None
Non-blocking suggestions:
- None
Gator metadata
- Validation: Project-valid as a focused implementation of the “Rejected chunks show persisted guidance” item in accepted issue #1098.
- Docs: Missing for this direct TUI UX change; update the relevant Fern page under
docs/(navigation only if needed). - Checks: Branch Checks and Helm Lint have not been dispatched for the current head.
- E2E: Not required for this presentation-only TUI change.
- Head SHA:
30f8375acfd4f41dbb7079b82b83bd93e6dae535 - Base SHA:
905e99aa2a960c17d5672b11cf64f10ff7d18b41 - Merge base SHA:
905e99aa2a960c17d5672b11cf64f10ff7d18b41 - Patch ID:
5a767987002dffe3c12f471538e37f0530e8f858 - Gator payload:
7 - Review mode:
initial - Previous reviewed SHA: none
- Review budget exhausted: no
- Maintainer decision required: no
- Next state:
gator:in-review
A rejection reason has no server-side length cap, so a paragraph-length one overflowed the fixed 22-row detail popup: the tail was clipped and the later fields and both action hints were pushed off screen. The same overflow already affected the unbounded rationale and security_notes fields, so fix the popup rather than special-case the guidance line. Split the popup's inner area into a scrolling body and a pinned hint row, following the pattern in ui/create_provider.rs, and drive it with j/k, the arrow keys, PageUp, PageDown, g and G. The approve and close controls now stay on screen at every scroll position, and the bottom border carries the scroll position. Wrap free-form values explicitly instead of relying on Paragraph's Wrap, so the rendered row count is exactly lines.len() and the scroll clamp cannot under-run the content. Paragraph::line_count would answer the same question but sits behind ratatui's unstable-rendered-line-info feature. Add deterministic TestBackend coverage at 80x24 with a 2,000-character reason, covering the head and tail, the pinned hints, over-scroll clamping, and the pre-existing long-rationale case. Document the reviewer-facing guidance in the policy advisor page. Part of NVIDIA#1098. Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
|
Thanks — Before the fix:
So the tail was lost and every later field plus both action hints were pushed out of the 22-row popup. Two things I confirmed while verifying:
What changedScrollable body with pinned controls. The inner area now splits into a scrolling body and a one-row footer, following the existing pattern in Explicit wrapping instead of After the fix, at max scroll: CoverageDeterministic render tests against
Plus four DocsAdded to the "Review Proposals" section of Verification
The fix is in |
Making the scroll clamp exact meant dropping Paragraph's Wrap, which also removed wrapping from the lines that do not go through push_wrapped. At 70 columns the denial row clipped mid-value and lost the last-seen timestamp, and at 60 a pending chunk with scrollable content pushed [Esc] Close past the right edge of the single hint row. The key still worked, but that is the same controls-not-visible failure one axis over. Keep the denial row on one line while it fits and wrap it onto the label indent when it does not, so the common 80-column layout is unchanged. Pack the footer hints into as many rows as they need without splitting a hint, and derive the footer height from that: adding a hint row shrinks the body and can itself change whether the content scrolls, so the two settle together. The earlier tests were all 80x24, which is why they missed this. Cover the denial timestamps and the approve, reject and close hints at 60, 70 and 80 columns, plus the packing helper directly. Part of NVIDIA#1098. Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
|
Self-review of Making the scroll clamp exact meant dropping And at 60 columns a pending chunk with scrollable content produced FixThe denial row stays on one line while it fits and wraps onto the label indent when it does not, so nothing is lost and the common 80-column case is unchanged. The footer now packs hints into as many rows as they need without ever splitting a single hint, and the footer height is derived from that — body height and hint rows settle together, since adding a hint row shrinks the body and can itself change whether the content scrolls. At 60 columns: CoverageMy earlier tests were all 80×24, which is exactly why they missed this. The new ones sweep widths:
67 tests in |
johntmyers
left a comment
There was a problem hiding this comment.
gator-agent
PR Review Status
Thanks @vyncint. I checked the scrollable body, pinned controls, narrow-footer follow-up, deterministic render coverage, and Fern docs you added. Those resolve the original ASCII long-guidance reproducer, but the same reachability obligation remains open for valid double-width Unicode guidance: the manual wrapper counts characters rather than terminal display columns, so CJK text can still be clipped horizontally with no way to scroll to the missing tail.
Action required: measure wrapping and hard breaks by terminal display width, and add an 80×24 render regression with a CJK reason whose suffix must remain reachable.
Blocking findings:
- None newly introduced
Carried findings:
GATOR-30f8375a-01: partially resolved; vertical overflow and controls are fixed, but double-width Unicode guidance can still be clipped atcrates/openshell-tui/src/ui/sandbox_draft.rs:563.
Gator metadata
- Validation: Project-valid as a focused implementation of the “Rejected chunks show persisted guidance” item in accepted issue #1098.
- Docs: Fern documentation updated under
docs/sandboxes/policy-advisor.mdx; navigation change not needed. - Checks: Branch Checks and Helm Lint have not been dispatched for the current head.
- E2E: Not required for this presentation-only TUI change.
- Head SHA:
03c67b18b405fd4c8d8204973efcc1474457f8a8 - Base SHA:
905e99aa2a960c17d5672b11cf64f10ff7d18b41 - Merge base SHA:
905e99aa2a960c17d5672b11cf64f10ff7d18b41 - Patch ID:
b112d40f4bdec6b84a9adaa7f47af9388bb9cdee - Gator payload:
7 - Review mode:
follow_up - Previous reviewed SHA:
30f8375acfd4f41dbb7079b82b83bd93e6dae535 - Review budget exhausted: no
- Maintainer decision required: no
- Next state:
gator:in-review
wrap_value packed rows by chars().count(), and a CJK glyph is one char but two terminal columns. A double-width rejection reason therefore produced rows about twice the popup's width: the right half of every row was clipped, and unlike the vertical case there is no horizontal scroll to recover it. Measure width with Span::width(), the same measurement ratatui applies when it lays cells out, so the wrap agrees with the renderer by construction and no new dependency is needed. This covers word packing, the hard-break path for a word wider than the line, the label indent, the footer hint packing, and the denial row's fits-on-one-line check. The list row's shortened copy had the same defect through truncate_str, which counts chars. Leave truncate_str alone for its two existing callers and add truncate_display for the guidance row. Cover it with an 80x24 render regression that interleaves markers through the CJK text: a trailing marker lands on its own short row in both the broken and fixed layouts, so it would not detect this. The two unit tests measure with an independent column oracle rather than the helper under test, which would make them tautological. Part of NVIDIA#1098. Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
|
Confirmed, and thanks — this one was real and I would not have caught it from the ASCII case. Reproduced first: a CJK reason at 80×24 renders every wrapped row at roughly twice the popup's column width, so the right half of each row is clipped and there is no horizontal scroll to recover it. The cause is exactly as described at FixWidth is now measured in display columns via The list row's shortened copy had the same defect for a different reason: CoverageThe render regression you asked for is I also checked the tests actually bite. Reverting The first versions of the two unit tests asserted with 70 tests in |
|
/ok to test b47d01e |
johntmyers
left a comment
There was a problem hiding this comment.
gator-agent
PR Review Status
Thanks @vyncint. I checked the display-column wrapping, hard-break path, list-row truncation, footer packing, and the independent 80×24 CJK render regression in the latest delta. These changes resolve GATOR-30f8375a-01; the Gator-owned thread is now resolved, and no blocking findings remain.
Blocking findings:
- No blocking findings remain
Carried findings:
GATOR-30f8375a-01: resolved by the display-width-aware wrapping and deterministic CJK reachability coverage.
Gator metadata
- Validation: Project-valid as a focused implementation of the rejected-guidance item in accepted issue #1098.
- Docs: Fern documentation updated under
docs/sandboxes/policy-advisor.mdx; navigation change not needed. - Checks: Current-head Branch Checks and Helm Lint are in progress after
/ok to test; required workflow dispatch is confirmed. - E2E: Not required for this presentation-only TUI change; a current-head Branch E2E workflow was also dispatched.
- Head SHA:
b47d01e00d6972aba3e636edbad107af55564ce7 - Base SHA:
905e99aa2a960c17d5672b11cf64f10ff7d18b41 - Merge base SHA:
905e99aa2a960c17d5672b11cf64f10ff7d18b41 - Patch ID:
f6f6b12d89879e1ff19e389dcc5bae9e3db1e819 - Gator payload:
7 - Review mode:
follow_up - Previous reviewed SHA:
03c67b18b405fd4c8d8204973efcc1474457f8a8 - Review budget exhausted: no
- Maintainer decision required: no
- Next state:
gator:watch-pipeline
Summary
The reviewer's rejection note is already stored in
PolicyChunk.rejection_reasonand already reaches the TUI inGetDraftPolicyResponse.chunks, butopenshell-tuinever read the field — so the note was dropped at the last step and there was no way to recall why a chunk had been rejected. This renders it: truncated on the list row, in full in the detail popup.Related Issue
Part of #1098 — Definition of Done item "Rejected chunks show persisted guidance."
Deliberately not
Fixes/Closes: #1098 has seven DoD items and this PR completes one, so closing the issue would be wrong. The other items are untouched —human_summary(item 1) and the L4/no-method-path scoping warning (item 4) still need a proto field or a design decision;intent_summary(item 2) looks reachable through the existingPolicyChunk.rationalebut is a separate change;validation_result(item 3), the ~2s poll cadence (item 6), and the approve/reject hotkeys (item 7) are already implemented onmaineven though their checkboxes are unticked.Changes
All in
crates/openshell-tui/src/ui/sandbox_draft.rs(+88, −0):rejection_guidance(&PolicyChunk) -> Option<&str>— a small pure accessor next totruncate_str, gated onstatus == "rejected"and trimming the value.Guidance:line after theReview:block, usingt.status_errand the existing 12-character label column. Placed high on purpose:popup_heightis capped at 22 rows with wrapping but no scrolling, so content after a long endpoint list can fall off the bottom.truncate_str, dimmed witht.mutedto match how rejected rows are already greyed, and appended last so it clips harmlessly instead of displacing anything on a narrow terminal.#[cfg(test)] mod testswith 7 cases.The split (truncate in the list, full text in the popup) follows the Long content: truncate + detail popup convention in
.agents/skills/tui-development/SKILL.md.Why the status gate, and not just a non-empty check
Approving a chunk passes
Nonefor the reason (grpc/policy.rs:1537,:4760,:5174), and the gateway writes the field only when it isSome(persistence/sqlite.rs:1147-1149). So a chunk that was rejected and later approved — reachable with the[a] Approvehotkey the popup already offers on a rejected chunk — keeps its old note. Reading the field unconditionally would print a stale rejection on an approved rule. Only revert-to-pending clears it, viaSome("")atgrpc/policy.rs:5376.approved_chunk_hides_stale_reasonpins this.Coordination with #2168
#2168 (draft) rewrites
approval_annotation()and deletesvalidation_issue_summary()in this same file. Nothing here touches that region or calls either function, so the two rebase cleanly in either order.Testing
mise run pre-commitpassesopenshell-tui, which has no e2e surfaceRun locally on Linux x86-64, Rust 1.95.0:
mise run pre-commitmise run cicargo test -p openshell-tuiThe 7 new tests:
rejected_chunk_exposes_its_reason,approved_chunk_hides_stale_reason,pending_chunk_has_no_guidance,blank_reason_is_dropped,reason_is_trimmed,long_reason_truncates_for_the_list_row,short_reason_is_not_truncated.Rendering was checked, not assumed. I rendered
draw_detail_popupinto aratatui::backend::TestBackendand read the cell buffer back:That probe is not in the diff — the crate has no rendering-test precedent (every existing test in
ui/andtheme.rscovers pure functions, and there are no dev-dependencies), so I kept the shipped tests to the same shape and removed the probe afterwards. Happy to land it as a permanent test if you would rather have coverage of the frame itself.One limitation I want to be straight about: the list-row span is compile-checked and its truncation is unit-tested, but I did not render that view.
draw()takes&mut App, which owns a gRPC client and is not cheaply constructible in a unit test.Checklist
feat(tui): show persisted guidance on rejected policy chunks