From 4b08855e1300d8882d3bbb62019c41730345392b Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Sat, 30 May 2026 11:21:54 -0400 Subject: [PATCH] feat(sidebar): 3-state session indicators driven by real processing state --- ...-05-30-session-status-indicators-design.md | 34 +++++++++++++++++++ src/components/app/AppContent.tsx | 1 + src/components/sidebar/types/types.ts | 1 + src/components/sidebar/view/Sidebar.tsx | 2 ++ .../view/subcomponents/SidebarProjectItem.tsx | 3 ++ .../view/subcomponents/SidebarProjectList.tsx | 3 ++ .../subcomponents/SidebarProjectSessions.tsx | 3 ++ .../view/subcomponents/SidebarSessionItem.tsx | 33 ++++++++++++++---- src/hooks/useProjectsState.ts | 4 +++ 9 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 docs/superpowers/specs/2026-05-30-session-status-indicators-design.md diff --git a/docs/superpowers/specs/2026-05-30-session-status-indicators-design.md b/docs/superpowers/specs/2026-05-30-session-status-indicators-design.md new file mode 100644 index 0000000..7d64561 --- /dev/null +++ b/docs/superpowers/specs/2026-05-30-session-status-indicators-design.md @@ -0,0 +1,34 @@ +# Session Status Indicators - Design + +Date: 2026-05-30 +Status: Approved (3-distinct-states) + +## Problem +Sidebar session indicator is an animated green dot driven purely by timestamp +(isActive: diffInMinutes < 10 in sidebar/utils/utils.ts). It lights up for any +session touched in the last 10 min regardless of whether an agent is running. +The real processing state (processingSessions Set, event-driven) is only shown +in the chat-area status bar, never in the sidebar. + +## Three States +- Processing: processingSessions.has(session.id) -> amber dot, animate-ping halo + solid core. Title "Agent running". +- Recently active: sessionView.isActive && !isProcessing -> static sky-blue dot. Title "Recently active". +- Idle: neither -> no indicator. + +## Data flow (new threading, parallel to currentTime) +AppContent -> useProjectsState({processingSessions}) -> sidebarSharedProps +-> Sidebar (SidebarProps) -> projectListProps -> SidebarProjectList +-> SidebarProjectItem -> SidebarProjectSessions -> SidebarSessionItem. +processingSessions optional in leaf types -> graceful degrade. + +## Files +1. app/AppContent.tsx - pass processingSessions into useProjectsState +2. hooks/useProjectsState.ts - accept + sidebarSharedProps + deps +3. sidebar/types/types.ts - SidebarProps gains processingSessions +4. sidebar/view/Sidebar.tsx - destructure + projectListProps +5. sidebar/view/subcomponents/SidebarProjectList.tsx - type + passthrough +6. sidebar/view/subcomponents/SidebarProjectItem.tsx - type + passthrough +7. sidebar/view/subcomponents/SidebarProjectSessions.tsx - type + passthrough +8. sidebar/view/subcomponents/SidebarSessionItem.tsx - 3-state indicator + +## Risk: low, additive prop threading + presentational swap. Feature branch, no deploy until reviewed. diff --git a/src/components/app/AppContent.tsx b/src/components/app/AppContent.tsx index 1ba41b9..3b3ac8c 100644 --- a/src/components/app/AppContent.tsx +++ b/src/components/app/AppContent.tsx @@ -58,6 +58,7 @@ function AppContentInner() { latestMessage, isMobile, activeSessions, + processingSessions, }); usePaletteOpsRegister({ diff --git a/src/components/sidebar/types/types.ts b/src/components/sidebar/types/types.ts index 0f44cf2..56d6134 100644 --- a/src/components/sidebar/types/types.ts +++ b/src/components/sidebar/types/types.ts @@ -56,6 +56,7 @@ export type SidebarProps = { settingsInitialTab: string; onCloseSettings: () => void; isMobile: boolean; + processingSessions?: Set; }; export type SessionViewModel = { diff --git a/src/components/sidebar/view/Sidebar.tsx b/src/components/sidebar/view/Sidebar.tsx index ebc046c..aa3105e 100644 --- a/src/components/sidebar/view/Sidebar.tsx +++ b/src/components/sidebar/view/Sidebar.tsx @@ -39,6 +39,7 @@ function Sidebar({ settingsInitialTab, onCloseSettings, isMobile, + processingSessions, }: SidebarProps) { const { t } = useTranslation(['sidebar', 'common']); const { isPWA } = useDeviceSettings({ trackMobile: false }); @@ -152,6 +153,7 @@ function Sidebar({ editingName, initialSessionsLoaded, currentTime, + processingSessions, editingSession, editingSessionName, deletingProjects, diff --git a/src/components/sidebar/view/subcomponents/SidebarProjectItem.tsx b/src/components/sidebar/view/subcomponents/SidebarProjectItem.tsx index fa38f87..022072a 100644 --- a/src/components/sidebar/view/subcomponents/SidebarProjectItem.tsx +++ b/src/components/sidebar/view/subcomponents/SidebarProjectItem.tsx @@ -23,6 +23,7 @@ type SidebarProjectItemProps = { initialSessionsLoaded: boolean; isLoadingMoreSessions: boolean; currentTime: Date; + processingSessions?: Set; editingSession: string | null; editingSessionName: string; tasksEnabled: boolean; @@ -69,6 +70,7 @@ export default function SidebarProjectItem({ initialSessionsLoaded, isLoadingMoreSessions, currentTime, + processingSessions, editingSession, editingSessionName, tasksEnabled, @@ -412,6 +414,7 @@ export default function SidebarProjectItem({ hasMoreSessions={Boolean(project.sessionMeta?.hasMore)} isLoadingMoreSessions={isLoadingMoreSessions} currentTime={currentTime} + processingSessions={processingSessions} editingSession={editingSession} editingSessionName={editingSessionName} onEditingSessionNameChange={onEditingSessionNameChange} diff --git a/src/components/sidebar/view/subcomponents/SidebarProjectList.tsx b/src/components/sidebar/view/subcomponents/SidebarProjectList.tsx index fc9fac6..8136c3b 100644 --- a/src/components/sidebar/view/subcomponents/SidebarProjectList.tsx +++ b/src/components/sidebar/view/subcomponents/SidebarProjectList.tsx @@ -19,6 +19,7 @@ export type SidebarProjectListProps = { editingName: string; initialSessionsLoaded: Set; currentTime: Date; + processingSessions?: Set; editingSession: string | null; editingSessionName: string; deletingProjects: Set; @@ -63,6 +64,7 @@ export default function SidebarProjectList({ editingName, initialSessionsLoaded, currentTime, + processingSessions, editingSession, editingSessionName, deletingProjects, @@ -131,6 +133,7 @@ export default function SidebarProjectList({ initialSessionsLoaded={initialSessionsLoaded.has(project.projectId)} isLoadingMoreSessions={loadingMoreProjects.has(project.projectId)} currentTime={currentTime} + processingSessions={processingSessions} editingSession={editingSession} editingSessionName={editingSessionName} tasksEnabled={tasksEnabled} diff --git a/src/components/sidebar/view/subcomponents/SidebarProjectSessions.tsx b/src/components/sidebar/view/subcomponents/SidebarProjectSessions.tsx index f68daad..9cefabc 100644 --- a/src/components/sidebar/view/subcomponents/SidebarProjectSessions.tsx +++ b/src/components/sidebar/view/subcomponents/SidebarProjectSessions.tsx @@ -16,6 +16,7 @@ type SidebarProjectSessionsProps = { hasMoreSessions: boolean; isLoadingMoreSessions: boolean; currentTime: Date; + processingSessions?: Set; editingSession: string | null; editingSessionName: string; onEditingSessionNameChange: (value: string) => void; @@ -62,6 +63,7 @@ export default function SidebarProjectSessions({ hasMoreSessions, isLoadingMoreSessions, currentTime, + processingSessions, editingSession, editingSessionName, onEditingSessionNameChange, @@ -121,6 +123,7 @@ export default function SidebarProjectSessions({ session={session} selectedSession={selectedSession} currentTime={currentTime} + processingSessions={processingSessions} editingSession={editingSession} editingSessionName={editingSessionName} onEditingSessionNameChange={onEditingSessionNameChange} diff --git a/src/components/sidebar/view/subcomponents/SidebarSessionItem.tsx b/src/components/sidebar/view/subcomponents/SidebarSessionItem.tsx index 4e97a6b..25ca139 100644 --- a/src/components/sidebar/view/subcomponents/SidebarSessionItem.tsx +++ b/src/components/sidebar/view/subcomponents/SidebarSessionItem.tsx @@ -13,6 +13,7 @@ type SidebarSessionItemProps = { session: SessionWithProvider; selectedSession: ProjectSession | null; currentTime: Date; + processingSessions?: Set; editingSession: string | null; editingSessionName: string; onEditingSessionNameChange: (value: string) => void; @@ -63,6 +64,7 @@ export default function SidebarSessionItem({ session, selectedSession, currentTime, + processingSessions, editingSession, editingSessionName, onEditingSessionNameChange, @@ -76,6 +78,8 @@ export default function SidebarSessionItem({ }: SidebarSessionItemProps) { const sessionView = createSessionViewModel(session, currentTime, t); const isSelected = selectedSession?.id === session.id; + const isProcessing = processingSessions?.has(session.id) ?? false; + const isRecentlyActive = sessionView.isActive && !isProcessing; const compactSessionAge = formatCompactSessionAge(sessionView.sessionTime, currentTime); // Sessions are owned by a project identified by `projectId` (DB primary key) @@ -95,20 +99,35 @@ export default function SidebarSessionItem({ return (
- {sessionView.isActive && ( -
-
+ {isProcessing ? ( +
+ + + +
- )} + ) : isRecentlyActive ? ( +
+ +
+ ) : null}
diff --git a/src/hooks/useProjectsState.ts b/src/hooks/useProjectsState.ts index a5397b6..92a7704 100644 --- a/src/hooks/useProjectsState.ts +++ b/src/hooks/useProjectsState.ts @@ -18,6 +18,7 @@ type UseProjectsStateArgs = { latestMessage: AppSocketMessage | null; isMobile: boolean; activeSessions: Set; + processingSessions: Set; }; type FetchProjectsOptions = { @@ -241,6 +242,7 @@ export function useProjectsState({ latestMessage, isMobile, activeSessions, + processingSessions, }: UseProjectsStateArgs) { const [projects, setProjects] = useState([]); const [selectedProject, setSelectedProject] = useState(null); @@ -830,6 +832,7 @@ export function useProjectsState({ settingsInitialTab, onCloseSettings: () => setShowSettings(false), isMobile, + processingSessions, }), [ handleNewSession, @@ -847,6 +850,7 @@ export function useProjectsState({ selectedProject, selectedSession, showSettings, + processingSessions, ], );