sillyspec 3.7.14 → 3.7.16

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 (85) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +77 -0
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +591 -0
  3. package/.claude/skills/sillyspec-continue/SKILL.md +44 -0
  4. package/.claude/skills/sillyspec-execute/SKILL.md +233 -0
  5. package/.claude/skills/sillyspec-explore/SKILL.md +96 -0
  6. package/.claude/skills/sillyspec-export/SKILL.md +53 -0
  7. package/.claude/skills/sillyspec-init/SKILL.md +171 -0
  8. package/.claude/skills/sillyspec-plan/SKILL.md +263 -0
  9. package/.claude/skills/sillyspec-propose/SKILL.md +248 -0
  10. package/.claude/skills/sillyspec-quick/SKILL.md +102 -0
  11. package/.claude/skills/sillyspec-resume/SKILL.md +111 -0
  12. package/{templates/scan.md → .claude/skills/sillyspec-scan/SKILL.md} +22 -60
  13. package/.claude/skills/sillyspec-state/SKILL.md +54 -0
  14. package/.claude/skills/sillyspec-status/SKILL.md +131 -0
  15. package/.claude/skills/sillyspec-verify/SKILL.md +146 -0
  16. package/.claude/skills/sillyspec-workspace/SKILL.md +149 -0
  17. package/.sillyspec/changes/run-command-design/design.md +1230 -0
  18. package/.sillyspec/docs/sillyspec/scan/.gitkeep +0 -0
  19. package/.sillyspec/knowledge/INDEX.md +8 -0
  20. package/.sillyspec/knowledge/uncategorized.md +3 -0
  21. package/.sillyspec/projects/sillyspec.yaml +3 -0
  22. package/package.json +1 -1
  23. package/packages/dashboard/dist/assets/index-Bx0cgoK_.js +7446 -0
  24. package/packages/dashboard/dist/assets/index-DbkUSsNO.css +1 -0
  25. package/packages/dashboard/dist/index.html +2 -2
  26. package/packages/dashboard/package-lock.json +220 -0
  27. package/packages/dashboard/package.json +8 -5
  28. package/packages/dashboard/server/index.js +91 -3
  29. package/packages/dashboard/server/parser.js +252 -28
  30. package/packages/dashboard/src/App.vue +54 -8
  31. package/packages/dashboard/src/components/ActionBar.vue +23 -39
  32. package/packages/dashboard/src/components/CommandPalette.vue +40 -65
  33. package/packages/dashboard/src/components/DetailPanel.vue +68 -53
  34. package/packages/dashboard/src/components/DocPreview.vue +137 -20
  35. package/packages/dashboard/src/components/DocTree.vue +48 -26
  36. package/packages/dashboard/src/components/LogStream.vue +12 -32
  37. package/packages/dashboard/src/components/PipelineStage.vue +8 -8
  38. package/packages/dashboard/src/components/PipelineView.vue +35 -43
  39. package/packages/dashboard/src/components/ProjectList.vue +51 -77
  40. package/packages/dashboard/src/components/ProjectOverview.vue +178 -0
  41. package/packages/dashboard/src/components/StageBadge.vue +13 -13
  42. package/packages/dashboard/src/components/StepCard.vue +11 -11
  43. package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -0
  44. package/packages/dashboard/src/components/detail/GitDetail.vue +61 -0
  45. package/packages/dashboard/src/components/detail/TechDetail.vue +43 -0
  46. package/packages/dashboard/src/main.js +4 -1
  47. package/packages/dashboard/src/style.css +14 -14
  48. package/src/index.js +55 -8
  49. package/src/init.js +69 -196
  50. package/src/migrate.js +1 -18
  51. package/src/progress.js +279 -281
  52. package/src/run.js +320 -0
  53. package/src/setup.js +1 -9
  54. package/src/stages/brainstorm.js +210 -0
  55. package/src/stages/execute.js +190 -0
  56. package/src/stages/index.js +22 -0
  57. package/src/stages/plan.js +118 -0
  58. package/src/stages/propose.js +115 -0
  59. package/src/stages/verify.js +98 -0
  60. package/packages/dashboard/dist/assets/index-hNnQCobe.css +0 -1
  61. package/packages/dashboard/dist/assets/index-qgPzQGjk.js +0 -17
  62. package/templates/archive.md +0 -121
  63. package/templates/brainstorm.md +0 -246
  64. package/templates/commit.md +0 -123
  65. package/templates/continue.md +0 -32
  66. package/templates/execute.md +0 -314
  67. package/templates/explore.md +0 -60
  68. package/templates/export.md +0 -21
  69. package/templates/init.md +0 -61
  70. package/templates/plan.md +0 -157
  71. package/templates/progress-format.md +0 -90
  72. package/templates/propose.md +0 -73
  73. package/templates/quick.md +0 -135
  74. package/templates/resume-dialog.md +0 -55
  75. package/templates/resume.md +0 -53
  76. package/templates/scan-quick.md +0 -49
  77. package/templates/skills/playwright-e2e/SKILL.md +0 -340
  78. package/templates/status.md +0 -72
  79. package/templates/verify.md +0 -253
  80. package/templates/workspace-sync.md +0 -89
  81. package/templates/workspace.md +0 -67
  82. /package/.sillyspec/{docs/sillyspec/brainstorm → changes/brainstorm-archive}/2026-04-05-dashboard-design.md +0 -0
  83. /package/.sillyspec/{docs/sillyspec/brainstorm → changes/brainstorm-archive}/2026-04-05-unified-docs-design.md +0 -0
  84. /package/.sillyspec/{specs/2026-04-05-dashboard-design.md → changes/dashboard/design.md.braindraft} +0 -0
  85. /package/.sillyspec/{specs/2026-04-05-unified-docs-design.md → changes/unified-docs-design/design.md} +0 -0
