feat(web): pin running sessions in an 'in progress' section with a live badge

This commit is contained in:
2026-08-02 21:26:30 +08:00
parent fb6f697555
commit f61ee46fec
5 changed files with 75 additions and 4 deletions
+2
View File
@@ -1096,6 +1096,8 @@
"@twsxtd/hapi-linux-x64": ["@twsxtd/hapi-linux-x64@0.25.3", "", { "os": "linux", "cpu": "x64", "bin": { "hapi": "bin/hapi" } }, "sha512-mLe7mJKctdEIeMmf19u0wWcpf4Lj7sA/SJT65cd+4G5wWRy632SAPcJ7pl+KSdsbEu9TMTyrXm1L4usB0xc4Gg=="],
"@twsxtd/hapi-win32-x64": ["@twsxtd/hapi-win32-x64@0.25.3", "", { "os": "win32", "cpu": "x64", "bin": { "hapi": "bin/hapi.exe" } }, "sha512-K1zhYTj8eAPt5W7q92U4ywdUs0122faulU2kywHOuzmwO95nI4zQ0CXlCNkJlGpTv6AYj7GiQdU21sR1NJSYFg=="],
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
+40 -3
View File
@@ -764,9 +764,10 @@ function SessionItem(props: {
api: ApiClient | null
selected?: boolean
showDetailedStatus?: boolean
inRunningSection?: boolean
}) {
const { t } = useTranslation()
const { session: s, onSelect, showPath = true, api, selected = false, showDetailedStatus = false } = props
const { session: s, onSelect, showPath = true, api, selected = false, showDetailedStatus = false, inRunningSection = false } = props
const { haptic } = usePlatform()
const [menuOpen, setMenuOpen] = useState(false)
const [menuAnchorPoint, setMenuAnchorPoint] = useState<{ x: number; y: number }>({ x: 0, y: 0 })
@@ -859,6 +860,7 @@ function SessionItem(props: {
nestedTooltips
attentionTooltipId={attentionId}
scheduleTooltipId={scheduleId}
inRunningSection={inRunningSection}
/>
</button>
@@ -1066,8 +1068,14 @@ 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 groups = useMemo(
() => groupSessionsByDirectory(machineFilteredSessions),
() => groupSessionsByDirectory(machineFilteredSessions.filter((session) => !session.active)),
[machineFilteredSessions]
)
const [collapseOverrides, setCollapseOverrides] = useState<Map<string, boolean>>(
@@ -1393,12 +1401,41 @@ export function SessionList(props: {
/>
) : null}
{props.sessions.length > 0 && (isFiltering || activeMachineFilter !== null) && groups.length === 0 ? (
{props.sessions.length > 0 && (isFiltering || activeMachineFilter !== null) && groups.length === 0 && runningSessions.length === 0 ? (
<div className="px-4 py-8 text-center text-sm text-[var(--app-hint)]">
{t('sessions.search.noResults')}
</div>
) : null}
{runningSessions.length > 0 ? (
<div key="running-section">
<div className="group/running flex min-w-0 w-full select-none items-center gap-2 rounded-lg py-1.5 pl-2 pr-2">
<span className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center" aria-hidden="true">
<span className="h-1.5 w-1.5 rounded-full bg-[var(--app-badge-success-text)] animate-pulse" />
</span>
<span className="min-w-0 flex-1 truncate text-sm font-medium">
{t('sessions.runningSection')}
</span>
<span className="shrink-0 text-[11px] tabular-nums text-[var(--app-hint)]">
({runningSessions.length})
</span>
</div>
<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
/>
))}
</div>
</div>
) : null}
{groups.map((group) => {
const isCollapsed = isGroupCollapsed(group)
const visibleGroupSessions = getVisibleGroupSessions(group)
+29 -1
View File
@@ -111,6 +111,8 @@ export function SessionRowSummary(props: {
attentionTooltipId?: string
scheduleTooltipId?: string
className?: string
/** Rows inside the pinned "in progress" section skip the text label (dot only). */
inRunningSection?: boolean
}) {
const {
session: s,
@@ -121,6 +123,7 @@ export function SessionRowSummary(props: {
attentionTooltipId: attentionTooltipIdProp,
scheduleTooltipId: scheduleTooltipIdProp,
className,
inRunningSection = false,
} = props
const { t } = useTranslation()
const sessionName = getSessionTitle(s)
@@ -136,6 +139,8 @@ export function SessionRowSummary(props: {
[s, selected, showDetailedStatus]
)
const attentionLabel = attention ? getAttentionLabel(attention, t) : null
const urgentAttention = attention !== null
&& (attention.kind === 'permission' || attention.kind === 'input')
const scheduledLabel = s.futureScheduledMessageCount > 1
? t('session.item.scheduledMessages', { count: s.futureScheduledMessageCount })
: t('session.item.scheduledMessage')
@@ -160,7 +165,30 @@ export function SessionRowSummary(props: {
{sessionName}
</div>
{s.active && s.thinking ? (
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin-slow text-[var(--app-hint)]" />
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin-slow text-[var(--app-badge-success-text)]" />
) : urgentAttention && nestedTooltips && attentionId ? (
<SessionAttentionIndicator
attention={attention}
summary={s}
label={attentionLabel ?? ''}
tooltipId={attentionId}
/>
) : urgentAttention ? (
<span
className={`inline-flex h-2 w-2 shrink-0 rounded-full ${ATTENTION_DOT_CLASS[attention.kind]}`}
title={attentionLabel ?? undefined}
aria-label={attentionLabel ?? undefined}
/>
) : s.active ? (
<span
className="inline-flex shrink-0 items-center gap-1 text-[var(--app-badge-success-text)]"
title={t('session.item.running')}
>
<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.running')}</span>
) : null}
</span>
) : attention && nestedTooltips && attentionId ? (
<SessionAttentionIndicator
attention={attention}
+2
View File
@@ -74,6 +74,7 @@ export default {
'sessions.timeFilter.dayWithActivity': '{date}, has session activity',
'sessions.group.expand': 'Expand {n}',
'sessions.group.collapse': 'Collapse {n}',
'sessions.runningSection': 'In progress',
'sessions.group.new': 'New session in this directory',
'sessions.machineFilter.label': 'Filter sessions by machine',
'sessions.machineFilter.all': 'All',
@@ -133,6 +134,7 @@ export default {
'session.item.worktree': 'worktree',
'session.item.pending': 'pending',
'session.item.thinking': 'thinking',
'session.item.running': 'Running',
'session.item.permission': 'Permission required',
'session.item.needsInput': 'Needs input',
'session.item.background': 'Background tasks running',
+2
View File
@@ -74,6 +74,7 @@ export default {
'sessions.timeFilter.dayWithActivity': '{date},有会话活动',
'sessions.group.expand': '展开 {n} 个',
'sessions.group.collapse': '收起 {n} 个',
'sessions.runningSection': '进行中',
'sessions.group.new': '在此目录新建会话',
'sessions.machineFilter.label': '按机器筛选会话',
'sessions.machineFilter.all': '全部',
@@ -133,6 +134,7 @@ export default {
'session.item.worktree': '工作树',
'session.item.pending': '待处理',
'session.item.thinking': '思考中',
'session.item.running': '运行中',
'session.item.permission': '需要权限',
'session.item.needsInput': '需要输入',
'session.item.background': '后台任务运行中',