helixevo 0.2.22 → 0.2.24

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.
@@ -1,26 +1,31 @@
1
- import { getDashboardSummary, loadFrontier, loadHistory, loadGraph, loadFailures } from '@/lib/data'
1
+ import Link from 'next/link'
2
+ import { getDashboardSummary, loadFrontier, loadHistory, loadGraph, loadFailures, listProjects } from '@/lib/data'
2
3
  import { OverviewActions } from '@/components/overview-actions'
3
4
 
4
5
  export const dynamic = 'force-dynamic'
5
6
 
6
- function StatCard({ value, label, color, sub, accent }: { value: string | number; label: string; color: string; sub?: string; accent?: string }) {
7
+ function StatCard({ value, label, color, sub, accent, href }: {
8
+ value: string | number; label: string; color: string; sub?: string; accent?: string; href: string
9
+ }) {
7
10
  return (
8
- <div className="stat-card">
9
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
10
- <div>
11
- <div className="stat-value" style={{ color }}>{value}</div>
12
- <div className="stat-label">{label}</div>
13
- {sub && <div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 2 }}>{sub}</div>}
11
+ <Link href={href} style={{ textDecoration: 'none', color: 'inherit' }}>
12
+ <div className="stat-card" style={{ cursor: 'pointer' }}>
13
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
14
+ <div>
15
+ <div className="stat-value" style={{ color }}>{value}</div>
16
+ <div className="stat-label">{label}</div>
17
+ {sub && <div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 2 }}>{sub}</div>}
18
+ </div>
19
+ {accent && (
20
+ <div style={{
21
+ width: 36, height: 36, borderRadius: 10,
22
+ background: `${color}12`, display: 'flex', alignItems: 'center', justifyContent: 'center',
23
+ color, fontSize: 16,
24
+ }}>{accent}</div>
25
+ )}
14
26
  </div>
15
- {accent && (
16
- <div style={{
17
- width: 36, height: 36, borderRadius: 10,
18
- background: `${color}12`, display: 'flex', alignItems: 'center', justifyContent: 'center',
19
- color, fontSize: 16,
20
- }}>{accent}</div>
21
- )}
22
27
  </div>
23
- </div>
28
+ </Link>
24
29
  )
25
30
  }
26
31
 
