Files
Mike Swanson 75ce1c2fd5 feat: Add Sequential Thinking to Code Review + Frontend Validation
Enhanced code review and frontend validation with intelligent triggers:

Code Review Agent Enhancement:
- Added Sequential Thinking MCP integration for complex issues
- Triggers on 2+ rejections or 3+ critical issues
- New escalation format with root cause analysis
- Comprehensive solution strategies with trade-off evaluation
- Educational feedback to break rejection cycles
- Files: .claude/agents/code-review.md (+308 lines)
- Docs: CODE_REVIEW_ST_ENHANCEMENT.md, CODE_REVIEW_ST_TESTING.md

Frontend Design Skill Enhancement:
- Automatic invocation for ANY UI change
- Comprehensive validation checklist (200+ checkpoints)
- 8 validation categories (visual, interactive, responsive, a11y, etc.)
- 3 validation levels (quick, standard, comprehensive)
- Integration with code review workflow
- Files: .claude/skills/frontend-design/SKILL.md (+120 lines)
- Docs: UI_VALIDATION_CHECKLIST.md (462 lines), AUTOMATIC_VALIDATION_ENHANCEMENT.md (587 lines)

Settings Optimization:
- Repaired .claude/settings.local.json (fixed m365 pattern)
- Reduced permissions from 49 to 33 (33% reduction)
- Removed duplicates, sorted alphabetically
- Created SETTINGS_PERMISSIONS.md documentation

Checkpoint Command Enhancement:
- Dual checkpoint system (git + database)
- Saves session context to API for cross-machine recall
- Includes git metadata in database context
- Files: .claude/commands/checkpoint.md (+139 lines)

Decision Rationale:
- Sequential Thinking MCP breaks rejection cycles by identifying root causes
- Automatic frontend validation catches UI issues before code review
- Dual checkpoints enable complete project memory across machines
- Settings optimization improves maintainability

Total: 1,200+ lines of documentation and enhancements

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 16:23:52 -07:00

366 lines
16 KiB
Plaintext

