sillyspec 3.20.2 → 3.20.4

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 (133) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +21 -21
  2. package/.claude/skills/sillyspec-auto/SKILL.md +83 -83
  3. package/.claude/skills/sillyspec-brainstorm/SKILL.md +44 -44
  4. package/.claude/skills/sillyspec-commit/SKILL.md +106 -106
  5. package/.claude/skills/sillyspec-continue/SKILL.md +45 -45
  6. package/.claude/skills/sillyspec-doctor/SKILL.md +31 -31
  7. package/.claude/skills/sillyspec-execute/SKILL.md +30 -30
  8. package/.claude/skills/sillyspec-explore/SKILL.md +109 -109
  9. package/.claude/skills/sillyspec-knowledge/SKILL.md +269 -269
  10. package/.claude/skills/sillyspec-plan/SKILL.md +21 -21
  11. package/.claude/skills/sillyspec-propose/SKILL.md +21 -21
  12. package/.claude/skills/sillyspec-quick/SKILL.md +21 -21
  13. package/.claude/skills/sillyspec-resume/SKILL.md +68 -68
  14. package/.claude/skills/sillyspec-scan/SKILL.md +21 -21
  15. package/.claude/skills/sillyspec-state/SKILL.md +54 -54
  16. package/.claude/skills/sillyspec-status/SKILL.md +21 -21
  17. package/.claude/skills/sillyspec-verify/SKILL.md +21 -21
  18. package/.claude/skills/sillyspec-workspace/SKILL.md +157 -157
  19. package/.husky/pre-push +13 -13
  20. package/CLAUDE.md +18 -18
  21. package/README.md +198 -188
  22. package/SKILL.md +90 -91
  23. package/bin/sillyspec.js +2 -2
  24. package/docs/brainstorm-plan-contract.md +64 -64
  25. package/docs/plan-execute-contract.md +123 -123
  26. package/docs/platform-scan-protocol.md +298 -298
  27. package/docs/revision-mode.md +115 -115
  28. package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +99 -99
  29. package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +218 -218
  30. package/docs/sillyspec/file-lifecycle/stage-artifacts.md +167 -167
  31. package/docs/sillyspec/file-lifecycle/storage-and-state.md +148 -148
  32. package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +211 -193
  33. package/docs/sillyspec/file-lifecycle.md +125 -125
  34. package/docs/workflow-contract-regression.md +106 -106
  35. package/docs/worktree-isolation.md +252 -252
  36. package/package.json +40 -40
  37. package/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -7446
  38. package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -1
  39. package/packages/dashboard/dist/index.html +16 -16
  40. package/packages/dashboard/index.html +15 -15
  41. package/packages/dashboard/package-lock.json +2384 -2384
  42. package/packages/dashboard/package.json +25 -25
  43. package/packages/dashboard/server/executor.js +86 -86
  44. package/packages/dashboard/server/index.js +588 -588
  45. package/packages/dashboard/server/parser.js +526 -526
  46. package/packages/dashboard/server/watcher.js +344 -344
  47. package/packages/dashboard/src/App.vue +558 -558
  48. package/packages/dashboard/src/components/ActionBar.vue +93 -93
  49. package/packages/dashboard/src/components/CommandPalette.vue +96 -96
  50. package/packages/dashboard/src/components/DetailPanel.vue +137 -137
  51. package/packages/dashboard/src/components/LogStream.vue +65 -65
  52. package/packages/dashboard/src/components/PipelineStage.vue +95 -95
  53. package/packages/dashboard/src/components/PipelineView.vue +156 -156
  54. package/packages/dashboard/src/components/ProjectList.vue +210 -210
  55. package/packages/dashboard/src/components/StageBadge.vue +67 -67
  56. package/packages/dashboard/src/components/StepCard.vue +94 -94
  57. package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -48
  58. package/packages/dashboard/src/components/detail/GitDetail.vue +61 -61
  59. package/packages/dashboard/src/components/detail/TechDetail.vue +43 -43
  60. package/packages/dashboard/src/composables/useDashboard.js +170 -170
  61. package/packages/dashboard/src/composables/useKeyboard.js +119 -119
  62. package/packages/dashboard/src/composables/useWebSocket.js +129 -129
  63. package/packages/dashboard/src/main.js +8 -8
  64. package/packages/dashboard/src/style.css +132 -132
  65. package/packages/dashboard/vite.config.js +18 -18
  66. package/src/brainstorm-postcheck.js +158 -158
  67. package/src/change-list.js +52 -52
  68. package/src/change-risk-profile.js +352 -352
  69. package/src/classify-change.js +73 -73
  70. package/src/constants.js +70 -70
  71. package/src/contract-matrix.js +278 -278
  72. package/src/db.js +201 -201
  73. package/src/endpoint-extractor.js +315 -315
  74. package/src/hooks/claude-pre-tool-use.cjs +125 -125
  75. package/src/hooks/worktree-guard.js +653 -653
  76. package/src/index.js +922 -900
  77. package/src/init.js +431 -431
  78. package/src/knowledge-match.js +130 -130
  79. package/src/migrate.js +117 -117
  80. package/src/modules.js +482 -482
  81. package/src/progress.js +1734 -1734
  82. package/src/run.js +3465 -3358
  83. package/src/scan-postcheck.js +387 -383
  84. package/src/setup.js +398 -398
  85. package/src/stage-contract.js +700 -700
  86. package/src/stages/archive.js +160 -160
  87. package/src/stages/brainstorm-auto.js +229 -229
  88. package/src/stages/brainstorm.js +645 -645
  89. package/src/stages/doctor.js +365 -365
  90. package/src/stages/execute.js +625 -625
  91. package/src/stages/explore.js +34 -34
  92. package/src/stages/index.js +29 -29
  93. package/src/stages/knowledge.js +498 -498
  94. package/src/stages/plan-postcheck.js +511 -513
  95. package/src/stages/plan.js +582 -582
  96. package/src/stages/propose.js +174 -174
  97. package/src/stages/quick.js +82 -82
  98. package/src/stages/scan.js +558 -558
  99. package/src/stages/status.js +65 -65
  100. package/src/stages/verify.js +322 -322
  101. package/src/sync.js +497 -497
  102. package/src/task-review.js +346 -346
  103. package/src/workflow.js +785 -785
  104. package/src/worktree-apply.js +549 -549
  105. package/src/worktree-deps.js +185 -0
  106. package/src/worktree.js +982 -932
  107. package/templates/workflows/archive-impact.yaml +79 -79
  108. package/templates/workflows/scan-docs.yaml +132 -132
  109. package/test/brainstorm-plan-contract.test.mjs +273 -273
  110. package/test/check-syntax.mjs +26 -26
  111. package/test/contract-artifacts.test.mjs +323 -323
  112. package/test/decision-supersede.test.mjs +277 -277
  113. package/test/knowledge-match.test.mjs +231 -231
  114. package/test/plan-execute-contract.test.mjs +330 -330
  115. package/test/plan-optimization.test.mjs +572 -572
  116. package/test/platform-artifacts.test.mjs +166 -166
  117. package/test/platform-failure-samples.test.mjs +199 -199
  118. package/test/platform-recovery-chain.test.mjs +167 -167
  119. package/test/platform-recovery.test.mjs +136 -136
  120. package/test/platform-scan-p0.test.mjs +168 -168
  121. package/test/revision-v1.test.mjs +1145 -1145
  122. package/test/run-scan-project-parse.test.mjs +200 -200
  123. package/test/run-tests.mjs +48 -48
  124. package/test/scan-knowledge.test.mjs +175 -175
  125. package/test/scan-paths.test.mjs +68 -68
  126. package/test/scan-postcheck.test.mjs +197 -197
  127. package/test/spec-dir.test.mjs +206 -206
  128. package/test/stage-contract.test.mjs +299 -299
  129. package/test/stage-definitions.test.mjs +39 -39
  130. package/test/wait-gates.test.mjs +496 -496
  131. package/test/worktree-deps-provision.test.mjs +148 -0
  132. package/test/worktree-guard.test.mjs +71 -71
  133. package/test/worktree-native-overlay.test.mjs +188 -188
