Skip to content

Commit c48eb02

Browse files
authored
feat: worktrees, refactors, bug fixes (#164)
1 parent 33e3d72 commit c48eb02

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+3296
-1746
lines changed

apps/array/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
"@vitejs/plugin-react": "^4.2.1",
5252
"@vitest/ui": "^4.0.10",
5353
"autoprefixer": "^10.4.17",
54-
"electron": "^28.2.0",
54+
"electron": "^30.0.0",
5555
"husky": "^9.1.7",
5656
"jsdom": "^26.0.0",
5757
"knip": "^5.66.3",
@@ -133,6 +133,7 @@
133133
"sonner": "^2.0.7",
134134
"uuid": "^9.0.1",
135135
"zod": "^4.1.12",
136-
"zustand": "^4.5.0"
136+
"zustand": "^4.5.0",
137+
"electron-store": "^11.0.0"
137138
}
138139
}

apps/array/src/main/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import {
1212
} from "electron";
1313
import { ANALYTICS_EVENTS } from "../types/analytics.js";
1414
import { registerAgentIpc, type TaskController } from "./services/agent.js";
15-
import { ensureDataDirectory } from "./services/data.js";
1615
import { registerFoldersIpc } from "./services/folders.js";
16+
import { registerWorktreeIpc } from "./services/worktree.js";
1717
import "./services/index.js";
1818
import { registerFileWatcherIpc } from "./services/fileWatcher.js";
1919
import { registerFsIpc } from "./services/fs.js";
@@ -176,10 +176,9 @@ function createWindow(): void {
176176
});
177177
}
178178

