fix: derive run status from final result
This commit is contained in:
@@ -53,6 +53,12 @@ Keep Electron security boundaries intact:
|
||||
- Reuse installed libraries and shared contracts before adding dependencies.
|
||||
- Keep changes focused. Do not add unrelated refactors or documentation.
|
||||
- Add or update focused tests for behavioral changes and regressions.
|
||||
- After completing any functional change, inspect the affected product,
|
||||
architecture, design, feature, setup, and operational documentation and
|
||||
update every relevant document to match the implemented behavior. Treat the
|
||||
final code and validated runtime behavior as the source of truth: correct
|
||||
stale documentation rather than preserving outdated intent. Avoid
|
||||
documentation churn only when the change has no documented impact.
|
||||
- Avoid broad catches that erase HTTP status, cancellation, or provider error
|
||||
context.
|
||||
- Keep UI accessible with labels, keyboard behavior, semantic roles, and visible
|
||||
|
||||
+2
-1
@@ -505,7 +505,8 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
|
||||
- 使用 `dashboard` 壳层,并通过 `PageTabs` 提供“任务与会话 / 活动时间线 / 用量统计”三个同级视图。
|
||||
- 默认视图按“项目 → 任务或会话 → 活动详情”组织,项目范围持续可见,任务或会话详情可以折叠。
|
||||
- 活动时间线按项目分组、按任务或会话建立横向轨道,所有轨道共享同一执行顺序并按事件时间排列,以带身份名称的节点表达用户、主 Agent、子专家、工具、审批、状态和并行关系;选择节点后显示所属会话、不可变范围快照和完整详情。
|
||||
- 任务或会话的综合状态以最近一次顶层请求对应的最终 Agent 结果为准;最终结果尚未产生时使用该请求的当前状态。中间工具或子专家的失败、取消和中断保留在活动详情中,但不得覆盖最终成功状态。
|
||||
- 活动时间线按项目分组、按任务或会话建立横向轨道,所有轨道共享同一执行顺序并按事件时间排列,以带身份名称的节点表达用户、主 Agent、子专家、工具、审批、状态和并行关系。节点内使用 `U / G / S / T / A` 拉丁字母简称,节点下显示完整身份或名称;选择节点后显示所属会话、不可变范围快照和完整详情。
|
||||
- 用量统计与活动记录分离,支持按项目、会话和模型切换统计维度,宽表格在独立容器内横向滚动。
|
||||
- 活动状态筛选使用 `SegmentedControl`,不与页面页签混合。清空历史遵循破坏性操作政策。
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ GoodBuddy 已将企业微信、钉钉和微信 ClawBot 远程消息通道纳入
|
||||
- 通道项目标识平台并确定默认工作目录、处理后端和默认模式。
|
||||
- 远程会话标识具体发送者或群聊。
|
||||
- 消息记录具体发送者和本次实际使用的模式。
|
||||
- 任务与活动记录执行、工具调用和结果。
|
||||
- 运行记录覆盖任务执行、工具调用和结果。
|
||||
|
||||
## 2. 已确认的产品决策
|
||||
|
||||
@@ -119,7 +119,7 @@ GoodBuddy 已将企业微信、钉钉和微信 ClawBot 远程消息通道纳入
|
||||
| 通道项目 | 平台、连接状态、默认根目录、处理后端、默认模式 |
|
||||
| 远程会话 | 平台账号、私聊用户或群聊、连续上下文 |
|
||||
| 消息 | 具体发送者、本次实际模式、正文、时间和处理状态 |
|
||||
| 任务与活动 | Runtime、工具调用、执行结果和错误 |
|
||||
| 运行记录 | Runtime、工具调用、执行结果和错误 |
|
||||
|
||||
### 5.3 会话命名
|
||||
|
||||
@@ -268,7 +268,7 @@ Execute 启动前检查解析后的后端是否支持工具执行,并返回可
|
||||
- 尚无远程会话时显示等待首条客户端消息的空状态和设置入口。
|
||||
- 旧版本误建在通道项目中的普通本地会话不参与通道会话列表,但保留其数据。
|
||||
- 远程会话底部说明客户端联动方式,只显示历史、任务和执行结果,不再提及已移除的审批流程。
|
||||
- “任务与活动”页面按会话分组显示远程任务;所有分组首次进入时默认收起,包括进行中、失败和已完成状态,用户可通过原生展开控件查看明细。
|
||||
- “运行记录”的“任务与会话”视图按“项目 → 任务或会话 → 活动详情”显示远程任务;所有任务或会话首次进入时默认收起,用户可通过原生展开控件查看明细。综合状态以最近一次顶层请求对应的最终 Agent 结果为准,最终结果尚未产生时使用请求当前状态;中间工具或子专家的失败、取消和中断不得覆盖最终成功状态。
|
||||
|
||||
### 7.6 微信扫码绑定
|
||||
|
||||
@@ -672,7 +672,7 @@ Renderer 快照只返回是否已配置和脱敏标识。
|
||||
- [ ] 切换到通道项目不会创建普通本地会话。
|
||||
- [ ] 通道项目隐藏“新建对话”和 `Ctrl+N`,全局快捷命令也不创建会话。
|
||||
- [ ] 没有远程会话时显示等待客户端首条消息的空状态。
|
||||
- [ ] “任务与活动”中的会话分组默认收起,进行中、失败和已完成状态行为一致。
|
||||
- [ ] “运行记录”的任务或会话分组默认收起;综合状态使用最近一次顶层请求的最终 Agent 结果,中间工具或子专家失败不得覆盖最终成功状态。
|
||||
- [ ] 微信图片和文件显示在对应远程消息中,附件消息无需附带文字。
|
||||
- [ ] 支持的附件进入所选后端现有图片或文档上下文;不支持和超限附件返回明确提示。
|
||||
|
||||
|
||||
@@ -162,6 +162,58 @@ describe('ActivityPanel', () => {
|
||||
).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('uses the latest top-level Agent result for conversation status', () => {
|
||||
const request: ActivityRecord = {
|
||||
...makeRecord(1, 'running'),
|
||||
conversationId: 'conversation-status',
|
||||
requestId: 'request-status',
|
||||
kind: 'request',
|
||||
title: '最终成功的任务'
|
||||
}
|
||||
const failedTool: ActivityRecord = {
|
||||
...makeRecord(2, 'failed'),
|
||||
conversationId: request.conversationId,
|
||||
requestId: request.requestId,
|
||||
kind: 'tool',
|
||||
title: '中间失败的工具'
|
||||
}
|
||||
const cancelledSubagent: ActivityRecord = {
|
||||
...makeRecord(3, 'cancelled'),
|
||||
conversationId: request.conversationId,
|
||||
requestId: request.requestId,
|
||||
kind: 'subagent',
|
||||
title: '已取消的子专家'
|
||||
}
|
||||
const result: ActivityRecord = {
|
||||
...makeRecord(4, 'completed'),
|
||||
conversationId: request.conversationId,
|
||||
requestId: request.requestId,
|
||||
kind: 'result',
|
||||
title: '任务执行完成'
|
||||
}
|
||||
render(
|
||||
<ActivityPanel
|
||||
onClear={vi.fn()}
|
||||
onOpenConversation={vi.fn()}
|
||||
records={[result, cancelledSubagent, failedTool, request]}
|
||||
tokenUsage={makeTokenUsage()}
|
||||
/>
|
||||
)
|
||||
|
||||
const conversation = screen
|
||||
.getByText('对话:最终成功的任务')
|
||||
.closest('summary')
|
||||
expect(conversation).not.toBeNull()
|
||||
if (!conversation) {
|
||||
return
|
||||
}
|
||||
expect(within(conversation).getByText('已完成')).toBeInTheDocument()
|
||||
expect(within(conversation).queryByText('失败')).not.toBeInTheDocument()
|
||||
expect(
|
||||
within(conversation).queryByText('已取消')
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('filters active and exceptional activity and opens its conversation', () => {
|
||||
const onOpenConversation = vi.fn()
|
||||
render(
|
||||
|
||||
@@ -127,9 +127,32 @@ function activityScopeKey(scope: ActivityRecord['scope']): string {
|
||||
function conversationStatus(
|
||||
records: readonly ActivityRecord[]
|
||||
): ActivityRecord['status'] {
|
||||
const latestRecord = (
|
||||
candidates: readonly ActivityRecord[]
|
||||
): ActivityRecord | undefined =>
|
||||
candidates.reduce<ActivityRecord | undefined>(
|
||||
(latest, record) =>
|
||||
!latest || record.createdAt > latest.createdAt ? record : latest,
|
||||
undefined
|
||||
)
|
||||
const latestRequest = latestRecord(
|
||||
records.filter((record) => record.kind === 'request')
|
||||
)
|
||||
if (latestRequest) {
|
||||
const latestResult = latestRecord(
|
||||
records.filter(
|
||||
(record) =>
|
||||
record.kind === 'result' &&
|
||||
record.requestId === latestRequest.requestId
|
||||
)
|
||||
)
|
||||
return latestResult?.status ?? latestRequest.status
|
||||
}
|
||||
|
||||
return (
|
||||
records.find(isActive)?.status ??
|
||||
records.find(isFailed)?.status ??
|
||||
latestRecord(records.filter((record) => record.kind === 'result'))
|
||||
?.status ??
|
||||
latestRecord(records)?.status ??
|
||||
'completed'
|
||||
)
|
||||
}
|
||||
@@ -154,6 +177,13 @@ function groupActivityRecordsByProject(
|
||||
allRecords: readonly ActivityRecord[]
|
||||
): ProjectActivityGroup[] {
|
||||
const conversationTitles = getConversationTitles(allRecords)
|
||||
const allConversationRecords = new Map<string, ActivityRecord[]>()
|
||||
for (const record of allRecords) {
|
||||
const conversationRecords =
|
||||
allConversationRecords.get(record.conversationId) ?? []
|
||||
conversationRecords.push(record)
|
||||
allConversationRecords.set(record.conversationId, conversationRecords)
|
||||
}
|
||||
const projectGroups = new Map<
|
||||
string,
|
||||
{
|
||||
@@ -186,7 +216,9 @@ function groupActivityRecordsByProject(
|
||||
conversationTitles.get(conversationId) ?? items[0]!.title,
|
||||
records: items,
|
||||
latestAt: Math.max(...items.map((record) => record.createdAt)),
|
||||
status: conversationStatus(items)
|
||||
status: conversationStatus(
|
||||
allConversationRecords.get(conversationId) ?? items
|
||||
)
|
||||
})
|
||||
)
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user