diff --git a/ui/src/App.tsx b/ui/src/App.tsx index a268f84..4077755 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -13,6 +13,7 @@ import { SetupWizard } from './components/SetupWizard' import { AddFeatureForm } from './components/AddFeatureForm' import { FeatureModal } from './components/FeatureModal' import { DebugLogViewer } from './components/DebugLogViewer' +import { AgentThought } from './components/AgentThought' import { Plus, Loader2 } from 'lucide-react' import type { Feature } from './lib/types' @@ -182,6 +183,12 @@ function App() { isConnected={wsState.isConnected} /> + {/* Agent Thought - shows latest agent narrative */} + + {/* Initializing Features State - show when agent is running but no features yet */} {features && features.pending.length === 0 && diff --git a/ui/src/components/AgentThought.tsx b/ui/src/components/AgentThought.tsx new file mode 100644 index 0000000..7b46472 --- /dev/null +++ b/ui/src/components/AgentThought.tsx @@ -0,0 +1,157 @@ +import { useMemo, useState, useEffect } from 'react' +import { Brain, Sparkles } from 'lucide-react' +import type { AgentStatus } from '../lib/types' + +interface AgentThoughtProps { + logs: Array<{ line: string; timestamp: string }> + agentStatus: AgentStatus +} + +const IDLE_TIMEOUT = 30000 // 30 seconds + +/** + * Determines if a log line is an agent "thought" (narrative text) + * vs. tool mechanics that should be hidden + */ +function isAgentThought(line: string): boolean { + const trimmed = line.trim() + + // Skip tool mechanics + if (/^\[Tool:/.test(trimmed)) return false + if (/^\s*Input:\s*\{/.test(trimmed)) return false + if (/^\[(Done|Error)\]/.test(trimmed)) return false + if (/^\[Error\]/.test(trimmed)) return false + if (/^Output:/.test(trimmed)) return false + + // Skip JSON and very short lines + if (/^[\[\{]/.test(trimmed)) return false + if (trimmed.length < 15) return false + + // Skip lines that are just paths or technical output + if (/^[A-Za-z]:\\/.test(trimmed)) return false + if (/^\/[a-z]/.test(trimmed)) return false + + // Keep narrative text (starts with capital, looks like a sentence) + return /^[A-Z]/.test(trimmed) && trimmed.length > 20 +} + +/** + * Extracts the latest agent thought from logs + */ +function getLatestThought(logs: Array<{ line: string; timestamp: string }>): string | null { + // Search from most recent + for (let i = logs.length - 1; i >= 0; i--) { + if (isAgentThought(logs[i].line)) { + return logs[i].line.trim() + } + } + return null +} + +export function AgentThought({ logs, agentStatus }: AgentThoughtProps) { + const thought = useMemo(() => getLatestThought(logs), [logs]) + const [displayedThought, setDisplayedThought] = useState(null) + const [textVisible, setTextVisible] = useState(true) + const [isVisible, setIsVisible] = useState(false) + + // Get last log timestamp for idle detection + const lastLogTimestamp = logs.length > 0 + ? new Date(logs[logs.length - 1].timestamp).getTime() + : 0 + + // Determine if component should be visible + const shouldShow = useMemo(() => { + if (!thought) return false + if (agentStatus === 'running') return true + if (agentStatus === 'paused') { + return Date.now() - lastLogTimestamp < IDLE_TIMEOUT + } + return false + }, [thought, agentStatus, lastLogTimestamp]) + + // Animate text changes using CSS transitions + useEffect(() => { + if (thought !== displayedThought && thought) { + // Fade out + setTextVisible(false) + // After fade out, update text and fade in + const timeout = setTimeout(() => { + setDisplayedThought(thought) + setTextVisible(true) + }, 150) // Match transition duration + return () => clearTimeout(timeout) + } + }, [thought, displayedThought]) + + // Handle visibility transitions + useEffect(() => { + if (shouldShow) { + setIsVisible(true) + } else { + // Delay hiding to allow exit animation + const timeout = setTimeout(() => setIsVisible(false), 300) + return () => clearTimeout(timeout) + } + }, [shouldShow]) + + if (!isVisible || !displayedThought) return null + + const isRunning = agentStatus === 'running' + + return ( +
+
+ {/* Brain Icon with subtle glow */} +
+ + {isRunning && ( + + )} +
+ + {/* Thought text with fade transition */} +

+ {displayedThought?.replace(/:$/, '')} +

+ + {/* Subtle running indicator bar */} + {isRunning && ( +
+
+
+ )} +
+
+ ) +} diff --git a/ui/src/styles/globals.css b/ui/src/styles/globals.css index 51f88fe..2505ba5 100644 --- a/ui/src/styles/globals.css +++ b/ui/src/styles/globals.css @@ -332,6 +332,19 @@ } } +@keyframes thoughtFade { + 0% { + opacity: 0; + transform: translateY(-4px); + filter: blur(2px); + } + 100% { + opacity: 1; + transform: translateY(0); + filter: blur(0); + } +} + /* ============================================================================ Utilities Layer ============================================================================ */ @@ -353,6 +366,10 @@ animation: completePop 0.5s var(--transition-neo-fast); } + .animate-thought-fade { + animation: thoughtFade 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) backwards; + } + .font-display { font-family: var(--font-neo-display); } diff --git a/ui/tsconfig.tsbuildinfo b/ui/tsconfig.tsbuildinfo index 6792149..4f25603 100644 --- a/ui/tsconfig.tsbuildinfo +++ b/ui/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/addfeatureform.tsx","./src/components/agentcontrol.tsx","./src/components/chatmessage.tsx","./src/components/debuglogviewer.tsx","./src/components/featurecard.tsx","./src/components/featuremodal.tsx","./src/components/kanbanboard.tsx","./src/components/kanbancolumn.tsx","./src/components/newprojectmodal.tsx","./src/components/progressdashboard.tsx","./src/components/projectselector.tsx","./src/components/questionoptions.tsx","./src/components/setupwizard.tsx","./src/components/speccreationchat.tsx","./src/components/typingindicator.tsx","./src/hooks/usecelebration.ts","./src/hooks/usefeaturesound.ts","./src/hooks/useprojects.ts","./src/hooks/usespecchat.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/types.ts"],"version":"5.6.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/addfeatureform.tsx","./src/components/agentcontrol.tsx","./src/components/agentthought.tsx","./src/components/chatmessage.tsx","./src/components/debuglogviewer.tsx","./src/components/featurecard.tsx","./src/components/featuremodal.tsx","./src/components/kanbanboard.tsx","./src/components/kanbancolumn.tsx","./src/components/newprojectmodal.tsx","./src/components/progressdashboard.tsx","./src/components/projectselector.tsx","./src/components/questionoptions.tsx","./src/components/setupwizard.tsx","./src/components/speccreationchat.tsx","./src/components/typingindicator.tsx","./src/hooks/usecelebration.ts","./src/hooks/usefeaturesound.ts","./src/hooks/useprojects.ts","./src/hooks/usespecchat.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/types.ts"],"version":"5.6.3"} \ No newline at end of file