Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d64f78a
Add git integration: branch picker + worktrees
juliusmarminge Feb 9, 2026
3047974
Add branch/env picker toolbar and create branch flow
juliusmarminge Feb 9, 2026
5944859
Add git checkout support and clear errors on success
juliusmarminge Feb 9, 2026
69e38a2
Fix typecheck errors for branch/worktree fields
juliusmarminge Feb 9, 2026
8225352
Add branch/worktreePath to persistence test expectation
juliusmarminge Feb 9, 2026
e9cfe37
Add light mode support for git branch picker and default branch label
juliusmarminge Feb 9, 2026
f47dc3c
Extract git functions and add integration tests
juliusmarminge Feb 9, 2026
7e20148
resource management
juliusmarminge Feb 9, 2026
2a610f9
kewl
juliusmarminge Feb 9, 2026
c5cbe15
Fix "Open in editor" to respect worktree path
juliusmarminge Feb 9, 2026
6ce773f
Fix branch query cwd for worktree threads and error banner light mode
juliusmarminge Feb 9, 2026
6474490
Add test for listGitBranches from worktree cwd
juliusmarminge Feb 9, 2026
d1ecacd
Polish git worktree helpers after rebase
juliusmarminge Feb 10, 2026
c13e9f8
Restore git toolbar via websocket backend
juliusmarminge Feb 10, 2026
79ac59f
eh
juliusmarminge Feb 10, 2026
9fc5ad2
plans
juliusmarminge Feb 10, 2026
d6437a7
Move toolbar below chat input, fix electron preload bundling, and upd…
juliusmarminge Feb 11, 2026
74f6ce8
Fix CI typecheck: point contracts types to source and remove dead code
juliusmarminge Feb 11, 2026
4ab095f
Fix CI typecheck: build contracts before typechecking dependents
juliusmarminge Feb 11, 2026
e2052b8
Fix dev:desktop task graph to match main's env var patterns
juliusmarminge Feb 11, 2026
ceceba2
fix
juliusmarminge Feb 11, 2026
ca356d0
Fix server build: add zod to noExternal to prevent tsdown error
juliusmarminge Feb 11, 2026
01c3e82
Suppress tsdown inlineOnly warning for server build
juliusmarminge Feb 11, 2026
7e5cd01
Polish turbo task graph and dev scripts
juliusmarminge Feb 11, 2026
6e3726e
Merge remote-tracking branch 'origin/main' into juliusmarminge/git-in…
juliusmarminge Feb 11, 2026
198cc58
Fix envMode reset bug and use shell-free git spawn
juliusmarminge Feb 11, 2026
807e3d1
Add react-query, refactor BranchToolbar async state, and fix stale deps
juliusmarminge Feb 11, 2026
8324949
Enrich branches with worktree paths, fix toolbar display, and improve…
juliusmarminge Feb 11, 2026
5b7af7b
Fix session not restarting when switching to a worktree branch
juliusmarminge Feb 11, 2026
d6ecf15
Fix worktree→local branch switch causing 'already used by worktree' e…
juliusmarminge Feb 11, 2026
cf66f53
Fix flash of 'Initialize git' when switching branch/worktree
juliusmarminge Feb 11, 2026
9442019
Filter out stale worktree paths from branch listing
juliusmarminge Feb 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions .plans/branch-environment-picker-in-chatview-input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Branch/Environment Picker in ChatView Input

## Summary

Add a secondary toolbar below the ChatView input area (similar to Codex UI) that lets users select the target branch and environment mode (Local vs New worktree) before sending their first message.

## UX

