From 55f1d3737379aa76cdff4cb6c97f7b3045331ee3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 14:50:29 +0000 Subject: [PATCH] bun_client: add a Re-run button for executed plugins Plugins that finished executing now show a Re-run button to the left of their status indicator, in both the review drawer (PluginsPanel) and the standalone plugin output view. It calls the same RerunPlugins RPC the Execute button already uses, scoped to that one plugin. Deferred plugins keep their Execute button and get no Re-run, since they have never run; plugins still pending are skipped too, as they are in flight. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01AVBMnBAUNMCYhR1pWNsxEb --- .../frontend/src/components/PluginOutput.tsx | 18 ++++- .../src/components/review/PluginsPanel.tsx | 79 ++++++++++++------- bun_client/frontend/src/plugin_utils.test.ts | 21 +++++ bun_client/frontend/src/plugin_utils.ts | 13 +++ 4 files changed, 100 insertions(+), 31 deletions(-) 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);