authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-08 21:27:34-08:00
committergravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-11 09:00:18-08:00
logd64f78a9498ae017db7efde4cc5fbbfd07091a79
tree25f7eda340cccb1538ee128625c419ce2a32551f
parent9023d139295fb14035424c93b9b0f30eb77e1d1d

Add git integration: branch picker + worktrees

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,6 +34,8 @@ const persistedThreadSchema = z.object({
34 model: z.string().min(1),34 model: z.string().min(1),
35 messages: z.array(persistedMessageSchema),35 messages: z.array(persistedMessageSchema),
36 createdAt: z.string().min(1),36 createdAt: z.string().min(1),
37 branch: z.string().min(1).nullable().optional(),
38 worktreePath: z.string().min(1).nullable().optional(),
37});39});
3840
39const persistedStateBodySchema = z.object({41const persistedStateBodySchema = z.object({
...@@ -112,6 +114,8 @@ function hydrateThread(...@@ -112,6 +114,8 @@ function hydrateThread(
112 events: [],114 events: [],
113 error: null,115 error: null,
114 createdAt: thread.createdAt,116 createdAt: thread.createdAt,
117 branch: thread.branch ?? null,
118 worktreePath: thread.worktreePath ?? null,
115 };119 };
116}120}
117121
...@@ -170,6 +174,8 @@ export function toPersistedState(...@@ -170,6 +174,8 @@ export function toPersistedState(
170 model: thread.model,174 model: thread.model,
171 messages: thread.messages,175 messages: thread.messages,
172 createdAt: thread.createdAt,176 createdAt: thread.createdAt,
177 branch: thread.branch,
178 worktreePath: thread.worktreePath,
173 })),179 })),
174 activeThreadId: state.activeThreadId,180 activeThreadId: state.activeThreadId,
175 runtimeMode: state.runtimeMode,181 runtimeMode: state.runtimeMode,
apps/web/src/types.ts+2
...@@ -35,4 +35,6 @@ export interface Thread {...@@ -35,4 +35,6 @@ export interface Thread {
35 latestTurnStartedAt?: string | undefined;35 latestTurnStartedAt?: string | undefined;
36 latestTurnCompletedAt?: string | undefined;36 latestTurnCompletedAt?: string | undefined;
37 latestTurnDurationMs?: number | undefined;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 @@
1import { z } from "zod";
2
3// ── Input schemas ──
4
5export const gitListBranchesInputSchema = z.object({
6 cwd: z.string().min(1),
7});
8
9export 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
15export const gitRemoveWorktreeInputSchema = z.object({
16 cwd: z.string().min(1),
17 path: z.string().min(1),
18});
19
20export const gitInitInputSchema = z.object({
21 cwd: z.string().min(1),
22});
23
24// ── Output schemas ──
25
26export const gitBranchSchema = z.object({
27 name: z.string().min(1),
28 current: z.boolean(),
29});
30
31export const gitWorktreeSchema = z.object({
32 path: z.string().min(1),
33 branch: z.string().min(1),
34});
35
36// ── Types ──
37
38export type GitListBranchesInput = z.infer<typeof gitListBranchesInputSchema>;
39export type GitBranch = z.infer<typeof gitBranchSchema>;
40export type GitCreateWorktreeInput = z.infer<
41 typeof gitCreateWorktreeInputSchema
42>;
43export type GitWorktree = z.infer<typeof gitWorktreeSchema>;
44export type GitRemoveWorktreeInput = z.infer<
45 typeof gitRemoveWorktreeInputSchema
46>;
47export type GitInitInput = z.infer<typeof gitInitInputSchema>;
48
49export interface GitListBranchesResult {
50 branches: GitBranch[];
51 isRepo: boolean;
52}
53
54export interface GitCreateWorktreeResult {
55 worktree: GitWorktree;
56}
packages/contracts/src/index.ts+1
...@@ -6,3 +6,4 @@ export * from "./provider";...@@ -6,3 +6,4 @@ export * from "./provider";
6export * from "./model";6export * from "./model";
7export * from "./ws";7export * from "./ws";
8export * from "./project";8export * from "./project";
9export * from "./git";
packages/contracts/src/ipc.ts+14
...@@ -1,4 +1,12 @@...@@ -1,4 +1,12 @@
1import type { AgentConfig, AgentExit, OutputChunk } from "./agent";1import type { AgentConfig, AgentExit, OutputChunk } from "./agent";
2import type {
3 GitCreateWorktreeInput,
4 GitCreateWorktreeResult,
5 GitInitInput,
6 GitListBranchesInput,
7 GitListBranchesResult,
8 GitRemoveWorktreeInput,
9} from "./git";
2import type {10import type {
3 ProviderEvent,11 ProviderEvent,
4 ProviderInterruptTurnInput,12 ProviderInterruptTurnInput,
...@@ -62,4 +70,10 @@ export interface NativeApi {...@@ -62,4 +70,10 @@ export interface NativeApi {
62 shell: {70 shell: {
63 openInEditor: (cwd: string, editor: EditorId) => Promise<void>;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}