Skip to content

Open pull requests from Pane, and follow them from the review panel - #387

Open
0x92 wants to merge 4 commits into
dcouple:mainfrom
0x92:feature/pull-request-workflow
Open

Open pull requests from Pane, and follow them from the review panel#387
0x92 wants to merge 4 commits into
dcouple:mainfrom
0x92:feature/pull-request-workflow

Conversation

@0x92

@0x92 0x92 commented Aug 11, 2026

Copy link
Copy Markdown

Description

Open a pull request for a session's branch without leaving Pane, and follow it from the review panel afterwards.

A session already is a branch in a worktree, so everything a pull request needs is on hand. Today the flow leaves the app: switch to a browser, find the fork, remember which repository the contribution is meant for. This adds a Create Pull Request entry to the session's git menu that gathers the branch, its commits, the repository's PR template and the possible targets in one round trip, pushes the branch and calls gh pr create.

Once the pull request exists, the same session's Review panel gains a status strip: state, review decision, mergeability, size and reviewers — the questions that otherwise send you back to the browser.

image

What it does

Creating

  • Title and body are proposed from the commits ahead of the base: the first commit's subject becomes the title, the rest become a list, and the repository's pull_request_template.md is appended rather than replacing them.
  • Fork-aware target picker. A fork has two candidates, and picking your own by accident is a silent mistake, so the parent is offered first and the head ref is spelled owner:branch when the pull request crosses repositories.
  • Searchable base-branch picker. The short list is the branches this clone actually works on that also exist in the target; everything else is one click away. (A busy upstream has ~200 branches — a plain list is unusable, and a native <datalist> hides everything that does not match what is already typed.)
  • Changes section: which files the pull request would carry, with per-file +/- and totals, compared against the merge base the way GitHub counts it. "Show diff" loads the patch on demand and renders it in the existing DiffViewer.
  • Says what is wrong instead of failing obscurely: no commits ahead, detached HEAD, gh missing or signed out, an existing pull request for this branch (shown as a link — there is nothing to create), a base branch the target repository does not have.
  • Uncommitted changes are called out explicitly, because they stay behind.

Following

  • A strip under the review header: Open / Draft / Merged / Closed, number and title, review decision, comment count, and a conflict warning when GitHub reports CONFLICTING.
  • Expanded: owner:head → repo:base, size, and each reviewer with their newest verdict — a "changes requested" that the same person later approved is not an open objection.
  • Refreshes itself every 90s while the panel is on screen, and on demand.
  • CI checks continue to be shown by the existing checks chip.

How it works

  • PullRequestManager (main process) is the only place that runs commands; everything that decides what to send is a pure function next to it, tested on its own: deriveDraftText, resolveTargets, buildCreateArgs, normalizeBaseBranch, sortBaseBranches, parsePullRequestStatus, parseChecks, …
  • gh is invoked through the session's CommandRunner, so a WSL project uses the gh inside the distro rather than the one on the Windows host. When gh is not on the snapshotted PATH — for instance because it was installed while Pane was running — the usual install locations are tried before giving up.
  • The body never goes through a shell: it is written to a temp file and passed with --body-file, so backticks, quotes and newlines in markdown survive.
  • All channels are daemon-owned (pr:*): the branch, its remote and the gh binary live on the machine that runs the agents, so a remote runtime answers these itself instead of having the laptop push a branch it does not have.

Type of Change

  • New feature (non-breaking change which adds functionality)

Checklist

  • I have read the CONTRIBUTING.md guidelines
  • My code follows the code style of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have run pnpm typecheck and pnpm lint locally
  • I have tested the Electron app locally with pnpm electron-dev

Critical Areas Modified

  • State management/IPC events

Additional Notes

Builds on the per-file commit details PR — it reuses shared/types/git.ts, the numstat/name-status parsers in gitDiffManager and parseUnifiedDiff. Please merge that one first.

Tested against the real GitHub CLI and this repository: draft generation, the target list for a fork, base-branch listing across several API pages, the file comparison cross-checked against git diff --name-only <base>...HEAD, and status for open pull requests. One real pull request was created end to end and closed again.

0x92 added 4 commits August 11, 2026 11:51
Reviewing a session meant reading one combined patch: which files a commit
touched, and how much, was not visible anywhere. Two changes, both in the
diff path:

- Commit history rows expand to a per-file list with status and line
  counts, and clicking a file opens that file's diff directly
- Reviewing a large working tree no longer blocks the UI

The freeze was real work, not a hang: capturing the working-directory diff
spawned one `cat` per untracked file and one `wc -l` per file, all
synchronously on the main thread — 800+ spawns for a session with many
untracked files, several seconds each pass. The path is now async and
batched, with the file list gathered once instead of per consumer.

The parsers for `--numstat -z`, `--name-status -z` and porcelain `-z` are
pure functions with their own tests; the `-z` format packs `XY path` into a
single token, which is easy to get wrong and impossible to notice by eye.

Tests: commit file changes (23) and the unified diff parser (11).
Two things went wrong for WSL projects, both because the host is Windows
while the shell that runs the command is bash inside the distro.

Arguments were quoted with escapeShellArg(), which picks its style from
process.platform and therefore handed bash double quotes. Inside those,
$, backticks and \ stay live. Git only forbids space, ~^:?*[\ and
control characters in a ref, so a branch named fix-`whoami` is legal and
would have run as a command; the pull request title is free user text on
the same path. quoteArg() now asks the CommandRunner which shell it
targets and uses escapeForBash() from wslUtils for WSL, which exists for
exactly this and says so in its comment.

The pull request body was written to the host tmpdir() and its C:\ path
passed to a gh running inside the distro, which cannot open it, so
--body-file failed before a pull request was ever created.
resolveBodyFile() now writes to the distro's /tmp through the UNC view
Windows has of it and names the Linux path to gh. Host projects keep the
behaviour they had.
Two defects, both in the path that inlines untracked files into a
working-tree diff.

The listing used `git ls-files --others --exclude-standard` split on
newlines. Git delimits with newlines there, which a filename may contain,
and quotes anything non-ASCII into a C-style escape — `täst.txt` arrived
as the literal `"t\303\244st.txt"`, a name matching no file on disk, so
the file vanished from the diff and from the stats without a word. The
listing now uses `-z` and is split on NUL, and nothing is trimmed: a
leading or trailing space is part of the name.

Content and line counts were then read with `cat "<worktree>/<file>"` and
`wc -l "<worktree>/<file>"`, built by interpolation. Git allows `$`,
backticks and parentheses in a filename, and inside bash double quotes
those are still syntax: a file named `back`whoami`.txt` in a repository
was enough to run a command, with no interaction beyond opening the
session. Both now go through `fs` — readFile for content, a streamed
newline count for the totals — so a repository-controlled name never
reaches a shell. untrackedFilePath() resolves the worktree-relative name
git reports, going through the UNC mount for a WSL project the same way
gitPlumbingCommands already does.

The performance fix this PR made is kept and improved: capture spawns a
fixed handful of commands whatever the file count, where before it was
one `cat` and one batched `wc` per file. MAX_UNTRACKED_INLINE_FILES and
MAX_UNTRACKED_INLINE_BYTES still bound what is inlined, and a per-file
ceiling stands in for the 1 MB buffer `cat` used to have.

chunkByCommandLength() goes with them — nothing builds a command line out
of paths any more.
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