diff --git a/bun_client/frontend/src/components/PluginOutput.tsx b/bun_client/frontend/src/components/PluginOutput.tsx index fd28508..62fb8e3 100644 --- a/bun_client/frontend/src/components/PluginOutput.tsx +++ b/bun_client/frontend/src/components/PluginOutput.tsx @@ -1,7 +1,12 @@ import { useState, useEffect } from 'react'; import { rpcCall } from '../api'; import { Button, Badge, Card, mapStatusToVariant, Theme } from '../design'; -import { resolvePluginBody, sortedAnnotations, type PluginResult } from '../plugin_utils'; +import { + canRerunPlugin, + resolvePluginBody, + sortedAnnotations, + type PluginResult, +} from '../plugin_utils'; import PluginAnnotations from './PluginAnnotations'; import PluginBodyView from './PluginBodyView'; @@ -159,6 +164,17 @@ export default function PluginOutput({ gap: '10px', }} > + {canRerunPlugin(plugin) && ( + + )} {plugin.status} diff --git a/bun_client/frontend/src/components/review/PluginsPanel.tsx b/bun_client/frontend/src/components/review/PluginsPanel.tsx index a9ade61..9b84942 100644 --- a/bun_client/frontend/src/components/review/PluginsPanel.tsx +++ b/bun_client/frontend/src/components/review/PluginsPanel.tsx @@ -1,5 +1,5 @@ import { Button, colors, shadows } from '../../design'; -import { resolvePluginBody, sortedAnnotations } from '../../plugin_utils'; +import { canRerunPlugin, resolvePluginBody, sortedAnnotations } from '../../plugin_utils'; import PluginAnnotations from '../PluginAnnotations'; import PluginBodyView from '../PluginBodyView'; import type { PluginResult } from './types'; @@ -115,39 +115,58 @@ export default function PluginsPanel({ {name} - - {data.status.toUpperCase()} - - {data.status.toLowerCase() === 'deferred' && ( - + )} + - Execute - - )} + {data.status.toUpperCase()} + + {data.status.toLowerCase() === 'deferred' && ( + + )} +
{ }); }); +describe('canRerunPlugin', () => { + test('offers a re-run for plugins that finished executing', () => { + expect(canRerunPlugin({ result: 'summary', status: 'success' })).toBe(true); + expect(canRerunPlugin({ result: 'Error: boom', status: 'error' })).toBe(true); + }); + + test('does not offer a re-run for a deferred plugin that never ran', () => { + expect(canRerunPlugin({ result: '', status: 'deferred' })).toBe(false); + expect(canRerunPlugin({ result: '', status: 'DEFERRED' })).toBe(false); + }); + + test('does not offer a re-run while a plugin is still in flight', () => { + expect(canRerunPlugin({ result: '', status: 'pending' })).toBe(false); + }); + + test('treats a missing status as not re-runnable', () => { + expect(canRerunPlugin({ result: '', status: '' })).toBe(false); + }); +}); + describe('totalAnnotationCount', () => { test('sums annotations across plugins and ignores plugins without any', () => { const plugins: Record = { diff --git a/bun_client/frontend/src/plugin_utils.ts b/bun_client/frontend/src/plugin_utils.ts index 968aada..e78a5bd 100644 --- a/bun_client/frontend/src/plugin_utils.ts +++ b/bun_client/frontend/src/plugin_utils.ts @@ -90,6 +90,19 @@ export function annotationSeverityVariant(severity: string): StatusVariant { } } +/** + * Whether a plugin can be re-run from the UI. + * + * Only plugins that have finished executing offer a re-run: a `deferred` + * plugin has never run (it gets an Execute button instead) and a `pending` + * one is still in flight. Everything else — success, error, or a status a + * newer server invents — has a result on screen worth refreshing. + */ +export function canRerunPlugin(plugin: PluginResult): boolean { + const status = (plugin.status || '').toLowerCase(); + return status !== '' && status !== 'deferred' && status !== 'pending'; +} + /** Total number of annotations across every plugin, for summary counts. */ export function totalAnnotationCount(plugins: Record): number { return Object.values(plugins).reduce((sum, p) => sum + (p.annotations?.length ?? 0), 0);