@@ -51,21 +56,23 @@ export default function Overview() {
51
56
  hasFailures={failures.length > 0}
52
57
  hasEdges={graph.edges.length > 0}
53
58
  unresolvedCount={s.failures.unresolved}
59
+ skillCount={graph.nodes.length}
60
+ projectList={listProjects()}
54
61
  />
55
62
  </div>
56
63
 
57
- {/* Stats */}
64
+ {/* Stats — all clickable, linking to relevant pages */}
58
65
  <div className="grid-5" style={{ marginBottom: 28 }}>
59
- <StatCard value={s.skills.total} label="Total Skills" color="var(--purple)" sub={`${s.skills.evolved} evolved`} accent="◆" />
60
- <StatCard value={s.evolution.accepted} label="Accepted" color="var(--green)" sub={`${s.evolution.rejected} rejected`} accent="✓" />
61
- <StatCard value={s.failures.unresolved} label="Unresolved" color="var(--yellow)" sub={`of ${s.failures.total} total`} accent="!" />
62
- <StatCard value={s.buffer.discoveries} label="Discoveries" color="var(--blue)" sub={`${s.buffer.drafts} drafts`} accent="◎" />
63
- <StatCard value={frontier.programs.length} label="Frontier" color="var(--text-secondary)" sub={`/${frontier.capacity} capacity`} accent="▲" />
66
+ <StatCard value={s.skills.total} label="Total Skills" color="var(--purple)" sub={`${s.skills.evolved} evolved`} accent="◆" href="/network" />
67
+ <StatCard value={s.evolution.accepted} label="Accepted" color="var(--green)" sub={`${s.evolution.rejected} rejected`} accent="✓" href="/evolution" />
68
+ <StatCard value={s.failures.unresolved} label="Unresolved" color="var(--yellow)" sub={`of ${s.failures.total} total`} accent="!" href="#unresolved" />
69
+ <StatCard value={s.buffer.discoveries} label="Discoveries" color="var(--blue)" sub={`${s.buffer.drafts} drafts`} accent="◎" href="/research" />
70
+ <StatCard value={frontier.programs.length} label="Frontier" color="var(--text-secondary)" sub={`/${frontier.capacity} capacity`} accent="▲" href="/frontier" />
64
71
  </div>
65
72
 
66
73
  {/* Unresolved Failures */}
67
74
  {failures.filter(f => !f.resolved).length > 0 && (
68
- <div className="card" style={{ marginBottom: 20 }}>
75
+ <div id="unresolved" className="card" style={{ marginBottom: 20 }}>
69
76
  <div className="card-body">
70
77
  <div className="card-header-label">
71
78
  Unresolved Corrections ({failures.filter(f => !f.resolved).length})
@@ -108,98 +115,104 @@ export default function Overview() {
108
115
 
109
116
  <div className="grid-2">
110
117
  {/* Frontier */}
111
- <div className="card">
112
- <div className="card-body">
113
- <div className="card-header-label">Pareto Frontier</div>
114
- {frontier.programs.map((p, i) => (
115
- <div key={`${p.id}-${i}`} style={{
116
- padding: '12px 0',
117
- borderBottom: i < frontier.programs.length - 1 ? '1px solid var(--border-subtle)' : 'none',
118
- }}>
119
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
120
- <span style={{ fontSize: 13, fontWeight: 600 }}>{p.id}</span>
121
- <span style={{
122
- fontSize: 20, fontWeight: 800, lineHeight: 1,
123
- color: p.score >= 0.8 ? 'var(--green)' : 'var(--yellow)',
124
- }}>
125
- {(p.score * 100).toFixed(0)}
126
- </span>
127
- </div>
128
- <div style={{ display: 'flex', gap: 6, fontSize: 11 }}>
129
- <span className="score-pill" style={{ color: 'var(--green)' }}>T:{(p.scores.taskCompletion * 10).toFixed(0)}</span>
130
- <span className="score-pill" style={{ color: 'var(--blue)' }}>A:{(p.scores.correctionAlignment * 10).toFixed(0)}</span>
131
- <span className="score-pill" style={{ color: 'var(--purple)' }}>S:{(p.scores.sideEffectFree * 10).toFixed(0)}</span>
132
- </div>
133
- <div style={{ fontSize: 11, color: 'var(--text-dim)', marginTop: 4, lineHeight: 1.4 }}>
134
- {p.changesDescription.slice(0, 80)}
118
+ <Link href="/frontier" style={{ textDecoration: 'none', color: 'inherit' }}>
119
+ <div className="card" style={{ cursor: 'pointer', height: '100%' }}>
120
+ <div className="card-body">
121
+ <div className="card-header-label">Pareto Frontier</div>
122
+ {frontier.programs.map((p, i) => (
123
+ <div key={`${p.id}-${i}`} style={{
124
+ padding: '12px 0',
125
+ borderBottom: i < frontier.programs.length - 1 ? '1px solid var(--border-subtle)' : 'none',
126
+ }}>
127
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
128
+ <span style={{ fontSize: 13, fontWeight: 600 }}>{p.id}</span>
129
+ <span style={{
130
+ fontSize: 20, fontWeight: 800, lineHeight: 1,
131
+ color: p.score >= 0.8 ? 'var(--green)' : 'var(--yellow)',
132
+ }}>
133
+ {(p.score * 100).toFixed(0)}
134
+ </span>
135
+ </div>
136
+ <div style={{ display: 'flex', gap: 6, fontSize: 11 }}>
137
+ <span className="score-pill" style={{ color: 'var(--green)' }}>T:{(p.scores.taskCompletion * 10).toFixed(0)}</span>
138
+ <span className="score-pill" style={{ color: 'var(--blue)' }}>A:{(p.scores.correctionAlignment * 10).toFixed(0)}</span>
139
+ <span className="score-pill" style={{ color: 'var(--purple)' }}>S:{(p.scores.sideEffectFree * 10).toFixed(0)}</span>
140
+ </div>
141
+ <div style={{ fontSize: 11, color: 'var(--text-dim)', marginTop: 4, lineHeight: 1.4 }}>
142
+ {p.changesDescription.slice(0, 80)}
143
+ </div>
135
144
  </div>
136
- </div>
137
- ))}
145
+ ))}
146
+ </div>
138
147
  </div>
139
- </div>
148
+ </Link>
140
149
 
141
150
  {/* Recent Evolution */}
142
- <div className="card">
143
- <div className="card-body">
144
- <div className="card-header-label">Recent Evolution</div>
145
- {recent.map((iter, idx) => (
146
- <div key={`${iter.id}-${idx}`} style={{
147
- padding: '10px 0',
148
- borderBottom: '1px solid var(--border-subtle)',
149
- }}>
150
- <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11, marginBottom: 8 }}>
151
- <span style={{ fontWeight: 600, color: 'var(--text)' }}>{iter.id}</span>
152
- <span style={{ color: 'var(--text-muted)' }}>{new Date(iter.timestamp).toLocaleDateString()}</span>
153
- </div>
154
- {iter.proposals.map(p => (
155
- <div key={p.id} style={{
156
- display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, marginBottom: 4,
157
- padding: '4px 0',
158
- }}>
159
- <span style={{
160
- width: 8, height: 8, borderRadius: '50%', flexShrink: 0,
161
- background: p.outcome === 'accepted' ? 'var(--green)' : 'var(--red)',
162
- }} />
163
- <span style={{ fontWeight: 500 }}>{p.targetSkill}</span>
164
- <span className="badge badge-gray" style={{ fontSize: 9 }}>{p.action}</span>
151
+ <Link href="/evolution" style={{ textDecoration: 'none', color: 'inherit' }}>
152
+ <div className="card" style={{ cursor: 'pointer', height: '100%' }}>
153
+ <div className="card-body">
154
+ <div className="card-header-label">Recent Evolution</div>
155
+ {recent.map((iter, idx) => (
156
+ <div key={`${iter.id}-${idx}`} style={{
157
+ padding: '10px 0',
158
+ borderBottom: '1px solid var(--border-subtle)',
159
+ }}>
160
+ <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11, marginBottom: 8 }}>
161
+ <span style={{ fontWeight: 600, color: 'var(--text)' }}>{iter.id}</span>
162
+ <span style={{ color: 'var(--text-muted)' }}>{new Date(iter.timestamp).toLocaleDateString()}</span>
165
163
  </div>
166
- ))}
167
- </div>
168
- ))}
164
+ {iter.proposals.map(p => (
165
+ <div key={p.id} style={{
166
+ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, marginBottom: 4,
167
+ padding: '4px 0',
168
+ }}>
169
+ <span style={{
170
+ width: 8, height: 8, borderRadius: '50%', flexShrink: 0,
171
+ background: p.outcome === 'accepted' ? 'var(--green)' : 'var(--red)',
172
+ }} />
173
+ <span style={{ fontWeight: 500 }}>{p.targetSkill}</span>
174
+ <span className="badge badge-gray" style={{ fontSize: 9 }}>{p.action}</span>
175
+ </div>
176
+ ))}
177
+ </div>
178
+ ))}
179
+ </div>
169
180
  </div>
170
- </div>
181
+ </Link>
171
182
  </div>
172
183
 
173
184
  {/* All Skills Summary */}
