fix: keep project tasks compact with visible running counts

Collapse the project task list by default while retaining create and filter actions. Move the project name into the conversation statistics card and keep the running task count visible independently of list filters.

Validation: 60 sidebar tests, typecheck, and lint passed. The earlier full test run encountered an offline dependency package failure and timed out.

Release note: 项目任务默认收起,标题旁显示运行中任务数量;项目名称整合到当前会话统计卡片中。
This commit is contained in:
mesalogo
2026-09-18 15:48:26 +08:00
parent ca3c919a34
commit daa14d0b7e
6 changed files with 92 additions and 8 deletions
@@ -69,6 +69,7 @@ FR-TC2:顶部紧凑展示当前会话消息总数与 Agent 累计回复时间
缺失历史或时间证据不完整时提示“部分历史用时不可用”,已知用时不能表示为完整总量。
远程执行缺少可靠时间证据时显示不可用或不完整,不用事件接收时间虚构远程用时。
没有当前会话时提示选择会话,项目任务列表仍保留。
项目名称放在当前会话统计卡片内部的顶部。
FR-TC3:任务累计执行时间按其历次执行请求汇总,周期触发仍显示在同一任务内。
未执行与缺失执行证据必须区分;具体关联和统计边界见[技术设计](./technical-design.md)。
@@ -77,6 +78,8 @@ FR-TC3:任务累计执行时间按其历次执行请求汇总,周期触发
- 点击条目打开关联 Conversation,并定位到该 Task。
- FR-TC4:项目任务标题右侧依次放置“+”新建按钮和筛选图标。状态筛选默认收起、默认全部,
项目任务列表默认收起,标题与数量、新建及筛选入口保持可见;点击标题切换展开状态,点击筛选同时展开列表。
标题旁同时显示当前项目顶层任务中处于 `running` 状态的数量(如“2 运行中”),不受筛选或收起影响;为零时省略。
展开后提供全部、进行中、暂停、已结束;收起不清除已选条件,非全部时图标高亮并显示筛选条件。
- 进行中包含 `idle`、`queued`、`running`、`waiting_approval`、`failed` 和 `interrupted`;
暂停对应 `paused`,已结束包含 `completed` 和 `cancelled`。分类只影响列表筛选,
@@ -247,6 +247,8 @@ function sidebarElement({
function renderSidebar(options: Parameters<typeof sidebarElement>[0] = {}): HTMLElement {
render(sidebarElement(options))
const taskListToggle = screen.queryByRole('button', { name: /^项目任务 /u })
if (taskListToggle) fireEvent.click(taskListToggle)
return screen.getByRole('complementary', {
name: '助手工作栏'
})
@@ -360,6 +362,7 @@ describe('RightAssistantSidebar resizing', () => {
taskDurations: new Map([['a', { durationMs: 125_000, incomplete: true }]])
}
const view = render(sidebarElement(props))
fireEvent.click(screen.getByRole('button', { name: '项目任务 2' }))
const stats = within(screen.getByRole('region', { name: '当前会话统计' }))
expect(stats.getByText('01:01:01')).toBeVisible()
expect(stats.getByText('24')).toBeVisible()
@@ -644,6 +647,43 @@ describe('RightAssistantSidebar resizing', () => {
expect(sidebar).toHaveAttribute('role', 'complementary')
})
it('collapses project tasks by default and toggles the list from its heading', () => {
render(sidebarElement({ tab: 'tasks' }))
const toggle = screen.getByRole('button', { name: '项目任务 0' })
const list = document.getElementById(toggle.getAttribute('aria-controls')!)
expect(toggle).toHaveAttribute('aria-expanded', 'false')
expect(list).not.toBeVisible()
expect(screen.getByRole('button', { name: '新建任务' })).toBeVisible()
fireEvent.click(toggle)
expect(list).toBeVisible()
fireEvent.click(toggle)
expect(list).not.toBeVisible()
fireEvent.click(screen.getByRole('button', { name: '筛选任务' }))
expect(toggle).toHaveAttribute('aria-expanded', 'true')
expect(list).toBeVisible()
})
it('keeps the project inside statistics and shows running counts while collapsed or filtered', () => {
const tasks: AssistantTask[] = ['running', 'running', 'queued', 'completed'].map((status, index) => ({
id: `summary-${index}`, title: `Summary task ${index}`, instructions: '',
projectId: currentProject.id, origin: 'schedule', status: status as AssistantTask['status'],
createdAt: '2026-09-18T00:00:00Z'
}))
tasks.push({ ...tasks[0]!, id: 'other-project', projectId: 'other-project' })
tasks.push({ ...tasks[0]!, id: 'child-task', parentTaskId: tasks[0]!.id })
const view = render(sidebarElement({ tab: 'tasks', tasks }))
const stats = screen.getByRole('region', { name: '当前会话统计' })
expect(within(stats).getByText(`项目:${currentProject.name}`)).toBeVisible()
expect(screen.getByRole('button', { name: '项目任务 4' })).toHaveAttribute('aria-expanded', 'false')
expect(screen.getByText('2 运行中')).toBeVisible()
fireEvent.click(screen.getByRole('button', { name: '筛选任务' }))
fireEvent.click(within(screen.getByRole('group', { name: '筛选任务' })).getByRole('button', { name: '已结束' }))
expect(screen.getByText('2 运行中')).toBeVisible()
view.rerender(sidebarElement({ tab: 'tasks', tasks: [] }))
expect(screen.queryByText('2 运行中')).not.toBeInTheDocument()
expect(screen.queryByText('0 运行中')).not.toBeInTheDocument()
})
it('keeps the product Task index in the task center', () => {
const onCreateCustomTask = vi.fn()
renderSidebar({ tab: 'tasks', onCreateCustomTask })
+24 -7
View File
@@ -8,7 +8,6 @@ import {
ExternalLink,
FileText,
Filter,
ClockFading,
Maximize2,
Minimize2,
Plus,
@@ -886,6 +885,7 @@ export function RightAssistantSidebar({
'all' | 'active' | 'paused' | 'finished'
>('all')
const [taskFiltersExpanded, setTaskFiltersExpanded] = useState(false)
const [taskListExpanded, setTaskListExpanded] = useState(false)
const [actionErrorState, setActionErrorState] = useState({
scope: { instanceId: activeWorkbarInstanceId, open },
message: ''
@@ -987,6 +987,7 @@ export function RightAssistantSidebar({
),
[taskMatchesScope, tasks]
)
const runningTaskCount = topLevelTasks.filter((task) => task.status === 'running').length
const { approvalsByTask, unassociatedApprovals } = useMemo(() => {
const tasksById = new Map(topLevelTasks.map((task) => [task.id, task]))
const approvalsByTask = new Map<string, PendingSidebarApproval[]>()
@@ -1933,10 +1934,10 @@ export function RightAssistantSidebar({
) : null}
{instance.appId === 'tasks' && (
<section className="assistant-sidebar__section task-center">
<section className="task-center__stats" aria-label={t('sidebar.tasks.stats.label')}>
<p className="task-center__scope">{currentProject
? t('sidebar.tasks.projectScope', { project: currentProject.name })
: t('sidebar.tasks.scope.currentProject')}</p>
<section className="task-center__stats" aria-label={t('sidebar.tasks.stats.label')}>
<span className="task-center__eyebrow">{t('sidebar.tasks.stats.currentConversation')}</span>
<h3>{activeConversationId
? currentStats?.title ?? conversationTitles.get(activeConversationId) ?? t('sidebar.tasks.stats.currentConversation')
@@ -1956,9 +1957,20 @@ export function RightAssistantSidebar({
<>
<div className="task-center__index-heading">
<h3>
<ClockFading size={15} />
<button
className="task-center__list-toggle"
type="button"
aria-expanded={taskListExpanded}
aria-controls={`project-tasks-${instance.id}`}
onClick={() => setTaskListExpanded((expanded) => !expanded)}
>
<ChevronRight aria-hidden="true" size={15} />
{t('sidebar.tasks.taskIndexTitle')}{' '}
<span className="task-center__count">{topLevelTasks.length}</span>
</button>
{runningTaskCount > 0 && <span className="task-center__running-count">
{t('sidebar.tasks.runningCount', { count: runningTaskCount })}
</span>}
</h3>
<button
className="secondary-button task-center__create"
@@ -1973,14 +1985,17 @@ export function RightAssistantSidebar({
className="secondary-button task-center__filter-toggle"
aria-label={t('sidebar.tasks.filters.ariaLabel')}
title={t('sidebar.tasks.filters.ariaLabel')}
aria-expanded={taskFiltersExpanded}
aria-expanded={taskListExpanded && taskFiltersExpanded}
aria-controls={`task-filters-${instance.id}`}
data-active={taskFilter !== 'all'}
onClick={() => setTaskFiltersExpanded((expanded) => !expanded)}
onClick={() => {
setTaskListExpanded(true)
setTaskFiltersExpanded((expanded) => !expanded)
}}
type="button"
><Filter aria-hidden="true" size={14} /></button>
</div>
<div className="task-center__filters" id={`task-filters-${instance.id}`} hidden={!taskFiltersExpanded}
<div className="task-center__filters" id={`task-filters-${instance.id}`} hidden={!taskListExpanded || !taskFiltersExpanded}
onFocusCapture={(event) => event.target.scrollIntoView?.({ block: 'nearest', inline: 'nearest' })}>
<SegmentedControl
ariaLabel={t('sidebar.tasks.filters.ariaLabel')}
@@ -2003,7 +2018,7 @@ export function RightAssistantSidebar({
value={taskFilter}
/>
</div>
{taskFilter !== 'all' && <div className="task-center__filter-summary">
{taskListExpanded && taskFilter !== 'all' && <div className="task-center__filter-summary">
<span>{t('sidebar.tasks.filters.summary', { filter: t(`sidebar.tasks.filters.${taskFilter}`), count: filteredTasks.length })}</span>
<button type="button" onClick={() => setTaskFilter('all')}>{t('sidebar.tasks.filters.clear')}</button>
</div>}
@@ -2013,6 +2028,7 @@ export function RightAssistantSidebar({
<SidebarApproval key={approval.approvalId} approval={approval} onRespondApproval={onRespondApproval} />
))}
</>}
<div className="task-center__list" id={`project-tasks-${instance.id}`} hidden={!taskListExpanded}>
{filteredTasks.length === 0 ? (
<p className="assistant-sidebar__empty">
{topLevelTasks.length === 0
@@ -2124,6 +2140,7 @@ export function RightAssistantSidebar({
)
})
)}
</div>
</>
)}
</section>
@@ -323,6 +323,7 @@ export const workspace = {
deny: 'Deny',
allowOnce: 'Allow once',
taskIndexTitle: 'Project tasks',
runningCount: '{{count}} running',
stats: {
label: 'Current conversation statistics',
currentConversation: 'Current conversation',
@@ -315,6 +315,7 @@ export const workspace = {
deny: '拒绝',
allowOnce: '仅此次允许',
taskIndexTitle: '项目任务',
runningCount: '{{count}} 运行中',
stats: {
label: '当前会话统计',
currentConversation: '当前会话',
+23 -1
View File
@@ -1,5 +1,5 @@
.task-center .task-center__scope {
margin: 0;
margin: 0 0 var(--space-2);
color: var(--text-secondary);
font-size: var(--font-caption);
overflow-wrap: anywhere;
@@ -59,6 +59,28 @@
.task-center__count { color: var(--text-muted); }
.task-center__running-count {
color: var(--accent);
font-size: var(--font-caption);
white-space: nowrap;
}
.task-center__list-toggle {
display: inline-flex;
align-items: center;
gap: var(--space-2);
padding: 0;
border: 0;
background: transparent;
color: inherit;
font: inherit;
cursor: pointer;
}
.task-center__list-toggle[aria-expanded='true'] > svg { transform: rotate(90deg); }
.task-center__list { display: flex; flex-direction: column; gap: var(--space-2); }
.task-center__list[hidden] { display: none; }
.task-center .task-center__create,
.task-center .task-center__filter-toggle {
display: inline-flex;