Skip to content

feat: add GitHub actions to comment, merge, close, and reopen - #42

Merged
osolmaz merged 23 commits into
mainfrom
feat/resource-actions
Jul 23, 2026
Merged

feat: add GitHub actions to comment, merge, close, and reopen#42
osolmaz merged 23 commits into
mainfrom
feat/resource-actions

Conversation

@osolmaz

@osolmaz osolmaz commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Summary

ghzinga could only watch a PR or issue; acting on it still meant switching to the browser.
This change adds GitHub write actions to the TUI: comment, merge, close, and reopen, right from the viewer.
Every step works with the mouse alone or the keyboard alone, comments are written in an in-app multiline composer, and the view refreshes from GitHub after each action so it never shows guessed state.
Actions only appear when the viewer is authenticated and actually allowed to act, and the server stays the final authority.

What Changed

The feature follows the existing Elm-style architecture: pure domain types and reducer logic, a GraphQL adapter for the writes, and a background pipeline that mirrors how fetches already work.

  • Domain (src/domain/action.rs): ResourceAction, MergeMethod, ActionKind, and pure available_actions gating based on a new ActionContext (GraphQL node id, viewerCanUpdate, locked) plus repository-allowed merge methods, all fetched with the existing resource query.
  • GitHub adapter (src/github/mutations.rs): GraphQL mutations for addComment, closeIssue/reopenIssue, closePullRequest/reopenPullRequest, and mergePullRequest, reusing the existing authenticated transport. The unauthenticated REST fallback stays read-only.
  • UI: an [actions] footer button and A key open a context-sensitive menu; close/reopen and merge get confirm modals (merge with a method picker limited to what the repo allows); comments get a multiline composer modal with clickable submit/cancel buttons, click-to-place cursor, wheel scroll, and width-aware wrapping shared between rendering and input mapping (src/app/composer.rs).
  • Pipeline (src/fetch.rs, src/runner.rs): submissions spawn in the background, results come back over a dedicated channel, state is finalized on the main thread, and a successful action triggers an automatic refresh. Failures surface the API error in the status line and keep an unsent comment draft.
  • Terminal: bracketed paste is now enabled, so multi-line paste arrives as one event instead of a stream of fake keypresses.
  • Mouse/keyboard parity is structural: every operation is one reducer arm reachable from both a HitTarget and a key binding.

Testing

All local gates pass, and the new flows are covered by reducer, render, adapter, and pipeline tests.

  • cargo fmt --check, cargo check --all-targets, cargo clippy --all-targets --all-features -- -D warnings
  • cargo test: 597 tests pass (573 lib + architecture + CLI), including ~50 new tests for action availability, menu/confirm/composer key and click flows, paste handling, mutation guards, and modal rendering with hit-area assertions
  • cargo llvm-cov --fail-under-lines 85: passes at 85.86% lines
  • cargo audit, slophammer-rs check . and dry . (0.4.0): clean
  • Terminal captures regenerated with the new binary and all capture/mouse-smoke validators pass; manifests re-stamped
  • cargo mutants --in-diff (executing run) was started locally; the CI gate runs the same check on this PR
  • Not tested locally: real mutations against GitHub (comment/merge/close on a live repo) — exercised after merge

Risks

The riskiest part is state drift after a write, and the design avoids it by never patching state locally: after any successful action the resource is refetched from GitHub.

  • Server-side rejections (branch protection, merge conflicts, permissions) surface as status-line errors; the UI only pre-filters using viewerCanUpdate, locked, and allowed merge methods, so a rare 403/409 is still handled gracefully.
  • The composer is a new hand-rolled editor; its wrapping/cursor logic is pure and unit-tested (multibyte, wide chars, CRLF paste, wrap boundaries), and v1 deliberately omits selection/undo.
  • Offline fixture mode and the unauthenticated fallback hide all actions, so existing capture-based rendering is unchanged except for the new help line.

osolmaz added 15 commits July 23, 2026 19:33
Adds ResourceAction/MergeMethod/ActionKind domain types with pure
availability gating, an ActionContext (node id, viewerCanUpdate,
locked) fetched with every resource, repository merge-method flags on
pull requests, and a GraphQL mutations adapter for comment, close,
reopen, and merge.
Pure text-and-cursor state for the in-app comment editor: char-accurate
inserts and deletes across multibyte text, CRLF-normalizing paste,
width-aware wrapping shared by rendering and click mapping, sticky
vertical movement through wrapped rows, and cursor-following scroll.
Wires write actions end to end: an actions footer button and Shift-A
menu listing only the actions the viewer can take, confirm modals for
close/reopen and merge (with an allowed-merge-method picker), the
multiline comment composer modal with clickable submit/cancel buttons
and click-to-place cursor, bracketed paste support, and a background
mutation pipeline that mirrors the fetch runtime: spawn, deliver over
a channel, finalize on the main thread, refresh on success.

Every operation is reachable by mouse and keyboard through the same
reducer arm.
A mutation finishing while the user had switched or closed tabs used
to start the refresh against whichever tab was active, letting the
refetched resource overwrite an unrelated tab. The refresh now runs
inside the originating tab via apply_to_resource_tab, is skipped when
that tab is gone or shows a different resource, and a closed tab can
no longer leave the app with a stuck pending action.
Confirmation and menu shortcuts now require unmodified keys, so
terminal control sequences like Ctrl-Y can no longer merge or close a
resource. The comment composer is snapshotted with its resource tab,
which keeps an in-flight comment on one tab from discarding an unsent
draft on another and preserves drafts across tab switches.
…reshes

Freezes the composer while its comment is posting so edits typed after
Ctrl-S cannot be silently dropped when the posted snapshot closes the
draft, with an explicit hint line while locked. Post-action refreshes
that collide with an in-flight fetch are now queued on the app state
and started once loading goes idle, instead of relying on the
deferred-refresh flag that the older fetch's completion wipes.
Completion messages and draft handling now apply only when the
originating tab still shows the acted-on resource; otherwise the
pending flag is cleared without touching the new resource's status.
A replacement draft opened while an earlier comment posts now
survives that comment's completion; the close is tied to the
submitted body instead of whichever composer happens to be open.
@osolmaz
osolmaz force-pushed the feat/resource-actions branch from e172a9e to 92b6a5f Compare July 23, 2026 12:31
osolmaz added 2 commits July 23, 2026 20:36
An unrelated draft on another tab (or a replacement draft) stays
editable while a comment posts; the posting lock and hint apply only
to the composer whose body was submitted.
@osolmaz
osolmaz force-pushed the feat/resource-actions branch from f85bcbb to 525296c Compare July 23, 2026 12:47
osolmaz added 2 commits July 23, 2026 20:47
Adds boundary tests for composer editing, per-key reducer coverage,
exact modal geometry and style assertions, merge-flag parsing, and
paste event mapping. Restructures scroll_cursor_into_view into clamp
form and simplifies composer layout guards so no equivalent mutants
remain, and declares narrow cargo-mutants exclusions for the three
terminal/network boundary functions with justifications.
@osolmaz
osolmaz force-pushed the feat/resource-actions branch from 525296c to 72403ad Compare July 23, 2026 12:47
osolmaz added 4 commits July 23, 2026 21:19
Rewrites cursor_visual_in around a single last-matching-row rule
(removing the redundant row_holds_cursor mechanism whose overlap made
its mutants unobservable), drops the dead padding loop in the exact-
height menu modal, converts wheel direction to an enum, reworks the
deferred-refresh queue so entries survive a still-loading race instead
of being dropped, and adds the missing negative tests for modified-key
guards, overwide-char wrapping, and confirm-button alignment.
Ctrl+Alt+S no longer posts a comment; only an unmodified Ctrl chord
submits or cancels. Combining marks and other zero-width code points
now count as zero columns in wrapping, cursor math, and click mapping,
matching how terminals render them.
@osolmaz
osolmaz merged commit 9319b58 into main Jul 23, 2026
1 check passed
@osolmaz
osolmaz deleted the feat/resource-actions branch July 23, 2026 13:45
@osolmaz

osolmaz commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

Final report

Merged as 9319b58. GitHub write actions (comment, merge, close, reopen) are now available in the TUI behind the [actions] footer button and the A key, fully usable by mouse alone or keyboard alone.

Review

Six Codex review rounds (gpt-5.6-terra, high reasoning) against main. All P1s and all but one P2 were fixed:

  • post-action refresh routed to the originating tab, skipped when the tab closed or navigated away, with no stuck pending state
  • confirmation/menu/submit shortcuts require unmodified keys (Ctrl-Y, Ctrl+Alt+S, etc. can no longer act)
  • comment drafts are tab-scoped; the posting draft is frozen and only the composer holding the posted body is closed
  • deferred post-action refreshes survive an in-flight fetch instead of being wiped by its completion
  • completion messages never leak onto a resource the tab navigated to
  • zero-width Unicode counts as zero columns in composer wrapping/cursor/click math

Skipped (with rationale): tracking the posting composer by identity instead of body — only observable when a replacement draft is byte-identical to the comment that just posted, in which case nothing is lost.

Validation

  • cargo fmt --check, cargo check --all-targets, cargo clippy --all-targets --all-features -- -D warnings: clean
  • cargo test: 660 tests pass (636 lib + 15 architecture + 9 CLI)
  • cargo llvm-cov --fail-under-lines 85: 86.51% lines
  • cargo audit, slophammer-rs check ., slophammer-rs dry . (0.4.0): clean
  • cargo mutants --in-diff (executing): first full run 108 missed / 405 → all addressed via ~90 new tests, three structural rewrites that removed unkillable-equivalent mutants, and three justified boundary exclusions in .cargo/mutants.toml; a verification run had 0 missed at the point it was cut short on request, and the PR's CI run exercises the full gate
  • Terminal captures regenerated against the new binary; all capture and mouse-smoke validators pass; manifests re-stamped
  • Not tested locally: live mutations against real GitHub resources (needs a disposable issue/PR)

Mutation testing also surfaced one real bug beyond review findings: deferred refresh entries could be dropped when a fetch raced in; the queue now retains them.

@osolmaz

osolmaz commented Jul 25, 2026

Copy link
Copy Markdown
Owner Author

Testing comments inside ghzinga

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