- A toolbar appears **below** the input form (always visible when it's a git repo)
- Two controls:
1. **Environment mode** (left side): toggles between "Local" and "New worktree" — **locked after first message** (no longer clickable, just shows current mode as label)
2. **Branch picker** (right side): dropdown showing local branches — **always changeable**, even after messages are sent
- If not a git repo, the toolbar is hidden entirely (thread uses project cwd as-is)

## Changes

### 0. Install `@tanstack/react-query` in `apps/renderer`

Add dependency + wrap app in `QueryClientProvider`.

### 1. `apps/renderer/src/store.ts` — MODIFY

Add a new action to the reducer:

```ts
| { type: "SET_THREAD_BRANCH"; threadId: string; branch: string | null; worktreePath: string | null }
```

Reducer case updates `branch` and `worktreePath` on the thread.

### 2. `apps/renderer/src/components/ChatView.tsx` — MODIFY

**Fetch branches** via `useQuery`:

```ts
const branchQuery = useQuery({
queryKey: ["git-branches", activeProject?.cwd],
queryFn: () => api.git.listBranches({ cwd: activeProject!.cwd }),
enabled: !!activeProject,
});
```

**Local state:**

- `envMode: "local" | "worktree"` — environment mode (local component state)

**UI:** Below the `<form>`, render a toolbar bar (hidden if `!branchQuery.data?.isRepo`):

- Left side: env mode button ("Local" / "New worktree") — disabled after first message (locked in)
- Right side: branch dropdown from `branchQuery.data.branches`
- Both styled like existing model picker (small text, chevron, dropdown menus)

**Behavior:**

- Branch picker is always active — changing branch dispatches `SET_THREAD_BRANCH` immediately
- Env mode is only clickable when `activeThread.messages.length === 0`. After first message, it becomes a static label showing the locked-in mode
- On first send (`onSend`): if `envMode === "worktree"` and a branch is selected, call `api.git.createWorktree` before starting the session, then dispatch `SET_THREAD_BRANCH` with the worktreePath
- `ensureSession` already uses `activeThread.worktreePath ?? activeProject.cwd`

### Files to modify

1. `apps/renderer/package.json` — add `@tanstack/react-query`
2. `apps/renderer/src/main.tsx` (or App entry) — wrap in `QueryClientProvider`
3. `apps/renderer/src/store.ts` — add `SET_THREAD_BRANCH` action
4. `apps/renderer/src/components/ChatView.tsx` — branch/env picker UI with `useQuery`

## Verification

1. `turbo build` — compiles
2. Create a new thread → branch bar appears below input with "Local" + current branch
3. Change branch in dropdown → branch updates on thread
4. Toggle "New worktree" → send message → worktree created, session uses worktree cwd
5. After first message: env mode label locks to "Worktree" (not clickable), branch picker still works
6. Non-git project → no branch bar shown
99 changes: 99 additions & 0 deletions .plans/git-flows-integration-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Git Flows Integration Tests

## Overview

Real integration tests that run actual git commands against temporary repos. No mocking.

## Step 1: Extract git functions into `apps/desktop/src/git.ts`

The git functions (`listGitBranches`, `createGitWorktree`, `removeGitWorktree`, `createGitBranch`, `checkoutGitBranch`, `initGitRepo`) and their helper `runTerminalCommand` are currently private in `main.ts`. Extract them into a new `apps/desktop/src/git.ts` module with named exports.

`main.ts` will import and re-use them — no behavior change, just moving code.

**Files modified:**

- `apps/desktop/src/git.ts` — new file with all git functions exported
- `apps/desktop/src/main.ts` — import from `./git` instead of defining inline

## Step 2: Create `apps/desktop/src/git.test.ts`

Integration tests using real temp git repos. Each test group creates a fresh temp directory with `git init`, makes commits, creates branches as needed, and cleans up after.

### Setup/teardown pattern

```ts
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
listGitBranches,
createGitBranch,
checkoutGitBranch,
createGitWorktree,
removeGitWorktree,
initGitRepo,
} from "./git";

// Helper: run a raw git command in a dir (for test setup, not under test)
// Helper: create an initial commit (git needs at least one commit for branches)
```

### Test groups

**1. initGitRepo**

- Creates a valid git repo in a temp dir
- listGitBranches reports `isRepo: true` after init

**2. listGitBranches**

- Returns `isRepo: false` for non-git directory
- Returns the current branch with `current: true`
- Sorts current branch first
- Lists multiple branches after creating them
- `isDefault` is false when no remote (no origin/HEAD)

**3. checkoutGitBranch**

- Checks out an existing branch (current flag moves)
- Throws when branch doesn't exist
- Throws when checkout would overwrite uncommitted changes (dirty working tree)

**4. createGitBranch**

- Creates a new branch (appears in listGitBranches)
- Throws when branch already exists

**5. createGitWorktree + removeGitWorktree**

- Creates a worktree directory at the expected path
- Worktree has the correct branch checked out
- Throws when branch is already checked out in another worktree
- removeGitWorktree cleans up the worktree

**6. Full flow: local branch checkout**

- init → commit → create branch → checkout → verify current

**7. Full flow: worktree creation from selected branch**

- init → commit → create branch → create worktree → verify worktree dir exists and has correct branch

**8. Full flow: thread switching simulation**

- init → commit → create branch-a, branch-b → checkout a → checkout b → checkout a → verify current matches

**9. Full flow: checkout conflict**

- init → commit → create branch → modify file (unstaged) → checkout other branch → expect error

## Verification

```bash
# Run the git integration tests
cd apps/desktop && bun run test

# Or just the git test file
npx vitest run apps/desktop/src/git.test.ts
```
Comment on lines +93 to +99
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use bunx instead of npx in the verification snippet.
This keeps docs consistent with the Bun-only tooling standard.

✏️ Proposed doc fix
-# Or just the git test file
-npx vitest run apps/desktop/src/git.test.ts
+# Or just the git test file
+bunx vitest run apps/desktop/src/git.test.ts

Based on learnings: Use Bun as the package manager. Use bunx <BIN> when executing scripts from npm packages not installed as a project dependency.

🤖 Prompt for AI Agents
In @.plans/git-flows-integration-tests.md around lines 93 - 99, The
documentation uses "npx vitest run apps/desktop/src/git.test.ts" which
contradicts the Bun-centric tooling elsewhere; update the verification snippet
to use Bun's runner by replacing the "npx" invocation with "bunx" (so the second
command uses bunx vitest run apps/desktop/src/git.test.ts) and keep the existing
"cd apps/desktop && bun run test" line unchanged; ensure both examples
consistently recommend Bun tooling.

