@@ -2,11 +2,12 @@ import type { SessionSummary } from "@pi-deck/core/domain/session.js";
22import { useEffect , useRef , useState } from "react" ;
33import { ConfirmDialog } from "../../components/dialogs/ConfirmDialog.js" ;
44import { InlineRename } from "../../components/InlineRename.js" ;
5- import { Package , Pencil , Trash2 } from "../../components/icons/index.js" ;
5+ import { CheckCheck , Package , Pencil , Trash2 } from "../../components/icons/index.js" ;
66import { ContextMenu , type ContextMenuItem } from "../../components/ui/ContextMenu.js" ;
77import { relativeTime } from "../../lib/format/relative-time" ;
88import { useNavStore } from "../../lib/useNavStore" ;
9- import { useMessagesStore } from "../chat/useMessagesStore.js" ;
9+ import type { RailStatus } from "../chat/types.js" ;
10+ import { selectSessionRailStatus , useMessagesStore } from "../chat/useMessagesStore.js" ;
1011import { warmSession } from "./sessionWarmup.js" ;
1112import { useSessionsStore } from "./useSessionsStore" ;
1213
@@ -16,6 +17,15 @@ const HOVER_PREFETCH_DELAY_MS = 150;
1617// to sit alongside `var(--t-12)` mono labels.
1718const MENU_ICON_SIZE = 14 ;
1819
20+ // Accessible name for the status dot per state. `idle` gets no label (it's the resting default —
21+ // labelling every quiet row just adds screen-reader noise).
22+ const STATUS_LABEL : Record < Exclude < RailStatus , "idle" > , string > = {
23+ working : "Running" ,
24+ waiting : "Waiting for your input" ,
25+ done : "Finished" ,
26+ failed : "Failed" ,
27+ } ;
28+
1929export interface PidSessionRowProps {
2030 session : SessionSummary ;
2131 active : boolean ;
@@ -24,11 +34,20 @@ export interface PidSessionRowProps {
2434export function PidSessionRow ( { session, active } : PidSessionRowProps ) {
2535 const [ confirmOpen , setConfirmOpen ] = useState ( false ) ;
2636 const [ editing , setEditing ] = useState ( false ) ;
27- // The session is "live" while its worker is mid-turn (prompt sent, no turn-end yet).
28- // Only the active session can ever be in flight — but we still key by session id so the
29- // selector cost stays per-row and the rendered dot follows the right session if the user
30- // switches focus mid-turn.
31- const live = useMessagesStore ( ( s ) => s . bySession [ session . id ] ?. isTurnInFlight ?? false ) ;
37+ // Coarse lifecycle for the status dot: working / waiting / done / failed / idle. Keyed by
38+ // session id so the cost stays per-row and the dot follows the right session across focus
39+ // switches. The selector returns a primitive, so a stable status doesn't re-render the row.
40+ const status = useMessagesStore ( selectSessionRailStatus ( session . id ) ) ;
41+
42+ // Viewing the session you're focused on clears its done/failed dot to neutral idle. Covers the
43+ // "finished while you're already watching it" case; `activateSession` covers "switched to a
44+ // finished session", and the "Mark as completed" menu item is a manual trigger for the same
45+ // `markViewed` — acknowledging the turn without opening the session.
46+ useEffect ( ( ) => {
47+ if ( active && ( status === "done" || status === "failed" ) ) {
48+ useMessagesStore . getState ( ) . markViewed ( session . id ) ;
49+ }
50+ } , [ active , status , session . id ] ) ;
3251
3352 const onClick = ( ) => {
3453 if ( editing ) return ;
@@ -63,7 +82,20 @@ export function PidSessionRow({ session, active }: PidSessionRowProps) {
6382 [ ] ,
6483 ) ;
6584
85+ // "Mark as completed" acknowledges a finished/failed turn — it greys the dot to idle without
86+ // opening the session. Only meaningful when there's a terminal outcome to acknowledge.
87+ const canAcknowledge = status === "done" || status === "failed" ;
6688 const menuItems : ContextMenuItem [ ] = [
89+ ...( canAcknowledge
90+ ? ( [
91+ {
92+ label : "Mark as completed" ,
93+ icon : < CheckCheck size = { MENU_ICON_SIZE } aria-hidden /> ,
94+ onSelect : ( ) => useMessagesStore . getState ( ) . markViewed ( session . id ) ,
95+ } ,
96+ { kind : "separator" } ,
97+ ] satisfies ContextMenuItem [ ] )
98+ : [ ] ) ,
6799 {
68100 label : "Rename" ,
69101 icon : < Pencil size = { MENU_ICON_SIZE } aria-hidden /> ,
@@ -106,9 +138,11 @@ export function PidSessionRow({ session, active }: PidSessionRowProps) {
106138 title = { session . title }
107139 >
108140 < span
109- className = "pid-rail-row-marker"
110- data-state = { active ? "active" : "idle" }
111- aria-hidden
141+ className = "pid-rail-row-dot"
142+ data-status = { status }
143+ { ...( status === "idle"
144+ ? { "aria-hidden" : true }
145+ : { role : "img" , "aria-label" : STATUS_LABEL [ status ] , title : STATUS_LABEL [ status ] } ) }
112146 />
113147 < span className = "pid-rail-row-main" >
114148 { editing ? (
@@ -127,16 +161,7 @@ export function PidSessionRow({ session, active }: PidSessionRowProps) {
127161 ) }
128162 { session . branch ? < span className = "pid-rail-row-branch" > { session . branch } </ span > : null }
129163 </ span >
130- { live ? (
131- < span
132- className = "pid-rail-row-live"
133- role = "status"
134- aria-label = "Session is running"
135- title = "Running"
136- />
137- ) : (
138- < span className = "pid-rail-row-meta" > { relativeTime ( session . lastActivityAt ) } </ span >
139- ) }
164+ < span className = "pid-rail-row-meta" > { relativeTime ( session . lastActivityAt ) } </ span >
140165 </ button >
141166 </ ContextMenu >
142167 < ConfirmDialog
0 commit comments