Skip to content

Review comment navigator, gate deny split button, and a pinned policy self-deny - #171

Merged
radutopala merged 3 commits into
mainfrom
feat/approval-split-button-review-comment-nav
Jul 28, 2026
Merged

Review comment navigator, gate deny split button, and a pinned policy self-deny#171
radutopala merged 3 commits into
mainfrom
feat/approval-split-button-review-comment-nav

Conversation

@radutopala

Copy link
Copy Markdown
Owner

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.

  • 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 — 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 an allow the agent writes for itself, taking effect on the next container start.

injectPolicySelfDenyRule puts a deny on /etc/loop/** at the head of the file rules after the merge, the one position no config layer can precede.

  • link is in the operation set (unlike the generic rule) because linkat/symlinkat match 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 — the file is not a secret, and seeing the active policy is useful when debugging a denial.

Verification

  • go test ./internal/container/ ./internal/config/ -count=1 — both ok; internal/container coverage stays at 100%.
  • make lint — 0 issues; biome + tsc --noEmit clean.
  • Both UI features driven end-to-end against a live dev instance: a real execve gate exercised the split button, the caret menu, the prompt sub-form, and deny-and-stop (agent saw Exit 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 at 13 / 13, and orphan-last ordering.

Known issue, not fixed here

Clicking next rapidly (~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.

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
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.
@radutopala
radutopala merged commit 1dd62a0 into main Jul 28, 2026
17 checks passed
@radutopala
radutopala deleted the feat/approval-split-button-review-comment-nav branch July 28, 2026 16:38
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.

1 participant