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>
366 lines
16 KiB
Plaintext
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>
|