Skip to content

Commit 6baf95a

Browse files
authored
feat: Repository and task handling improvements (#393)
- We now have settings tab for the repository. Currently the only option is to delete the repository with all associated tasks and their workspaces. - If the repository folder is moved (even in runtime) then it will let the user know they need to restore the folder to continue with their tasks. Previously this was just broken and asked the user if they wanted to initialize a git repo (in a loop?). - Added a touch more padding on sidebar items, open to feedback on it. - Implemented the ability to pin and delete a tasks from the sidebar. The default sort order for pinned and unpinned is by latest activity.
1 parent a6003aa commit 6baf95a

19 files changed

Lines changed: 671 additions & 128 deletions

File tree

apps/array/src/main/services/context-menu/schemas.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,18 @@ export type TabAction = z.infer<typeof tabAction>;
7777
export type FileAction = z.infer<typeof fileAction>;
7878
export type SplitDirection = z.infer<typeof splitDirection>;
7979

80+
export const confirmDeleteTaskInput = z.object({
81+
taskTitle: z.string(),
82+
hasWorktree: z.boolean(),
83+
});
84+
85+
export const confirmDeleteTaskOutput = z.object({
86+
confirmed: z.boolean(),
87+
});
88+
89+
export type ConfirmDeleteTaskInput = z.infer<typeof confirmDeleteTaskInput>;
90+
export type ConfirmDeleteTaskResult = z.infer<typeof confirmDeleteTaskOutput>;
91+
8092
export type TaskContextMenuResult = z.infer<typeof taskContextMenuOutput>;
8193
export type FolderContextMenuResult = z.infer<typeof folderContextMenuOutput>;
8294
export type TabContextMenuResult = z.infer<typeof tabContextMenuOutput>;

apps/array/src/main/services/context-menu/service.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { MAIN_TOKENS } from "../../di/tokens.js";
1010
import { getMainWindow } from "../../trpc/context.js";
1111
import type { ExternalAppsService } from "../external-apps/service.js";
1212
import type {
13+
ConfirmDeleteTaskInput,
14+
ConfirmDeleteTaskResult,
1315
FileAction,
1416
FileContextMenuInput,
1517
FileContextMenuResult,
@@ -49,30 +51,31 @@ export class ContextMenuService {
4951
return { apps, lastUsedAppId: lastUsed.lastUsedApp };
5052
}
5153

54+
async confirmDeleteTask(
55+
input: ConfirmDeleteTaskInput,
56+
): Promise<ConfirmDeleteTaskResult> {
57+
const confirmed = await this.confirm({
58+
title: "Delete Task",
59+
message: `Delete "${input.taskTitle}"?`,
60+
detail: input.hasWorktree
61+
? "This will permanently delete the task and its associated worktree."
62+
: "This will permanently delete the task.",
63+
confirmLabel: "Delete",
64+
});
65+
return { confirmed };
66+
}
67+
5268
async showTaskContextMenu(
5369
input: TaskContextMenuInput,
5470
): Promise<TaskContextMenuResult> {
55-
const { taskTitle, worktreePath } = input;
71+
const { worktreePath } = input;
5672
const { apps, lastUsedAppId } = await this.getExternalAppsData();
5773

5874
return this.showMenu<TaskAction>([
5975
this.item("Rename", { type: "rename" }),
6076
this.item("Duplicate", { type: "duplicate" }),
6177
this.separator(),
62-
this.item(
63-
"Delete",
64-
{ type: "delete" },
65-
{
66-
confirm: {
67-
title: "Delete Task",
68-
message: `Delete "${taskTitle}"?`,
69-
detail: worktreePath
70-
? "This will permanently delete the task and its associated worktree."
71-
: "This will permanently delete the task.",
72-
confirmLabel: "Delete",
73-
},
74-
},
75-
),
78+
this.item("Delete", { type: "delete" }),
7679
...(worktreePath
7780
? [
7881
this.separator(),

apps/array/src/main/services/folders/schemas.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,17 @@ export const registeredFolderSchema = z.object({
88
createdAt: z.string(),
99
});
1010

11-
export const getFoldersOutput = z.array(registeredFolderSchema);
11+
export const registeredFolderWithExistsSchema = registeredFolderSchema.extend({
12+
exists: z.boolean(),
13+
});
14+
15+
export const getFoldersOutput = z.array(registeredFolderWithExistsSchema);
1216

1317
export const addFolderInput = z.object({
1418
folderPath: z.string().min(2, "Folder path must be a valid directory path"),
1519
});
1620

17-
export const addFolderOutput = registeredFolderSchema;
21+
export const addFolderOutput = registeredFolderWithExistsSchema;
1822

1923
export const removeFolderInput = z.object({
2024
folderId: z.string(),

apps/array/src/main/services/folders/service.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { exec } from "node:child_process";
2+
import fs from "node:fs";
23
import path from "node:path";
34
import { promisify } from "node:util";
45
import { WorktreeManager } from "@posthog/agent";
@@ -20,13 +21,21 @@ const log = logger.scope("folders-service");
2021

2122
@injectable()
2223
export class FoldersService {
23-
async getFolders(): Promise<RegisteredFolder[]> {
24+
async getFolders(): Promise<(RegisteredFolder & { exists: boolean })[]> {
2425
const folders = foldersStore.get("folders", []);
2526
// Filter out any folders with empty names (from invalid paths like "/")
26-
return folders.filter((f) => f.name && f.path);
27+
// Also add exists property to check if path is valid on disk
28+
return folders
29+
.filter((f) => f.name && f.path)
30+
.map((f) => ({
31+
...f,
32+
exists: fs.existsSync(f.path),
33+
}));
2734
}
2835

29-
async addFolder(folderPath: string): Promise<RegisteredFolder> {
36+
async addFolder(
37+
folderPath: string,
38+
): Promise<RegisteredFolder & { exists: boolean }> {
3039
// Validate the path before proceeding
3140
const folderName = path.basename(folderPath);
3241
if (!folderPath || !folderName) {
@@ -69,7 +78,7 @@ export class FoldersService {
6978
if (existing) {
7079
existing.lastAccessed = new Date().toISOString();
7180
foldersStore.set("folders", folders);
72-
return existing;
81+
return { ...existing, exists: true };
7382
}
7483

7584
const newFolder: RegisteredFolder = {
@@ -83,7 +92,7 @@ export class FoldersService {
8392
folders.push(newFolder);
8493
foldersStore.set("folders", folders);
8594

86-
return newFolder;
95+
return { ...newFolder, exists: true };
8796
}
8897

8998
async removeFolder(folderId: string): Promise<void> {

apps/array/src/main/trpc/routers/context-menu.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { container } from "../../di/container.js";
22
import { MAIN_TOKENS } from "../../di/tokens.js";
33
import {
4+
confirmDeleteTaskInput,
5+
confirmDeleteTaskOutput,
46
fileContextMenuInput,
57
fileContextMenuOutput,
68
folderContextMenuInput,
@@ -18,6 +20,11 @@ const getService = () =>
1820
container.get<ContextMenuService>(MAIN_TOKENS.ContextMenuService);
1921

2022
export const contextMenuRouter = router({
23+
confirmDeleteTask: publicProcedure
24+
.input(confirmDeleteTaskInput)
25+
.output(confirmDeleteTaskOutput)
26+
.mutation(({ input }) => getService().confirmDeleteTask(input)),
27+
2128
showTaskContextMenu: publicProcedure
2229
.input(taskContextMenuInput)
2330
.output(taskContextMenuOutput)

apps/array/src/renderer/components/GlobalEventHandlers.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { usePanelLayoutStore } from "@features/panels/store/panelLayoutStore";
22
import { useRightSidebarStore } from "@features/right-sidebar";
33
import { useSidebarStore } from "@features/sidebar/stores/sidebarStore";
4+
import { useWorkspaceStore } from "@features/workspace/stores/workspaceStore";
45
import { SHORTCUTS } from "@renderer/constants/keyboard-shortcuts";
56
import { clearApplicationStorage } from "@renderer/lib/clearStorage";
7+
import { useRegisteredFoldersStore } from "@renderer/stores/registeredFoldersStore";
68
import { useNavigationStore } from "@stores/navigationStore";
79
import { useCallback, useEffect } from "react";
810
import { useHotkeys } from "react-hotkeys-hook";
@@ -23,8 +25,14 @@ export function GlobalEventHandlers({
2325
const navigateToTaskInput = useNavigationStore(
2426
(state) => state.navigateToTaskInput,
2527
);
28+
const navigateToFolderSettings = useNavigationStore(
29+
(state) => state.navigateToFolderSettings,
30+
);
31+
const view = useNavigationStore((state) => state.view);
2632
const goBack = useNavigationStore((state) => state.goBack);
2733
const goForward = useNavigationStore((state) => state.goForward);
34+
const folders = useRegisteredFoldersStore((state) => state.folders);
35+
const workspaces = useWorkspaceStore.use.workspaces();
2836
const clearAllLayouts = usePanelLayoutStore((state) => state.clearAllLayouts);
2937
const toggleLeftSidebar = useSidebarStore((state) => state.toggle);
3038
const toggleRightSidebar = useRightSidebarStore((state) => state.toggle);
@@ -101,6 +109,28 @@ export function GlobalEventHandlers({
101109
};
102110
}, [goBack, goForward]);
103111

112+
// Reload folders when window regains focus to detect moved/deleted folders
113+
useEffect(() => {
114+
const handleFocus = () => {
115+
useRegisteredFoldersStore.getState().loadFolders();
116+
};
117+
window.addEventListener("focus", handleFocus);
118+
return () => window.removeEventListener("focus", handleFocus);
119+
}, []);
120+
121+
// Check if current task's folder became invalid (e.g., moved while app was open)
122+
useEffect(() => {
123+
if (view.type !== "task-detail" || !view.data) return;
124+
125+
const workspace = workspaces[view.data.id];
126+
if (!workspace?.folderId) return;
127+
128+
const folder = folders.find((f) => f.id === workspace.folderId);
129+
if (folder && folder.exists === false) {
130+
navigateToFolderSettings(folder.id);
131+
}
132+
}, [view, folders, workspaces, navigateToFolderSettings]);
133+
104134
trpcReact.ui.onOpenSettings.useSubscription(undefined, {
105135
onData: handleOpenSettings,
106136
});

apps/array/src/renderer/components/MainLayout.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { StatusBar } from "@components/StatusBar";
44
import { UpdatePrompt } from "@components/UpdatePrompt";
55
import { CommandMenu } from "@features/command/components/CommandMenu";
66
import { RightSidebar, RightSidebarContent } from "@features/right-sidebar";
7+
import { FolderSettingsView } from "@features/settings/components/FolderSettingsView";
78
import { SettingsView } from "@features/settings/components/SettingsView";
89
import { MainSidebar } from "@features/sidebar/components/MainSidebar";
910
import { TaskDetail } from "@features/task-detail/components/TaskDetail";
@@ -55,6 +56,8 @@ export function MainLayout() {
5556
)}
5657

5758
{view.type === "settings" && <SettingsView />}
59+
60+
{view.type === "folder-settings" && <FolderSettingsView />}
5861
</Box>
5962

6063
{view.type === "task-detail" && view.data && (

apps/array/src/renderer/features/message-editor/components/MessageEditor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export const MessageEditor = forwardRef<EditorHandle, MessageEditorProps>(
113113
onClick={handleContainerClick}
114114
style={{ cursor: "text" }}
115115
>
116-
<div className="max-h-[200px] min-h-[30px] flex-1 overflow-y-auto font-mono text-sm">
116+
<div className="max-h-[200px] min-h-[50px] flex-1 overflow-y-auto font-mono text-sm">
117117
<EditorContent editor={editor} />
118118
</div>
119119

0 commit comments

Comments
 (0)