refactor: compact Progress card and merge agent thought into it

- Redesign ProgressDashboard from tall stacked layout to compact inline:
  title/badge left, passing/total right, progress bar with percentage below
- Absorb AgentThought functionality directly into ProgressDashboard,
  showing the agent's current thought below the progress bar
- Remove standalone AgentThought usage from App.tsx (component now unused)
- Pass logs/agentStatus to ProgressDashboard in single-agent mode only

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Auto
2026-02-01 14:57:33 +02:00
parent 24481d474d
commit e1e5209866
2 changed files with 126 additions and 62 deletions

View File

@@ -13,7 +13,6 @@ import { SetupWizard } from './components/SetupWizard'
import { AddFeatureForm } from './components/AddFeatureForm' import { AddFeatureForm } from './components/AddFeatureForm'
import { FeatureModal } from './components/FeatureModal' import { FeatureModal } from './components/FeatureModal'
import { DebugLogViewer, type TabType } from './components/DebugLogViewer' import { DebugLogViewer, type TabType } from './components/DebugLogViewer'
import { AgentThought } from './components/AgentThought'
import { AgentMissionControl } from './components/AgentMissionControl' import { AgentMissionControl } from './components/AgentMissionControl'
import { CelebrationOverlay } from './components/CelebrationOverlay' import { CelebrationOverlay } from './components/CelebrationOverlay'
import { AssistantFAB } from './components/AssistantFAB' import { AssistantFAB } from './components/AssistantFAB'
@@ -390,6 +389,8 @@ function App() {
total={progress.total} total={progress.total}
percentage={progress.percentage} percentage={progress.percentage}
isConnected={wsState.isConnected} isConnected={wsState.isConnected}
logs={wsState.activeAgents.length === 0 ? wsState.logs : undefined}
agentStatus={wsState.activeAgents.length === 0 ? wsState.agentStatus : undefined}
/> />
{/* Agent Mission Control - shows orchestrator status and active agents in parallel mode */} {/* Agent Mission Control - shows orchestrator status and active agents in parallel mode */}
@@ -400,13 +401,6 @@ function App() {
getAgentLogs={wsState.getAgentLogs} getAgentLogs={wsState.getAgentLogs}
/> />
{/* Agent Thought - shows latest agent narrative (single agent mode) */}
{wsState.activeAgents.length === 0 && (
<AgentThought
logs={wsState.logs}
agentStatus={wsState.agentStatus}
/>
)}
{/* Initializing Features State - show when agent is running but no features yet */} {/* Initializing Features State - show when agent is running but no features yet */}
{features && {features &&

View File

@@ -1,12 +1,40 @@
import { Wifi, WifiOff } from 'lucide-react' import { useMemo, useState, useEffect } from 'react'
import { Wifi, WifiOff, Brain, Sparkles } from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import type { AgentStatus } from '../lib/types'
interface ProgressDashboardProps { interface ProgressDashboardProps {
passing: number passing: number
total: number total: number
percentage: number percentage: number
isConnected: boolean isConnected: boolean
logs?: Array<{ line: string; timestamp: string }>
agentStatus?: AgentStatus
}
const IDLE_TIMEOUT = 30000
function isAgentThought(line: string): boolean {
const trimmed = line.trim()
if (/^\[Tool:/.test(trimmed)) return false
if (/^\s*Input:\s*\{/.test(trimmed)) return false
if (/^\[(Done|Error)\]/.test(trimmed)) return false
if (/^Output:/.test(trimmed)) return false
if (/^[[{]/.test(trimmed)) return false
if (trimmed.length < 10) return false
if (/^[A-Za-z]:\\/.test(trimmed)) return false
if (/^\/[a-z]/.test(trimmed)) return false
return true
}
function getLatestThought(logs: Array<{ line: string; timestamp: string }>): string | null {
for (let i = logs.length - 1; i >= 0; i--) {
if (isAgentThought(logs[i].line)) {
return logs[i].line.trim()
}
}
return null
} }
export function ProgressDashboard({ export function ProgressDashboard({
@@ -14,67 +42,109 @@ export function ProgressDashboard({
total, total,
percentage, percentage,
isConnected, isConnected,
logs = [],
agentStatus,
}: ProgressDashboardProps) { }: ProgressDashboardProps) {
const thought = useMemo(() => getLatestThought(logs), [logs])
const [displayedThought, setDisplayedThought] = useState<string | null>(null)
const [textVisible, setTextVisible] = useState(true)
const lastLogTimestamp = logs.length > 0
? new Date(logs[logs.length - 1].timestamp).getTime()
: 0
const showThought = useMemo(() => {
if (!thought) return false
if (agentStatus === 'running') return true
if (agentStatus === 'paused') {
return Date.now() - lastLogTimestamp < IDLE_TIMEOUT
}
return false
}, [thought, agentStatus, lastLogTimestamp])
useEffect(() => {
if (thought !== displayedThought && thought) {
setTextVisible(false)
const timeout = setTimeout(() => {
setDisplayedThought(thought)
setTextVisible(true)
}, 150)
return () => clearTimeout(timeout)
}
}, [thought, displayedThought])
const isRunning = agentStatus === 'running'
return ( return (
<Card> <Card>
<CardHeader className="flex-row items-center justify-between space-y-0 pb-4"> <CardHeader className="flex-row items-center justify-between space-y-0 pb-0">
<CardTitle className="text-xl uppercase tracking-wide"> <div className="flex items-center gap-3">
Progress <CardTitle className="text-xl uppercase tracking-wide">
</CardTitle> Progress
<Badge variant={isConnected ? 'default' : 'destructive'} className="gap-1"> </CardTitle>
{isConnected ? ( <Badge variant={isConnected ? 'default' : 'destructive'} className="gap-1">
<> {isConnected ? (
<Wifi size={14} /> <>
Live <Wifi size={14} />
</> Live
) : ( </>
<> ) : (
<WifiOff size={14} /> <>
Offline <WifiOff size={14} />
</> Offline
)} </>
</Badge> )}
</Badge>
</div>
<div className="flex items-baseline gap-1">
<span className="font-mono text-lg font-bold text-primary">
{passing}
</span>
<span className="text-sm text-muted-foreground">/</span>
<span className="font-mono text-lg font-bold">
{total}
</span>
</div>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="pt-3 pb-3">
{/* Large Percentage */} <div className="flex items-center gap-4">
<div className="text-center mb-6"> {/* Progress Bar */}
<span className="inline-flex items-baseline"> <div className="h-2.5 bg-muted rounded-full overflow-hidden flex-1">
<span className="text-6xl font-bold tabular-nums"> <div
{percentage.toFixed(1)} className="h-full bg-primary rounded-full transition-all duration-500 ease-out"
</span> style={{ width: `${percentage}%` }}
<span className="text-3xl font-semibold text-muted-foreground"> />
% </div>
</span> {/* Percentage */}
<span className="text-sm font-bold tabular-nums text-muted-foreground w-12 text-right">
{percentage.toFixed(1)}%
</span> </span>
</div> </div>
{/* Progress Bar */} {/* Agent Thought */}
<div className="h-3 bg-muted rounded-full overflow-hidden mb-6"> <div
<div className={`
className="h-full bg-primary rounded-full transition-all duration-500 ease-out" transition-all duration-300 ease-out overflow-hidden
style={{ width: `${percentage}%` }} ${showThought && displayedThought ? 'opacity-100 max-h-10 mt-3' : 'opacity-0 max-h-0 mt-0'}
/> `}
</div> >
<div className="flex items-center gap-2">
{/* Stats */} <div className="relative shrink-0">
<div className="flex justify-center gap-8 text-center"> <Brain size={16} className="text-primary" strokeWidth={2.5} />
<div> {isRunning && (
<span className="font-mono text-3xl font-bold text-primary"> <Sparkles size={8} className="absolute -top-1 -right-1 text-yellow-500 animate-pulse" />
{passing} )}
</span> </div>
<span className="block text-sm text-muted-foreground uppercase"> <p
Passing className="font-mono text-sm truncate text-muted-foreground transition-all duration-150 ease-out"
</span> style={{
</div> opacity: textVisible ? 1 : 0,
<div className="text-4xl text-muted-foreground">/</div> transform: textVisible ? 'translateY(0)' : 'translateY(-4px)',
<div> }}
<span className="font-mono text-3xl font-bold"> >
{total} {displayedThought?.replace(/:$/, '')}
</span> </p>
<span className="block text-sm text-muted-foreground uppercase">
Total
</span>
</div> </div>
</div> </div>
</CardContent> </CardContent>