11import { useState , useCallback , useEffect , useMemo } from "react" ;
2- import { QueryClient , QueryClientProvider , useQuery } from "@tanstack/react-query" ;
2+ import { QueryClient , QueryClientProvider } from "@tanstack/react-query" ;
33import { Header } from "./components/layout/Header" ;
44import { Board } from "./components/kanban/Board" ;
55import { TaskDetail } from "./components/task/TaskDetail" ;
66import { CommandPalette } from "./components/layout/CommandPalette" ;
77import { useWebSocket } from "./hooks/useWebSocket" ;
88import { useCommitToasts } from "./hooks/useCommitToasts" ;
9- import { useProjects } from "./hooks/useProjects" ;
9+ import { useProjectTaskOverviews , useProjects } from "./hooks/useProjects" ;
1010import { useTasks } from "./hooks/useTasks" ;
1111import { useTheme } from "./hooks/useTheme" ;
1212import { useKeyboardShortcut } from "./hooks/useKeyboardShortcut" ;
1313import { ChatBubble } from "./components/chat/ChatBubble" ;
1414import { ChatPanel } from "./components/chat/ChatPanel" ;
15- import { calculateTaskMetrics } from "./lib/taskMetrics" ;
15+ import { calculateOverviewMetrics , calculateTaskMetrics } from "./lib/taskMetrics" ;
1616import { readStorage , writeStorage , removeStorage } from "./lib/storage" ;
1717import { STORAGE_KEYS } from "./lib/storageKeys" ;
18- import { api } from "./lib/api" ;
19- import type { Project , Task } from "@aif/shared/browser" ;
18+ import type { Project } from "@aif/shared/browser" ;
2019import { ProjectRuntimeSettings } from "./components/project/ProjectRuntimeSettings" ;
2120import { ProjectsOverview } from "./components/project/ProjectsOverview" ;
2221import { ToastProvider } from "./components/ui/toast" ;
@@ -30,13 +29,31 @@ const queryClient = new QueryClient({
3029 } ,
3130} ) ;
3231
32+ const PROJECT_ROUTE_PATTERN = / ^ \/ p r o j e c t \/ ( [ ^ / ] + ) (?: \/ t a s k \/ ( [ ^ / ] + ) ) ? / ;
33+
34+ function readInitialSelection ( ) : { projectId : string | null ; taskId : string | null } {
35+ const match = window . location . pathname . match ( PROJECT_ROUTE_PATTERN ) ;
36+ if ( match ) {
37+ return { projectId : match [ 1 ] ?? null , taskId : match [ 2 ] ?? null } ;
38+ }
39+
40+ return {
41+ projectId : readStorage ( STORAGE_KEYS . SELECTED_PROJECT ) ,
42+ taskId : null ,
43+ } ;
44+ }
45+
3346function AppContent ( ) {
3447 useWebSocket ( ) ;
3548 useCommitToasts ( ) ;
3649 const { theme, toggleTheme } = useTheme ( ) ;
3750 const { data : projects } = useProjects ( ) ;
38- const [ selectedProjectId , setSelectedProjectId ] = useState < string | null > ( null ) ;
39- const [ selectedTaskId , setSelectedTaskId ] = useState < string | null > ( null ) ;
51+ const [ selectedProjectId , setSelectedProjectId ] = useState < string | null > (
52+ ( ) => readInitialSelection ( ) . projectId ,
53+ ) ;
54+ const [ selectedTaskId , setSelectedTaskId ] = useState < string | null > (
55+ ( ) => readInitialSelection ( ) . taskId ,
56+ ) ;
4057 const [ commandOpen , setCommandOpen ] = useState ( false ) ;
4158 const [ chatOpen , setChatOpen ] = useState ( false ) ;
4259 const [ runtimeSettingsOpen , setRuntimeSettingsOpen ] = useState ( false ) ;
@@ -52,18 +69,17 @@ function AppContent() {
5269 ( ) => projects ?. find ( ( candidate ) => candidate . id === selectedProjectId ) ?? null ,
5370 [ projects , selectedProjectId ] ,
5471 ) ;
55- const { data : projectTasks } = useTasks ( project ?. id ?? null ) ;
56- const { data : allTasks } = useQuery < Task [ ] > ( {
57- queryKey : [ "tasks" , "all" ] ,
58- queryFn : ( ) => api . listTasks ( ) ,
59- enabled : ! project ,
60- } ) ;
72+ const { data : projectTasks } = useTasks ( selectedProjectId ) ;
73+ const { data : projectTaskOverviews } = useProjectTaskOverviews ( ! selectedProjectId ) ;
6174 const taskMetrics = useMemo (
62- ( ) => calculateTaskMetrics ( ( project ? projectTasks : allTasks ) ?? [ ] ) ,
63- [ project , projectTasks , allTasks ] ,
75+ ( ) =>
76+ selectedProjectId
77+ ? calculateTaskMetrics ( projectTasks ?? [ ] )
78+ : calculateOverviewMetrics ( projectTaskOverviews ?? [ ] ) ,
79+ [ projectTaskOverviews , projectTasks , selectedProjectId ] ,
6480 ) ;
6581 const aggregateProjectTotals = useMemo ( ( ) => {
66- if ( project || ! projects ?. length ) return null ;
82+ if ( selectedProjectId || ! projects ?. length ) return null ;
6783 return projects . reduce (
6884 ( acc , p ) => ( {
6985 tokenInput : acc . tokenInput + ( p . tokenInput ?? 0 ) ,
@@ -73,7 +89,7 @@ function AppContent() {
7389 } ) ,
7490 { tokenInput : 0 , tokenOutput : 0 , tokenTotal : 0 , costUsd : 0 } ,
7591 ) ;
76- } , [ project , projects ] ) ;
92+ } , [ projects , selectedProjectId ] ) ;
7793
7894 useEffect ( ( ) => {
7995 writeStorage ( STORAGE_KEYS . DENSITY , density ) ;
@@ -83,50 +99,40 @@ function AppContent() {
8399 writeStorage ( STORAGE_KEYS . VIEW_MODE , viewMode ) ;
84100 } , [ viewMode ] ) ;
85101
86- // Restore state from URL or localStorage on initial load
102+ // Validate restored state after projects load.
87103 useEffect ( ( ) => {
88- if ( ! projects ?. length ) return ;
89- if ( selectedProjectId ) return ;
104+ if ( ! projects || ! selectedProjectId ) return ;
90105
91- const match = window . location . pathname . match ( / ^ \/ p r o j e c t \/ ( [ ^ / ] + ) (?: \/ t a s k \/ ( [ ^ / ] + ) ) ? / ) ;
92- if ( match ) {
93- const urlProjectId = match [ 1 ] ;
94- const urlTaskId = match [ 2 ] ?? null ;
95- const found = projects . find ( ( p ) => p . id === urlProjectId ) ;
96- if ( found ) {
97- queueMicrotask ( ( ) => {
98- setSelectedProjectId ( found . id ) ;
99- writeStorage ( STORAGE_KEYS . SELECTED_PROJECT , found . id ) ;
100- if ( urlTaskId ) setSelectedTaskId ( urlTaskId ) ;
101- } ) ;
102- return ;
103- }
106+ const found = projects . find ( ( p ) => p . id === selectedProjectId ) ;
107+ if ( found ) {
108+ writeStorage ( STORAGE_KEYS . SELECTED_PROJECT , found . id ) ;
109+ return ;
104110 }
105111
106- const savedId = readStorage ( STORAGE_KEYS . SELECTED_PROJECT ) ;
107- if ( savedId ) {
108- const found = projects . find ( ( p ) => p . id === savedId ) ;
109- if ( found ) {
110- queueMicrotask ( ( ) => {
111- setSelectedProjectId ( found . id ) ;
112- } ) ;
113- }
114- }
112+ console . debug ( "[app] Clearing stale selected project" , {
113+ selectedProjectId,
114+ reason : "missing_from_projects" ,
115+ } ) ;
116+
117+ const clearTimer = window . setTimeout ( ( ) => {
118+ setSelectedProjectId ( null ) ;
119+ setSelectedTaskId ( null ) ;
120+ removeStorage ( STORAGE_KEYS . SELECTED_PROJECT ) ;
121+ } , 0 ) ;
122+
123+ return ( ) => window . clearTimeout ( clearTimer ) ;
115124 } , [ projects , selectedProjectId ] ) ;
116125
117126 // Handle browser back/forward
118127 useEffect ( ( ) => {
119128 const onPopState = ( ) => {
120- const match = window . location . pathname . match ( / ^ \/ p r o j e c t \/ ( [ ^ / ] + ) (?: \/ t a s k \/ ( [ ^ / ] + ) ) ? / ) ;
129+ const match = window . location . pathname . match ( PROJECT_ROUTE_PATTERN ) ;
121130 if ( match ) {
122131 const urlProjectId = match [ 1 ] ;
123132 const urlTaskId = match [ 2 ] ?? null ;
124- const found = projects ?. find ( ( p ) => p . id === urlProjectId ) ;
125- if ( found ) {
126- setSelectedProjectId ( found . id ) ;
127- setSelectedTaskId ( urlTaskId ) ;
128- return ;
129- }
133+ setSelectedProjectId ( urlProjectId ) ;
134+ setSelectedTaskId ( urlTaskId ) ;
135+ return ;
130136 }
131137 setSelectedProjectId ( null ) ;
132138 setSelectedTaskId ( null ) ;
@@ -253,7 +259,7 @@ function AppContent() {
253259 onOpenChange = { setCommandOpen }
254260 projects = { projects ?? [ ] }
255261 tasks = { projectTasks ?? [ ] }
256- selectedProjectId = { project ?. id ?? null }
262+ selectedProjectId = { selectedProjectId }
257263 density = { density }
258264 theme = { theme }
259265 onSelectProject = { handleSelectProject }
0 commit comments