103 changes: 103 additions & 0 deletions .plans/git-flows-test-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Git Flows Test Plan

## Overview

Add tests for git branch/worktree flows. Two files:

1. **Extend** `apps/renderer/src/store.test.ts` — reducer tests for `SET_THREAD_BRANCH`
2. **Create** `apps/renderer/src/git-flows.test.ts` — flow logic tests

All tests are pure Vitest unit tests (no React rendering). They test the reducer directly and simulate handler logic via sequential reducer dispatches + mocked API calls.

## File 1: `apps/renderer/src/store.test.ts` (extend)

Add `describe("SET_THREAD_BRANCH reducer")` with 6 tests:

- Sets branch + worktreePath atomically
- Clears both to null
- Updates branch while preserving worktreePath
- Does not affect other threads (multi-thread state)
- No-op for nonexistent thread id
- Does not mutate messages, error, or session fields

Uses existing `makeThread`, `makeState` factories.

## File 2: `apps/renderer/src/git-flows.test.ts` (new)

### Factories

- `makeThread()`, `makeState()`, `makeSession()` — same pattern as store.test.ts
- `makeBranch()` — creates `GitBranch` objects
- `makeMessage()` — creates `ChatMessage` objects
- `makeGitApi()` — returns `{ checkout, createWorktree, createBranch, listBranches }` with `vi.fn()` mocks

### Test groups (~30 tests total)

**1. Local branch checkout flow** (2 tests)

- Successful checkout → SET_THREAD_BRANCH updates branch
- Checkout failure → SET_ERROR, branch unchanged

**2. Thread branch conflict on send** (3 tests)

- Two threads maintain independent branch state after SET_ACTIVE_THREAD
- Branch state preserved through multiple thread switches + updates
- Checkout failure on thread switch sets error only on target thread

**3. Worktree creation on send** (5 tests)

- First message in worktree mode → createWorktree → SET_THREAD_BRANCH with worktreePath
- No worktree when messages already exist
- No worktree in local envMode
- No worktree when worktreePath already set
- createWorktree failure → SET_ERROR, send aborted, no messages pushed

**4. Env mode locking** (4 tests)

- envLocked=false when no messages
- envLocked=true with messages
- Transitions false→true after PUSH_USER_MESSAGE
- Remains true after SET_ERROR and UPDATE_SESSION

**5. Auto-fill current branch** (3 tests)

- Dispatches SET_THREAD_BRANCH when thread has no branch and current branch exists
- Does not overwrite existing branch
- No-op when no branch is marked current

**6. Default branch detection** (2 tests)

- isDefault flag on branch objects
- current and isDefault can be on different branches

**7. Branch creation + checkout** (3 tests)

- Successful create + checkout updates branch
- createBranch failure → error, branch unchanged
- checkout failure after successful create → error, branch unchanged

**8. Session CWD resolution** (3 tests)

- Uses worktreePath when available
- cwdOverride takes precedence over worktreePath
- Falls back to project cwd when no worktree

**9. Error handling patterns** (4 tests)

- SET_ERROR sets error on correct thread
- SET_ERROR with null clears error
- Error on one thread doesn't affect others
- Error cleared before successful branch operations

## Verification

```bash
# Run all renderer tests
cd apps/renderer && bun run test

# Run just the new test file
npx vitest run apps/renderer/src/git-flows.test.ts

# Run just the store tests
npx vitest run apps/renderer/src/store.test.ts
```
115 changes: 115 additions & 0 deletions .plans/git-integration-branch-picker-worktrees.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Git Integration: Branch Picker + Worktrees

## Summary

Add git integration to let users start new threads from a specific branch, optionally creating a git worktree for isolated agent work.

## UX Flow