174
- <div className="card" style={{ marginTop: 16 }}>
175
- <div className="card-body">
176
- <div className="card-header-label">All Skills</div>
177
- <div className="grid-2" style={{ gap: 6 }}>
178
- {graph.nodes.sort((a, b) => b.score - a.score).map(n => (
179
- <div key={n.id} style={{
180
- display: 'flex', alignItems: 'center', justifyContent: 'space-between',
181
- padding: '8px 12px', borderRadius: 8, background: 'var(--bg-section)',
182
- }}>
183
- <div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
184
- <span style={{ fontSize: 13, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{n.name}</span>
185
- {n.generation > 0 && <span className="badge badge-green" style={{ fontSize: 9 }}>gen {n.generation}</span>}
186
- </div>
187
- <div style={{ display: 'flex', alignItems: 'center', gap: 6, minWidth: 90, flexShrink: 0 }}>
188
- <div className="score-track" style={{ width: 50 }}>
189
- <div className="score-fill" style={{
190
- width: `${n.score * 100}%`,
191
- background: n.score >= 0.8 ? 'var(--green)' : n.score >= 0.6 ? 'var(--yellow)' : 'var(--red)',
192
- }} />
185
+ <Link href="/network" style={{ textDecoration: 'none', color: 'inherit' }}>
186
+ <div className="card" style={{ marginTop: 16, cursor: 'pointer' }}>
187
+ <div className="card-body">
188
+ <div className="card-header-label">All Skills</div>
189
+ <div className="grid-2" style={{ gap: 6 }}>
190
+ {graph.nodes.sort((a, b) => b.score - a.score).map(n => (
191
+ <div key={n.id} style={{
192
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
193
+ padding: '8px 12px', borderRadius: 8, background: 'var(--bg-section)',
194
+ }}>
195
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
196
+ <span style={{ fontSize: 13, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{n.name}</span>
197
+ {n.generation > 0 && <span className="badge badge-green" style={{ fontSize: 9 }}>gen {n.generation}</span>}
198
+ </div>
199
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6, minWidth: 90, flexShrink: 0 }}>
200
+ <div className="score-track" style={{ width: 50 }}>
201
+ <div className="score-fill" style={{
202
+ width: `${n.score * 100}%`,
203
+ background: n.score >= 0.8 ? 'var(--green)' : n.score >= 0.6 ? 'var(--yellow)' : 'var(--red)',
204
+ }} />
205
+ </div>
206
+ <span style={{ fontSize: 12, fontWeight: 600, color: n.score >= 0.8 ? 'var(--green)' : 'var(--yellow)' }}>
207
+ {(n.score * 100).toFixed(0)}
208
+ </span>
193
209
  </div>
194
- <span style={{ fontSize: 12, fontWeight: 600, color: n.score >= 0.8 ? 'var(--green)' : 'var(--yellow)' }}>
195
- {(n.score * 100).toFixed(0)}
196
- </span>
197
210
  </div>
198
- </div>
199
- ))}
211
+ ))}
212
+ </div>
200
213
  </div>
201
214
  </div>
202
- </div>
215
+ </Link>
203
216
  </div>
204
217
  )
205
218
  }
@@ -0,0 +1,359 @@
1
+ 'use client'
2
+
3
+ import { useState, useRef } from 'react'
4
+
5
+ interface Discovery {
6
+ id: string; title: string; summary: string; score: number; gap: string; lastAccessed: string
7
+ }
8
+
9
+ interface Draft {
10
+ id: string; skillName: string; avgScore: number; passRate: number; iteration: number; hypothesis: string
11
+ }
12
+
13
+ interface KnowledgeBuffer {
14
+ discoveries: Discovery[]
15
+ drafts: Draft[]
16
+ }
17
+
18
+ interface Props {
19
+ buffer: KnowledgeBuffer
20
+ projects: string[]
21
+ }
22
+
23
+ type RunState = 'idle' | 'running' | 'success' | 'error' | 'stopped'
24
+
25
+ const PIPELINE_STEPS = [
26
+ {
27
+ step: 1,
28
+ title: 'Identify Gaps',
29
+ desc: 'Analyze your existing skills and project context to find what capabilities are missing.',
30
+ icon: '🔍',
31
+ color: 'var(--blue)',
32
+ bgColor: 'var(--blue-light)',
33
+ },
34
+ {
35
+ step: 2,
36
+ title: 'Web Search',
37
+ desc: 'Search the web for the latest best practices, tools, and techniques to fill each gap.',
38
+ icon: '🌐',
39
+ color: 'var(--purple)',
40
+ bgColor: 'var(--purple-light)',
41
+ },
42
+ {
43
+ step: 3,
44
+ title: 'Draft Skills',
45
+ desc: 'Generate hypotheses and draft new SKILL.md files from the discoveries.',
46
+ icon: '📝',
47
+ color: 'var(--yellow)',
48
+ bgColor: 'var(--yellow-light)',
49
+ },
50
+ {
51
+ step: 4,
52
+ title: 'Test & Score',
53
+ desc: 'Test each draft against scenarios. Skills scoring ≥7/10 with ≥67% pass rate are accepted.',
54
+ icon: '✓',
55
+ color: 'var(--green)',
56
+ bgColor: 'var(--green-light)',
57
+ },
58
+ ]
59
+
60
+ export default function ResearchClient({ buffer, projects }: Props) {
61
+ const [runState, setRunState] = useState<RunState>('idle')
62
+ const [output, setOutput] = useState<string | null>(null)
63
+ const abortRef = useRef<AbortController | null>(null)
64
+
65
+ const handleRun = async () => {
66
+ setRunState('running')
67
+ setOutput(null)
68
+ const controller = new AbortController()
69
+ abortRef.current = controller
70
+
71
+ try {
72
+ const res = await fetch('/api/run', {
73
+ method: 'POST',
74
+ headers: { 'Content-Type': 'application/json' },
75
+ body: JSON.stringify({ command: 'research' }),
76
+ signal: controller.signal,
77
+ })
78
+ const data = await res.json()
79
+ if (data.stopped) {
80
+ setOutput(data.output ?? 'Stopped')
81
+ setRunState('stopped')
82
+ } else {
83
+ setOutput(data.output ?? data.error ?? 'No output')
84
+ setRunState(data.success ? 'success' : 'error')
85
+ }
86
+ } catch (err: unknown) {
87
+ if (err instanceof Error && err.name === 'AbortError') {
88
+ try { await fetch('/api/run', { method: 'DELETE' }) } catch {}
89
+ setOutput('Stopped by user')
90
+ setRunState('stopped')
91
+ } else {
92
+ setOutput('Network error')
93
+ setRunState('error')
94
+ }
95
+ } finally {
96
+ abortRef.current = null
97
+ }
98
+ }
99
+
100
+ const handleStop = async () => {
101
+ if (abortRef.current) abortRef.current.abort()
102
+ try { await fetch('/api/run', { method: 'DELETE' }) } catch {}
103
+ }
104
+
105
+ const nearPass = buffer.drafts.filter(d => d.passRate >= 0.67)
106
+
107
+ return (
108
+ <div>
109
+ <div className="page-header">
110
+ <h1 className="page-title">Proactive Research</h1>
111
+ <p className="page-desc">
112
+ Discover new skills by searching the web for techniques and best practices your skill set doesn&apos;t cover yet
113
+ </p>
114
+ </div>
115
+
116
+ {/* Pipeline Visualization */}
117
+ <div className="card" style={{ marginBottom: 24, padding: '20px 24px' }}>
118
+ <div style={{ fontSize: 10, fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 16 }}>
119
+ How Research Works
120
+ </div>
121
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 0 }}>
122
+ {PIPELINE_STEPS.map((step, i) => (
123
+ <div key={step.step} style={{ display: 'flex', alignItems: 'flex-start' }}>
124
+ <div style={{ flex: 1 }}>
125
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
126
+ <div style={{
127
+ width: 32, height: 32, borderRadius: 9,
128
+ background: step.bgColor,
129
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
130
+ fontSize: 14, flexShrink: 0,
131
+ }}>
132
+ {step.icon}
133
+ </div>
134
+ <div>
135
+ <div style={{ fontSize: 9, fontWeight: 600, color: step.color, letterSpacing: 0.5 }}>STEP {step.step}</div>
136
+ <div style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)' }}>{step.title}</div>
137
+ </div>
138
+ </div>
139
+ <div style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.5, paddingRight: 12 }}>
140
+ {step.desc}
141
+ </div>
142
+ </div>
143
+ {i < 3 && (
144
+ <svg width="20" height="60" viewBox="0 0 20 60" style={{ flexShrink: 0, marginTop: 8 }}>
145
+ <path d="M4 30h12M12 24l4 6-4 6" stroke="var(--border)" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round" />
146
+ </svg>
147
+ )}
148
+ </div>
149
+ ))}
150
+ </div>
151
+
152
+ {/* Run button */}
153
+ <div style={{ marginTop: 20, paddingTop: 16, borderTop: '1px solid var(--border)' }}>
154
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
155
+ {runState === 'idle' && (
156
+ <button
157
+ onClick={handleRun}
158
+ style={{
159
+ display: 'flex', alignItems: 'center', gap: 6,
160
+ padding: '9px 20px',
161
+ background: 'var(--teal, var(--green))',
162
+ color: '#fff', border: 'none',
163
+ borderRadius: 'var(--radius)',
164
+ fontSize: 13, fontWeight: 600, cursor: 'pointer',
165
+ transition: 'opacity 0.15s',
166
+ }}
167
+ onMouseOver={e => (e.currentTarget.style.opacity = '0.9')}
168
+ onMouseOut={e => (e.currentTarget.style.opacity = '1')}
169
+ >
170
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
171
+ <path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
172
+ </svg>
173
+ Run Research
174
+ </button>
175
+ )}
176
+ {runState === 'running' && (
177
+ <button
178
+ onClick={handleStop}
179
+ style={{
180
+ display: 'flex', alignItems: 'center', gap: 6,
181
+ padding: '9px 20px',
182
+ background: 'var(--red)', color: '#fff', border: 'none',
183
+ borderRadius: 'var(--radius)',
184
+ fontSize: 13, fontWeight: 600, cursor: 'pointer',
185
+ }}
186
+ >
187
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor" stroke="none">
188
+ <rect x="4" y="4" width="16" height="16" rx="2" />
189
+ </svg>
190
+ Stop Research
191
+ </button>
192
+ )}
193
+ {(runState === 'success' || runState === 'error' || runState === 'stopped') && (
194
+ <div style={{ display: 'flex', gap: 8 }}>
195
+ <button onClick={handleRun} style={{
196
+ padding: '8px 16px', background: 'var(--bg-section)', border: '1px solid var(--border)',
197
+ borderRadius: 'var(--radius)', fontSize: 12, fontWeight: 600, cursor: 'pointer',
198
+ color: 'var(--text-secondary)',
199
+ }}>
200
+ Run Again
201
+ </button>
202
+ <button onClick={() => window.location.reload()} style={{
203
+ padding: '8px 16px', background: 'var(--bg-section)', border: '1px solid var(--border)',
204
+ borderRadius: 'var(--radius)', fontSize: 12, fontWeight: 600, cursor: 'pointer',
205
+ color: 'var(--text-secondary)',
206
+ }}>
207
+ Refresh Data
208
+ </button>
209
+ </div>
210
+ )}
211
+ <span style={{ fontSize: 11, color: 'var(--text-muted)' }}>
212
+ {runState === 'running' && 'Searching the web and testing hypotheses...'}
213
+ {runState === 'success' && 'Research complete — refresh to see new discoveries'}
214
+ {runState === 'error' && 'Research failed — check output below'}
215
+ {runState === 'stopped' && 'Research stopped'}
216
+ {runState === 'idle' && 'Analyzes all your skills, searches for gaps, and drafts new skills'}
217
+ </span>
218
+ </div>
219
+ </div>
220
+ </div>
221
+
222
+ {/* Output panel */}
223
+ {runState !== 'idle' && output && (
224
+ <div className="card" style={{ marginBottom: 24, overflow: 'hidden' }}>
225
+ <div style={{
226
+ padding: '10px 16px',
227
+ background: runState === 'success' ? 'var(--green-light)' : runState === 'error' ? 'var(--red-light)' : 'var(--bg-section)',
228
+ borderBottom: '1px solid var(--border)',
229
+ fontSize: 12, fontWeight: 600,
230
+ color: runState === 'success' ? 'var(--green)' : runState === 'error' ? 'var(--red)' : 'var(--text-secondary)',
231
+ }}>
232
+ {runState === 'running' && '● Running research pipeline...'}
233
+ {runState === 'success' && '✓ Research completed'}
234
+ {runState === 'error' && '✗ Research failed'}
235
+ {runState === 'stopped' && '■ Research stopped'}
236
+ </div>
237
+ <pre style={{
238
+ padding: '14px 16px', margin: 0,
239
+ fontSize: 11, lineHeight: 1.55,
240
+ fontFamily: 'var(--font-mono)',
241
+ color: 'var(--text-secondary)',
242
+ maxHeight: 300, overflow: 'auto',
243
+ whiteSpace: 'pre-wrap', wordBreak: 'break-word',
244
+ }}>
245
+ {output}
246
+ </pre>
247
+ </div>
248
+ )}
249
+
250
+ {/* Summary Stats */}
251
+ <div className="grid-3" style={{ marginBottom: 24 }}>
252
+ <div className="stat-card">
253
+ <div className="stat-value" style={{ color: 'var(--blue)' }}>{buffer.discoveries.length}</div>
254
+ <div className="stat-label">Discoveries</div>
255
+ <div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 2 }}>of 50 capacity</div>
256
+ </div>
257
+ <div className="stat-card">
258
+ <div className="stat-value" style={{ color: 'var(--yellow)' }}>{buffer.drafts.length}</div>
259
+ <div className="stat-label">Drafts In Progress</div>
260
+ <div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 2 }}>of 10 capacity</div>
261
+ </div>
262
+ <div className="stat-card">
263
+ <div className="stat-value" style={{ color: 'var(--green)' }}>{nearPass.length}</div>
264
+ <div className="stat-label">Near-Pass Drafts</div>
265
+ <div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 2 }}>≥67% pass rate — close to becoming skills</div>
266
+ </div>
267
+ </div>
268
+
269
+ <div className="grid-2">
270
+ {/* Discoveries */}
271
+ <div className="card">
272
+ <div className="card-body">
273
+ <div className="card-header-label">Discoveries ({buffer.discoveries.length}/50)</div>
274
+ {buffer.discoveries.length === 0 ? (
275
+ <div className="empty-state" style={{ padding: 32 }}>
276
+ <div className="empty-state-icon">
277
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
278
+ <path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
279
+ </svg>
280
+ </div>
281
+ <div className="empty-state-title">No discoveries yet</div>
282
+ <div className="empty-state-desc">
283
+ Click <strong>Run Research</strong> above to search the web for techniques your skills don&apos;t cover
284
+ </div>
285
+ </div>
286
+ ) : (
287
+ buffer.discoveries.sort((a, b) => b.score - a.score).map(d => (
288
+ <div key={d.id} style={{
289
+ padding: '12px 0',
290
+ borderBottom: '1px solid var(--border-subtle)',
291
+ }}>
292
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 4 }}>
293
+ <span style={{ fontSize: 13, fontWeight: 600, lineHeight: 1.3 }}>{d.title}</span>
294
+ <span className="score-pill" style={{ color: 'var(--blue)', flexShrink: 0, marginLeft: 8 }}>
295
+ {(d.score * 100).toFixed(0)}%
296
+ </span>
297
+ </div>
298
+ <div style={{ fontSize: 11, color: 'var(--text-secondary)', lineHeight: 1.5, marginBottom: 6 }}>
299
+ {d.summary.slice(0, 140)}
300
+ </div>
301
+ <span className="badge badge-blue">{d.gap}</span>
302
+ </div>
303
+ ))
304
+ )}
305
+ </div>
306
+ </div>
307
+
308
+ {/* Drafts */}
309
+ <div className="card">
310
+ <div className="card-body">
311
+ <div className="card-header-label">Skill Drafts ({buffer.drafts.length}/10)</div>
312
+ {buffer.drafts.length === 0 ? (
313
+ <div className="empty-state" style={{ padding: 32 }}>
314
+ <div className="empty-state-icon">
315
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
316
+ <path d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
317
+ </svg>
318
+ </div>
319
+ <div className="empty-state-title">No drafts yet</div>
320
+ <div className="empty-state-desc">
321
+ Drafts are skill prototypes that almost passed quality checks. Running research again improves them iteratively.
322
+ </div>
323
+ </div>
324
+ ) : (
325
+ buffer.drafts.sort((a, b) => b.avgScore - a.avgScore).map(d => (
326
+ <div key={d.id} style={{
327
+ padding: '12px 0',
328
+ borderBottom: '1px solid var(--border-subtle)',
329
+ }}>
330
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 4 }}>
331
+ <span style={{ fontSize: 13, fontWeight: 600 }}>{d.skillName}</span>
332
+ <span className={`badge ${d.avgScore >= 6 ? 'badge-green' : 'badge-yellow'}`}>
333
+ {d.avgScore.toFixed(1)}/10 · iter {d.iteration}
334
+ </span>
335
+ </div>
336
+ <div style={{ fontSize: 11, color: 'var(--text-secondary)', lineHeight: 1.5, marginBottom: 6 }}>
337
+ {d.hypothesis.slice(0, 140)}
338
+ </div>
339
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
340
+ <span style={{ fontSize: 11, color: 'var(--text-dim)' }}>Pass rate:</span>
341
+ <div className="score-track" style={{ width: 60, height: 4 }}>
342
+ <div className="score-fill" style={{
343
+ width: `${d.passRate * 100}%`,
344
+ background: d.passRate >= 0.67 ? 'var(--green)' : 'var(--yellow)',
345
+ }} />
346
+ </div>
347
+ <b style={{ fontSize: 11, color: d.passRate >= 0.67 ? 'var(--green)' : 'var(--yellow)' }}>
348
+ {(d.passRate * 100).toFixed(0)}%
349
+ </b>
350
+ </div>
351
+ </div>
352
+ ))
353
+ )}
354
+ </div>
355
+ </div>
356
+ </div>
357
+ </div>
358
+ )
359
+ }
@@ -1,124 +1,11 @@
1
- import { loadBuffer } from '@/lib/data'
1
+ import { loadBuffer, listProjects } from '@/lib/data'
2
+ import ResearchClient from './client'
2
3
 
3
4
  export const dynamic = 'force-dynamic'
4
5
 
5
6
  export default function ResearchPage() {
6
7
  const buffer = loadBuffer()
8
+ const projects = listProjects()
7
9
 
8
- return (
9
- <div>
10
- <div className="page-header">
11
- <h1 className="page-title">Proactive Research</h1>
12
- <p className="page-desc">
13
- Web search discoveries and skill drafts from the knowledge buffer
14
- </p>
15
- </div>
16
-
17
- {/* Summary Stats */}
18
- <div className="grid-3" style={{ marginBottom: 28 }}>
19
- <div className="stat-card">
20
- <div className="stat-value" style={{ color: 'var(--blue)' }}>{buffer.discoveries.length}</div>
21
- <div className="stat-label">Discoveries</div>
22
- <div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 2 }}>of 50 capacity</div>
23
- </div>
24
- <div className="stat-card">
25
- <div className="stat-value" style={{ color: 'var(--yellow)' }}>{buffer.drafts.length}</div>
26
- <div className="stat-label">Drafts</div>
27
- <div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 2 }}>of 10 capacity</div>
28
- </div>
29
- <div className="stat-card">
30
- <div className="stat-value" style={{ color: 'var(--green)' }}>
31
- {buffer.drafts.filter(d => d.passRate >= 0.67).length}
32
- </div>
33
- <div className="stat-label">Near-Pass Drafts</div>
34
- <div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 2 }}>≥67% pass rate</div>
35
- </div>
36
- </div>
37
-
38
- <div className="grid-2">
39
- {/* Discoveries */}
40
- <div className="card">
41
- <div className="card-body">
42
- <div className="card-header-label">Discoveries ({buffer.discoveries.length}/50)</div>
43
- {buffer.discoveries.length === 0 ? (
44
- <div className="empty-state" style={{ padding: 32 }}>
45
- <div className="empty-state-icon">
46
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
47
- <path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
48
- </svg>
49
- </div>
50
- <div className="empty-state-title">No discoveries yet</div>
51
- <div className="empty-state-desc">Run <code>helixevo research</code> to start proactive web research</div>
52
- </div>
53
- ) : (
54
- buffer.discoveries.sort((a, b) => b.score - a.score).map(d => (
55
- <div key={d.id} style={{
56
- padding: '12px 0',
57
- borderBottom: '1px solid var(--border-subtle)',
58
- }}>
59
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 4 }}>
60
- <span style={{ fontSize: 13, fontWeight: 600, lineHeight: 1.3 }}>{d.title}</span>
61
- <span className="score-pill" style={{ color: 'var(--blue)', flexShrink: 0, marginLeft: 8 }}>
62
- {(d.score * 100).toFixed(0)}%
63
- </span>
64
- </div>
65
- <div style={{ fontSize: 11, color: 'var(--text-secondary)', lineHeight: 1.5, marginBottom: 6 }}>
66
- {d.summary.slice(0, 140)}
67
- </div>
68
- <span className="badge badge-blue">{d.gap}</span>
69
- </div>
70
- ))
71
- )}
72
- </div>
73
- </div>
74
-
75
- {/* Drafts */}
76
- <div className="card">
77
- <div className="card-body">
78
- <div className="card-header-label">Drafts ({buffer.drafts.length}/10)</div>
79
- {buffer.drafts.length === 0 ? (
80
- <div className="empty-state" style={{ padding: 32 }}>
81
- <div className="empty-state-icon">
82
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
83
- <path d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
84
- </svg>
85
- </div>
86
- <div className="empty-state-title">No drafts yet</div>
87
- <div className="empty-state-desc">Near-pass proposals are automatically saved here for future iteration</div>
88
- </div>
89
- ) : (
90
- buffer.drafts.sort((a, b) => b.avgScore - a.avgScore).map(d => (
91
- <div key={d.id} style={{
92
- padding: '12px 0',
93
- borderBottom: '1px solid var(--border-subtle)',
94
- }}>
95
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 4 }}>
96
- <span style={{ fontSize: 13, fontWeight: 600 }}>{d.skillName}</span>
97
- <span className={`badge ${d.avgScore >= 6 ? 'badge-green' : 'badge-yellow'}`}>
98
- {d.avgScore.toFixed(1)}/10 · iter {d.iteration}
99
- </span>
100
- </div>
101
- <div style={{ fontSize: 11, color: 'var(--text-secondary)', lineHeight: 1.5, marginBottom: 6 }}>
102
- {d.hypothesis.slice(0, 140)}
103
- </div>
104
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
105
- <span style={{ fontSize: 11, color: 'var(--text-dim)' }}>Pass rate:</span>
106
- <div className="score-track" style={{ width: 60, height: 4 }}>
107
- <div className="score-fill" style={{
108
- width: `${d.passRate * 100}%`,
109
- background: d.passRate >= 0.67 ? 'var(--green)' : 'var(--yellow)',
110
- }} />
111
- </div>
112
- <b style={{ fontSize: 11, color: d.passRate >= 0.67 ? 'var(--green)' : 'var(--yellow)' }}>
113
- {(d.passRate * 100).toFixed(0)}%
114
- </b>
115
- </div>
116
- </div>
117
- ))
118
- )}
119
- </div>
120
- </div>
121
- </div>
122
- </div>
123
- )
10
+ return <ResearchClient buffer={buffer} projects={projects} />
124
11
  }
@@ -7,77 +7,111 @@ interface OverviewActionsProps {
7
7
  hasSkills: boolean
8
8
  hasEdges: boolean
9
9
  unresolvedCount: number
10
+ skillCount: number
11
+ projectList: string[]
10
12
  }
11
13
 
12
- export function OverviewActions({ hasFailures, hasSkills, hasEdges, unresolvedCount }: OverviewActionsProps) {
14
+ // Small inline SVG flow diagrams for each action
15
+ function FlowDiagram({ steps, color }: { steps: string[]; color: string }) {
16
+ return (
17
+ <div style={{ display: 'flex', alignItems: 'center', gap: 0, marginTop: 8, marginBottom: 2, flexWrap: 'wrap', rowGap: 4 }}>
18
+ {steps.map((step, i) => (
19
+ <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 0 }}>
20
+ <div style={{
21
+ fontSize: 9, fontWeight: 600, padding: '2px 6px',
22
+ background: i === steps.length - 1 ? `${color}18` : 'var(--bg-section)',
23
+ color: i === steps.length - 1 ? color : 'var(--text-dim)',
24
+ borderRadius: 4, whiteSpace: 'nowrap',
25
+ border: i === steps.length - 1 ? `1px solid ${color}30` : '1px solid var(--border-subtle)',
26
+ }}>
27
+ {step}
28
+ </div>
29
+ {i < steps.length - 1 && (
30
+ <span style={{ fontSize: 9, color: 'var(--text-muted)', padding: '0 2px' }}>&rarr;</span>
31
+ )}
32
+ </div>
33
+ ))}
34
+ </div>
35
+ )
36
+ }
37
+
38
+ export function OverviewActions({ hasFailures, hasSkills, hasEdges, unresolvedCount, skillCount, projectList }: OverviewActionsProps) {
13
39
  const actions = [
14
40
  {
15
41
  id: 'graph-rebuild',
16
42
  label: 'Organize Skills',
17
- subtitle: 'Map how your skills relate to each otherfinds dependencies, enhancements, and conflicts between them.',
43
+ subtitle: `Analyze ${skillCount} skills to discover how they relate — dependencies, enhancements, and conflicts.`,
18
44
  command: 'graph-rebuild',
19
45
  icon: 'M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1',
20
46
  color: 'var(--purple)',
21
- description: 'Use LLM to infer relationships between skills (depends, enhances, conflicts)',
47
+ description: 'Use LLM to infer relationships between skills',
22
48
  disabled: !hasSkills,
23
49
  disabledReason: 'No skills imported yet — run helixevo init first',
50
+ flowSteps: [`${skillCount} skills`, 'LLM analysis', 'Skill graph'],
24
51
  },
25
52
  {
26
53
  id: 'generalize',
27
54
  label: 'Generalize',
28
- subtitle: 'Find common patterns across multiple skills and create higher-level abstract skills from them.',
55
+ subtitle: 'Find common patterns across your skills and create higher-level abstract skills.',
29
56
  command: 'generalize',
30
57
  icon: 'M5 10l7-7m0 0l7 7m-7-7v18',
31
58
  color: 'var(--blue)',
32
- description: 'Detect patterns across skills and promote to abstract parent skills',
59
+ description: 'Promote cross-skill patterns to abstract parent skills',
33
60
  disabled: !hasSkills,
34
61
  disabledReason: 'No skills imported yet',
62
+ flowSteps: ['All skills', 'Pattern detection', 'Abstract skills'],
35
63
  },
36
64
  {
37
65
  id: 'evolve',
38
- label: `Evolve${unresolvedCount > 0 ? ` (${unresolvedCount} pending)` : ''}`,
66
+ label: `Evolve${unresolvedCount > 0 ? ` (${unresolvedCount})` : ''}`,
39
67
  subtitle: hasFailures
40
- ? `Improve skills based on ${unresolvedCount} correction${unresolvedCount !== 1 ? 's' : ''} you've made. Each correction teaches the system what went wrong.`
41
- : 'Improve skills based on your corrections. Use Craft Agent normally corrections are captured automatically.',
68
+ ? `Use ${unresolvedCount} correction${unresolvedCount !== 1 ? 's' : ''} to propose and test skill improvements.`
69
+ : 'Use your corrections to improve skills. Corrections are captured automatically when you use Craft Agent.',
42
70
  command: 'evolve',
43
71
  icon: 'M13 7h8m0 0v8m0-8l-8 8-4-4-6 6',
44
72
  color: 'var(--green)',
45
- description: 'Evolve skills based on captured failures',
73
+ description: 'Evolve skills based on captured corrections',
46
74
  disabled: !hasFailures,
47
- disabledReason: 'No corrections captured yet — use Craft Agent normally, and when you correct its output, those corrections are captured automatically.',
75
+ disabledReason: 'No corrections yet — use Craft Agent normally. When you correct its output, those corrections are captured automatically.',
76
+ flowSteps: hasFailures
77
+ ? [`${unresolvedCount} corrections`, 'Propose changes', 'Judge & test', 'Apply']
78
+ : ['Corrections', 'Propose', 'Judge', 'Apply'],
48
79
  },
49
80
  {
50
81
  id: 'health',
51
82
  label: 'Health Check',
52
- subtitle: 'Analyze your skill network for gaps, imbalances, and areas that need attention.',
83
+ subtitle: 'Assess your skill network for coverage gaps, cohesion issues, and imbalances.',
53
84
  command: 'health',
54
85
  icon: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z',
55
86
  color: 'var(--yellow)',
56
- description: 'Assess network cohesion, coverage, balance, and cross-project transfer',
87
+ description: 'Assess network cohesion and coverage',
57
88
  disabled: !hasSkills,
58
89
  disabledReason: 'No skills imported yet',
90
+ flowSteps: ['Skill network', 'Coverage analysis', 'Health report'],
59
91
  },
60
92
  {
61
93
  id: 'graph-optimize',
62
- label: 'Optimize',
63
- subtitle: 'Find skills that should be merged, split, or flagged as conflicting with each other.',
94
+ label: 'Optimize Network',
95
+ subtitle: 'Find skills that should be merged, split, or that conflict with each other.',
64
96
  command: 'graph-optimize',
65
97
  icon: 'M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15',
66
98
  color: 'var(--text-secondary)',
67
- description: 'Detect merge/split/conflict opportunities in the skill network',
99
+ description: 'Detect merge/split opportunities',
68
100
  disabled: !hasEdges,
69
- disabledReason: 'Run "Organize Skills" first to build relationships between skills.',
101
+ disabledReason: 'Run "Organize Skills" first to discover relationships between skills.',
102
+ flowSteps: ['Relationships', 'Detect overlaps', 'Merge/split/flag'],
70
103
  },
71
104
  {
72
105
  id: 'research',
73
- label: 'Research',
74
- subtitle: 'Search the web for new techniques and best practices, then draft new skills from discoveries.',
106
+ label: 'Discover New Skills',
107
+ subtitle: `Search the web for techniques your ${skillCount} skills don't cover yet, then draft new skills.`,
75
108
  command: 'research',
76
109
  icon: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z',
77
110
  color: 'var(--purple)',
78
- description: 'Proactive web research to discover new skill opportunities',
111
+ description: 'Proactive web research discover draft → test',
112
+ flowSteps: ['Find gaps', 'Web search', 'Draft skills', 'Test & score'],
79
113
  },
80
114
  ]
81
115
 
82
- return <QuickActions actions={actions} />
116
+ return <QuickActions actions={actions} flowDiagram={FlowDiagram} />
83
117
  }
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useState, useRef } from 'react'
3
+ import { useState, useRef, type ReactNode, type ComponentType } from 'react'
4
4
 
