mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat(web): pin running sessions in an in-progress section with state badges (#1315)
* feat(web): pin running sessions in an 'in progress' section with a live badge * feat(web): show project name on pinned running session rows * feat(web): make the pinned 'in progress' section collapsible * fix(web): don't auto-expand directory groups when opening pinned running sessions * fix(web): keep running section open while searching; clear auto-expand guard when selection leaves a group * feat(web): show machine label on pinned running session rows * fix(web): make running-section toggle keyboard-accessible with correct filtered state * feat(web): split pinned running section into working/pending/idle groups with distinct badges
This commit is contained in:
@@ -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-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/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=="],
|
"@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=="],
|
||||||
|
|||||||
@@ -388,9 +388,9 @@ describe('SessionList collapse behavior', () => {
|
|||||||
]
|
]
|
||||||
const { rerender } = render(renderSessionList(baseSessions))
|
const { rerender } = render(renderSessionList(baseSessions))
|
||||||
|
|
||||||
expect(getProjectPanel().getAttribute('data-open')).toBe('true')
|
// The running session is pinned in the "in progress" section; the
|
||||||
|
// directory group now only holds inactive sessions and starts
|
||||||
fireEvent.click(screen.getByTitle('/work/hapi'))
|
// collapsed.
|
||||||
expect(getProjectPanel().getAttribute('data-open')).toBeNull()
|
expect(getProjectPanel().getAttribute('data-open')).toBeNull()
|
||||||
|
|
||||||
rerender(renderSessionList([
|
rerender(renderSessionList([
|
||||||
@@ -410,9 +410,7 @@ describe('SessionList collapse behavior', () => {
|
|||||||
it('auto-expands the path again when the selected session changes', async () => {
|
it('auto-expands the path again when the selected session changes', async () => {
|
||||||
const sessions = [
|
const sessions = [
|
||||||
makeSession({
|
makeSession({
|
||||||
id: 'session-running',
|
id: 'session-first',
|
||||||
active: true,
|
|
||||||
thinking: true,
|
|
||||||
updatedAt: 100,
|
updatedAt: 100,
|
||||||
metadata: { path: '/work/hapi', name: 'Running task', flavor: 'codex' },
|
metadata: { path: '/work/hapi', name: 'Running task', flavor: 'codex' },
|
||||||
}),
|
}),
|
||||||
@@ -424,16 +422,85 @@ describe('SessionList collapse behavior', () => {
|
|||||||
]
|
]
|
||||||
const { rerender } = render(renderSessionList(sessions))
|
const { rerender } = render(renderSessionList(sessions))
|
||||||
|
|
||||||
fireEvent.click(screen.getByTitle('/work/hapi'))
|
// Inactive-only groups start collapsed; selecting a session inside
|
||||||
|
// one auto-expands it.
|
||||||
expect(getProjectPanel().getAttribute('data-open')).toBeNull()
|
expect(getProjectPanel().getAttribute('data-open')).toBeNull()
|
||||||
|
|
||||||
rerender(renderSessionList(sessions, 'session-next'))
|
rerender(renderSessionList([
|
||||||
|
...sessions,
|
||||||
|
makeSession({
|
||||||
|
id: 'session-running',
|
||||||
|
active: true,
|
||||||
|
thinking: true,
|
||||||
|
updatedAt: 110,
|
||||||
|
metadata: { path: '/work/hapi', name: 'Running task', flavor: 'codex' },
|
||||||
|
}),
|
||||||
|
], 'session-next'))
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(getProjectPanel().getAttribute('data-open')).toBe('true')
|
expect(getProjectPanel().getAttribute('data-open')).toBe('true')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps the running section open while searching even when collapsed', () => {
|
||||||
|
const sessions = [
|
||||||
|
makeSession({
|
||||||
|
id: 'session-running',
|
||||||
|
active: true,
|
||||||
|
thinking: true,
|
||||||
|
updatedAt: 100,
|
||||||
|
metadata: { path: '/work/hapi', name: 'Running task', flavor: 'codex' },
|
||||||
|
}),
|
||||||
|
makeSession({
|
||||||
|
id: 'session-idle',
|
||||||
|
updatedAt: 50,
|
||||||
|
metadata: { path: '/work/hapi', name: 'Idle task', flavor: 'codex' },
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
render(renderSessionList(sessions))
|
||||||
|
|
||||||
|
const runningPanel = () => screen.getByTitle('In progress').nextElementSibling
|
||||||
|
|
||||||
|
expect(runningPanel()?.getAttribute('data-open')).toBe('true')
|
||||||
|
expect(screen.getByTitle('In progress').getAttribute('aria-expanded')).toBe('true')
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTitle('In progress'))
|
||||||
|
expect(runningPanel()?.getAttribute('data-open')).toBeNull()
|
||||||
|
expect(screen.getByTitle('In progress').getAttribute('aria-expanded')).toBe('false')
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Search sessions' }))
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('Search sessions…'), {
|
||||||
|
target: { value: 'Running' },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(runningPanel()?.getAttribute('data-open')).toBe('true')
|
||||||
|
// The section stays reported open while searching even though the
|
||||||
|
// underlying collapsed state is still set.
|
||||||
|
expect(screen.getByTitle('In progress').getAttribute('aria-expanded')).toBe('true')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toggles the running section with the keyboard', () => {
|
||||||
|
const sessions = [
|
||||||
|
makeSession({
|
||||||
|
id: 'session-running',
|
||||||
|
active: true,
|
||||||
|
thinking: true,
|
||||||
|
updatedAt: 100,
|
||||||
|
metadata: { path: '/work/hapi', name: 'Running task', flavor: 'codex' },
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
render(renderSessionList(sessions))
|
||||||
|
|
||||||
|
const header = screen.getByRole('button', { name: /In progress/ })
|
||||||
|
expect(header.getAttribute('aria-expanded')).toBe('true')
|
||||||
|
|
||||||
|
fireEvent.keyDown(header, { key: 'Enter' })
|
||||||
|
expect(header.getAttribute('aria-expanded')).toBe('false')
|
||||||
|
|
||||||
|
fireEvent.keyDown(header, { key: ' ' })
|
||||||
|
expect(header.getAttribute('aria-expanded')).toBe('true')
|
||||||
|
})
|
||||||
|
|
||||||
it('keeps the previous selected path open when selection moves', async () => {
|
it('keeps the previous selected path open when selection moves', async () => {
|
||||||
const sessions = [
|
const sessions = [
|
||||||
makeSession({
|
makeSession({
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ type SessionGroup = {
|
|||||||
hasActiveSession: boolean
|
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 = {
|
export type SessionTimeRange = {
|
||||||
start: number | null
|
start: number | null
|
||||||
end: number | null
|
end: number | null
|
||||||
@@ -764,9 +770,12 @@ function SessionItem(props: {
|
|||||||
api: ApiClient | null
|
api: ApiClient | null
|
||||||
selected?: boolean
|
selected?: boolean
|
||||||
showDetailedStatus?: boolean
|
showDetailedStatus?: boolean
|
||||||
|
inRunningSection?: boolean
|
||||||
|
projectLabel?: string
|
||||||
|
machineLabel?: string
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
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, projectLabel, machineLabel } = props
|
||||||
const { haptic } = usePlatform()
|
const { haptic } = usePlatform()
|
||||||
const [menuOpen, setMenuOpen] = useState(false)
|
const [menuOpen, setMenuOpen] = useState(false)
|
||||||
const [menuAnchorPoint, setMenuAnchorPoint] = useState<{ x: number; y: number }>({ x: 0, y: 0 })
|
const [menuAnchorPoint, setMenuAnchorPoint] = useState<{ x: number; y: number }>({ x: 0, y: 0 })
|
||||||
@@ -859,6 +868,9 @@ function SessionItem(props: {
|
|||||||
nestedTooltips
|
nestedTooltips
|
||||||
attentionTooltipId={attentionId}
|
attentionTooltipId={attentionId}
|
||||||
scheduleTooltipId={scheduleId}
|
scheduleTooltipId={scheduleId}
|
||||||
|
inRunningSection={inRunningSection}
|
||||||
|
projectLabel={projectLabel}
|
||||||
|
machineLabel={machineLabel}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -1066,13 +1078,41 @@ export function SessionList(props: {
|
|||||||
: visibleSessions.filter(session => (session.metadata?.machineId ?? UNKNOWN_MACHINE_ID) === activeMachineFilter),
|
: visibleSessions.filter(session => (session.metadata?.machineId ?? UNKNOWN_MACHINE_ID) === activeMachineFilter),
|
||||||
[visibleSessions, activeMachineFilter]
|
[visibleSessions, activeMachineFilter]
|
||||||
)
|
)
|
||||||
|
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(
|
const groups = useMemo(
|
||||||
() => groupSessionsByDirectory(machineFilteredSessions),
|
() => groupSessionsByDirectory(machineFilteredSessions.filter((session) => !session.active)),
|
||||||
[machineFilteredSessions]
|
[machineFilteredSessions]
|
||||||
)
|
)
|
||||||
const [collapseOverrides, setCollapseOverrides] = useState<Map<string, boolean>>(
|
const [collapseOverrides, setCollapseOverrides] = useState<Map<string, boolean>>(
|
||||||
() => new Map()
|
() => new Map()
|
||||||
)
|
)
|
||||||
|
const [runningSectionCollapsed, setRunningSectionCollapsed] = useState(false)
|
||||||
const autoExpandedSelectedSessionKeyRef = useRef<string | null>(null)
|
const autoExpandedSelectedSessionKeyRef = useRef<string | null>(null)
|
||||||
const isGroupCollapsed = (group: SessionGroup): boolean => {
|
const isGroupCollapsed = (group: SessionGroup): boolean => {
|
||||||
if (isFiltering) return false
|
if (isFiltering) return false
|
||||||
@@ -1159,17 +1199,28 @@ export function SessionList(props: {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const group = allGroups.find(g =>
|
// Pinned "in progress" sessions are not rendered inside directory
|
||||||
|
// groups, so only auto-expand when the selected session actually lives
|
||||||
|
// in a visible group. Using `allGroups` here would expand the group
|
||||||
|
// below whenever a running session is opened.
|
||||||
|
const group = groups.find(g =>
|
||||||
g.sessions.some(s => s.id === selectedSessionId)
|
g.sessions.some(s => s.id === selectedSessionId)
|
||||||
)
|
)
|
||||||
if (!group) return
|
if (!group) {
|
||||||
|
// The selected session is not rendered inside any directory group
|
||||||
|
// (e.g. it moved to the pinned "in progress" section). Drop the
|
||||||
|
// guard so it auto-expands again when it transitions back into a
|
||||||
|
// group later.
|
||||||
|
autoExpandedSelectedSessionKeyRef.current = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const autoExpandKey = `${selectedSessionId}::${group.key}`
|
const autoExpandKey = `${selectedSessionId}::${group.key}`
|
||||||
if (autoExpandedSelectedSessionKeyRef.current === autoExpandKey) return
|
if (autoExpandedSelectedSessionKeyRef.current === autoExpandKey) return
|
||||||
autoExpandedSelectedSessionKeyRef.current = autoExpandKey
|
autoExpandedSelectedSessionKeyRef.current = autoExpandKey
|
||||||
|
|
||||||
setCollapseOverrides(prev => expandSelectedSessionCollapseOverrides(prev, group))
|
setCollapseOverrides(prev => expandSelectedSessionCollapseOverrides(prev, group))
|
||||||
}, [selectedSessionId, allGroups])
|
}, [selectedSessionId, groups])
|
||||||
|
|
||||||
// Clean up stale collapse overrides
|
// Clean up stale collapse overrides
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1393,12 +1444,75 @@ export function SessionList(props: {
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{props.sessions.length > 0 && (isFiltering || activeMachineFilter !== null) && groups.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)]">
|
<div className="px-4 py-8 text-center text-sm text-[var(--app-hint)]">
|
||||||
{t('sessions.search.noResults')}
|
{t('sessions.search.noResults')}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{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)]"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-expanded={!runningSectionCollapsed || isFiltering}
|
||||||
|
onClick={() => setRunningSectionCollapsed((value) => !value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault()
|
||||||
|
setRunningSectionCollapsed((value) => !value)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title={t('sessions.runningSection')}
|
||||||
|
>
|
||||||
|
<ChevronIcon className="h-3.5 w-3.5 text-[var(--app-hint)] shrink-0" collapsed={runningSectionCollapsed && !isFiltering} />
|
||||||
|
<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)]">
|
||||||
|
({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">
|
||||||
|
{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>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{groups.map((group) => {
|
{groups.map((group) => {
|
||||||
const isCollapsed = isGroupCollapsed(group)
|
const isCollapsed = isGroupCollapsed(group)
|
||||||
const visibleGroupSessions = getVisibleGroupSessions(group)
|
const visibleGroupSessions = getVisibleGroupSessions(group)
|
||||||
|
|||||||
@@ -111,6 +111,12 @@ export function SessionRowSummary(props: {
|
|||||||
attentionTooltipId?: string
|
attentionTooltipId?: string
|
||||||
scheduleTooltipId?: string
|
scheduleTooltipId?: string
|
||||||
className?: string
|
className?: string
|
||||||
|
/** Rows inside the pinned "in progress" section skip the text label (dot only). */
|
||||||
|
inRunningSection?: boolean
|
||||||
|
/** Short project name shown under the title (pinned "in progress" rows). */
|
||||||
|
projectLabel?: string
|
||||||
|
/** Machine label shown next to the project name (pinned "in progress" rows). */
|
||||||
|
machineLabel?: string
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
session: s,
|
session: s,
|
||||||
@@ -121,6 +127,9 @@ export function SessionRowSummary(props: {
|
|||||||
attentionTooltipId: attentionTooltipIdProp,
|
attentionTooltipId: attentionTooltipIdProp,
|
||||||
scheduleTooltipId: scheduleTooltipIdProp,
|
scheduleTooltipId: scheduleTooltipIdProp,
|
||||||
className,
|
className,
|
||||||
|
inRunningSection = false,
|
||||||
|
projectLabel,
|
||||||
|
machineLabel,
|
||||||
} = props
|
} = props
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const sessionName = getSessionTitle(s)
|
const sessionName = getSessionTitle(s)
|
||||||
@@ -136,6 +145,8 @@ export function SessionRowSummary(props: {
|
|||||||
[s, selected, showDetailedStatus]
|
[s, selected, showDetailedStatus]
|
||||||
)
|
)
|
||||||
const attentionLabel = attention ? getAttentionLabel(attention, t) : null
|
const attentionLabel = attention ? getAttentionLabel(attention, t) : null
|
||||||
|
const urgentAttention = attention !== null
|
||||||
|
&& (attention.kind === 'permission' || attention.kind === 'input')
|
||||||
const scheduledLabel = s.futureScheduledMessageCount > 1
|
const scheduledLabel = s.futureScheduledMessageCount > 1
|
||||||
? t('session.item.scheduledMessages', { count: s.futureScheduledMessageCount })
|
? t('session.item.scheduledMessages', { count: s.futureScheduledMessageCount })
|
||||||
: t('session.item.scheduledMessage')
|
: t('session.item.scheduledMessage')
|
||||||
@@ -160,7 +171,50 @@ export function SessionRowSummary(props: {
|
|||||||
{sessionName}
|
{sessionName}
|
||||||
</div>
|
</div>
|
||||||
{s.active && s.thinking ? (
|
{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 && (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')}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
) : 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 ? (
|
) : attention && nestedTooltips && attentionId ? (
|
||||||
<SessionAttentionIndicator
|
<SessionAttentionIndicator
|
||||||
attention={attention}
|
attention={attention}
|
||||||
@@ -214,7 +268,11 @@ export function SessionRowSummary(props: {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{showPath || worktreeLabel ? (
|
{projectLabel || machineLabel ? (
|
||||||
|
<div className="truncate text-xs text-[var(--app-hint)]" title={[projectLabel, machineLabel].filter(Boolean).join(' · ')}>
|
||||||
|
{[projectLabel, machineLabel].filter(Boolean).join(' · ')}
|
||||||
|
</div>
|
||||||
|
) : showPath || worktreeLabel ? (
|
||||||
<div
|
<div
|
||||||
className="truncate text-xs text-[var(--app-hint)]"
|
className="truncate text-xs text-[var(--app-hint)]"
|
||||||
title={worktreeLabel
|
title={worktreeLabel
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export default {
|
|||||||
'sessions.timeFilter.dayWithActivity': '{date}, has session activity',
|
'sessions.timeFilter.dayWithActivity': '{date}, has session activity',
|
||||||
'sessions.group.expand': 'Expand {n}',
|
'sessions.group.expand': 'Expand {n}',
|
||||||
'sessions.group.collapse': 'Collapse {n}',
|
'sessions.group.collapse': 'Collapse {n}',
|
||||||
|
'sessions.runningSection': 'In progress',
|
||||||
'sessions.group.new': 'New session in this directory',
|
'sessions.group.new': 'New session in this directory',
|
||||||
'sessions.machineFilter.label': 'Filter sessions by machine',
|
'sessions.machineFilter.label': 'Filter sessions by machine',
|
||||||
'sessions.machineFilter.all': 'All',
|
'sessions.machineFilter.all': 'All',
|
||||||
@@ -133,6 +134,8 @@ export default {
|
|||||||
'session.item.worktree': 'worktree',
|
'session.item.worktree': 'worktree',
|
||||||
'session.item.pending': 'pending',
|
'session.item.pending': 'pending',
|
||||||
'session.item.thinking': 'thinking',
|
'session.item.thinking': 'thinking',
|
||||||
|
'session.item.running': 'Running',
|
||||||
|
'session.item.idle': 'Idle',
|
||||||
'session.item.permission': 'Permission required',
|
'session.item.permission': 'Permission required',
|
||||||
'session.item.needsInput': 'Needs input',
|
'session.item.needsInput': 'Needs input',
|
||||||
'session.item.background': 'Background tasks running',
|
'session.item.background': 'Background tasks running',
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export default {
|
|||||||
'sessions.timeFilter.dayWithActivity': '{date},有会话活动',
|
'sessions.timeFilter.dayWithActivity': '{date},有会话活动',
|
||||||
'sessions.group.expand': '展开 {n} 个',
|
'sessions.group.expand': '展开 {n} 个',
|
||||||
'sessions.group.collapse': '收起 {n} 个',
|
'sessions.group.collapse': '收起 {n} 个',
|
||||||
|
'sessions.runningSection': '进行中',
|
||||||
'sessions.group.new': '在此目录新建会话',
|
'sessions.group.new': '在此目录新建会话',
|
||||||
'sessions.machineFilter.label': '按机器筛选会话',
|
'sessions.machineFilter.label': '按机器筛选会话',
|
||||||
'sessions.machineFilter.all': '全部',
|
'sessions.machineFilter.all': '全部',
|
||||||
@@ -133,6 +134,8 @@ export default {
|
|||||||
'session.item.worktree': '工作树',
|
'session.item.worktree': '工作树',
|
||||||
'session.item.pending': '待处理',
|
'session.item.pending': '待处理',
|
||||||
'session.item.thinking': '思考中',
|
'session.item.thinking': '思考中',
|
||||||
|
'session.item.running': '运行中',
|
||||||
|
'session.item.idle': '空闲',
|
||||||
'session.item.permission': '需要权限',
|
'session.item.permission': '需要权限',
|
||||||
'session.item.needsInput': '需要输入',
|
'session.item.needsInput': '需要输入',
|
||||||
'session.item.background': '后台任务运行中',
|
'session.item.background': '后台任务运行中',
|
||||||
|
|||||||
Reference in New Issue
Block a user