| author | |
| committer | |
| log | 9fc5ad20830a148ae36abf2381efef8283dc3699 |
| tree | be5c3b8832eb4f3fed177fbb93b386d00420cbe8 |
| parent | 79ac59f04e8f02b113dfdf5de9e9a5305c748f33 |
4 files changed, 391 insertions(+), 0 deletions(-)
.plans/branch-environment-picker-in-chatview-input.md created+74| ... | @@ -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 | ||
.plans/git-flows-integration-tests.md created+99| ... | @@ -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 | ``` | ||
.plans/git-flows-test-plan.md created+103| ... | @@ -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 | ``` | ||
.plans/git-integration-branch-picker-worktrees.md created+115| ... | @@ -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 | ||