fix: simplify task center status selection and rendering
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
|
||||
[当前会话统计 Demo](./hierarchy-demo.html) 用于评审任务中心中的紧凑统计区:上方显示当前会话的消息数与 Agent 累计回复时长,下方管理当前项目全部会话的任务及用时。切换会话只更新会话统计,任务范围固定为当前项目,没有全局或所有项目入口。支持状态筛选、定位所属会话,以及暂停、继续和取消任务的模拟操作。新建任务支持填写内容、关联本项目已有或新建会话、立即或定时执行;仅在页面内模拟,刷新后恢复样例。
|
||||
配套 `hierarchy-demo.css` 与 `hierarchy-demo.js` 仅服务于该静态原型;使用演示数据,未接入产品。
|
||||
项目任务标题右侧依次为 `+` 新建按钮与筛选图标,状态筛选默认收起;应用筛选后图标高亮并显示当前筛选条件。
|
||||
应用中的项目任务标题右侧仅保留 `+` 新建按钮;展开列表后直接显示状态选项,默认选中“进行中”,收起后保留所选状态。静态 Demo 的筛选入口仍为旧交互。
|
||||
时间为固定样例,Demo 从每次回复的起止秒数计算。正式功能按请求有效区间累计,包含轮内工具和审批等待、不计轮间空闲;任务用时按 `schedule_runs` 关联请求汇总,不按父任务生命周期计算。缺失历史有提示,远程缺少可靠证据时不推算用时,数据来源与边界以技术设计为准。
|
||||
|
||||
先阅读统一领域模型。其他功能文档不得重新定义 Task、Conversation、Job、Run 或 Subagent。
|
||||
|
||||
@@ -77,10 +77,10 @@ FR-TC3:任务累计执行时间按其历次执行请求汇总,周期触发
|
||||
## 5. 交互
|
||||
|
||||
- 点击条目打开关联 Conversation,并定位到该 Task。
|
||||
- FR-TC4:项目任务标题右侧依次放置“+”新建按钮和筛选图标。状态筛选默认收起、默认全部,
|
||||
项目任务列表默认收起,标题与数量、新建及筛选入口保持可见;点击标题切换展开状态,点击筛选同时展开列表。
|
||||
- FR-TC4:项目任务标题右侧放置“+”新建按钮,不再提供独立筛选图标。
|
||||
项目任务列表默认收起,标题与数量、新建入口保持可见;点击标题切换展开状态。
|
||||
标题旁同时显示当前项目顶层任务中处于 `running` 状态的数量(如“2 运行中”),不受筛选或收起影响;为零时省略。
|
||||
展开后提供全部、进行中、暂停、已结束;收起不清除已选条件,非全部时图标高亮并显示筛选条件。
|
||||
展开后直接显示全部、进行中、暂停、已结束状态选项,初始选中进行中;收起不清除已选条件。
|
||||
- 进行中包含 `idle`、`queued`、`running`、`waiting_approval`、`failed` 和 `interrupted`;
|
||||
暂停对应 `paused`,已结束包含 `completed` 和 `cancelled`。分类只影响列表筛选,
|
||||
不改变任务状态语义;任务行保留具体状态、错误信息和原有操作。
|
||||
@@ -158,7 +158,7 @@ FR-TC3:任务累计执行时间按其历次执行请求汇总,周期触发
|
||||
- [ ] FR-TC1:仅显示当前项目任务,切换会话不缩小列表;无项目时显示选择项目提示。
|
||||
- [ ] FR-TC2:消息总数与累计回复时间随当前会话更新;轮内工具和审批计时,轮间空闲不计时,历史缺失及远程证据不足有提示。
|
||||
- [ ] FR-TC3:多次执行汇总到同一任务,不按父 Task 生命周期计算用时。
|
||||
- [ ] FR-TC4:标题右侧先新建、后筛选;筛选默认收起且为全部,收起后保留所选条件。
|
||||
- [x] FR-TC4:标题右侧仅保留新建;展开列表直接显示状态选项,默认进行中,收起后保留所选条件。
|
||||
- [ ] FR-TC5:创建默认立即执行和当前会话,可选择本项目其他会话、新会话及定时周期,空名称从内容生成。
|
||||
- [x] 任务中心保持单实例、默认存在且不可关闭。
|
||||
|
||||
|
||||
@@ -363,6 +363,7 @@ describe('RightAssistantSidebar resizing', () => {
|
||||
}
|
||||
const view = render(sidebarElement(props))
|
||||
fireEvent.click(screen.getByRole('button', { name: '项目任务 2' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '全部' }))
|
||||
const stats = within(screen.getByRole('region', { name: '当前会话统计' }))
|
||||
expect(stats.getByText('01:01:01')).toBeVisible()
|
||||
expect(stats.getByText('24')).toBeVisible()
|
||||
@@ -385,29 +386,22 @@ describe('RightAssistantSidebar resizing', () => {
|
||||
for (const task of tasks) expect(screen.queryByText(task.title)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps an applied filter visible when collapsed and clears it without opening the filter panel', () => {
|
||||
it('shows the task status tabs when expanded and defaults to active tasks', () => {
|
||||
renderSidebar({ tasks: [{
|
||||
id: 'completed', projectId: currentProject.id, title: 'Completed task', instructions: '',
|
||||
origin: 'schedule', status: 'completed', createdAt: '2026-09-17T00:00:00Z'
|
||||
}] })
|
||||
const toggle = screen.getByRole('button', { name: '筛选任务' })
|
||||
expect(screen.getByText('Completed task')).toBeVisible()
|
||||
fireEvent.click(toggle)
|
||||
fireEvent.click(screen.getByRole('button', { name: '进行中' }))
|
||||
fireEvent.click(toggle)
|
||||
expect(toggle).toHaveAttribute('aria-expanded', 'false')
|
||||
expect(toggle).toHaveAttribute('data-active', 'true')
|
||||
expect(screen.queryByRole('group', { name: '筛选任务' })).not.toBeInTheDocument()
|
||||
expect(screen.getByText('筛选:进行中 · 0 项')).toBeVisible()
|
||||
expect(screen.getByText('当前筛选条件下没有任务。')).toBeVisible()
|
||||
fireEvent.click(screen.getByRole('button', { name: '清除筛选' }))
|
||||
expect(toggle).toHaveAttribute('data-active', 'false')
|
||||
expect(screen.queryByText('Completed task')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: '筛选任务' })).not.toBeInTheDocument()
|
||||
const filters = within(screen.getByRole('group', { name: '筛选任务' }))
|
||||
expect(filters.getByRole('button', { name: '进行中' })).toHaveAttribute('aria-pressed', 'true')
|
||||
expect(screen.queryByText('Completed task')).not.toBeInTheDocument()
|
||||
fireEvent.click(filters.getByRole('button', { name: '已结束' }))
|
||||
expect(screen.getByText('Completed task')).toBeVisible()
|
||||
})
|
||||
|
||||
it('scrolls the focused task filter into view when keyboard navigation reaches either end', () => {
|
||||
renderSidebar()
|
||||
fireEvent.click(screen.getByRole('button', { name: '筛选任务' }))
|
||||
const filters = within(screen.getByRole('group', { name: '筛选任务' }))
|
||||
const first = filters.getByRole('button', { name: '全部' })
|
||||
const last = filters.getByRole('button', { name: '已结束' })
|
||||
@@ -658,7 +652,7 @@ describe('RightAssistantSidebar resizing', () => {
|
||||
expect(list).toBeVisible()
|
||||
fireEvent.click(toggle)
|
||||
expect(list).not.toBeVisible()
|
||||
fireEvent.click(screen.getByRole('button', { name: '筛选任务' }))
|
||||
fireEvent.click(toggle)
|
||||
expect(toggle).toHaveAttribute('aria-expanded', 'true')
|
||||
expect(list).toBeVisible()
|
||||
})
|
||||
@@ -676,7 +670,7 @@ describe('RightAssistantSidebar resizing', () => {
|
||||
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(screen.getByRole('button', { name: '项目任务 4' }))
|
||||
fireEvent.click(within(screen.getByRole('group', { name: '筛选任务' })).getByRole('button', { name: '已结束' }))
|
||||
expect(screen.getByText('2 运行中')).toBeVisible()
|
||||
view.rerender(sidebarElement({ tab: 'tasks', tasks: [] }))
|
||||
@@ -701,10 +695,7 @@ describe('RightAssistantSidebar resizing', () => {
|
||||
)
|
||||
fireEvent.click(newTaskButton)
|
||||
expect(onCreateCustomTask).toHaveBeenCalledOnce()
|
||||
expect(
|
||||
screen.queryByRole('group', { name: '筛选任务' })
|
||||
).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: '筛选任务' })).toHaveAttribute('aria-expanded', 'false')
|
||||
expect(screen.getByRole('group', { name: '筛选任务' })).toBeVisible()
|
||||
expect(screen.queryByLabelText('定时任务标题')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('最近任务')).not.toBeInTheDocument()
|
||||
})
|
||||
@@ -736,7 +727,6 @@ describe('RightAssistantSidebar resizing', () => {
|
||||
approvals: [approval], onOpenTask, onRespondApproval
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '筛选任务' }))
|
||||
const filters = within(screen.getByRole('group', { name: '筛选任务' }))
|
||||
expect(filters.getAllByRole('button').map((button) => button.textContent))
|
||||
.toEqual(['全部', '进行中', '暂停', '已结束'])
|
||||
@@ -782,7 +772,6 @@ describe('RightAssistantSidebar resizing', () => {
|
||||
}
|
||||
const onRespondApproval = vi.fn()
|
||||
renderSidebar({ tasks: [task], approvals: [approval], onRespondApproval })
|
||||
fireEvent.click(screen.getByRole('button', { name: '筛选任务' }))
|
||||
const row = screen.getByText(task.title).closest('article')!
|
||||
expect(within(row).getByLabelText('等待审批: Confirm write')).toBeVisible()
|
||||
expect(screen.queryByRole('heading', { name: '等待审批' })).not.toBeInTheDocument()
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
Copy,
|
||||
ExternalLink,
|
||||
FileText,
|
||||
Filter,
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
Plus,
|
||||
@@ -48,7 +47,6 @@ import {
|
||||
isBrowserViewportOccluded
|
||||
} from './browser-viewport-occlusion'
|
||||
import {
|
||||
findTaskSchedule,
|
||||
TaskScheduleActions
|
||||
} from './TaskScheduleActions'
|
||||
import type {
|
||||
@@ -883,8 +881,7 @@ export function RightAssistantSidebar({
|
||||
useState('')
|
||||
const [taskFilter, setTaskFilter] = useState<
|
||||
'all' | 'active' | 'paused' | 'finished'
|
||||
>('all')
|
||||
const [taskFiltersExpanded, setTaskFiltersExpanded] = useState(false)
|
||||
>('active')
|
||||
const [taskListExpanded, setTaskListExpanded] = useState(false)
|
||||
const [actionErrorState, setActionErrorState] = useState({
|
||||
scope: { instanceId: activeWorkbarInstanceId, open },
|
||||
@@ -1027,6 +1024,14 @@ export function RightAssistantSidebar({
|
||||
}),
|
||||
[approvalsByTask, taskFilter, topLevelTasks]
|
||||
)
|
||||
const schedulesByTaskKey = useMemo(() => {
|
||||
const byKey = new Map<string, AssistantSchedule>()
|
||||
for (const schedule of schedules) {
|
||||
byKey.set(schedule.id, schedule)
|
||||
byKey.set(schedule.taskId, schedule)
|
||||
}
|
||||
return byKey
|
||||
}, [schedules])
|
||||
|
||||
useEffect(() => {
|
||||
// Context switches preserve the selected instance and its pinned binding.
|
||||
@@ -1981,21 +1986,8 @@ export function RightAssistantSidebar({
|
||||
>
|
||||
<Plus aria-hidden="true" size={13} />
|
||||
</button>
|
||||
<button
|
||||
className="secondary-button task-center__filter-toggle"
|
||||
aria-label={t('sidebar.tasks.filters.ariaLabel')}
|
||||
title={t('sidebar.tasks.filters.ariaLabel')}
|
||||
aria-expanded={taskListExpanded && taskFiltersExpanded}
|
||||
aria-controls={`task-filters-${instance.id}`}
|
||||
data-active={taskFilter !== 'all'}
|
||||
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={!taskListExpanded || !taskFiltersExpanded}
|
||||
<div className="task-center__filters" id={`task-filters-${instance.id}`} hidden={!taskListExpanded}
|
||||
onFocusCapture={(event) => event.target.scrollIntoView?.({ block: 'nearest', inline: 'nearest' })}>
|
||||
<SegmentedControl
|
||||
ariaLabel={t('sidebar.tasks.filters.ariaLabel')}
|
||||
@@ -2018,10 +2010,6 @@ export function RightAssistantSidebar({
|
||||
value={taskFilter}
|
||||
/>
|
||||
</div>
|
||||
{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>}
|
||||
{unassociatedApprovals.length > 0 && <>
|
||||
<h3><ShieldAlert size={15} />{t('sidebar.tasks.approvalsTitle')}</h3>
|
||||
{unassociatedApprovals.map((approval) => (
|
||||
@@ -2037,7 +2025,9 @@ export function RightAssistantSidebar({
|
||||
</p>
|
||||
) : (
|
||||
filteredTasks.map((task) => {
|
||||
const schedule = findTaskSchedule(task, schedules)
|
||||
const schedule =
|
||||
schedulesByTaskKey.get(task.scheduleId ?? '') ??
|
||||
schedulesByTaskKey.get(task.id)
|
||||
const conversationTitle = task.conversationId
|
||||
? conversationTitles.get(task.conversationId)
|
||||
: undefined
|
||||
|
||||
@@ -81,8 +81,13 @@
|
||||
.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 {
|
||||
/* Keep expanding a long history responsive by skipping off-screen cards. */
|
||||
.task-center__list > .task-center__item {
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: 0 132px;
|
||||
}
|
||||
|
||||
.task-center .task-center__create {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -92,13 +97,6 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.task-center .task-center__filter-toggle[data-active='true'],
|
||||
.task-center .task-center__filter-toggle[aria-expanded='true'] {
|
||||
border-color: var(--accent-selected);
|
||||
background: var(--accent-subtle);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.task-center .task-center__filters { flex: 0 0 auto; }
|
||||
.task-center .task-center__filters .segmented-control { overflow-x: auto; }
|
||||
.task-center .task-center__filters .segmented-control__option {
|
||||
@@ -107,26 +105,6 @@
|
||||
}
|
||||
.task-center .task-center__filters[hidden] { display: none; }
|
||||
|
||||
.task-center__filter-summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-2);
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-caption);
|
||||
}
|
||||
|
||||
.task-center__filter-summary button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--accent);
|
||||
padding: var(--space-2);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.task-center__filter-summary button:hover { background: var(--accent-subtle); }
|
||||
.task-center button:focus-visible,
|
||||
.task-center summary:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
|
||||
Reference in New Issue
Block a user