179-
app.whenReady().then(async () => {
179+
app.whenReady().then(() => {
180180
createWindow();
181181
ensureClaudeConfigDir();
182-
await ensureDataDirectory();
183182

184183
// Initialize PostHog analytics
185184
initializePostHog();
@@ -214,4 +213,5 @@ registerAgentIpc(taskControllers, () => mainWindow);
214213
registerFsIpc();
215214
registerFileWatcherIpc(() => mainWindow);
216215
registerFoldersIpc();
216+
registerWorktreeIpc();
217217
registerShellIpc();

apps/array/src/main/preload.ts

Lines changed: 129 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
import { contextBridge, type IpcRendererEvent, ipcRenderer } from "electron";
2-
import type {
3-
CloudRegion,
4-
OAuthTokenResponse,
5-
StoredOAuthTokens,
6-
} from "../shared/types/oauth";
2+
import type { CloudRegion, OAuthTokenResponse } from "../shared/types/oauth";
73
import type {
84
FolderContextMenuResult,
95
SplitContextMenuResult,
@@ -47,16 +43,6 @@ contextBridge.exposeInMainWorld("electronAPI", {
4743
region: CloudRegion,
4844
): Promise<{ success: boolean; data?: OAuthTokenResponse; error?: string }> =>
4945
ipcRenderer.invoke("oauth:start-flow", region),
50-
oauthEncryptTokens: (
51-
tokens: StoredOAuthTokens,
52-
): Promise<{ success: boolean; encrypted?: string; error?: string }> =>
53-
ipcRenderer.invoke("oauth:encrypt-tokens", tokens),
54-
oauthRetrieveTokens: (
55-
encrypted: string,
56-
): Promise<{ success: boolean; data?: StoredOAuthTokens; error?: string }> =>
57-
ipcRenderer.invoke("oauth:retrieve-tokens", encrypted),
58-
oauthDeleteTokens: (): Promise<{ success: boolean }> =>
59-
ipcRenderer.invoke("oauth:delete-tokens"),
6046
oauthRefreshToken: (
6147
refreshToken: string,
6248
region: CloudRegion,
@@ -367,5 +353,133 @@ contextBridge.exposeInMainWorld("electronAPI", {
367353
updateFolderAccessed: (folderId: string): Promise<void> =>
368354
ipcRenderer.invoke("update-folder-accessed", folderId),
369355
clearAllData: (): Promise<void> => ipcRenderer.invoke("clear-all-data"),
356+
getTaskAssociations: (): Promise<
357+
Array<{
358+
taskId: string;
359+
folderId: string;
360+
folderPath: string;
361+
worktree?: {
362+
worktreePath: string;
363+
worktreeName: string;
364+
branchName: string;
365+
baseBranch: string;
366+
createdAt: string;
367+
};
368+
}>
369+
> => ipcRenderer.invoke("get-task-associations"),
370+
getTaskAssociation: (
371+
taskId: string,
372+
): Promise<{
373+
taskId: string;
374+
folderId: string;
375+
folderPath: string;
376+
worktree?: {
377+
worktreePath: string;
378+
worktreeName: string;
379+
branchName: string;
380+
baseBranch: string;
381+
createdAt: string;
382+
};
383+
} | null> => ipcRenderer.invoke("get-task-association", taskId),
384+
setTaskAssociation: (
385+
taskId: string,
386+
folderId: string,
387+
folderPath: string,
388+
worktree?: {
389+
worktreePath: string;
390+
worktreeName: string;
391+
branchName: string;
392+
baseBranch: string;
393+
createdAt: string;
394+
},
395+
): Promise<{
396+
taskId: string;
397+
folderId: string;
398+
folderPath: string;
399+
worktree?: {
400+
worktreePath: string;
401+
worktreeName: string;
402+
branchName: string;
403+
baseBranch: string;
404+
createdAt: string;
405+
};
406+
}> =>
407+
ipcRenderer.invoke(
408+
"set-task-association",
409+
taskId,
410+
folderId,
411+
folderPath,
412+
worktree,
413+
),
414+
updateTaskWorktree: (
415+
taskId: string,
416+
worktree: {
417+
worktreePath: string;
418+
worktreeName: string;
419+
branchName: string;
420+
baseBranch: string;
421+
createdAt: string;
422+
},
423+
): Promise<{
424+
taskId: string;
425+
folderId: string;
426+
folderPath: string;
427+
worktree?: {
428+
worktreePath: string;
429+
worktreeName: string;
430+
branchName: string;
431+
baseBranch: string;
432+
createdAt: string;
433+
};
434+
} | null> => ipcRenderer.invoke("update-task-worktree", taskId, worktree),
435+
removeTaskAssociation: (taskId: string): Promise<void> =>
436+
ipcRenderer.invoke("remove-task-association", taskId),
437+
clearTaskWorktree: (taskId: string): Promise<void> =>
438+
ipcRenderer.invoke("clear-task-worktree", taskId),
439+
},
440+
// Worktree API
441+
worktree: {
442+
create: (
443+
mainRepoPath: string,
444+
): Promise<{
445+
worktreePath: string;
446+
worktreeName: string;
447+
branchName: string;
448+
baseBranch: string;
449+
createdAt: string;
450+
}> => ipcRenderer.invoke("worktree-create", mainRepoPath),
451+
delete: (mainRepoPath: string, worktreePath: string): Promise<void> =>
452+
ipcRenderer.invoke("worktree-delete", mainRepoPath, worktreePath),
453+
getInfo: (
454+
mainRepoPath: string,
455+
worktreePath: string,
456+
): Promise<{
457+
worktreePath: string;
458+
worktreeName: string;
459+
branchName: string;
460+
baseBranch: string;
461+
createdAt: string;
462+
} | null> =>
463+
ipcRenderer.invoke("worktree-get-info", mainRepoPath, worktreePath),
464+
exists: (mainRepoPath: string, name: string): Promise<boolean> =>
465+
ipcRenderer.invoke("worktree-exists", mainRepoPath, name),
466+
list: (
467+
mainRepoPath: string,
468+
): Promise<
469+
Array<{
470+
worktreePath: string;
471+
worktreeName: string;
472+
branchName: string;
473+
baseBranch: string;
474+
createdAt: string;
475+
}>
476+
> => ipcRenderer.invoke("worktree-list", mainRepoPath),
477+
isWorktree: (mainRepoPath: string, repoPath: string): Promise<boolean> =>
478+
ipcRenderer.invoke("worktree-is-worktree", mainRepoPath, repoPath),
479+
getMainRepoPath: (
480+
mainRepoPath: string,
481+
worktreePath: string,
482+
): Promise<string | null> =>
483+
ipcRenderer.invoke("worktree-get-main-repo", mainRepoPath, worktreePath),
370484
},
371485
});

apps/array/src/main/services/data.ts

Lines changed: 0 additions & 104 deletions
This file was deleted.

apps/array/src/main/services/fileWatcher.ts

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -137,25 +137,45 @@ class FileService {
137137
{ ignore: WATCHER_IGNORE_PATTERNS },
138138
);
139139

140-
const gitDir = path.join(repoPath, ".git");
140+
const gitPath = path.join(repoPath, ".git");
141141
let gitSubscription: watcher.AsyncSubscription | null = null;
142142
try {
143-
await fs.access(gitDir);
144-
gitSubscription = await watcher.subscribe(gitDir, (err, events) => {
145-
if (err) {
146-
console.error("Git watcher error:", err);
147-
return;
148-
}
149-
if (
150-
events.some(
151-
(e) => e.path.endsWith("/HEAD") || e.path.endsWith("/index"),
152-
)
153-
) {
154-
this.emit("git:state-changed", { repoPath });
143+
const gitStat = await fs.stat(gitPath);
144+
let gitDirToWatch: string;
145+
146+
if (gitStat.isDirectory()) {
147+
// Regular repo: .git is a directory
148+
gitDirToWatch = gitPath;
149+
} else if (gitStat.isFile()) {
150+
// Worktree: .git is a file containing "gitdir: /path/to/main/.git/worktrees/name"
151+
const gitFileContent = await fs.readFile(gitPath, "utf-8");
152+
const match = gitFileContent.match(/gitdir:\s*(.+)/);
153+
if (!match) {
154+
throw new Error("Invalid .git file format");
155155
}
156-
});
157-
} catch {
158-
// .git directory doesn't exist
156+
gitDirToWatch = match[1].trim();
157+
} else {
158+
throw new Error(".git is neither file nor directory");
159+
}
160+
161+
gitSubscription = await watcher.subscribe(
162+
gitDirToWatch,
163+
(err, events) => {
164+
if (err) {
165+
console.error("Git watcher error:", err);
166+
return;
167+
}
168+
if (
169+
events.some(
170+
(e) => e.path.endsWith("/HEAD") || e.path.endsWith("/index"),
171+
)
172+
) {
173+
this.emit("git:state-changed", { repoPath });
174+
}
175+
},
176+
);
177+
} catch (error) {
178+
console.warn("Failed to set up git watcher:", error);
159179
}
160180

161181
state.subscription = subscription;

0 commit comments

Comments
 (0)