Skip to content

Commit fe7c231

Browse files
committed
Merge branch 'feature/eslint-to-biome'
2 parents 6e27669 + 34b9fba commit fe7c231

3 files changed

Lines changed: 462 additions & 0 deletions

File tree

auto-claude-ui/biome.json

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
{
2+
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3+
"organizeImports": {
4+
"enabled": true
5+
},
6+
"linter": {
7+
"enabled": true,
8+
"rules": {
9+
"recommended": true,
10+
"correctness": {
11+
"noUnusedVariables": "warn",
12+
"noUnusedImports": "warn",
13+
"useHookAtTopLevel": "error",
14+
"useExhaustiveDependencies": "warn"
15+
},
16+
"suspicious": {
17+
"noExplicitAny": "warn",
18+
"noConsoleLog": "warn",
19+
"noAssignInExpressions": "off",
20+
"noArrayIndexKey": "warn",
21+
"noShadowRestrictedNames": "off"
22+
},
23+
"style": {
24+
"useConst": "warn",
25+
"noUnusedTemplateLiteral": "warn",
26+
"noNonNullAssertion": "off",
27+
"noParameterAssign": "off"
28+
},
29+
"complexity": {
30+
"noForEach": "off",
31+
"noBannedTypes": "off"
32+
},
33+
"a11y": {
34+
"useButtonType": "off",
35+
"useKeyWithClickEvents": "off",
36+
"noSvgWithoutTitle": "off",
37+
"useSemanticElements": "off",
38+
"noLabelWithoutControl": "off"
39+
},
40+
"security": {
41+
"noDangerouslySetInnerHtml": "warn"
42+
}
43+
}
44+
},
45+
"formatter": {
46+
"enabled": true,
47+
"indentStyle": "space",
48+
"indentWidth": 2,
49+
"lineWidth": 100
50+
},
51+
"javascript": {
52+
"formatter": {
53+
"quoteStyle": "single",
54+
"semicolons": "asNeeded",
55+
"trailingCommas": "es5"
56+
}
57+
},
58+
"overrides": [
59+
{
60+
"include": ["scripts/**"],
61+
"linter": {
62+
"rules": {
63+
"suspicious": {
64+
"noConsoleLog": "off"
65+
}
66+
}
67+
}
68+
}
69+
],
70+
"files": {
71+
"ignore": ["out/**", "dist/**", "node_modules/**", "*.config.js", "*.config.mjs"]
72+
}
73+
}
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
import {
2+
AlertCircle,
3+
AlertTriangle,
4+
Bug,
5+
ChevronDown,
6+
ChevronUp,
7+
Filter,
8+
Info,
9+
Terminal,
10+
Trash2,
11+
X,
12+
} from 'lucide-react'
13+
import { useEffect, useMemo, useRef, useState } from 'react'
14+
import { cn } from '../lib/utils'
15+
import { type ConsoleLogEntry, useConsoleStore } from '../stores/console-store'
16+
import { Button } from './ui/button'
17+
import {
18+
DropdownMenu,
19+
DropdownMenuCheckboxItem,
20+
DropdownMenuContent,
21+
DropdownMenuLabel,
22+
DropdownMenuSeparator,
23+
DropdownMenuTrigger,
24+
} from './ui/dropdown-menu'
25+
import { ScrollArea } from './ui/scroll-area'
26+
27+
const levelIcons = {
28+
info: Info,
29+
warn: AlertTriangle,
30+
error: AlertCircle,
31+
debug: Bug,
32+
}
33+
34+
const levelColors = {
35+
info: 'text-blue-500',
36+
warn: 'text-yellow-500',
37+
error: 'text-red-500',
38+
debug: 'text-gray-500',
39+
}
40+
41+
const sourceColors: Record<string, string> = {
42+
ideation: 'bg-purple-500/20 text-purple-400',
43+
roadmap: 'bg-green-500/20 text-green-400',
44+
changelog: 'bg-blue-500/20 text-blue-400',
45+
task: 'bg-orange-500/20 text-orange-400',
46+
system: 'bg-gray-500/20 text-gray-400',
47+
ipc: 'bg-cyan-500/20 text-cyan-400',
48+
}
49+
50+
function LogEntry({ log }: { log: ConsoleLogEntry }) {
51+
const [expanded, setExpanded] = useState(false)
52+
const Icon = levelIcons[log.level]
53+
54+
return (
55+
<div
56+
className={cn(
57+
'px-3 py-1.5 border-b border-border/50 hover:bg-muted/50 font-mono text-xs',
58+
log.level === 'error' && 'bg-red-500/5'
59+
)}
60+
>
61+
<div className="flex items-start gap-2">
62+
<Icon className={cn('h-3.5 w-3.5 mt-0.5 shrink-0', levelColors[log.level])} />
63+
<span className="text-muted-foreground shrink-0">{log.timestamp.toLocaleTimeString()}</span>
64+
<span
65+
className={cn(
66+
'px-1.5 py-0.5 rounded text-[10px] uppercase font-medium shrink-0',
67+
sourceColors[log.source] || 'bg-gray-500/20 text-gray-400'
68+
)}
69+
>
70+
{log.source}
71+
</span>
72+
<span className="flex-1 break-all">{log.message}</span>
73+
{log.details && (
74+
<Button
75+
variant="ghost"
76+
size="sm"
77+
className="h-5 w-5 p-0"
78+
onClick={() => setExpanded(!expanded)}
79+
>
80+
{expanded ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
81+
</Button>
82+
)}
83+
</div>
84+
{expanded && log.details && (
85+
<div className="mt-1 ml-6 p-2 bg-muted/50 rounded text-muted-foreground whitespace-pre-wrap">
86+
{log.details}
87+
</div>
88+
)}
89+
</div>
90+
)
91+
}
92+
93+
const ALL_SOURCES = ['ideation', 'roadmap', 'changelog', 'task', 'system', 'ipc'] as const
94+
95+
export function ConsoleLogsPanel() {
96+
const { logs, isOpen, filter, setOpen, clearLogs, setFilter } = useConsoleStore()
97+
const scrollRef = useRef<HTMLDivElement>(null)
98+
const [autoScroll, setAutoScroll] = useState(true)
99+
100+
// Filter logs in component with useMemo for performance
101+
const filteredLogs = useMemo(() => {
102+
return logs.filter((log) => {
103+
// Level filter
104+
if (!filter.level.includes(log.level)) return false
105+
// Source filter (empty = all sources)
106+
if (filter.source.length > 0 && !filter.source.includes(log.source)) return false
107+
return true
108+
})
109+
}, [logs, filter.level, filter.source])
110+
111+
// Auto-scroll to bottom when new logs arrive
112+
useEffect(() => {
113+
if (autoScroll && scrollRef.current) {
114+
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
115+
}
116+
}, [logs, autoScroll])
117+
118+
if (!isOpen) {
119+
return null
120+
}
121+
122+
const handleLevelToggle = (level: ConsoleLogEntry['level']) => {
123+
const currentLevels = filter.level
124+
if (currentLevels.includes(level)) {
125+
setFilter({ level: currentLevels.filter((l) => l !== level) })
126+
} else {
127+
setFilter({ level: [...currentLevels, level] })
128+
}
129+
}
130+
131+
const handleSourceToggle = (source: string) => {
132+
const currentSources = filter.source
133+
// When no sources are explicitly selected (= all sources implicitly selected),
134+
// clicking a source should explicitly select all EXCEPT that source
135+
if (currentSources.length === 0) {
136+
setFilter({ source: ALL_SOURCES.filter((s) => s !== source) })
137+
} else if (currentSources.includes(source)) {
138+
const newSources = currentSources.filter((s) => s !== source)
139+
// If removing this would leave no sources, reset to "all" (empty array)
140+
setFilter({ source: newSources.length === 0 ? [] : newSources })
141+
} else {
142+
// Adding a source - if this completes all sources, reset to empty (= all)
143+
const newSources = [...currentSources, source]
144+
setFilter({ source: newSources.length === ALL_SOURCES.length ? [] : newSources })
145+
}
146+
}
147+
148+
return (
149+
<div className="border-t border-border bg-card flex flex-col" style={{ height: '250px' }}>
150+
{/* Header */}
151+
<div className="flex items-center justify-between px-3 py-2 border-b border-border bg-muted/30">
152+
<div className="flex items-center gap-2">
153+
<Terminal className="h-4 w-4 text-muted-foreground" />
154+
<span className="text-sm font-medium">Console Logs</span>
155+
<span className="text-xs text-muted-foreground">
156+
({filteredLogs.length} / {logs.length})
157+
</span>
158+
</div>
159+
<div className="flex items-center gap-1">
160+
{/* Filter dropdown */}
161+
<DropdownMenu>
162+
<DropdownMenuTrigger asChild>
163+
<Button variant="ghost" size="sm" className="h-7 px-2">
164+
<Filter className="h-3.5 w-3.5 mr-1" />
165+
Filter
166+
</Button>
167+
</DropdownMenuTrigger>
168+
<DropdownMenuContent align="end" className="w-48">
169+
<DropdownMenuLabel>Log Levels</DropdownMenuLabel>
170+
{(['info', 'warn', 'error', 'debug'] as const).map((level) => (
171+
<DropdownMenuCheckboxItem
172+
key={level}
173+
checked={filter.level.includes(level)}
174+
onCheckedChange={() => handleLevelToggle(level)}
175+
>
176+
<span className={cn('capitalize', levelColors[level])}>{level}</span>
177+
</DropdownMenuCheckboxItem>
178+
))}
179+
<DropdownMenuSeparator />
180+
<DropdownMenuLabel>Sources</DropdownMenuLabel>
181+
{ALL_SOURCES.map((source) => (
182+
<DropdownMenuCheckboxItem
183+
key={source}
184+
checked={filter.source.length === 0 || filter.source.includes(source)}
185+
onCheckedChange={() => handleSourceToggle(source)}
186+
>
187+
<span className="capitalize">{source}</span>
188+
</DropdownMenuCheckboxItem>
189+
))}
190+
</DropdownMenuContent>
191+
</DropdownMenu>
192+
193+
{/* Auto-scroll toggle */}
194+
<Button
195+
variant={autoScroll ? 'secondary' : 'ghost'}
196+
size="sm"
197+
className="h-7 px-2"
198+
onClick={() => setAutoScroll(!autoScroll)}
199+
>
200+
{autoScroll ? 'Auto-scroll On' : 'Auto-scroll Off'}
201+
</Button>
202+
203+
{/* Clear logs */}
204+
<Button variant="ghost" size="sm" className="h-7 px-2" onClick={clearLogs}>
205+
<Trash2 className="h-3.5 w-3.5" />
206+
</Button>
207+
208+
{/* Close */}
209+
<Button variant="ghost" size="sm" className="h-7 w-7 p-0" onClick={() => setOpen(false)}>
210+
<X className="h-4 w-4" />
211+
</Button>
212+
</div>
213+
</div>
214+
215+
{/* Logs area */}
216+
<ScrollArea ref={scrollRef} className="flex-1">
217+
{filteredLogs.length === 0 ? (
218+
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
219+
No logs to display
220+
</div>
221+
) : (
222+
<div>
223+
{filteredLogs.map((log) => (
224+
<LogEntry key={log.id} log={log} />
225+
))}
226+
</div>
227+
)}
228+
</ScrollArea>
229+
</div>
230+
)
231+
}
232+
233+
// Toggle button for the sidebar
234+
export function ConsoleLogsToggle() {
235+
const { isOpen, toggleOpen, logs } = useConsoleStore()
236+
237+
// Optimize: count errors and warnings in single iteration
238+
const { errorCount, warnCount } = useMemo(() => {
239+
let errors = 0
240+
let warnings = 0
241+
for (const log of logs) {
242+
if (log.level === 'error') errors++
243+
else if (log.level === 'warn') warnings++
244+
}
245+
return { errorCount: errors, warnCount: warnings }
246+
}, [logs])
247+
248+
return (
249+
<Button
250+
variant={isOpen ? 'secondary' : 'ghost'}
251+
size="sm"
252+
className="justify-start gap-2 relative"
253+
onClick={toggleOpen}
254+
>
255+
<Terminal className="h-4 w-4" />
256+
Console
257+
{(errorCount > 0 || warnCount > 0) && (
258+
<span
259+
className={cn(
260+
'ml-auto text-xs px-1.5 py-0.5 rounded-full',
261+
errorCount > 0 ? 'bg-red-500/20 text-red-400' : 'bg-yellow-500/20 text-yellow-400'
262+
)}
263+
>
264+
{errorCount > 0 ? errorCount : warnCount}
265+
</span>
266+
)}
267+
</Button>
268+
)
269+
}

0 commit comments

Comments
 (0)