Skip to content

Commit ad27a47

Browse files
committed
refactor: move task execution logic to store
1 parent 8683431 commit ad27a47

File tree

3 files changed

+392
-200
lines changed

3 files changed

+392
-200
lines changed

src/main/services/agent.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ export function registerAgentIpc(taskControllers: Map<string, TaskController>, g
2020
throw new Error('prompt and repoPath are required');
2121
}
2222

23-
// Now using top-level import since we're compiling to ES modules
24-
2523
const agent = createAgent(
2624
new ClaudeCodeAgent({
2725
permissionMode: 'bypassPermissions'
@@ -97,8 +95,8 @@ export function registerAgentIpc(taskControllers: Map<string, TaskController>, g
9795
const { taskId, stream } = await agent.run({
9896
prompt: fullPrompt,
9997
repoPath,
100-
signal: abortController.signal,
101-
} as any);
98+
permissionMode: 'auto',
99+
});
102100

103101
const channel = `agent-event:${taskId}`;
104102

src/renderer/components/TaskDetail.tsx

Lines changed: 28 additions & 196 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
1-
import React, { useEffect, useRef, useState } from 'react';
2-
import { Flex, Box, Heading, Text, Badge, Button, Link, SegmentedControl, DataList } from '@radix-ui/themes';
1+
import React, { useEffect } from 'react';
2+
import { Flex, Box, Text, Badge, Button, Link, SegmentedControl, DataList, Code } from '@radix-ui/themes';
33
import { Task } from '@shared/types';
44
import { format } from 'date-fns';
5-
import { useAuthStore } from '../stores/authStore';
65
import { useStatusBarStore } from '../stores/statusBarStore';
6+
import { useTaskExecutionStore } from '../stores/taskExecutionStore';
77
import { LogView } from './LogView';
88

99
interface TaskDetailProps {
1010
task: Task;
1111
}
1212

1313
export function TaskDetail({ task }: TaskDetailProps) {
14-
const { client } = useAuthStore();
1514
const { setStatusBar, reset } = useStatusBarStore();
16-
const [isRunning, setIsRunning] = useState(false);
17-
const [logs, setLogs] = useState<any[]>([]);
18-
const [repoPath, setRepoPath] = useState<string | null>(null);
19-
const [currentTaskId, setCurrentTaskId] = useState<string | null>(null);
20-
const unsubscribeRef = useRef<null | (() => void)>(null);
21-
const [runMode, setRunMode] = useState<'local' | 'cloud'>('local');
15+
const {
16+
getTaskState,
17+
setRunMode: setStoreRunMode,
18+
selectRepositoryForTask,
19+
runTask,
20+
cancelTask,
21+
} = useTaskExecutionStore();
22+
23+
// Get persistent state for this task
24+
const taskState = getTaskState(task.id);
25+
const { isRunning, logs, repoPath, runMode } = taskState;
2226

2327
useEffect(() => {
2428
setStatusBar({
@@ -41,203 +45,31 @@ export function TaskDetail({ task }: TaskDetailProps) {
4145
};
4246
}, [setStatusBar, reset, isRunning]);
4347

44-
const handleSelectRepo = async () => {
45-
try {
46-
const selected = await window.electronAPI.selectDirectory();
47-
if (selected) {
48-
const isRepo = await window.electronAPI.validateRepo(selected);
49-
if (!isRepo) {
50-
setLogs(prev => [...prev, `Selected folder is not a git repository: ${selected}`]);
51-
return;
52-
}
53-
const canWrite = await window.electronAPI.checkWriteAccess(selected);
54-
if (!canWrite) {
55-
setLogs(prev => [...prev, `No write permission in selected folder: ${selected}`]);
56-
const { response } = await window.electronAPI.showMessageBox({
57-
type: 'warning',
58-
title: 'Folder is not writable',
59-
message: 'The selected folder is not writable by the app.',
60-
detail: 'Grant access by selecting a different folder or adjusting permissions.',
61-
buttons: ['Grant Access', 'Cancel'],
62-
defaultId: 0,
63-
cancelId: 1,
64-
});
65-
if (response === 0) {
66-
// Let user reselect and validate again
67-
return handleSelectRepo();
68-
}
69-
return;
70-
}
71-
setRepoPath(selected);
72-
}
73-
} catch (err) {
74-
setLogs(prev => [...prev, `Error selecting directory: ${err instanceof Error ? err.message : String(err)}`]);
75-
}
48+
// Simple event handlers that delegate to store actions
49+
const handleSelectRepo = () => {
50+
selectRepositoryForTask(task.id);
7651
};
7752

78-
const handleRunTask = async () => {
79-
if (isRunning) return;
80-
81-
// Ensure repo path is selected
82-
let effectiveRepoPath = repoPath;
83-
if (!effectiveRepoPath) {
84-
await handleSelectRepo();
85-
effectiveRepoPath = repoPath;
86-
}
87-
if (!effectiveRepoPath) {
88-
setLogs(prev => [...prev, 'No repository folder selected. ']);
89-
return;
90-
}
91-
const isRepo = await window.electronAPI.validateRepo(effectiveRepoPath);
92-
if (!isRepo) {
93-
setLogs(prev => [...prev, `Selected folder is not a git repository: ${effectiveRepoPath}`]);
94-
return;
95-
}
96-
const canWrite = await window.electronAPI.checkWriteAccess(effectiveRepoPath);
97-
if (!canWrite) {
98-
setLogs(prev => [...prev, `No write permission in selected folder: ${effectiveRepoPath}`]);
99-
const { response } = await window.electronAPI.showMessageBox({
100-
type: 'warning',
101-
title: 'Folder is not writable',
102-
message: 'This folder is not writable by the app.',
103-
detail: 'Grant access by selecting a different folder or adjusting permissions.',
104-
buttons: ['Grant Access', 'Cancel'],
105-
defaultId: 0,
106-
cancelId: 1,
107-
});
108-
if (response === 0) {
109-
await handleSelectRepo();
110-
}
111-
return;
112-
}
113-
114-
// Build a helpful prompt for the agent
115-
const promptLines: string[] = [];
116-
promptLines.push(`Task: ${task.title}`);
117-
if (task.description) {
118-
promptLines.push('Description:');
119-
promptLines.push(task.description);
120-
}
121-
const prompt = promptLines.join('\n');
122-
123-
setIsRunning(true);
124-
setLogs([
125-
{ type: 'text', ts: Date.now(), content: `Starting ${runMode} Claude Code agent...` },
126-
{ type: 'text', ts: Date.now(), content: `Repo: ${effectiveRepoPath}` },
127-
]);
128-
129-
try {
130-
const { taskId, channel } = await window.electronAPI.agentStart({
131-
prompt,
132-
repoPath: effectiveRepoPath,
133-
model: 'claude-4-sonnet',
134-
});
135-
setCurrentTaskId(taskId);
136-
137-
// Subscribe to streaming events
138-
if (unsubscribeRef.current) {
139-
unsubscribeRef.current();
140-
unsubscribeRef.current = null;
141-
}
142-
unsubscribeRef.current = window.electronAPI.onAgentEvent(channel, (ev: any) => {
143-
switch (ev?.type) {
144-
case 'token':
145-
if (typeof ev.content === 'string' && ev.content.trim().length > 0) {
146-
setLogs(prev => [...prev, { type: 'text', ts: ev.ts || Date.now(), content: ev.content }]);
147-
}
148-
break;
149-
case 'status':
150-
if (ev.phase) {
151-
setLogs(prev => [...prev, { type: 'status', ts: ev.ts || Date.now(), phase: ev.phase }]);
152-
} else if (ev.message) {
153-
setLogs(prev => [...prev, { type: 'text', ts: ev.ts || Date.now(), content: ev.message }]);
154-
}
155-
break;
156-
case 'tool_call':
157-
setLogs(prev => [
158-
...prev,
159-
{ type: 'tool_call', ts: ev.ts || Date.now(), toolName: ev.toolName || ev.tool || ev.name || 'unknown-tool', callId: ev.callId, args: ev.args ?? ev.input },
160-
]);
161-
break;
162-
case 'tool_result':
163-
setLogs(prev => [
164-
...prev,
165-
{ type: 'tool_result', ts: ev.ts || Date.now(), toolName: ev.toolName || ev.tool || ev.name || 'unknown-tool', callId: ev.callId, result: ev.result ?? ev.output },
166-
]);
167-
break;
168-
case 'diff':
169-
setLogs(prev => [
170-
...prev,
171-
{ type: 'diff', ts: ev.ts || Date.now(), file: ev.file || ev.path || '', patch: ev.patch ?? ev.patchText ?? ev.diff, summary: ev.summary },
172-
]);
173-
break;
174-
case 'file_write':
175-
setLogs(prev => [
176-
...prev,
177-
{ type: 'file_write', ts: ev.ts || Date.now(), path: ev.path || '', bytes: ev.bytes },
178-
]);
179-
break;
180-
case 'metric':
181-
setLogs(prev => [
182-
...prev,
183-
{ type: 'metric', ts: ev.ts || Date.now(), key: ev.key || '', value: ev.value ?? 0, unit: ev.unit },
184-
]);
185-
break;
186-
case 'artifact':
187-
setLogs(prev => [
188-
...prev,
189-
{ type: 'artifact', ts: ev.ts || Date.now(), kind: ev.kind || 'artifact', content: ev.content },
190-
]);
191-
break;
192-
case 'error':
193-
setLogs(prev => [...prev, { type: 'text', ts: ev.ts || Date.now(), level: 'error', content: `error: ${ev.message || 'Unknown error'}` }]);
194-
setIsRunning(false);
195-
break;
196-
case 'done':
197-
setLogs(prev => [...prev, { type: 'text', ts: ev.ts || Date.now(), content: ev.success ? 'Agent run completed' : 'Agent run ended with errors' }]);
198-
setIsRunning(false);
199-
if (unsubscribeRef.current) {
200-
unsubscribeRef.current();
201-
unsubscribeRef.current = null;
202-
}
203-
break;
204-
default:
205-
setLogs(prev => [...prev, `event: ${JSON.stringify(ev)}`]);
206-
}
207-
});
208-
} catch (error) {
209-
setLogs(prev => [...prev, `Error starting agent: ${error instanceof Error ? error.message : 'Unknown error'}`]);
210-
setIsRunning(false);
211-
}
53+
const handleRunTask = () => {
54+
runTask(task.id, task);
21255
};
21356

214-
const handleCancel = async () => {
215-
if (!currentTaskId) return;
216-
try {
217-
await window.electronAPI.agentCancel(currentTaskId);
218-
} catch { }
219-
setIsRunning(false);
220-
if (unsubscribeRef.current) {
221-
unsubscribeRef.current();
222-
unsubscribeRef.current = null;
223-
}
57+
const handleCancel = () => {
58+
cancelTask(task.id);
22459
};
22560

226-
useEffect(() => {
227-
return () => {
228-
if (unsubscribeRef.current) {
229-
unsubscribeRef.current();
230-
unsubscribeRef.current = null;
231-
}
232-
};
233-
}, []);
61+
const handleRunModeChange = (value: string) => {
62+
setStoreRunMode(task.id, value as 'local' | 'cloud');
63+
};
23464

23565
return (
23666
<Flex height="100%">
23767
{/* Left pane - Task details */}
23868
<Box width="50%" className="border-r border-gray-6" overflowY="auto">
23969
<Box p="4">
240-
<Heading size="5" mb="4">{task.title}</Heading>
70+
<Box mb="4">
71+
<Code size="5">{task.title}</Code>
72+
</Box>
24173

24274
<DataList.Root>
24375
<DataList.Item>
@@ -308,7 +140,7 @@ export function TaskDetail({ task }: TaskDetailProps) {
308140
<DataList.Value>
309141
<SegmentedControl.Root
310142
value={runMode}
311-
onValueChange={(value) => setRunMode(value as 'local' | 'cloud')}
143+
onValueChange={handleRunModeChange}
312144
>
313145
<SegmentedControl.Item value="local">Local</SegmentedControl.Item>
314146
<SegmentedControl.Item value="cloud">Cloud</SegmentedControl.Item>

0 commit comments

Comments
 (0)