feat(bf-6b9f): sort workers by needle state + hide test workers toggle
WorkerGrid now sorts by WORKING > CLAIMING > SELECTING > BOOTING > CLOSING > EXHAUSTED_IDLE > STOPPED and filters out test workers (test-*, claude-test-*, nonexistent-*, needle-test, strand-runner, -test-worker) by default. App header gets a toggle button to show/hide test workers. EXHAUSTED_IDLE added to NeedleState type and propagated to WorkerDetail and WorkerGrid state maps. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
402e9340ad
commit
cffeb26e79
8 changed files with 179 additions and 4 deletions
16
.beads/traces/bf-6b9f/metadata.json
Normal file
16
.beads/traces/bf-6b9f/metadata.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"bead_id": "bf-6b9f",
|
||||
"agent": "echo-test",
|
||||
"provider": null,
|
||||
"model": null,
|
||||
"exit_code": 0,
|
||||
"outcome": "success",
|
||||
"duration_ms": 0,
|
||||
"input_tokens": null,
|
||||
"output_tokens": null,
|
||||
"cost_usd": null,
|
||||
"captured_at": "2026-05-15T20:49:31.040008412Z",
|
||||
"trace_format": "raw_text",
|
||||
"pruned": false,
|
||||
"template_version": null
|
||||
}
|
||||
0
.beads/traces/bf-6b9f/stderr.txt
Normal file
0
.beads/traces/bf-6b9f/stderr.txt
Normal file
1
.beads/traces/bf-6b9f/stdout.txt
Normal file
1
.beads/traces/bf-6b9f/stdout.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
done
|
||||
|
|
@ -258,6 +258,7 @@ const App: React.FC = () => {
|
|||
const [showGitIntegration, setShowGitIntegration] = useState(false);
|
||||
const [showNarrative, setShowNarrative] = useState(false);
|
||||
const [budgetBannerDismissed, setBudgetBannerDismissed] = useState(false);
|
||||
const [hideTestWorkers, setHideTestWorkers] = useState(true);
|
||||
|
||||
// Budget alert state polled from /api/cost/summary
|
||||
const [budgetSummary, setBudgetSummary] = useState<{
|
||||
|
|
@ -826,6 +827,14 @@ const App: React.FC = () => {
|
|||
<span className="narrative-toggle-icon">📝</span>
|
||||
<span className="narrative-toggle-label">Narrative</span>
|
||||
</button>
|
||||
<button
|
||||
className={`hide-test-workers-toggle ${hideTestWorkers ? 'active' : ''}`}
|
||||
onClick={() => setHideTestWorkers(prev => !prev)}
|
||||
title={hideTestWorkers ? 'Test workers hidden — click to show' : 'Test workers visible — click to hide'}
|
||||
>
|
||||
<span className="hide-test-workers-icon">🧪</span>
|
||||
<span className="hide-test-workers-label">{hideTestWorkers ? 'Hide Tests' : 'Show Tests'}</span>
|
||||
</button>
|
||||
{unacknowledgedAlertCount > 0 && (
|
||||
<button
|
||||
className="collision-alert-toggle"
|
||||
|
|
@ -872,6 +881,7 @@ const App: React.FC = () => {
|
|||
pinnedWorkers={pinnedWorkers}
|
||||
onTogglePin={togglePinWorker}
|
||||
focusModeEnabled={focusModeEnabled}
|
||||
hideTestWorkers={hideTestWorkers}
|
||||
/>
|
||||
|
||||
{showTimeline && (
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ const NEEDLE_STATE_ICONS: Record<NeedleState, string> = {
|
|||
CLAIMING: '🎯',
|
||||
WORKING: '●',
|
||||
CLOSING: '⏹',
|
||||
EXHAUSTED_IDLE: '💤',
|
||||
STOPPED: '○',
|
||||
};
|
||||
|
||||
|
|
@ -19,6 +20,7 @@ const NEEDLE_STATE_COLORS: Record<NeedleState, string> = {
|
|||
CLAIMING: '#9b59b6',
|
||||
WORKING: '#5cb85c',
|
||||
CLOSING: '#f0ad4e',
|
||||
EXHAUSTED_IDLE: '#95a5a6',
|
||||
STOPPED: '#777',
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ const NEEDLE_STATE_LABELS: Record<NeedleState, string> = {
|
|||
CLAIMING: 'CLAIMING',
|
||||
WORKING: 'WORKING',
|
||||
CLOSING: 'CLOSING',
|
||||
EXHAUSTED_IDLE: 'EXHAUSTED',
|
||||
STOPPED: 'STOPPED',
|
||||
};
|
||||
|
||||
|
|
@ -16,9 +17,41 @@ const NEEDLE_STATE_COLORS: Record<NeedleState, string> = {
|
|||
CLAIMING: '#9b59b6',
|
||||
WORKING: '#5cb85c',
|
||||
CLOSING: '#f0ad4e',
|
||||
EXHAUSTED_IDLE: '#95a5a6',
|
||||
STOPPED: '#777',
|
||||
};
|
||||
|
||||
// Lower number = higher priority (shown first)
|
||||
const NEEDLE_STATE_PRIORITY: Partial<Record<string, number>> = {
|
||||
WORKING: 0,
|
||||
CLAIMING: 1,
|
||||
SELECTING: 2,
|
||||
BOOTING: 3,
|
||||
CLOSING: 4,
|
||||
EXHAUSTED_IDLE: 5,
|
||||
STOPPED: 6,
|
||||
};
|
||||
|
||||
const TEST_WORKER_PATTERNS: RegExp[] = [
|
||||
/^test-/,
|
||||
/^claude-test-/,
|
||||
/^nonexistent-/,
|
||||
/^needle-test$/,
|
||||
/^strand-runner$/,
|
||||
/-test-worker$/,
|
||||
];
|
||||
|
||||
function isTestWorker(id: string): boolean {
|
||||
return TEST_WORKER_PATTERNS.some(pattern => pattern.test(id));
|
||||
}
|
||||
|
||||
function stateSort(a: WorkerInfo, b: WorkerInfo): number {
|
||||
const pa = a.needleState != null ? (NEEDLE_STATE_PRIORITY[a.needleState] ?? 7) : 7;
|
||||
const pb = b.needleState != null ? (NEEDLE_STATE_PRIORITY[b.needleState] ?? 7) : 7;
|
||||
if (pa !== pb) return pa - pb;
|
||||
return a.id.localeCompare(b.id);
|
||||
}
|
||||
|
||||
interface WorkerGridProps {
|
||||
workers: WorkerInfo[];
|
||||
selectedWorker: string | null;
|
||||
|
|
@ -26,6 +59,7 @@ interface WorkerGridProps {
|
|||
pinnedWorkers?: Set<string>;
|
||||
onTogglePin?: (workerId: string) => void;
|
||||
focusModeEnabled?: boolean;
|
||||
hideTestWorkers?: boolean;
|
||||
}
|
||||
|
||||
const WorkerGrid: React.FC<WorkerGridProps> = ({
|
||||
|
|
@ -35,6 +69,7 @@ const WorkerGrid: React.FC<WorkerGridProps> = ({
|
|||
pinnedWorkers = new Set(),
|
||||
onTogglePin,
|
||||
focusModeEnabled = false,
|
||||
hideTestWorkers = true,
|
||||
}) => {
|
||||
const formatLastSeen = (timestamp: string) => {
|
||||
const diff = Date.now() - new Date(timestamp).getTime();
|
||||
|
|
@ -47,16 +82,20 @@ const WorkerGrid: React.FC<WorkerGridProps> = ({
|
|||
};
|
||||
|
||||
const handlePinClick = (e: React.MouseEvent, workerId: string) => {
|
||||
e.stopPropagation(); // Prevent card selection when clicking pin
|
||||
e.stopPropagation();
|
||||
if (onTogglePin) {
|
||||
onTogglePin(workerId);
|
||||
}
|
||||
};
|
||||
|
||||
const visibleWorkers = [...workers]
|
||||
.filter(w => !hideTestWorkers || !isTestWorker(w.id))
|
||||
.sort(stateSort);
|
||||
|
||||
return (
|
||||
<div className="worker-grid">
|
||||
<h2>
|
||||
Workers ({workers.length})
|
||||
Workers ({visibleWorkers.length})
|
||||
{focusModeEnabled && pinnedWorkers.size > 0 && (
|
||||
<span style={{ marginLeft: '0.5rem', fontSize: '0.9rem', color: '#666' }}>
|
||||
(Focus: {pinnedWorkers.size} pinned)
|
||||
|
|
@ -64,7 +103,7 @@ const WorkerGrid: React.FC<WorkerGridProps> = ({
|
|||
)}
|
||||
</h2>
|
||||
|
||||
{workers.length === 0 ? (
|
||||
{visibleWorkers.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<p>{focusModeEnabled && pinnedWorkers.size === 0
|
||||
? 'No pinned workers. Pin workers to see them in Focus Mode.'
|
||||
|
|
@ -76,7 +115,7 @@ const WorkerGrid: React.FC<WorkerGridProps> = ({
|
|||
</p>
|
||||
</div>
|
||||
) : (
|
||||
workers.map(worker => {
|
||||
visibleWorkers.map(worker => {
|
||||
const isPinned = pinnedWorkers.has(worker.id);
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export type NeedleState =
|
|||
| 'CLAIMING'
|
||||
| 'WORKING'
|
||||
| 'CLOSING'
|
||||
| 'EXHAUSTED_IDLE'
|
||||
| 'STOPPED';
|
||||
|
||||
export interface LogEvent {
|
||||
|
|
|
|||
|
|
@ -520,4 +520,110 @@ describe('WorkerGrid', () => {
|
|||
expect(indicator).toHaveAttribute('title', 'File collision detected!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('test worker filtering', () => {
|
||||
const testWorkerIds = [
|
||||
'test-worker',
|
||||
'test-alpha',
|
||||
'claude-test-worker',
|
||||
'claude-test-foo',
|
||||
'nonexistent-worker',
|
||||
'nonexistent-abc',
|
||||
'needle-test',
|
||||
'strand-runner',
|
||||
'claude-interactive-charlie-test-worker',
|
||||
];
|
||||
|
||||
it.each(testWorkerIds)('should hide test worker "%s" by default', (id) => {
|
||||
const workers = [createMockWorker({ id })];
|
||||
render(
|
||||
<WorkerGrid workers={workers} selectedWorker={null} onSelectWorker={mockOnSelectWorker} />
|
||||
);
|
||||
expect(screen.queryByText(id)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each(testWorkerIds)('should show test worker "%s" when hideTestWorkers=false', (id) => {
|
||||
const workers = [createMockWorker({ id })];
|
||||
render(
|
||||
<WorkerGrid
|
||||
workers={workers}
|
||||
selectedWorker={null}
|
||||
onSelectWorker={mockOnSelectWorker}
|
||||
hideTestWorkers={false}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText(id)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show non-test workers by default', () => {
|
||||
const workers = [createMockWorker({ id: 'claude-interactive-alpha' })];
|
||||
render(
|
||||
<WorkerGrid workers={workers} selectedWorker={null} onSelectWorker={mockOnSelectWorker} />
|
||||
);
|
||||
expect(screen.getByText('claude-interactive-alpha')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should reflect filtered count in header', () => {
|
||||
const workers = [
|
||||
createMockWorker({ id: 'worker-real' }),
|
||||
createMockWorker({ id: 'test-hidden' }),
|
||||
];
|
||||
render(
|
||||
<WorkerGrid workers={workers} selectedWorker={null} onSelectWorker={mockOnSelectWorker} />
|
||||
);
|
||||
expect(screen.getByText('Workers (1)')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('worker sort order by needle state', () => {
|
||||
it('should sort WORKING before STOPPED', () => {
|
||||
const workers = [
|
||||
createMockWorker({ id: 'stopped-worker', needleState: 'STOPPED' }),
|
||||
createMockWorker({ id: 'working-worker', needleState: 'WORKING' }),
|
||||
];
|
||||
const { container } = render(
|
||||
<WorkerGrid workers={workers} selectedWorker={null} onSelectWorker={mockOnSelectWorker} />
|
||||
);
|
||||
const cards = container.querySelectorAll('.worker-id');
|
||||
expect(cards[0].textContent).toContain('working-worker');
|
||||
expect(cards[1].textContent).toContain('stopped-worker');
|
||||
});
|
||||
|
||||
it('should sort WORKING > CLAIMING > SELECTING > BOOTING > CLOSING > STOPPED', () => {
|
||||
const workers = [
|
||||
createMockWorker({ id: 'w-stopped', needleState: 'STOPPED' }),
|
||||
createMockWorker({ id: 'w-booting', needleState: 'BOOTING' }),
|
||||
createMockWorker({ id: 'w-claiming', needleState: 'CLAIMING' }),
|
||||
createMockWorker({ id: 'w-selecting', needleState: 'SELECTING' }),
|
||||
createMockWorker({ id: 'w-closing', needleState: 'CLOSING' }),
|
||||
createMockWorker({ id: 'w-working', needleState: 'WORKING' }),
|
||||
];
|
||||
const { container } = render(
|
||||
<WorkerGrid workers={workers} selectedWorker={null} onSelectWorker={mockOnSelectWorker} />
|
||||
);
|
||||
const cards = container.querySelectorAll('.worker-id');
|
||||
const order = Array.from(cards).map(c => c.textContent?.trim());
|
||||
expect(order).toEqual([
|
||||
'w-working',
|
||||
'w-claiming',
|
||||
'w-selecting',
|
||||
'w-booting',
|
||||
'w-closing',
|
||||
'w-stopped',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should sort workers without needle state after workers with states', () => {
|
||||
const workers = [
|
||||
createMockWorker({ id: 'no-state' }),
|
||||
createMockWorker({ id: 'w-working', needleState: 'WORKING' }),
|
||||
];
|
||||
const { container } = render(
|
||||
<WorkerGrid workers={workers} selectedWorker={null} onSelectWorker={mockOnSelectWorker} />
|
||||
);
|
||||
const cards = container.querySelectorAll('.worker-id');
|
||||
expect(cards[0].textContent).toContain('w-working');
|
||||
expect(cards[1].textContent).toContain('no-state');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue