fix: show task approvals in their task cards
Task Center previously separated task approvals from their tasks and showed an empty approval section. Associated approvals now appear with allow-once and deny actions inside task cards, and those tasks appear under Active. Only unassociated approvals retain a separate section; no approval section or badge is shown when there are no pending requests. Release note: Review and handle pending approvals directly in their Task Center cards. Tasks awaiting approval appear under Active, while requests without a matching task remain separately accessible.
This commit is contained in:
@@ -1063,7 +1063,8 @@ todo/plan,本次不为其增加工具。也不增加 plan 工作模式、计
|
|||||||
- 建立实例级跟随、固定和失效目标语义。
|
- 建立实例级跟随、固定和失效目标语义。
|
||||||
- 保留现有任务中心、工作区、浏览器和成果行为;附件与知识库继续由 Composer 管理。
|
- 保留现有任务中心、工作区、浏览器和成果行为;附件与知识库继续由 Composer 管理。
|
||||||
- 将 Task Center 明确为 Task 的单例索引,并补齐范围、状态、最近进展、需要关注和直接打开 Conversation。
|
- 将 Task Center 明确为 Task 的单例索引,并补齐范围、状态、最近进展、需要关注和直接打开 Conversation。
|
||||||
- 审批在所属任务或 Runtime 中持续可见,不新增独立审批面板。
|
- 审批在所属任务或 Runtime 中持续可见;任务中心只为未关联任务的审批保留按需显示的处理
|
||||||
|
区域,具体规则见 [Task Center 交互](../task-and-job/task-center-prd.md#5-交互)。
|
||||||
- 智能心跳菜单入口承接完整配置和范围后,再从任务中心移除重复表单;不得移除任务中心本身。
|
- 智能心跳菜单入口承接完整配置和范围后,再从任务中心移除重复表单;不得移除任务中心本身。
|
||||||
|
|
||||||
### 阶段 1:成果与 Runtime 可观测性
|
### 阶段 1:成果与 Runtime 可观测性
|
||||||
|
|||||||
@@ -1115,6 +1115,20 @@ src/agent-daemon/runtime-composition.test.ts src/main/ipc.test.ts` 为 179 项
|
|||||||
测试应用与具名浏览器会话已退出,所属 Electron/Node 进程及 fixture 监听端口均为 0;
|
测试应用与具名浏览器会话已退出,所属 Electron/Node 进程及 fixture 监听端口均为 0;
|
||||||
临时截图和隔离 profile 保留在本机测试目录,不进入仓库。
|
临时截图和隔离 profile 保留在本机测试目录,不进入仓库。
|
||||||
|
|
||||||
|
## 2026-09-16 任务卡内审批
|
||||||
|
|
||||||
|
- 任务中心按 [Task Center 交互规则](../task-and-job/task-center-prd.md#5-交互) 展示卡内审批,
|
||||||
|
未关联审批保留处理入口;没有审批时不渲染空审批区域。
|
||||||
|
- 当前定时运行从队列 `taskId` 记录到 `ActiveRun`,审批事件再写入消息的临时审批数据;
|
||||||
|
历史消息使用明确的 `message.task.id`。Sidebar 按任务 ID 匹配当前范围内顶层任务,
|
||||||
|
并核对会话 ID 和项目 ID,不按会话、标题、时间或计划猜测运行归属。
|
||||||
|
- `RightAssistantSidebar.resize.test.tsx` 的 54 项测试通过,覆盖项目范围、状态筛选、
|
||||||
|
卡内处理、无审批、普通会话审批及关联不一致。App 使用 `-t "approval|scheduled instructions"`
|
||||||
|
定向运行的 14 项测试通过,验证历史与当前定时审批的映射、允许/拒绝接口调用及成功移除。
|
||||||
|
- `npm run typecheck`、`npm run lint` 通过。本次仅修改 Renderer 展示与临时关联数据,
|
||||||
|
本机和远程审批继续使用原有接口;未改 Main、Agent 或存储协议。未进行真实 Electron
|
||||||
|
窗口和模型调用验收;按本次要求未运行全量测试及无关数据库迁移测试。
|
||||||
|
|
||||||
## 2026-09-16 任务中心范围简化
|
## 2026-09-16 任务中心范围简化
|
||||||
|
|
||||||
- 任务中心仅保留“当前项目”和“所有项目”;无项目任务在后者中标注“未绑定项目”。具体规则见
|
- 任务中心仅保留“当前项目”和“所有项目”;无项目任务在后者中标注“未绑定项目”。具体规则见
|
||||||
|
|||||||
@@ -76,7 +76,11 @@ Task 行只显示聚合后的用户状态,不要求用户理解内部 Job/Run
|
|||||||
- 进行中包含 `idle`、`queued`、`running`、`waiting_approval`、`failed` 和 `interrupted`;
|
- 进行中包含 `idle`、`queued`、`running`、`waiting_approval`、`failed` 和 `interrupted`;
|
||||||
暂停对应 `paused`,已结束包含 `completed` 和 `cancelled`。分类只影响列表筛选,
|
暂停对应 `paused`,已结束包含 `completed` 和 `cancelled`。分类只影响列表筛选,
|
||||||
不改变任务状态语义;任务行保留具体状态、错误信息和原有操作。
|
不改变任务状态语义;任务行保留具体状态、错误信息和原有操作。
|
||||||
- 等待审批区域和审批徽标按项目范围展示,不受状态筛选影响,保留拒绝和允许一次操作。
|
- 关联当前范围内顶层任务的审批直接显示在任务卡内,可拒绝或仅此次允许;有待审批的任务
|
||||||
|
归入“进行中”,审批随任务卡参与状态筛选,不再重复显示独立审批分类或区域。
|
||||||
|
- 普通会话或无法关联到当前范围内任务的审批保留独立处理区域,不受任务状态筛选影响;
|
||||||
|
该区域仅在存在此类审批时显示。没有审批时不显示审批标题、空提示或徽标。
|
||||||
|
- 审批徽标统计当前项目范围内全部待审批请求,不受状态筛选影响。
|
||||||
- 支持立即运行、暂停、恢复、取消、编辑和删除。
|
- 支持立即运行、暂停、恢复、取消、编辑和删除。
|
||||||
- 后台变化更新状态和徽标,不自动抢占当前页面。
|
- 后台变化更新状态和徽标,不自动抢占当前页面。
|
||||||
- 立即运行、重试和恢复在 UI 上都是 Task 操作,内部 Job/Run 不单独显示。
|
- 立即运行、重试和恢复在 UI 上都是 Task 操作,内部 Job/Run 不单独显示。
|
||||||
|
|||||||
@@ -2540,7 +2540,14 @@ describe("App", () => {
|
|||||||
expect(screen.getByText("仅此次")).toBeInTheDocument();
|
expect(screen.getByText("仅此次")).toBeInTheDocument();
|
||||||
expect(screen.getByLabelText("任务结果:发布任务")).toBeInTheDocument();
|
expect(screen.getByLabelText("任务结果:发布任务")).toBeInTheDocument();
|
||||||
expect(screen.getByRole("button", { name: /全项目活动/u })).toHaveTextContent("1 个待处理");
|
expect(screen.getByRole("button", { name: /全项目活动/u })).toHaveTextContent("1 个待处理");
|
||||||
fireEvent.click(screen.getByRole("button", { name: "仅此次" }));
|
fireEvent.click(screen.getByRole("button", { name: "切换助手工作栏" }));
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: /任务中心/u }));
|
||||||
|
const taskRow = screen.getByRole("button", { name: /发布任务发布审批会话/u }).closest("article")!;
|
||||||
|
expect(within(taskRow).getByLabelText("等待审批: write_file")).toBeVisible();
|
||||||
|
expect(screen.queryByRole("heading", { name: "等待审批" })).not.toBeInTheDocument();
|
||||||
|
fireEvent.click(within(taskRow).getByRole("button", { name: "仅此次允许" }));
|
||||||
|
await waitFor(() => expect(api.agent.respondApproval).toHaveBeenCalledWith("00000000-0000-4000-8000-000000000834", "once"));
|
||||||
|
await waitFor(() => expect(within(taskRow).queryByLabelText("等待审批: write_file")).not.toBeInTheDocument());
|
||||||
await waitFor(() => expect(screen.getByRole("button", { name: /全项目活动/u })).toHaveTextContent("1 个运行中"));
|
await waitFor(() => expect(screen.getByRole("button", { name: /全项目活动/u })).toHaveTextContent("1 个运行中"));
|
||||||
expect(screen.getByRole("button", { name: /全项目活动/u })).not.toHaveTextContent("待处理");
|
expect(screen.getByRole("button", { name: /全项目活动/u })).not.toHaveTextContent("待处理");
|
||||||
});
|
});
|
||||||
@@ -5882,6 +5889,37 @@ describe("App", () => {
|
|||||||
expect(runtimeButton).toBeEnabled();
|
expect(runtimeButton).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("associates live scheduled approvals using the dispatched task rather than another task in the conversation", async () => {
|
||||||
|
const conversationId = crypto.randomUUID();
|
||||||
|
const taskId = crypto.randomUUID();
|
||||||
|
const runId = crypto.randomUUID();
|
||||||
|
const approvalId = crypto.randomUUID();
|
||||||
|
vi.mocked(api.conversations.list).mockResolvedValue([{
|
||||||
|
id: conversationId, projectId, title: "Scheduled approval target", updatedAt: Date.now(), messages: [],
|
||||||
|
}]);
|
||||||
|
const task = { id: taskId, conversationId, projectId, title: "Dispatched task", instructions: "Write", origin: "schedule" as const, status: "idle" as const, createdAt: new Date().toISOString() };
|
||||||
|
vi.mocked(api.tasks.list).mockResolvedValue([task, { ...task, id: crypto.randomUUID(), title: "Another task" }]);
|
||||||
|
render(<App />);
|
||||||
|
await waitFor(() => expect(api.conversationQueue.ready).toHaveBeenCalledWith(conversationId));
|
||||||
|
act(() => conversationQueueDispatchListener?.({
|
||||||
|
scheduled: true,
|
||||||
|
item: { id: runId, scheduleRunId: runId, taskId, conversationId, source: "schedule", label: "Timed message", createdAt: new Date().toISOString() },
|
||||||
|
input: { conversationId, projectId, prompt: "Write a scheduled file" },
|
||||||
|
}));
|
||||||
|
await waitFor(() => expect(run).toHaveBeenCalledOnce());
|
||||||
|
act(() => agentListener?.({ requestId: runId, type: "approval", approvalId, title: "Scheduled write", description: "Confirm write", toolName: "write_file", argumentSummary: "file.md", allowPermanent: false }));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "切换助手工作栏" }));
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: /任务中心/u }));
|
||||||
|
const row = screen.getByRole("button", { name: /Dispatched taskScheduled approval target/u }).closest("article")!;
|
||||||
|
expect(within(row).getByLabelText("等待审批: write_file")).toBeVisible();
|
||||||
|
const otherRow = screen.getByRole("button", { name: /Another taskScheduled approval target/u }).closest("article")!;
|
||||||
|
expect(within(otherRow).queryByLabelText("等待审批: write_file")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole("heading", { name: "等待审批" })).not.toBeInTheDocument();
|
||||||
|
fireEvent.click(within(row).getByRole("button", { name: "拒绝" }));
|
||||||
|
await waitFor(() => expect(api.agent.respondApproval).toHaveBeenCalledWith(approvalId, "deny"));
|
||||||
|
await waitFor(() => expect(within(row).queryByLabelText("等待审批: write_file")).not.toBeInTheDocument());
|
||||||
|
});
|
||||||
|
|
||||||
it("sends scheduled instructions with the target conversation's current settings and history", async () => {
|
it("sends scheduled instructions with the target conversation's current settings and history", async () => {
|
||||||
const conversationId = crypto.randomUUID();
|
const conversationId = crypto.randomUUID();
|
||||||
const runId = crypto.randomUUID();
|
const runId = crypto.randomUUID();
|
||||||
|
|||||||
@@ -502,6 +502,7 @@ type Conversation = Omit<ConversationListSnapshot, "messages"> & {
|
|||||||
type ActiveRun = {
|
type ActiveRun = {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
|
taskId?: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
runtimeSelectionKey: string;
|
runtimeSelectionKey: string;
|
||||||
};
|
};
|
||||||
@@ -3979,6 +3980,7 @@ function App(): React.JSX.Element {
|
|||||||
conversationId: conversation.id,
|
conversationId: conversation.id,
|
||||||
projectId: conversation.projectId,
|
projectId: conversation.projectId,
|
||||||
messageId: message.id,
|
messageId: message.id,
|
||||||
|
taskId: message.approval.taskId ?? message.task?.id,
|
||||||
approvalId: message.approval.id,
|
approvalId: message.approval.id,
|
||||||
title: message.approval.title,
|
title: message.approval.title,
|
||||||
description: message.approval.description,
|
description: message.approval.description,
|
||||||
@@ -4864,6 +4866,7 @@ function App(): React.JSX.Element {
|
|||||||
status: undefined,
|
status: undefined,
|
||||||
approval: {
|
approval: {
|
||||||
id: event.approvalId,
|
id: event.approvalId,
|
||||||
|
taskId: run.taskId,
|
||||||
title: event.title,
|
title: event.title,
|
||||||
description: event.description,
|
description: event.description,
|
||||||
toolName: event.toolName,
|
toolName: event.toolName,
|
||||||
@@ -7502,6 +7505,7 @@ function App(): React.JSX.Element {
|
|||||||
activeRuns.current.set(requestId, {
|
activeRuns.current.set(requestId, {
|
||||||
conversationId,
|
conversationId,
|
||||||
messageId: assistantMessage.id,
|
messageId: assistantMessage.id,
|
||||||
|
taskId: queuedDispatch?.scheduled ? queuedDispatch.item.taskId : undefined,
|
||||||
projectId: projectIdSnapshot,
|
projectId: projectIdSnapshot,
|
||||||
runtimeSelectionKey: agentRuntimeSelectionKey(
|
runtimeSelectionKey: agentRuntimeSelectionKey(
|
||||||
runtimeSettings
|
runtimeSettings
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export type Message = {
|
|||||||
subagents?: SubagentActivity[]
|
subagents?: SubagentActivity[]
|
||||||
approval?: {
|
approval?: {
|
||||||
id: string
|
id: string
|
||||||
|
taskId?: string
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
toolName?: string
|
toolName?: string
|
||||||
|
|||||||
@@ -555,7 +555,8 @@ describe('RightAssistantSidebar resizing', () => {
|
|||||||
it('keeps the product Task index in the task center', () => {
|
it('keeps the product Task index in the task center', () => {
|
||||||
renderSidebar({ tab: 'tasks' })
|
renderSidebar({ tab: 'tasks' })
|
||||||
|
|
||||||
expect(screen.getByText('等待审批')).toBeInTheDocument()
|
expect(screen.queryByText('等待审批')).not.toBeInTheDocument()
|
||||||
|
expect(screen.queryByText(i18n.t('sidebar.tasks.noApprovals', { ns: 'workspace' }))).not.toBeInTheDocument()
|
||||||
const taskIndexHeading = screen.getByRole('heading', {
|
const taskIndexHeading = screen.getByRole('heading', {
|
||||||
name: '任务索引'
|
name: '任务索引'
|
||||||
})
|
})
|
||||||
@@ -631,6 +632,65 @@ describe('RightAssistantSidebar resizing', () => {
|
|||||||
expect(onRespondApproval).toHaveBeenLastCalledWith(approval, 'deny')
|
expect(onRespondApproval).toHaveBeenLastCalledWith(approval, 'deny')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('embeds matched approvals only in active task cards and preserves their actions', () => {
|
||||||
|
const task: AssistantTask = {
|
||||||
|
id: 'scheduled-task', conversationId: 'conversation-1', projectId: currentProject.id,
|
||||||
|
title: 'Scheduled task', instructions: '', origin: 'schedule', status: 'paused',
|
||||||
|
createdAt: '2026-09-16T00:00:00Z'
|
||||||
|
}
|
||||||
|
const approval: PendingSidebarApproval = {
|
||||||
|
taskId: task.id, conversationId: task.conversationId!, projectId: task.projectId,
|
||||||
|
messageId: 'message-1', approvalId: 'approval-1', title: 'Confirm write', description: 'write file'
|
||||||
|
}
|
||||||
|
const onRespondApproval = vi.fn()
|
||||||
|
renderSidebar({ tasks: [task], approvals: [approval], onRespondApproval })
|
||||||
|
const row = screen.getByText(task.title).closest('article')!
|
||||||
|
expect(within(row).getByLabelText('等待审批: Confirm write')).toBeVisible()
|
||||||
|
expect(screen.queryByRole('heading', { name: '等待审批' })).not.toBeInTheDocument()
|
||||||
|
fireEvent.click(within(row).getByRole('button', { name: '仅此次允许' }))
|
||||||
|
expect(onRespondApproval).toHaveBeenLastCalledWith(approval, 'once')
|
||||||
|
fireEvent.click(within(row).getByRole('button', { name: '拒绝' }))
|
||||||
|
expect(onRespondApproval).toHaveBeenLastCalledWith(approval, 'deny')
|
||||||
|
for (const filter of ['暂停', '已结束']) {
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: filter }))
|
||||||
|
expect(screen.queryByText(task.title)).not.toBeInTheDocument()
|
||||||
|
expect(screen.queryByLabelText('等待审批: Confirm write')).not.toBeInTheDocument()
|
||||||
|
expect(screen.queryByRole('heading', { name: '等待审批' })).not.toBeInTheDocument()
|
||||||
|
expect(screen.getByLabelText('等待审批: 1')).toBeVisible()
|
||||||
|
}
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '进行中' }))
|
||||||
|
expect(screen.getByLabelText('等待审批: Confirm write')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps unmatched approvals accessible without guessing by conversation or crossing projects', () => {
|
||||||
|
const task: AssistantTask = {
|
||||||
|
id: 'task-1', conversationId: 'conversation-1', projectId: currentProject.id,
|
||||||
|
title: 'Task one', instructions: '', origin: 'schedule', status: 'running',
|
||||||
|
createdAt: '2026-09-16T00:00:00Z'
|
||||||
|
}
|
||||||
|
const approvals: PendingSidebarApproval[] = [
|
||||||
|
{ taskId: task.id, conversationId: task.conversationId!, projectId: task.projectId },
|
||||||
|
{ taskId: undefined, conversationId: task.conversationId!, projectId: task.projectId },
|
||||||
|
{ taskId: 'other-run', conversationId: task.conversationId!, projectId: task.projectId },
|
||||||
|
{ taskId: task.id, conversationId: 'other-conversation', projectId: task.projectId },
|
||||||
|
{ taskId: task.id, conversationId: task.conversationId!, projectId: 'other-project' },
|
||||||
|
{ taskId: 'child', conversationId: task.conversationId!, projectId: task.projectId }
|
||||||
|
].map((identity, index) => ({ ...identity, messageId: `message-${index}`, approvalId: `approval-${index}`, title: `Approval ${index}`, description: '' }))
|
||||||
|
renderSidebar({ tasks: [task, { ...task, id: 'child', parentTaskId: task.id }], approvals })
|
||||||
|
const row = screen.getByText(task.title).closest('article')!
|
||||||
|
expect(within(row).getAllByRole('article')).toHaveLength(1)
|
||||||
|
expect(within(row).getByText('Approval 0')).toBeVisible()
|
||||||
|
for (const index of [1, 2, 3, 5]) {
|
||||||
|
expect(screen.getByText(`Approval ${index}`)).toBeVisible()
|
||||||
|
expect(within(row).queryByText(`Approval ${index}`)).not.toBeInTheDocument()
|
||||||
|
}
|
||||||
|
expect(screen.queryByText('Approval 4')).not.toBeInTheDocument()
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '所有项目' }))
|
||||||
|
expect(screen.getByText('Approval 4')).toBeVisible()
|
||||||
|
expect(within(row).queryByText('Approval 4')).not.toBeInTheDocument()
|
||||||
|
expect(screen.getAllByText('Approval 0')).toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
it('labels approval counts and cards without duplicate live regions', () => {
|
it('labels approval counts and cards without duplicate live regions', () => {
|
||||||
renderSidebar({
|
renderSidebar({
|
||||||
approvals: [
|
approvals: [
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export type SidebarArtifact = {
|
|||||||
|
|
||||||
export type PendingSidebarApproval = {
|
export type PendingSidebarApproval = {
|
||||||
conversationId: string
|
conversationId: string
|
||||||
|
taskId?: string
|
||||||
projectId?: string
|
projectId?: string
|
||||||
messageId: string
|
messageId: string
|
||||||
approvalId: string
|
approvalId: string
|
||||||
@@ -163,6 +164,31 @@ const minimumPaneWidth = 160
|
|||||||
const keyboardResizeStep = 16
|
const keyboardResizeStep = 16
|
||||||
const workbarStorageKey = 'goodbuddy.workbar-layout.v1'
|
const workbarStorageKey = 'goodbuddy.workbar-layout.v1'
|
||||||
|
|
||||||
|
function SidebarApproval({ approval, onRespondApproval }: {
|
||||||
|
approval: PendingSidebarApproval
|
||||||
|
onRespondApproval: RightAssistantSidebarProps['onRespondApproval']
|
||||||
|
}): React.JSX.Element {
|
||||||
|
const { t } = useTranslation('workspace')
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
aria-label={`${t('sidebar.tasks.approvalsTitle')}: ${approval.toolName ?? approval.title}`}
|
||||||
|
className="assistant-sidebar__approval"
|
||||||
|
>
|
||||||
|
<strong>{approval.title}</strong>
|
||||||
|
<p>{approval.description}</p>
|
||||||
|
{approval.toolName && <code>{approval.toolName}</code>}
|
||||||
|
<div className="assistant-sidebar__approval-actions">
|
||||||
|
<button className="secondary-button" onClick={() => onRespondApproval(approval, 'deny')} type="button">
|
||||||
|
{t('sidebar.tasks.deny')}
|
||||||
|
</button>
|
||||||
|
<button className="primary-button" onClick={() => onRespondApproval(approval, 'once')} type="button">
|
||||||
|
{t('sidebar.tasks.allowOnce')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function loadPersistedWorkbarLayout(): ReturnType<
|
function loadPersistedWorkbarLayout(): ReturnType<
|
||||||
typeof workbarLayoutPreferencesSchema.parse
|
typeof workbarLayoutPreferencesSchema.parse
|
||||||
> | undefined {
|
> | undefined {
|
||||||
@@ -942,9 +968,27 @@ export function RightAssistantSidebar({
|
|||||||
),
|
),
|
||||||
[taskMatchesScope, tasks]
|
[taskMatchesScope, tasks]
|
||||||
)
|
)
|
||||||
|
const { approvalsByTask, unassociatedApprovals } = useMemo(() => {
|
||||||
|
const tasksById = new Map(topLevelTasks.map((task) => [task.id, task]))
|
||||||
|
const approvalsByTask = new Map<string, PendingSidebarApproval[]>()
|
||||||
|
const unassociatedApprovals: PendingSidebarApproval[] = []
|
||||||
|
for (const approval of scopedApprovals) {
|
||||||
|
const task = approval.taskId ? tasksById.get(approval.taskId) : undefined
|
||||||
|
// A shared conversation alone cannot identify a task or a scheduled run.
|
||||||
|
if (task && task.conversationId === approval.conversationId && task.projectId === approval.projectId) {
|
||||||
|
const pending = approvalsByTask.get(task.id) ?? []
|
||||||
|
pending.push(approval)
|
||||||
|
approvalsByTask.set(task.id, pending)
|
||||||
|
} else {
|
||||||
|
unassociatedApprovals.push(approval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { approvalsByTask, unassociatedApprovals }
|
||||||
|
}, [scopedApprovals, topLevelTasks])
|
||||||
const filteredTasks = useMemo(
|
const filteredTasks = useMemo(
|
||||||
() =>
|
() =>
|
||||||
topLevelTasks.filter((task) => {
|
topLevelTasks.filter((task) => {
|
||||||
|
if (approvalsByTask.has(task.id)) return taskFilter === 'active'
|
||||||
if (taskFilter === 'active') {
|
if (taskFilter === 'active') {
|
||||||
return (
|
return (
|
||||||
task.status === 'idle' ||
|
task.status === 'idle' ||
|
||||||
@@ -960,7 +1004,7 @@ export function RightAssistantSidebar({
|
|||||||
}
|
}
|
||||||
return task.status === 'completed' || task.status === 'cancelled'
|
return task.status === 'completed' || task.status === 'cancelled'
|
||||||
}),
|
}),
|
||||||
[taskFilter, topLevelTasks]
|
[approvalsByTask, taskFilter, topLevelTasks]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1894,49 +1938,15 @@ export function RightAssistantSidebar({
|
|||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
{unassociatedApprovals.length > 0 && <>
|
||||||
<h3>
|
<h3>
|
||||||
<ShieldAlert size={15} />
|
<ShieldAlert size={15} />
|
||||||
{t('sidebar.tasks.approvalsTitle')}
|
{t('sidebar.tasks.approvalsTitle')}
|
||||||
</h3>
|
</h3>
|
||||||
{scopedApprovals.length === 0 ? (
|
{unassociatedApprovals.map((approval) => (
|
||||||
<p className="assistant-sidebar__empty">
|
<SidebarApproval key={approval.approvalId} approval={approval} onRespondApproval={onRespondApproval} />
|
||||||
{t('sidebar.tasks.noApprovals')}
|
))}
|
||||||
</p>
|
</>}
|
||||||
) : (
|
|
||||||
scopedApprovals.map((approval) => (
|
|
||||||
<article
|
|
||||||
aria-label={`${t('sidebar.tasks.approvalsTitle')}: ${
|
|
||||||
approval.toolName ?? approval.title
|
|
||||||
}`}
|
|
||||||
className="assistant-sidebar__approval"
|
|
||||||
key={approval.approvalId}
|
|
||||||
>
|
|
||||||
<strong>{approval.title}</strong>
|
|
||||||
<p>{approval.description}</p>
|
|
||||||
{approval.toolName && <code>{approval.toolName}</code>}
|
|
||||||
<div className="assistant-sidebar__approval-actions">
|
|
||||||
<button
|
|
||||||
className="secondary-button"
|
|
||||||
onClick={() =>
|
|
||||||
onRespondApproval(approval, 'deny')
|
|
||||||
}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
{t('sidebar.tasks.deny')}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="primary-button"
|
|
||||||
onClick={() =>
|
|
||||||
onRespondApproval(approval, 'once')
|
|
||||||
}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
{t('sidebar.tasks.allowOnce')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="task-center__index-heading">
|
<div className="task-center__index-heading">
|
||||||
<h3>
|
<h3>
|
||||||
@@ -2058,6 +2068,9 @@ export function RightAssistantSidebar({
|
|||||||
})
|
})
|
||||||
: t('sidebar.tasks.notStarted'))}
|
: t('sidebar.tasks.notStarted'))}
|
||||||
</p>
|
</p>
|
||||||
|
{approvalsByTask.get(task.id)?.map((approval) => (
|
||||||
|
<SidebarApproval key={approval.approvalId} approval={approval} onRespondApproval={onRespondApproval} />
|
||||||
|
))}
|
||||||
{schedule && (
|
{schedule && (
|
||||||
<div className="task-center__actions">
|
<div className="task-center__actions">
|
||||||
<TaskScheduleActions
|
<TaskScheduleActions
|
||||||
|
|||||||
Reference in New Issue
Block a user