- **Left click** "+ New thread" → immediately creates a thread (current behavior, unchanged)
- **Right click** "+ New thread" → opens a context menu with git options:
- List of local branches → clicking one creates a thread on that branch (uses project cwd)
- Each branch has a "worktree" sub-option → creates a worktree, then creates thread with worktree as cwd
- When thread has a worktree, the agent session uses the worktree path as its cwd
- If git fails (not a repo), context menu shows "Not a git repository" disabled item

## Changes

### 1. `packages/contracts/src/git.ts` — CREATE

New Zod schemas and types:

- `gitListBranchesInputSchema` — `{ cwd: string }`
- `gitCreateWorktreeInputSchema` — `{ cwd: string, branch: string, path?: string }`
- `gitRemoveWorktreeInputSchema` — `{ cwd: string, path: string }`
- `gitBranchSchema` — `{ name: string, current: boolean }`
- Result types for each

### 2. `packages/contracts/src/ipc.ts` — MODIFY

- Add 3 IPC channels: `git:list-branches`, `git:create-worktree`, `git:remove-worktree`
- Add `git` namespace to `NativeApi` with `listBranches`, `createWorktree`, `removeWorktree`

### 3. `packages/contracts/src/index.ts` — MODIFY

- Add `export * from "./git"`

### 4. `apps/desktop/src/main.ts` — MODIFY

Add 3 IPC handlers + helper functions:

- `listGitBranches()` — runs `git branch --no-color`, parses output into `{ name, current }[]`
- `createGitWorktree()` — runs `git worktree add <path> <branch>`, defaults path to `../{repo}-worktrees/{branch}`
- `removeGitWorktree()` — runs `git worktree remove <path>`

Reuses existing `runTerminalCommand()`.

### 5. `apps/desktop/src/preload.ts` — MODIFY

Add `git` namespace with 3 `ipcRenderer.invoke` calls.

### 6. `apps/renderer/src/types.ts` — MODIFY

Add to `Thread`:

```
branch: string | null
worktreePath: string | null
```

### 7. `apps/renderer/src/persistenceSchema.ts` — MODIFY

- Add optional `branch`/`worktreePath` to persisted thread schema (`.nullable().optional()` for backwards compat)
- Add V3 schema, update union
- Update `hydrateThread` to default new fields to `null`
- Update `toPersistedState` to serialize new fields

### 8. `apps/renderer/src/store.ts` — MODIFY

- Update persisted state key to v3, keep v2 as legacy fallback

### 9. `apps/renderer/src/components/Sidebar.tsx` — MODIFY (main UI work)

- Keep existing left-click `handleNewThread` unchanged (immediate thread creation)
- Add `onContextMenu` handler to "+ New thread" buttons (both global and per-project)
- On right-click: fetch branches via `api.git.listBranches`, show a custom context menu
- Context menu items: branch names, each with a nested option to create with worktree
- Clicking a branch → creates thread with `branch` set, title = branch name
- Clicking "with worktree" → calls `api.git.createWorktree` first, then creates thread with `worktreePath`
- Show branch badge on thread list items
- If not a git repo, show "Not a git repository" as disabled menu item

Context menu component: a positioned `<div>` with `position: fixed` anchored to the click position, dismissed on click-outside or Escape. Follows the existing dropdown pattern from ChatView's model picker.

### 10. `apps/renderer/src/components/ChatView.tsx` — MODIFY

- Line 157: use `activeThread.worktreePath ?? activeProject.cwd` as session cwd
- Show branch/worktree badge in header bar

## Implementation Order

1. `packages/contracts/src/git.ts` (new schemas)
2. `packages/contracts/src/ipc.ts` + `index.ts` (wire up channels)
3. `apps/desktop/src/main.ts` (git command handlers)
4. `apps/desktop/src/preload.ts` (bridge methods)
5. `apps/renderer/src/types.ts` (Thread type update)
6. `apps/renderer/src/persistenceSchema.ts` + `store.ts` (persistence migration)
7. `apps/renderer/src/components/Sidebar.tsx` (branch picker UI)
8. `apps/renderer/src/components/ChatView.tsx` (worktree cwd + badge)

## Edge Cases

- **Not a git repo**: `git branch` fails → context menu shows "Not a git repository" disabled item
- **Branch has slashes**: `feature/foo` → worktree dir becomes `feature-foo`
- **Worktree exists**: git error surfaces to user via inline error message in context menu
- **No persistence breakage**: `.nullable().optional()` fields parse fine with old data

## Verification

1. `turbo build` — confirm contracts/desktop/renderer all compile
2. Launch app, add a project pointing to a git repo
3. Click "+ New thread" → verify branch list loads
4. Select a branch, click Start → thread created with branch in title
5. Enable worktree checkbox, pick branch, Start → verify worktree directory created on disk
6. Send a message in worktree thread → verify agent runs in worktree cwd
7. Add a non-git project → verify graceful error, can still create thread
Loading