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' ;
33import { Task } from '@shared/types' ;
44import { format } from 'date-fns' ;
5- import { useAuthStore } from '../stores/authStore' ;
65import { useStatusBarStore } from '../stores/statusBarStore' ;
6+ import { useTaskExecutionStore } from '../stores/taskExecutionStore' ;
77import { LogView } from './LogView' ;
88
99interface TaskDetailProps {
1010 task : Task ;
1111}
1212
1313export 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