| author | |
| committer | |
| log | d64f78a9498ae017db7efde4cc5fbbfd07091a79 |
| tree | 25f7eda340cccb1538ee128625c419ce2a32551f |
| parent | 9023d139295fb14035424c93b9b0f30eb77e1d1d |
Allows users to right-click "+ New thread" to create threads from specific git branches, with optional worktree creation for isolated work. Includes branch sorting (current/default first), graceful handling of non-git repos, and ability to initialize git repos.
- New git IPC contract (listBranches, createWorktree, removeWorktree, init)
- Branch context menu with worktree sub-option in Sidebar
- Thread model extended with branch/worktreePath fields
- Session cwd uses worktree path when available
- Branch sorting: current > main/master > alphabetical
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>5 files changed, 79 insertions(+), 0 deletions(-)
apps/web/src/persistenceSchema.ts+6| ... | ... | @@ -34,6 +34,8 @@ const persistedThreadSchema = z.object({ |
| 34 | 34 | model: z.string().min(1), |
| 35 | 35 | messages: z.array(persistedMessageSchema), |
| 36 | 36 | createdAt: z.string().min(1), |
| 37 | branch: z.string().min(1).nullable().optional(), | |
| 38 | worktreePath: z.string().min(1).nullable().optional(), | |
| 37 | 39 | }); |
| 38 | 40 | |
| 39 | 41 | const persistedStateBodySchema = z.object({ |
| ... | ... | @@ -112,6 +114,8 @@ function hydrateThread( |
| 112 | 114 | events: [], |
| 113 | 115 | error: null, |
| 114 | 116 | createdAt: thread.createdAt, |
| 117 | branch: thread.branch ?? null, | |
| 118 | worktreePath: thread.worktreePath ?? null, | |
| 115 | 119 | }; |
| 116 | 120 | } |
| 117 | 121 | |
| ... | ... | @@ -170,6 +174,8 @@ export function toPersistedState( |
| 170 | 174 | model: thread.model, |
| 171 | 175 | messages: thread.messages, |
| 172 | 176 | createdAt: thread.createdAt, |
| 177 | branch: thread.branch, | |
| 178 | worktreePath: thread.worktreePath, | |
| 173 | 179 | })), |
| 174 | 180 | activeThreadId: state.activeThreadId, |
| 175 | 181 | runtimeMode: state.runtimeMode, |
apps/web/src/types.ts+2| ... | ... | @@ -35,4 +35,6 @@ export interface Thread { |
| 35 | 35 | latestTurnStartedAt?: string | undefined; |
| 36 | 36 | latestTurnCompletedAt?: string | undefined; |
| 37 | 37 | latestTurnDurationMs?: number | undefined; |
| 38 | branch: string | null; | |
| 39 | worktreePath: string | null; | |
| 38 | 40 | } |
packages/contracts/src/git.ts created+56| ... | ... | @@ -0,0 +1,56 @@ |
| 1 | import { z } from "zod"; | |
| 2 | ||
| 3 | // ── Input schemas ── | |
| 4 | ||
| 5 | export const gitListBranchesInputSchema = z.object({ | |
| 6 | cwd: z.string().min(1), | |
| 7 | }); | |
| 8 | ||
| 9 | export const gitCreateWorktreeInputSchema = z.object({ | |
| 10 | cwd: z.string().min(1), | |
| 11 | branch: z.string().min(1), | |
| 12 | path: z.string().min(1).optional(), | |
| 13 | }); | |
| 14 | ||
| 15 | export const gitRemoveWorktreeInputSchema = z.object({ | |
| 16 | cwd: z.string().min(1), | |
| 17 | path: z.string().min(1), | |
| 18 | }); | |
| 19 | ||
| 20 | export const gitInitInputSchema = z.object({ | |
| 21 | cwd: z.string().min(1), | |
| 22 | }); | |
| 23 | ||
| 24 | // ── Output schemas ── | |
| 25 | ||
| 26 | export const gitBranchSchema = z.object({ | |
| 27 | name: z.string().min(1), | |
| 28 | current: z.boolean(), | |
| 29 | }); | |
| 30 | ||
| 31 | export const gitWorktreeSchema = z.object({ | |
| 32 | path: z.string().min(1), | |
| 33 | branch: z.string().min(1), | |
| 34 | }); | |
| 35 | ||
| 36 | // ── Types ── | |
| 37 | ||
| 38 | export type GitListBranchesInput = z.infer<typeof gitListBranchesInputSchema>; | |
| 39 | export type GitBranch = z.infer<typeof gitBranchSchema>; | |
| 40 | export type GitCreateWorktreeInput = z.infer< | |
| 41 | typeof gitCreateWorktreeInputSchema | |
| 42 | >; | |
| 43 | export type GitWorktree = z.infer<typeof gitWorktreeSchema>; | |
| 44 | export type GitRemoveWorktreeInput = z.infer< | |
| 45 | typeof gitRemoveWorktreeInputSchema | |
| 46 | >; | |
| 47 | export type GitInitInput = z.infer<typeof gitInitInputSchema>; | |
| 48 | ||
| 49 | export interface GitListBranchesResult { | |
| 50 | branches: GitBranch[]; | |
| 51 | isRepo: boolean; | |
| 52 | } | |
| 53 | ||
| 54 | export interface GitCreateWorktreeResult { | |
| 55 | worktree: GitWorktree; | |
| 56 | } |
packages/contracts/src/index.ts+1| ... | ... | @@ -6,3 +6,4 @@ export * from "./provider"; |
| 6 | 6 | export * from "./model"; |
| 7 | 7 | export * from "./ws"; |
| 8 | 8 | export * from "./project"; |
| 9 | export * from "./git"; |
packages/contracts/src/ipc.ts+14| ... | ... | @@ -1,4 +1,12 @@ |
| 1 | 1 | import type { AgentConfig, AgentExit, OutputChunk } from "./agent"; |
| 2 | import type { | |
| 3 | GitCreateWorktreeInput, | |
| 4 | GitCreateWorktreeResult, | |
| 5 | GitInitInput, | |
| 6 | GitListBranchesInput, | |
| 7 | GitListBranchesResult, | |
| 8 | GitRemoveWorktreeInput, | |
| 9 | } from "./git"; | |
| 2 | 10 | import type { |
| 3 | 11 | ProviderEvent, |
| 4 | 12 | ProviderInterruptTurnInput, |
| ... | ... | @@ -62,4 +70,10 @@ export interface NativeApi { |
| 62 | 70 | shell: { |
| 63 | 71 | openInEditor: (cwd: string, editor: EditorId) => Promise<void>; |
| 64 | 72 | }; |
| 73 | git: { | |
| 74 | listBranches: (input: GitListBranchesInput) => Promise<GitListBranchesResult>; | |
| 75 | createWorktree: (input: GitCreateWorktreeInput) => Promise<GitCreateWorktreeResult>; | |
| 76 | removeWorktree: (input: GitRemoveWorktreeInput) => Promise<void>; | |
| 77 | init: (input: GitInitInput) => Promise<void>; | |
| 78 | }; | |
| 65 | 79 | } |