@@ -1,4 +1,5 @@
1
- import { readFileSync, existsSync, readdirSync } from 'fs'
1
+ import { readFileSync, existsSync, readdirSync, statSync } from 'fs'
2
+ import { execSync } from 'child_process'
2
3
  import { join } from 'path'
3
4
  import { fileURLToPath } from 'url'
4
5
  import { dirname } from 'path'
@@ -10,6 +11,243 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
10
11
  * @param {string} projectPath - Path to the project directory
11
12
  * @returns {object} Docs tree grouped by type
12
13
  */
14
+ // Known framework detection keywords in package.json dependencies
15
+ const FRAMEWORK_PATTERNS = [
16
+ { keys: ['react', 'react-dom'], name: 'React' },
17
+ { keys: ['vue'], name: 'Vue' },
18
+ { keys: ['next'], name: 'Next.js' },
19
+ { keys: ['nuxt'], name: 'Nuxt' },
20
+ { keys: ['express'], name: 'Express' },
21
+ { keys: ['koa'], name: 'Koa' },
22
+ { keys: ['fastify'], name: 'Fastify' },
23
+ { keys: ['nestjs', '@nestjs/core'], name: 'NestJS' },
24
+ { keys: ['svelte'], name: 'Svelte' },
25
+ { keys: ['astro'], name: 'Astro' },
26
+ { keys: ['vite'], name: 'Vite' },
27
+ { keys: ['webpack'], name: 'Webpack' },
28
+ { keys: ['typescript'], name: 'TypeScript' },
29
+ { keys: ['tailwindcss'], name: 'Tailwind' },
30
+ { keys: ['prisma'], name: 'Prisma' },
31
+ { keys: ['drizzle-orm'], name: 'Drizzle' },
32
+ ]
33
+
34
+ /**
35
+ * Parse project overview info
36
+ * @param {string} projectPath
37
+ * @returns {object} Overview data
38
+ */
39
+ export function parseProjectOverview(projectPath) {
40
+ const result = {
41
+ techStack: [],
42
+ lastActive: null,
43
+ docStats: { design: 0, plan: 0, archive: 0, changes: 0, scan: 0, quicklog: 0, total: 0 },
44
+ git: { branch: '', lastCommit: '', dirtyCount: 0 }
45
+ }
46
+
47
+ // --- Tech stack ---
48
+ const pkgPath = join(projectPath, 'package.json')
49
+ if (existsSync(pkgPath)) {
50
+ try {
51
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
52
+ const deps = Object.keys({ ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) })
53
+ for (const pattern of FRAMEWORK_PATTERNS) {
54
+ if (pattern.keys.some(k => deps.includes(k))) {
55
+ result.techStack.push(pattern.name)
56
+ }
57
+ }
58
+ } catch {}
59
+ }
60
+ if (existsSync(join(projectPath, 'pom.xml'))) {
61
+ result.techStack.push('Java')
62
+ try {
63
+ const content = readFileSync(join(projectPath, 'pom.xml'), 'utf-8')
64
+ if (content.includes('spring-boot')) result.techStack.push('Spring Boot')
65
+ } catch {}
66
+ }
67
+ if (existsSync(join(projectPath, 'build.gradle')) || existsSync(join(projectPath, 'build.gradle.kts'))) {
68
+ if (!result.techStack.includes('Java')) result.techStack.push('Gradle')
69
+ }
70
+ if (existsSync(join(projectPath, 'requirements.txt')) || existsSync(join(projectPath, 'pyproject.toml'))) {
71
+ result.techStack.push('Python')
72
+ }
73
+ if (existsSync(join(projectPath, 'go.mod'))) {
74
+ result.techStack.push('Go')
75
+ }
76
+ if (result.techStack.length === 0) result.techStack = []
77
+
78
+ // --- Last active ---
79
+ const sillyspecDir = join(projectPath, '.sillyspec')
80
+ const progressPath = join(sillyspecDir, '.runtime', 'progress.json')
81
+ if (existsSync(progressPath)) {
82
+ try {
83
+ const progress = JSON.parse(readFileSync(progressPath, 'utf-8'))
84
+ if (progress.stages) {
85
+ for (const stageData of Object.values(progress.stages)) {
86
+ if (stageData.lastActive && (!result.lastActive || new Date(stageData.lastActive) > new Date(result.lastActive))) {
87
+ result.lastActive = stageData.lastActive
88
+ }
89
+ }
90
+ }
91
+ if (progress.lastActive) result.lastActive = progress.lastActive
92
+ } catch {}
93
+ }
94
+ if (!result.lastActive) {
95
+ // Fallback: most recently modified file in .sillyspec
96
+ try {
97
+ const findRecent = (dir) => {
98
+ let latest = null
99
+ if (!existsSync(dir)) return latest
100
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
101
+ const p = join(dir, entry.name)
102
+ try {
103
+ const s = statSync(p)
104
+ if (entry.isDirectory()) {
105
+ const sub = findRecent(p)
106
+ if (sub && (!latest || sub > latest)) latest = sub
107
+ } else if (!latest || s.mtimeMs > latest) {
108
+ latest = s.mtimeMs
109
+ }
110
+ } catch {}
111
+ }
112
+ return latest
113
+ }
114
+ const ts = findRecent(sillyspecDir)
115
+ if (ts) result.lastActive = new Date(ts).toISOString()
116
+ } catch {}
117
+ }
118
+
119
+ // --- Doc stats ---
120
+ const docsDir = join(sillyspecDir, 'docs')
121
+ if (existsSync(docsDir)) {
122
+ const typeMap = { brainstorm: 'design', plan: 'plan', archive: 'archive', changes: 'changes', scan: 'scan', quicklog: 'quicklog' }
123
+ try {
124
+ for (const projDir of readdirSync(docsDir, { withFileTypes: true }).filter(d => d.isDirectory())) {
125
+ for (const typeDir of readdirSync(join(docsDir, projDir.name), { withFileTypes: true }).filter(d => d.isDirectory())) {
126
+ const key = typeMap[typeDir.name]
127
+ if (!key) continue
128
+ const count = countMdFiles(join(docsDir, projDir.name, typeDir.name))
129
+ result.docStats[key] += count
130
+ result.docStats.total += count
131
+ }
132
+ }
133
+ } catch {}
134
+ }
135
+
136
+ // --- Git info ---
137
+ try {
138
+ result.git.lastCommit = execSync('git log -1 --format=%s', { cwd: projectPath, encoding: 'utf-8' }).trim()
139
+ } catch {}
140
+ try {
141
+ result.git.branch = execSync('git branch --show-current', { cwd: projectPath, encoding: 'utf-8' }).trim()
142
+ } catch {}
143
+ try {
144
+ result.git.dirtyCount = parseInt(execSync('git status --porcelain', { cwd: projectPath, encoding: 'utf-8' }).trim().split('\n').filter(Boolean).length, 10) || 0
145
+ } catch {}
146
+
147
+ return result
148
+ }
149
+
150
+ export function parseGitDetail(projectPath) {
151
+ const result = { branch: '', commits: [], untracked: [] }
152
+ try {
153
+ result.branch = execSync('git branch --show-current', { cwd: projectPath, encoding: 'utf-8' }).trim()
154
+ } catch {}
155
+ try {
156
+ const log = execSync('git log -5 --format=%h|%s|%an|%aI', { cwd: projectPath, encoding: 'utf-8' }).trim()
157
+ result.commits = log.split('\n').filter(Boolean).map(line => {
158
+ const [hash, message, author, date] = line.split('|')
159
+ return { hash, message, author, date }
160
+ })
161
+ } catch {}
162
+ try {
163
+ const status = execSync('git status --porcelain', { cwd: projectPath, encoding: 'utf-8' }).trim()
164
+ result.untracked = status.split('\n').filter(Boolean).map(line => ({
165
+ status: line.slice(0, 2).trim(),
166
+ file: line.slice(3)
167
+ }))
168
+ } catch {}
169
+ return result
170
+ }
171
+
172
+ export function parseTechStackDetail(projectPath) {
173
+ const result = { frameworks: [], dependencies: {}, devDependencies: {} }
174
+ const pkgPath = join(projectPath, 'package.json')
175
+ if (existsSync(pkgPath)) {
176
+ try {
177
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
178
+ result.dependencies = pkg.dependencies || {}
179
+ result.devDependencies = pkg.devDependencies || {}
180
+ const allDeps = Object.keys({ ...result.dependencies, ...result.devDependencies })
181
+ for (const pattern of FRAMEWORK_PATTERNS) {
182
+ if (pattern.keys.some(k => allDeps.includes(k))) {
183
+ result.frameworks.push(pattern.name)
184
+ }
185
+ }
186
+ } catch {}
187
+ }
188
+ return result
189
+ }
190
+
191
+ export function parseDocsList(projectPath) {
192
+ const sillyspecDir = join(projectPath, '.sillyspec')
193
+ const docsDir = join(sillyspecDir, 'docs')
194
+ const groups = []
195
+ if (!existsSync(docsDir)) return groups
196
+ const typeMap = {
197
+ brainstorm: { label: '设计文档', icon: '📋' },
198
+ plan: { label: '实现计划', icon: '📐' },
199
+ archive: { label: '已归档', icon: '📦' },
200
+ changes: { label: '当前变更', icon: '⚙️' },
201
+ scan: { label: '架构文档', icon: '🔍' },
202
+ quicklog: { label: '快速修复', icon: '⚡' }
203
+ }
204
+ try {
205
+ for (const projDir of readdirSync(docsDir, { withFileTypes: true }).filter(d => d.isDirectory())) {
206
+ for (const typeDir of readdirSync(join(docsDir, projDir.name), { withFileTypes: true }).filter(d => d.isDirectory())) {
207
+ const cfg = typeMap[typeDir.name]
208
+ if (!cfg) continue
209
+ const files = listFilesRecursive(join(docsDir, projDir.name, typeDir.name))
210
+ if (files.length) {
211
+ groups.push({ key: typeDir.name, label: cfg.label, icon: cfg.icon, files })
212
+ }
213
+ }
214
+ }
215
+ } catch {}
216
+ return groups
217
+ }
218
+
219
+ function listFilesRecursive(dir) {
220
+ const files = []
221
+ try {
222
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
223
+ const p = join(dir, entry.name)
224
+ if (entry.isDirectory()) {
225
+ const sub = listFilesRecursive(p)
226
+ for (const f of sub) f.name = entry.name + '/' + f.name
227
+ files.push(...sub)
228
+ } else if (entry.name.endsWith('.md')) {
229
+ const s = statSync(p)
230
+ files.push({ name: entry.name, path: p, size: s.size, mtime: s.mtime.toISOString() })
231
+ }
232
+ }
233
+ } catch {}
234
+ return files
235
+ }
236
+
237
+ function countMdFiles(dir) {
238
+ let count = 0
239
+ try {
240
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
241
+ if (entry.isDirectory()) {
242
+ count += countMdFiles(join(dir, entry.name))
243
+ } else if (entry.name.endsWith('.md')) {
244
+ count++
245
+ }
246
+ }
247
+ } catch {}
248
+ return count
249
+ }
250
+
13
251
  export function parseDocsTree(projectPath) {
14
252
  const sillyspecDir = join(projectPath, '.sillyspec')
15
253
  const docsDir = join(sillyspecDir, 'docs')
@@ -97,43 +335,29 @@ export function parseProjectState(projectPath) {
97
335
  return null
98
336
  }
99
337
 
100
- let currentStage = 'unknown'
338
+ let currentStage = ''
101
339
  let nextStep = null
102
340
  let progress = { stages: {} }
103
341
  let stages = []
104
342
  let specs = []
105
343
  let lastActive = null
106
344
 
107
- // Read STATE.md for current stage and next step
108
- const statePath = join(sillyspecDir, 'STATE.md')
109
- if (existsSync(statePath)) {
110
- try {
111
- const stateContent = readFileSync(statePath, 'utf-8')
112
- const stageMatch = stateContent.match(/当前阶段[::]\s*(\w+)/) || stateContent.match(/current_stage:\s*(\w+)/i)
113
- const stepMatch = stateContent.match(/下一步[::]\s*(.+)/) || stateContent.match(/next_step:\s*(.+)/i)
114
-
115
- if (stageMatch) currentStage = stageMatch[1]
116
- if (stepMatch) nextStep = stepMatch[1].trim()
117
- } catch (err) {
118
- // State file exists but couldn't be read
119
- }
120
- }
121
-
122
- // Read progress.json from .runtime directory
345
+ // Read progress.json for current stage
123
346
  const progressPath = join(sillyspecDir, '.runtime', 'progress.json')
124
347
  if (existsSync(progressPath)) {
125
348
  try {
126
- const progressContent = readFileSync(progressPath, 'utf-8')
127
- progress = JSON.parse(progressContent)
128
- stages = Object.keys(progress.stages || {})
349
+ const progressData = JSON.parse(readFileSync(progressPath, 'utf-8'))
350
+ progress = progressData
351
+ currentStage = progressData.currentStage || ''
352
+ stages = Object.keys(progressData.stages || {})
129
353
 
130
- // Find last active stage
131
- if (progress.stages) {
132
- for (const [stageName, stageData] of Object.entries(progress.stages)) {
133
- if (stageData.lastActive) {
134
- if (!lastActive || new Date(stageData.lastActive) > new Date(lastActive)) {
135
- lastActive = stageData.lastActive
136
- }
354
+ // Find last active
355
+ if (progressData.lastActive) lastActive = progressData.lastActive
356
+ if (progressData.stages) {
357
+ for (const [stageName, stageData] of Object.entries(progressData.stages)) {
358
+ if (stageData.lastActive || stageData.startedAt) {
359
+ const t = stageData.lastActive || stageData.startedAt
360
+ if (!lastActive || new Date(t) > new Date(lastActive)) lastActive = t
137
361
  }
138
362
  }
139
363
  }
@@ -1,5 +1,6 @@
1
1
  <template>
2
- <div class="h-screen w-screen flex flex-col overflow-hidden font-[DM_Sans,sans-serif] relative" style="background-color: #151820;">
2
+ <n-config-provider :theme-overrides="themeOverrides">
3
+ <div class="h-screen w-screen flex flex-col overflow-hidden font-[DM_Sans,sans-serif] relative" style="background-color: #F5F5F7;">
3
4
  <!-- Ambient background -->
4
5
  <div class="absolute inset-0 pointer-events-none" style="background: radial-gradient(ellipse 60% 40% at 10% 20%, rgba(251,191,36,0.04) 0%, transparent 70%), radial-gradient(ellipse 50% 50% at 90% 80%, rgba(251,191,36,0.02) 0%, transparent 70%);" />
5
6
 
@@ -11,7 +12,7 @@
11
12
  <!-- Left: Project List -->
12
13
  <aside
13
14
  class="flex-shrink-0 relative overflow-hidden"
14
- :style="`width: ${leftWidth}px; background: #1A1E28; border-right: none;`"
15
+ :style="`width: ${leftWidth}px; background: #FFFFFF; border-right: none;`"
15
16
  >
16
17
  <ProjectList
17
18
  :projects="dashboard.state.projects"
@@ -26,13 +27,14 @@
26
27
 
27
28
  <!-- Left ↔ Center resize handle -->
28
29
  <div
29
- class="w-[2px] flex-shrink-0 cursor-col-resize hover:bg-[#FBBF24] active:bg-[#FBBF24] relative z-20"
30
+ class="w-[2px] flex-shrink-0 cursor-col-resize hover:bg-[#D97706] active:bg-[#D97706] relative z-20"
30
31
  style="background: #2A3040;"
31
32
  @mousedown="startDragLeft"
32
33
  />
33
34
 
34
35
  <!-- Center: Pipeline View -->
35
- <main class="flex-1 overflow-hidden accent-stripe" style="min-width: 300px;">
36
+ <main class="flex-1 overflow-hidden accent-stripe flex flex-col" style="min-width: 300px;">
37
+ <ProjectOverview :project="dashboard.state.activeProject" @show-detail="handleShowDetail" />
36
38
  <PipelineView
37
39
  :project="dashboard.state.activeProject"
38
40
  :active-step="dashboard.state.activeStep"
@@ -50,7 +52,7 @@
50
52
  <!-- Center ↔ Right resize handle -->
51
53
  <div
52
54
  v-if="dashboard.state.isPanelOpen"
53
- class="w-[2px] flex-shrink-0 cursor-col-resize hover:bg-[#FBBF24] active:bg-[#FBBF24] relative z-20"
55
+ class="w-[2px] flex-shrink-0 cursor-col-resize hover:bg-[#D97706] active:bg-[#D97706] relative z-20"
54
56
  style="background: #2A3040;"
55
57
  @mousedown="startDragRight"
56
58
  />
@@ -61,14 +63,17 @@
61
63
  'flex-shrink-0 relative overflow-hidden',
62
64
  dashboard.state.isPanelOpen ? '' : 'w-0'
63
65
  ]"
64
- :style="dashboard.state.isPanelOpen ? `width: ${rightWidth}px; background: #1A1E28; transition: none;` : 'background: #1A1E28;'"
66
+ :style="dashboard.state.isPanelOpen ? `width: ${rightWidth}px; background: #FFFFFF; transition: none;` : 'background: #FFFFFF;'"
65
67
  >
66
68
  <DetailPanel
67
69
  :is-open="dashboard.state.isPanelOpen"
68
70
  :active-step="dashboard.state.activeStep"
69
71
  :logs="dashboard.state.logs"
70
- @close="dashboard.closePanel"
72
+ :detail-type="detailType"
73
+ :detail-data="detailData"
74
+ @close="handleDetailClose"
71
75
  @clear-logs="dashboard.clearLogs"
76
+ @open-doc-file="handleOpenDocFromDetail"
72
77
  />
73
78
  </aside>
74
79
  </div>
@@ -94,10 +99,11 @@
94
99
  @select-stage="handleSelectStage"
95
100
  />
96
101
  </div>
102
+ </n-config-provider>
97
103
  </template>
98
104
 
99
105
  <script setup>
100
- import { ref, onMounted } from 'vue'
106
+ import { ref, onMounted, readonly } from 'vue'
101
107
  import { useWebSocket } from './composables/useWebSocket.js'
102
108
  import { useDashboard } from './composables/useDashboard.js'
103
109
  import { useDashboardKeyboard } from './composables/useKeyboard.js'
@@ -105,6 +111,7 @@ import ProjectList from './components/ProjectList.vue'
105
111
  import PipelineView from './components/PipelineView.vue'
106
112
  import DetailPanel from './components/DetailPanel.vue'
107
113
  import ActionBar from './components/ActionBar.vue'
114
+ import ProjectOverview from './components/ProjectOverview.vue'
108
115
  import CommandPalette from './components/CommandPalette.vue'
109
116
 
110
117
  // Composables
@@ -113,6 +120,8 @@ const dashboard = useDashboard()
113
120
  const isCommandPaletteOpen = ref(false)
114
121
  const executionResult = ref(null)
115
122
  const scanPaths = ref([])
123
+ const detailType = ref(null)
124
+ const detailData = ref(null)
116
125
 
117
126
  // Panel resize state
118
127
  const STORAGE_KEY = 'dashboard-panel-widths'
@@ -264,6 +273,43 @@ function handleAddScanPath(path) {
264
273
  function handleRemoveScanPath(path) {
265
274
  ws.send({ type: 'scan:remove-path', data: { path } })
266
275
  }
276
+
277
+ async function handleShowDetail(type) {
278
+ const project = dashboard.state.activeProject
279
+ if (!project?.path) return
280
+ detailType.value = type
281
+ detailData.value = null
282
+ dashboard.openPanel()
283
+ try {
284
+ const url = `/api/projects/${encodeURIComponent(project.path)}/detail?type=${type}`
285
+ const res = await fetch(url)
286
+ if (res.ok) detailData.value = await res.json()
287
+ } catch {}
288
+ }
289
+
290
+ function handleDetailClose() {
291
+ detailType.value = null
292
+ detailData.value = null
293
+ dashboard.closePanel()
294
+ }
295
+
296
+ function handleOpenDocFromDetail(file) {
297
+ detailType.value = null
298
+ detailData.value = null
299
+ dashboard.setActiveTab('docs')
300
+ handleSelectDocFile(file)
301
+ }
302
+
303
+ const themeOverrides = {
304
+ common: {
305
+ primaryColor: '#D97706',
306
+ primaryColorHover: '#F59E0B',
307
+ primaryColorPressed: '#B45309',
308
+ borderRadius: '6px',
309
+ fontFamily: 'DM Sans, sans-serif',
310
+ fontFamilyMono: 'JetBrains Mono, monospace'
311
+ }
312
+ }
267
313
  </script>
268
314
 
269
315
  <style>
@@ -1,16 +1,16 @@
1
1
  <template>
2
- <div class="h-12 flex items-center justify-between px-5 relative" style="background: rgba(17,17,19,0.9); backdrop-filter: blur(20px); border-top: 1px solid #1F1F22;">
2
+ <div class="h-12 flex items-center justify-between px-5 relative" style="background: rgba(17,17,19,0.9); backdrop-filter: blur(20px); border-top: 1px solid #F0F0F3;">
3
3
  <!-- Ambient top glow -->
4
4
  <div class="absolute inset-x-0 top-0 h-px" style="background: linear-gradient(90deg, transparent, rgba(251,191,36,0.1), transparent);"></div>
5
5
 
6
6
  <!-- Left: Status -->
7
7
  <div class="flex items-center gap-3">
8
8
  <div v-if="project" class="flex items-center gap-2">
9
- <span class="text-[11px] font-[JetBrains_Mono,monospace]" style="color: #8B8FA3;">{{ project.name }}</span>
10
- <span style="color: #1F1F22;">|</span>
9
+ <span class="text-[11px] font-[JetBrains_Mono,monospace]" style="color: #6B7280;">{{ project.name }}</span>
10
+ <span style="color: #F0F0F3;">|</span>
11
11
  <StageBadge v-if="project.state?.currentStage" :status="getProjectStatus()" :label="stageLabel()" />
12
12
  </div>
13
- <div v-else class="text-[10px] font-[JetBrains_Mono,monospace]" style="color: #3A3A3D;">
13
+ <div v-else class="text-[10px] font-[JetBrains_Mono,monospace]" style="color: #D1D1D6;">
14
14
  未选择项目
15
15
  </div>
16
16
  </div>
@@ -18,11 +18,11 @@
18
18
  <!-- Center: Execution -->
19
19
  <div class="flex items-center gap-2">
20
20
  <div v-if="isExecuting" class="flex items-center gap-2">
21
- <div class="w-1 h-1 rounded-full animate-pulse-dot" style="background: #FBBF24;" />
22
- <span class="text-[10px] font-[JetBrains_Mono,monospace]" style="color: #FBBF24;">执行中...</span>
21
+ <div class="w-1 h-1 rounded-full animate-pulse-dot" style="background: #D97706;" />
22
+ <span class="text-[10px] font-[JetBrains_Mono,monospace]" style="color: #D97706;">执行中...</span>
23
23
  </div>
24
24
  <div v-else-if="executionResult" class="flex items-center gap-1.5">
25
- <span class="text-[10px] font-[JetBrains_Mono,monospace]" :style="{ color: executionResult.exitCode === 0 ? '#34D399' : '#EF4444' }">
25
+ <span class="text-[10px] font-[JetBrains_Mono,monospace]" :style="{ color: executionResult.exitCode === 0 ? '#16A34A' : '#DC2626' }">
26
26
  {{ executionResult.exitCode === 0 ? '● 完成' : `● 失败 (${executionResult.exitCode})` }}
27
27
  </span>
28
28
  </div>
@@ -30,46 +30,30 @@
30
30
 
31
31
  <!-- Right: Actions -->
32
32
  <div class="flex items-center gap-1">
33
- <button
34
- @click="$emit('toggle-panel')"
35
- class="p-1.5 rounded-sm transition-colors duration-100"
36
- style="color: #8B8FA3;"
37
- @mouseenter="$event.target.style.color='#FBBF24';$event.target.style.background='rgba(251,191,36,0.06)'"
38
- @mouseleave="$event.target.style.color='#8B8FA3';$event.target.style.background='transparent'"
39
- title="切换详情面板"
40
- >
41
- <svg :class="['w-3.5 h-3.5 transition-transform duration-200', { 'rotate-180': !isPanelOpen }]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
42
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
43
- </svg>
44
- </button>
33
+ <n-button quaternary size="tiny" @click="$emit('toggle-panel')" title="切换详情面板">
34
+ <template #icon>
35
+ <svg :class="['w-3.5 h-3.5 transition-transform duration-200', { 'rotate-180': !isPanelOpen }]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
36
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
37
+ </svg>
38
+ </template>
39
+ </n-button>
45
40
 
46
- <button
47
- v-if="isExecuting"
48
- @click="$emit('kill')"
49
- class="px-2.5 py-1 rounded-sm text-[10px] font-[JetBrains_Mono,monospace] transition-colors duration-100"
50
- style="background: rgba(239,68,68,0.08); border: 1px solid rgba(239,68,68,0.2); color: #EF4444;"
51
- >
41
+ <n-button v-if="isExecuting" size="tiny" type="error" @click="$emit('kill')">
52
42
  停止
53
- </button>
43
+ </n-button>
54
44
 
55
- <button
56
- @click="$emit('open-palette')"
57
- class="p-1.5 rounded-sm transition-colors duration-100"
58
- style="color: #8B8FA3;"
59
- @mouseenter="$event.target.style.color='#FBBF24';$event.target.style.background='rgba(251,191,36,0.06)'"
60
- @mouseleave="$event.target.style.color='#8B8FA3';$event.target.style.background='transparent'"
61
- title="命令面板 (⌘K)"
62
- >
63
- <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
64
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
65
- </svg>
66
- </button>
45
+ <n-button quaternary size="tiny" @click="$emit('open-palette')" title="命令面板 (⌘K)">
46
+ <template #icon>
47
+ <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
48
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
49
+ </svg>
50
+ </template>
51
+ </n-button>
67
52
  </div>
68
53
  </div>
69
54
  </template>
70
55
 
71
56
  <script setup>
72
- import { computed } from 'vue'
73
57
  import StageBadge from './StageBadge.vue'
74
58
 
75
59
  const props = defineProps({
@@ -1,67 +1,49 @@
1
1
  <template>
2
- <Teleport to="body">
3
- <Transition name="backdrop">
4
- <div v-if="isOpen" class="fixed inset-0 z-40" style="background: rgba(0,0,0,0.7); backdrop-filter: blur(4px);" @click="close" />
5
- </Transition>
6
-
7
- <Transition name="palette">
8
- <div v-if="isOpen" class="fixed left-1/2 top-[18%] -translate-x-1/2 w-full max-w-md z-50">
9
- <div class="overflow-hidden rounded-md" style="background: #141416; border: 1px solid #2A2A2D; box-shadow: 0 25px 60px rgba(0,0,0,0.5), 0 0 0 1px rgba(251,191,36,0.05);">
10
- <!-- Search -->
11
- <div class="px-4 py-3" style="border-bottom: 1px solid #1F1F22;">
12
- <div class="flex items-center gap-3">
13
- <svg class="w-3.5 h-3.5 flex-shrink-0" style="color: #8B8FA3;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
14
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
15
- </svg>
16
- <input
17
- ref="searchInput"
18
- v-model="searchQuery"
19
- type="text"
20
- placeholder="搜索项目或命令..."
21
- class="flex-1 bg-transparent border-none outline-none text-[12px] font-[JetBrains_Mono,monospace]"
22
- style="color: #E4E4E7;"
23
- @keydown="handleKeydown"
24
- />
25
- <kbd v-if="searchQuery" class="text-[9px] px-1 py-0.5 rounded-sm font-mono-log" style="color: #8B8FA3; background: #0A0A0B; border: 1px solid #2A2A2D;">ESC</kbd>
26
- </div>
27
- </div>
2
+ <n-modal :show="isOpen" @update:show="$emit('close')" :mask-closable="true" transform-origin="center" style="max-width: 480px;">
3
+ <div class="overflow-hidden rounded-md" style="background: #FFFFFF; border: 1px solid #E5E5EA; box-shadow: 0 25px 60px rgba(0,0,0,0.1);">
4
+ <!-- Search -->
5
+ <div class="px-4 py-3" style="border-bottom: 1px solid #F0F0F3;">
6
+ <n-input v-model:value="searchQuery" placeholder="搜索项目或命令..." ref="searchInput" @keydown="handleKeydown" size="small">
7
+ <template #prefix>
8
+ <svg class="w-3.5 h-3.5" style="color: #6B7280;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
9
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
10
+ </svg>
11
+ </template>
12
+ </n-input>
13
+ </div>
28
14
 
29
- <!-- Results -->
30
- <div class="max-h-72 overflow-y-auto">
31
- <div v-if="filteredItems.length === 0" class="py-10 text-center">
32
- <p class="text-[11px] font-[JetBrains_Mono,monospace]" style="color: #3A3A3D;">无结果</p>
15
+ <!-- Results -->
16
+ <div class="max-h-72 overflow-y-auto">
17
+ <n-empty v-if="filteredItems.length === 0" description="无结果" style="padding: 40px 0;" />
18
+ <div v-else class="py-0.5">
19
+ <div
20
+ v-for="(item, index) in filteredItems"
21
+ :key="item.id"
22
+ :class="['px-4 py-2.5 cursor-pointer transition-colors duration-100 flex items-center gap-3']"
23
+ :style="{ background: selectedIndex === index ? 'rgba(217,119,6,0.06)' : 'transparent' }"
24
+ @click="selectItem(item)"
25
+ @mouseenter="selectedIndex = index"
26
+ >
27
+ <div class="w-6 h-6 rounded-sm flex items-center justify-center text-[10px] font-[JetBrains_Mono,monospace] flex-shrink-0" style="background: #F0F0F3; border: 1px solid #F0F0F3; color: #6B7280;">
28
+ {{ item.type === 'project' ? '□' : '◇' }}
33
29
  </div>
34
- <div v-else class="py-0.5">
35
- <div
36
- v-for="(item, index) in filteredItems"
37
- :key="item.id"
38
- :class="['px-4 py-2.5 cursor-pointer transition-colors duration-100 flex items-center gap-3']"
39
- :style="{ background: selectedIndex === index ? 'rgba(251,191,36,0.06)' : 'transparent' }"
40
- @click="selectItem(item)"
41
- @mouseenter="selectedIndex = index"
42
- >
43
- <div class="w-6 h-6 rounded-sm flex items-center justify-center text-[10px] font-[JetBrains_Mono,monospace] flex-shrink-0" style="background: #0A0A0B; border: 1px solid #1F1F22; color: #8B8FA3;">
44
- {{ item.type === 'project' ? '□' : '◇' }}
45
- </div>
46
- <div class="flex-1 min-w-0">
47
- <div class="text-[12px] font-medium truncate font-[JetBrains_Mono,monospace]" style="color: #E4E4E7;">{{ item.title }}</div>
48
- <div class="text-[10px] truncate" style="color: #8B8FA3;">{{ item.subtitle }}</div>
49
- </div>
50
- <StageBadge v-if="item.status" :status="item.status" size="sm" />
51
- </div>
30
+ <div class="flex-1 min-w-0">
31
+ <div class="text-[12px] font-medium truncate font-[JetBrains_Mono,monospace]" style="color: #1C1C1E;">{{ item.title }}</div>
32
+ <div class="text-[10px] truncate" style="color: #6B7280;">{{ item.subtitle }}</div>
52
33
  </div>
53
- </div>
54
-
55
- <!-- Footer -->
56
- <div class="px-4 py-2 flex items-center gap-4 text-[9px] font-mono-log" style="border-top: 1px solid #1F1F22; color: #3A3A3D;">
57
- <span><kbd class="px-1 rounded-sm" style="background: #0A0A0B; border: 1px solid #1F1F22;">↑↓</kbd> 导航</span>
58
- <span><kbd class="px-1 rounded-sm" style="background: #0A0A0B; border: 1px solid #1F1F22;">↵</kbd> 打开</span>
59
- <span><kbd class="px-1 rounded-sm" style="background: #0A0A0B; border: 1px solid #1F1F22;">esc</kbd> 关闭</span>
34
+ <StageBadge v-if="item.status" :status="item.status" size="sm" />
60
35
  </div>
61
36
  </div>
62
37
  </div>
63
- </Transition>
64
- </Teleport>
38
+
39
+ <!-- Footer -->
40
+ <div class="px-4 py-2 flex items-center gap-4 text-[9px] font-mono-log" style="border-top: 1px solid #F0F0F3; color: #D1D1D6;">
41
+ <span><kbd class="px-1 rounded-sm" style="background: #F0F0F3; border: 1px solid #F0F0F3;">↑↓</kbd> 导航</span>
42
+ <span><kbd class="px-1 rounded-sm" style="background: #F0F0F3; border: 1px solid #F0F0F3;">↵</kbd> 打开</span>
43
+ <span><kbd class="px-1 rounded-sm" style="background: #F0F0F3; border: 1px solid #F0F0F3;">esc</kbd> 关闭</span>
44
+ </div>
45
+ </div>
46
+ </n-modal>
65
47
  </template>
66
48
 
67
49
  <script setup>
@@ -106,12 +88,5 @@ function handleKeydown(e) {
106
88
  else if (e.key === 'Escape') { e.preventDefault(); close() }
107
89
  }
108
90
  watch(filteredItems, () => { selectedIndex.value = 0 })
109
- watch(() => props.isOpen, (v) => { if (v) nextTick(() => { searchInput.value?.focus() }) })
91
+ watch(() => props.isOpen, (v) => { if (v) { nextTick(() => { searchInput.value?.focus() }) } })
110
92
  </script>
111
-
112
- <style scoped>
113
- .backdrop-enter-active, .backdrop-leave-active { transition: opacity 150ms ease; }
114
- .backdrop-enter-from, .backdrop-leave-to { opacity: 0; }
115
- .palette-enter-active, .palette-leave-active { transition: all 200ms cubic-bezier(0.16, 1, 0.3, 1); }
116
- .palette-enter-from, .palette-leave-to { opacity: 0; transform: translate(-50%, -8px) scale(0.98); }
117
- </style>