|
2 | 2 | * Copyright (c) Microsoft Corporation. All rights reserved. |
3 | 3 | * Licensed under the MIT License. See License.txt in the project root for license information. |
4 | 4 | *--------------------------------------------------------------------------------------------*/ |
5 | | -import React, { useCallback, useEffect, useMemo, useState } from 'react'; |
| 5 | +import React, { useCallback, useEffect, useState } from 'react'; |
6 | 6 |
|
7 | 7 | import { render } from 'react-dom'; |
8 | 8 | import { ChatInput } from './components/ChatInput'; |
9 | 9 | import { EmptyState } from './components/EmptyState'; |
10 | | -import { GlobalSessionItem } from './components/GlobalSessionItem'; |
11 | 10 | import { IssueItem } from './components/IssueItem'; |
12 | 11 | import { LoadingState } from './components/LoadingState'; |
13 | 12 | import { SessionItem } from './components/SessionItem'; |
14 | 13 | import { SortDropdown } from './components/SortDropdown'; |
15 | | -import { DashboardReady, DashboardState, extractMilestoneFromQuery, GlobalDashboardReady, IssueData, ProjectData, SessionData, vscode } from './types'; |
| 14 | +import { DashboardReady, DashboardState, IssueData, SessionData } from './types'; |
| 15 | +import { extractMilestoneFromQuery, vscode } from './util'; |
16 | 16 |
|
17 | 17 | export function main() { |
18 | 18 | render(<Dashboard />, document.getElementById('app')); |
@@ -101,7 +101,7 @@ function Dashboard() { |
101 | 101 | }, []); |
102 | 102 |
|
103 | 103 | const handleIssueCountClick = useCallback(() => { |
104 | | - if (dashboardState?.state === 'ready' && !dashboardState.isGlobal) { |
| 104 | + if (dashboardState?.state === 'ready') { |
105 | 105 | const readyState = dashboardState as DashboardReady; |
106 | 106 | const { owner, name } = readyState.repository || { owner: '', name: '' }; |
107 | 107 |
|
@@ -168,208 +168,131 @@ function Dashboard() { |
168 | 168 | }, [dashboardState]); |
169 | 169 |
|
170 | 170 | // Derived state from discriminated union with proper type narrowing |
171 | | - const isGlobal = dashboardState?.isGlobal ?? false; |
172 | 171 | const isReady = dashboardState?.state === 'ready'; |
173 | | - |
174 | | - const readyState = (isReady && !isGlobal) ? dashboardState as DashboardReady : null; |
175 | | - const globalReadyState = (isReady && isGlobal) ? dashboardState as GlobalDashboardReady : null; |
| 172 | + const readyState = isReady ? dashboardState as DashboardReady : null; |
176 | 173 |
|
177 | 174 | const issueQuery = readyState?.issueQuery || ''; |
178 | 175 | const milestoneIssues = readyState?.milestoneIssues || []; |
179 | 176 | const activeSessions = isReady ? dashboardState.activeSessions : []; |
180 | | - const recentProjects = globalReadyState?.recentProjects || []; |
181 | | - const currentBranch = readyState?.currentBranch; |
182 | | - |
183 | | - // For global dashboards, create a mixed array of sessions and projects |
184 | | - const mixedItems = useMemo(() => { |
185 | | - if (!isGlobal) return []; |
186 | | - |
187 | | - const mixed: Array<{ type: 'session', data: SessionData, index: number } | { type: 'project', data: ProjectData }> = []; |
188 | | - |
189 | | - activeSessions.forEach((session, index) => { |
190 | | - mixed.push({ type: 'session', data: session, index }); |
191 | | - }); |
192 | | - |
193 | | - recentProjects.forEach((project: ProjectData) => { |
194 | | - mixed.push({ type: 'project', data: project }); |
195 | | - }); |
196 | | - |
197 | | - // Simple shuffle algorithm |
198 | | - for (let i = mixed.length - 1; i > 0; i--) { |
199 | | - const j = Math.floor(Math.random() * (i + 1)); |
200 | | - [mixed[i], mixed[j]] = [mixed[j], mixed[i]]; |
201 | | - } |
202 | | - |
203 | | - return mixed; |
204 | | - }, [isGlobal, activeSessions, recentProjects]); |
205 | | - |
206 | | - return ( |
207 | | - <div className={`dashboard-container${isGlobal ? ' global-dashboard' : ''}`}> |
208 | | - {!isGlobal && ( |
209 | | - <div className={`dashboard-header${isGlobal ? ' global-header' : ''}`}> |
210 | | - <h1 className="dashboard-title">My Tasks</h1> |
211 | | - <div className="header-buttons"> |
212 | | - {readyState?.currentBranch && |
213 | | - readyState.currentBranch !== 'main' && |
214 | | - readyState.currentBranch !== 'master' && ( |
215 | | - <button |
216 | | - className="switch-to-main-button" |
217 | | - onClick={handleSwitchToMain} |
218 | | - title={`Switch from ${readyState.currentBranch} to main`} |
219 | | - > |
220 | | - <span className="codicon codicon-git-branch"></span> |
221 | | - <span>Switch to main</span> |
222 | | - </button> |
223 | | - )} |
224 | | - <button className="refresh-button" onClick={handleRefresh} disabled={refreshing} title="Refresh dashboard"> |
225 | | - <span className={`codicon ${refreshing ? 'codicon-sync codicon-modifier-spin' : 'codicon-refresh'}`}></span> |
226 | | - </button> |
227 | | - </div> |
| 177 | + const currentBranch = readyState?.currentBranch; return ( |
| 178 | + <div className="dashboard-container"> |
| 179 | + <div className="dashboard-header"> |
| 180 | + <h1 className="dashboard-title">My Tasks</h1> |
| 181 | + <div className="header-buttons"> |
| 182 | + {readyState?.currentBranch && |
| 183 | + readyState.currentBranch !== 'main' && |
| 184 | + readyState.currentBranch !== 'master' && ( |
| 185 | + <button |
| 186 | + className="switch-to-main-button" |
| 187 | + onClick={handleSwitchToMain} |
| 188 | + title={`Switch from ${readyState.currentBranch} to main`} |
| 189 | + > |
| 190 | + <span className="codicon codicon-git-branch"></span> |
| 191 | + <span>Switch to main</span> |
| 192 | + </button> |
| 193 | + )} |
| 194 | + <button className="refresh-button" onClick={handleRefresh} disabled={refreshing} title="Refresh dashboard"> |
| 195 | + <span className={`codicon ${refreshing ? 'codicon-sync codicon-modifier-spin' : 'codicon-refresh'}`}></span> |
| 196 | + </button> |
228 | 197 | </div> |
229 | | - )} |
| 198 | + </div> |
230 | 199 |
|
231 | | - <div className={`dashboard-content${isGlobal ? ' global-dashboard' : ''}`}> |
| 200 | + <div className="dashboard-content"> |
232 | 201 | {/* Input Area */} |
233 | 202 | <div className="input-area"> |
234 | 203 | <h2 className="area-header new-task">Start new task</h2> |
235 | 204 | <ChatInput |
236 | 205 | data={dashboardState} |
237 | | - isGlobal={!!isGlobal} |
238 | 206 | value={chatInputValue} |
239 | 207 | onValueChange={setChatInputValue} |
240 | 208 | focusTrigger={focusTrigger} |
241 | 209 | /> |
242 | | - |
243 | 210 | </div> |
244 | 211 |
|
245 | 212 | {/* Issues/Projects Area */} |
246 | 213 | <div className="issues-area"> |
247 | | - {!isGlobal && ( |
248 | | - <> |
249 | | - <div className="area-header milestone-header"> |
250 | | - <h3 |
251 | | - className="milestone-title" |
252 | | - title={`Issue Query: ${issueQuery}`} |
253 | | - > |
254 | | - {issueQuery ? extractMilestoneFromQuery(issueQuery) : 'Issues'} |
255 | | - </h3> |
256 | | - {isReady && ( |
257 | | - <SortDropdown |
258 | | - issueSort={issueSort} |
259 | | - onSortChange={setIssueSort} |
260 | | - /> |
261 | | - )} |
262 | | - </div> |
263 | | - {isReady && ( |
264 | | - <div |
265 | | - className="section-count clickable-count" |
266 | | - onClick={handleIssueCountClick} |
267 | | - title="Click to open GitHub issues" |
268 | | - > |
269 | | - {milestoneIssues.length || 0} issue{milestoneIssues.length !== 1 ? 's' : ''} |
270 | | - </div> |
271 | | - )} |
272 | | - <div className="area-content"> |
273 | | - {dashboardState?.state === 'loading' ? ( |
274 | | - <LoadingState message="Loading issues..." /> |
275 | | - ) : isReady && !milestoneIssues.length ? ( |
276 | | - <EmptyState message={`No issues found for ${issueQuery ? extractMilestoneFromQuery(issueQuery).toLowerCase() : 'issues'}`} /> |
277 | | - ) : isReady ? ( |
278 | | - getSortedIssues(milestoneIssues).map((issue) => { |
279 | | - const associatedSession = findAssociatedSession(issue); |
280 | | - return ( |
281 | | - <IssueItem |
282 | | - key={issue.number} |
283 | | - issue={issue} |
284 | | - onIssueClick={handleIssueClick} |
285 | | - onPopulateLocalInput={handlePopulateLocalInput} |
286 | | - onPopulateRemoteInput={handlePopulateRemoteInput} |
287 | | - onSwitchToLocalTask={handleSwitchToLocalTask} |
288 | | - associatedSession={associatedSession} |
289 | | - onSessionClick={handleSessionClick} |
290 | | - onPullRequestClick={handlePullRequestClick} |
291 | | - onHover={() => setHoveredIssue(issue)} |
292 | | - onHoverEnd={() => setHoveredIssue(null)} |
293 | | - currentBranch={currentBranch} |
294 | | - /> |
295 | | - ); |
296 | | - }) |
297 | | - ) : null} |
298 | | - </div> |
299 | | - </> |
| 214 | + <div className="area-header milestone-header"> |
| 215 | + <h3 |
| 216 | + className="milestone-title" |
| 217 | + title={`Issue Query: ${issueQuery}`} |
| 218 | + > |
| 219 | + {issueQuery ? extractMilestoneFromQuery(issueQuery) : 'Issues'} |
| 220 | + </h3> |
| 221 | + {isReady && ( |
| 222 | + <SortDropdown |
| 223 | + issueSort={issueSort} |
| 224 | + onSortChange={setIssueSort} |
| 225 | + /> |
| 226 | + )} |
| 227 | + </div> |
| 228 | + {isReady && ( |
| 229 | + <div |
| 230 | + className="section-count clickable-count" |
| 231 | + onClick={handleIssueCountClick} |
| 232 | + title="Click to open GitHub issues" |
| 233 | + > |
| 234 | + {milestoneIssues.length || 0} issue{milestoneIssues.length !== 1 ? 's' : ''} |
| 235 | + </div> |
300 | 236 | )} |
| 237 | + <div className="area-content"> |
| 238 | + {dashboardState?.state === 'loading' ? ( |
| 239 | + <LoadingState message="Loading issues..." /> |
| 240 | + ) : isReady && !milestoneIssues.length ? ( |
| 241 | + <EmptyState message={`No issues found for ${issueQuery ? extractMilestoneFromQuery(issueQuery).toLowerCase() : 'issues'}`} /> |
| 242 | + ) : isReady ? ( |
| 243 | + getSortedIssues(milestoneIssues).map((issue) => { |
| 244 | + const associatedSession = findAssociatedSession(issue); |
| 245 | + return ( |
| 246 | + <IssueItem |
| 247 | + key={issue.number} |
| 248 | + issue={issue} |
| 249 | + onIssueClick={handleIssueClick} |
| 250 | + onPopulateLocalInput={handlePopulateLocalInput} |
| 251 | + onPopulateRemoteInput={handlePopulateRemoteInput} |
| 252 | + onSwitchToLocalTask={handleSwitchToLocalTask} |
| 253 | + associatedSession={associatedSession} |
| 254 | + onSessionClick={handleSessionClick} |
| 255 | + onPullRequestClick={handlePullRequestClick} |
| 256 | + onHover={() => setHoveredIssue(issue)} |
| 257 | + onHoverEnd={() => setHoveredIssue(null)} |
| 258 | + currentBranch={currentBranch} |
| 259 | + /> |
| 260 | + ); |
| 261 | + }) |
| 262 | + ) : null} |
| 263 | + </div> |
301 | 264 | </div> |
302 | 265 |
|
303 | 266 | {/* Tasks Area */} |
304 | 267 | <div className="tasks-area"> |
305 | 268 | <div className="area-header-container"> |
306 | 269 | <h2 className="area-header"> |
307 | | - {isGlobal ? 'Continue working on...' : |
308 | | - isReady ? |
309 | | - `${activeSessions.length || 0} active task${activeSessions.length !== 1 ? 's' : ''}` : |
310 | | - 'Active tasks' |
| 270 | + {isReady ? |
| 271 | + `${activeSessions.length || 0} active task${activeSessions.length !== 1 ? 's' : ''}` : |
| 272 | + 'Active tasks' |
311 | 273 | } |
312 | 274 | </h2> |
313 | 275 | </div> |
314 | 276 | <div className="area-content"> |
315 | 277 | {dashboardState?.state === 'loading' ? ( |
316 | 278 | <LoadingState message="Loading tasks..." /> |
317 | | - ) : isReady && !activeSessions.length && (!isGlobal || !recentProjects.length) ? ( |
| 279 | + ) : isReady && !activeSessions.length ? ( |
318 | 280 | <EmptyState message="No active tasks found" /> |
319 | 281 | ) : isReady ? ( |
320 | | - <> |
321 | | - {isGlobal ? ( |
322 | | - // Render mixed items for global dashboard |
323 | | - mixedItems.map((item) => |
324 | | - item.type === 'session' ? ( |
325 | | - <GlobalSessionItem |
326 | | - key={item.data.id} |
327 | | - session={item.data} |
328 | | - index={item.index} |
329 | | - onSessionClick={() => handleSessionClick(item.data)} |
330 | | - onPullRequestClick={handlePullRequestClick} |
331 | | - /> |
332 | | - ) : ( |
333 | | - <div |
334 | | - key={`project-${item.data.path}`} |
335 | | - className="session-item project-item" |
336 | | - onClick={() => vscode.postMessage({ command: 'open-project', args: { path: item.data.path } })} |
337 | | - title={`Click to open project: ${item.data.name}`} |
338 | | - > |
339 | | - <div className="item-title"> |
340 | | - <span className="task-type-indicator project" title="Recent project"> |
341 | | - <span className="codicon codicon-folder-opened"></span> |
342 | | - </span> |
343 | | - <span className="item-title-text">{item.data.name}</span> |
344 | | - </div> |
345 | | - {item.data.path && ( |
346 | | - <div className="item-metadata"> |
347 | | - <div className="metadata-item"> |
348 | | - <span className="project-path-text">{item.data.path}</span> |
349 | | - </div> |
350 | | - </div> |
351 | | - )} |
352 | | - </div> |
353 | | - ) |
354 | | - ) |
355 | | - ) : ( |
356 | | - // Render sessions only for regular dashboard |
357 | | - activeSessions.map((session, index) => ( |
358 | | - <SessionItem |
359 | | - key={session.id} |
360 | | - session={session} |
361 | | - index={index} |
362 | | - onSessionClick={() => handleSessionClick(session)} |
363 | | - onPullRequestClick={handlePullRequestClick} |
364 | | - isHighlighted={hoveredIssue !== null && findAssociatedSession(hoveredIssue)?.id === session.id} |
365 | | - /> |
366 | | - )) |
367 | | - )} |
368 | | - </> |
| 282 | + activeSessions.map((session, index) => ( |
| 283 | + <SessionItem |
| 284 | + key={session.id} |
| 285 | + session={session} |
| 286 | + index={index} |
| 287 | + onSessionClick={() => handleSessionClick(session)} |
| 288 | + onPullRequestClick={handlePullRequestClick} |
| 289 | + isHighlighted={hoveredIssue !== null && findAssociatedSession(hoveredIssue)?.id === session.id} |
| 290 | + /> |
| 291 | + )) |
369 | 292 | ) : null} |
370 | 293 | </div> |
371 | 294 | </div> |
372 | 295 | </div> |
373 | | - </div > |
| 296 | + </div> |
374 | 297 | ); |
375 | 298 | } |
0 commit comments