1→/**
2→ * TerminalPanel Component
3→ * =======================
4→ *
5→ * Real-time terminal display for agent output with timestamps, auto-scroll,
6→ * and color-coded log types.
7→ */
8→
9→import { useState, useEffect, useRef, useCallback } from 'react'
10→import { useWebSocket, type WebSocketMessage } from '../hooks/useWebSocket'
11→
12→export type LogType = 'info' | 'error' | 'tool' | 'permission' | 'system'
13→
14→export interface LogEntry {
15→ id: string
16→ timestamp: string
17→ agentId: string
18→ type: LogType
19→ content: string
20→}
21→
22→interface TerminalPanelProps {
23→ projectName: string
24→ agentId?: 'A' | 'B' | 'all'
25→ maxLines?: number
26→ showTimestamps?: boolean
27→ autoScroll?: boolean
28→ onLogEntry?: (entry: LogEntry) => void
29→}
30→
31→const LOG_TYPE_COLORS: Record<LogType, string> = {
32→ info: 'text-text-primary',
33→ error: 'text-error',
34→ tool: 'text-accent',
35→ permission: 'text-stalled',
36→ system: 'text-text-secondary',
37→}
38→
39→const LOG_TYPE_ICONS: Record<LogType, string> = {
40→ info: 'INFO',
41→ error: 'ERR ',
42→ tool: 'TOOL',
43→ permission: 'PERM',
44→ system: 'SYS ',
45→}
46→
47→export function TerminalPanel({
48→ projectName,
49→ agentId = 'all',
50→ maxLines = 1000,
51→ showTimestamps = true,
52→ autoScroll: initialAutoScroll = true,
53→ onLogEntry,
54→}: TerminalPanelProps) {
55→ const [logs, setLogs] = useState<LogEntry[]>([])
56→ const [autoScroll, setAutoScroll] = useState(initialAutoScroll)
57→ const [filter, setFilter] = useState<LogType | 'all'>('all')
58→ const [searchQuery, setSearchQuery] = useState('')
59→ const terminalRef = useRef<HTMLDivElement>(null)
60→ const bottomRef = useRef<HTMLDivElement>(null)
61→ const userScrolledRef = useRef(false)
62→
63→ // Generate unique ID for log entries
64→ const generateId = useCallback(() => {
65→ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
66→ }, [])
67→
68→ // Handle WebSocket messages
69→ const handleMessage = useCallback((message: WebSocketMessage) => {
70→ if (message.type === 'agent_log') {
71→ const logAgentId = message.agent_id as string
72→
73→ // Filter by agent if specified
74→ if (agentId !== 'all' && logAgentId !== agentId) {
75→ return
76→ }
77→
78→ const entry: LogEntry = {
79→ id: generateId(),
80→ timestamp: message.timestamp as string,
81→ agentId: logAgentId,
82→ type: (message.log_type as LogType) || 'info',
83→ content: message.content as string,
84→ }
85→
86→ setLogs((prev) => {
87→ const newLogs = [...prev, entry]
88→ // Trim to max lines
89→ if (newLogs.length > maxLines) {
90→ return newLogs.slice(-maxLines)
91→ }
92→ return newLogs
93→ })
94→
95→ onLogEntry?.(entry)
96→ } else if (message.type === 'connected') {
97→ // Add system message for connection
98→ const entry: LogEntry = {
99→ id: generateId(),
100→ timestamp: message.timestamp as string,
101→ agentId: 'SYS',
102→ type: 'system',
103→ content: `Connected to project "${projectName}"`,
104→ }
105→ setLogs((prev) => [...prev, entry])
106→ } else if (message.type === 'agent_status') {
107→ // Add system message for status changes
108→ const entry: LogEntry = {
109→ id: generateId(),
110→ timestamp: message.timestamp as string,
111→ agentId: message.agent_id as string,
112→ type: 'system',
113→ content: `Agent ${message.agent_id} status changed to ${message.status}`,
114→ }
115→ setLogs((prev) => [...prev, entry])
116→ }
117→ }, [agentId, maxLines, generateId, onLogEntry, projectName])
118→
119→ // WebSocket connection
120→ const { status: wsStatus, reconnectAttempts } = useWebSocket({
121→ projectName,
122→ onMessage: handleMessage,
123→ autoReconnect: true,
124→ maxReconnectAttempts: 0, // Infinite retries
125→ })
126→
127→ // Auto-scroll effect
128→ useEffect(() => {
129→ if (autoScroll && !userScrolledRef.current && bottomRef.current) {
130→ bottomRef.current.scrollIntoView({ behavior: 'smooth' })
131→ }
132→ }, [logs, autoScroll])
133→
134→ // Handle manual scroll
135→ const handleScroll = useCallback(() => {
136→ if (!terminalRef.current) return
137→
138→ const { scrollTop, scrollHeight, clientHeight } = terminalRef.current
139→ const isAtBottom = scrollHeight - scrollTop - clientHeight < 50
140→
141→ userScrolledRef.current = !isAtBottom
142→
143→ if (isAtBottom && !autoScroll) {
144→ setAutoScroll(true)
145→ }
146→ }, [autoScroll])
147→
148→ // Clear logs
149→ const clearLogs = useCallback(() => {
150→ setLogs([])
151→ }, [])
152→
153→ // Copy logs to clipboard
154→ const copyLogs = useCallback(() => {
155→ const text = filteredLogs
156→ .map((log) => {
157→ const timestamp = showTimestamps ? `[${formatTimestamp(log.timestamp)}] ` : ''
158→ return `${timestamp}[${log.agentId}] [${LOG_TYPE_ICONS[log.type]}] ${log.content}`
159→ })
160→ .join('\n')
161→
162→ navigator.clipboard.writeText(text).then(() => {
163→ // Could add a toast notification here
164→ console.log('Logs copied to clipboard')
165→ })
166→ }, [logs, showTimestamps])
167→
168→ // Format timestamp
169→ const formatTimestamp = (timestamp: string): string => {
170→ try {
171→ const date = new Date(timestamp)
172→ return date.toLocaleTimeString('en-US', {
173→ hour12: false,
174→ hour: '2-digit',
175→ minute: '2-digit',
176→ second: '2-digit',
177→ fractionalSecondDigits: 3,
178→ })
179→ } catch {
180→ return timestamp
181→ }
182→ }
183→
184→ // Filter logs
185→ const filteredLogs = logs.filter((log) => {
186→ if (filter !== 'all' && log.type !== filter) return false
187→ if (searchQuery && !log.content.toLowerCase().includes(searchQuery.toLowerCase())) return false
188→ return true
189→ })
190→
191→ // Connection status indicator
192→ const getStatusIndicator = () => {
193→ switch (wsStatus) {
194→ case 'connected':
195→ return { color: 'bg-success', text: 'Connected' }
196→ case 'connecting':
197→ return { color: 'bg-warning animate-pulse', text: 'Connecting...' }
198→ case 'reconnecting':
199→ return { color: 'bg-warning animate-pulse', text: `Reconnecting (${reconnectAttempts})...` }
200→ case 'disconnected':
201→ return { color: 'bg-error', text: 'Disconnected' }
202→ default:
203→ return { color: 'bg-text-secondary', text: 'Unknown' }
204→ }
205→ }
206→
207→ const statusIndicator = getStatusIndicator()
208→
209→ return (
210→ <div className="flex flex-col h-full bg-surface border border-border rounded-lg overflow-hidden" data-testid="terminal-panel">
211→ {/* Terminal Header */}
212→ <div className="flex items-center justify-between px-3 py-2 bg-surface-elevated border-b border-border">
213→ <div className="flex items-center gap-3">
214→ <div className="flex items-center gap-2">
215→ <div className={`w-2 h-2 rounded-full ${statusIndicator.color}`} />
216→ <span className="text-xs text-text-secondary">{statusIndicator.text}</span>
217→ </div>
218→ <span className="text-xs text-text-secondary">
219→ Agent: <span className="font-medium text-text-primary">{agentId === 'all' ? 'All' : agentId}</span>
220→ </span>
221→ <span className="text-xs text-text-secondary">
222→ Lines: <span className="font-medium text-text-primary">{filteredLogs.length}</span>
223→ </span>
224→ </div>
225→
226→ <div className="flex items-center gap-2">
227→ {/* Filter Dropdown */}
228→ <select
229→ value={filter}
230→ onChange={(e) => setFilter(e.target.value as LogType | 'all')}
231→ className="text-xs bg-surface border border-border rounded px-2 py-1 text-text-primary focus:outline-none focus:ring-1 focus:ring-accent"
232→ data-testid="terminal-filter"
233→ >
234→ <option value="all">All Types</option>
235→ <option value="info">Info</option>
236→ <option value="error">Errors</option>
237→ <option value="tool">Tools</option>
238→ <option value="permission">Permissions</option>
239→ <option value="system">System</option>
240→ </select>
241→
242→ {/* Search Input */}
243→ <div className="relative">
244→ <svg className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-text-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
245→ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
246→ </svg>
247→ <input
248→ type="text"
249→ value={searchQuery}
250→ onChange={(e) => setSearchQuery(e.target.value)}
251→ placeholder="Search..."
252→ className="text-xs bg-surface border border-border rounded pl-7 pr-2 py-1 w-32 text-text-primary placeholder-text-secondary focus:outline-none focus:ring-1 focus:ring-accent"
253→ data-testid="terminal-search"
254→ />
255→ </div>
256→
257→ {/* Auto-scroll Toggle */}
258→ <button
259→ onClick={() => {
260→ setAutoScroll(!autoScroll)
261→ userScrolledRef.current = false
262→ }}
263→ className={`text-xs px-2 py-1 rounded border transition-colors ${
264→ autoScroll
265→ ? 'bg-accent/20 border-accent text-accent'
266→ : 'border-border text-text-secondary hover:text-text-primary hover:border-text-secondary'
267→ }`}
268→ title={autoScroll ? 'Auto-scroll enabled' : 'Auto-scroll disabled'}
269→ data-testid="terminal-autoscroll"
270→ >
271→ <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
272→ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
273→ </svg>
274→ </button>
275→
276→ {/* Copy Button */}
277→ <button
278→ onClick={copyLogs}
279→ className="text-xs px-2 py-1 rounded border border-border text-text-secondary hover:text-text-primary hover:border-text-secondary transition-colors"
280→ title="Copy logs to clipboard"
281→ data-testid="terminal-copy"
282→ >
283→ <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
284→ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
285→ </svg>
286→ </button>
287→
288→ {/* Clear Button */}
289→ <button
290→ onClick={clearLogs}
291→ className="text-xs px-2 py-1 rounded border border-border text-text-secondary hover:text-error hover:border-error transition-colors"
292→ title="Clear terminal"
293→ data-testid="terminal-clear"
294→ >
295→ <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
296→ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
297→ </svg>
298→ </button>
299→ </div>
300→ </div>
301→
302→ {/* Terminal Content */}
303→ <div
304→ ref={terminalRef}
305→ onScroll={handleScroll}
306→ className="flex-1 overflow-y-auto font-mono text-xs leading-5 p-2 bg-[#0a0a0a]"
307→ data-testid="terminal-content"
308→ >
309→ {filteredLogs.length === 0 ? (
310→ <div className="flex items-center justify-center h-full text-text-secondary">
311→ <div className="text-center">
312→ <svg className="w-8 h-8 mx-auto mb-2 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
313→ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
314→ </svg>
315→ <p>No log entries yet</p>
316→ <p className="text-text-secondary/60 mt-1">Start an agent to see output here</p>
317→ </div>
318→ </div>
319→ ) : (
320→ filteredLogs.map((log) => (
321→ <div
322→ key={log.id}
323→ className="flex hover:bg-white/5 px-1 rounded"
324→ data-testid={`log-entry-${log.id}`}
325→ >
326→ {/* Timestamp */}
327→ {showTimestamps && (
328→ <span className="text-text-secondary/60 mr-2 select-none flex-shrink-0" data-testid="log-timestamp">
329→ [{formatTimestamp(log.timestamp)}]
330→ </span>
331→ )}
332→
333→ {/* Agent ID */}
334→ <span className={`mr-2 select-none flex-shrink-0 ${
335→ log.agentId === 'A' ? 'text-accent' :
336→ log.agentId === 'B' ? 'text-success' :
337→ 'text-text-secondary'
338→ }`}>
339→ [{log.agentId}]
340→ </span>
341→
342→ {/* Log Type */}
343→ <span className={`mr-2 select-none flex-shrink-0 ${LOG_TYPE_COLORS[log.type]}`}>
344→ [{LOG_TYPE_ICONS[log.type]}]
345→ </span>
346→
347→ {/* Content */}
348→ <span className={LOG_TYPE_COLORS[log.type]}>
349→ {log.content}
350→ </span>
351→ </div>
352→ ))
353→ )}
354→ <div ref={bottomRef} />
355→ </div>
356→ </div>
357→ )
358→}
359→
360→export default TerminalPanel
361→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>