Phase 20 delivers five improvements spanning file viewing, PR lifecycle, app stability, keyboard shortcuts, and merge UX. It includes: opening fully-added files (especially markdown) in the normal file viewer instead of the syntax-highlighted diff view; evolving the PR button into a full PR-merge-archive lifecycle flow; prompting the user before quitting when worktrees are in a loading state; adding a Cmd+G keyboard shortcut for the sidebar merge action; and replacing the merge button with a styled archive button when the selected merge-from branch is already up-to-date.
- Open fully-added files in the standard file viewer (markdown files use the markdown preview)
- Evolve the PR button into a merge button after PR creation, then an archive button after merge
- Prompt the user before quitting when worktrees are loading
- Add Cmd+G keybind to trigger the sidebar merge action
- Show archive button instead of merge when the selected branch is already up-to-date
| Component | Technology |
|---|---|
| Added file viewer routing | Update ChangesView.handleViewDiff to route new files through openFile instead of setActiveDiff |
| PR lifecycle state machine | New prState map in useGitStore tracking per-worktree PR status (none → created → merged → archived) |
| PR URL detection | Parse AI session streaming output for GitHub PR URLs when session originated from PR button |
| PR merge via gh CLI | New git:prMerge IPC channel calling gh pr merge + cross-worktree local branch sync |
| Quit confirmation | before-quit handler in main process checking worktree loading state via IPC to renderer |
| Cmd+G shortcut | New entry in DEFAULT_SHORTCUTS, handler in useKeyboardShortcuts dispatching sidebar merge |
| Branch up-to-date check | New git:isBranchMerged IPC channel, UI swap in GitPushPull from merge button to archive button |
When a user clicks a file in ChangesView (src/renderer/src/components/file-tree/ChangesView.tsx, line 219-234), handleViewDiff is called. For new/added files (status ? or A), it sets isNewFile: true in the ActiveDiff and opens InlineDiffViewer.
InlineDiffViewer (src/renderer/src/components/diff/InlineDiffViewer.tsx, lines 356-389) detects isNewFile and renders the raw file content with react-syntax-highlighter (Prism + oneDark). This means:
- Markdown files show as raw markdown source code with syntax highlighting, not a rendered preview
- All new files show in a diff-oriented chrome (with "New file" badge, copy-diff button, context line controls) that is irrelevant for viewing a wholly new file
- The
FileViewercomponent (src/renderer/src/components/file-viewer/FileViewer.tsx), which has proper markdown preview with source/preview toggle (lines 220-262), is never reached becauseactiveDifftakes priority inMainPane(line 61-73)
Clicking a fully-added file in ChangesView:
Before:
┌──────────────────────────────────────────┐
│ [New file] [Copy] [Context: 3] │ ← diff chrome
│ │
│ 1 │ # README.md │ ← raw syntax-highlighted
│ 2 │ │ markdown source code
│ 3 │ This is a description... │
│ 4 │ │
│ 5 │ ## Installation │
│ 6 │ ```bash │
│ 7 │ npm install │
└──────────────────────────────────────────┘
After:
┌──────────────────────────────────────────┐
│ README.md [Source] [Preview] │ ← normal file viewer
│ │
│ # README.md │ ← rendered markdown
│ │ (preview mode default)
│ This is a description... │
│ │
│ ## Installation │
│ ┌──────────────────────────────┐ │
│ │ npm install │ │
│ └──────────────────────────────┘ │
└──────────────────────────────────────────┘
For non-markdown added files (e.g. new .ts, .css files):
Same as before -- opens in the standard FileViewer with
syntax highlighting and line numbers (which FileViewer
already does via SyntaxHighlighter).
Key behavior: When a file in ChangesView has status ? (untracked) or A (added) and is entirely new (not a renamed/copied file with partial changes), clicking it should open it via useFileViewerStore.openFile() instead of setActiveDiff(). This routes through the standard FileViewer component, which:
- Detects
.md/.mdxfiles and defaults to preview mode (line 107-108) - Renders via
MarkdownRendererin preview mode (line 256-262) - Has source/preview toggle for markdown (lines 220-243)
- Shows proper syntax highlighting for all other file types
A. Update handleViewDiff in ChangesView.tsx (lines 219-234):
Route new/added files through openFile instead of setActiveDiff:
const handleViewDiff = useCallback(
(file: GitFileStatus) => {
if (!worktreePath) return
const isNewFile = file.status === '?' || file.status === 'A'
if (isNewFile) {
// Open fully-added files in the standard file viewer
const fullPath = `${worktreePath}/${file.relativePath}`
const fileName = file.relativePath.split('/').pop() || file.relativePath
const worktreeId = useWorktreeStore.getState().selectedWorktreeId
if (worktreeId) {
useFileViewerStore.getState().openFile(fullPath, fileName, worktreeId)
}
} else {
// Open modified/deleted files in the diff viewer
useFileViewerStore.getState().setActiveDiff({
worktreePath,
filePath: file.relativePath,
fileName: file.relativePath.split('/').pop() || file.relativePath,
staged: file.staged,
isUntracked: file.status === '?',
isNewFile: false
})
}
onFileClick?.(file.relativePath)
},
[worktreePath, onFileClick]
)B. Apply the same logic to GitStatusPanel.tsx (lines 251-264) if it has the same pattern.
C. No changes needed to InlineDiffViewer, FileViewer, or MainPane -- the existing routing logic already works correctly when openFile is called instead of setActiveDiff. MainPane line 76-78 checks activeFilePath and renders FileViewer when no activeDiff is active.
| File | Change |
|---|---|
src/renderer/src/components/file-tree/ChangesView.tsx |
Update handleViewDiff to route new files through openFile |
src/renderer/src/components/git/GitStatusPanel.tsx |
Same change to its handleViewDiff equivalent |
The PR button in Header.tsx (lines 183-227) is a static "PR" button that always creates a new AI session with a prompt to run gh pr create. It has a target-branch dropdown next to it. After clicking, an AI session is created and the AI runs the command -- but the app has no awareness of whether a PR was actually created, its URL, or its state.
The merge functionality lives entirely in the sidebar (GitPushPull.tsx, lines 174-189) as a local git merge from a selected branch.
There is no connection between PR creation and subsequent merge/cleanup actions.
The PR button evolves through a state machine based on the worktree lifecycle:
PR Lifecycle State Machine (per worktree):
┌─────────┐ PR button ┌──────────────┐ AI outputs ┌─────────────┐
│ none │ ──── click ────→ │ creating │ ── PR URL ─────→ │ created │
└─────────┘ └──────────────┘ └─────────────┘
│ │
│ (clean tree + │
│ PR detected) Merge button
│ click (Cmd+G
│ does NOT
▼ trigger this)
┌─────────┐ │
│ created │ ◄────────────────────────────────────────────────────────┘
└─────────┘ │
│ │
│ gh pr merge │
│ succeeds ▼
│ ┌─────────────┐
└────────────────────────────────────────────────────────→│ merged │
└─────────────┘
│
Archive button
click
│
▼
┌─────────────┐
│ archived │
└─────────────┘
(worktree gone,
user switched
to no-worktree)
Header button appearance at each state:
none: [PR] → {target branch dropdown}
Standard outline button. Creates a PR session.
creating: [PR ⟳] → {target branch dropdown}
Disabled, spinner. AI session is running.
created: [Merge PR]
Green/success variant. Clean working tree required.
Dropdown hidden (target already set).
merged: [Archive]
Destructive/red variant. Archives the worktree.
PR detection mechanism:
When the PR button is clicked, the created session is tagged as a "PR session" in store state. As the AI streams its response, the renderer watches for GitHub PR URLs in the output (pattern: https://github.com/.+/pull/\d+). When detected, the PR number and URL are stored in useGitStore.prInfo (in-memory only -- does not survive app restart).
Merge operation (gh pr merge):
When the user clicks the "Merge PR" button:
- Run
gh pr merge <number> --merge(or--squash/--rebasebased on repo default) via a new IPC channel - On success, sync the local branch: use
git worktree listto find if any local worktree corresponds to the PR target branch. If found, rungit merge <our-branch>in that worktree's directory to fast-forward the local copy. - Transition the button to "Archive" state.
Archive operation:
The "Archive" button calls the existing archiveWorktree() from useWorktreeStore (lines 194-284), which handles killing processes, aborting sessions, deleting the worktree, and switching to no-worktree view.
A. Add PR state to useGitStore.ts:
// New types
interface PRInfo {
state: 'none' | 'creating' | 'created' | 'merged'
prNumber?: number
prUrl?: string
targetBranch?: string
sessionId?: string // The session that created the PR (for output watching)
}
// New state in useGitStore
prInfo: Map<string, PRInfo> // worktreeId → PRInfo
// New actions
setPrState: (worktreeId: string, info: PRInfo) => voidB. Add PR URL detection in session streaming output.
The session store or a dedicated hook should watch for PR URLs in the AI's streamed response. Only sessions created by the PR button (tracked via sessionId in PRInfo) should be monitored.
When a URL matching https://github.com/.+/pull/(\d+) is found in the session output:
- Extract the PR number
- Update
prInfofor the worktree tostate: 'created'with the PR number and URL
This can be implemented as a useEffect in Header.tsx or a dedicated usePRDetection hook that watches the active session's messages.
C. Add new IPC channel git:prMerge:
New handler in src/main/ipc/git-file-handlers.ts:
ipcMain.handle('git:prMerge', async (_event, worktreePath: string, prNumber: number) => {
try {
// Step 1: Merge the PR on GitHub
const mergeResult = await execPromise(`gh pr merge ${prNumber} --merge`, { cwd: worktreePath })
// Step 2: Sync local branch
// Find the PR's target branch
const prInfoResult = await execPromise(
`gh pr view ${prNumber} --json baseRefName -q '.baseRefName'`,
{ cwd: worktreePath }
)
const targetBranch = prInfoResult.stdout.trim()
// Check if any local worktree corresponds to the target branch
const worktreeListResult = await execPromise('git worktree list --porcelain', {
cwd: worktreePath
})
// Parse worktree list to find one on the target branch
const targetWorktreePath = parseWorktreeForBranch(worktreeListResult.stdout, targetBranch)
if (targetWorktreePath) {
// Merge our branch into the target branch's worktree
const currentBranch = await execPromise('git branch --show-current', { cwd: worktreePath })
await execPromise(`git merge ${currentBranch.stdout.trim()}`, { cwd: targetWorktreePath })
}
return { success: true }
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error)
}
}
})D. Add preload bridge for git:prMerge:
In src/preload/index.ts under gitOps:
prMerge: (worktreePath: string, prNumber: number) =>
ipcRenderer.invoke('git:prMerge', worktreePath, prNumber)Add type declaration in src/preload/index.d.ts.
E. Update Header.tsx PR button section (lines 183-227):
Replace the static PR button with a state-driven button:
{isGitHub && (() => {
const pr = prInfo?.get(selectedWorktreeId!)
const isCleanTree = /* derive from git status: no staged, unstaged, untracked */
if (pr?.state === 'merged') {
// Archive button
return (
<Button
size="sm"
variant="destructive"
className="h-7 text-xs"
onClick={handleArchiveWorktree}
data-testid="archive-button"
>
<Archive className="h-3.5 w-3.5 mr-1" />
Archive
</Button>
)
}
if (pr?.state === 'created' && isCleanTree) {
// Merge PR button
return (
<Button
size="sm"
variant="default"
className="h-7 text-xs bg-green-600 hover:bg-green-700"
onClick={handleMergePR}
data-testid="merge-pr-button"
>
<GitMerge className="h-3.5 w-3.5 mr-1" />
Merge PR
</Button>
)
}
// Default: PR creation button + target dropdown (existing UI)
return (
<>
<Button
size="sm"
variant="outline"
className="h-7 text-xs"
onClick={handleCreatePR}
disabled={isOperating || pr?.state === 'creating'}
data-testid="pr-button"
>
{pr?.state === 'creating' ? (
<Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" />
) : (
<GitPullRequest className="h-3.5 w-3.5 mr-1" />
)}
PR
</Button>
{/* existing target branch dropdown */}
</>
)
})()}F. Implement handleMergePR in Header.tsx:
const handleMergePR = useCallback(async () => {
if (!selectedWorktree?.path || !selectedWorktreeId) return
const pr = useGitStore.getState().prInfo.get(selectedWorktreeId)
if (!pr?.prNumber) return
try {
const result = await window.gitOps.prMerge(selectedWorktree.path, pr.prNumber)
if (result.success) {
toast.success('PR merged successfully')
useGitStore.getState().setPrState(selectedWorktreeId, {
...pr,
state: 'merged'
})
} else {
toast.error(`Merge failed: ${result.error}`)
}
} catch (error) {
toast.error('Failed to merge PR')
}
}, [selectedWorktree?.path, selectedWorktreeId])G. Implement handleArchiveWorktree in Header.tsx:
const handleArchiveWorktree = useCallback(async () => {
if (!selectedWorktreeId) return
await useWorktreeStore.getState().archiveWorktree(selectedWorktreeId)
}, [selectedWorktreeId])| File | Change |
|---|---|
src/renderer/src/stores/useGitStore.ts |
Add prInfo map, setPrState action |
src/renderer/src/components/layout/Header.tsx |
State-driven PR/Merge/Archive button, handleMergePR, handleArchiveWorktree |
src/renderer/src/hooks/usePRDetection.ts |
New file. Hook to watch session output for PR URLs |
src/main/ipc/git-file-handlers.ts |
Add git:prMerge handler with gh pr merge + local branch sync |
src/preload/index.ts |
Add prMerge to gitOps namespace |
src/preload/index.d.ts |
Add prMerge type declaration, add PRInfo type |
The app quit handling is in src/main/index.ts:
- Lines 356-360:
window-all-closedcallsapp.quit()on non-macOS - Lines 363-374:
will-quitruns cleanup (terminals, scripts, file watchers, OpenCode, database) - No
before-quithandler exists - No confirmation dialog is shown before quitting
Worktree loading state is tracked in the renderer (useWorktreeStore.isLoading, line 32). However, the concept of "worktrees in a loading state" likely refers to worktrees that have running AI sessions or scripts -- specifically, worktrees with active OpenCode/script processes, not just the store's isLoading boolean.
The relevant state is likely:
- Running scripts: tracked in
useScriptStoreor via terminal PTY processes - Active streaming sessions: tracked in
useSessionStoreviastreamingSessionIdsor similar - The
archivingWorktreeIdsset inuseWorktreeStore(worktrees currently being archived)
Quit confirmation dialog:
When the user presses Cmd+Q or closes the app window,
if any worktrees have active processes running:
┌────────────────────────────────────────────────┐
│ │
│ Are you sure you want to quit? │
│ │
│ You have pending worktrees running. │
│ Quitting now will terminate all active │
│ sessions and processes. │
│ │
│ [Cancel] [Quit Anyway] │
└────────────────────────────────────────────────┘
"Pending worktrees running" means:
- Any worktree has an active streaming AI session
- Any worktree has a running script process
If no worktrees are in a loading state, quit immediately
without showing the dialog.
A. Add IPC channel to query running worktree state from main process:
The main process needs to know if any worktrees are "busy". Two approaches:
Approach 1 (simpler): Add a before-quit handler in src/main/index.ts that sends an IPC message to the renderer asking "are any worktrees busy?" The renderer checks its stores and responds.
Approach 2 (recommended): The main process already tracks running processes:
- Terminal PTYs are tracked in terminal management code
- OpenCode connections are tracked in
opencode-service.ts - Scripts are tracked in script runner code
Check these in the before-quit handler directly without needing to query the renderer.
// In src/main/index.ts
let forceQuit = false
app.on('before-quit', (event) => {
if (forceQuit) return // User confirmed, proceed with quit
const hasRunningProcesses = checkForRunningProcesses()
if (hasRunningProcesses) {
event.preventDefault()
const mainWindow = getMainWindow()
if (!mainWindow) return
dialog
.showMessageBox(mainWindow, {
type: 'warning',
title: 'Quit Hive?',
message: 'Are you sure you want to quit?',
detail:
'You have pending worktrees running. Quitting now will terminate all active sessions and processes.',
buttons: ['Cancel', 'Quit Anyway'],
defaultId: 0,
cancelId: 0
})
.then(({ response }) => {
if (response === 1) {
forceQuit = true
app.quit()
}
})
}
})B. Implement checkForRunningProcesses():
This function checks the main process's own tracking of running terminals, OpenCode connections, and script processes. It returns true if any are active.
function checkForRunningProcesses(): boolean {
// Check for active terminal PTYs
const activeTerminals = getActiveTerminalCount()
// Check for active OpenCode connections that are streaming
const activeOpenCode = getActiveOpenCodeCount()
// Check for running scripts
const activeScripts = getActiveScriptCount()
return activeTerminals > 0 || activeOpenCode > 0 || activeScripts > 0
}The exact implementation depends on how these services expose their active connection counts. Each service should expose a simple getter for this.
C. Handle macOS dock-quit behavior:
On macOS, Cmd+Q and quitting from the dock both trigger before-quit. The window-close (close event on BrowserWindow) should also be intercepted for the same check when it's the last window.
| File | Change |
|---|---|
src/main/index.ts |
Add before-quit handler with dialog, add forceQuit flag |
src/main/services/opencode-service.ts |
Expose getActiveConnectionCount() or similar |
src/main/services/script-runner.ts (or equivalent) |
Expose getActiveScriptCount() if not already available |
src/main/services/terminal-service.ts (or equivalent) |
Expose getActiveTerminalCount() if not already available |
Keyboard shortcuts are defined in src/renderer/src/lib/keyboard-shortcuts.ts (lines 30-161) with 17 shortcuts across 6 categories. The handler mapping is in src/renderer/src/hooks/useKeyboardShortcuts.ts (lines 241-498).
The Git category currently has three shortcuts:
Cmd+Shift+C→ CommitCmd+Shift+P→ PushCmd+Shift+L→ Pull
The sidebar merge action lives in GitPushPull.tsx (lines 174-189) and calls window.gitOps.merge(worktreePath, mergeBranch).
New shortcut:
Cmd+G → Merge (sidebar merge action)
Behavior:
- Triggers the same action as clicking the "Merge" button in
the GitPushPull section of the right sidebar
- Uses whatever branch is currently selected in the merge
dropdown
- If no branch is selected, shows a toast: "Select a branch
to merge from first"
- If a merge is already in progress, the shortcut is ignored
- This does NOT trigger the PR merge from the header
Category: Git
Display: ⌘G
A. Add shortcut definition in keyboard-shortcuts.ts:
Add to the Git category in DEFAULT_SHORTCUTS:
{
id: 'merge',
label: 'Merge',
description: 'Merge selected branch',
category: 'Git',
defaultBinding: { key: 'g', meta: true }
}B. Add handler in useKeyboardShortcuts.ts (getShortcutHandlers):
The handler needs to read the current merge branch from GitPushPull's state. Since mergeBranch is local component state in GitPushPull, we need to either:
- Lift
mergeBranchstate to the store (recommended) -- move the selected merge branch intouseGitStoreso the shortcut handler can read it, or - Dispatch a custom event that
GitPushPulllistens for
Option 1 (recommended):
Add selectedMergeBranch: Map<string, string> to useGitStore (keyed by worktree path). GitPushPull reads/writes from this instead of local state. The shortcut handler can then:
{
shortcutId: 'merge',
handler: () => {
const worktreePath = getActiveWorktreePath()
if (!worktreePath) return
const mergeBranch = useGitStore.getState().selectedMergeBranch.get(worktreePath)
if (!mergeBranch) {
toast.error('Select a branch to merge from first')
return
}
// Call the same merge logic as GitPushPull.handleMerge
window.gitOps.merge(worktreePath, mergeBranch).then((result) => {
if (result.success) {
toast.success(`Merged ${mergeBranch}`)
// Refresh statuses
} else {
toast.error(`Merge failed: ${result.error}`)
}
})
}
}C. Register with menu if needed:
Add a menu item in the application menu under a "Git" submenu or "Actions" so the shortcut appears in the menu bar.
| File | Change |
|---|---|
src/renderer/src/lib/keyboard-shortcuts.ts |
Add merge shortcut definition to Git category |
src/renderer/src/hooks/useKeyboardShortcuts.ts |
Add merge handler to getShortcutHandlers |
src/renderer/src/stores/useGitStore.ts |
Add selectedMergeBranch map (lift from local state) |
src/renderer/src/components/git/GitPushPull.tsx |
Use selectedMergeBranch from store instead of local useState |
src/renderer/src/components/settings/SettingsShortcuts.tsx |
No change needed (auto-picks up new shortcut from DEFAULT_SHORTCUTS) |
The merge dropdown in GitPushPull.tsx (lines 242-341) lets the user select a branch to merge from. The merge button (lines 331-340) is a static "Merge" button that is disabled only when merging, operating, or no branch is selected.
There is no check for whether the selected branch has already been fully merged into the current branch (i.e., the branches are up-to-date).
When selecting a branch from the merge dropdown:
Case 1: Branch has unmerged changes (normal)
┌─────────────────────────────────────────────┐
│ Merge from: [main ▼] [Merge] │
└─────────────────────────────────────────────┘
Standard outline button.
Case 2: Branch is already merged / up-to-date
┌──────────────────────────────────────────────┐
│ Merge from: [main ▼] [Archive] │
└──────────────────────────────────────────────┘
Destructive/red variant button. Clicking archives
the current worktree directly (no confirmation dialog
since user explicitly chose this).
Detection: Run `git merge-base --is-ancestor <merge-from> HEAD`
If exit code 0, the selected branch is already an ancestor
of HEAD (meaning all its changes are already in our branch).
Also check the reverse: if our HEAD is already an ancestor
of the merge-from branch, we're also "up to date" in the
sense that a merge would be a no-op or fast-forward of
already-integrated work.
The check runs every time the merge dropdown selection changes.
A. Add new IPC channel git:isBranchMerged:
In src/main/ipc/git-file-handlers.ts:
ipcMain.handle('git:isBranchMerged', async (_event, worktreePath: string, branch: string) => {
try {
// Check if the selected branch is an ancestor of HEAD
// (all its commits are already in our branch)
await execPromise(`git merge-base --is-ancestor ${branch} HEAD`, { cwd: worktreePath })
return { success: true, isMerged: true }
} catch {
// Non-zero exit code means branch is NOT an ancestor
return { success: true, isMerged: false }
}
})B. Add preload bridge:
isBranchMerged: (worktreePath: string, branch: string) =>
ipcRenderer.invoke('git:isBranchMerged', worktreePath, branch)C. Update GitPushPull.tsx:
Add state and effect to check merge status when branch selection changes:
const [isBranchMerged, setIsBranchMerged] = useState(false)
useEffect(() => {
if (!worktreePath || !mergeBranch) {
setIsBranchMerged(false)
return
}
window.gitOps.isBranchMerged(worktreePath, mergeBranch).then((result) => {
if (result.success) {
setIsBranchMerged(result.isMerged)
}
})
}, [worktreePath, mergeBranch])Replace the merge button conditionally:
{
isBranchMerged ? (
<Button
variant="destructive"
size="sm"
className="h-6 text-xs whitespace-nowrap"
onClick={handleArchiveWorktree}
data-testid="archive-merged-button"
>
Archive
</Button>
) : (
<Button
variant="outline"
size="sm"
className="h-6 text-xs whitespace-nowrap"
onClick={handleMerge}
disabled={isMerging || isOperating || !mergeBranch.trim()}
data-testid="merge-button"
>
{isMerging ? <Loader2 className="h-3 w-3 animate-spin" /> : 'Merge'}
</Button>
)
}D. Implement handleArchiveWorktree in GitPushPull.tsx:
const handleArchiveWorktree = useCallback(async () => {
const worktreeId = useWorktreeStore.getState().selectedWorktreeId
if (!worktreeId) return
await useWorktreeStore.getState().archiveWorktree(worktreeId)
}, [])No confirmation dialog since the user explicitly sees "Archive" and clicks it, and the branch is confirmed merged.
| File | Change |
|---|---|
src/main/ipc/git-file-handlers.ts |
Add git:isBranchMerged handler |
src/preload/index.ts |
Add isBranchMerged to gitOps |
src/preload/index.d.ts |
Add isBranchMerged type declaration |
src/renderer/src/components/git/GitPushPull.tsx |
Add merged check, swap merge/archive button, add handleArchiveWorktree |
- Click an untracked
.mdfile in Changes → verify markdown preview opens (not syntax-highlighted source) - Click an untracked
.tsfile in Changes → verify it opens in FileViewer with syntax highlighting - Click a staged
.mdfile (statusA) in Changes → verify markdown preview opens - Click a modified
.mdfile in Changes → verify diff view still opens (not file viewer) - Verify file tab appears in the tab bar with correct file name
- Verify source/preview toggle works for markdown files opened this way
- Click PR button → verify AI session is created with PR prompt
- After AI outputs a PR URL → verify button changes to "Merge PR" (when tree is clean)
- Click "Merge PR" → verify
gh pr mergeis called, button transitions to "Archive" - Click "Archive" → verify worktree is archived and view switches to no-worktree
- Verify local branch sync: if a worktree exists for the target branch, verify
git mergeruns there - Verify button stays as "PR" when tree has uncommitted changes even after PR is created
- Verify the PR button resets to "PR" on app restart (in-memory only)
- With running AI session, press Cmd+Q → verify confirmation dialog appears
- Click "Cancel" → verify app does not quit
- Click "Quit Anyway" → verify app quits and cleanup runs
- With no running processes, press Cmd+Q → verify app quits immediately (no dialog)
- Test macOS dock quit and window close button both trigger the check
- Select a branch in merge dropdown, press Cmd+G → verify merge executes
- With no branch selected, press Cmd+G → verify toast "Select a branch to merge from first"
- During an active merge, press Cmd+G → verify shortcut is ignored
- Verify shortcut appears in Settings > Shortcuts under Git category
- Verify shortcut is customizable
- Select a branch that is already merged → verify "Archive" button appears (red/destructive)
- Select a branch with unmerged changes → verify normal "Merge" button appears
- Click "Archive" on merged branch → verify worktree archives without confirmation dialog
- Change branch selection → verify button swaps correctly between merge and archive
- Verify the check runs on each branch selection change (not just initial load)
-
PR merge method: Should the merge button offer a choice between merge, squash, and rebase? Or should it use the repository's default merge method? (Current design: use
--mergeflag. Could be enhanced to respect repo settings.) -
PR session detection edge cases: What if the AI fails to create the PR (e.g.,
ghnot installed, auth failure)? The button would stay in "creating" state. Need a timeout or error detection to reset to "none". -
Multiple PRs per worktree: If a user creates a PR, it fails, and they create another -- the detection should use the latest session. The current design handles this by overwriting
prInfostate.