-
Notifications
You must be signed in to change notification settings - Fork 801
feat(web): show session status indicators in sidebar #1701
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -102,6 +102,10 @@ function App() { | |
|
|
||
| const [streamStatus, setStreamStatus] = useState<ChatStatus>("ready"); | ||
|
|
||
| // Track sessions with unread results (completed a turn while user wasn't viewing) | ||
| const [unreadSessionIds, setUnreadSessionIds] = useState<Set<string>>(new Set()); | ||
| const prevSessionStatusRef = useRef<Map<string, string>>(new Map()); | ||
|
|
||
| useEffect(() => { | ||
| const token = consumeAuthTokenFromUrl(); | ||
| if (token) { | ||
|
|
@@ -266,10 +270,48 @@ function App() { | |
| setStreamStatus(nextStatus); | ||
| }, []); | ||
|
|
||
| // Detect busy→idle transitions from API refresh to mark sessions as unread | ||
| useEffect(() => { | ||
| const prev = prevSessionStatusRef.current; | ||
| const next = new Map<string, string>(); | ||
| const newUnread: string[] = []; | ||
|
|
||
| for (const session of sessions) { | ||
| const state = session.status?.state ?? null; | ||
| if (state) { | ||
| next.set(session.sessionId, state); | ||
| } | ||
| const prevState = prev.get(session.sessionId); | ||
| // Detect busy → non-busy (idle, stopped, null/gone) for non-selected sessions | ||
| if (prevState === "busy" && state !== "busy" && session.sessionId !== selectedSessionId) { | ||
| newUnread.push(session.sessionId); | ||
| } | ||
| } | ||
|
|
||
| prevSessionStatusRef.current = next; | ||
|
|
||
| if (newUnread.length > 0) { | ||
| setUnreadSessionIds((prev) => { | ||
| const updated = new Set(prev); | ||
| for (const id of newUnread) updated.add(id); | ||
| return updated; | ||
| }); | ||
| } | ||
| }, [sessions, selectedSessionId]); | ||
|
|
||
| const handleSessionStatus = useCallback( | ||
| (status: SessionStatus) => { | ||
| applySessionStatus(status); | ||
|
|
||
| // Mark as unread when a non-selected session finishes (becomes non-busy) | ||
| if (status.state !== "busy" && status.sessionId !== selectedSessionId) { | ||
| setUnreadSessionIds((prev) => { | ||
| const next = new Set(prev); | ||
| next.add(status.sessionId); | ||
| return next; | ||
| }); | ||
| } | ||
|
|
||
| if (status.state !== "idle") { | ||
| return; | ||
| } | ||
|
|
@@ -291,7 +333,7 @@ function App() { | |
| ); | ||
| refreshSession(status.sessionId); | ||
| }, | ||
| [applySessionStatus, refreshSession], | ||
| [applySessionStatus, refreshSession, selectedSessionId], | ||
| ); | ||
|
|
||
| const handleCreateSession = useCallback( | ||
|
|
@@ -319,6 +361,13 @@ function App() { | |
| (sessionId: string) => { | ||
| selectSession(sessionId); | ||
| setIsMobileSidebarOpen(false); | ||
| // Clear unread status when user views the session | ||
| setUnreadSessionIds((prev) => { | ||
| if (!prev.has(sessionId)) return prev; | ||
| const next = new Set(prev); | ||
| next.delete(sessionId); | ||
| return next; | ||
|
Comment on lines
+365
to
+369
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Unread state is currently cleared only in the sidebar click handler, so programmatic selection paths (for example URL restore in Useful? React with 👍 / 👎. |
||
| }); | ||
| }, | ||
| [selectSession], | ||
| ); | ||
|
|
@@ -343,8 +392,10 @@ function App() { | |
| updatedAt: formatRelativeTime(session.lastUpdated), | ||
| workDir: session.workDir, | ||
| lastUpdated: session.lastUpdated, | ||
| statusState: session.status?.state ?? null, | ||
| isUnread: unreadSessionIds.has(session.sessionId), | ||
| })), | ||
| [sessions], | ||
| [sessions, unreadSessionIds], | ||
| ); | ||
|
|
||
| // Transform archived Session[] to SessionSummary[] for sidebar | ||
|
|
@@ -356,6 +407,8 @@ function App() { | |
| updatedAt: formatRelativeTime(session.lastUpdated), | ||
| workDir: session.workDir, | ||
| lastUpdated: session.lastUpdated, | ||
| statusState: session.status?.state ?? null, | ||
| isUnread: false, | ||
| })), | ||
| [archivedSessions], | ||
| ); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡
handleSessionStatusmarks unread on any non-busy status instead of only busy→non-busy transitionsThe unread marking logic in
handleSessionStatusat line 307 marks a session as unread wheneverstatus.state !== "busy"for a non-selected session, without checking whether the previous state was"busy". This differs from theuseEffectatweb/src/App.tsx:284-287which correctly checksprevState === "busy" && state !== "busy"to detect transitions only.In the current architecture this code is effectively dead — the WebSocket connects per-session to the selected session, so
status.sessionId !== selectedSessionIdis alwaysfalse. However, if the backend or architecture ever changes to deliver cross-session status events through a single WebSocket, this would cause false-positive unread badges: any non-busy status (e.g., anidlestatus withreason: "config_update", or astoppedsession that was never busy) would incorrectly mark the session as unread, and the unread dot would reappear even after the user has cleared it by clicking the session.Prompt for agents
Was this helpful? React with 👍 or 👎 to provide feedback.