sillyspec 3.7.7 → 3.7.9

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.
Files changed (43) hide show
  1. package/.sillyspec/changes/dashboard/design.md +219 -0
  2. package/.sillyspec/plans/2026-04-05-dashboard.md +737 -0
  3. package/.sillyspec/specs/2026-04-05-dashboard-design.md +206 -0
  4. package/bin/sillyspec.js +0 -0
  5. package/package.json +1 -1
  6. package/packages/dashboard/dist/assets/index-Bh-GPjKY.css +1 -0
  7. package/packages/dashboard/dist/assets/index-CrCn5Gg6.js +17 -0
  8. package/packages/dashboard/dist/index.html +16 -0
  9. package/packages/dashboard/index.html +15 -0
  10. package/packages/dashboard/package-lock.json +2164 -0
  11. package/packages/dashboard/package.json +22 -0
  12. package/packages/dashboard/server/executor.js +86 -0
  13. package/packages/dashboard/server/index.js +359 -0
  14. package/packages/dashboard/server/parser.js +154 -0
  15. package/packages/dashboard/server/watcher.js +277 -0
  16. package/packages/dashboard/src/App.vue +154 -0
  17. package/packages/dashboard/src/components/ActionBar.vue +100 -0
  18. package/packages/dashboard/src/components/CommandPalette.vue +117 -0
  19. package/packages/dashboard/src/components/DetailPanel.vue +122 -0
  20. package/packages/dashboard/src/components/LogStream.vue +85 -0
  21. package/packages/dashboard/src/components/PipelineStage.vue +75 -0
  22. package/packages/dashboard/src/components/PipelineView.vue +94 -0
  23. package/packages/dashboard/src/components/ProjectList.vue +152 -0
  24. package/packages/dashboard/src/components/StageBadge.vue +53 -0
  25. package/packages/dashboard/src/components/StepCard.vue +89 -0
  26. package/packages/dashboard/src/composables/useDashboard.js +171 -0
  27. package/packages/dashboard/src/composables/useKeyboard.js +117 -0
  28. package/packages/dashboard/src/composables/useWebSocket.js +129 -0
  29. package/packages/dashboard/src/main.js +5 -0
  30. package/packages/dashboard/src/style.css +132 -0
  31. package/packages/dashboard/vite.config.js +18 -0
  32. package/src/index.js +68 -8
  33. package/src/init.js +23 -1
  34. package/src/progress.js +422 -0
  35. package/src/setup.js +16 -0
  36. package/templates/archive.md +56 -0
  37. package/templates/brainstorm.md +82 -26
  38. package/templates/commit.md +2 -0
  39. package/templates/execute.md +20 -1
  40. package/templates/progress-format.md +90 -0
  41. package/templates/quick.md +36 -3
  42. package/templates/resume-dialog.md +55 -0
  43. package/templates/skills/playwright-e2e/SKILL.md +1 -1
