helixevo 0.2.26 → 0.2.28

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.
@@ -313,7 +313,7 @@ export default function NetworkClient({
313
313
  <button
314
314
  key={sv.key}
315
315
  className={`tab-item ${view === sv.key ? 'active' : ''}`}
316
- onClick={() => { setView(sv.key); setSelectedSkill(null) }}
316
+ onClick={() => { setView(sv.key) }}
317
317
  >
318
318
  {sv.label}
319
319
  </button>
@@ -324,7 +324,7 @@ export default function NetworkClient({
324
324
  <div style={{ display: 'flex', gap: 0 }}>
325
325
  {/* Main content */}
326
326
  <div style={{ flex: 1, minWidth: 0 }}>
327
- {view === 'graph' && <GraphView flowNodes={flowNodes} flowEdges={flowEdges} stats={stats} />}
327
+ {view === 'graph' && <GraphView flowNodes={flowNodes} flowEdges={flowEdges} stats={stats} onNodeClick={setSelectedSkill} />}
328
328
  {view === 'general' && (
329
329
  <GeneralView
330
330
  generalized={generalized} evolved={evolved} original={original}
@@ -339,6 +339,7 @@ export default function NetworkClient({
339
339
  <CoEvolutionView
340
340
  generalized={generalized} evolved={evolved} original={original}
341
341
  edges={graphEdges} evolutionBySkill={evolutionBySkill} projects={projects}
342
+ onSelect={setSelectedSkill}
342
343
  />
343
344
  )}
344
345
  </div>
@@ -386,20 +387,20 @@ export default function NetworkClient({
386
387
  {selectedEdges.inheritsFrom.length > 0 && (
387
388
  <div style={{ fontSize: 12 }}>
388
389
  <span style={{ color: 'var(--text-dim)' }}>Inherits from: </span>
389
- {selectedEdges.inheritsFrom.map(e => <span key={e.from} className="badge badge-purple" style={{ marginRight: 3 }}>{e.from}</span>)}
390
+ {selectedEdges.inheritsFrom.map(e => <span key={e.from} className="badge badge-purple" style={{ marginRight: 3, cursor: 'pointer' }} onClick={() => setSelectedSkill(e.from)}>{e.from}</span>)}
390
391
  </div>
391
392
  )}
392
393
  {selectedEdges.children.length > 0 && (
393
394
  <div style={{ fontSize: 12 }}>
394
395
  <span style={{ color: 'var(--text-dim)' }}>Children: </span>
395
- {selectedEdges.children.map(e => <span key={e.to} className="badge badge-blue" style={{ marginRight: 3 }}>{e.to}</span>)}
396
+ {selectedEdges.children.map(e => <span key={e.to} className="badge badge-blue" style={{ marginRight: 3, cursor: 'pointer' }} onClick={() => setSelectedSkill(e.to)}>{e.to}</span>)}
396
397
  </div>
397
398
  )}
398
399
  {selectedEdges.enhances.length > 0 && (
399
400
  <div style={{ fontSize: 12 }}>
400
401
  <span style={{ color: 'var(--text-dim)' }}>Enhances: </span>
401
402
  {[...new Set(selectedEdges.enhances.map(e => e.from === selectedSkill ? e.to : e.from))].map(id =>
402
- <span key={id} className="badge badge-green" style={{ marginRight: 3 }}>{id}</span>
403
+ <span key={id} className="badge badge-green" style={{ marginRight: 3, cursor: 'pointer' }} onClick={() => setSelectedSkill(id)}>{id}</span>
403
404
  )}
404
405
  </div>
405
406
  )}
@@ -407,7 +408,7 @@ export default function NetworkClient({
407
408
  <div style={{ fontSize: 12 }}>
408
409
  <span style={{ color: 'var(--text-dim)' }}>Conflicts: </span>
409
410
  {[...new Set(selectedEdges.conflicts.map(e => e.from === selectedSkill ? e.to : e.from))].map(id =>
410
- <span key={id} className="badge badge-red" style={{ marginRight: 3 }}>{id}</span>
411
+ <span key={id} className="badge badge-red" style={{ marginRight: 3, cursor: 'pointer' }} onClick={() => setSelectedSkill(id)}>{id}</span>
411
412
  )}
412
413
  </div>
413
414
  )}
@@ -440,6 +441,28 @@ export default function NetworkClient({
440
441
  </div>
441
442
  )}
442
443
 
444
+ {/* ─── Cross-Links ─── */}
445
+ <div style={{ marginBottom: 16 }}>
446
+ <div className="section-label">Navigate</div>
447
+ <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
448
+ {view !== 'graph' && (
449
+ <button onClick={() => setView('graph')} style={actionBtnStyle('var(--blue)')}>
450
+ ◎ View in Graph
451
+ </button>
452
+ )}
453
+ {view !== 'general' && (
454
+ <button onClick={() => setView('general')} style={actionBtnStyle()}>
455
+ ▤ View in Skills
456
+ </button>
457
+ )}
458
+ {view !== 'coevolution' && (
459
+ <button onClick={() => setView('coevolution')} style={actionBtnStyle()}>
460
+ ↕ Co-Evolution
461
+ </button>
462
+ )}
463
+ </div>
464
+ </div>
465
+
443
466
  {/* ─── Management Actions ─── */}
444
467
  <div style={{ marginBottom: 20 }}>
445
468
  <div className="section-label">Actions</div>
@@ -583,7 +606,7 @@ export default function NetworkClient({
583
606
  // SUB-VIEW: Graph
584
607
  // ═══════════════════════════════════════════════════════════════════
585
608
 
586
- function GraphView({ flowNodes, flowEdges, stats }: { flowNodes: Node[]; flowEdges: Edge[]; stats: Props['stats'] }) {
609
+ function GraphView({ flowNodes, flowEdges, stats, onNodeClick }: { flowNodes: Node[]; flowEdges: Edge[]; stats: Props['stats']; onNodeClick: (id: string) => void }) {
587
610
  return (
588
611
  <div style={{ position: 'relative', height: 'calc(100vh - 200px)', width: '100%', borderRadius: 12, overflow: 'hidden', border: '1px solid var(--border)', background: 'var(--bg)' }}>
589
612
  <ReactFlow
@@ -591,6 +614,7 @@ function GraphView({ flowNodes, flowEdges, stats }: { flowNodes: Node[]; flowEdg
591
614
  edges={flowEdges}
592
615
  nodeTypes={nodeTypes}
593
616
  connectionLineType={ConnectionLineType.SmoothStep}
617
+ onNodeClick={(_event, node) => onNodeClick(node.id)}
594
618
  fitView
595
619
  fitViewOptions={{ padding: 0.2 }}
596
620
  proOptions={{ hideAttribution: true }}
@@ -808,13 +832,24 @@ function ProjectsView({
808
832
  </div>
809
833
  </div>
810
834
  {unresolved.length >= 3 && (
811
- <div style={{
812
- padding: '6px 14px', borderRadius: 8,
813
- background: 'var(--green-light)', color: 'var(--green)',
814
- fontSize: 11, fontWeight: 600,
815
- }}>
816
- Ready to specialize
817
- </div>
835
+ <button
836
+ onClick={async () => {
837
+ try {
838
+ await fetch('/api/run', {
839
+ method: 'POST',
840
+ headers: { 'Content-Type': 'application/json' },
841
+ body: JSON.stringify({ command: 'specialize', project: project.name }),
842
+ })
843
+ } catch {}
844
+ }}
845
+ style={{
846
+ padding: '6px 14px', borderRadius: 8, border: 'none', cursor: 'pointer',
847
+ background: 'var(--green)', color: '#fff',
848
+ fontSize: 11, fontWeight: 600,
849
+ }}
850
+ >
851
+ Specialize for {project.name} →
852
+ </button>
818
853
  )}
819
854
  </div>
820
855
  </div>
@@ -869,10 +904,11 @@ function ProjectsView({
869
904
  // ═══════════════════════════════════════════════════════════════════
870
905
 
871
906
  function CoEvolutionView({
872
- generalized, evolved, original, edges, evolutionBySkill, projects,
907
+ generalized, evolved, original, edges, evolutionBySkill, projects, onSelect,
873
908
  }: {
874
909
  generalized: SkillNode[]; evolved: SkillNode[]; original: SkillNode[]
875
910
  edges: GraphEdge[]; evolutionBySkill: Record<string, EvolutionEntry[]>; projects: ProjectData[]
911
+ onSelect: (id: string) => void
876
912
  }) {
877
913
  const allNodes = [...generalized, ...evolved, ...original]
878
914
 
@@ -924,7 +960,7 @@ function CoEvolutionView({
924
960
  width: 8, height: 8, borderRadius: '50%',
925
961
  background: 'var(--purple)', flexShrink: 0,
926
962
  }} />
927
- <span style={{ fontWeight: 700, fontSize: 14 }}>{chain.parent.name}</span>
963
+ <span onClick={() => onSelect(chain.parent.id)} style={{ fontWeight: 700, fontSize: 14, cursor: 'pointer' }}>{chain.parent.name}</span>
928
964
  <span style={{ fontSize: 22, fontWeight: 800, color: scoreColor(chain.parent.score), marginLeft: 'auto' }}>
929
965
  {(chain.parent.score * 100).toFixed(0)}
930
966
  </span>
@@ -939,7 +975,7 @@ function CoEvolutionView({
939
975
  padding: '6px 0',
940
976
  }}>
941
977
  <span style={{ fontSize: 10, color: 'var(--purple)' }}>↳</span>
942
- <span style={{ fontWeight: 500, fontSize: 13 }}>{child.name}</span>
978
+ <span onClick={() => onSelect(child.id)} style={{ fontWeight: 500, fontSize: 13, cursor: 'pointer' }}>{child.name}</span>
943
979
  <span className="badge badge-green" style={{ fontSize: 9 }}>gen {child.generation}</span>
944
980
  <span style={{ marginLeft: 'auto', fontSize: 13, fontWeight: 700, color: scoreColor(child.score) }}>
945
981
  {(child.score * 100).toFixed(0)}
@@ -968,9 +1004,9 @@ function CoEvolutionView({
968
1004
  display: 'flex', alignItems: 'center', gap: 10,
969
1005
  padding: '8px 0', borderBottom: i < enhancePairs.length - 1 ? '1px solid var(--border)' : 'none',
970
1006
  }}>
971
- <span style={{ fontWeight: 500, fontSize: 13 }}>{pair.from.name}</span>
1007
+ <span onClick={() => onSelect(pair.from.id)} style={{ fontWeight: 500, fontSize: 13, cursor: 'pointer' }}>{pair.from.name}</span>
972
1008
  <span style={{ color: 'var(--green)', fontSize: 12 }}>↔</span>
973
- <span style={{ fontWeight: 500, fontSize: 13 }}>{pair.to.name}</span>
1009
+ <span onClick={() => onSelect(pair.to.id)} style={{ fontWeight: 500, fontSize: 13, cursor: 'pointer' }}>{pair.to.name}</span>
974
1010
  <span style={{ marginLeft: 'auto', fontSize: 11, color: 'var(--text-dim)' }}>
975
1011
  strength: {(pair.strength * 100).toFixed(0)}%
976
1012
  </span>
@@ -991,9 +1027,9 @@ function CoEvolutionView({
991
1027
  display: 'flex', alignItems: 'center', gap: 10,
992
1028
  padding: '8px 0', borderBottom: i < conflictPairs.length - 1 ? '1px solid var(--border)' : 'none',
993
1029
  }}>
994
- <span style={{ fontWeight: 500, fontSize: 13 }}>{pair.from.name}</span>
1030
+ <span onClick={() => onSelect(pair.from.id)} style={{ fontWeight: 500, fontSize: 13, cursor: 'pointer' }}>{pair.from.name}</span>
995
1031
  <span style={{ color: 'var(--red)', fontSize: 12 }}>⚡</span>
996
- <span style={{ fontWeight: 500, fontSize: 13 }}>{pair.to.name}</span>
1032
+ <span onClick={() => onSelect(pair.to.id)} style={{ fontWeight: 500, fontSize: 13, cursor: 'pointer' }}>{pair.to.name}</span>
997
1033
  <span style={{ marginLeft: 'auto', fontSize: 11, color: 'var(--text-dim)' }}>
998
1034
  strength: {(pair.strength * 100).toFixed(0)}%
999
1035
  </span>
@@ -3,7 +3,7 @@
3
3
  import { useState, useEffect } from 'react'
4
4
 
5
5
  const REGISTRY_URL = 'https://registry.npmjs.org/helixevo/latest'
6
- const CHECK_INTERVAL_MS = 60 * 60 * 1000 // re-check every hour
6
+ const CHECK_INTERVAL_MS = 60 * 60 * 1000
7
7
 
8
8
  function compareVersions(current: string, latest: string): boolean {
9
9
  const c = current.split('.').map(Number)
@@ -15,7 +15,7 @@ function compareVersions(current: string, latest: string): boolean {
15
15
  return false
16
16
  }
17
17
 
18
- type UpdateState = 'idle' | 'updating' | 'success' | 'error'
18
+ type UpdateState = 'idle' | 'updating' | 'installed' | 'error'
19
19
 
20
20
  export function UpdateBanner({ currentVersion }: { currentVersion: string }) {
21
21
  const [latestVersion, setLatestVersion] = useState<string | null>(null)
@@ -23,10 +23,10 @@ export function UpdateBanner({ currentVersion }: { currentVersion: string }) {
23
23
  const [state, setState] = useState<UpdateState>('idle')
24
24
  const [newVersion, setNewVersion] = useState<string | null>(null)
25
25
  const [errorMsg, setErrorMsg] = useState<string | null>(null)
26
+ const [copied, setCopied] = useState(false)
26
27
 
27
28
  useEffect(() => {
28
29
  let mounted = true
29
-
30
30
  async function check() {
31
31
  try {
32
32
  const res = await fetch(REGISTRY_URL)
@@ -37,7 +37,6 @@ export function UpdateBanner({ currentVersion }: { currentVersion: string }) {
37
37
  }
38
38
  } catch {}
39
39
  }
40
-
41
40
  check()
42
41
  const interval = setInterval(check, CHECK_INTERVAL_MS)
43
42
  return () => { mounted = false; clearInterval(interval) }
@@ -54,49 +53,35 @@ export function UpdateBanner({ currentVersion }: { currentVersion: string }) {
54
53
  const data = await res.json()
55
54
 
56
55
  if (data.success) {
57
- setState('success')
56
+ setState('installed')
58
57
  setNewVersion(data.version)
59
-
60
- // Tell server to restart, then wait and reload
61
- try {
62
- await fetch('/api/restart', { method: 'POST' })
63
- } catch {}
64
-
65
- // Wait for: shutdown (2s) + port release (2s) + restart + compile (~8s)
66
- // Then try to reload, retrying if server isn't ready
67
- await new Promise(r => setTimeout(r, 6000))
68
-
69
- for (let attempt = 0; attempt < 20; attempt++) {
70
- try {
71
- const check = await fetch('/?_bust=' + Date.now(), { cache: 'no-store' })
72
- if (check.ok) {
73
- window.location.href = window.location.pathname + '?updated=' + Date.now()
74
- return
75
- }
76
- } catch {}
77
- await new Promise(r => setTimeout(r, 2000))
78
- }
79
-
80
- // Final fallback
81
- window.location.href = window.location.pathname + '?updated=' + Date.now()
82
58
  } else {
83
59
  setState('error')
84
60
  setErrorMsg(data.error ?? 'Update failed')
85
61
  }
86
- } catch (err) {
62
+ } catch {
87
63
  setState('error')
88
64
  setErrorMsg('Network error — try running: npm install -g helixevo@latest')
89
65
  }
90
66
  }
91
67
 
68
+ const restartCmd = 'helixevo dashboard'
69
+ const handleCopy = async () => {
70
+ try {
71
+ await navigator.clipboard.writeText(restartCmd)
72
+ setCopied(true)
73
+ setTimeout(() => setCopied(false), 2000)
74
+ } catch {}
75
+ }
76
+
92
77
  return (
93
78
  <div style={{
94
79
  position: 'fixed',
95
80
  bottom: 20,
96
81
  right: 20,
97
- width: 320,
82
+ width: 340,
98
83
  background: 'var(--bg-card)',
99
- border: `1px solid ${state === 'success' ? 'var(--green-border)' : state === 'error' ? 'var(--red-border)' : 'var(--purple-border)'}`,
84
+ border: `1px solid ${state === 'installed' ? 'var(--green-border)' : state === 'error' ? 'var(--red-border)' : 'var(--purple-border)'}`,
100
85
  borderRadius: 'var(--radius-lg)',
101
86
  boxShadow: 'var(--shadow-xl)',
102
87
  padding: '16px 18px',
@@ -123,7 +108,7 @@ export function UpdateBanner({ currentVersion }: { currentVersion: string }) {
123
108
  <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
124
109
  <div style={{
125
110
  width: 28, height: 28, borderRadius: '50%',
126
- background: state === 'success' ? 'var(--green-light)'
111
+ background: state === 'installed' ? 'var(--green-light)'
127
112
  : state === 'error' ? 'var(--red-light)'
128
113
  : 'var(--purple-light)',
129
114
  display: 'flex',
@@ -135,7 +120,7 @@ export function UpdateBanner({ currentVersion }: { currentVersion: string }) {
135
120
  borderTopColor: 'var(--purple)', borderRadius: '50%',
136
121
  animation: 'updateSpin 0.8s linear infinite',
137
122
  }} />
138
- ) : state === 'success' ? (
123
+ ) : state === 'installed' ? (
139
124
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--green)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
140
125
  <path d="M20 6L9 17l-5-5" />
141
126
  </svg>
@@ -151,21 +136,21 @@ export function UpdateBanner({ currentVersion }: { currentVersion: string }) {
151
136
  </div>
152
137
  <div>
153
138
  <div style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)', letterSpacing: -0.2 }}>
154
- {state === 'updating' ? 'Updating...'
155
- : state === 'success' ? 'Updated!'
139
+ {state === 'updating' ? 'Installing...'
140
+ : state === 'installed' ? 'Installed!'
156
141
  : state === 'error' ? 'Update Failed'
157
142
  : 'Update Available'}
158
143
  </div>
159
144
  <div style={{ fontSize: 11, color: 'var(--text-dim)' }}>
160
- {state === 'success'
161
- ? <>Now on <span style={{ color: 'var(--green)', fontWeight: 600 }}>v{newVersion}</span> restarting...</>
145
+ {state === 'installed'
146
+ ? <>v{newVersion} is ready</>
162
147
  : <>v{currentVersion} &rarr; <span style={{ color: 'var(--green)', fontWeight: 600 }}>v{latestVersion}</span></>
163
148
  }
164
149
  </div>
165
150
  </div>
166
151
  </div>
167
152
 
168
- {/* Action area */}
153
+ {/* Idle Update Now button */}
169
154
  {state === 'idle' && (
170
155
  <button
171
156
  onClick={handleUpdate}
@@ -183,18 +168,16 @@ export function UpdateBanner({ currentVersion }: { currentVersion: string }) {
183
168
  alignItems: 'center',
184
169
  justifyContent: 'center',
185
170
  gap: 6,
186
- transition: 'opacity 0.15s',
187
171
  }}
188
- onMouseOver={e => (e.currentTarget.style.opacity = '0.9')}
189
- onMouseOut={e => (e.currentTarget.style.opacity = '1')}
190
172
  >
191
173
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
192
- <path d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9" />
174
+ <path d="M12 19V5m-7 7l7-7 7 7" />
193
175
  </svg>
194
176
  Update Now
195
177
  </button>
196
178
  )}
197
179
 
180
+ {/* Updating spinner */}
198
181
  {state === 'updating' && (
199
182
  <div style={{
200
183
  width: '100%',
@@ -209,21 +192,52 @@ export function UpdateBanner({ currentVersion }: { currentVersion: string }) {
209
192
  </div>
210
193
  )}
211
194
 
212
- {state === 'success' && (
213
- <div style={{
214
- width: '100%',
215
- padding: '9px 16px',
216
- background: 'var(--green-light)',
217
- borderRadius: 'var(--radius)',
218
- fontSize: 12,
219
- color: 'var(--green)',
220
- textAlign: 'center',
221
- fontWeight: 600,
222
- }}>
223
- Restarting dashboard...
195
+ {/* Installed restart instructions */}
196
+ {state === 'installed' && (
197
+ <div>
198
+ <div style={{
199
+ padding: '10px 14px',
200
+ background: 'var(--green-light)',
201
+ borderRadius: 'var(--radius)',
202
+ fontSize: 12,
203
+ color: 'var(--green)',
204
+ fontWeight: 600,
205
+ marginBottom: 10,
206
+ textAlign: 'center',
207
+ }}>
208
+ v{newVersion} installed successfully
209
+ </div>
210
+ <div style={{ fontSize: 11, color: 'var(--text-dim)', marginBottom: 8, lineHeight: 1.5 }}>
211
+ Restart the dashboard to load the new version:
212
+ </div>
213
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
214
+ <div style={{ fontSize: 11, color: 'var(--text-dim)' }}>
215
+ 1. Press <kbd style={{ padding: '1px 5px', background: 'var(--bg-section)', borderRadius: 3, fontFamily: 'var(--font-mono)', fontSize: 10, border: '1px solid var(--border)' }}>Ctrl+C</kbd> in the terminal running the dashboard
216
+ </div>
217
+ <div style={{ fontSize: 11, color: 'var(--text-dim)' }}>
218
+ 2. Run:
219
+ </div>
220
+ <div
221
+ onClick={handleCopy}
222
+ style={{
223
+ display: 'flex', justifyContent: 'space-between', alignItems: 'center',
224
+ padding: '7px 12px',
225
+ background: 'var(--bg-section)', border: '1px solid var(--border)',
226
+ borderRadius: 'var(--radius)',
227
+ fontFamily: 'var(--font-mono)', fontSize: 11,
228
+ color: 'var(--text)', cursor: 'pointer',
229
+ }}
230
+ >
231
+ <span>$ {restartCmd}</span>
232
+ <span style={{ fontSize: 10, color: copied ? 'var(--green)' : 'var(--purple)', fontWeight: 600, fontFamily: 'var(--font)' }}>
233
+ {copied ? 'Copied!' : 'Copy'}
234
+ </span>
235
+ </div>
236
+ </div>
224
237
  </div>
225
238
  )}
226
239
 
240
+ {/* Error */}
227
241
  {state === 'error' && (
228
242
  <>
229
243
  <div style={{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helixevo",
3
- "version": "0.2.26",
3
+ "version": "0.2.28",
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": {