@@ -7,8 +7,8 @@ import { workspaceQueryKey } from "../hooks/useWorkspaceQuery";
77import { formatTimeCompact } from "../lib/format-time" ;
88import { useSessionScmSummary , type SessionPRSummary } from "../hooks/useSessionScmSummary" ;
99import { prBrowserUrl , prStatusRows , sessionPRDisplaySummaries , type PRDisplayTone } from "../lib/pr-display" ;
10- import type { SessionStatus , WorkspaceSession } from "../types/workspace" ;
11- import { sortedPRs , workerDisplayStatus } from "../types/workspace" ;
10+ import type { SessionActivityState , WorkspaceSession } from "../types/workspace" ;
11+ import { sortedPRs } from "../types/workspace" ;
1212import { BrowserPanelView } from "./BrowserPanel" ;
1313import type { BrowserViewModel } from "../hooks/useBrowserView" ;
1414import { Badge } from "./ui/badge" ;
@@ -259,38 +259,61 @@ type TimelineTone = "now" | "good" | "warn" | "neutral";
259259
260260function ActivityTimeline ( { session } : { session : WorkspaceSession } ) {
261261 const events : { tone : TimelineTone ; node : ReactNode ; ts : string | null } [ ] = [ ] ;
262- const detail = activityDetail ( session . status ) ;
262+
263+ events . push ( {
264+ tone : "neutral" ,
265+ node : < > Created worktree & branch</ > ,
266+ ts : formatTimeCompact ( session . createdAt ?? session . updatedAt ) ,
267+ } ) ;
268+
269+ for ( const pr of sortedPRs ( session ) ) {
270+ events . push ( {
271+ tone : pr . state === "merged" ? "good" : "neutral" ,
272+ node :
273+ pr . state === "merged" ? (
274+ < >
275+ Merged < b > PR #{ pr . number } </ b >
276+ </ >
277+ ) : (
278+ < >
279+ Opened < b > PR #{ pr . number } </ b >
280+ </ >
281+ ) ,
282+ ts : null ,
283+ } ) ;
284+ }
285+
286+ for ( const event of scmActivityEvents ( session ) ) {
287+ events . push ( event ) ;
288+ }
263289
264290 events . push ( {
265291 tone : "now" ,
266292 node : (
267293 < >
268294 < span className = "inspector-timeline__badge" >
269- < InspectorStatusPill session = { session } />
295+ < InspectorActivityPill state = { session . activity ?. state ?? "unknown" } />
270296 </ span >
271- { detail ? < span className = "inspector-timeline__detail" > — { detail } </ span > : null }
272297 </ >
273298 ) ,
274- ts : formatTimeCompact ( session . updatedAt ) ,
299+ ts : session . activity ?. lastActivityAt ? formatTimeCompact ( session . activity . lastActivityAt ) : null ,
275300 } ) ;
276301
277- for ( const pr of sortedPRs ( session ) ) {
302+ if ( session . status === "merged" ) {
278303 events . push ( {
279304 tone : "good" ,
280- node : (
281- < >
282- Opened < b > PR #{ pr . number } </ b >
283- </ >
284- ) ,
285- ts : null ,
305+ node : < > Done</ > ,
306+ ts : formatTimeCompact ( session . updatedAt ) ,
286307 } ) ;
287308 }
288309
289- events . push ( {
290- tone : "neutral" ,
291- node : < > Created worktree & branch</ > ,
292- ts : formatTimeCompact ( session . createdAt ?? session . updatedAt ) ,
293- } ) ;
310+ if ( session . status === "terminated" ) {
311+ events . push ( {
312+ tone : "neutral" ,
313+ node : < > Terminated</ > ,
314+ ts : formatTimeCompact ( session . updatedAt ) ,
315+ } ) ;
316+ }
294317
295318 return (
296319 < div className = "inspector-timeline" >
@@ -313,38 +336,35 @@ function ActivityTimeline({ session }: { session: WorkspaceSession }) {
313336 ) ;
314337}
315338
316- function activityDetail ( status : SessionStatus ) : string | null {
317- switch ( status ) {
318- case "idle" :
319- return "Session idle" ;
320- case "needs_input" :
321- return "Waiting for your input" ;
322- case "no_signal" :
323- return "No recent agent signal" ;
324- case "working" :
325- return null ;
326- default :
327- return null ;
339+ function scmActivityEvents ( session : WorkspaceSession ) : { tone : TimelineTone ; node : ReactNode ; ts : string | null } [ ] {
340+ const events : { tone : TimelineTone ; node : ReactNode ; ts : string | null } [ ] = [ ] ;
341+ if ( session . status === "ci_failed" || session . prs . some ( ( pr ) => pr . ci === "failing" ) ) {
342+ events . push ( {
343+ tone : "warn" ,
344+ node : < > CI failed</ > ,
345+ ts : formatTimeCompact ( session . updatedAt ) ,
346+ } ) ;
347+ }
348+ if ( session . status === "review_pending" || session . prs . some ( ( pr ) => pr . review === "review_required" ) ) {
349+ events . push ( {
350+ tone : "neutral" ,
351+ node : < > Review required</ > ,
352+ ts : formatTimeCompact ( session . updatedAt ) ,
353+ } ) ;
328354 }
355+ return events ;
329356}
330357
331- const STATUS_PILL : Record <
332- ReturnType < typeof workerDisplayStatus > | "idle" ,
333- { label : string ; tone : string ; breathe : boolean }
334- > = {
335- working : { label : "Working" , tone : "var(--orange)" , breathe : true } ,
336- needs_you : { label : "Input needed" , tone : "var(--amber)" , breathe : false } ,
337- ci_failed : { label : "CI failed" , tone : "var(--red)" , breathe : false } ,
338- no_signal : { label : "No signal" , tone : "var(--fg-muted)" , breathe : false } ,
339- mergeable : { label : "Ready" , tone : "var(--green)" , breathe : false } ,
340- done : { label : "Done" , tone : "var(--fg-muted)" , breathe : false } ,
341- unknown : { label : "Unknown" , tone : "var(--fg-muted)" , breathe : false } ,
358+ const ACTIVITY_PILL : Record < SessionActivityState , { label : string ; tone : string ; breathe : boolean } > = {
359+ active : { label : "Working" , tone : "var(--orange)" , breathe : true } ,
342360 idle : { label : "Idle" , tone : "var(--fg-muted)" , breathe : false } ,
361+ waiting_input : { label : "Input needed" , tone : "var(--amber)" , breathe : false } ,
362+ exited : { label : "Exited" , tone : "var(--fg-muted)" , breathe : false } ,
363+ unknown : { label : "Unknown" , tone : "var(--fg-muted)" , breathe : false } ,
343364} ;
344365
345- function InspectorStatusPill ( { session } : { session : WorkspaceSession } ) {
346- const key = session . status === "idle" ? "idle" : workerDisplayStatus ( session ) ;
347- const { label, tone, breathe } = STATUS_PILL [ key ] ;
366+ function InspectorActivityPill ( { state } : { state : SessionActivityState } ) {
367+ const { label, tone, breathe } = ACTIVITY_PILL [ state ] ;
348368 return (
349369 < span
350370 className = "inline-flex shrink-0 items-center gap-[7px] whitespace-nowrap rounded-[7px] px-[11px] py-[5px] text-[11.5px] font-semibold"
0 commit comments