@@ -162,6 +162,19 @@ actor SessionStore {
162162
163163 processToolTracking ( event: event, session: & session)
164164 processSubagentTracking ( event: event, session: & session)
165+ processTaskTracking ( event: event, session: & session)
166+
167+ // Debug: log tool names to diagnose plan tracking
168+ if let tool = event. tool {
169+ Self . logger. debug ( " Hook event: \( event. event, privacy: . public) tool= \( tool, privacy: . public) " )
170+ }
171+ processPlanTracking ( event: event, session: & session)
172+
173+ if event. event == " UserPromptSubmit " {
174+ session. resetTasks ( )
175+ session. planContent = nil
176+ session. planFilePath = nil
177+ }
165178
166179 if event. event == " Stop " {
167180 session. subagentState = SubagentState ( )
@@ -318,6 +331,98 @@ actor SessionStore {
318331 }
319332 }
320333
334+ private func processTaskTracking( event: HookEvent , session: inout SessionState ) {
335+ switch event. event {
336+ case " PreToolUse " :
337+ guard let toolName = event. tool else { return }
338+
339+ if toolName == " TaskCreate " {
340+ if let subject = event. toolInput ? [ " subject " ] ? . value as? String {
341+ session. addTask ( subject: subject)
342+ Self . logger. debug ( " TaskCreate: added task ' \( subject, privacy: . public) ' " )
343+ }
344+ } else if toolName == " TaskUpdate " {
345+ if let taskId = event. toolInput ? [ " taskId " ] ? . value as? String {
346+ let existingIds = session. tasks. map { $0. id } . joined ( separator: " , " )
347+ Self . logger. debug ( " TaskUpdate: looking for \( taskId, privacy: . public) in [ \( existingIds, privacy: . public) ] " )
348+ if let statusStr = event. toolInput ? [ " status " ] ? . value as? String ,
349+ let status = TaskItem . TaskStatus ( rawValue: statusStr) {
350+ session. updateTask ( taskId: taskId, status: status)
351+ Self . logger. debug ( " TaskUpdate: \( taskId, privacy: . public) → \( statusStr, privacy: . public) " )
352+ }
353+ if let subject = event. toolInput ? [ " subject " ] ? . value as? String ,
354+ let idx = session. tasks. firstIndex ( where: { $0. id == taskId } ) {
355+ session. tasks [ idx] . subject = subject
356+ }
357+ }
358+ } else if toolName == " TaskList " {
359+ session. pruneStaleTemporaryTasks ( )
360+ session. pruneCompletedTasks ( )
361+ }
362+
363+ case " PostToolUse " :
364+ guard event. tool == " TaskCreate " else { return }
365+
366+ // Primary: use resolved_task_id from tool_response (parsed by Python)
367+ if let realId = event. resolvedTaskId {
368+ let subject = event. resolvedTaskSubject
369+ ?? event. toolInput ? [ " subject " ] ? . value as? String
370+ if let subject {
371+ session. resolveTaskId ( subject: subject, realId: realId)
372+ } else if let idx = session. tasks. firstIndex ( where: { $0. id. hasPrefix ( " _t " ) } ) {
373+ session. tasks [ idx] . id = realId
374+ }
375+ Self . logger. debug ( " TaskCreate resolved: id= \( realId, privacy: . public) " )
376+ }
377+ // Fallback: parse from tool_result string "Task #8 created..."
378+ else if let result = event. toolResult {
379+ if let range = result. range ( of: #"Task #(\d+)"# , options: . regularExpression) ,
380+ let idRange = result [ range] . range ( of: #"\d+"# , options: . regularExpression) {
381+ let realId = String ( result [ idRange] )
382+ if let inputSubject = event. toolInput ? [ " subject " ] ? . value as? String {
383+ session. resolveTaskId ( subject: inputSubject, realId: realId)
384+ } else if let idx = session. tasks. firstIndex ( where: { $0. id. hasPrefix ( " _t " ) } ) {
385+ session. tasks [ idx] . id = realId
386+ }
387+ Self . logger. debug ( " TaskCreate resolved (fallback): id= \( realId, privacy: . public) " )
388+ }
389+ }
390+
391+ default :
392+ break
393+ }
394+ }
395+
396+ // MARK: - Plan Tracking
397+
398+ private func processPlanTracking( event: HookEvent , session: inout SessionState ) {
399+ guard let toolName = event. tool else { return }
400+ let isPlanTool = toolName == " ExitPlanMode "
401+ || toolName == " exit_plan_mode "
402+ || toolName. lowercased ( ) . contains ( " exitplanmode " )
403+ || toolName. lowercased ( ) . contains ( " planmode " )
404+
405+ guard isPlanTool else { return }
406+
407+ // Extract plan content and file path directly from toolInput
408+ if let input = event. toolInput {
409+ if let planContent = input [ " plan " ] ? . value as? String , !planContent. isEmpty {
410+ session. planContent = planContent
411+ Self . logger. debug ( " Plan loaded from toolInput ( \( planContent. count) chars) " )
412+ }
413+ if let pathStr = input [ " planFilePath " ] ? . value as? String , !pathStr. isEmpty {
414+ session. planFilePath = URL ( fileURLWithPath: pathStr)
415+ }
416+ }
417+
418+ // Fallback: read from known file path
419+ if session. planContent == nil , let path = session. planFilePath,
420+ let content = try ? String ( contentsOf: path, encoding: . utf8) {
421+ session. planContent = content
422+ Self . logger. debug ( " Plan loaded from file: \( path. lastPathComponent, privacy: . public) " )
423+ }
424+ }
425+
321426 /// Push the current subagent tool lists from subagentState into the
322427 /// corresponding ChatHistoryItem.subagentTools so the UI renders them live.
323428 private func syncSubagentToolsToChatItems( session: inout SessionState ) {
0 commit comments