feat(web): split pinned running section into working/pending/idle groups with distinct badges

This commit is contained in:
2026-08-02 22:48:23 +08:00
parent b4a8e89fe3
commit ceeff60235
4 changed files with 87 additions and 24 deletions
+64 -23
View File
@@ -41,6 +41,12 @@ type SessionGroup = {
hasActiveSession: boolean
}
const RUNNING_BUCKETS = [
{ key: 'working', labelKey: 'session.item.running', colorClass: 'text-[var(--app-badge-success-text)]', pulse: true },
{ key: 'pending', labelKey: 'session.item.pending', colorClass: 'text-[var(--app-badge-warning-text)]', pulse: true },
{ key: 'idle', labelKey: 'session.item.idle', colorClass: 'text-[var(--app-hint)]', pulse: false },
] as const
export type SessionTimeRange = {
start: number | null
end: number | null
@@ -1072,12 +1078,33 @@ export function SessionList(props: {
: visibleSessions.filter(session => (session.metadata?.machineId ?? UNKNOWN_MACHINE_ID) === activeMachineFilter),
[visibleSessions, activeMachineFilter]
)
const runningSessions = useMemo(
() => machineFilteredSessions
.filter((session) => session.active)
.sort((a, b) => b.updatedAt - a.updatedAt),
[machineFilteredSessions]
)
const runningSessions = useMemo(() => {
const buckets: Record<'working' | 'pending' | 'idle', SessionSummary[]> = {
working: [],
pending: [],
idle: []
}
for (const session of machineFilteredSessions) {
if (!session.active) {
continue
}
if (session.thinking || (session.backgroundTaskCount ?? 0) > 0) {
buckets.working.push(session)
} else if ((session.pendingRequestsCount ?? 0) > 0) {
buckets.pending.push(session)
} else {
buckets.idle.push(session)
}
}
const byRecent = (a: SessionSummary, b: SessionSummary) => b.updatedAt - a.updatedAt
for (const key of Object.keys(buckets) as Array<keyof typeof buckets>) {
buckets[key].sort(byRecent)
}
return buckets
}, [machineFilteredSessions])
const runningSessionTotal = runningSessions.working.length
+ runningSessions.pending.length
+ runningSessions.idle.length
const groups = useMemo(
() => groupSessionsByDirectory(machineFilteredSessions.filter((session) => !session.active)),
[machineFilteredSessions]
@@ -1417,13 +1444,13 @@ export function SessionList(props: {
/>
) : null}
{props.sessions.length > 0 && (isFiltering || activeMachineFilter !== null) && groups.length === 0 && runningSessions.length === 0 ? (
{props.sessions.length > 0 && (isFiltering || activeMachineFilter !== null) && groups.length === 0 && runningSessionTotal === 0 ? (
<div className="px-4 py-8 text-center text-sm text-[var(--app-hint)]">
{t('sessions.search.noResults')}
</div>
) : null}
{runningSessions.length > 0 ? (
{runningSessionTotal > 0 ? (
<div key="running-section">
<div
className="group/running flex min-w-0 w-full select-none cursor-pointer items-center gap-2 rounded-lg py-1.5 pl-2 pr-2 transition-colors hover:bg-[var(--app-secondary-bg)]"
@@ -1447,26 +1474,40 @@ export function SessionList(props: {
{t('sessions.runningSection')}
</span>
<span className="shrink-0 text-[11px] tabular-nums text-[var(--app-hint)]">
({runningSessions.length})
({runningSessionTotal})
</span>
</div>
<div className="collapsible-panel" data-open={(!runningSectionCollapsed || isFiltering) || undefined}>
<div className="collapsible-inner">
<div className="flex flex-col gap-0.5 ml-3 pl-1 py-1">
{runningSessions.map((s) => (
<SessionItem
key={s.id}
session={s}
onSelect={props.onSelect}
showPath={false}
api={api}
selected={s.id === selectedSessionId}
showDetailedStatus={showDetailedStatus}
inRunningSection
projectLabel={getGroupDisplayName(s.metadata?.worktree?.basePath ?? s.metadata?.path ?? 'Other')}
machineLabel={resolveMachineLabel(s.metadata?.machineId ?? null)}
/>
))}
{RUNNING_BUCKETS.map((bucket) => {
const sessions = runningSessions[bucket.key]
if (sessions.length === 0) {
return null
}
return (
<div key={bucket.key}>
<div className={`flex items-center gap-1 px-1 pt-1 pb-0.5 text-[11px] font-medium ${bucket.colorClass}`}>
<span className={`h-1.5 w-1.5 shrink-0 rounded-full bg-current ${bucket.pulse ? 'animate-pulse' : ''}`} aria-hidden="true" />
{t(bucket.labelKey)} ({sessions.length})
</div>
{sessions.map((s) => (
<SessionItem
key={s.id}
session={s}
onSelect={props.onSelect}
showPath={false}
api={api}
selected={s.id === selectedSessionId}
showDetailedStatus={showDetailedStatus}
inRunningSection
projectLabel={getGroupDisplayName(s.metadata?.worktree?.basePath ?? s.metadata?.path ?? 'Other')}
machineLabel={resolveMachineLabel(s.metadata?.machineId ?? null)}
/>
))}
</div>
)
})}
</div>
</div>
</div>
+21 -1
View File
@@ -185,7 +185,7 @@ export function SessionRowSummary(props: {
title={attentionLabel ?? undefined}
aria-label={attentionLabel ?? undefined}
/>
) : s.active ? (
) : s.active && (s.backgroundTaskCount ?? 0) > 0 ? (
<span
className="inline-flex shrink-0 items-center gap-1 text-[var(--app-badge-success-text)]"
title={t('session.item.running')}
@@ -195,6 +195,26 @@ export function SessionRowSummary(props: {
<span className="text-[11px] font-medium leading-none">{t('session.item.running')}</span>
) : null}
</span>
) : s.active && (s.pendingRequestsCount ?? 0) > 0 ? (
<span
className="inline-flex shrink-0 items-center gap-1 text-[var(--app-badge-warning-text)]"
title={t('session.item.pending')}
>
<span className="h-1.5 w-1.5 rounded-full bg-current animate-pulse" aria-hidden="true" />
{!inRunningSection ? (
<span className="text-[11px] font-medium leading-none">{t('session.item.pending')}</span>
) : null}
</span>
) : s.active ? (
<span
className="inline-flex shrink-0 items-center gap-1 text-[var(--app-hint)]"
title={t('session.item.idle')}
>
<span className="h-1.5 w-1.5 rounded-full bg-current" aria-hidden="true" />
{!inRunningSection ? (
<span className="text-[11px] font-medium leading-none">{t('session.item.idle')}</span>
) : null}
</span>
) : attention && nestedTooltips && attentionId ? (
<SessionAttentionIndicator
attention={attention}
+1
View File
@@ -135,6 +135,7 @@ export default {
'session.item.pending': 'pending',
'session.item.thinking': 'thinking',
'session.item.running': 'Running',
'session.item.idle': 'Idle',
'session.item.permission': 'Permission required',
'session.item.needsInput': 'Needs input',
'session.item.background': 'Background tasks running',
+1
View File
@@ -135,6 +135,7 @@ export default {
'session.item.pending': '待处理',
'session.item.thinking': '思考中',
'session.item.running': '运行中',
'session.item.idle': '空闲',
'session.item.permission': '需要权限',
'session.item.needsInput': '需要输入',
'session.item.background': '后台任务运行中',