11import {
22 getState ,
33 setState ,
4+ getTaskUIState ,
45 type Task ,
6+ type TaskDetails ,
57 type TasksPushMessage ,
68 type TaskTemplate ,
79} from "@repo/webview-shared" ;
@@ -15,23 +17,56 @@ import {
1517 ErrorState ,
1618 NoTemplateState ,
1719 NotSupportedState ,
20+ TaskDetailView ,
1821 TaskList ,
1922} from "./components" ;
2023import { POLLING_CONFIG } from "./config" ;
21- import { taskArraysEqual , templateArraysEqual } from "./utils" ;
24+ import {
25+ taskArraysEqual ,
26+ taskDetailsEqual ,
27+ taskEqual ,
28+ templateArraysEqual ,
29+ } from "./utils" ;
2230
2331interface PersistedState {
2432 tasks : Task [ ] ;
2533 templates : TaskTemplate [ ] ;
34+ selectedTaskId : string | null ;
35+ selectedTask : TaskDetails | null ;
2636 createExpanded : boolean ;
2737 historyExpanded : boolean ;
2838 tasksSupported : boolean ;
2939}
3040
41+ function validatePersistedState (
42+ state : PersistedState | undefined ,
43+ ) : PersistedState | undefined {
44+ if ( ! state ) return undefined ;
45+
46+ if ( state . selectedTask && state . tasks . length > 0 ) {
47+ const taskExists = state . tasks . some (
48+ ( t ) => t . id === state . selectedTask ?. task . id ,
49+ ) ;
50+ if ( ! taskExists ) {
51+ return { ...state , selectedTaskId : null , selectedTask : null } ;
52+ }
53+ }
54+
55+ return state ;
56+ }
57+
58+ function isTaskActive ( task : Task | null | undefined ) : boolean {
59+ if ( ! task ) return false ;
60+ const state = getTaskUIState ( task ) ;
61+ return state === "working" || state === "initializing" ;
62+ }
63+
3164export default function App ( ) {
3265 const api = useTasksApi ( ) ;
3366
34- const persistedState = useRef ( getState < PersistedState > ( ) ) ;
67+ const persistedState = useRef (
68+ validatePersistedState ( getState < PersistedState > ( ) ) ,
69+ ) ;
3570 const restored = persistedState . current ;
3671
3772 const [ initialized , setInitialized ] = useState ( ! ! restored ?. tasks ?. length ) ;
@@ -43,22 +78,35 @@ export default function App() {
4378 restored ?. tasksSupported ?? true ,
4479 ) ;
4580
81+ const [ selectedTask , setSelectedTask ] = useState < TaskDetails | null > (
82+ restored ?. selectedTask ?? null ,
83+ ) ;
4684 const [ createExpanded , setCreateExpanded ] = useState (
4785 restored ?. createExpanded ?? true ,
4886 ) ;
4987 const [ historyExpanded , setHistoryExpanded ] = useState (
5088 restored ?. historyExpanded ?? true ,
5189 ) ;
90+ const [ isTransitioning , setIsTransitioning ] = useState ( false ) ;
5291
5392 useEffect ( ( ) => {
5493 setState < PersistedState > ( {
5594 tasks,
5695 templates,
96+ selectedTaskId : selectedTask ?. task . id ?? null ,
97+ selectedTask,
5798 createExpanded,
5899 historyExpanded,
59100 tasksSupported,
60101 } ) ;
61- } , [ tasks , templates , createExpanded , historyExpanded , tasksSupported ] ) ;
102+ } , [
103+ tasks ,
104+ templates ,
105+ selectedTask ,
106+ createExpanded ,
107+ historyExpanded ,
108+ tasksSupported ,
109+ ] ) ;
62110
63111 const [ initLoading , setInitLoading ] = useState ( ! restored ?. tasks ?. length ) ;
64112 const [ initError , setInitError ] = useState < string | null > ( null ) ;
@@ -76,6 +124,16 @@ export default function App() {
76124 setTasksSupported ( data . tasksSupported ) ;
77125 setInitialized ( true ) ;
78126 setInitError ( null ) ;
127+
128+ if ( selectedTaskRef . current ) {
129+ const taskExists = data . tasks . some (
130+ ( t ) => t . id === selectedTaskRef . current ?. task . id ,
131+ ) ;
132+ if ( ! taskExists ) {
133+ expectedTaskIdRef . current = null ;
134+ setSelectedTask ( null ) ;
135+ }
136+ }
79137 } catch ( err ) {
80138 if ( cancelled ) return ;
81139 setInitError (
@@ -97,12 +155,19 @@ export default function App() {
97155 const tasksRef = useRef < Task [ ] > ( tasks ) ;
98156 tasksRef . current = tasks ;
99157
158+ const selectedTaskRef = useRef < TaskDetails | null > ( selectedTask ) ;
159+ selectedTaskRef . current = selectedTask ;
160+
100161 const templatesRef = useRef < TaskTemplate [ ] > ( templates ) ;
101162 templatesRef . current = templates ;
102163
103- // Poll for task list updates
164+ const expectedTaskIdRef = useRef < string | null > (
165+ restored ?. selectedTaskId ?? null ,
166+ ) ;
167+
168+ // Poll for task list updates when not viewing a specific task
104169 useEffect ( ( ) => {
105- if ( ! initialized ) return ;
170+ if ( ! initialized || selectedTask ) return ;
106171
107172 let cancelled = false ;
108173 const pollInterval = setInterval ( ( ) => {
@@ -121,7 +186,38 @@ export default function App() {
121186 cancelled = true ;
122187 clearInterval ( pollInterval ) ;
123188 } ;
124- } , [ api , initialized ] ) ;
189+ } , [ api , initialized , selectedTask ] ) ;
190+
191+ const selectedTaskId = selectedTask ?. task . id ?? null ;
192+ const isActive = isTaskActive ( selectedTask ?. task ) ;
193+
194+ // Poll for selected task with adaptive interval based on task state
195+ useEffect ( ( ) => {
196+ if ( ! initialized || ! selectedTaskId ) return ;
197+
198+ let cancelled = false ;
199+ const interval = isActive
200+ ? POLLING_CONFIG . TASK_ACTIVE_INTERVAL_MS
201+ : POLLING_CONFIG . TASK_IDLE_INTERVAL_MS ;
202+
203+ const poll = ( ) => {
204+ api
205+ . getTaskDetails ( selectedTaskId )
206+ . then ( ( details ) => {
207+ if ( cancelled || expectedTaskIdRef . current !== selectedTaskId ) return ;
208+ if ( ! taskDetailsEqual ( selectedTaskRef . current , details ) ) {
209+ setSelectedTask ( details ) ;
210+ }
211+ } )
212+ . catch ( ( ) => undefined ) ;
213+ } ;
214+
215+ const pollInterval = setInterval ( poll , interval ) ;
216+ return ( ) => {
217+ cancelled = true ;
218+ clearInterval ( pollInterval ) ;
219+ } ;
220+ } , [ api , initialized , selectedTaskId , isActive ] ) ;
125221
126222 const handleRetry = useCallback ( ( ) => {
127223 setInitLoading ( true ) ;
@@ -147,18 +243,44 @@ export default function App() {
147243
148244 useMessage < TasksPushMessage > ( ( msg ) => {
149245 switch ( msg . type ) {
150- case "tasksUpdated" :
246+ case "tasksUpdated" : {
151247 setTasks ( msg . data ) ;
248+ const currentSelectedId = selectedTaskRef . current ?. task . id ;
249+ if ( currentSelectedId ) {
250+ const updatedTask = msg . data . find ( ( t ) => t . id === currentSelectedId ) ;
251+ if (
252+ updatedTask &&
253+ ! taskEqual ( selectedTaskRef . current ?. task , updatedTask )
254+ ) {
255+ setSelectedTask ( ( prev ) =>
256+ prev ? { ...prev , task : updatedTask } : null ,
257+ ) ;
258+ }
259+ }
152260 break ;
261+ }
153262
154263 case "taskUpdated" : {
155264 const updatedTask = msg . data ;
156265 setTasks ( ( prev ) =>
157266 prev . map ( ( t ) => ( t . id === updatedTask . id ? updatedTask : t ) ) ,
158267 ) ;
268+ if ( selectedTaskRef . current ?. task . id === updatedTask . id ) {
269+ setSelectedTask ( ( prev ) =>
270+ prev ? { ...prev , task : updatedTask } : null ,
271+ ) ;
272+ }
159273 break ;
160274 }
161275
276+ case "logsAppend" :
277+ if ( selectedTaskRef . current ) {
278+ setSelectedTask ( ( prev ) =>
279+ prev ? { ...prev , logs : [ ...prev . logs , ...msg . data ] } : null ,
280+ ) ;
281+ }
282+ break ;
283+
162284 case "refresh" : {
163285 api
164286 . getTasks ( )
@@ -176,22 +298,63 @@ export default function App() {
176298 }
177299 } )
178300 . catch ( ( ) => undefined ) ;
301+ const taskIdAtRequest = selectedTaskRef . current ?. task . id ;
302+ if ( taskIdAtRequest ) {
303+ api
304+ . getTaskDetails ( taskIdAtRequest )
305+ . then ( ( details ) => {
306+ if ( expectedTaskIdRef . current !== taskIdAtRequest ) return ;
307+ if ( ! taskDetailsEqual ( selectedTaskRef . current , details ) ) {
308+ setSelectedTask ( details ) ;
309+ }
310+ } )
311+ . catch ( ( ) => undefined ) ;
312+ }
179313 break ;
180314 }
181315
182316 case "showCreateForm" :
317+ setSelectedTask ( null ) ;
183318 setCreateExpanded ( true ) ;
184319 break ;
185-
186- case "logsAppend" :
187- // Task detail view will handle this in next PR
188- break ;
189320 }
190321 } ) ;
191322
192- const handleSelectTask = useCallback ( ( _taskId : string ) => {
193- // Task detail view will be added in next PR
194- } , [ ] ) ;
323+ const handleSelectTask = useCallback (
324+ ( taskId : string ) => {
325+ expectedTaskIdRef . current = taskId ;
326+ setIsTransitioning ( true ) ;
327+
328+ api
329+ . getTaskDetails ( taskId )
330+ . then ( ( details ) => {
331+ if ( expectedTaskIdRef . current === taskId ) {
332+ setSelectedTask ( details ) ;
333+ setIsTransitioning ( false ) ;
334+ }
335+ } )
336+ . catch ( ( ) => {
337+ if ( expectedTaskIdRef . current === taskId ) {
338+ setIsTransitioning ( false ) ;
339+ }
340+ } ) ;
341+ } ,
342+ [ api ] ,
343+ ) ;
344+
345+ const handleDeselectTask = useCallback ( ( ) => {
346+ expectedTaskIdRef . current = null ;
347+ setSelectedTask ( null ) ;
348+
349+ api
350+ . getTasks ( )
351+ . then ( ( updatedTasks ) => {
352+ if ( ! taskArraysEqual ( tasksRef . current , updatedTasks ) ) {
353+ setTasks ( updatedTasks ) ;
354+ }
355+ } )
356+ . catch ( ( ) => undefined ) ;
357+ } , [ api ] ) ;
195358
196359 if ( initLoading ) {
197360 return (
@@ -228,7 +391,18 @@ export default function App() {
228391 expanded = { historyExpanded }
229392 onToggle = { ( ) => setHistoryExpanded ( ! historyExpanded ) }
230393 >
231- < TaskList tasks = { tasks } onSelectTask = { handleSelectTask } />
394+ < div
395+ className = { `task-history-content ${ isTransitioning ? "transitioning" : "" } ` }
396+ >
397+ { selectedTask ? (
398+ < TaskDetailView
399+ details = { selectedTask }
400+ onBack = { handleDeselectTask }
401+ />
402+ ) : (
403+ < TaskList tasks = { tasks } onSelectTask = { handleSelectTask } />
404+ ) }
405+ </ div >
232406 </ CollapsibleSection >
233407 </ div >
234408 ) ;
0 commit comments