Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/array/src/main/services/context-menu/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@ export type TabAction = z.infer<typeof tabAction>;
export type FileAction = z.infer<typeof fileAction>;
export type SplitDirection = z.infer<typeof splitDirection>;

export const confirmDeleteTaskInput = z.object({
taskTitle: z.string(),
hasWorktree: z.boolean(),
});

export const confirmDeleteTaskOutput = z.object({
confirmed: z.boolean(),
});

export type ConfirmDeleteTaskInput = z.infer<typeof confirmDeleteTaskInput>;
export type ConfirmDeleteTaskResult = z.infer<typeof confirmDeleteTaskOutput>;

export type TaskContextMenuResult = z.infer<typeof taskContextMenuOutput>;
export type FolderContextMenuResult = z.infer<typeof folderContextMenuOutput>;
export type TabContextMenuResult = z.infer<typeof tabContextMenuOutput>;
Expand Down
33 changes: 18 additions & 15 deletions apps/array/src/main/services/context-menu/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { MAIN_TOKENS } from "../../di/tokens.js";
import { getMainWindow } from "../../trpc/context.js";
import type { ExternalAppsService } from "../external-apps/service.js";
import type {
ConfirmDeleteTaskInput,
ConfirmDeleteTaskResult,
FileAction,
FileContextMenuInput,
FileContextMenuResult,
Expand Down Expand Up @@ -49,30 +51,31 @@ export class ContextMenuService {
return { apps, lastUsedAppId: lastUsed.lastUsedApp };
}

async confirmDeleteTask(
input: ConfirmDeleteTaskInput,
): Promise<ConfirmDeleteTaskResult> {
const confirmed = await this.confirm({
title: "Delete Task",
message: `Delete "${input.taskTitle}"?`,
detail: input.hasWorktree
? "This will permanently delete the task and its associated worktree."
: "This will permanently delete the task.",
confirmLabel: "Delete",
});
return { confirmed };
}

async showTaskContextMenu(
input: TaskContextMenuInput,
): Promise<TaskContextMenuResult> {
const { taskTitle, worktreePath } = input;
const { worktreePath } = input;
const { apps, lastUsedAppId } = await this.getExternalAppsData();

return this.showMenu<TaskAction>([
this.item("Rename", { type: "rename" }),
this.item("Duplicate", { type: "duplicate" }),
this.separator(),
this.item(
"Delete",
{ type: "delete" },
{
confirm: {
title: "Delete Task",
message: `Delete "${taskTitle}"?`,
detail: worktreePath
? "This will permanently delete the task and its associated worktree."
: "This will permanently delete the task.",
confirmLabel: "Delete",
},
},
),
this.item("Delete", { type: "delete" }),
...(worktreePath
? [
this.separator(),
Expand Down
8 changes: 6 additions & 2 deletions apps/array/src/main/services/folders/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ export const registeredFolderSchema = z.object({
createdAt: z.string(),
});

export const getFoldersOutput = z.array(registeredFolderSchema);
export const registeredFolderWithExistsSchema = registeredFolderSchema.extend({
exists: z.boolean(),
});

export const getFoldersOutput = z.array(registeredFolderWithExistsSchema);

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

export const addFolderOutput = registeredFolderSchema;
export const addFolderOutput = registeredFolderWithExistsSchema;

export const removeFolderInput = z.object({
folderId: z.string(),
Expand Down
19 changes: 14 additions & 5 deletions apps/array/src/main/services/folders/service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { exec } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { promisify } from "node:util";
import { WorktreeManager } from "@posthog/agent";
Expand All @@ -20,13 +21,21 @@ const log = logger.scope("folders-service");

@injectable()
export class FoldersService {
async getFolders(): Promise<RegisteredFolder[]> {
async getFolders(): Promise<(RegisteredFolder & { exists: boolean })[]> {
const folders = foldersStore.get("folders", []);
// Filter out any folders with empty names (from invalid paths like "/")
return folders.filter((f) => f.name && f.path);
// Also add exists property to check if path is valid on disk
return folders
.filter((f) => f.name && f.path)
.map((f) => ({
...f,
exists: fs.existsSync(f.path),
}));
}

async addFolder(folderPath: string): Promise<RegisteredFolder> {
async addFolder(
folderPath: string,
): Promise<RegisteredFolder & { exists: boolean }> {
// Validate the path before proceeding
const folderName = path.basename(folderPath);
if (!folderPath || !folderName) {
Expand Down Expand Up @@ -69,7 +78,7 @@ export class FoldersService {
if (existing) {
existing.lastAccessed = new Date().toISOString();
foldersStore.set("folders", folders);
return existing;
return { ...existing, exists: true };
}

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

return newFolder;
return { ...newFolder, exists: true };
}

async removeFolder(folderId: string): Promise<void> {
Expand Down
7 changes: 7 additions & 0 deletions apps/array/src/main/trpc/routers/context-menu.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { container } from "../../di/container.js";
import { MAIN_TOKENS } from "../../di/tokens.js";
import {
confirmDeleteTaskInput,
confirmDeleteTaskOutput,
fileContextMenuInput,
fileContextMenuOutput,
folderContextMenuInput,
Expand All @@ -18,6 +20,11 @@ const getService = () =>
container.get<ContextMenuService>(MAIN_TOKENS.ContextMenuService);

export const contextMenuRouter = router({
confirmDeleteTask: publicProcedure
.input(confirmDeleteTaskInput)
.output(confirmDeleteTaskOutput)
.mutation(({ input }) => getService().confirmDeleteTask(input)),

showTaskContextMenu: publicProcedure
.input(taskContextMenuInput)
.output(taskContextMenuOutput)
Expand Down
30 changes: 30 additions & 0 deletions apps/array/src/renderer/components/GlobalEventHandlers.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { usePanelLayoutStore } from "@features/panels/store/panelLayoutStore";
import { useRightSidebarStore } from "@features/right-sidebar";
import { useSidebarStore } from "@features/sidebar/stores/sidebarStore";
import { useWorkspaceStore } from "@features/workspace/stores/workspaceStore";
import { SHORTCUTS } from "@renderer/constants/keyboard-shortcuts";
import { clearApplicationStorage } from "@renderer/lib/clearStorage";
import { useRegisteredFoldersStore } from "@renderer/stores/registeredFoldersStore";
import { useNavigationStore } from "@stores/navigationStore";
import { useCallback, useEffect } from "react";
import { useHotkeys } from "react-hotkeys-hook";
Expand All @@ -23,8 +25,14 @@ export function GlobalEventHandlers({
const navigateToTaskInput = useNavigationStore(
(state) => state.navigateToTaskInput,
);
const navigateToFolderSettings = useNavigationStore(
(state) => state.navigateToFolderSettings,
);
const view = useNavigationStore((state) => state.view);
const goBack = useNavigationStore((state) => state.goBack);
const goForward = useNavigationStore((state) => state.goForward);
const folders = useRegisteredFoldersStore((state) => state.folders);
const workspaces = useWorkspaceStore.use.workspaces();
const clearAllLayouts = usePanelLayoutStore((state) => state.clearAllLayouts);
const toggleLeftSidebar = useSidebarStore((state) => state.toggle);
const toggleRightSidebar = useRightSidebarStore((state) => state.toggle);
Expand Down Expand Up @@ -101,6 +109,28 @@ export function GlobalEventHandlers({
};
}, [goBack, goForward]);

// Reload folders when window regains focus to detect moved/deleted folders
useEffect(() => {
const handleFocus = () => {
useRegisteredFoldersStore.getState().loadFolders();
};
window.addEventListener("focus", handleFocus);
return () => window.removeEventListener("focus", handleFocus);
}, []);

// Check if current task's folder became invalid (e.g., moved while app was open)
useEffect(() => {
if (view.type !== "task-detail" || !view.data) return;

const workspace = workspaces[view.data.id];
if (!workspace?.folderId) return;

const folder = folders.find((f) => f.id === workspace.folderId);
if (folder && folder.exists === false) {
navigateToFolderSettings(folder.id);
}
}, [view, folders, workspaces, navigateToFolderSettings]);

trpcReact.ui.onOpenSettings.useSubscription(undefined, {
onData: handleOpenSettings,
});
Expand Down
3 changes: 3 additions & 0 deletions apps/array/src/renderer/components/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { StatusBar } from "@components/StatusBar";
import { UpdatePrompt } from "@components/UpdatePrompt";
import { CommandMenu } from "@features/command/components/CommandMenu";
import { RightSidebar, RightSidebarContent } from "@features/right-sidebar";
import { FolderSettingsView } from "@features/settings/components/FolderSettingsView";
import { SettingsView } from "@features/settings/components/SettingsView";
import { MainSidebar } from "@features/sidebar/components/MainSidebar";
import { TaskDetail } from "@features/task-detail/components/TaskDetail";
Expand Down Expand Up @@ -55,6 +56,8 @@ export function MainLayout() {
)}

{view.type === "settings" && <SettingsView />}

{view.type === "folder-settings" && <FolderSettingsView />}
</Box>

{view.type === "task-detail" && view.data && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export const MessageEditor = forwardRef<EditorHandle, MessageEditorProps>(
onClick={handleContainerClick}
style={{ cursor: "text" }}
>
<div className="max-h-[200px] min-h-[30px] flex-1 overflow-y-auto font-mono text-sm">
<div className="max-h-[200px] min-h-[50px] flex-1 overflow-y-auto font-mono text-sm">
<EditorContent editor={editor} />
</div>

Expand Down
Loading