Unified · Split
| @@ -0,0 +1,74 @@ | ||
| 1 | +# Branch/Environment Picker in ChatView Input | |
| 2 | + | |
| 3 | +## Summary | |
| 4 | + | |
| 5 | +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. | |
| 6 | + | |
| 7 | +## UX | |
| 8 | + | |
| 9 | +- A toolbar appears **below** the input form (always visible when it's a git repo) | |
| 10 | +- Two controls: | |
| 11 | + 1. **Environment mode** (left side): toggles between "Local" and "New worktree" — **locked after first message** (no longer clickable, just shows current mode as label) | |
| 12 | + 2. **Branch picker** (right side): dropdown showing local branches — **always changeable**, even after messages are sent | |
| 13 | +- If not a git repo, the toolbar is hidden entirely (thread uses project cwd as-is) | |
| 14 | + | |
| 15 | +## Changes | |
| 16 | + | |
| 17 | +### 0. Install `@tanstack/react-query` in `apps/renderer` | |
| 18 | + | |
| 19 | +Add dependency + wrap app in `QueryClientProvider`. | |
| 20 | + | |
| 21 | +### 1. `apps/renderer/src/store.ts` — MODIFY | |
| 22 | + | |
| 23 | +Add a new action to the reducer: | |
| 24 | + | |
| 25 | +```ts | |
| 26 | +| { type: "SET_THREAD_BRANCH"; threadId: string; branch: string | null; worktreePath: string | null } | |
| 27 | +``` | |
| 28 | + | |
| 29 | +Reducer case updates `branch` and `worktreePath` on the thread. | |
| 30 | + | |
| 31 | +### 2. `apps/renderer/src/components/ChatView.tsx` — MODIFY | |
| 32 | + | |
| 33 | +**Fetch branches** via `useQuery`: | |
| 34 | + | |
| 35 | +```ts | |
| 36 | +const branchQuery = useQuery({ | |
| 37 | + queryKey: ["git-branches", activeProject?.cwd], | |
| 38 | + queryFn: () => api.git.listBranches({ cwd: activeProject!.cwd }), | |
| 39 | + enabled: !!activeProject, | |
| 40 | +}); | |
| 41 | +``` | |
| 42 | + | |
| 43 | +**Local state:** | |
| 44 | + | |
| 45 | +- `envMode: "local" | "worktree"` — environment mode (local component state) | |
| 46 | + | |
| 47 | +**UI:** Below the `<form>`, render a toolbar bar (hidden if `!branchQuery.data?.isRepo`): | |
| 48 | + | |
| 49 | +- Left side: env mode button ("Local" / "New worktree") — disabled after first message (locked in) | |
| 50 | +- Right side: branch dropdown from `branchQuery.data.branches` | |
| 51 | +- Both styled like existing model picker (small text, chevron, dropdown menus) | |
| 52 | + | |
| 53 | +**Behavior:** | |
| 54 | + | |
| 55 | +- Branch picker is always active — changing branch dispatches `SET_THREAD_BRANCH` immediately | |
| 56 | +- Env mode is only clickable when `activeThread.messages.length === 0`. After first message, it becomes a static label showing the locked-in mode | |
| 57 | +- 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 | |
| 58 | +- `ensureSession` already uses `activeThread.worktreePath ?? activeProject.cwd` | |
| 59 | + | |
| 60 | +### Files to modify | |
| 61 | + | |
| 62 | +1. `apps/renderer/package.json` — add `@tanstack/react-query` | |
| 63 | +2. `apps/renderer/src/main.tsx` (or App entry) — wrap in `QueryClientProvider` | |
| 64 | +3. `apps/renderer/src/store.ts` — add `SET_THREAD_BRANCH` action | |
| 65 | +4. `apps/renderer/src/components/ChatView.tsx` — branch/env picker UI with `useQuery` | |
| 66 | + | |
| 67 | +## Verification | |
| 68 | + | |
| 69 | +1. `turbo build` — compiles | |
| 70 | +2. Create a new thread → branch bar appears below input with "Local" + current branch | |
| 71 | +3. Change branch in dropdown → branch updates on thread | |
| 72 | +4. Toggle "New worktree" → send message → worktree created, session uses worktree cwd | |
| 73 | +5. After first message: env mode label locks to "Worktree" (not clickable), branch picker still works | |
| 74 | +6. Non-git project → no branch bar shown | |
| @@ -0,0 +1,99 @@ | ||
| 1 | +# Git Flows Integration Tests | |
| 2 | + | |
| 3 | +## Overview | |
| 4 | + | |
| 5 | +Real integration tests that run actual git commands against temporary repos. No mocking. | |
| 6 | + | |
| 7 | +## Step 1: Extract git functions into `apps/desktop/src/git.ts` | |
| 8 | + | |
| 9 | +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. | |
| 10 | + | |
| 11 | +`main.ts` will import and re-use them — no behavior change, just moving code. | |
| 12 | + | |
| 13 | +**Files modified:** | |
| 14 | + | |
| 15 | +- `apps/desktop/src/git.ts` — new file with all git functions exported | |
| 16 | +- `apps/desktop/src/main.ts` — import from `./git` instead of defining inline | |
| 17 | + | |
| 18 | +## Step 2: Create `apps/desktop/src/git.test.ts` | |
| 19 | + | |
| 20 | +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. | |
| 21 | + | |
| 22 | +### Setup/teardown pattern | |
| 23 | + | |
| 24 | +```ts | |
| 25 | +import { mkdtemp, rm, writeFile } from "node:fs/promises"; | |
| 26 | +import { tmpdir } from "node:os"; | |
| 27 | +import path from "node:path"; | |
| 28 | +import { afterEach, beforeEach, describe, expect, it } from "vitest"; | |
| 29 | +import { | |
| 30 | + listGitBranches, | |
| 31 | + createGitBranch, | |
| 32 | + checkoutGitBranch, | |
| 33 | + createGitWorktree, | |
| 34 | + removeGitWorktree, | |
| 35 | + initGitRepo, | |
| 36 | +} from "./git"; | |
| 37 | + | |
| 38 | +// Helper: run a raw git command in a dir (for test setup, not under test) | |
| 39 | +// Helper: create an initial commit (git needs at least one commit for branches) | |
| 40 | +``` | |
| 41 | + | |
| 42 | +### Test groups | |
| 43 | + | |
| 44 | +**1. initGitRepo** | |
| 45 | + | |
| 46 | +- Creates a valid git repo in a temp dir | |
| 47 | +- listGitBranches reports `isRepo: true` after init | |
| 48 | + | |
| 49 | +**2. listGitBranches** | |
| 50 | + | |
| 51 | +- Returns `isRepo: false` for non-git directory | |
| 52 | +- Returns the current branch with `current: true` | |
| 53 | +- Sorts current branch first | |
| 54 | +- Lists multiple branches after creating them | |
| 55 | +- `isDefault` is false when no remote (no origin/HEAD) | |
| 56 | + | |
| 57 | +**3. checkoutGitBranch** | |
| 58 | + | |
| 59 | +- Checks out an existing branch (current flag moves) | |
| 60 | +- Throws when branch doesn't exist | |
| 61 | +- Throws when checkout would overwrite uncommitted changes (dirty working tree) | |
| 62 | + | |
| 63 | +**4. createGitBranch** | |
| 64 | + | |
| 65 | +- Creates a new branch (appears in listGitBranches) | |
| 66 | +- Throws when branch already exists | |
| 67 | + | |
| 68 | +**5. createGitWorktree + removeGitWorktree** | |
| 69 | + | |
| 70 | +- Creates a worktree directory at the expected path | |
| 71 | +- Worktree has the correct branch checked out | |
| 72 | +- Throws when branch is already checked out in another worktree | |
| 73 | +- removeGitWorktree cleans up the worktree | |
| 74 | + | |
| 75 | +**6. Full flow: local branch checkout** | |
| 76 | + | |
| 77 | +- init → commit → create branch → checkout → verify current | |
| 78 | + | |
| 79 | +**7. Full flow: worktree creation from selected branch** | |
| 80 | + | |
| 81 | +- init → commit → create branch → create worktree → verify worktree dir exists and has correct branch | |
| 82 | + | |
| 83 | +**8. Full flow: thread switching simulation** | |
| 84 | + | |
| 85 | +- init → commit → create branch-a, branch-b → checkout a → checkout b → checkout a → verify current matches | |
| 86 | + | |
| 87 | +**9. Full flow: checkout conflict** | |
| 88 | + | |
| 89 | +- init → commit → create branch → modify file (unstaged) → checkout other branch → expect error | |
| 90 | + | |
| 91 | +## Verification | |
| 92 | + | |
| 93 | +```bash | |
| 94 | +# Run the git integration tests | |
| 95 | +cd apps/desktop && bun run test | |
| 96 | + | |
| 97 | +# Or just the git test file | |
| 98 | +npx vitest run apps/desktop/src/git.test.ts | |
| 99 | +``` | |
| @@ -0,0 +1,103 @@ | ||
| 1 | +# Git Flows Test Plan | |
| 2 | + | |
| 3 | +## Overview | |
| 4 | + | |
| 5 | +Add tests for git branch/worktree flows. Two files: | |
| 6 | + | |
| 7 | +1. **Extend** `apps/renderer/src/store.test.ts` — reducer tests for `SET_THREAD_BRANCH` | |
| 8 | +2. **Create** `apps/renderer/src/git-flows.test.ts` — flow logic tests | |
| 9 | + | |
| 10 | +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. | |
| 11 | + | |
| 12 | +## File 1: `apps/renderer/src/store.test.ts` (extend) | |
| 13 | + | |
| 14 | +Add `describe("SET_THREAD_BRANCH reducer")` with 6 tests: | |
| 15 | + | |
| 16 | +- Sets branch + worktreePath atomically | |
| 17 | +- Clears both to null | |
| 18 | +- Updates branch while preserving worktreePath | |
| 19 | +- Does not affect other threads (multi-thread state) | |
| 20 | +- No-op for nonexistent thread id | |
| 21 | +- Does not mutate messages, error, or session fields | |
| 22 | + | |
| 23 | +Uses existing `makeThread`, `makeState` factories. | |
| 24 | + | |
| 25 | +## File 2: `apps/renderer/src/git-flows.test.ts` (new) | |
| 26 | + | |
| 27 | +### Factories | |
| 28 | + | |
| 29 | +- `makeThread()`, `makeState()`, `makeSession()` — same pattern as store.test.ts | |
| 30 | +- `makeBranch()` — creates `GitBranch` objects | |
| 31 | +- `makeMessage()` — creates `ChatMessage` objects | |
| 32 | +- `makeGitApi()` — returns `{ checkout, createWorktree, createBranch, listBranches }` with `vi.fn()` mocks | |
| 33 | + | |
| 34 | +### Test groups (~30 tests total) | |
| 35 | + | |
| 36 | +**1. Local branch checkout flow** (2 tests) | |
| 37 | + | |
| 38 | +- Successful checkout → SET_THREAD_BRANCH updates branch | |
| 39 | +- Checkout failure → SET_ERROR, branch unchanged | |
| 40 | + | |
| 41 | +**2. Thread branch conflict on send** (3 tests) | |
| 42 | + | |
| 43 | +- Two threads maintain independent branch state after SET_ACTIVE_THREAD | |
| 44 | +- Branch state preserved through multiple thread switches + updates | |
| 45 | +- Checkout failure on thread switch sets error only on target thread | |
| 46 | + | |
| 47 | +**3. Worktree creation on send** (5 tests) | |
| 48 | + | |
| 49 | +- First message in worktree mode → createWorktree → SET_THREAD_BRANCH with worktreePath | |
| 50 | +- No worktree when messages already exist | |
| 51 | +- No worktree in local envMode | |
| 52 | +- No worktree when worktreePath already set | |
| 53 | +- createWorktree failure → SET_ERROR, send aborted, no messages pushed | |
| 54 | + | |
| 55 | +**4. Env mode locking** (4 tests) | |
| 56 | + | |
| 57 | +- envLocked=false when no messages | |
| 58 | +- envLocked=true with messages | |
| 59 | +- Transitions false→true after PUSH_USER_MESSAGE | |
| 60 | +- Remains true after SET_ERROR and UPDATE_SESSION | |
| 61 | + | |
| 62 | +**5. Auto-fill current branch** (3 tests) | |
| 63 | + | |
| 64 | +- Dispatches SET_THREAD_BRANCH when thread has no branch and current branch exists | |
| 65 | +- Does not overwrite existing branch | |
| 66 | +- No-op when no branch is marked current | |
| 67 | + | |
| 68 | +**6. Default branch detection** (2 tests) | |
| 69 | + | |
| 70 | +- isDefault flag on branch objects | |
| 71 | +- current and isDefault can be on different branches | |
| 72 | + | |
| 73 | +**7. Branch creation + checkout** (3 tests) | |
| 74 | + | |
| 75 | +- Successful create + checkout updates branch | |
| 76 | +- createBranch failure → error, branch unchanged | |
| 77 | +- checkout failure after successful create → error, branch unchanged | |
| 78 | + | |
| 79 | +**8. Session CWD resolution** (3 tests) | |
| 80 | + | |
| 81 | +- Uses worktreePath when available | |
| 82 | +- cwdOverride takes precedence over worktreePath | |
| 83 | +- Falls back to project cwd when no worktree | |
| 84 | + | |
| 85 | +**9. Error handling patterns** (4 tests) | |
| 86 | + | |
| 87 | +- SET_ERROR sets error on correct thread | |
| 88 | +- SET_ERROR with null clears error | |
| 89 | +- Error on one thread doesn't affect others | |
| 90 | +- Error cleared before successful branch operations | |
| 91 | + | |
| 92 | +## Verification | |
| 93 | + | |
| 94 | +```bash | |
| 95 | +# Run all renderer tests | |
| 96 | +cd apps/renderer && bun run test | |
| 97 | + | |
| 98 | +# Run just the new test file | |
| 99 | +npx vitest run apps/renderer/src/git-flows.test.ts | |
| 100 | + | |
| 101 | +# Run just the store tests | |
| 102 | +npx vitest run apps/renderer/src/store.test.ts | |
| 103 | +``` | |
| @@ -0,0 +1,115 @@ | ||
| 1 | +# Git Integration: Branch Picker + Worktrees | |
| 2 | + | |
| 3 | +## Summary | |
| 4 | + | |
| 5 | +Add git integration to let users start new threads from a specific branch, optionally creating a git worktree for isolated agent work. | |
| 6 | + | |
| 7 | +## UX Flow | |
| 8 | + | |
| 9 | +- **Left click** "+ New thread" → immediately creates a thread (current behavior, unchanged) | |
| 10 | +- **Right click** "+ New thread" → opens a context menu with git options: | |
| 11 | + - List of local branches → clicking one creates a thread on that branch (uses project cwd) | |
| 12 | + - Each branch has a "worktree" sub-option → creates a worktree, then creates thread with worktree as cwd | |
| 13 | +- When thread has a worktree, the agent session uses the worktree path as its cwd | |
| 14 | +- If git fails (not a repo), context menu shows "Not a git repository" disabled item | |
| 15 | + | |
| 16 | +## Changes | |
| 17 | + | |
| 18 | +### 1. `packages/contracts/src/git.ts` — CREATE | |
| 19 | + | |
| 20 | +New Zod schemas and types: | |
| 21 | + | |
| 22 | +- `gitListBranchesInputSchema` — `{ cwd: string }` | |
| 23 | +- `gitCreateWorktreeInputSchema` — `{ cwd: string, branch: string, path?: string }` | |
| 24 | +- `gitRemoveWorktreeInputSchema` — `{ cwd: string, path: string }` | |
| 25 | +- `gitBranchSchema` — `{ name: string, current: boolean }` | |
| 26 | +- Result types for each | |
| 27 | + | |
| 28 | +### 2. `packages/contracts/src/ipc.ts` — MODIFY | |
| 29 | + | |
| 30 | +- Add 3 IPC channels: `git:list-branches`, `git:create-worktree`, `git:remove-worktree` | |
| 31 | +- Add `git` namespace to `NativeApi` with `listBranches`, `createWorktree`, `removeWorktree` | |
| 32 | + | |
| 33 | +### 3. `packages/contracts/src/index.ts` — MODIFY | |
| 34 | + | |
| 35 | +- Add `export * from "./git"` | |
| 36 | + | |
| 37 | +### 4. `apps/desktop/src/main.ts` — MODIFY | |
| 38 | + | |
| 39 | +Add 3 IPC handlers + helper functions: | |
| 40 | + | |
| 41 | +- `listGitBranches()` — runs `git branch --no-color`, parses output into `{ name, current }[]` | |
| 42 | +- `createGitWorktree()` — runs `git worktree add <path> <branch>`, defaults path to `../{repo}-worktrees/{branch}` | |
| 43 | +- `removeGitWorktree()` — runs `git worktree remove <path>` | |
| 44 | + | |
| 45 | +Reuses existing `runTerminalCommand()`. | |
| 46 | + | |
| 47 | +### 5. `apps/desktop/src/preload.ts` — MODIFY | |
| 48 | + | |
| 49 | +Add `git` namespace with 3 `ipcRenderer.invoke` calls. | |
| 50 | + | |
| 51 | +### 6. `apps/renderer/src/types.ts` — MODIFY | |
| 52 | + | |
| 53 | +Add to `Thread`: | |
| 54 | + | |
| 55 | +``` | |
| 56 | +branch: string | null | |
| 57 | +worktreePath: string | null | |
| 58 | +``` | |
| 59 | + | |
| 60 | +### 7. `apps/renderer/src/persistenceSchema.ts` — MODIFY | |
| 61 | + | |
| 62 | +- Add optional `branch`/`worktreePath` to persisted thread schema (`.nullable().optional()` for backwards compat) | |
| 63 | +- Add V3 schema, update union | |
| 64 | +- Update `hydrateThread` to default new fields to `null` | |
| 65 | +- Update `toPersistedState` to serialize new fields | |
| 66 | + | |
| 67 | +### 8. `apps/renderer/src/store.ts` — MODIFY | |
| 68 | + | |
| 69 | +- Update persisted state key to v3, keep v2 as legacy fallback | |
| 70 | + | |
| 71 | +### 9. `apps/renderer/src/components/Sidebar.tsx` — MODIFY (main UI work) | |
| 72 | + | |
| 73 | +- Keep existing left-click `handleNewThread` unchanged (immediate thread creation) | |
| 74 | +- Add `onContextMenu` handler to "+ New thread" buttons (both global and per-project) | |
| 75 | +- On right-click: fetch branches via `api.git.listBranches`, show a custom context menu | |
| 76 | +- Context menu items: branch names, each with a nested option to create with worktree | |
| 77 | +- Clicking a branch → creates thread with `branch` set, title = branch name | |
| 78 | +- Clicking "with worktree" → calls `api.git.createWorktree` first, then creates thread with `worktreePath` | |
| 79 | +- Show branch badge on thread list items | |
| 80 | +- If not a git repo, show "Not a git repository" as disabled menu item | |
| 81 | + | |
| 82 | +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. | |
| 83 | + | |
| 84 | +### 10. `apps/renderer/src/components/ChatView.tsx` — MODIFY | |
| 85 | + | |
| 86 | +- Line 157: use `activeThread.worktreePath ?? activeProject.cwd` as session cwd | |
| 87 | +- Show branch/worktree badge in header bar | |
| 88 | + | |
| 89 | +## Implementation Order | |
| 90 | + | |
| 91 | +1. `packages/contracts/src/git.ts` (new schemas) | |
| 92 | +2. `packages/contracts/src/ipc.ts` + `index.ts` (wire up channels) | |
| 93 | +3. `apps/desktop/src/main.ts` (git command handlers) | |
| 94 | +4. `apps/desktop/src/preload.ts` (bridge methods) | |
| 95 | +5. `apps/renderer/src/types.ts` (Thread type update) | |
| 96 | +6. `apps/renderer/src/persistenceSchema.ts` + `store.ts` (persistence migration) | |
| 97 | +7. `apps/renderer/src/components/Sidebar.tsx` (branch picker UI) | |
| 98 | +8. `apps/renderer/src/components/ChatView.tsx` (worktree cwd + badge) | |
| 99 | + | |
| 100 | +## Edge Cases | |
| 101 | + | |
| 102 | +- **Not a git repo**: `git branch` fails → context menu shows "Not a git repository" disabled item | |
| 103 | +- **Branch has slashes**: `feature/foo` → worktree dir becomes `feature-foo` | |
| 104 | +- **Worktree exists**: git error surfaces to user via inline error message in context menu | |
| 105 | +- **No persistence breakage**: `.nullable().optional()` fields parse fine with old data | |
| 106 | + | |
| 107 | +## Verification | |
| 108 | + | |
| 109 | +1. `turbo build` — confirm contracts/desktop/renderer all compile | |
| 110 | +2. Launch app, add a project pointing to a git repo | |
| 111 | +3. Click "+ New thread" → verify branch list loads | |
| 112 | +4. Select a branch, click Start → thread created with branch in title | |
| 113 | +5. Enable worktree checkbox, pick branch, Start → verify worktree directory created on disk | |
| 114 | +6. Send a message in worktree thread → verify agent runs in worktree cwd | |
| 115 | +7. Add a non-git project → verify graceful error, can still create thread | |