OpenProgram Docs

Git folder pills#

Every working folder a conversation uses that sits inside a Git checkout gets a git half in its folder pill in the composer's environment row. The pill answers "what branch is this folder on and what has changed", and its menu holds the few Git actions a chat user needs without leaving the conversation: switch branch, work in a worktree, review the changes, open a pull request.

Which folders get a pill#

  • The main project folder, when the conversation is bound to a real project. The default home-folder project has no pill: its repository is OpenProgram's own storage, not the user's code.
  • Each additional working folder.
  • Folders that are not inside a Git checkout show nothing.

Two folders inside the same checkout (the project root and one of its subfolders, say) share one branch, one change set and one PR, so only one git half shows. Each mounted git half claims its checkout root with an order, project first and then working folders in list order, and the lowest order wins (components/chat/top-bar/git-chip-registry.ts). Separate worktrees of one repository have separate roots, so each keeps its own git half.

The pill#

One raised pill per folder (components/chat/top-bar/folder-pill.ts) with two segments separated by a full-height hairline, each its own hover and click target. The hairline takes no width, so both hover fills meet on the same edge, and it fades while either segment is hovered or open. The left segment is the folder: icon, name, and for a working folder a ✕ that appears only on hover or keyboard focus. It opens the project menu. The right segment is git: Solar git-branch, the last path segment of the branch name (claude/foo shows foo; the short commit id when HEAD is detached), and the uncommitted +N −N as a diff badge, a green half and a red half, as on the file-change card. New untracked text files count as added lines. When every change is binary (no line counts), the badge is a neutral "N files" and the level-3 dot is grey, never "+0 −0". A clean folder shows the branch alone. Both segments stretch to the full pill height, so each hover fill covers its whole half. The tooltip carries repository, full branch and counts.

When the row runs out of room it squeezes one level at a time:

Level Shows
0 everything
1 folder and branch names capped near 12 characters with an ellipsis; the Local chip icon-only
2 branch name hidden: branch icon and diff badge
3 all names hidden; the diff badge becomes a half-green, half-red dot

See composer-responsive-controls.html for the measuring rule.

The menu#

  1. Header: the repository name, tagged worktree when this folder is a linked worktree.
  2. Changes: "N uncommitted files +N −N". Clicking opens Review in its workspace scope, which already diffs every working folder against Git. Before the first message there is no session to review, so the row is inert. Ahead/behind against the upstream follows when non-zero.
  3. Branch: a filter field and the local branches, most recent commit first (up to fifty). Where a click takes the branch depends on the folder, and there is no switch to flip: a clean folder runs git switch in place; a folder with uncommitted changes opens the branch in a new worktree beside the repository and moves this folder slot onto it, so nothing in the folder moves, and the section header says "opens in a new worktree". Rows carry a tag only where it adds something: current on the branch this folder is on, and worktree on a branch already checked out in another worktree, which a click opens instead (git allows one checkout per branch). Typing a name that does not exist offers "Create branch" (or "… in a new worktree"), and Enter does the same. A folder slot that cannot move onto a worktree always switches in place.
  4. Worktrees: a new worktree is created beside the repository at <repo>-worktrees/<branch>, never inside it, so the source folder's files and uncommitted changes stay where they are. There is no separate worktree list: a branch that has one is in the branch list with its tag. What "move to a worktree" means depends on the folder slot:
    • a working folder is replaced in the conversation's folder list;
    • the main folder of an unsent draft re-points the draft's project to the worktree (registered as a project);
    • the main folder of a started conversation is fixed, so the worktree joins as an additional working folder, and the menu says so.
  5. Pull request:
    • an open PR for the branch shows "View pull request #N" and opens it in the system browser;
    • otherwise "Create pull request" pushes the branch (git push -u origin HEAD) and runs gh pr create --fill against the default branch, then opens the new PR. It is disabled, with the reason underneath, when gh is missing, there is no origin, HEAD is detached, or the branch is the default branch. The backend also refuses when the branch has no commits ahead of the base;
    • with uncommitted changes, "Commit & open PR with the agent…" puts a ready instruction in the composer (it does not send it), since writing the commit message and PR description is the agent's job.

Errors from git or gh appear as a card at the bottom of the menu; the menu stays open so the user can retry. The card has a plain title, and git's own output sits behind a "Show git output" text button, never a native disclosure widget. One refusal gets more: when an in-place switch would overwrite uncommitted changes, the card names the files and offers two ways out, Open in a worktree and Carry the changes over. The carry stashes everything (untracked included), switches, and pops the stash; if the pop conflicts, the switch stands and the changes stay in the latest stash, which the card says. There is no force-switch and no discard: an agent conversation is the wrong place to lose work.

Freshness#

Nothing polls. A pill reads its folder's state on mount and when its folder changes, when its menu opens (this is the only read that also lists branches and looks up the PR, since both cost more), when a turn of its conversation ends, when the window regains focus, when the project changes, and after any pill's mutation on the same checkout (op:git-changed).

Backend#

openprogram/worktree/folder_git.py holds the logic; ws_actions/folder_git.py moves it off the event loop and frames the replies. Every reply echoes path, so concurrent pills each take their own answer.

Action Reply Does
git_folder_status git_folder_status status --porcelain=v2 --branch (branch, upstream, ahead/behind, file counts), diff --numstat HEAD (line counts), worktree list, default branch, origin presence, gh presence; with include_branches / include_pr, the branch list and gh pr view
git_switch_branch git_switch_branch_result git switch [-c] <branch>; with carry, stash push -u → switch → stash pop. A refusal frame carries code (overwrite with files, or stash_conflict) and detail (git's output)
git_create_worktree git_worktree_created git worktree add [-b] <beside-repo path> <branch>
git_create_pr git_pr_created push the branch, gh pr create --fill, return the URL (or the existing open PR)

Status reads run with GIT_OPTIONAL_LOCKS=0 so they never contend with the user's own git commands, and GIT_TERMINAL_PROMPT=0 so a push that needs credentials fails instead of hanging.

Implementation status#

  • Pills for the project folder and working folders, same-checkout dedupe, branch switch / create, worktree create / move, change counts with Review, PR create / view, and agent hand-off: implemented.
  • Base-branch choice for a PR (always the repository's default branch today) and draft PRs: not implemented.
  • Pull / push / fetch controls and ahead/behind against the default branch when there is no upstream: not implemented.
Last updated · 2026-10-10