Skip to content

feat: hand a pull request's problem to a session on its own branch - #447

Merged
omartelo merged 1 commit into
mainfrom
handoff-opens-the-session
Sep 4, 2026
Merged

omartelo merged 1 commit into
mainfrom
handoff-opens-the-session

Conversation

@omartelo

@omartelo omartelo commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

The Resolve conflicts / Fix CI errors button wrote its prompt through the
screen's ordinary inject, which targets whichever session happens to be
active
— the one you were last in, on another branch, with nothing on screen
to say it had happened. With none active it answered Open a session to send this to, which reads as a button that needs another button pressed first.

Now the button does the whole job:

  • It works on the pull request's own branch. openInSession answers with the
    session it opened — the live one activated, a parked one resumed, or a worktree
    created from the PR with a session on it — so the handoff reuses that flow
    whole instead of guessing at a target.
  • It goes there. The screen the click happened on cannot show what became of
    the prompt, so the click ends in the session that got it. The button says
    Opening session… while a worktree is being made.
  • It waits for a prompt to land at. A write issued the moment a session's card
    exists reaches whatever that PTY is up to — the checkout's setup script, a
    provider still taking over the tty — and both read it and discard it, leaving a
    prompt that was never given anything. terminal.Ready is the same question the
    relay asks before delivering a task, and it also answers for a session that has
    not spawned yet, so one call covers every branch above.

Polled from the page rather than waited on in Go: the wait can be minutes on a
first worktree install, and a request held open that long is one of the browser's
handful of connections to this backend. No backend change — terminal.Ready was
already an exported method, so registration already exposed it.

Enter stays the user's. The relay remains the one place lich presses it, for a
sender that is not a person.

Ceilings

The handoff now waits on the same draft gate the relay does, without an Enter of
its own to justify it: hand a conflict to a session you left half a sentence in
and nothing appears at that prompt until the draft goes stale (draftIdle).
Written into the existing bullet in docs/ceilings.md — the alternative, pasting
into the middle of someone's unfinished line, is the worse of the two.

Test plan

  • gofmt -l ., go vet ./..., go test ./...
  • biome check src, tsc --noEmit, vitest run (1359 tests), vite build
  • New unit test: the write does not happen until Ready says so
  • By hand: conflicting PR with no checkout → button creates the worktree,
    lands on the new session, prompt appears once the agent is up (not in the
    setup script)
  • By hand: same PR with its session already live → activates, navigates,
    prompt is there immediately

https://claude.ai/code/session_01V4L4uMYd3WoQWRraqo9FPa

The "Resolve conflicts" and "Fix CI errors" buttons wrote their prompt
through the screen's ordinary inject — into whichever session happened to
be active. That is the session the user was last in, on another branch,
with nothing on screen to say anything had happened; and with none active
the click only answered "Open a session to send this to", which read as a
button that needs another one pressed first.

It works on the pull request's own branch now. openInSession answers with
the session it opened — the live one, a parked one resumed, or a worktree
created from the PR and a session on it — so the handoff reuses the whole
of that flow, then goes there. What the click did is on screen, because
the screen is now the session it went to.

The prompt waits for a prompt to land at. A write issued the moment a
session's card exists reaches whatever that PTY is up to: the checkout's
setup script, a provider still taking over the tty, both of which read it
and discard it. terminal.Ready is the question the relay already asks
before it delivers, and it is polled from the page rather than waited on
in Go: the wait can be minutes, and a held request is one of the
browser's handful of connections to this backend.

Enter stays the user's. The relay is still the one place lich presses it.

Claude-Session: https://claude.ai/code/session_01V4L4uMYd3WoQWRraqo9FPa
@omartelo
omartelo merged commit 9135ac1 into main Sep 4, 2026
4 checks passed
@omartelo
omartelo deleted the handoff-opens-the-session branch September 4, 2026 19:11
omartelo added a commit that referenced this pull request Sep 5, 2026
The New worktree dialog's name field took a branch name or the task in
plain words. It takes an issue too now — "#128", or the URL copied out of
the browser — looked up through gh on the project's own account.

What that buys is the other half: the session opened on that worktree
finds the issue at its prompt. Number, title, link and body, as one
bracketed paste so the body's newlines are text rather than a run of
Enters, and unsent — the Enter stays the user's, as with the pull request
handoff. Typing the context in by hand was the only way an agent learned
what its worktree was for.

Everything the flow needs already existed: toBranchName slugs the title
and prints the exact branch under the field before anything is created
(#413), CreateWorktree makes an ordinary branch off the chosen base,
writeAtPrompt waits out the setup script and the provider's boot (#447),
and bracketedPaste is why the paste is one block. What is new is one gh
call, one pure module, and a row in the gh message table.

Two refusals, both because the field is a branch name first:

- A bare number stays a branch name. "#" is the marker, so there is one
  field with one meaning rather than a mode to be in.
- A pull request's number is refused, naming the screen that does open
  pull requests. GitHub counts issues and pull requests in one sequence
  and `gh issue view` answers for either; taking it would branch off the
  base under its title instead of checking its head out. The URL is the
  only field in the answer that tells the two apart.

And the reference itself never names the branch, resolved or not: git
accepts "#381" as a branch name, and most shells read the "#" as the
start of a comment, so `git checkout #381` is `git checkout`.

Claude-Session: https://claude.ai/code/session_01GSu6sRs6pBuZHmWaJmuZ3o
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