Skip to content

Commit 543d2af

Browse files
authored
feat: auto-registering IPC services and UI (#144)
1 parent ba02bc1 commit 543d2af

22 files changed

+1057
-489
lines changed

src/main/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from "electron";
1313
import { ANALYTICS_EVENTS } from "../types/analytics.js";
1414
import { registerAgentIpc, type TaskController } from "./services/agent.js";
15+
import "./services/index.js";
1516
import { registerFsIpc } from "./services/fs.js";
1617
import { registerGitIpc } from "./services/git.js";
1718
import { registerOAuthHandlers } from "./services/oauth.js";

src/main/ipc/createIpcService.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { ipcMain } from "electron";
2+
3+
type IpcHandler<TArgs extends any[], TResult> = (
4+
event: Electron.IpcMainInvokeEvent,
5+
...args: TArgs
6+
) => Promise<TResult> | TResult;
7+
8+
interface IpcServiceConfig<TArgs extends any[], TResult> {
9+
channel: string;
10+
handler: IpcHandler<TArgs, TResult>;
11+
}
12+
13+
export function createIpcService<TArgs extends any[], TResult>(
14+
config: IpcServiceConfig<TArgs, TResult>,
15+
) {
16+
ipcMain.handle(config.channel, config.handler);
17+
18+
return {
19+
channel: config.channel,
20+
};
21+
}

src/main/preload.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
OAuthTokenResponse,
55
StoredOAuthTokens,
66
} from "../shared/types/oauth";
7+
import type { ContextMenuResult } from "./services/contextMenu.types.js";
78

89
interface MessageBoxOptions {
910
type?: "info" | "error" | "warning" | "question";
@@ -242,4 +243,10 @@ contextBridge.exposeInMainWorld("electronAPI", {
242243
ipcRenderer.on(channel, wrapped);
243244
return () => ipcRenderer.removeListener(channel, wrapped);
244245
},
246+
// Context Menu API
247+
showTaskContextMenu: (
248+
taskId: string,
249+
taskTitle: string,
250+
): Promise<ContextMenuResult> =>
251+
ipcRenderer.invoke("show-task-context-menu", taskId, taskTitle),
245252
});

