History

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

Julius Marminge committed 9 months ago

Commit d64f78a1 parent 9023d135 files · +79 −0Browse files

Unified · Split

5 files
  1. apps/web/src/persistenceSchema.ts +6 −0
  2. apps/web/src/types.ts +2 −0
  3. packages/contracts/src/git.ts +56 −0
  4. packages/contracts/src/index.ts +1 −0
  5. packages/contracts/src/ipc.ts +14 −0
apps/web/src/persistenceSchema.ts+6−0
@@ -34,6 +34,8 @@ const persistedThreadSchema = z.object({ · 33 unmodified lines
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});
38 40
39const persistedStateBodySchema = z.object({41const persistedStateBodySchema = z.object({
@@ -112,6 +114,8 @@ function hydrateThread( · 72 unmodified lines
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}
117 121
@@ -170,6 +174,8 @@ export function toPersistedState( · 52 unmodified lines
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−0
@@ -35,4 +35,6 @@ export interface Thread { · 34 unmodified lines
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+56−0
@@ -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−0
@@ -6,3 +6,4 @@ export * from "./provider"; · 5 unmodified lines
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−0
@@ -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 { · 57 unmodified lines
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}