@@ -0,0 +1,737 @@
1
+ # SillySpec Dashboard — 实现计划
2
+
3
+ ## 锚定确认
4
+
5
+ - [x] design.md — 技术方案和文件变更清单
6
+ - [x] src/index.js — CLI 入口,理解现有命令结构
7
+ - [x] package.json — ES Module,Node >=18
8
+
9
+ ## 执行顺序
10
+
11
+ **Wave 1**(基础骨架,并行):
12
+ - Task 1: 项目脚手架 + 依赖安装
13
+ - Task 2: 后端 HTTP + WebSocket 服务
14
+
15
+ **Wave 2**(依赖 Wave 1):
16
+ - Task 3: 文件监听 + 数据解析
17
+ - Task 4: REST API
18
+
19
+ **Wave 3**(依赖 Wave 2):
20
+ - Task 5: 前端三栏布局 + 状态管理
21
+ - Task 6: Pipeline 视图 + StepCard 组件
22
+
23
+ **Wave 4**(依赖 Wave 3):
24
+ - Task 7: 日志流 + 命令面板
25
+ - Task 8: CLI 集成(dashboard 命令)
26
+
27
+ ---
28
+
29
+ ## Task 1: 项目脚手架 + 依赖安装
30
+
31
+ **文件:**
32
+ - 新建:`packages/dashboard/package.json`
33
+ - 新建:`packages/dashboard/vite.config.js`
34
+ - 新建:`packages/dashboard/index.html`
35
+ - 新建:`packages/dashboard/tailwind.config.js`
36
+ - 新建:`packages/dashboard/postcss.config.js`
37
+ - 新建:`packages/dashboard/src/main.js`
38
+ - 新建:`packages/dashboard/src/App.vue`
39
+ - 新建:`packages/dashboard/src/style.css`
40
+
41
+ **步骤:**
42
+
43
+ - [ ] 创建 `packages/dashboard/package.json`:
44
+ ```json
45
+ {
46
+ "name": "@sillyspec/dashboard",
47
+ "version": "1.0.0",
48
+ "type": "module",
49
+ "private": true,
50
+ "scripts": {
51
+ "dev": "vite",
52
+ "build": "vite build",
53
+ "preview": "vite preview"
54
+ },
55
+ "dependencies": {
56
+ "vue": "^3.5.0",
57
+ "ws": "^8.18.0",
58
+ "chokidar": "^4.0.0",
59
+ "open": "^10.1.0"
60
+ },
61
+ "devDependencies": {
62
+ "@vitejs/plugin-vue": "^5.2.0",
63
+ "vite": "^6.0.0",
64
+ "tailwindcss": "^4.0.0",
65
+ "@tailwindcss/vite": "^4.0.0",
66
+ "autoprefixer": "^10.4.0"
67
+ }
68
+ }
69
+ ```
70
+
71
+ - [ ] 创建 `packages/dashboard/vite.config.js`:
72
+ ```javascript
73
+ import { defineConfig } from 'vite'
74
+ import vue from '@vitejs/plugin-vue'
75
+ import tailwindcss from '@tailwindcss/vite'
76
+
77
+ export default defineConfig({
78
+ plugins: [vue(), tailwindcss()],
79
+ build: {
80
+ outDir: 'dist',
81
+ emptyOutDir: true
82
+ },
83
+ server: {
84
+ port: 3456
85
+ }
86
+ })
87
+ ```
88
+
89
+ - [ ] 创建 `packages/dashboard/index.html`:
90
+ ```html
91
+ <!DOCTYPE html>
92
+ <html lang="zh-CN" class="dark">
93
+ <head>
94
+ <meta charset="UTF-8">
95
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
96
+ <title>SillySpec Dashboard</title>
97
+ </head>
98
+ <body class="bg-[#0D1117] text-gray-100">
99
+ <div id="app"></div>
100
+ <script type="module" src="/src/main.js"></script>
101
+ </body>
102
+ </html>
103
+ ```
104
+
105
+ - [ ] 创建 `packages/dashboard/src/style.css`:
106
+ ```css
107
+ @import "tailwindcss";
108
+
109
+ @theme {
110
+ --color-bg: #0D1117;
111
+ --color-primary: #00D4AA;
112
+ --color-warning: #F59E0B;
113
+ --color-danger: #F87171;
114
+ --color-muted: #6B7280;
115
+ --color-surface: #161B22;
116
+ --color-border: #30363D;
117
+ }
118
+
119
+ body {
120
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
121
+ overflow: hidden;
122
+ }
123
+
124
+ /* 动效 */
125
+ @keyframes pulse-glow {
126
+ 0% { box-shadow: 0 0 0 0 rgba(0, 212, 170, 0.4); }
127
+ 70% { box-shadow: 0 0 0 8px rgba(0, 212, 170, 0); }
128
+ 100% { box-shadow: 0 0 0 0 rgba(0, 212, 170, 0); }
129
+ }
130
+
131
+ .pulse-complete { animation: pulse-glow 200ms ease-out; }
132
+
133
+ .fade-enter-active, .fade-leave-active { transition: opacity 100ms; }
134
+ .fade-enter-from, .fade-leave-to { opacity: 0; }
135
+
136
+ /* 日志字体 */
137
+ .font-mono-log { font-family: 'JetBrains Mono', 'Fira Code', monospace; }
138
+ ```
139
+
140
+ - [ ] 创建 `packages/dashboard/src/main.js`:
141
+ ```javascript
142
+ import { createApp } from 'vue'
143
+ import App from './App.vue'
144
+ import './style.css'
145
+
146
+ createApp(App).mount('#app')
147
+ ```
148
+
149
+ - [ ] 创建 `packages/dashboard/src/App.vue`(骨架,后续 Task 填充):
150
+ ```vue
151
+ <template>
152
+ <div class="h-screen flex flex-col">
153
+ <div class="h-full flex">
154
+ <!-- 三栏布局占位 -->
155
+ <div class="w-[200px] bg-[#161B22] border-r border-[#30363D] flex-shrink-0">
156
+ <div class="p-4 text-sm text-[#00D4AA] font-semibold">项目列表</div>
157
+ </div>
158
+ <div class="flex-1 overflow-auto p-6">
159
+ <div class="text-[#6B7280]">选择左侧项目查看详情</div>
160
+ </div>
161
+ <div class="w-[320px] bg-[#161B22] border-l border-[#30363D] flex-shrink-0">
162
+ <div class="p-4 text-sm text-[#6B7280]">详情面板</div>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ </template>
167
+ ```
168
+
169
+ - [ ] 安装依赖:`cd packages/dashboard && npm install`
170
+ - [ ] 验证:`cd packages/dashboard && npm run dev` → 浏览器打开 `http://localhost:3456` 看到三栏骨架
171
+ - [ ] git commit -m "feat(dashboard): project scaffold with Vue 3 + Vite + Tailwind"
172
+
173
+ ---
174
+
175
+ ## Task 2: 后端 HTTP + WebSocket 服务
176
+
177
+ **文件:**
178
+ - 新建:`packages/dashboard/server/index.js`
179
+
180
+ **步骤:**
181
+
182
+ - [ ] 创建 `packages/dashboard/server/index.js`:
183
+ ```javascript
184
+ import { createServer } from 'http'
185
+ import { WebSocketServer } from 'ws'
186
+ import { existsSync, statSync, readFileSync, readdirSync } from 'fs'
187
+ import { join, resolve } from 'path'
188
+
189
+ let wss
190
+
191
+ function broadcast(data) {
192
+ const msg = JSON.stringify(data)
193
+ for (const client of wss.clients) {
194
+ if (client.readyState === 1) client.send(msg)
195
+ }
196
+ }
197
+
198
+ async function startServer({ port = 3456, open: shouldOpen = true } = {}) {
199
+ const server = createServer((req, res) => {
200
+ // API 路由
201
+ if (req.url?.startsWith('/api/')) {
202
+ return handleApi(req, res)
203
+ }
204
+ // SPA fallback: serve dist/ 静态文件
205
+ res.writeHead(404)
206
+ res.end('Not found. Run `npm run build` first.')
207
+ })
208
+
209
+ wss = new WebSocketServer({ server })
210
+ wss.on('connection', (ws) => {
211
+ ws.on('message', (data) => handleMessage(ws, data.toString()))
212
+ })
213
+
214
+ // 导入并启动 watcher(Task 3 实现)
215
+ try {
216
+ const { startWatcher } = await import('./watcher.js')
217
+ startWatcher((event) => broadcast(event))
218
+ } catch {
219
+ console.warn('⚠️ Watcher not available yet')
220
+ }
221
+
222
+ server.listen(port, () => {
223
+ console.log(`🚀 SillySpec Dashboard: http://localhost:${port}`)
224
+ if (shouldOpen) {
225
+ import('open').then(m => m.default(`http://localhost:${port}`)).catch(() => {})
226
+ }
227
+ })
228
+
229
+ return server
230
+ }
231
+
232
+ async function handleApi(req, res) {
233
+ const url = new URL(req.url, 'http://localhost')
234
+ res.setHeader('Content-Type', 'application/json')
235
+
236
+ if (url.pathname === '/api/projects') {
237
+ const projects = discoverProjects()
238
+ res.writeHead(200)
239
+ res.end(JSON.stringify(projects))
240
+ } else if (url.pathname.startsWith('/api/project/')) {
241
+ const name = decodeURIComponent(url.pathname.split('/api/project/')[1])
242
+ const project = getProjectStatus(name)
243
+ if (!project) {
244
+ res.writeHead(404)
245
+ res.end(JSON.stringify({ error: 'Project not found' }))
246
+ return
247
+ }
248
+ res.writeHead(200)
249
+ res.end(JSON.stringify(project))
250
+ } else {
251
+ res.writeHead(404)
252
+ res.end(JSON.stringify({ error: 'Not found' }))
253
+ }
254
+ }
255
+
256
+ function handleMessage(ws, raw) {
257
+ try {
258
+ const msg = JSON.parse(raw)
259
+ if (msg.type === 'cli:execute') {
260
+ const { spawn } = await import('child_process')
261
+ // Task 4 实现 executor
262
+ }
263
+ } catch {}
264
+ }
265
+
266
+ function discoverProjects() {
267
+ const cwd = process.cwd()
268
+ const home = process.env.HOME || '/root'
269
+ const dirs = [cwd]
270
+ try {
271
+ for (const entry of readdirSync(home)) {
272
+ const p = join(home, entry)
273
+ if (statSync(p).isDirectory() && existsSync(join(p, '.sillyspec'))) {
274
+ dirs.push(p)
275
+ }
276
+ }
277
+ } catch {}
278
+ // 去重
279
+ const unique = [...new Set(dirs)]
280
+ return unique.map(p => ({
281
+ name: p.split('/').pop(),
282
+ path: p
283
+ }))
284
+ }
285
+
286
+ function getProjectStatus(name) {
287
+ // 简单实现:读取 STATE.md
288
+ // Task 3 完善
289
+ return null
290
+ }
291
+
292
+ export { startServer, broadcast }
293
+ ```
294
+
295
+ - [ ] 验证:`node packages/dashboard/server/index.js --port 3456` → 输出 `🚀 SillySpec Dashboard: http://localhost:3456`
296
+ - [ ] git commit -m "feat(dashboard): HTTP + WebSocket server"
297
+
298
+ ---
299
+
300
+ ## Task 3: 文件监听 + 数据解析
301
+
302
+ **文件:**
303
+ - 新建:`packages/dashboard/server/watcher.js`
304
+ - 新建:`packages/dashboard/server/parser.js`
305
+
306
+ **步骤:**
307
+
308
+ - [ ] 创建 `packages/dashboard/server/parser.js`:
309
+ ```javascript
310
+ import { existsSync, readFileSync } from 'fs'
311
+ import { join } from 'path'
312
+
313
+ export function parseProjectState(projectPath) {
314
+ const sillyDir = join(projectPath, '.sillyspec')
315
+ if (!existsSync(sillyDir)) return null
316
+
317
+ const state = {}
318
+
319
+ // 1. 解析 STATE.md
320
+ const stateFile = join(sillyDir, 'STATE.md')
321
+ if (existsSync(stateFile)) {
322
+ const content = readFileSync(stateFile, 'utf8')
323
+ state.currentStage = extractField(content, '当前阶段') || 'unknown'
324
+ state.nextStep = extractField(content, '下一步') || ''
325
+ }
326
+
327
+ // 2. 解析 progress.json
328
+ const progressFile = join(sillyDir, '.runtime', 'progress.json')
329
+ if (existsSync(progressFile)) {
330
+ try {
331
+ const data = JSON.parse(readFileSync(progressFile, 'utf8'))
332
+ state.progress = data
333
+ state.stages = data.stages || {}
334
+ } catch {}
335
+ }
336
+
337
+ // 3. 列出 specs
338
+ const specsDir = join(sillyDir, 'specs')
339
+ if (existsSync(specsDir)) {
340
+ const { readdirSync } = await import('fs')
341
+ state.specs = readdirSync(specsDir).filter(f => f.endsWith('.md'))
342
+ }
343
+
344
+ state.lastActive = state.progress?.lastActiveAt || state.progress?._meta?.updatedAt || null
345
+
346
+ return state
347
+ }
348
+
349
+ function extractField(content, fieldName) {
350
+ const match = content.match(new RegExp(`${fieldName}[::]\\s*(.+)`))
351
+ return match ? match[1].trim() : null
352
+ }
353
+ ```
354
+
355
+ - [ ] 创建 `packages/dashboard/server/watcher.js`:
356
+ ```javascript
357
+ import { watch } from 'chokidar'
358
+ import { parseProjectState } from './parser.js'
359
+
360
+ let watcher = null
361
+
362
+ export function startWatcher(onChange) {
363
+ const home = process.env.HOME || '/root'
364
+ const pattern = `${home}/*/.sillyspec/**/*`
365
+
366
+ watcher = watch(pattern, {
367
+ ignored: /node_modules/,
368
+ persistent: true,
369
+ ignoreInitial: true,
370
+ awaitWriteFinish: { stabilityThreshold: 300, pollInterval: 100 }
371
+ })
372
+
373
+ watcher.on('all', (event, filePath) => {
374
+ // 从文件路径反推项目名
375
+ const match = filePath.match(/\/([^/]+)\/\.sillyspec\//)
376
+ if (!match) return
377
+ const projectName = match[1]
378
+ const projectPath = filePath.split('.sillyspec')[0]
379
+
380
+ const state = parseProjectState(projectPath)
381
+ if (state) {
382
+ onChange({ type: 'project:updated', project: { name: projectName, path: projectPath, ...state } })
383
+ }
384
+ })
385
+
386
+ return watcher
387
+ }
388
+
389
+ export function stopWatcher() {
390
+ watcher?.close()
391
+ }
392
+ ```
393
+
394
+ - [ ] 验证:在另一个终端执行 `sillyspec init --dir /tmp/test-project`,确认 WebSocket 推送了 `project:updated` 事件
395
+ - [ ] git commit -m "feat(dashboard): file watcher + state parser"
396
+
397
+ ---
398
+
399
+ ## Task 4: REST API + CLI 执行器
400
+
401
+ **文件:**
402
+ - 修改:`packages/dashboard/server/index.js`(完善 API 路由)
403
+ - 新建:`packages/dashboard/server/executor.js`
404
+
405
+ **步骤:**
406
+
407
+ - [ ] 创建 `packages/dashboard/server/executor.js`:
408
+ ```javascript
409
+ import { spawn } from 'child_process'
410
+
411
+ const runningProcesses = new Map()
412
+
413
+ export function executeCommand(projectPath, command, onOutput, onComplete) {
414
+ const key = `${projectPath}:${command}`
415
+ if (runningProcesses.has(key)) {
416
+ onOutput(`⚠️ Command already running: ${command}`)
417
+ return
418
+ }
419
+
420
+ const child = spawn('npx', ['sillyspec', ...command.split(' ')], {
421
+ cwd: projectPath,
422
+ shell: true,
423
+ env: { ...process.env, FORCE_COLOR: '1' }
424
+ })
425
+
426
+ runningProcesses.set(key, child)
427
+
428
+ child.stdout.on('data', (data) => {
429
+ onOutput(data.toString())
430
+ })
431
+
432
+ child.stderr.on('data', (data) => {
433
+ onOutput(data.toString())
434
+ })
435
+
436
+ child.on('close', (code) => {
437
+ runningProcesses.delete(key)
438
+ onComplete(code)
439
+ })
440
+
441
+ return () => child.kill()
442
+ }
443
+ ```
444
+
445
+ - [ ] 完善 `handleApi` 中的 `/api/project/:name` 路由,调用 `parseProjectState` 返回完整数据
446
+ - [ ] 完善 `handleMessage`,处理 `cli:execute` 消息,调用 `executeCommand` 并通过 `broadcast` 推送输出
447
+ - [ ] 验证:通过 WebSocket 发送 `{ type: 'cli:execute', project: 'test', command: 'progress status' }`,收到 `cli:output` 和 `cli:complete`
448
+ - [ ] git commit -m "feat(dashboard): REST API + CLI executor"
449
+
450
+ ---
451
+
452
+ ## Task 5: 前端三栏布局 + 状态管理
453
+
454
+ **文件:**
455
+ - 新建:`packages/dashboard/src/composables/useDashboard.js`
456
+ - 新建:`packages/dashboard/src/composables/useWebSocket.js`
457
+ - 新建:`packages/dashboard/src/composables/useKeyboard.js`
458
+ - 修改:`packages/dashboard/src/App.vue`
459
+ - 新建:`packages/dashboard/src/components/ProjectList.vue`
460
+
461
+ **步骤:**
462
+
463
+ - [ ] 创建 `packages/dashboard/src/composables/useWebSocket.js`:
464
+ ```javascript
465
+ import { ref, onMounted, onUnmounted } from 'vue'
466
+
467
+ export function useWebSocket(url = `ws://${location.host}`) {
468
+ const connected = ref(false)
469
+ let ws = null
470
+ const handlers = new Map()
471
+
472
+ function connect() {
473
+ ws = new WebSocket(url)
474
+ ws.onopen = () => { connected.value = true }
475
+ ws.onclose = () => { connected.value = false; setTimeout(connect, 3000) }
476
+ ws.onmessage = (e) => {
477
+ const data = JSON.parse(e.data)
478
+ handlers.get(data.type)?.forEach(fn => fn(data))
479
+ }
480
+ }
481
+
482
+ function on(type, handler) {
483
+ if (!handlers.has(type)) handlers.set(type, [])
484
+ handlers.get(type).push(handler)
485
+ }
486
+
487
+ function send(data) {
488
+ if (ws?.readyState === 1) ws.send(JSON.stringify(data))
489
+ }
490
+
491
+ onMounted(connect)
492
+ onUnmounted(() => ws?.close())
493
+
494
+ return { connected, on, send }
495
+ }
496
+ ```
497
+
498
+ - [ ] 创建 `packages/dashboard/src/composables/useDashboard.js`:
499
+ ```javascript
500
+ import { ref, reactive } from 'vue'
501
+
502
+ const state = reactive({
503
+ projects: [],
504
+ activeProject: null,
505
+ activeStep: null,
506
+ logs: [],
507
+ isPanelOpen: true
508
+ })
509
+
510
+ export function useDashboard() {
511
+ function selectProject(project) {
512
+ state.activeProject = project
513
+ state.activeStep = null
514
+ state.logs = []
515
+ }
516
+
517
+ function appendLog(lines) {
518
+ state.logs.push(...lines)
519
+ // 保留最近 500 行
520
+ if (state.logs.length > 500) state.logs.splice(0, state.logs.length - 500)
521
+ }
522
+
523
+ return { state, selectProject, appendLog }
524
+ }
525
+ ```
526
+
527
+ - [ ] 创建 `packages/dashboard/src/composables/useKeyboard.js`:
528
+ ```javascript
529
+ import { onMounted, onUnmounted } from 'vue'
530
+
531
+ export function useKeyboard(handlers) {
532
+ function onKeyDown(e) {
533
+ // Cmd/Ctrl+K
534
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
535
+ e.preventDefault()
536
+ handlers['cmd+k']?.()
537
+ }
538
+ // j/k 导航
539
+ if (!e.metaKey && !e.ctrlKey && !e.altKey) {
540
+ if (e.key === 'j') handlers['j']?.()
541
+ if (e.key === 'k') handlers['k']?.()
542
+ if (e.key === 'Escape') handlers['escape']?.()
543
+ }
544
+ }
545
+
546
+ onMounted(() => document.addEventListener('keydown', onKeyDown))
547
+ onUnmounted(() => document.removeEventListener('keydown', onKeyDown))
548
+ }
549
+ ```
550
+
551
+ - [ ] 创建 `packages/dashboard/src/components/ProjectList.vue`:
552
+ ```vue
553
+ <template>
554
+ <div class="h-full flex flex-col">
555
+ <div class="p-4 text-sm text-[#00D4AA] font-semibold border-b border-[#30363D]">
556
+ 📊 SillySpec
557
+ </div>
558
+ <div class="flex-1 overflow-auto">
559
+ <div v-for="project in projects" :key="project.path"
560
+ @click="$emit('select', project)"
561
+ :class="[
562
+ 'px-4 py-3 cursor-pointer text-sm border-b border-[#30363D] hover:bg-[#1C2128] transition-colors duration-100',
563
+ isActive(project) ? 'bg-[#1C2128] text-[#00D4AA]' : 'text-gray-300'
564
+ ]">
565
+ <div class="font-medium truncate">{{ project.name }}</div>
566
+ <div v-if="project.currentStage" class="text-xs text-[#6B7280] mt-1">
567
+ {{ project.currentStage }}
568
+ </div>
569
+ </div>
570
+ <div v-if="!projects.length" class="p-4 text-sm text-[#6B7280]">
571
+ 未发现 SillySpec 项目
572
+ </div>
573
+ </div>
574
+ </div>
575
+ </template>
576
+
577
+ <script setup>
578
+ const props = defineProps({ projects: Array, activeName: String })
579
+ defineEmits(['select'])
580
+
581
+ function isActive(project) {
582
+ return project.name === props.activeName
583
+ }
584
+ </script>
585
+ ```
586
+
587
+ - [ ] 更新 `App.vue`,集成 WebSocket + 状态管理 + 三栏布局
588
+ - [ ] 验证:启动后端,浏览器中左侧显示项目列表,点击项目高亮
589
+ - [ ] git commit -m "feat(dashboard): three-panel layout + state management"
590
+
591
+ ---
592
+
593
+ ## Task 6: Pipeline 视图 + StepCard 组件
594
+
595
+ **文件:**
596
+ - 新建:`packages/dashboard/src/components/PipelineView.vue`
597
+ - 新建:`packages/dashboard/src/components/StepCard.vue`
598
+ - 新建:`packages/dashboard/src/components/StageBadge.vue`
599
+
600
+ **步骤:**
601
+
602
+ - [ ] 创建 `packages/dashboard/src/components/StageBadge.vue`:
603
+ ```vue
604
+ <template>
605
+ <span :class="badgeClass">
606
+ {{ icon }} {{ label }}
607
+ </span>
608
+ </template>
609
+
610
+ <script setup>
611
+ import { computed } from 'vue'
612
+
613
+ const props = defineProps({ status: String })
614
+
615
+ const configs = {
616
+ completed: { icon: '✅', label: '已完成', class: 'bg-green-900/30 text-green-400' },
617
+ 'in-progress': { icon: '⏳', label: '进行中', class: 'bg-[#00D4AA]/20 text-[#00D4AA]' },
618
+ blocked: { icon: '🟡', label: '阻塞', class: 'bg-yellow-900/30 text-yellow-400' },
619
+ failed: { icon: '🔴', label: '失败', class: 'bg-red-900/30 text-red-400' },
620
+ pending: { icon: '⬜', label: '未开始', class: 'bg-gray-800 text-gray-500' }
621
+ }
622
+
623
+ const config = computed(() => configs[props.status] || configs.pending)
624
+ const icon = computed(() => config.value.icon)
625
+ const label = computed(() => config.value.label)
626
+ const badgeClass = computed(() => `inline-flex items-center gap-1 px-2 py-1 rounded text-xs ${config.value.class}`)
627
+ </script>
628
+ ```
629
+
630
+ - [ ] 创建 `packages/dashboard/src/components/StepCard.vue`:
631
+ 三级信息密度实现:默认显示标题,hover 显示摘要,点击 emit select 事件。参考 design.md 中的模板。
632
+
633
+ - [ ] 创建 `packages/dashboard/src/components/PipelineView.vue`:
634
+ 纵向 pipeline,四个阶段(brainstorm/plan/execute/verify),每个阶段内显示步骤卡片。下方显示时间线(步骤耗时列表)。
635
+
636
+ - [ ] 验证:选择一个有 STATE.md 的项目,pipeline 正确显示四个阶段状态
637
+ - [ ] git commit -m "feat(dashboard): pipeline view + step cards"
638
+
639
+ ---
640
+
641
+ ## Task 7: 日志流 + 命令面板
642
+
643
+ **文件:**
644
+ - 新建:`packages/dashboard/src/components/LogStream.vue`
645
+ - 新建:`packages/dashboard/src/components/CommandPalette.vue`
646
+ - 新建:`packages/dashboard/src/components/DetailPanel.vue`
647
+ - 新建:`packages/dashboard/src/components/ActionBar.vue`
648
+
649
+ **步骤:**
650
+
651
+ - [ ] 创建 `packages/dashboard/src/components/LogStream.vue`:
652
+ - 等宽字体(font-mono-log)
653
+ - 新行淡入动画
654
+ - 顶部搜索框,实时过滤
655
+ - 自动滚动到底部,用户上翻时暂停
656
+ - 最大 500 行,超出裁剪
657
+
658
+ - [ ] 创建 `packages/dashboard/src/components/CommandPalette.vue`:
659
+ - `Cmd+K` 打开,`Escape` 关闭
660
+ - 模糊搜索项目名、阶段名
661
+ - 列表用上下键选择,Enter 确认
662
+ - 遮罩层 + 居中弹窗
663
+
664
+ - [ ] 创建 `packages/dashboard/src/components/DetailPanel.vue`:
665
+ - 右侧面板,可收起
666
+ - 上半部分:步骤详情(结论、决策、用户原话)
667
+ - 下半部分:LogStream 组件
668
+
669
+ - [ ] 创建 `packages/dashboard/src/components/ActionBar.vue`:
670
+ - "下一步" 按钮 → 发送 `cli:execute` 消息
671
+ - 显示当前执行状态(空闲/执行中/完成)
672
+
673
+ - [ ] 验证:搜索日志、命令面板跳转项目、点击下一步按钮
674
+ - [ ] git commit -m "feat(dashboard): log stream + command palette + detail panel"
675
+
676
+ ---
677
+
678
+ ## Task 8: CLI 集成 + 构建优化
679
+
680
+ **文件:**
681
+ - 修改:`src/index.js`(新增 dashboard 命令)
682
+ - 修改:`printUsage`(添加 dashboard 用法)
683
+ - 修改:`packages/dashboard/vite.config.js`(生产构建优化)
684
+
685
+ **步骤:**
686
+
687
+ - [ ] 在 `src/index.js` 的 `switch (command)` 中新增:
688
+ ```javascript
689
+ case 'dashboard': {
690
+ const portIdx = args.indexOf('--port')
691
+ const port = portIdx >= 0 && args[portIdx + 1] ? parseInt(args[portIdx + 1]) : 3456
692
+ const noOpen = args.includes('--no-open')
693
+ const { startServer } = await import('../packages/dashboard/server/index.js')
694
+ await startServer({ port, open: !noOpen })
695
+ break
696
+ }
697
+ ```
698
+
699
+ - [ ] 在 `printUsage` 中添加:
700
+ ```
701
+ sillyspec dashboard 启动可视化仪表盘
702
+ [--port <number>] 端口号(默认 3456)
703
+ [--no-open] 不自动打开浏览器
704
+ ```
705
+
706
+ - [ ] 执行 `cd packages/dashboard && npm run build`,确认 dist/ 生成
707
+ - [ ] 修改 `server/index.js`,生产模式下 serve `dist/` 静态文件:
708
+ ```javascript
709
+ import { readFile } from 'fs/promises'
710
+
711
+ // 在 createServer handler 中:
712
+ if (!req.url?.startsWith('/api/')) {
713
+ try {
714
+ const html = await readFile(join(__dirname, '../dist/index.html'), 'utf8')
715
+ res.writeHead(200, { 'Content-Type': 'text/html' })
716
+ res.end(html)
717
+ return
718
+ } catch {
719
+ res.writeHead(404)
720
+ res.end('Run `npm run build` first.')
721
+ return
722
+ }
723
+ }
724
+ ```
725
+
726
+ - [ ] 验证:全局安装后执行 `sillyspec dashboard`,浏览器自动打开,显示完整仪表盘
727
+ - [ ] git commit -m "feat(dashboard): CLI integration + production build"
728
+
729
+ ---
730
+
731
+ ## 自检门控
732
+
733
+ - [x] 每个 task 包含具体文件路径
734
+ - [x] 每个 task 包含验证命令和预期输出
735
+ - [x] 标注了 Wave 和执行顺序(Wave 1-4)
736
+ - [x] plan 与 design.md 的文件变更清单一致(21 个新增文件 + 2 个修改文件全部覆盖)
737
+ - [x] 代码示例包含完整可运行内容