5
5
  interface Action {
6
6
  id: string
@@ -12,15 +12,17 @@ interface Action {
12
12
  description: string
13
13
  disabled?: boolean
14
14
  disabledReason?: string
15
+ flowSteps?: string[]
15
16
  }
16
17
 
17
18
  interface QuickActionsProps {
18
19
  actions: Action[]
20
+ flowDiagram?: ComponentType<{ steps: string[]; color: string }>
19
21
  }
20
22
 
21
23
  type RunState = 'idle' | 'running' | 'success' | 'error' | 'stopped'
22
24
 
23
- export function QuickActions({ actions }: QuickActionsProps) {
25
+ export function QuickActions({ actions, flowDiagram: FlowDiagram }: QuickActionsProps) {
24
26
  const [running, setRunning] = useState<string | null>(null)
25
27
  const [state, setState] = useState<RunState>('idle')
26
28
  const [output, setOutput] = useState<string | null>(null)
@@ -52,7 +54,6 @@ export function QuickActions({ actions }: QuickActionsProps) {
52
54
  }
53
55
  } catch (err: unknown) {
54
56
  if (err instanceof Error && err.name === 'AbortError') {
55
- // Client-side abort — also kill server-side process
56
57
  try { await fetch('/api/run', { method: 'DELETE' }) } catch {}
57
58
  setOutput('Stopped by user')
58
59
  setState('stopped')
@@ -66,14 +67,8 @@ export function QuickActions({ actions }: QuickActionsProps) {
66
67
  }
67
68
 
68
69
  const handleStop = async () => {
69
- // Abort the fetch request
70
- if (abortRef.current) {
71
- abortRef.current.abort()
72
- }
73
- // Also kill server-side process
74
- try {
75
- await fetch('/api/run', { method: 'DELETE' })
76
- } catch {}
70
+ if (abortRef.current) abortRef.current.abort()
71
+ try { await fetch('/api/run', { method: 'DELETE' }) } catch {}
77
72
  }
78
73
 
79
74
  const handleClose = () => {
@@ -99,7 +94,7 @@ export function QuickActions({ actions }: QuickActionsProps) {
99
94
 
100
95
  return (
101
96
  <>
102
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: 10 }}>
97
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', gap: 10 }}>
103
98
  {actions.map(action => (
104
99
  <button
105
100
  key={action.id}
@@ -107,45 +102,60 @@ export function QuickActions({ actions }: QuickActionsProps) {
107
102
  disabled={action.disabled || (running !== null && running !== action.id)}
108
103
  title={action.disabled ? action.disabledReason : action.description}
109
104
  style={{
110
- display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 6,
111
- padding: '12px 14px',
105
+ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 4,
106
+ padding: '14px 16px',
112
107
  background: action.disabled ? 'var(--bg-section)' : 'var(--bg-card)',
113
108
  border: `1px solid ${running === action.id && state === 'running' ? action.color : 'var(--border)'}`,
114
- borderRadius: 'var(--radius)',
109
+ borderRadius: 'var(--radius-lg)',
115
110
  fontSize: 12,
116
111
  fontWeight: 600,
117
112
  color: action.disabled ? 'var(--text-muted)' : 'var(--text-secondary)',
118
113
  cursor: action.disabled ? 'not-allowed' : 'pointer',
119
- opacity: (running !== null && running !== action.id) ? 0.5 : 1,
120
- transition: 'all 0.15s',
114
+ opacity: (running !== null && running !== action.id) ? 0.4 : 1,
115
+ transition: 'all 0.2s',
121
116
  textAlign: 'left',
122
117
  }}
123
118
  >
124
- <div style={{ display: 'flex', alignItems: 'center', gap: 6, width: '100%' }}>
119
+ {/* Header: icon + label */}
120
+ <div style={{ display: 'flex', alignItems: 'center', gap: 7, width: '100%' }}>
125
121
  {running === action.id && state === 'running' ? (
126
122
  <span style={{
127
- width: 14, height: 14, border: '2px solid var(--border)',
123
+ width: 16, height: 16, border: '2px solid var(--border)',
128
124
  borderTopColor: action.color, borderRadius: '50%',
129
125
  animation: 'actionSpin 0.8s linear infinite',
130
126
  flexShrink: 0,
131
127
  }} />
132
128
  ) : (
133
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none"
134
- stroke={action.disabled ? 'var(--text-muted)' : action.color}
135
- strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
136
- <path d={action.icon} />
137
- </svg>
129
+ <div style={{
130
+ width: 26, height: 26, borderRadius: 7, flexShrink: 0,
131
+ background: action.disabled ? 'var(--bg-hover)' : `${action.color}12`,
132
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
133
+ }}>
134
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none"
135
+ stroke={action.disabled ? 'var(--text-muted)' : action.color}
136
+ strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
137
+ <path d={action.icon} />
138
+ </svg>
139
+ </div>
138
140
  )}
139
- {action.label}
141
+ <span style={{ fontSize: 13 }}>{action.label}</span>
140
142
  </div>
143
+
144
+ {/* Subtitle */}
141
145
  {action.subtitle && (
142
146
  <div style={{
143
147
  fontSize: 11, fontWeight: 400, lineHeight: 1.45,
144
148
  color: action.disabled ? 'var(--text-muted)' : 'var(--text-dim)',
149
+ marginTop: 2,
145
150
  }}>
146
151
  {action.disabled ? action.disabledReason : action.subtitle}
147
152
  </div>
148
153
  )}
154
+
155
+ {/* Flow diagram */}
156
+ {FlowDiagram && action.flowSteps && !action.disabled && (
157
+ <FlowDiagram steps={action.flowSteps} color={action.color} />
158
+ )}
149
159
  </button>
150
160
  ))}
151
161
  </div>
@@ -153,13 +163,12 @@ export function QuickActions({ actions }: QuickActionsProps) {
153
163
  {/* Output panel */}
154
164
  {running && state !== 'idle' && (
155
165
  <div style={{
156
- marginTop: 12,
166
+ marginTop: 14,
157
167
  background: 'var(--bg-card)',
158
168
  border: `1px solid ${stateBorderColor}`,
159
169
  borderRadius: 'var(--radius-lg)',
160
170
  overflow: 'hidden',
161
171
  }}>
162
- {/* Header */}
163
172
  <div style={{
164
173
  display: 'flex', justifyContent: 'space-between', alignItems: 'center',
165
174
  padding: '10px 14px',
@@ -218,18 +227,13 @@ export function QuickActions({ actions }: QuickActionsProps) {
218
227
  </div>
219
228
  </div>
220
229
 
221
- {/* Output */}
222
230
  <pre style={{
223
- padding: '12px 14px',
224
- margin: 0,
225
- fontSize: 11,
226
- lineHeight: 1.5,
231
+ padding: '12px 14px', margin: 0,
232
+ fontSize: 11, lineHeight: 1.5,
227
233
  fontFamily: 'var(--font-mono)',
228
234
  color: 'var(--text-secondary)',
229
- maxHeight: 400,
230
- overflow: 'auto',
231
- whiteSpace: 'pre-wrap',
232
- wordBreak: 'break-word',
235
+ maxHeight: 400, overflow: 'auto',
236
+ whiteSpace: 'pre-wrap', wordBreak: 'break-word',
233
237
  }}>
234
238
  {output ?? 'Waiting for output...'}
235
239
  </pre>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helixevo",
3
- "version": "0.2.22",
3
+ "version": "0.2.24",
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": {