helixevo 0.2.38 → 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 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,6 +30,9 @@ 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('')
@@ -40,6 +43,32 @@ export default function ProjectsClient({ profiles, skillCount }: { profiles: Pro
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
- border: `1px solid ${inputMode === 'github' ? 'var(--border-focus)' : 'var(--border)'}`,
198
- borderRadius: '0 8px 8px 0', borderLeft: 'none',
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,18 +242,31 @@ 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
- <input
214
- value={projectPath}
215
- onChange={e => setProjectPath(e.target.value)}
216
- placeholder="/path/to/your/project or . or ~/projects/myapp"
217
- disabled={setupState === 'analyzing'}
218
- style={{
219
- width: '100%', padding: '9px 14px',
220
- border: '1px solid var(--border)', borderRadius: 'var(--radius)',
221
- fontSize: 13, fontFamily: 'var(--font-mono)',
222
- background: 'var(--bg-input)', color: 'var(--text)',
223
- }}
224
- />
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>
225
270
  <div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 6, display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
226
271
  <span>Quick select:</span>
227
272
  {['.', '~/Documents/GitHub', '~/Desktop', '~/HelixEvo', ...profiles.map(p => p.path).filter(p => !['.', '~/Documents/GitHub', '~/Desktop', '~/HelixEvo'].includes(p))].map(p => (
@@ -564,6 +609,102 @@ export default function ProjectsClient({ profiles, skillCount }: { profiles: Pro
564
609
  </div>
565
610
  </div>
566
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
+ &times;
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)' }}>&rarr;</span>
700
+ )}
701
+ </div>
702
+ ))
703
+ )}
704
+ </div>
705
+ </div>
706
+ </div>
707
+ )}
567
708
  </div>
568
709
  )
569
710
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helixevo",
3
- "version": "0.2.38",
3
+ "version": "0.2.39",
4
4
  "description": "Self-evolving skill ecosystem for AI agents. Skills and projects co-evolve through multi-judge evaluation and a Pareto frontier.",
5
5
  "type": "module",
6
6
  "bin": {