src/main/services/contextMenu.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Menu, type MenuItemConstructorOptions } from "electron";
2+
import { createIpcService } from "../ipc/createIpcService.js";
3+
import type { ContextMenuResult } from "./contextMenu.types.js";
4+
5+
export type {
6+
ContextMenuAction,
7+
ContextMenuResult,
8+
} from "./contextMenu.types.js";
9+
10+
export const showTaskContextMenuService = createIpcService({
11+
channel: "show-task-context-menu",
12+
handler: async (
13+
_event,
14+
_taskId: string,
15+
_taskTitle: string,
16+
): Promise<ContextMenuResult> => {
17+
return new Promise((resolve) => {
18+
const template: MenuItemConstructorOptions[] = [
19+
{
20+
label: "Rename",
21+
click: () => resolve({ action: "rename" }),
22+
},
23+
{
24+
label: "Duplicate",
25+
click: () => resolve({ action: "duplicate" }),
26+
},
27+
{ type: "separator" },
28+
{
29+
label: "Delete",
30+
click: () => resolve({ action: "delete" }),
31+
},
32+
];
33+
34+
const menu = Menu.buildFromTemplate(template);
35+
menu.popup({
36+
callback: () => resolve({ action: null }),
37+
});
38+
});
39+
},
40+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export type ContextMenuAction = "rename" | "duplicate" | "delete" | null;
2+
3+
export interface ContextMenuResult {
4+
action: ContextMenuAction;
5+
}
6+
7+
declare global {
8+
interface IElectronAPI {
9+
showTaskContextMenu: (
10+
taskId: string,
11+
taskTitle: string,
12+
) => Promise<ContextMenuResult>;
13+
}
14+
}

src/main/services/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Auto-register all IPC services
3+
* This file is auto-generated by vite-plugin-auto-services.ts
4+
*/
5+
6+
import "./agent.js";
7+
import "./contextMenu.js";
8+
import "./fs.js";
9+
import "./git.js";
10+
import "./oauth.js";
11+
import "./os.js";
12+
import "./posthog-analytics.js";
13+
import "./posthog.js";
14+
import "./shell.js";
15+
import "./transcription-prompts.js";
16+
import "./updates.js";

src/main/services/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* Auto-import all IPC service types for declaration merging
3+
* This file is auto-generated by vite-plugin-auto-services.ts
4+
*/
5+
6+
import "./contextMenu.types.js";
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { useUpdateTask } from "@features/tasks/hooks/useTasks";
2+
import { Button, Dialog, Flex, Text, TextField } from "@radix-ui/themes";
3+
import type { Task } from "@shared/types";
4+
import { useCallback, useEffect, useState } from "react";
5+
6+
interface RenameTaskDialogProps {
7+
task: Task | null;
8+
open: boolean;
9+
onOpenChange: (open: boolean) => void;
10+
}
11+
12+
export function RenameTaskDialog({
13+
task,
14+
open,
15+
onOpenChange,
16+
}: RenameTaskDialogProps) {
17+
const [newTitle, setNewTitle] = useState("");
18+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
19+
const updateTask = useUpdateTask();
20+
21+
useEffect(() => {
22+
if (task && open) {
23+
setNewTitle(task.title);
24+
setErrorMessage(null);
25+
}
26+
}, [task, open]);
27+
28+
const handleRename = useCallback(async () => {
29+
if (!task || !newTitle.trim()) {
30+
return;
31+
}
32+
33+
try {
34+
await updateTask.mutateAsync({
35+
taskId: task.id,
36+
updates: { title: newTitle.trim() },
37+
});
38+
onOpenChange(false);
39+
} catch (error) {
40+
console.error("[rename] Failed to rename task", error);
41+
setErrorMessage("Failed to rename task. Please try again.");
42+
}
43+
}, [task, newTitle, updateTask, onOpenChange]);
44+
45+
const handleKeyDown = useCallback(
46+
(e: React.KeyboardEvent) => {
47+
if (e.key === "Enter" && !e.shiftKey) {
48+
e.preventDefault();
49+
handleRename();
50+
} else if (e.key === "Escape") {
51+
onOpenChange(false);
52+
}
53+
},
54+
[handleRename, onOpenChange],
55+
);
56+
57+
if (!task) {
58+
return null;
59+
}
60+
61+
return (
62+
<Dialog.Root open={open} onOpenChange={onOpenChange}>
63+
<Dialog.Content maxWidth="450px" size="1">
64+
<Flex direction="column">
65+
<Dialog.Title size="2">Rename task</Dialog.Title>
66+
<TextField.Root
67+
value={newTitle}
68+
onChange={(e) => setNewTitle(e.target.value)}
69+
onKeyDown={handleKeyDown}
70+
placeholder={task.title}
71+
autoFocus
72+
size="1"
73+
mb="2"
74+
/>
75+
{errorMessage && (
76+
<Text size="1" color="red">
77+
{errorMessage}
78+
</Text>
79+
)}
80+
<Flex justify="end" gap="3" mt="2">
81+
<Button
82+
size="1"
83+
type="button"
84+
variant="soft"
85+
color="gray"
86+
onClick={() => onOpenChange(false)}
87+
disabled={updateTask.isPending}
88+
>
89+
Cancel
90+
</Button>
91+
<Button
92+
type="button"
93+
size="1"
94+
onClick={handleRename}
95+
disabled={updateTask.isPending || !newTitle.trim()}
96+
loading={updateTask.isPending}
97+
>
98+
Save
99+
</Button>
100+
</Flex>
101+
</Flex>
102+
</Dialog.Content>
103+
</Dialog.Root>
104+
);
105+
}

src/renderer/components/ui/sidebar/Sidebar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Box, Flex } from "@radix-ui/themes";
33
import { useSidebarStore } from "@stores/sidebarStore";
44
import React from "react";
55

6-
const MIN_WIDTH = 100;
6+
const MIN_WIDTH = 140;
77

88
export const Sidebar: React.FC<{ children: React.ReactNode }> = ({
99
children,

0 commit comments

Comments
 (0)