| 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 |