@@ -1,210 +1,210 @@
1
- <template>
2
- <div class="h-full flex flex-col noise-bg">
3
- <!-- Header -->
4
- <div class="relative z-10 px-5 pt-5 pb-4">
5
- <div class="flex items-center gap-3">
6
- <div class="w-8 h-8 rounded-md flex items-center justify-center" style="background: linear-gradient(135deg, #D97706 0%, #F59E0B 100%); clip-path: polygon(0 0, 100% 0, 85% 100%, 15% 100%);">
7
- <span class="text-[10px] font-bold text-black font-[JetBrains_Mono,monospace]">S</span>
8
- </div>
9
- <img src="/logo.jpg" style="width:28px;height:28px;border-radius:6px;margin-right:8px;">
10
- <div class="flex-1">
11
- <h1 class="text-[13px] font-semibold tracking-tight font-[JetBrains_Mono,monospace]" style="color: #1C1C1E;">
12
- SillySpec
13
- </h1>
14
- <p class="text-[10px] tracking-widest uppercase" style="color: #6B7280;">控制台</p>
15
- </div>
16
- <!-- Scan paths gear button -->
17
- <n-button quaternary size="tiny" @click="showScanPanel = !showScanPanel" :type="showScanPanel ? 'primary' : 'default'">
18
- <template #icon>
19
- <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
20
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
21
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
22
- </svg>
23
- </template>
24
- </n-button>
25
- </div>
26
-
27
- <!-- Scan paths panel (inline) -->
28
- <Transition name="slide">
29
- <div v-if="showScanPanel" class="mt-3 rounded-md p-3" style="background: #FFFFFF; border: 1px solid #F0F0F3;">
30
- <div class="text-[10px] font-semibold uppercase tracking-[0.15em] mb-2 font-[JetBrains_Mono,monospace]" style="color: #6B7280;">扫描路径</div>
31
-
32
- <div v-if="scanPaths.length === 0" class="text-[10px] py-1" style="color: #D1D1D6;">暂无自定义路径</div>
33
- <div v-else class="space-y-1 mb-2">
34
- <div v-for="(p, i) in scanPaths" :key="i" class="flex items-center gap-2 text-[10px] group">
35
- <span class="flex-1 truncate font-mono-log" style="color: #636366;">{{ p }}</span>
36
- <n-button quaternary size="tiny" type="error" @click="removePath(p)">✕</n-button>
37
- </div>
38
- </div>
39
-
40
- <!-- Add path -->
41
- <div v-if="showAddInput" class="flex items-center gap-2">
42
- <n-input v-model:value="newPath" size="tiny" placeholder="输入目录路径..." @keydown.enter="addPath" @keydown.escape="showAddInput = false" ref="pathInput" />
43
- <n-button size="tiny" type="primary" @click="addPath">添加</n-button>
44
- </div>
45
- <n-button v-else size="tiny" dashed @click="showAddInput = true">+ 添加目录</n-button>
46
- </div>
47
- </Transition>
48
- </div>
49
-
50
- <!-- Divider -->
51
- <div class="mx-4 h-px" style="background: linear-gradient(90deg, transparent, #E5E5EA, transparent);"></div>
52
-
53
- <!-- Projects List -->
54
- <div class="flex-1 overflow-y-auto py-3 relative z-10">
55
- <!-- Loading skeleton -->
56
- <div v-if="isLoading" class="px-4 space-y-2">
57
- <n-card v-for="i in 4" :key="i" size="small" :bordered="false">
58
- <n-skeleton text :width="80" size="small" />
59
- <n-skeleton text :width="140" size="small" style="margin-top: 6px;" />
60
- </n-card>
61
- <p class="text-center text-[10px] mt-4 font-[JetBrains_Mono,monospace]" style="color: #6B7280;">
62
- 正在扫描项目...
63
- </p>
64
- </div>
65
-
66
- <!-- Empty state -->
67
- <n-empty v-else-if="projects.length === 0" description="未发现 SillySpec 项目" style="padding: 48px 0;" />
68
-
69
- <!-- Projects -->
70
- <div v-else class="px-3 space-y-0.5">
71
- <div
72
- v-for="project in projects"
73
- :key="project.path"
74
- :class="['relative rounded-md cursor-pointer transition-all duration-150 overflow-hidden group']"
75
- :style="{
76
- background: isActive(project) ? 'rgba(217,119,6,0.06)' : 'transparent',
77
- borderLeft: isActive(project) ? '2px solid #D97706' : '2px solid transparent',
78
- }"
79
- @mouseenter="$event.currentTarget.style.background = isActive(project) ? 'rgba(217,119,6,0.08)' : 'rgba(255,255,255,0.02)'"
80
- @mouseleave="$event.currentTarget.style.background = isActive(project) ? 'rgba(217,119,6,0.06)' : 'transparent'"
81
- @click="$emit('select', project)"
82
- >
83
- <div class="px-3 py-2.5">
84
- <div class="flex items-center justify-between gap-2">
85
- <div class="flex-1 min-w-0">
86
- <h3
87
- class="text-[12px] font-medium truncate transition-colors duration-150 font-[JetBrains_Mono,monospace]"
88
- :style="{ color: isActive(project) ? '#D97706' : '#1C1C1E' }"
89
- >
90
- {{ project.name }}
91
- </h3>
92
- <p class="text-[10px] mt-0.5 truncate font-mono-log" style="color: #6B7280;">
93
- {{ project.path }}
94
- </p>
95
- </div>
96
- <n-tag
97
- v-if="project.state?.currentStage"
98
- :type="statusTagType(getProjectStatus(project))"
99
- size="small"
100
- :bordered="false"
101
- round
102
- >
103
- {{ stageLabel(project) }}
104
- </n-tag>
105
- </div>
106
-
107
- <!-- Progress -->
108
- <div v-if="project.state?.progress" class="mt-2 h-[2px] rounded-full overflow-hidden" style="background: #FFFFFF;">
109
- <div
110
- class="h-full rounded-full transition-all duration-500 progress-gradient"
111
- :style="{ width: getProjectProgress(project) + '%' }"
112
- />
113
- </div>
114
- </div>
115
- </div>
116
- </div>
117
- </div>
118
-
119
- <!-- Footer -->
120
- <div class="relative z-10 px-4 py-2.5" style="border-top: 1px solid #F0F0F3;">
121
- <div class="flex items-center justify-between">
122
- <span class="text-[10px] font-[JetBrains_Mono,monospace]" style="color: #6B7280;">{{ projects.length }} 个项目</span>
123
- <kbd class="text-[9px] px-1.5 py-0.5 rounded font-mono-log" style="color: #6B7280; background: #FFFFFF; border: 1px solid #E5E5EA;">⌘K</kbd>
124
- </div>
125
- </div>
126
- </div>
127
- </template>
128
-
129
- <script setup>
130
- import { ref, nextTick, watch } from 'vue'
131
-
132
- const props = defineProps({
133
- projects: { type: Array, default: () => [] },
134
- activeProject: { type: Object, default: null },
135
- isLoading: { type: Boolean, default: false },
136
- scanPaths: { type: Array, default: () => [] }
137
- })
138
-
139
- const emit = defineEmits(['select', 'scan:add-path', 'scan:remove-path'])
140
-
141
- const showScanPanel = ref(false)
142
- const showAddInput = ref(false)
143
- const newPath = ref('')
144
- const pathInput = ref(null)
145
-
146
- watch(showAddInput, (v) => {
147
- if (v) nextTick(() => { pathInput.value?.focus() })
148
- })
149
-
150
- function isActive(project) {
151
- return props.activeProject?.path === project.path
152
- }
153
-
154
- function statusTagType(status) {
155
- const map = { 'in-progress': 'warning', 'completed': 'success', 'failed': 'error', 'blocked': 'warning' }
156
- return map[status] || 'default'
157
- }
158
-
159
- function getProjectStatus(project) {
160
- const stage = project.state?.currentStage
161
- if (!stage) return 'pending'
162
- const stageData = project.state?.progress?.stages?.[stage]
163
- if (!stageData) return 'pending'
164
- const steps = stageData.steps || []
165
- if (steps.length === 0) return 'pending'
166
- if (steps.some(s => s.status === 'failed')) return 'failed'
167
- if (steps.some(s => s.status === 'blocked')) return 'blocked'
168
- if (steps.some(s => s.status === 'in-progress')) return 'in-progress'
169
- if (steps.every(s => s.status === 'completed')) return 'completed'
170
- return 'pending'
171
- }
172
-
173
- function stageLabel(project) {
174
- const stage = project.state?.currentStage
175
- const labels = { 'brainstorm': '头脑风暴', 'plan': '规划', 'execute': '执行', 'verify': '验证' }
176
- return labels[stage] || stage || '未知'
177
- }
178
-
179
- function getProjectProgress(project) {
180
- const progress = project.state?.progress
181
- if (!progress) return 0
182
- const stages = progress.stages || {}
183
- const stageNames = ['brainstorm', 'plan', 'execute', 'verify']
184
- let total = 0, done = 0
185
- for (const s of stageNames) {
186
- const st = stages[s]
187
- if (st?.steps) { total += st.steps.length; done += st.steps.filter(x => x.status === 'completed').length }
188
- }
189
- return total === 0 ? 0 : Math.round((done / total) * 100)
190
- }
191
-
192
- function addPath() {
193
- const p = newPath.value.trim()
194
- if (p) {
195
- emit('scan:add-path', p)
196
- newPath.value = ''
197
- showAddInput.value = false
198
- }
199
- }
200
-
201
- function removePath(p) {
202
- emit('scan:remove-path', p)
203
- }
204
- </script>
205
-
206
- <style scoped>
207
- .slide-enter-active, .slide-leave-active { transition: all 200ms ease; }
208
- .slide-enter-from, .slide-leave-to { opacity: 0; max-height: 0; overflow: hidden; }
209
- .slide-enter-to, .slide-leave-from { max-height: 300px; }
210
- </style>
1
+ <template>
2
+ <div class="h-full flex flex-col noise-bg">
3
+ <!-- Header -->
4
+ <div class="relative z-10 px-5 pt-5 pb-4">
5
+ <div class="flex items-center gap-3">
6
+ <div class="w-8 h-8 rounded-md flex items-center justify-center" style="background: linear-gradient(135deg, #D97706 0%, #F59E0B 100%); clip-path: polygon(0 0, 100% 0, 85% 100%, 15% 100%);">
7
+ <span class="text-[10px] font-bold text-black font-[JetBrains_Mono,monospace]">S</span>
8
+ </div>
9
+ <img src="/logo.jpg" style="width:28px;height:28px;border-radius:6px;margin-right:8px;">
10
+ <div class="flex-1">
11
+ <h1 class="text-[13px] font-semibold tracking-tight font-[JetBrains_Mono,monospace]" style="color: #1C1C1E;">
12
+ SillySpec
13
+ </h1>
14
+ <p class="text-[10px] tracking-widest uppercase" style="color: #6B7280;">控制台</p>
15
+ </div>
16
+ <!-- Scan paths gear button -->
17
+ <n-button quaternary size="tiny" @click="showScanPanel = !showScanPanel" :type="showScanPanel ? 'primary' : 'default'">
18
+ <template #icon>
19
+ <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
20
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
21
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
22
+ </svg>
23
+ </template>
24
+ </n-button>
25
+ </div>
26
+
27
+ <!-- Scan paths panel (inline) -->
28
+ <Transition name="slide">
29
+ <div v-if="showScanPanel" class="mt-3 rounded-md p-3" style="background: #FFFFFF; border: 1px solid #F0F0F3;">
30
+ <div class="text-[10px] font-semibold uppercase tracking-[0.15em] mb-2 font-[JetBrains_Mono,monospace]" style="color: #6B7280;">扫描路径</div>
31
+
32
+ <div v-if="scanPaths.length === 0" class="text-[10px] py-1" style="color: #D1D1D6;">暂无自定义路径</div>
33
+ <div v-else class="space-y-1 mb-2">
34
+ <div v-for="(p, i) in scanPaths" :key="i" class="flex items-center gap-2 text-[10px] group">
35
+ <span class="flex-1 truncate font-mono-log" style="color: #636366;">{{ p }}</span>
36
+ <n-button quaternary size="tiny" type="error" @click="removePath(p)">✕</n-button>
37
+ </div>
38
+ </div>
39
+
40
+ <!-- Add path -->
41
+ <div v-if="showAddInput" class="flex items-center gap-2">
42
+ <n-input v-model:value="newPath" size="tiny" placeholder="输入目录路径..." @keydown.enter="addPath" @keydown.escape="showAddInput = false" ref="pathInput" />
43
+ <n-button size="tiny" type="primary" @click="addPath">添加</n-button>
44
+ </div>
45
+ <n-button v-else size="tiny" dashed @click="showAddInput = true">+ 添加目录</n-button>
46
+ </div>
47
+ </Transition>
48
+ </div>
49
+
50
+ <!-- Divider -->
51
+ <div class="mx-4 h-px" style="background: linear-gradient(90deg, transparent, #E5E5EA, transparent);"></div>
52
+
53
+ <!-- Projects List -->
54
+ <div class="flex-1 overflow-y-auto py-3 relative z-10">
55
+ <!-- Loading skeleton -->
56
+ <div v-if="isLoading" class="px-4 space-y-2">
57
+ <n-card v-for="i in 4" :key="i" size="small" :bordered="false">
58
+ <n-skeleton text :width="80" size="small" />
59
+ <n-skeleton text :width="140" size="small" style="margin-top: 6px;" />
60
+ </n-card>
61
+ <p class="text-center text-[10px] mt-4 font-[JetBrains_Mono,monospace]" style="color: #6B7280;">
62
+ 正在扫描项目...
63
+ </p>
64
+ </div>
65
+
66
+ <!-- Empty state -->
67
+ <n-empty v-else-if="projects.length === 0" description="未发现 SillySpec 项目" style="padding: 48px 0;" />
68
+
69
+ <!-- Projects -->
70
+ <div v-else class="px-3 space-y-0.5">
71
+ <div
72
+ v-for="project in projects"
73
+ :key="project.path"
74
+ :class="['relative rounded-md cursor-pointer transition-all duration-150 overflow-hidden group']"
75
+ :style="{
76
+ background: isActive(project) ? 'rgba(217,119,6,0.06)' : 'transparent',
77
+ borderLeft: isActive(project) ? '2px solid #D97706' : '2px solid transparent',
78
+ }"
79
+ @mouseenter="$event.currentTarget.style.background = isActive(project) ? 'rgba(217,119,6,0.08)' : 'rgba(255,255,255,0.02)'"
80
+ @mouseleave="$event.currentTarget.style.background = isActive(project) ? 'rgba(217,119,6,0.06)' : 'transparent'"
81
+ @click="$emit('select', project)"
82
+ >
83
+ <div class="px-3 py-2.5">
84
+ <div class="flex items-center justify-between gap-2">
85
+ <div class="flex-1 min-w-0">
86
+ <h3
87
+ class="text-[12px] font-medium truncate transition-colors duration-150 font-[JetBrains_Mono,monospace]"
88
+ :style="{ color: isActive(project) ? '#D97706' : '#1C1C1E' }"
89
+ >
90
+ {{ project.name }}
91
+ </h3>
92
+ <p class="text-[10px] mt-0.5 truncate font-mono-log" style="color: #6B7280;">
93
+ {{ project.path }}
94
+ </p>
95
+ </div>
96
+ <n-tag
97
+ v-if="project.state?.currentStage"
98
+ :type="statusTagType(getProjectStatus(project))"
99
+ size="small"
100
+ :bordered="false"
101
+ round
102
+ >
103
+ {{ stageLabel(project) }}
104
+ </n-tag>
105
+ </div>
106
+
107
+ <!-- Progress -->
108
+ <div v-if="project.state?.progress" class="mt-2 h-[2px] rounded-full overflow-hidden" style="background: #FFFFFF;">
109
+ <div
110
+ class="h-full rounded-full transition-all duration-500 progress-gradient"
111
+ :style="{ width: getProjectProgress(project) + '%' }"
112
+ />
113
+ </div>
114
+ </div>
115
+ </div>
116
+ </div>
117
+ </div>
118
+
119
+ <!-- Footer -->
120
+ <div class="relative z-10 px-4 py-2.5" style="border-top: 1px solid #F0F0F3;">
121
+ <div class="flex items-center justify-between">
122
+ <span class="text-[10px] font-[JetBrains_Mono,monospace]" style="color: #6B7280;">{{ projects.length }} 个项目</span>
123
+ <kbd class="text-[9px] px-1.5 py-0.5 rounded font-mono-log" style="color: #6B7280; background: #FFFFFF; border: 1px solid #E5E5EA;">⌘K</kbd>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ </template>
128
+
129
+ <script setup>
130
+ import { ref, nextTick, watch } from 'vue'
131
+
132
+ const props = defineProps({
133
+ projects: { type: Array, default: () => [] },
134
+ activeProject: { type: Object, default: null },
135
+ isLoading: { type: Boolean, default: false },
136
+ scanPaths: { type: Array, default: () => [] }
137
+ })
138
+
139
+ const emit = defineEmits(['select', 'scan:add-path', 'scan:remove-path'])
140
+
141
+ const showScanPanel = ref(false)
142
+ const showAddInput = ref(false)
143
+ const newPath = ref('')
144
+ const pathInput = ref(null)
145
+
146
+ watch(showAddInput, (v) => {
147
+ if (v) nextTick(() => { pathInput.value?.focus() })
148
+ })
149
+
150
+ function isActive(project) {
151
+ return props.activeProject?.path === project.path
152
+ }
153
+
154
+ function statusTagType(status) {
155
+ const map = { 'in-progress': 'warning', 'completed': 'success', 'failed': 'error', 'blocked': 'warning' }
156
+ return map[status] || 'default'
157
+ }
158
+
159
+ function getProjectStatus(project) {
160
+ const stage = project.state?.currentStage
161
+ if (!stage) return 'pending'
162
+ const stageData = project.state?.progress?.stages?.[stage]
163
+ if (!stageData) return 'pending'
164
+ const steps = stageData.steps || []
165
+ if (steps.length === 0) return 'pending'
166
+ if (steps.some(s => s.status === 'failed')) return 'failed'
167
+ if (steps.some(s => s.status === 'blocked')) return 'blocked'
168
+ if (steps.some(s => s.status === 'in-progress')) return 'in-progress'
169
+ if (steps.every(s => s.status === 'completed')) return 'completed'
170
+ return 'pending'
171
+ }
172
+
173
+ function stageLabel(project) {
174
+ const stage = project.state?.currentStage
175
+ const labels = { 'brainstorm': '头脑风暴', 'plan': '规划', 'execute': '执行', 'verify': '验证' }
176
+ return labels[stage] || stage || '未知'
177
+ }
178
+
179
+ function getProjectProgress(project) {
180
+ const progress = project.state?.progress
181
+ if (!progress) return 0
182
+ const stages = progress.stages || {}
183
+ const stageNames = ['brainstorm', 'plan', 'execute', 'verify']
184
+ let total = 0, done = 0
185
+ for (const s of stageNames) {
186
+ const st = stages[s]
187
+ if (st?.steps) { total += st.steps.length; done += st.steps.filter(x => x.status === 'completed').length }
188
+ }
189
+ return total === 0 ? 0 : Math.round((done / total) * 100)
190
+ }
191
+
192
+ function addPath() {
193
+ const p = newPath.value.trim()
194
+ if (p) {
195
+ emit('scan:add-path', p)
196
+ newPath.value = ''
197
+ showAddInput.value = false
198
+ }
199
+ }
200
+
201
+ function removePath(p) {
202
+ emit('scan:remove-path', p)
203
+ }
204
+ </script>
205
+
206
+ <style scoped>
207
+ .slide-enter-active, .slide-leave-active { transition: all 200ms ease; }
208
+ .slide-enter-from, .slide-leave-to { opacity: 0; max-height: 0; overflow: hidden; }
209
+ .slide-enter-to, .slide-leave-from { max-height: 300px; }
210
+ </style>
@@ -1,67 +1,67 @@
1
- <template>
2
- <span
3
- :class="['inline-flex items-center gap-1.5 rounded-sm text-[10px] font-medium font-[JetBrains_Mono,monospace] uppercase tracking-wider transition-colors duration-150', sizeClass]"
4
- :style="badgeStyle"
5
- >
6
- <span :class="['w-1 h-1 rounded-full', dotClass]" :style="dotStyle" />
7
- <span>{{ displayLabel }}</span>
8
- </span>
9
- </template>
10
-
11
- <script setup>
12
- import { computed } from 'vue'
13
-
14
- const props = defineProps({
15
- status: { type: String, default: 'pending' },
16
- label: { type: String, default: '' },
17
- size: { type: String, default: 'md' }
18
- })
19
-
20
- const displayLabel = computed(() => {
21
- if (props.label) return props.label
22
- const labels = {
23
- 'completed': 'done',
24
- 'in-progress': 'running',
25
- 'blocked': 'blocked',
26
- 'failed': 'error',
27
- 'revising': 'revising',
28
- 'stale': 'stale',
29
- 'waiting': 'waiting',
30
- }
31
- return labels[props.status] || ''
32
- })
33
-
34
- const sizeClass = computed(() => props.size === 'sm' ? 'px-1.5 py-0.5' : 'px-2 py-1')
35
-
36
- const badgeStyle = computed(() => {
37
- const styles = {
38
- 'completed': { background: 'rgba(22,163,74,0.1)', color: '#16A34A' },
39
- 'in-progress': { background: 'rgba(251,191,36,0.1)', color: '#D97706' },
40
- 'blocked': { background: 'rgba(251,146,60,0.1)', color: '#EA580C' },
41
- 'failed': { background: 'rgba(239,68,68,0.1)', color: '#DC2626' },
42
- 'pending': { background: 'rgba(82,82,82,0.15)', color: '#6B7280' },
43
- 'revising': { background: 'rgba(139,92,246,0.1)', color: '#7C3AED' },
44
- 'stale': { background: 'rgba(234,179,8,0.1)', color: '#CA8A04' },
45
- 'waiting': { background: 'rgba(59,130,246,0.1)', color: '#2563EB' },
46
- }
47
- return styles[props.status] || styles.pending
48
- })
49
-
50
- const dotClass = computed(() => {
51
- return props.status === 'in-progress' ? 'animate-pulse-dot' : ''
52
- })
53
-
54
- const dotStyle = computed(() => {
55
- const colors = {
56
- 'completed': '#16A34A',
57
- 'in-progress': '#D97706',
58
- 'blocked': '#EA580C',
59
- 'failed': '#DC2626',
60
- 'pending': '#6B7280',
61
- 'revising': '#7C3AED',
62
- 'stale': '#CA8A04',
63
- 'waiting': '#2563EB',
64
- }
65
- return { background: colors[props.status] || '#6B7280' }
66
- })
67
- </script>
1
+ <template>
2
+ <span
3
+ :class="['inline-flex items-center gap-1.5 rounded-sm text-[10px] font-medium font-[JetBrains_Mono,monospace] uppercase tracking-wider transition-colors duration-150', sizeClass]"
4
+ :style="badgeStyle"
5
+ >
6
+ <span :class="['w-1 h-1 rounded-full', dotClass]" :style="dotStyle" />
7
+ <span>{{ displayLabel }}</span>
8
+ </span>
9
+ </template>
10
+
11
+ <script setup>
12
+ import { computed } from 'vue'
13
+
14
+ const props = defineProps({
15
+ status: { type: String, default: 'pending' },
16
+ label: { type: String, default: '' },
17
+ size: { type: String, default: 'md' }
18
+ })
19
+
20
+ const displayLabel = computed(() => {
21
+ if (props.label) return props.label
22
+ const labels = {
23
+ 'completed': 'done',
24
+ 'in-progress': 'running',
25
+ 'blocked': 'blocked',
26
+ 'failed': 'error',
27
+ 'revising': 'revising',
28
+ 'stale': 'stale',
29
+ 'waiting': 'waiting',
30
+ }
31
+ return labels[props.status] || ''
32
+ })
33
+
34
+ const sizeClass = computed(() => props.size === 'sm' ? 'px-1.5 py-0.5' : 'px-2 py-1')
35
+
36
+ const badgeStyle = computed(() => {
37
+ const styles = {
38
+ 'completed': { background: 'rgba(22,163,74,0.1)', color: '#16A34A' },
39
+ 'in-progress': { background: 'rgba(251,191,36,0.1)', color: '#D97706' },
40
+ 'blocked': { background: 'rgba(251,146,60,0.1)', color: '#EA580C' },
41
+ 'failed': { background: 'rgba(239,68,68,0.1)', color: '#DC2626' },
42
+ 'pending': { background: 'rgba(82,82,82,0.15)', color: '#6B7280' },
43
+ 'revising': { background: 'rgba(139,92,246,0.1)', color: '#7C3AED' },
44
+ 'stale': { background: 'rgba(234,179,8,0.1)', color: '#CA8A04' },
45
+ 'waiting': { background: 'rgba(59,130,246,0.1)', color: '#2563EB' },
46
+ }
47
+ return styles[props.status] || styles.pending
48
+ })
49
+
50
+ const dotClass = computed(() => {
51
+ return props.status === 'in-progress' ? 'animate-pulse-dot' : ''
52
+ })
53
+
54
+ const dotStyle = computed(() => {
55
+ const colors = {
56
+ 'completed': '#16A34A',
57
+ 'in-progress': '#D97706',
58
+ 'blocked': '#EA580C',
59
+ 'failed': '#DC2626',
60
+ 'pending': '#6B7280',
61
+ 'revising': '#7C3AED',
62
+ 'stale': '#CA8A04',
63
+ 'waiting': '#2563EB',
64
+ }
65
+ return { background: colors[props.status] || '#6B7280' }
66
+ })
67
+ </script>