44 * Uses react-router for page navigation.
55 */
66
7- import { type CSSProperties , useCallback , useMemo , useState } from "react" ;
7+ import { type CSSProperties , useCallback , useEffect , useMemo , useState } from "react" ;
88import { Routes , Route , useNavigate , useLocation , Navigate } from "react-router" ;
99import "./styles/globals.css" ;
1010import { Sidebar } from "./components/sidebar" ;
@@ -16,19 +16,19 @@ import { TaskDetail } from "./features/tasks/task-detail";
1616import { CreateDialog } from "./features/tasks/create-dialog" ;
1717import { InboxPage } from "./features/inbox/inbox-page" ;
1818import { PrManagementPage } from "./features/prs/pr-management-page" ;
19+ import { PrDetailPage } from "./features/prs/pr-detail-page" ;
1920import { SettingsPage } from "./features/settings/settings-page" ;
2021import { useKeyboardShortcuts } from "./hooks/use-keyboard-shortcuts" ;
2122import { useWorkspaceStream } from "./hooks/use-workspace-stream" ;
2223import { useTrayStatus } from "./hooks/use-tray-status" ;
2324import { useGlobalShortcut } from "./hooks/use-global-shortcut" ;
2425import { useWebNotifications } from "./hooks/use-web-notifications" ;
26+ import { useQueryClient } from "@tanstack/react-query" ;
2527import {
2628 useListUnitTasks ,
27- useListNotifications ,
2829 useListPullRequests ,
2930 useCreateUnitTaskMutation ,
3031 useSubmitPlanDecisionMutation ,
31- useMarkNotificationReadMutation ,
3232} from "./hooks/use-dexdex-queries" ;
3333import {
3434 type AppState ,
@@ -38,14 +38,13 @@ import {
3838 getPersistedTheme ,
3939 persistTheme ,
4040 applyThemeToDocument ,
41+ getPersistedActiveWorkspaceId ,
42+ persistActiveWorkspaceId ,
4143} from "./stores/app-store" ;
42- import type { Notification } from "./lib/mock-data" ;
4344import { summarizePrompt } from "./lib/adapters" ;
4445import { PlanDecision } from "./lib/status" ;
4546import { AgentCliType , PlanDecision as ProtoPlanDecision } from "./gen/v1/dexdex_pb" ;
4647
47- const WORKSPACE_ID = "workspace-default" ;
48-
4948interface Tab {
5049 id : string ;
5150 label : string ;
@@ -61,20 +60,20 @@ function App() {
6160 const initialTheme = getPersistedTheme ( ) ;
6261 const [ theme , setThemeState ] = useState < Theme > ( initialTheme ) ;
6362 const [ sidebarOpen , setSidebarOpen ] = useState ( true ) ;
63+ const [ activeWorkspaceId , setActiveWorkspaceIdState ] = useState ( ( ) => getPersistedActiveWorkspaceId ( ) ) ;
6464 const [ connectionStatus , setConnectionStatus ] = useState < AppState [ "connectionStatus" ] > ( "connected" ) ;
6565 const [ commandPaletteOpen , setCommandPaletteOpen ] = useState ( false ) ;
6666 const [ createDialogOpen , setCreateDialogOpen ] = useState ( false ) ;
67+ const [ selectedTaskIndex , setSelectedTaskIndex ] = useState ( - 1 ) ;
6768
6869 // Tab state
6970 const [ tabs , setTabs ] = useState < Tab [ ] > ( [ ] ) ;
7071 const [ activeTabId , setActiveTabId ] = useState ( "" ) ;
7172
7273 // Data state - Connect RPC queries replace mock data
73- const { data : tasks = [ ] , isLoading : tasksLoading } = useListUnitTasks ( WORKSPACE_ID ) ;
74- const { data : notifications = [ ] , isLoading : notificationsLoading } = useListNotifications ( WORKSPACE_ID ) ;
75- const { data : pullRequestsData , isLoading : prsLoading } = useListPullRequests ( WORKSPACE_ID ) ;
74+ const { data : tasks = [ ] , isLoading : tasksLoading } = useListUnitTasks ( activeWorkspaceId ) ;
75+ const { data : pullRequestsData , isLoading : prsLoading } = useListPullRequests ( activeWorkspaceId ) ;
7676 const pullRequests = pullRequestsData ?. pullRequests ?? [ ] ;
77- const markReadMutation = useMarkNotificationReadMutation ( ) ;
7877
7978 // Apply initial theme
8079 useMemo ( ( ) => {
@@ -95,34 +94,46 @@ function App() {
9594 setSidebarOpen ( ( prev ) => ! prev ) ;
9695 } , [ ] ) ;
9796
97+ const setActiveWorkspaceId = useCallback ( ( id : string ) => {
98+ setActiveWorkspaceIdState ( id ) ;
99+ persistActiveWorkspaceId ( id ) ;
100+ } , [ ] ) ;
101+
98102 // Build store
99103 const store : AppStore = useMemo (
100104 ( ) => ( {
101105 theme,
102106 sidebarOpen,
103- activeWorkspaceId : WORKSPACE_ID ,
107+ activeWorkspaceId,
104108 connectionStatus,
105109 toggleTheme,
106110 setTheme,
107111 toggleSidebar,
108112 setSidebarOpen,
113+ setActiveWorkspaceId,
109114 setConnectionStatus,
110115 } ) ,
111- [ theme , sidebarOpen , connectionStatus , toggleTheme , setTheme , toggleSidebar ] ,
116+ [ theme , sidebarOpen , activeWorkspaceId , connectionStatus , toggleTheme , setTheme , toggleSidebar , setActiveWorkspaceId ] ,
112117 ) ;
113118
119+ // Invalidate all queries when workspace changes
120+ const queryClient = useQueryClient ( ) ;
121+ useEffect ( ( ) => {
122+ queryClient . invalidateQueries ( ) ;
123+ } , [ activeWorkspaceId , queryClient ] ) ;
124+
114125 // Web Notifications
115126 const { dispatchNotification } = useWebNotifications ( { onNavigate : routerNavigate } ) ;
116127
117128 // Workspace stream
118129 useWorkspaceStream ( {
119- workspaceId : WORKSPACE_ID ,
130+ workspaceId : activeWorkspaceId ,
120131 onStatusChange : setConnectionStatus ,
121132 onNotification : dispatchNotification ,
122133 } ) ;
123134
124135 // Tray status sync
125- useTrayStatus ( WORKSPACE_ID ) ;
136+ useTrayStatus ( activeWorkspaceId ) ;
126137
127138 // Navigation - wraps react-router navigate with tab management
128139 const navigate = useCallback (
@@ -171,14 +182,14 @@ function App() {
171182 const handleCreateTask = useCallback (
172183 ( prompt : string , repositoryGroupId : string , agentCliType : AgentCliType , usePlanMode : boolean ) => {
173184 createTaskMutation . mutate ( {
174- workspaceId : WORKSPACE_ID ,
185+ workspaceId : activeWorkspaceId ,
175186 prompt,
176187 repositoryGroupId,
177188 agentCliType,
178189 usePlanMode,
179190 } ) ;
180191 } ,
181- [ createTaskMutation ] ,
192+ [ createTaskMutation , activeWorkspaceId ] ,
182193 ) ;
183194
184195 const planDecisionMutation = useSubmitPlanDecisionMutation ( ) ;
@@ -192,43 +203,85 @@ function App() {
192203 ? ProtoPlanDecision . REVISE
193204 : ProtoPlanDecision . REJECT ;
194205 planDecisionMutation . mutate ( {
195- workspaceId : WORKSPACE_ID ,
206+ workspaceId : activeWorkspaceId ,
196207 subTaskId,
197208 decision : protoDecision ,
198209 revisionNote : revisionNote ?? "" ,
199210 } ) ;
200211 } ,
201- [ planDecisionMutation ] ,
202- ) ;
203-
204- const handleNotificationClick = useCallback (
205- ( notification : Notification ) => {
206- if ( notification . taskId ) {
207- navigate ( `/tasks/${ notification . taskId } ` ) ;
208- }
209- } ,
210- [ navigate ] ,
211- ) ;
212-
213- const handleMarkRead = useCallback (
214- ( notificationId : string ) => {
215- markReadMutation . mutate ( { workspaceId : WORKSPACE_ID , notificationId } ) ;
216- } ,
217- [ markReadMutation ] ,
212+ [ planDecisionMutation , activeWorkspaceId ] ,
218213 ) ;
219214
220215 // Global shortcut handler
221216 useGlobalShortcut ( {
222- workspaceId : WORKSPACE_ID ,
217+ workspaceId : activeWorkspaceId ,
223218 onNavigate : navigate ,
224219 } ) ;
225220
221+ // Reset selectedTaskIndex when navigating away from tasks
222+ useEffect ( ( ) => {
223+ if ( currentPath !== "/tasks" ) {
224+ setSelectedTaskIndex ( - 1 ) ;
225+ }
226+ } , [ currentPath ] ) ;
227+
226228 // Keyboard shortcuts
227229 useKeyboardShortcuts ( {
228230 onCommandPalette : ( ) => setCommandPaletteOpen ( true ) ,
229231 onToggleSidebar : toggleSidebar ,
230232 onNavigate : navigate ,
231233 onCreateTask : ( ) => setCreateDialogOpen ( true ) ,
234+ onCloseTab : ( ) => {
235+ const activeTab = tabs . find ( ( t ) => t . id === activeTabId ) ;
236+ if ( activeTab ) {
237+ handleTabClose ( activeTab ) ;
238+ }
239+ } ,
240+ onListDown : ( ) => {
241+ if ( currentPath === "/tasks" ) {
242+ setSelectedTaskIndex ( ( prev ) => Math . min ( prev + 1 , tasks . length - 1 ) ) ;
243+ }
244+ } ,
245+ onListUp : ( ) => {
246+ if ( currentPath === "/tasks" ) {
247+ setSelectedTaskIndex ( ( prev ) => Math . max ( prev - 1 , 0 ) ) ;
248+ }
249+ } ,
250+ onSwitchTabLeft : ( ) => {
251+ if ( tabs . length === 0 ) return ;
252+ const currentIdx = tabs . findIndex ( ( t ) => t . id === activeTabId ) ;
253+ if ( currentIdx > 0 ) {
254+ const prevTab = tabs [ currentIdx - 1 ] ;
255+ handleTabClick ( prevTab ) ;
256+ }
257+ } ,
258+ onSwitchTabRight : ( ) => {
259+ if ( tabs . length === 0 ) return ;
260+ const currentIdx = tabs . findIndex ( ( t ) => t . id === activeTabId ) ;
261+ if ( currentIdx < tabs . length - 1 ) {
262+ const nextTab = tabs [ currentIdx + 1 ] ;
263+ handleTabClick ( nextTab ) ;
264+ }
265+ } ,
266+ onApprovePlan : ( ) => {
267+ // Context-sensitive: only if on task detail with waiting subtask
268+ if ( ! currentPath . startsWith ( "/tasks/" ) ) return ;
269+ const taskId = currentPath . replace ( "/tasks/" , "" ) ;
270+ const task = tasks . find ( ( t ) => t . unitTaskId === taskId ) ;
271+ if ( task ) {
272+ // We need to find a waiting subtask - delegate to handlePlanDecision
273+ // This is a simplified version - the full version would need subtask data
274+ handlePlanDecision ( taskId , PlanDecision . APPROVE ) ;
275+ }
276+ } ,
277+ onRevisePlan : ( ) => {
278+ // For V key - context sensitive
279+ if ( ! currentPath . startsWith ( "/tasks/" ) ) return ;
280+ } ,
281+ onRejectPlan : ( ) => {
282+ // For Shift+X - context sensitive: cancel task if in progress, reject plan if waiting
283+ if ( ! currentPath . startsWith ( "/tasks/" ) ) return ;
284+ } ,
232285 } ) ;
233286
234287 // Task detail renderer (used by route)
@@ -250,6 +303,21 @@ function App() {
250303 ) ;
251304 }
252305
306+ // PR detail renderer (used by route)
307+ function PrDetailRoute ( ) {
308+ const prTrackingId = currentPath . replace ( "/prs/" , "" ) ;
309+ if ( ! prTrackingId ) {
310+ return < Navigate to = "/prs" replace /> ;
311+ }
312+ return (
313+ < PrDetailPage
314+ workspaceId = { activeWorkspaceId }
315+ prTrackingId = { prTrackingId }
316+ onBack = { ( ) => routerNavigate ( "/prs" ) }
317+ />
318+ ) ;
319+ }
320+
253321 const layoutStyle : CSSProperties = {
254322 display : "flex" ,
255323 height : "100%" ,
@@ -292,22 +360,31 @@ function App() {
292360 isLoading = { tasksLoading }
293361 onTaskSelect = { ( taskId ) => navigate ( `/tasks/${ taskId } ` ) }
294362 onCreateTask = { ( ) => setCreateDialogOpen ( true ) }
363+ selectedIndex = { selectedTaskIndex }
364+ onSelectIndex = { setSelectedTaskIndex }
295365 />
296366 }
297367 />
298368 < Route path = "/tasks/:taskId" element = { < TaskDetailRoute /> } />
299369 < Route
300370 path = "/inbox"
371+ element = { < InboxPage /> }
372+ />
373+ < Route
374+ path = "/prs"
301375 element = {
302- < InboxPage
303- notifications = { notifications }
304- isLoading = { notificationsLoading }
305- onNotificationClick = { handleNotificationClick }
306- onMarkRead = { handleMarkRead }
376+ < PrManagementPage
377+ pullRequests = { pullRequests }
378+ isLoading = { prsLoading }
379+ workspaceId = { activeWorkspaceId }
380+ onPrSelect = { ( prTrackingId ) => navigate ( `/prs/ ${ prTrackingId } ` ) }
307381 />
308382 }
309383 />
310- < Route path = "/prs" element = { < PrManagementPage pullRequests = { pullRequests } isLoading = { prsLoading } /> } />
384+ < Route
385+ path = "/prs/:prTrackingId"
386+ element = { < PrDetailRoute /> }
387+ />
311388 < Route path = "/settings" element = { < SettingsPage /> } />
312389 < Route path = "*" element = { < Navigate to = "/tasks" replace /> } />
313390 </ Routes >
@@ -327,7 +404,7 @@ function App() {
327404 />
328405 < CreateDialog
329406 isOpen = { createDialogOpen }
330- workspaceId = { WORKSPACE_ID }
407+ workspaceId = { activeWorkspaceId }
331408 onClose = { ( ) => setCreateDialogOpen ( false ) }
332409 onCreate = { handleCreateTask }
333410 />
0 commit comments