diff --git a/UI-DESIGN.md b/UI-DESIGN.md
index 811e36b1..27347c79 100644
--- a/UI-DESIGN.md
+++ b/UI-DESIGN.md
@@ -716,7 +716,7 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
- 监督者入口使用 `supervisor` 壳层;智能心跳设置位于同一壳层内。
- 顶层页签为“工作回顾 / 故事线图谱 / 自动监督 / 活动记录 / 设置”,使用一套共享 PageTabs,窄窗口单行横向滚动。页签下不重复同名标题,也不保留空标题容器;所选回顾使用“监督结果”标题,历史结果不得标为最新。
- 工作回顾只展示统一监督结果、历史选择和紧凑的新回顾控件;结果保留自身范围、覆盖区间和生成时间,图谱读取同一结果 ID。自动监督集中展示当前计划、运行概览、指标、趋势、自动监督报告、记忆与行动建议及心跳运行审计;活动记录只展示统一执行活动、进度和详情。每页保留所属数据的一处刷新入口,范围与时间选择仅作用于新回顾请求。详细布局与阅读规则见[监督者 UI 设计](docs/features/conversation-supervision/ui-design.md#应用入口)。
-- 监督者保留统一页面壳层;阅读与活动子面板分别复用 `--content-reading` 和 `--content-dashboard`,在壳层内左对齐。各面板的工具栏、历史导航、状态和结果共享宽度,不在全宽卡片内部再次居中窄正文。活动筛选按内容紧邻排列,阶段以文字状态行呈现。
+- 监督者保留统一页面壳层;工作回顾与活动子面板铺满壳层可用内容宽度,与页签左右对齐。工具栏、历史导航、状态和结果共享宽度,不再增加子面板宽度上限。工作回顾按纵向排列,空态独占工具栏下方整行并水平居中。活动筛选按内容紧邻排列,阶段使用编号圆点和连接线,次要统计收进详情;具体布局见[监督者 UI 设计](docs/features/conversation-supervision/ui-design.md)。
- 范围明确区分 Global 与指定的一个或多个 Project;Global 与指定项目互斥,多项目选择使用
Checkbox,不能依靠进入页面时的当前项目推断。
- 状态卡片使用统一状态令牌,不只依赖颜色。
diff --git a/docs/features/conversation-supervision/ui-design.md b/docs/features/conversation-supervision/ui-design.md
index 1f30c26b..4f9ee583 100644
--- a/docs/features/conversation-supervision/ui-design.md
+++ b/docs/features/conversation-supervision/ui-design.md
@@ -8,13 +8,13 @@
当前应用页只使用一套共享 PageTabs:“工作回顾 / 故事线图谱 / 自动监督 / 活动记录 / 设置”(英文活动标签为 Activity records)。自动监督独立管理计划;设置只显示模型超时与并发、回顾算法,不含计划、报告、审计或内部页签,也不显示整页全局徽标或独立刷新行。计划范围仍在表单和每条计划内显示。整个页面只有一个一级标题;监督者 bridge 不可用时显示明确错误,设置仍可访问,不回退到旧心跳概览。共享页签保留方向键、Home/End、游标焦点和单一激活面板,窄窗口横向滚动。
-工作回顾只读取统一监督结果。顶部为紧凑的“新回顾”范围、时间、发起与刷新控件,不新增 Modal;简短帮助说明选择仅作用于新请求。历史结果选择与图谱入口位于正文之前,即使只有一条结果也显示选择器。新回顾工具栏、状态、历史选择和结果面板铺满监督者页面壳层的可用内容宽度,与页签左右对齐,不另设阅读宽度上限;结果内部按面板内边距排版,不再单独居中窄正文。正文使用“监督结果”标题,常显生成日期、真实范围和覆盖区间,历史结果不标为最新。工作总结按原有空行分段并保留换行;本次变化和未解决事项直接使用已有字段,不补写结论或截断正文。390px 下新请求字段与操作各占两列,结果元数据可换行。
+工作回顾只读取统一监督结果。顶部紧凑排列范围、时间、发起与刷新控件,不重复显示“新回顾”标题和来源说明,不新增 Modal。历史结果选择与图谱入口位于正文之前,即使只有一条结果也显示选择器。新回顾工具栏、状态、历史选择和结果面板铺满监督者页面壳层的可用内容宽度,与页签左右对齐,不另设阅读宽度上限;结果内部按面板内边距排版,不再单独居中窄正文。正文使用“监督结果”标题,常显生成日期、真实范围和覆盖区间,历史结果不标为最新。工作总结按原有空行分段并保留换行;本次变化和未解决事项直接使用已有字段,不补写结论或截断正文。390px 下新请求字段与操作各占两列,结果元数据可换行。
-新回顾运行中保留已显示结果及其日期、范围,禁用重复提交,并显示简短状态和活动记录入口;该入口清除计划筛选并聚焦活动页签。工作回顾不读取自动监督报告,也不显示自动报告空态、记忆与行动建议或第二行全局范围与刷新。无监督结果时显示手动回顾入口;自动报告是否为空不影响此判断。
+新回顾运行中保留已显示结果及其日期、范围,禁用重复提交,并显示简短状态和活动记录入口;该入口清除计划筛选并聚焦活动页签。工作回顾不读取自动监督报告,也不显示自动报告空态、记忆与行动建议或第二行全局范围与刷新。无监督结果时,空态独占工具栏下方整行,标题与手动回顾入口水平居中;自动报告是否为空不影响此判断。运行中和暂停后使用独立状态条,提示当前状态及活动记录入口。
## 活动
-文案遵循根目录 [监督者文案规则](../../../UI-DESIGN.md#134-智能心跳):不把内部路线图、交付状态或实现缺项展示给用户。活动说明只描述手动回顾、自动监督执行及自动刷新;真实错误、失败和暂停状态仍展示,并保留恢复操作。事实详情提示展开批次并对照原文核对模型内容,不承诺完整语义识别;图谱说明实体状态以所选回顾结果为准。
+文案遵循根目录 [监督者文案规则](../../../UI-DESIGN.md#134-智能心跳):不把内部路线图、交付状态或实现缺项展示给用户。活动页不常显重复说明;真实错误、失败和暂停状态仍展示,并保留恢复操作。事实详情提示展开批次并对照原文核对模型内容,不承诺完整语义识别;图谱说明实体状态以所选回顾结果为准。
对应 FR-S10、US-S26。活动按开始时间倒序,每页 50 条,提供较新/更早记录和刷新。列表保留触发方式、执行范围、开始/结束时间、实际状态、阶段状态、错误及已保存摘要。没有结果时不显示结果按钮;有监督结果时可分别进入该结果的工作回顾和故事线图谱,焦点移到目标页签。超过最近 20 条的结果仍按 ID 读取,不能回退最新结果。
@@ -24,9 +24,9 @@
加载、空记录和读取失败分开显示;读取失败保留已显示记录并提供就地重试。活动页面激活时,存在运行中记录则在上次读取结束 2 秒后刷新,否则 10 秒后刷新;每次最多一个请求。切换顶层页签、离开监督者或关闭应用后停止定时器,迟到响应不更新界面。错误后等待用户重试,不持续重发失败查询。
-活动列表与工具栏铺满监督者页面壳层的可用内容宽度,与页签左右对齐,不另设仪表盘宽度上限。范围、计划筛选、清除筛选和刷新紧邻排列,空间不足时换行。列表使用共享表面、边框、状态颜色和按钮。每条运行首先显示类型、状态、开始时间和范围;“收集来源 / 整理批次 / 合并摘要 / 发布回顾”紧随其后,宽窗口使用按内容排布的紧凑状态行,窄于 720px 时两列,不为每个阶段铺设独立色块卡片。阶段读取已保存位置和真实运行状态,剩余来源为零不表示发布成功。旧运行没有阶段字段时显示“未记录”,不猜测失败位置;已提交来源清单可确认收集结束。无变化运行不显示新结果跳转,后续无需执行的阶段单独标注。
+活动列表与工具栏铺满监督者页面壳层的可用内容宽度,与页签左右对齐,不另设仪表盘宽度上限。范围、计划筛选、清除筛选和刷新紧邻排列,空间不足时换行。列表使用共享表面、边框、状态颜色和按钮。每条运行首先显示类型、状态、开始时间和范围;恢复与事实操作紧随其后,再展示“收集来源 / 整理批次 / 合并摘要 / 发布回顾”。阶段使用编号圆点和连接线,宽窗口四项等宽,窄于 720px 时两列;当前及已完成阶段突出显示,不为每个阶段铺设独立色块卡片。阶段读取已保存位置和真实运行状态,剩余来源为零不表示发布成功。旧运行没有阶段字段时显示“未记录”,不猜测失败位置;已提交来源清单可确认收集结束。无变化运行不显示新结果跳转,后续无需执行的阶段单独标注。
-阶段下显示已保存批数、已读字符数、剩余来源和已保存导航合并数,运行中另显示排队或在途数。字符进度按码点统计,界面不展示 Unicode 术语。恢复操作先于详情:运行中可暂停,失败或暂停后沿用同一运行继续;已记录合并、发布阶段时分别显示“继续合并摘要”“重试发布”。成功叶子保留。来源失效时显示新建回顾提示,不提供无效继续。原始错误默认折叠,展开后的完整文本最多占 240px 高度,可滚动、选择复制和键盘聚焦;错误与恢复操作位于批次树之前。结束时间、覆盖区间、报告状态和已有摘要收进“运行详情与配置”,已保存配置在其中再次折叠。
+阶段下突出显示已保存批数和剩余来源,运行中另显示合计的排队或在途数,不拆分未经接口提供的数量。已读字符数和已保存导航合并数放入默认折叠的“运行详情与配置”。字符进度按码点统计,界面不展示 Unicode 术语。恢复操作先于详情:运行中可暂停,失败或暂停后沿用同一运行继续;已记录合并、发布阶段时分别显示“继续合并摘要”“重试发布”。成功叶子保留。来源失效时显示新建回顾提示,不提供无效继续。原始错误默认折叠,展开后的完整文本最多占 240px 高度,可滚动、选择复制和键盘聚焦;错误与恢复操作位于批次树之前。结束时间、覆盖区间、报告状态和已有摘要收进“运行详情与配置”,已保存配置在其中再次折叠。
“已保留事实与来源”展开后才查询既有批次接口,每页 10 批,按本页持久记录组成项目、会话、批次层级。项目使用可用名称,会话优先使用保存来源的标题;只有已保存批次标记“已保存”,不创建未落库的待执行、在途或失败叶子,也不把本页数量当作项目总量。批次摘要收起时最多两行,展开后保留完整摘要、事件、实体、变化、关系及原文片段。来源显示消息 ID、码点范围及版本;长原文局部滚动。原生 details/summary 保留键盘展开和可见焦点,轮询不主动展开用户收起的会话。加载、空批次和读取失败分开显示,失败可重试;换页失败不显示上一页批次,切换 run 后旧响应失效。暂停状态为“未完成,可继续”,手动回顾主动暂停后在工作回顾页提示去活动继续,不显示完成通知。
@@ -99,6 +99,8 @@ Modal 按[共享表单 Modal 规则](../../../UI-DESIGN.md#615-表单-modal)复
隔离视觉验证使用 `tests/support/supervisor-layout-regression.tsx` 渲染生产 React 组件;所有输入标为模拟 fixture,不读取用户数据库或调用模型。`tests/supervisor-layout.electron.test.ts` 启动 Vite 与 Electron,通过 `executeJavaScript` 测量布局、`capturePage` 截图,覆盖 1440、1024、390px、浅深主题、长名称、密集事件和异常状态。可通过 `GOODBUDDY_SUPERVISOR_ARTIFACTS` 保留截图;交互预览入口为 `node tests/support/supervisor-preview.mjs`。
+设置 `GOODBUDDY_SUPERVISOR_CONTENT_LAYOUT=1` 验证工作回顾、空态、活动、待处理及失败场景,在 2000、1440、1024、390px 和浅深主题下共 40 个组合,检查全宽、空态居中及位于工具栏下方、阶段连接线和长内容溢出。
+
设置 `GOODBUDDY_SUPERVISOR_PLAN_MODAL=1` 可只验证五页签 fixture 中的计划 Modal:创建和编辑各覆盖浅深主题、1440×1100、390×1100、390×480,检查关闭按钮、底部按钮对齐、正文滚动、鼠标命中和原生键盘操作。截图及测量写入同一产物目录。
设置 `GOODBUDDY_SUPERVISOR_RECAP=1` 可验证本次工作回顾分区:1440×1100 和 390×1100 下覆盖监督结果与自动报告均有内容、仅有监督结果、两者均空,另检查历史结果运行中仍可阅读以及活动入口。该分支检查每页一处刷新、自动报告和建议的归属、正文段落与宽度,并截取自动页建议、展开报告和审计区块。
diff --git a/src/renderer/src/SupervisionReviewStages.tsx b/src/renderer/src/SupervisionReviewStages.tsx
index 53c2cfe9..85fbd02e 100644
--- a/src/renderer/src/SupervisionReviewStages.tsx
+++ b/src/renderer/src/SupervisionReviewStages.tsx
@@ -22,8 +22,7 @@ export function SupervisionReviewStages({ row }: { row: SupervisionActivity }) {
})}
-
{t('reviewSettings.progress', { batches: progress.batches, characters: progress.characters, remaining: progress.remainingSources })}
- {progress.navigationNodes !== undefined && {t('activity.navigationSaved', { count: progress.navigationNodes })}
}
+ {t('activity.coverage', { batches: progress.batches, remaining: progress.remainingSources })}
{!progress.complete && !progress.phase && {t('activity.stageUnknown')}
}
{row.supervisionStatus === 'running' && {t('reviewSettings.inFlight', { count: progress.inFlight ?? 0 })}
}
diff --git a/src/renderer/src/SupervisorActivity.test.tsx b/src/renderer/src/SupervisorActivity.test.tsx
index cf83df03..53411a09 100644
--- a/src/renderer/src/SupervisorActivity.test.tsx
+++ b/src/renderer/src/SupervisorActivity.test.tsx
@@ -28,7 +28,7 @@ it.each(['zh-CN', 'en-US'] as const)('retains operational failure details and re
expect(details.closest('details')).toHaveAttribute('open')
expect(details).toHaveStyle({ userSelect: 'text' })
expect(screen.getByText(copy.activity.status.failed)).toBeVisible()
- expect(screen.getByText(copy.activity.description)).toBeVisible()
+ expect(screen.queryByText(copy.activity.description)).not.toBeInTheDocument()
expect(screen.getByRole('button', { name: copy.reviewSettings.resume })).toBeEnabled()
})
@@ -134,12 +134,17 @@ it.each(['summarizing', 'saving', undefined] as const)('renders saved phase %s w
settings: { pageSize: 50, batchCharacters: 8000, batchMessages: 20, executionSeconds: 300, timeoutSeconds: 240, concurrency: 1 } }
}]), resume } })
const { container } = render()
- await screen.findByText(/已保存 6 批/)
+ expect(await screen.findByText('已保存 6 批 · 剩余 0 个来源')).toBeVisible()
+ const detailedProgress = screen.getByText('已保存 6 批 · 已读 4515 个字符 · 剩余 0 个来源')
+ expect(detailedProgress).not.toBeVisible()
expect(container.querySelector('[data-phase="saving"]')).toHaveAttribute('data-state', phase === 'saving' ? 'failed' : phase ? 'pending' : 'unknown')
expect(container.querySelector('[data-phase="summarizing"]')).toHaveAttribute('data-state', phase === 'saving' ? 'completed' : phase ? 'failed' : 'unknown')
expect(container.querySelector('.supervisor-activity__diagnostics')).not.toHaveAttribute('open')
expect(container.querySelector('.supervisor-activity__advanced')).not.toHaveAttribute('open')
expect(screen.getByText('本次回顾的已保存配置')).not.toBeVisible()
+ fireEvent.click(container.querySelector('.supervisor-activity__advanced > summary')!)
+ expect(detailedProgress).toBeVisible()
+ expect(screen.getByText('本次回顾的已保存配置')).toBeVisible()
const action = screen.getByRole('button', { name: phase === 'summarizing' ? '继续合并摘要' : phase === 'saving' ? '重试发布' : '继续已保存回顾' })
expect(action.compareDocumentPosition(container.querySelector('.supervisor-activity__diagnostics')!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
fireEvent.click(action)
diff --git a/src/renderer/src/SupervisorActivity.tsx b/src/renderer/src/SupervisorActivity.tsx
index e32c4a0a..8b10c21e 100644
--- a/src/renderer/src/SupervisorActivity.tsx
+++ b/src/renderer/src/SupervisorActivity.tsx
@@ -89,7 +89,6 @@ export function SupervisorActivity({ active, projects, onOpenResult, configId, c
{t('center.actions.refresh')}
- {t('activity.description')}
{error && {error}
}
@@ -102,7 +101,6 @@ export function SupervisorActivity({ active, projects, onOpenResult, configId, c
{!row.scope ? t('activity.unknownScope') : row.scope.kind === 'global' ? t('center.scope.global') : t('supervisor.projectScope', { names: row.scope.projectIds.map((id) => projects.find((project) => project.id === id)?.name ?? t('settings.scope.unavailableProject')).join(', ') })} · {t(`activity.triggers.${row.trigger}`)}
- {row.reviewProgress && }
{!row.reviewProgress && <>
{row.heartbeatStatus && {t('activity.heartbeatStage')}: {t(`statuses.run.${row.heartbeatStatus}`)}}
@@ -123,6 +121,7 @@ export function SupervisorActivity({ active, projects, onOpenResult, configId, c
>}
+ {row.reviewProgress && }
{row.error &&
{t(row.status === 'paused' ? 'reviewSettings.pausedHint' : /persistedId|candidateRef|entity identity/.test(row.error)
? 'reviewSettings.identityError' : 'reviewSettings.runError')}
@@ -141,6 +140,8 @@ export function SupervisorActivity({ active, projects, onOpenResult, configId, c
{t('activity.finished')}{row.completedAt ? date(row.completedAt) : t('common.unavailable')}
{row.timeRange &&
{t('supervisor.period')}{date(row.timeRange.from)} / {date(row.timeRange.to)}}
+ {row.reviewProgress &&
{t('reviewSettings.progress', { batches: row.reviewProgress.batches, characters: row.reviewProgress.characters, remaining: row.reviewProgress.remainingSources })}
}
+ {row.reviewProgress?.navigationNodes !== undefined &&
{t('activity.navigationSaved', { count: row.reviewProgress.navigationNodes })}
}
{row.reviewProgress && row.heartbeatStatus &&
{t('activity.heartbeatStage')}: {t(`statuses.run.${row.heartbeatStatus}`)}
}
{row.summary &&
{row.summary}
}
{row.reviewProgress?.settings &&
{t('reviewSettings.configuration')}
diff --git a/src/renderer/src/SupervisorWorkspace.tsx b/src/renderer/src/SupervisorWorkspace.tsx
index 642b59fa..814c7bc1 100644
--- a/src/renderer/src/SupervisorWorkspace.tsx
+++ b/src/renderer/src/SupervisorWorkspace.tsx
@@ -449,7 +449,7 @@ export function SupervisorWorkspace({
)}
- {(pending === 'run' || pausedReview) &&
+ {(pending === 'run' || pausedReview) &&
{t(pausedReview ? 'reviewSettings.pausedHint' : 'supervisor.runningHint')}
{onOpenActivity && }
}
@@ -480,7 +480,6 @@ export function SupervisorWorkspace({
{tab === 'overview' && (
<>
- {t('supervisor.newReview')}
-
- {t('supervisor.sourcesHint')}
-
{results.length > 0 &&