feat(sidebar): 3-state session indicators driven by real processing state
This commit is contained in:
parent
8e30d9c3cf
commit
4b08855e13
9 changed files with 77 additions and 7 deletions
|
|
@ -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.
|
||||||
|
|
@ -58,6 +58,7 @@ function AppContentInner() {
|
||||||
latestMessage,
|
latestMessage,
|
||||||
isMobile,
|
isMobile,
|
||||||
activeSessions,
|
activeSessions,
|
||||||
|
processingSessions,
|
||||||
});
|
});
|
||||||
|
|
||||||
usePaletteOpsRegister({
|
usePaletteOpsRegister({
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ export type SidebarProps = {
|
||||||
settingsInitialTab: string;
|
settingsInitialTab: string;
|
||||||
onCloseSettings: () => void;
|
onCloseSettings: () => void;
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
|
processingSessions?: Set<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SessionViewModel = {
|
export type SessionViewModel = {
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ function Sidebar({
|
||||||
settingsInitialTab,
|
settingsInitialTab,
|
||||||
onCloseSettings,
|
onCloseSettings,
|
||||||
isMobile,
|
isMobile,
|
||||||
|
processingSessions,
|
||||||
}: SidebarProps) {
|
}: SidebarProps) {
|
||||||
const { t } = useTranslation(['sidebar', 'common']);
|
const { t } = useTranslation(['sidebar', 'common']);
|
||||||
const { isPWA } = useDeviceSettings({ trackMobile: false });
|
const { isPWA } = useDeviceSettings({ trackMobile: false });
|
||||||
|
|
@ -152,6 +153,7 @@ function Sidebar({
|
||||||
editingName,
|
editingName,
|
||||||
initialSessionsLoaded,
|
initialSessionsLoaded,
|
||||||
currentTime,
|
currentTime,
|
||||||
|
processingSessions,
|
||||||
editingSession,
|
editingSession,
|
||||||
editingSessionName,
|
editingSessionName,
|
||||||
deletingProjects,
|
deletingProjects,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ type SidebarProjectItemProps = {
|
||||||
initialSessionsLoaded: boolean;
|
initialSessionsLoaded: boolean;
|
||||||
isLoadingMoreSessions: boolean;
|
isLoadingMoreSessions: boolean;
|
||||||
currentTime: Date;
|
currentTime: Date;
|
||||||
|
processingSessions?: Set<string>;
|
||||||
editingSession: string | null;
|
editingSession: string | null;
|
||||||
editingSessionName: string;
|
editingSessionName: string;
|
||||||
tasksEnabled: boolean;
|
tasksEnabled: boolean;
|
||||||
|
|
@ -69,6 +70,7 @@ export default function SidebarProjectItem({
|
||||||
initialSessionsLoaded,
|
initialSessionsLoaded,
|
||||||
isLoadingMoreSessions,
|
isLoadingMoreSessions,
|
||||||
currentTime,
|
currentTime,
|
||||||
|
processingSessions,
|
||||||
editingSession,
|
editingSession,
|
||||||
editingSessionName,
|
editingSessionName,
|
||||||
tasksEnabled,
|
tasksEnabled,
|
||||||
|
|
@ -412,6 +414,7 @@ export default function SidebarProjectItem({
|
||||||
hasMoreSessions={Boolean(project.sessionMeta?.hasMore)}
|
hasMoreSessions={Boolean(project.sessionMeta?.hasMore)}
|
||||||
isLoadingMoreSessions={isLoadingMoreSessions}
|
isLoadingMoreSessions={isLoadingMoreSessions}
|
||||||
currentTime={currentTime}
|
currentTime={currentTime}
|
||||||
|
processingSessions={processingSessions}
|
||||||
editingSession={editingSession}
|
editingSession={editingSession}
|
||||||
editingSessionName={editingSessionName}
|
editingSessionName={editingSessionName}
|
||||||
onEditingSessionNameChange={onEditingSessionNameChange}
|
onEditingSessionNameChange={onEditingSessionNameChange}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ export type SidebarProjectListProps = {
|
||||||
editingName: string;
|
editingName: string;
|
||||||
initialSessionsLoaded: Set<string>;
|
initialSessionsLoaded: Set<string>;
|
||||||
currentTime: Date;
|
currentTime: Date;
|
||||||
|
processingSessions?: Set<string>;
|
||||||
editingSession: string | null;
|
editingSession: string | null;
|
||||||
editingSessionName: string;
|
editingSessionName: string;
|
||||||
deletingProjects: Set<string>;
|
deletingProjects: Set<string>;
|
||||||
|
|
@ -63,6 +64,7 @@ export default function SidebarProjectList({
|
||||||
editingName,
|
editingName,
|
||||||
initialSessionsLoaded,
|
initialSessionsLoaded,
|
||||||
currentTime,
|
currentTime,
|
||||||
|
processingSessions,
|
||||||
editingSession,
|
editingSession,
|
||||||
editingSessionName,
|
editingSessionName,
|
||||||
deletingProjects,
|
deletingProjects,
|
||||||
|
|
@ -131,6 +133,7 @@ export default function SidebarProjectList({
|
||||||
initialSessionsLoaded={initialSessionsLoaded.has(project.projectId)}
|
initialSessionsLoaded={initialSessionsLoaded.has(project.projectId)}
|
||||||
isLoadingMoreSessions={loadingMoreProjects.has(project.projectId)}
|
isLoadingMoreSessions={loadingMoreProjects.has(project.projectId)}
|
||||||
currentTime={currentTime}
|
currentTime={currentTime}
|
||||||
|
processingSessions={processingSessions}
|
||||||
editingSession={editingSession}
|
editingSession={editingSession}
|
||||||
editingSessionName={editingSessionName}
|
editingSessionName={editingSessionName}
|
||||||
tasksEnabled={tasksEnabled}
|
tasksEnabled={tasksEnabled}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ type SidebarProjectSessionsProps = {
|
||||||
hasMoreSessions: boolean;
|
hasMoreSessions: boolean;
|
||||||
isLoadingMoreSessions: boolean;
|
isLoadingMoreSessions: boolean;
|
||||||
currentTime: Date;
|
currentTime: Date;
|
||||||
|
processingSessions?: Set<string>;
|
||||||
editingSession: string | null;
|
editingSession: string | null;
|
||||||
editingSessionName: string;
|
editingSessionName: string;
|
||||||
onEditingSessionNameChange: (value: string) => void;
|
onEditingSessionNameChange: (value: string) => void;
|
||||||
|
|
@ -62,6 +63,7 @@ export default function SidebarProjectSessions({
|
||||||
hasMoreSessions,
|
hasMoreSessions,
|
||||||
isLoadingMoreSessions,
|
isLoadingMoreSessions,
|
||||||
currentTime,
|
currentTime,
|
||||||
|
processingSessions,
|
||||||
editingSession,
|
editingSession,
|
||||||
editingSessionName,
|
editingSessionName,
|
||||||
onEditingSessionNameChange,
|
onEditingSessionNameChange,
|
||||||
|
|
@ -121,6 +123,7 @@ export default function SidebarProjectSessions({
|
||||||
session={session}
|
session={session}
|
||||||
selectedSession={selectedSession}
|
selectedSession={selectedSession}
|
||||||
currentTime={currentTime}
|
currentTime={currentTime}
|
||||||
|
processingSessions={processingSessions}
|
||||||
editingSession={editingSession}
|
editingSession={editingSession}
|
||||||
editingSessionName={editingSessionName}
|
editingSessionName={editingSessionName}
|
||||||
onEditingSessionNameChange={onEditingSessionNameChange}
|
onEditingSessionNameChange={onEditingSessionNameChange}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ type SidebarSessionItemProps = {
|
||||||
session: SessionWithProvider;
|
session: SessionWithProvider;
|
||||||
selectedSession: ProjectSession | null;
|
selectedSession: ProjectSession | null;
|
||||||
currentTime: Date;
|
currentTime: Date;
|
||||||
|
processingSessions?: Set<string>;
|
||||||
editingSession: string | null;
|
editingSession: string | null;
|
||||||
editingSessionName: string;
|
editingSessionName: string;
|
||||||
onEditingSessionNameChange: (value: string) => void;
|
onEditingSessionNameChange: (value: string) => void;
|
||||||
|
|
@ -63,6 +64,7 @@ export default function SidebarSessionItem({
|
||||||
session,
|
session,
|
||||||
selectedSession,
|
selectedSession,
|
||||||
currentTime,
|
currentTime,
|
||||||
|
processingSessions,
|
||||||
editingSession,
|
editingSession,
|
||||||
editingSessionName,
|
editingSessionName,
|
||||||
onEditingSessionNameChange,
|
onEditingSessionNameChange,
|
||||||
|
|
@ -76,6 +78,8 @@ export default function SidebarSessionItem({
|
||||||
}: SidebarSessionItemProps) {
|
}: SidebarSessionItemProps) {
|
||||||
const sessionView = createSessionViewModel(session, currentTime, t);
|
const sessionView = createSessionViewModel(session, currentTime, t);
|
||||||
const isSelected = selectedSession?.id === session.id;
|
const isSelected = selectedSession?.id === session.id;
|
||||||
|
const isProcessing = processingSessions?.has(session.id) ?? false;
|
||||||
|
const isRecentlyActive = sessionView.isActive && !isProcessing;
|
||||||
const compactSessionAge = formatCompactSessionAge(sessionView.sessionTime, currentTime);
|
const compactSessionAge = formatCompactSessionAge(sessionView.sessionTime, currentTime);
|
||||||
|
|
||||||
// Sessions are owned by a project identified by `projectId` (DB primary key)
|
// Sessions are owned by a project identified by `projectId` (DB primary key)
|
||||||
|
|
@ -95,20 +99,35 @@ export default function SidebarSessionItem({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group relative">
|
<div className="group relative">
|
||||||
{sessionView.isActive && (
|
{isProcessing ? (
|
||||||
<div className="absolute left-0 top-1/2 -translate-x-1 -translate-y-1/2 transform">
|
<div
|
||||||
<div className="h-2 w-2 animate-pulse rounded-full bg-green-500" />
|
className="absolute left-0 top-1/2 -translate-x-1 -translate-y-1/2 transform"
|
||||||
|
title={t('sessions.agentRunning', { defaultValue: 'Agent running' })}
|
||||||
|
>
|
||||||
|
<span className="relative flex h-2 w-2">
|
||||||
|
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-amber-400 opacity-75" />
|
||||||
|
<span className="relative inline-flex h-2 w-2 rounded-full bg-amber-500" />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : isRecentlyActive ? (
|
||||||
|
<div
|
||||||
|
className="absolute left-0 top-1/2 -translate-x-1 -translate-y-1/2 transform"
|
||||||
|
title={t('sessions.recentlyActive', { defaultValue: 'Recently active' })}
|
||||||
|
>
|
||||||
|
<span className="block h-1.5 w-1.5 rounded-full bg-sky-400/70" />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div className="md:hidden">
|
<div className="md:hidden">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'p-2 mx-3 my-0.5 rounded-md bg-card border active:scale-[0.98] transition-all duration-150 relative',
|
'p-2 mx-3 my-0.5 rounded-md bg-card border active:scale-[0.98] transition-all duration-150 relative',
|
||||||
isSelected ? 'bg-primary/5 border-primary/20' : '',
|
isSelected ? 'bg-primary/5 border-primary/20' : '',
|
||||||
!isSelected && sessionView.isActive
|
!isSelected && isProcessing
|
||||||
? 'border-green-500/30 bg-green-50/5 dark:bg-green-900/5'
|
? 'border-amber-500/40 bg-amber-50/10 dark:bg-amber-900/10'
|
||||||
: 'border-border/30',
|
: !isSelected && isRecentlyActive
|
||||||
|
? 'border-sky-500/20'
|
||||||
|
: 'border-border/30',
|
||||||
)}
|
)}
|
||||||
onClick={selectMobileSession}
|
onClick={selectMobileSession}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ type UseProjectsStateArgs = {
|
||||||
latestMessage: AppSocketMessage | null;
|
latestMessage: AppSocketMessage | null;
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
activeSessions: Set<string>;
|
activeSessions: Set<string>;
|
||||||
|
processingSessions: Set<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FetchProjectsOptions = {
|
type FetchProjectsOptions = {
|
||||||
|
|
@ -241,6 +242,7 @@ export function useProjectsState({
|
||||||
latestMessage,
|
latestMessage,
|
||||||
isMobile,
|
isMobile,
|
||||||
activeSessions,
|
activeSessions,
|
||||||
|
processingSessions,
|
||||||
}: UseProjectsStateArgs) {
|
}: UseProjectsStateArgs) {
|
||||||
const [projects, setProjects] = useState<Project[]>([]);
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
|
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
|
||||||
|
|
@ -830,6 +832,7 @@ export function useProjectsState({
|
||||||
settingsInitialTab,
|
settingsInitialTab,
|
||||||
onCloseSettings: () => setShowSettings(false),
|
onCloseSettings: () => setShowSettings(false),
|
||||||
isMobile,
|
isMobile,
|
||||||
|
processingSessions,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
handleNewSession,
|
handleNewSession,
|
||||||
|
|
@ -847,6 +850,7 @@ export function useProjectsState({
|
||||||
selectedProject,
|
selectedProject,
|
||||||
selectedSession,
|
selectedSession,
|
||||||
showSettings,
|
showSettings,
|
||||||
|
processingSessions,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue