fix: simplify task center scopes and status filters
Task Center previously split unbound tasks into a global scope and tasks needing attention into a separate filter. Keep Current project and All projects, label unbound tasks, and restore legacy global scope preferences as all-projects without changing task data. Keep Active, Paused, and Finished filters. Active includes idle, queued, running, waiting_approval, failed, and interrupted tasks; task details and project-scoped approvals remain available. Release note: Task Center now offers Current project or All projects and groups tasks needing attention under Active, while retaining project labels and approval actions.
This commit is contained in:
+3
-2
@@ -98,8 +98,9 @@ records are listed separately and do not introduce another feature status.
|
||||
toggle sits beside the theme switch. The right workbar uses a persistent “+”
|
||||
capability catalog and application tabs.
|
||||
Task Center and Workspace are permanent singleton tabs; browser tabs are
|
||||
independent instances. Task Center can show the current project, global
|
||||
tasks, or all projects, and retains the selected scope with the layout.
|
||||
independent instances. Task Center can show the current project or all projects,
|
||||
and retains the selected scope with the layout. Tasks without a project appear
|
||||
under all projects with an “Unbound project” label.
|
||||
Users can open multiple independent terminals for the current local or
|
||||
managed SSH project with bounded output, resizing, termination, and explicit
|
||||
reconnection. Closing a terminal tab ends its Shell; restarting the app
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@
|
||||
- [x] **工作区文件与 Git 管理**:支持文件和目录的新建、重命名、移动、删除、属性查看,以及本地/远程分支搜索与切换、新建分支、显式 Fetch、提交历史和单文件提交差异。工具按文件/Git 视图分组,分支搜索使用统一紧凑表单,子目录保留文字导航而根目录不再显示孤立图标。提交历史贴底,首次展开占 Git 内容区一半,支持调整比例、独立滚动和统一刷新;切换项目自动检测 Git,不沿用其他项目状态。移动限于工作区内,删除须确认,分支冲突不自动 stash 或丢弃改动;托管 SSH 操作需要 Agent `0.11.23`。
|
||||
- [x] **富文本回答**:支持 GitHub Flavored Markdown、LaTeX 数学公式、受控 Mermaid 图表和会话内 HTML 静态预览。Agent 完成回复后,完整 HTML 与 HTML 代码块可在原位置预览,并通过图标按钮查看源码或打开全屏预览;该能力默认开启,可在“平台功能 / 通用设置”关闭,预览不执行脚本、联网、表单提交或窗口操作。
|
||||
- [x] **AI 回复与完整会话复制**:已完成的 AI 回复可从消息底部复制不含推理、工具日志和引用元数据的 Markdown 原文;完整会话复制复用同一条经过 Preload/Main 校验的剪贴板路径。
|
||||
- [x] **助手工作栏、多终端与可调布局**:工作栏开关位于明暗主题按钮旁;右侧工作栏使用持久“+”能力目录和应用 Tab,任务中心与工作区为不可关闭的单实例,浏览器可打开多个独立实例。任务中心可切换当前项目、全局任务和全部项目,范围随布局保存。用户可为当前本机或托管 SSH 项目打开多个独立终端,获得有界输出、调整尺寸、结束与显式重连。关闭终端 Tab 会结束 Shell,应用重启只恢复已结束的 Tab 描述,不自动重启 Shell。主侧栏和魔法笔记列表支持鼠标与键盘调宽,魔法笔记列表可隐藏并记住布局,窄宽度下自动堆叠。
|
||||
- [x] **助手工作栏、多终端与可调布局**:工作栏开关位于明暗主题按钮旁;右侧工作栏使用持久“+”能力目录和应用 Tab,任务中心与工作区为不可关闭的单实例,浏览器可打开多个独立实例。任务中心可切换当前项目和所有项目,范围随布局保存;无项目任务在所有项目中标注“未绑定项目”。用户可为当前本机或托管 SSH 项目打开多个独立终端,获得有界输出、调整尺寸、结束与显式重连。关闭终端 Tab 会结束 Shell,应用重启只恢复已结束的 Tab 描述,不自动重启 Shell。主侧栏和魔法笔记列表支持鼠标与键盘调宽,魔法笔记列表可隐藏并记住布局,窄宽度下自动堆叠。
|
||||
- [ ] **项目 Agent Space**(规划中):在 Project 中统一角色、知识、Skills/MCP、模型、审批策略、预算和超时,并支持模板复用。
|
||||
- [ ] **应用导航与系统工具快捷入口**(仅设计):规划固定系统入口、内置应用显隐和打开现有工作栏的底部工具按钮;尚未实现新界面。详见[设计文档](./docs/features/application-tool-navigation/README.md)。
|
||||
- [ ] **通用助手工作栏与执行空间后续能力**(规划中):在现有工作栏和多终端基础上继续加入监督、统一 Runtime、受管进程、静态安全 HTML 预览、可固定目标的工作区/浏览器/成果实例、底部停靠与独立窗口。Task Center 保持 Task 的单例索引,附件与知识库继续由会话输入区管理,未来的记忆与历史执行上下文归入关联 Task。详见 [Feature PRD](./docs/features/assistant-workbar/prd.md)。
|
||||
|
||||
+2
-2
@@ -338,7 +338,7 @@
|
||||
单实例重复选择时聚焦已有 Tab;浏览器和终端每次选择创建独立实例。主侧栏系统工具快捷
|
||||
入口按当前上下文聚焦最近使用的匹配实例,额外实例仍从“+”目录创建。
|
||||
- Task Center 是默认跟随当前项目的单实例索引,不使用多实例模式。用户可在同一 Tab 内切换
|
||||
“当前项目”“全局任务”和“全部项目”;范围变化不得创建第二个 Task Center。每个 Task 只
|
||||
“当前项目”和“所有项目”;无项目任务在“所有项目”中标注“未绑定项目”。范围变化不得创建第二个 Task Center。每个 Task 只
|
||||
关联一条 Conversation,一条 Conversation 可以关联多个 Task;列表不得复制会话内容,也
|
||||
不得把 Job/Run 提升为可导航 UI 对象。
|
||||
- 各栏目读取当前会话或项目的对应内容;当前内容不可用时仍保留栏目入口并说明原因。
|
||||
@@ -687,7 +687,7 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
- 每项显示名称、关联 Conversation、Global 或 Project 范围、Ask/Execute、状态、最近进展、
|
||||
最近真实活动时间及需要关注信息。
|
||||
- 点击列表项打开关联 Conversation 并定位该 Task,不显示第二份内容载体。
|
||||
- 需要关注、进行中、已暂停和已结束使用共享 `SegmentedControl`;窄栏不足时单行滚动。
|
||||
- 进行中、暂停和已结束使用共享 `SegmentedControl`;窄栏不足时单行滚动。状态归类遵循[任务中心 PRD](docs/features/task-and-job/task-center-prd.md#5-交互)。
|
||||
- 完整消息留在 Conversation;长错误、工具、审批和成果按 Task 关联到 Runtime、活动记录和
|
||||
成果查看器,不撑高列表,也不显示 Job/Run 树。
|
||||
- “新建定制任务”使用共享 Modal,明确选择当前或新 Conversation。聊天入口默认当前
|
||||
|
||||
@@ -425,9 +425,8 @@ Renderer 只通过显式 IPC 列出、加入、删除、提升、释放和接收
|
||||
|
||||
```text
|
||||
任务中心
|
||||
├─ 需要关注
|
||||
├─ 进行中
|
||||
├─ 已暂停
|
||||
├─ 暂停
|
||||
└─ 已结束
|
||||
└─ 打开任务自身
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ Runtime
|
||||
### 3.2 当前上下文只提供默认值
|
||||
|
||||
任务中心只有一个工作栏实例,默认跟随当前项目,不跟随当前 Conversation。用户可以在同一
|
||||
实例内显式切换到全局任务或全部项目,但不能为不同项目打开多个任务中心 Tab。其他可绑定
|
||||
实例内显式切换到所有项目,但不能为不同项目打开多个任务中心 Tab。其他可绑定
|
||||
目标的能力使用当前会话、项目、Runtime 和主机帮助新面板实例初始定位,但这些上下文不是
|
||||
使用门槛:
|
||||
|
||||
@@ -260,7 +260,7 @@ Runtime
|
||||
Task Center 保留为工作栏中的稳定入口,并在现有基础上适度完善:
|
||||
|
||||
- 只索引 Task;每个 Task 只关联一条 Conversation,一条 Conversation 可以关联多个 Task。
|
||||
- 显示名称、关联 Conversation、Global 或 Project 范围、Ask/Execute、状态、最近进展、
|
||||
- 显示名称、关联 Conversation、所属项目(无项目时标注“未绑定项目”)、Ask/Execute、状态、最近进展、
|
||||
真实活动时间和需要关注信息。
|
||||
- 点击 Task 打开其关联 Conversation 并定位 Task。
|
||||
- 完整消息留在 Conversation;工具、Subagent、审批、错误和成果按 Task 关联到 Runtime、
|
||||
@@ -268,11 +268,11 @@ Task Center 保留为工作栏中的稳定入口,并在现有基础上适度
|
||||
- 智能心跳的报告、建议、历史和配置不进入任务中心。
|
||||
|
||||
任务中心是应用级单例,默认存在且不可关闭。它的数据范围默认跟随当前项目,并在标题或范围
|
||||
控件中明确显示项目;用户可以切换到“全局任务”或“全部项目”,该选择只改变同一实例的列表
|
||||
控件中明确显示项目;用户可以切换到“所有项目”,该选择只改变同一实例的列表
|
||||
范围,不创建第二个任务中心 Tab。切换回“跟随当前项目”后,项目变化立即刷新列表。后台状态
|
||||
可以更新徽标和排序,但不能抢占用户当前工作。
|
||||
|
||||
当前实现已按活动项目过滤 Scheduled Task 和审批,并提供“当前项目”“全局任务”“全部项目”
|
||||
当前实现已按活动项目过滤 Scheduled Task 和审批,并提供“当前项目”和“所有项目”
|
||||
范围控件。
|
||||
|
||||
主侧栏最近会话为关联 Task 提供轻量入口:父会话行只显示行首展开按钮,Task 子项使用
|
||||
|
||||
@@ -1115,13 +1115,24 @@ src/agent-daemon/runtime-composition.test.ts src/main/ipc.test.ts` 为 179 项
|
||||
测试应用与具名浏览器会话已退出,所属 Electron/Node 进程及 fixture 监听端口均为 0;
|
||||
临时截图和隔离 profile 保留在本机测试目录,不进入仓库。
|
||||
|
||||
## 2026-09-16 任务中心范围简化
|
||||
|
||||
- 任务中心仅保留“当前项目”和“所有项目”;无项目任务在后者中标注“未绑定项目”。具体规则见
|
||||
[Task Center 范围](../task-and-job/task-center-prd.md#4-范围)。
|
||||
- Renderer 将旧布局中的 `global` 恢复为 `all-projects` 并保存新值;共享解析契约、后端与任务
|
||||
数据不变。App 仍只向任务中心传入 schedule 来源的顶层任务。
|
||||
- `npx vitest run src/renderer/src/RightAssistantSidebar.resize.test.tsx src/shared/workbar-contracts.test.ts`
|
||||
通过 60 项;`npm run typecheck` 和 `npm run lint` 通过。未进行真实 Electron 窗口验收。
|
||||
- `npm test` 在 200 秒后超时,未取得整库结果;单独复跑 `heartbeat-database.test.ts` 为 6 项通过、
|
||||
2 项失败,均在未修改的数据库迁移代码中报 `duplicate column name: pinned`。
|
||||
|
||||
## 2026-09-10 固定基础页签与浏览器多实例
|
||||
|
||||
- 工作栏注册表已分别声明实例策略、默认上下文、默认打开、必须存在、可关闭和排序属性。
|
||||
任务中心与工作区保持默认存在且不可关闭;浏览器改为当前 Conversation 上下文的多实例。
|
||||
布局加载按 `required` 补回缺失的基础页签,并修复失效活动实例。
|
||||
- 任务中心增加“当前项目”“全局任务”“全部项目”范围,默认跟随活动项目;任务和审批按
|
||||
Conversation 所属项目过滤,范围模式随工作栏布局持久化。
|
||||
Conversation 所属项目过滤,范围模式随工作栏布局持久化。范围选项已于 2026-09-16 简化,见上方记录。
|
||||
- 浏览器服务已拆分 Conversation Context 与 Browser Tab。每个 Tab 拥有独立页面、Driver、
|
||||
操作队列、状态和元素引用空间,同一 Conversation 的 Tab 共享 partition、代理和 Cookie。
|
||||
- Renderer 使用工作栏实例 UUID 原子创建或恢复 Browser Tab,并以 UUID viewport token 防止
|
||||
|
||||
@@ -368,6 +368,8 @@ type WorkbarLayoutPreferences = {
|
||||
```
|
||||
|
||||
- 持久化工作栏打开状态、Tab 顺序、活动实例、应用 ID、自定义标题、公开目标引用和宽度比例。
|
||||
- `taskScope` 中的 `global` 仅保留旧布局解析兼容;UI 恢复时映射为 `all-projects`,此后只保存
|
||||
`current-project` 或 `all-projects`。范围规则见 [Task Center PRD](../task-and-job/task-center-prd.md#4-范围)。
|
||||
- 加载布局时根据 `required` 补回缺失实例,并拒绝重复的单实例应用;应用策略不写入实例
|
||||
数据,避免持久化状态覆盖注册表。
|
||||
- `reorderable` 已进入共享契约;当前 Tab 壳层尚未提供拖放或键盘重排,不能把声明属性视为
|
||||
|
||||
@@ -19,7 +19,7 @@ Task Center 是工作栏中的单实例 Task 索引,默认跟随当前项目
|
||||
Conversation 不因为关联 Task 而改变对象类型。
|
||||
|
||||
“单实例”和“项目范围”是两个独立属性:应用内只有一个 Task Center Tab,但该 Tab 默认展示
|
||||
当前项目的 Task。用户可以在同一 Tab 内切换到“全局任务”或“全部项目”,不能为每个项目创建
|
||||
当前项目的 Task。用户可以在同一 Tab 内切换到“所有项目”,不能为每个项目创建
|
||||
独立 Task Center 实例。
|
||||
|
||||
## 2. 收录边界
|
||||
@@ -45,7 +45,7 @@ Job/Subjob/Run 树、独立详情或路由。
|
||||
|
||||
每条 Task 至少显示:
|
||||
|
||||
- 名称和 Global / Project 范围。
|
||||
- 名称和所属项目;无项目时标注“未绑定项目”。
|
||||
- 关联 Conversation 标题。
|
||||
- Task 类型和触发来源。
|
||||
- Ask / Execute 模式。
|
||||
@@ -62,17 +62,21 @@ Task 行只显示聚合后的用户状态,不要求用户理解内部 Job/Run
|
||||
范围控件提供:
|
||||
|
||||
- `当前项目`:默认值,跟随活动项目变化,只显示 `projectId` 匹配的 Task。
|
||||
- `全局任务`:只显示没有 `projectId` 的 Task。
|
||||
- `全部项目`:显示当前用户可见的项目 Task 和全局 Task,并保留每行的项目或 Global 标识。
|
||||
- `所有项目`:显示当前用户可见的项目 Task 和没有 `projectId` 的历史 Task,后者标注“未绑定项目”。
|
||||
|
||||
没有活动项目时,“当前项目”显示明确空状态,并允许切换到“全局任务”或“全部项目”;系统不
|
||||
没有活动项目时,“当前项目”显示明确空状态,并允许切换到“所有项目”;系统不
|
||||
自动改变用户已经选择的范围。范围模式作为工作栏公开偏好持久化,但“当前项目”模式不保存
|
||||
具体项目 ID;重启后继续跟随届时的活动项目。
|
||||
具体项目 ID;重启后继续跟随届时的活动项目。旧布局保存的 `global` 范围在 UI 恢复时映射为
|
||||
`all-projects`,后续保存使用新值;不修改任务数据或后端项目绑定规则。
|
||||
|
||||
## 5. 交互
|
||||
|
||||
- 点击条目打开关联 Conversation,并定位到该 Task。
|
||||
- 支持按需要关注、进行中、已暂停、已结束筛选。
|
||||
- 状态筛选仅保留进行中、暂停、已结束,默认选择进行中。
|
||||
- 进行中包含 `idle`、`queued`、`running`、`waiting_approval`、`failed` 和 `interrupted`;
|
||||
暂停对应 `paused`,已结束包含 `completed` 和 `cancelled`。分类只影响列表筛选,
|
||||
不改变任务状态语义;任务行保留具体状态、错误信息和原有操作。
|
||||
- 等待审批区域和审批徽标按项目范围展示,不受状态筛选影响,保留拒绝和允许一次操作。
|
||||
- 支持立即运行、暂停、恢复、取消、编辑和删除。
|
||||
- 后台变化更新状态和徽标,不自动抢占当前页面。
|
||||
- 立即运行、重试和恢复在 UI 上都是 Task 操作,内部 Job/Run 不单独显示。
|
||||
@@ -138,5 +142,5 @@ Task 行只显示聚合后的用户状态,不要求用户理解内部 Job/Run
|
||||
- [ ] 删除 Task 默认保留 Conversation 和既有消息。
|
||||
- [x] Smart Heartbeat 不进入 Task Center。
|
||||
- [x] Task Center 默认只显示当前项目的 Task,切换项目后在同一 Tab 更新列表。
|
||||
- [x] “全局任务”只显示无项目 Task,“全部项目”显示所有可见 Task 并标注所属范围。
|
||||
- [x] 仅提供“当前项目”和“所有项目”;后者保留无项目 Task,并标注“未绑定项目”。
|
||||
- [x] 范围切换不创建第二个 Task Center Tab;任务中心保持默认存在且不可关闭。
|
||||
|
||||
@@ -156,6 +156,8 @@ function sidebarElement({
|
||||
onNavigateBrowser,
|
||||
onReloadBrowser,
|
||||
onStopLoadingBrowser,
|
||||
onOpenTask = vi.fn(),
|
||||
onRespondApproval = vi.fn(),
|
||||
onTabChange = vi.fn()
|
||||
}: {
|
||||
open?: boolean
|
||||
@@ -188,6 +190,8 @@ function sidebarElement({
|
||||
onNavigateBrowser?: (conversationId: string, tabId: BrowserTabId, url: string) => Promise<void>
|
||||
onReloadBrowser?: (conversationId: string, tabId: BrowserTabId) => Promise<void>
|
||||
onStopLoadingBrowser?: (conversationId: string, tabId: BrowserTabId) => Promise<void>
|
||||
onOpenTask?: (task: AssistantTask) => void
|
||||
onRespondApproval?: React.ComponentProps<typeof RightAssistantSidebar>['onRespondApproval']
|
||||
onTabChange?: (tab: AssistantSidebarTab) => void
|
||||
} = {}): React.JSX.Element {
|
||||
return (
|
||||
@@ -215,11 +219,11 @@ function sidebarElement({
|
||||
onRefreshChanges={vi.fn(async () => undefined)}
|
||||
onReloadBrowser={onReloadBrowser}
|
||||
onRemoveSchedule={vi.fn(async () => undefined)}
|
||||
onRespondApproval={vi.fn()}
|
||||
onRespondApproval={onRespondApproval}
|
||||
onRunSchedule={vi.fn(async () => undefined)}
|
||||
onSetScheduleEnabled={vi.fn(async () => undefined)}
|
||||
onStopLoadingBrowser={onStopLoadingBrowser}
|
||||
onOpenTask={vi.fn()}
|
||||
onOpenTask={onOpenTask}
|
||||
onTabChange={onTabChange}
|
||||
open={open}
|
||||
restoreFocusRef={restoreFocusRef}
|
||||
@@ -568,6 +572,65 @@ describe('RightAssistantSidebar resizing', () => {
|
||||
expect(screen.queryByText('最近任务')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('groups attention states as active while preserving task details and approvals across filters', () => {
|
||||
const statuses: AssistantTask['status'][] = [
|
||||
'idle', 'queued', 'running', 'waiting_approval', 'failed', 'interrupted',
|
||||
'paused', 'completed', 'cancelled'
|
||||
]
|
||||
const tasks = statuses.map((status): AssistantTask => ({
|
||||
id: status,
|
||||
title: `Task ${status}`,
|
||||
instructions: status,
|
||||
projectId: currentProject.id,
|
||||
origin: 'schedule',
|
||||
status,
|
||||
error: status === 'failed' ? 'Task execution failed' : undefined,
|
||||
createdAt: '2026-09-09T00:00:00.000Z'
|
||||
}))
|
||||
const approval: PendingSidebarApproval = {
|
||||
conversationId: 'conversation-1', messageId: 'message-1',
|
||||
approvalId: 'approval-1', projectId: currentProject.id,
|
||||
title: '写入工作区', description: '更新 release.md'
|
||||
}
|
||||
const onOpenTask = vi.fn()
|
||||
const onRespondApproval = vi.fn()
|
||||
renderSidebar({
|
||||
tasks: [...tasks, { ...tasks[4]!, id: 'child', title: 'Child task', parentTaskId: 'running' }],
|
||||
approvals: [approval], onOpenTask, onRespondApproval
|
||||
})
|
||||
|
||||
const filters = within(screen.getByRole('group', { name: '筛选任务' }))
|
||||
expect(filters.getAllByRole('button').map((button) => button.textContent))
|
||||
.toEqual(['进行中', '暂停', '已结束'])
|
||||
expect(filters.getByRole('button', { name: '进行中' })).toHaveAttribute('aria-pressed', 'true')
|
||||
for (const task of tasks.slice(0, 6)) {
|
||||
const row = screen.getByText(task.title).closest('article')!
|
||||
expect(within(row).getByText(i18n.t(`task.status.${task.status}`, { ns: 'workspace' }))).toBeVisible()
|
||||
fireEvent.click(within(row).getByRole('button'))
|
||||
expect(onOpenTask).toHaveBeenLastCalledWith(task)
|
||||
}
|
||||
expect(screen.getByText('Task execution failed')).toBeVisible()
|
||||
expect(screen.queryByText('Child task')).not.toBeInTheDocument()
|
||||
|
||||
for (const [filter, visibleStatuses] of [
|
||||
['进行中', statuses.slice(0, 6)],
|
||||
['暂停', ['paused']],
|
||||
['已结束', ['completed', 'cancelled']]
|
||||
] as const) {
|
||||
fireEvent.click(filters.getByRole('button', { name: filter }))
|
||||
for (const task of tasks) {
|
||||
expect(Boolean(screen.queryByText(task.title)))
|
||||
.toBe((visibleStatuses as readonly string[]).includes(task.status))
|
||||
}
|
||||
expect(screen.getByLabelText('等待审批: 1')).toBeVisible()
|
||||
expect(screen.getByLabelText('等待审批: 写入工作区')).toBeVisible()
|
||||
}
|
||||
fireEvent.click(screen.getByRole('button', { name: '仅此次允许' }))
|
||||
expect(onRespondApproval).toHaveBeenLastCalledWith(approval, 'once')
|
||||
fireEvent.click(screen.getByRole('button', { name: '拒绝' }))
|
||||
expect(onRespondApproval).toHaveBeenLastCalledWith(approval, 'deny')
|
||||
})
|
||||
|
||||
it('labels approval counts and cards without duplicate live regions', () => {
|
||||
renderSidebar({
|
||||
approvals: [
|
||||
@@ -722,15 +785,15 @@ describe('RightAssistantSidebar resizing', () => {
|
||||
fireEvent.click(
|
||||
within(screen.getByRole('group', { name: '任务范围' })).getByRole(
|
||||
'button',
|
||||
{ name: '全局' }
|
||||
{ name: '所有项目' }
|
||||
)
|
||||
)
|
||||
expect(screen.getByText('全局审批')).toBeInTheDocument()
|
||||
expect(screen.queryByText('当前项目审批')).not.toBeInTheDocument()
|
||||
expect(screen.getByLabelText('等待审批: 1')).toBeInTheDocument()
|
||||
expect(screen.getByText('当前项目审批')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('等待审批: 2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('scopes tasks to the current project, global tasks, or all projects and persists the choice', () => {
|
||||
it('scopes tasks to the current or all projects, labels unbound tasks and persists the choice', () => {
|
||||
const createTask = (
|
||||
id: string,
|
||||
title: string,
|
||||
@@ -758,15 +821,10 @@ describe('RightAssistantSidebar resizing', () => {
|
||||
|
||||
expect(screen.getByText('当前项目任务')).toBeInTheDocument()
|
||||
expect(screen.queryByText('全局任务')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(
|
||||
within(screen.getByRole('group', { name: '任务范围' })).getByRole(
|
||||
'button',
|
||||
{ name: '全局' }
|
||||
)
|
||||
)
|
||||
expect(screen.getByText('全局任务')).toBeInTheDocument()
|
||||
expect(screen.queryByText('当前项目任务')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('其他项目任务')).not.toBeInTheDocument()
|
||||
expect(within(screen.getByRole('group', { name: '任务范围' })).getAllByRole('button'))
|
||||
.toHaveLength(2)
|
||||
expect(screen.queryByRole('button', { name: '全局' })).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(
|
||||
within(screen.getByRole('group', { name: '任务范围' })).getByRole(
|
||||
@@ -777,10 +835,33 @@ describe('RightAssistantSidebar resizing', () => {
|
||||
expect(screen.getByText('当前项目任务')).toBeInTheDocument()
|
||||
expect(screen.getByText('全局任务')).toBeInTheDocument()
|
||||
expect(screen.getByText('其他项目任务')).toBeInTheDocument()
|
||||
expect(screen.getAllByText('未绑定项目')).toHaveLength(1)
|
||||
expect(screen.getByText('项目:00000000-0000-4000-8000-000000000302')).toBeInTheDocument()
|
||||
expect(JSON.parse(localStorage.getItem('goodbuddy.workbar-layout.v1')!))
|
||||
.toEqual(expect.objectContaining({ taskScope: 'all-projects' }))
|
||||
})
|
||||
|
||||
it('restores the legacy global scope as all projects and saves the updated preference', () => {
|
||||
localStorage.setItem('goodbuddy.workbar-layout.v1', JSON.stringify({
|
||||
instances: DEFAULT_WORKBAR_INSTANCES,
|
||||
activeInstanceId: DEFAULT_WORKBAR_INSTANCES[0]!.id,
|
||||
expanded: true,
|
||||
dock: 'right',
|
||||
widthRatio: 0.3,
|
||||
taskScope: 'global'
|
||||
}))
|
||||
renderSidebar({ activeProject: null, tasks: [{
|
||||
id: 'legacy-unbound-task', title: '历史任务', instructions: '历史任务',
|
||||
origin: 'schedule', status: 'running', createdAt: '2026-09-09T00:00:00.000Z'
|
||||
}] })
|
||||
|
||||
expect(screen.getByRole('button', { name: '所有项目' })).toHaveAttribute('aria-pressed', 'true')
|
||||
expect(screen.getByText('历史任务')).toBeInTheDocument()
|
||||
expect(screen.getByText('未绑定项目')).toBeInTheDocument()
|
||||
expect(JSON.parse(localStorage.getItem('goodbuddy.workbar-layout.v1')!).taskScope)
|
||||
.toBe('all-projects')
|
||||
})
|
||||
|
||||
it('shows a clear current-project empty state when no project is active', () => {
|
||||
renderSidebar({ activeProject: null })
|
||||
|
||||
|
||||
@@ -831,10 +831,12 @@ export function RightAssistantSidebar({
|
||||
const [workspacePreviewLoadMoreError, setWorkspacePreviewLoadMoreError] =
|
||||
useState('')
|
||||
const [taskFilter, setTaskFilter] = useState<
|
||||
'attention' | 'active' | 'paused' | 'finished'
|
||||
'active' | 'paused' | 'finished'
|
||||
>('active')
|
||||
const [taskScope, setTaskScope] = useState<WorkbarTaskScope>(
|
||||
initialLayout?.taskScope ?? 'current-project'
|
||||
const [taskScope, setTaskScope] = useState<Exclude<WorkbarTaskScope, 'global'>>(
|
||||
initialLayout?.taskScope === 'global'
|
||||
? 'all-projects'
|
||||
: initialLayout?.taskScope ?? 'current-project'
|
||||
)
|
||||
const [actionErrorState, setActionErrorState] = useState({
|
||||
scope: { instanceId: activeWorkbarInstanceId, open },
|
||||
@@ -925,9 +927,6 @@ export function RightAssistantSidebar({
|
||||
if (taskScope === 'all-projects') {
|
||||
return true
|
||||
}
|
||||
if (taskScope === 'global') {
|
||||
return !projectId
|
||||
}
|
||||
return Boolean(currentProject && projectId === currentProject.id)
|
||||
},
|
||||
[currentProject, taskScope]
|
||||
@@ -946,18 +945,14 @@ export function RightAssistantSidebar({
|
||||
const filteredTasks = useMemo(
|
||||
() =>
|
||||
topLevelTasks.filter((task) => {
|
||||
if (taskFilter === 'attention') {
|
||||
return (
|
||||
task.status === 'waiting_approval' ||
|
||||
task.status === 'failed' ||
|
||||
task.status === 'interrupted'
|
||||
)
|
||||
}
|
||||
if (taskFilter === 'active') {
|
||||
return (
|
||||
task.status === 'idle' ||
|
||||
task.status === 'queued' ||
|
||||
task.status === 'running'
|
||||
task.status === 'running' ||
|
||||
task.status === 'waiting_approval' ||
|
||||
task.status === 'failed' ||
|
||||
task.status === 'interrupted'
|
||||
)
|
||||
}
|
||||
if (taskFilter === 'paused') {
|
||||
@@ -1885,10 +1880,6 @@ export function RightAssistantSidebar({
|
||||
value: 'current-project',
|
||||
label: t('sidebar.tasks.scope.currentProject')
|
||||
},
|
||||
{
|
||||
value: 'global',
|
||||
label: t('sidebar.tasks.scope.global')
|
||||
},
|
||||
{
|
||||
value: 'all-projects',
|
||||
label: t('sidebar.tasks.scope.allProjects')
|
||||
@@ -1966,10 +1957,6 @@ export function RightAssistantSidebar({
|
||||
ariaLabel={t('sidebar.tasks.filters.ariaLabel')}
|
||||
onChange={setTaskFilter}
|
||||
options={[
|
||||
{
|
||||
value: 'attention',
|
||||
label: t('sidebar.tasks.filters.attention')
|
||||
},
|
||||
{
|
||||
value: 'active',
|
||||
label: t('sidebar.tasks.filters.active')
|
||||
@@ -2034,11 +2021,11 @@ export function RightAssistantSidebar({
|
||||
</button>
|
||||
<div className="task-center__metadata">
|
||||
<span>
|
||||
{projectName
|
||||
{task.projectId
|
||||
? t('sidebar.tasks.projectScope', {
|
||||
project: projectName
|
||||
project: projectName ?? task.projectId
|
||||
})
|
||||
: t('sidebar.tasks.globalScope')}
|
||||
: t('sidebar.tasks.unboundProject')}
|
||||
</span>
|
||||
<span>
|
||||
{schedule
|
||||
|
||||
@@ -327,21 +327,19 @@ export const workspace = {
|
||||
noFilterResults: 'No tasks match this filter.',
|
||||
conversationUnavailable: 'Conversation unavailable',
|
||||
projectScope: 'Project: {{project}}',
|
||||
globalScope: 'Global',
|
||||
unboundProject: 'Unbound project',
|
||||
startedAt: 'Started {{time}}',
|
||||
nextRunAt: 'Next run: {{time}}',
|
||||
notStarted: 'Not run yet',
|
||||
scope: {
|
||||
ariaLabel: 'Task scope',
|
||||
currentProject: 'Current project',
|
||||
global: 'Global',
|
||||
allProjects: 'All projects',
|
||||
noCurrentProject:
|
||||
'No project is active. Choose another task scope or open a project.'
|
||||
},
|
||||
filters: {
|
||||
ariaLabel: 'Filter tasks',
|
||||
attention: 'Attention',
|
||||
active: 'Active',
|
||||
paused: 'Paused',
|
||||
finished: 'Finished'
|
||||
|
||||
@@ -319,23 +319,21 @@ export const workspace = {
|
||||
noFilterResults: '当前筛选条件下没有任务。',
|
||||
conversationUnavailable: '关联会话不可用',
|
||||
projectScope: '项目:{{project}}',
|
||||
globalScope: '全局',
|
||||
unboundProject: '未绑定项目',
|
||||
startedAt: '{{time}} 开始',
|
||||
nextRunAt: '下次运行:{{time}}',
|
||||
notStarted: '尚未运行',
|
||||
scope: {
|
||||
ariaLabel: '任务范围',
|
||||
currentProject: '当前项目',
|
||||
global: '全局',
|
||||
allProjects: '所有项目',
|
||||
noCurrentProject: '没有活动项目。请选择其他任务范围或先打开一个项目。'
|
||||
},
|
||||
filters: {
|
||||
ariaLabel: '筛选任务',
|
||||
attention: '待关注',
|
||||
active: '进行中',
|
||||
paused: '已暂停',
|
||||
finished: '已完成'
|
||||
paused: '暂停',
|
||||
finished: '已结束'
|
||||
},
|
||||
schedule: {
|
||||
recurrence: {
|
||||
|
||||
Reference in New Issue
Block a user