helixevo 0.2.37 → 0.2.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dashboard/app/api/browse/route.ts +66 -0
- package/dashboard/app/projects/client.tsx +166 -17
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to HelixEvo are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.2.39] - 2026-03-23
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Folder browser**: "Browse" button opens a folder picker modal
|
|
9
|
+
- Navigates the file system with clickable folders
|
|
10
|
+
- Projects auto-detected (📦 icon) by presence of package.json/README/etc
|
|
11
|
+
- Click a project folder to select it instantly
|
|
12
|
+
- "Up" button to navigate parent directories
|
|
13
|
+
- "Select This Folder" button for non-project directories
|
|
14
|
+
- Server-side `/api/browse` endpoint for directory listing
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- CSS border shorthand conflict warning in Projects page mode toggle
|
|
18
|
+
- Quick-select path buttons and default clone directory updated to ~/HelixEvo
|
|
19
|
+
|
|
5
20
|
## [0.2.37] - 2026-03-23
|
|
6
21
|
|
|
7
22
|
### Added
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { readdirSync, statSync, existsSync } from 'fs'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
import { homedir } from 'os'
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic'
|
|
7
|
+
|
|
8
|
+
function resolvePath(p: string): string {
|
|
9
|
+
if (p.startsWith('~')) return join(homedir(), p.slice(1))
|
|
10
|
+
return p
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function GET(request: Request) {
|
|
14
|
+
const url = new URL(request.url)
|
|
15
|
+
const dir = url.searchParams.get('dir') ?? homedir()
|
|
16
|
+
const resolved = resolvePath(dir)
|
|
17
|
+
|
|
18
|
+
if (!existsSync(resolved)) {
|
|
19
|
+
return NextResponse.json({ error: 'Directory not found', path: resolved }, { status: 404 })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const stat = statSync(resolved)
|
|
24
|
+
if (!stat.isDirectory()) {
|
|
25
|
+
return NextResponse.json({ error: 'Not a directory', path: resolved }, { status: 400 })
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const items = readdirSync(resolved, { withFileTypes: true })
|
|
29
|
+
.filter(d => !d.name.startsWith('.') && d.name !== 'node_modules' && d.name !== '__pycache__')
|
|
30
|
+
.sort((a, b) => {
|
|
31
|
+
// Directories first, then files
|
|
32
|
+
if (a.isDirectory() && !b.isDirectory()) return -1
|
|
33
|
+
if (!a.isDirectory() && b.isDirectory()) return 1
|
|
34
|
+
return a.name.localeCompare(b.name)
|
|
35
|
+
})
|
|
36
|
+
.slice(0, 50)
|
|
37
|
+
.map(d => ({
|
|
38
|
+
name: d.name,
|
|
39
|
+
isDirectory: d.isDirectory(),
|
|
40
|
+
// Check if it looks like a project (has package.json, README, etc.)
|
|
41
|
+
isProject: d.isDirectory() && (
|
|
42
|
+
existsSync(join(resolved, d.name, 'package.json')) ||
|
|
43
|
+
existsSync(join(resolved, d.name, 'README.md')) ||
|
|
44
|
+
existsSync(join(resolved, d.name, 'Cargo.toml')) ||
|
|
45
|
+
existsSync(join(resolved, d.name, 'go.mod')) ||
|
|
46
|
+
existsSync(join(resolved, d.name, 'pyproject.toml'))
|
|
47
|
+
),
|
|
48
|
+
}))
|
|
49
|
+
|
|
50
|
+
// Build display path (replace homedir with ~)
|
|
51
|
+
const displayPath = resolved.replace(homedir(), '~')
|
|
52
|
+
|
|
53
|
+
// Get parent directory
|
|
54
|
+
const parent = join(resolved, '..')
|
|
55
|
+
|
|
56
|
+
return NextResponse.json({
|
|
57
|
+
path: resolved,
|
|
58
|
+
displayPath,
|
|
59
|
+
parent: parent !== resolved ? parent : null,
|
|
60
|
+
items,
|
|
61
|
+
})
|
|
62
|
+
} catch (err: unknown) {
|
|
63
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
64
|
+
return NextResponse.json({ error: message }, { status: 500 })
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -30,16 +30,45 @@ const PIPELINE = [
|
|
|
30
30
|
},
|
|
31
31
|
]
|
|
32
32
|
|
|
33
|
+
interface BrowseItem { name: string; isDirectory: boolean; isProject: boolean }
|
|
34
|
+
interface BrowseResult { path: string; displayPath: string; parent: string | null; items: BrowseItem[] }
|
|
35
|
+
|
|
33
36
|
export default function ProjectsClient({ profiles, skillCount }: { profiles: ProjectProfile[]; skillCount: number }) {
|
|
34
37
|
const [inputMode, setInputMode] = useState<InputMode>('local')
|
|
35
38
|
const [projectPath, setProjectPath] = useState('')
|
|
36
39
|
const [githubUrl, setGithubUrl] = useState('')
|
|
37
|
-
const [cloneDir, setCloneDir] = useState('~/
|
|
40
|
+
const [cloneDir, setCloneDir] = useState('~/HelixEvo')
|
|
38
41
|
const [setupState, setSetupState] = useState<SetupState>('idle')
|
|
39
42
|
const [output, setOutput] = useState('')
|
|
40
43
|
const outputRef = useRef<HTMLPreElement | null>(null)
|
|
41
44
|
const abortRef = useRef<AbortController | null>(null)
|
|
42
45
|
|
|
46
|
+
// Folder browser state
|
|
47
|
+
const [showBrowser, setShowBrowser] = useState(false)
|
|
48
|
+
const [browseData, setBrowseData] = useState<BrowseResult | null>(null)
|
|
49
|
+
const [browseLoading, setBrowseLoading] = useState(false)
|
|
50
|
+
|
|
51
|
+
const browseTo = async (dir: string) => {
|
|
52
|
+
setBrowseLoading(true)
|
|
53
|
+
try {
|
|
54
|
+
const res = await fetch(`/api/browse?dir=${encodeURIComponent(dir)}`)
|
|
55
|
+
const data = await res.json()
|
|
56
|
+
if (data.error) return
|
|
57
|
+
setBrowseData(data)
|
|
58
|
+
} catch {}
|
|
59
|
+
setBrowseLoading(false)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const openBrowser = () => {
|
|
63
|
+
setShowBrowser(true)
|
|
64
|
+
browseTo('~')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const selectFolder = (path: string) => {
|
|
68
|
+
setProjectPath(path)
|
|
69
|
+
setShowBrowser(false)
|
|
70
|
+
}
|
|
71
|
+
|
|
43
72
|
const effectivePath = inputMode === 'local' ? projectPath.trim() : ''
|
|
44
73
|
const canStart = inputMode === 'local' ? !!projectPath.trim() : !!githubUrl.trim()
|
|
45
74
|
|
|
@@ -194,8 +223,11 @@ export default function ProjectsClient({ profiles, skillCount }: { profiles: Pro
|
|
|
194
223
|
style={{
|
|
195
224
|
padding: '7px 16px', fontSize: 12, fontWeight: 600, cursor: 'pointer',
|
|
196
225
|
background: inputMode === 'github' ? 'var(--bg-card)' : 'var(--bg-section)',
|
|
197
|
-
|
|
198
|
-
|
|
226
|
+
borderTop: `1px solid ${inputMode === 'github' ? 'var(--border-focus)' : 'var(--border)'}`,
|
|
227
|
+
borderRight: `1px solid ${inputMode === 'github' ? 'var(--border-focus)' : 'var(--border)'}`,
|
|
228
|
+
borderBottom: `1px solid ${inputMode === 'github' ? 'var(--border-focus)' : 'var(--border)'}`,
|
|
229
|
+
borderLeft: 'none',
|
|
230
|
+
borderRadius: '0 8px 8px 0',
|
|
199
231
|
color: inputMode === 'github' ? 'var(--text)' : 'var(--text-dim)',
|
|
200
232
|
}}
|
|
201
233
|
>
|
|
@@ -210,20 +242,41 @@ export default function ProjectsClient({ profiles, skillCount }: { profiles: Pro
|
|
|
210
242
|
<label style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-dim)', display: 'block', marginBottom: 5 }}>
|
|
211
243
|
Project folder path
|
|
212
244
|
</label>
|
|
213
|
-
<
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
245
|
+
<div style={{ display: 'flex', gap: 6 }}>
|
|
246
|
+
<input
|
|
247
|
+
value={projectPath}
|
|
248
|
+
onChange={e => setProjectPath(e.target.value)}
|
|
249
|
+
placeholder="/path/to/your/project"
|
|
250
|
+
disabled={setupState === 'analyzing'}
|
|
251
|
+
style={{
|
|
252
|
+
flex: 1, padding: '9px 14px',
|
|
253
|
+
border: '1px solid var(--border)', borderRadius: 'var(--radius)',
|
|
254
|
+
fontSize: 13, fontFamily: 'var(--font-mono)',
|
|
255
|
+
background: 'var(--bg-input)', color: 'var(--text)',
|
|
256
|
+
}}
|
|
257
|
+
/>
|
|
258
|
+
<button onClick={openBrowser} disabled={setupState === 'analyzing'} style={{
|
|
259
|
+
padding: '9px 14px', border: '1px solid var(--border)', borderRadius: 'var(--radius)',
|
|
260
|
+
background: 'var(--bg-card)', cursor: 'pointer', fontSize: 12, fontWeight: 600,
|
|
261
|
+
color: 'var(--text-secondary)', display: 'flex', alignItems: 'center', gap: 5,
|
|
262
|
+
whiteSpace: 'nowrap',
|
|
263
|
+
}}>
|
|
264
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
265
|
+
<path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
|
266
|
+
</svg>
|
|
267
|
+
Browse
|
|
268
|
+
</button>
|
|
269
|
+
</div>
|
|
270
|
+
<div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 6, display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
|
|
271
|
+
<span>Quick select:</span>
|
|
272
|
+
{['.', '~/Documents/GitHub', '~/Desktop', '~/HelixEvo', ...profiles.map(p => p.path).filter(p => !['.', '~/Documents/GitHub', '~/Desktop', '~/HelixEvo'].includes(p))].map(p => (
|
|
273
|
+
<button key={p} onClick={() => setProjectPath(p)} style={{
|
|
274
|
+
padding: '1px 8px', fontSize: 10, background: projectPath === p ? 'var(--green-light)' : 'var(--bg-section)',
|
|
275
|
+
border: `1px solid ${projectPath === p ? 'var(--green-border)' : 'var(--border-subtle)'}`,
|
|
276
|
+
borderRadius: 4, cursor: 'pointer', color: projectPath === p ? 'var(--green)' : 'var(--text-dim)',
|
|
277
|
+
fontFamily: 'var(--font-mono)',
|
|
278
|
+
}}>{p}</button>
|
|
279
|
+
))}
|
|
227
280
|
</div>
|
|
228
281
|
</div>
|
|
229
282
|
{setupState !== 'analyzing' ? (
|
|
@@ -556,6 +609,102 @@ export default function ProjectsClient({ profiles, skillCount }: { profiles: Pro
|
|
|
556
609
|
</div>
|
|
557
610
|
</div>
|
|
558
611
|
)}
|
|
612
|
+
{/* Folder Browser Modal */}
|
|
613
|
+
{showBrowser && (
|
|
614
|
+
<div style={{
|
|
615
|
+
position: 'fixed', inset: 0, zIndex: 9999,
|
|
616
|
+
background: 'rgba(0,0,0,0.3)', display: 'flex',
|
|
617
|
+
alignItems: 'center', justifyContent: 'center',
|
|
618
|
+
}} onClick={() => setShowBrowser(false)}>
|
|
619
|
+
<div onClick={e => e.stopPropagation()} style={{
|
|
620
|
+
width: 520, maxHeight: '70vh', background: 'var(--bg-card)',
|
|
621
|
+
borderRadius: 'var(--radius-xl)', boxShadow: 'var(--shadow-xl)',
|
|
622
|
+
border: '1px solid var(--border)', overflow: 'hidden',
|
|
623
|
+
display: 'flex', flexDirection: 'column',
|
|
624
|
+
}}>
|
|
625
|
+
{/* Header */}
|
|
626
|
+
<div style={{
|
|
627
|
+
padding: '14px 18px', borderBottom: '1px solid var(--border)',
|
|
628
|
+
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
|
629
|
+
}}>
|
|
630
|
+
<div>
|
|
631
|
+
<div style={{ fontSize: 14, fontWeight: 700 }}>Browse Folders</div>
|
|
632
|
+
<div style={{ fontSize: 11, color: 'var(--text-dim)', fontFamily: 'var(--font-mono)', marginTop: 2 }}>
|
|
633
|
+
{browseData?.displayPath ?? '~'}
|
|
634
|
+
</div>
|
|
635
|
+
</div>
|
|
636
|
+
<div style={{ display: 'flex', gap: 6 }}>
|
|
637
|
+
{browseData?.parent && (
|
|
638
|
+
<button onClick={() => browseTo(browseData.parent!)} style={{
|
|
639
|
+
padding: '5px 10px', fontSize: 11, fontWeight: 600,
|
|
640
|
+
background: 'var(--bg-section)', border: '1px solid var(--border)',
|
|
641
|
+
borderRadius: 'var(--radius)', cursor: 'pointer', color: 'var(--text-secondary)',
|
|
642
|
+
}}>
|
|
643
|
+
↑ Up
|
|
644
|
+
</button>
|
|
645
|
+
)}
|
|
646
|
+
<button onClick={() => selectFolder(browseData?.path ?? '~')} style={{
|
|
647
|
+
padding: '5px 12px', fontSize: 11, fontWeight: 600,
|
|
648
|
+
background: 'var(--green)', color: '#fff', border: 'none',
|
|
649
|
+
borderRadius: 'var(--radius)', cursor: 'pointer',
|
|
650
|
+
}}>
|
|
651
|
+
Select This Folder
|
|
652
|
+
</button>
|
|
653
|
+
<button onClick={() => setShowBrowser(false)} style={{
|
|
654
|
+
padding: '5px 10px', fontSize: 14, background: 'none',
|
|
655
|
+
border: 'none', cursor: 'pointer', color: 'var(--text-dim)',
|
|
656
|
+
}}>
|
|
657
|
+
×
|
|
658
|
+
</button>
|
|
659
|
+
</div>
|
|
660
|
+
</div>
|
|
661
|
+
|
|
662
|
+
{/* Content */}
|
|
663
|
+
<div style={{ overflow: 'auto', flex: 1, padding: '8px 0' }}>
|
|
664
|
+
{browseLoading ? (
|
|
665
|
+
<div style={{ padding: 24, textAlign: 'center', color: 'var(--text-dim)', fontSize: 12 }}>Loading...</div>
|
|
666
|
+
) : browseData?.items.length === 0 ? (
|
|
667
|
+
<div style={{ padding: 24, textAlign: 'center', color: 'var(--text-dim)', fontSize: 12 }}>Empty directory</div>
|
|
668
|
+
) : (
|
|
669
|
+
browseData?.items.map(item => (
|
|
670
|
+
<div
|
|
671
|
+
key={item.name}
|
|
672
|
+
onClick={() => {
|
|
673
|
+
if (item.isDirectory) {
|
|
674
|
+
if (item.isProject) {
|
|
675
|
+
selectFolder(browseData.path + '/' + item.name)
|
|
676
|
+
} else {
|
|
677
|
+
browseTo(browseData.path + '/' + item.name)
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}}
|
|
681
|
+
style={{
|
|
682
|
+
padding: '7px 18px', display: 'flex', alignItems: 'center', gap: 10,
|
|
683
|
+
cursor: item.isDirectory ? 'pointer' : 'default',
|
|
684
|
+
fontSize: 13, color: item.isDirectory ? 'var(--text)' : 'var(--text-dim)',
|
|
685
|
+
background: item.isProject ? 'var(--green-light)' : 'transparent',
|
|
686
|
+
borderLeft: item.isProject ? '3px solid var(--green)' : '3px solid transparent',
|
|
687
|
+
}}
|
|
688
|
+
onMouseOver={e => { if (item.isDirectory) (e.currentTarget.style.background = item.isProject ? 'var(--green-light)' : 'var(--bg-hover)') }}
|
|
689
|
+
onMouseOut={e => { e.currentTarget.style.background = item.isProject ? 'var(--green-light)' : 'transparent' }}
|
|
690
|
+
>
|
|
691
|
+
<span style={{ fontSize: 14, width: 20, textAlign: 'center' }}>
|
|
692
|
+
{item.isProject ? '📦' : item.isDirectory ? '📁' : '📄'}
|
|
693
|
+
</span>
|
|
694
|
+
<span style={{ flex: 1, fontWeight: item.isProject ? 600 : 400 }}>{item.name}</span>
|
|
695
|
+
{item.isProject && (
|
|
696
|
+
<span style={{ fontSize: 10, color: 'var(--green)', fontWeight: 600 }}>project</span>
|
|
697
|
+
)}
|
|
698
|
+
{item.isDirectory && !item.isProject && (
|
|
699
|
+
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>→</span>
|
|
700
|
+
)}
|
|
701
|
+
</div>
|
|
702
|
+
))
|
|
703
|
+
)}
|
|
704
|
+
</div>
|
|
705
|
+
</div>
|
|
706
|
+
</div>
|
|
707
|
+
)}
|
|
559
708
|
</div>
|
|
560
709
|
)
|
|
561
710
|
}
|
package/package.json
CHANGED