fix(supervisor): review only finished sources and add an all-time range
Reviews now snapshot only messages in a terminal state (complete or error) and finished tasks, so a reply still streaming when the review starts waits for the next review instead of changing under it. A frozen source that still changes is skipped for that run and left for the next review, replacing the re-review at the latest version and its 200-source cap. The review period picker gains an All time option.
This commit is contained in:
@@ -24,7 +24,7 @@ export function SupervisionReviewStages({ row, stopping }: { row: SupervisionAct
|
||||
</ol>
|
||||
<p className="supervisor-activity__coverage">{t('activity.coverage', { batches: progress.batches, remaining: progress.remainingSources })}</p>
|
||||
{!!progress.omittedSources && <p className="supervisor-activity__note">{t('activity.omittedSources', { count: progress.omittedSources })}</p>}
|
||||
{!!progress.revisedSources && <p className="supervisor-activity__note">{t('activity.revisedSources', { count: progress.revisedSources })}</p>}
|
||||
{!!progress.skippedSources && <p className="supervisor-activity__note">{t('activity.skippedSources', { count: progress.skippedSources })}</p>}
|
||||
{!progress.complete && !progress.phase && <p className="supervisor-activity__note">{t('activity.stageUnknown')}</p>}
|
||||
{row.supervisionStatus === 'running' && !stopping && <p className="supervisor-activity__note">{t('reviewSettings.inFlight', { count: progress.inFlight ?? 0 })}</p>}
|
||||
</div>
|
||||
|
||||
@@ -229,7 +229,7 @@ export function SupervisorActivity({ active, projects, onOpenResult, configId, c
|
||||
</dl>
|
||||
{row.reviewProgress && <p>{t('reviewSettings.progress', { batches: row.reviewProgress.batches, characters: row.reviewProgress.characters, remaining: row.reviewProgress.remainingSources })}</p>}
|
||||
{!!row.reviewProgress?.omittedSources && <p>{t('activity.omittedSources', { count: row.reviewProgress.omittedSources })}</p>}
|
||||
{!!row.reviewProgress?.revisedSources && <p>{t('activity.revisedSources', { count: row.reviewProgress.revisedSources })}</p>}
|
||||
{!!row.reviewProgress?.skippedSources && <p>{t('activity.skippedSources', { count: row.reviewProgress.skippedSources })}</p>}
|
||||
{row.reviewProgress?.navigationNodes !== undefined && <p>{t('activity.navigationSaved', { count: row.reviewProgress.navigationNodes })}</p>}
|
||||
{row.summary && <p className="supervisor-activity__summary">{row.summary}</p>}
|
||||
{row.reviewProgress?.settings && <details><summary>{t('reviewSettings.configuration')}</summary>
|
||||
|
||||
@@ -225,7 +225,7 @@ describe('SupervisorWorkspace', () => {
|
||||
vi.setSystemTime(now)
|
||||
fireEvent.click(screen.getByRole('button', { name: copy.newReview }))
|
||||
const picker = screen.getByLabelText(copy.period)
|
||||
expect(within(picker).getAllByRole('option').map(option => (option as HTMLOptionElement).value)).toEqual(['1', '7', '30', 'custom', 'range'])
|
||||
expect(within(picker).getAllByRole('option').map(option => (option as HTMLOptionElement).value)).toEqual(['1', '7', '30', 'all', 'custom', 'range'])
|
||||
expect(picker).toHaveValue('7')
|
||||
fireEvent.change(picker, { target: { value: 'custom' } })
|
||||
fireEvent.change(screen.getByRole('spinbutton', { name: copy.dayCount }), { target: { value: '45' } })
|
||||
@@ -245,6 +245,29 @@ describe('SupervisorWorkspace', () => {
|
||||
timeRange: { from: new Date(now.getTime() - 46 * 86400_000).toISOString(), to: now.toISOString() } })
|
||||
})
|
||||
|
||||
it.each(['zh-CN', 'en-US'] as const)('reviews all time from the epoch through now and names it in the reanalysis dialog in %s', async locale => {
|
||||
await changeUiLocale(locale)
|
||||
const copy = i18nResources[locale].heartbeat.supervisor
|
||||
const run = vi.fn().mockResolvedValue(undefined)
|
||||
window.goodbuddy = { supervision: { overview: async () => [], run } } as never
|
||||
render(<SupervisorWorkspace />)
|
||||
await screen.findByText(copy.empty)
|
||||
vi.useFakeTimers({ toFake: ['Date'] })
|
||||
const now = new Date(2026, 9, 5, 15, 30)
|
||||
vi.setSystemTime(now)
|
||||
fireEvent.click(screen.getByRole('button', { name: copy.newReview }))
|
||||
fireEvent.change(screen.getByLabelText(copy.period), { target: { value: 'all' } })
|
||||
const timeRange = { from: new Date(0).toISOString(), to: now.toISOString() }
|
||||
await act(async () => fireEvent.click(screen.getByRole('button', { name: copy.run })))
|
||||
expect(run).toHaveBeenLastCalledWith({ trigger: 'manual', scope: { kind: 'global' }, timeRange })
|
||||
fireEvent.click(screen.getByRole('button', { name: copy.more }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: copy.reanalyze }))
|
||||
const dialog = screen.getByRole('alertdialog')
|
||||
expect(dialog).toHaveTextContent(copy.allTimeLabel)
|
||||
await act(async () => fireEvent.click(within(dialog).getByRole('button', { name: copy.reanalyzeConfirm })))
|
||||
expect(run).toHaveBeenLastCalledWith({ trigger: 'manual', reanalyze: true, scope: { kind: 'global' }, timeRange })
|
||||
})
|
||||
|
||||
it.each([
|
||||
['2025-11-02', '2025-11-02'],
|
||||
['2026-03-08', '2026-03-08'],
|
||||
|
||||
@@ -240,6 +240,8 @@ export function SupervisorWorkspace({
|
||||
const loading = overviewLoading || (tab === 'graph' && Boolean(resultId) && graphResultId !== resultId && !loadError)
|
||||
|
||||
const resolvePeriod = (now: Date): { range?: SupervisionRunRequest['timeRange']; label?: string; error?: string } => {
|
||||
// All time: from the epoch, which predates any stored source, through now.
|
||||
if (period === 'all') return { range: { from: new Date(0).toISOString(), to: now.toISOString() }, label: t('supervisor.allTimeLabel') }
|
||||
if (period === 'range') {
|
||||
const from = new Date(`${startDate}T00:00:00`)
|
||||
const end = new Date(`${endDate}T00:00:00`)
|
||||
@@ -651,6 +653,7 @@ export function SupervisorWorkspace({
|
||||
{t('supervisor.days', { count: value })}
|
||||
</option>
|
||||
))}
|
||||
<option value="all">{t('supervisor.allTime')}</option>
|
||||
<option value="custom">{t('supervisor.customDays')}</option>
|
||||
<option value="range">{t('supervisor.dateRange')}</option>
|
||||
</select>
|
||||
|
||||
@@ -64,7 +64,7 @@ export const heartbeat = {
|
||||
stageUnknown: 'This historical run has no saved stage. Zero remaining sources does not mean the review was published.',
|
||||
coverage: '{{batches}} saved batches · {{remaining}} sources remaining',
|
||||
omittedSources: '{{count}} sources skipped because their conversations were deleted. Coverage includes only retained sources. If none remain, the review finishes without an empty report.',
|
||||
revisedSources: '{{count}} sources changed during the review and were reviewed again at their latest version; batches of older versions were discarded.',
|
||||
skippedSources: '{{count}} sources changed during the review. They were skipped this time and will be handled in the next review.',
|
||||
navigationSaved: '{{count}} navigation merges saved',
|
||||
resumeMerge: 'Continue merging', resumeSave: 'Retry publication', runDetails: 'Run details and configuration',
|
||||
treePageHint: 'Expand projects and conversations to browse this page of saved batches. Tasks without a saved result are not listed here.',
|
||||
@@ -123,6 +123,7 @@ export const heartbeat = {
|
||||
empty: 'No successful review yet', emptyHint: 'You can review current progress manually even without an heartbeat plan.', run: 'Review', running: 'Reviewing…',
|
||||
more: 'More review actions', reanalyze: 'Reanalyze…', reanalyzeTitle: 'Reanalyze this period?', reanalyzeConfirm: 'Reanalyze',
|
||||
reanalyzeHint: 'All content in {{scope}} for {{period}} will be analyzed again, which may use more model tokens. Confirmed content and heartbeat progress are not affected.', retryRun: 'Retry review', dismiss: 'Dismiss',
|
||||
allTime: 'All time', allTimeLabel: 'all time',
|
||||
customDays: 'Custom days', dateRange: 'Date range', dayCount: 'Number of days', startDate: 'Start date', endDate: 'End date',
|
||||
recentDays: 'the last {{count}} days', dateRangeLabel: '{{from}} through {{to}} (local dates, through now if ending today)',
|
||||
customDaysHelp: 'Counts back from now in 24-hour days.', dateRangeHelp: 'Local dates, including the end day. If ending today, includes content through now.',
|
||||
|
||||
@@ -61,7 +61,7 @@ export const heartbeat = {
|
||||
stageUnknown: '此历史运行未保存阶段信息。剩余来源为零不代表回顾已发布。',
|
||||
coverage: '已保存 {{batches}} 批 · 剩余 {{remaining}} 个来源',
|
||||
omittedSources: '会话已删除,已跳过 {{count}} 个来源;覆盖统计仅包含保留的来源。没有剩余来源时结束回顾,不生成空报告。',
|
||||
revisedSources: '回顾期间有 {{count}} 个来源被修改,已按最新内容重新回顾;旧版本的批次已丢弃。',
|
||||
skippedSources: '回顾期间有 {{count}} 个来源发生变化,本次已跳过,将在下次回顾中处理。',
|
||||
navigationSaved: '已保存 {{count}} 次导航合并',
|
||||
resumeMerge: '继续合并摘要', resumeSave: '重试发布', runDetails: '运行详情与配置',
|
||||
treePageHint: '按项目、会话展开本页已保存批次。这里不包含尚未保存结果的任务。',
|
||||
@@ -120,6 +120,7 @@ export const heartbeat = {
|
||||
empty: '还没有成功回顾', emptyHint: '即使没有心跳计划,也可以手动回顾当前进展。', run: '回顾', running: '回顾整理中…',
|
||||
more: '更多回顾操作', reanalyze: '重新整理…', reanalyzeTitle: '重新整理这段时间?', reanalyzeConfirm: '重新整理',
|
||||
reanalyzeHint: '将重新分析{{scope}}在{{period}}内的全部内容,可能产生较多模型用量。已确认的内容和心跳处理进度不受影响。', retryRun: '重试回顾', dismiss: '关闭提示',
|
||||
allTime: '全部', allTimeLabel: '全部时间',
|
||||
customDays: '自定义天数', dateRange: '指定日期', dayCount: '天数', startDate: '开始日期', endDate: '结束日期',
|
||||
recentDays: '最近 {{count}} 天', dateRangeLabel: '{{from}} 至 {{to}}(本地日期,结束日期为今天时截至当前)',
|
||||
customDaysHelp: '从当前时刻向前计算,每天按 24 小时计。', dateRangeHelp: '按本地日期计算,包含结束日期当天;结束日期为今天时截至当前时刻。',
|
||||
|
||||
Reference in New Issue
Block a user