Review comment navigator, gate deny split button, and a pinned policy self-deny - #171
Merged
Merged
Conversation
Review panel: a floating prev/next pinned bottom-right over the diff steps comment-to-comment. The toolbar's existing pair only moves file-to-file, which is too coarse once a file carries several comments. Order follows the render: files top-to-bottom, within a file by the anchoring diff line, out-of-diff comments last. Jumping into a collapsed file expands it first. The counter re-measures on scroll so it stays honest when the user scrolls by hand. Also hoists four useMemo calls above ReviewDiffView's "no diff content" early return. They were conditional hooks: the branch flips when the first out-of-diff comment lands on an empty session, and React counts hooks per render. Gate approval card: the deny actions collapse into a split button. Plain Deny stays the default — it is the only non-terminal deny — and the variants that change what happens after the denial move into a caret menu. That menu now also exposes deny-session, which the gate manager has always accepted but no client offered: it caches the denial under the request's cache key so a retrying agent is refused without re-prompting.
File rules are first-match-wins, and both config layers author rules by
prepending them. The project layer is {workDir}/.loop/config.json — inside
the workspace the agent may write — so any rule reachable through config,
including the generic /etc/** system-path deny, can be shadowed by an allow
the agent writes for itself, taking effect on the next container start.
Inject a Deny on /etc/loop/** at the head of the list after the merge, the
one position no config layer can precede. `link` is in the operation set
(unlike the generic rule) because linkat/symlinkat are matched on the new
path only: without it the policy file could be hardlinked into the
blanket-allowed workspace and written through the second name. Reads stay
allowed — seeing the active policy is useful when debugging a denial.
radutopala
enabled auto-merge (rebase)
July 28, 2026 16:22
The split button moved "Deny with prompt" out of the card body and into the caret menu, so the two scenarios that asserted it on the page failed. Open the menu and assert there instead, and pin the one asymmetry between the surfaces while we're in here: chat's agent is an orchestrator run that owns a message queue and gets "Deny & stop run"; a terminal pane's agent is a TUI on the pane's stdin with nothing queued behind it, so that variant is withheld.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three changes, two UI and one gate-policy.
Review panel — comment navigator
A floating prev/next pinned bottom-right over the diff steps comment-to-comment. The toolbar's existing pair only moves file-to-file, which is too coarse once a file carries several comments.
Also hoists four
useMemocalls aboveReviewDiffView's "no diff content" early return. They were conditional hooks — the branch flips when the first out-of-diff comment lands on an empty session, and React counts hooks per render.Gate approval card — deny split button
The deny actions collapse into a split button. Plain Deny stays the default (it is the only non-terminal deny); the variants that change what happens after the denial move into a caret menu. The menu also exposes deny-session, which the gate manager has always accepted but no client offered — it caches the denial under the request's cache key, so a retrying agent is refused without re-prompting.
Gate — pinned self-deny on the policy directory
File rules are first-match-wins, and both config layers author rules by prepending them. The project layer is
{workDir}/.loop/config.json, which lives inside the workspace the agent may write. So any rule reachable through config — including the generic/etc/**system-path deny — can be shadowed by anallowthe agent writes for itself, taking effect on the next container start.injectPolicySelfDenyRuleputs adenyon/etc/loop/**at the head of the file rules after the merge, the one position no config layer can precede.linkis in the operation set (unlike the generic rule) becauselinkat/symlinkatmatch on the new path only. Without it the policy file could be hardlinked into the blanket-allowed workspace and written through the second name.Verification
go test ./internal/container/ ./internal/config/ -count=1— bothok;internal/containercoverage stays at 100%.make lint— 0 issues; biome +tsc --noEmitclean.execvegate exercised the split button, the caret menu, the prompt sub-form, and deny-and-stop (agent sawExit code 126 … Operation not permitted, the card and badge cleared, the run stopped). A stubbed review session with 3 files and 13 comments exercised counter stepping, cross-file jumps with file-rail sync, collapsed-file expansion, scroll re-measurement, clamping at13 / 13, and orphan-last ordering.Known issue, not fixed here
Clicking
nextrapidly (~5/s) makes the counter stutter and occasionally regress, because the scroll-driven sync fires during the in-flight smooth scroll. The fix is a short suppression window after a programmatic jump; left out to keep this diff scoped.