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.
- package/.claude/skills/sillyspec-archive/SKILL.md +21 -21
- package/.claude/skills/sillyspec-auto/SKILL.md +83 -83
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +44 -44
- package/.claude/skills/sillyspec-commit/SKILL.md +106 -106
- package/.claude/skills/sillyspec-continue/SKILL.md +45 -45
- package/.claude/skills/sillyspec-doctor/SKILL.md +31 -31
- package/.claude/skills/sillyspec-execute/SKILL.md +30 -30
- package/.claude/skills/sillyspec-explore/SKILL.md +109 -109
- package/.claude/skills/sillyspec-knowledge/SKILL.md +269 -269
- package/.claude/skills/sillyspec-plan/SKILL.md +21 -21
- package/.claude/skills/sillyspec-propose/SKILL.md +21 -21
- package/.claude/skills/sillyspec-quick/SKILL.md +21 -21
- package/.claude/skills/sillyspec-resume/SKILL.md +68 -68
- package/.claude/skills/sillyspec-scan/SKILL.md +21 -21
- package/.claude/skills/sillyspec-state/SKILL.md +54 -54
- package/.claude/skills/sillyspec-status/SKILL.md +21 -21
- package/.claude/skills/sillyspec-verify/SKILL.md +21 -21
- package/.claude/skills/sillyspec-workspace/SKILL.md +157 -157
- package/.husky/pre-push +13 -13
- package/CLAUDE.md +18 -18
- package/README.md +198 -188
- package/SKILL.md +90 -91
- package/bin/sillyspec.js +2 -2
- package/docs/brainstorm-plan-contract.md +64 -64
- package/docs/plan-execute-contract.md +123 -123
- package/docs/platform-scan-protocol.md +298 -298
- package/docs/revision-mode.md +115 -115
- package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +99 -99
- package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +218 -218
- package/docs/sillyspec/file-lifecycle/stage-artifacts.md +167 -167
- package/docs/sillyspec/file-lifecycle/storage-and-state.md +148 -148
- package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +211 -193
- package/docs/sillyspec/file-lifecycle.md +125 -125
- package/docs/workflow-contract-regression.md +106 -106
- package/docs/worktree-isolation.md +252 -252
- package/package.json +40 -40
- package/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -7446
- package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -1
- package/packages/dashboard/dist/index.html +16 -16
- package/packages/dashboard/index.html +15 -15
- package/packages/dashboard/package-lock.json +2384 -2384
- package/packages/dashboard/package.json +25 -25
- package/packages/dashboard/server/executor.js +86 -86
- package/packages/dashboard/server/index.js +588 -588
- package/packages/dashboard/server/parser.js +526 -526
- package/packages/dashboard/server/watcher.js +344 -344
- package/packages/dashboard/src/App.vue +558 -558
- package/packages/dashboard/src/components/ActionBar.vue +93 -93
- package/packages/dashboard/src/components/CommandPalette.vue +96 -96
- package/packages/dashboard/src/components/DetailPanel.vue +137 -137
- package/packages/dashboard/src/components/LogStream.vue +65 -65
- package/packages/dashboard/src/components/PipelineStage.vue +95 -95
- package/packages/dashboard/src/components/PipelineView.vue +156 -156
- package/packages/dashboard/src/components/ProjectList.vue +210 -210
- package/packages/dashboard/src/components/StageBadge.vue +67 -67
- package/packages/dashboard/src/components/StepCard.vue +94 -94
- package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -48
- package/packages/dashboard/src/components/detail/GitDetail.vue +61 -61
- package/packages/dashboard/src/components/detail/TechDetail.vue +43 -43
- package/packages/dashboard/src/composables/useDashboard.js +170 -170
- package/packages/dashboard/src/composables/useKeyboard.js +119 -119
- package/packages/dashboard/src/composables/useWebSocket.js +129 -129
- package/packages/dashboard/src/main.js +8 -8
- package/packages/dashboard/src/style.css +132 -132
- package/packages/dashboard/vite.config.js +18 -18
- package/src/brainstorm-postcheck.js +158 -158
- package/src/change-list.js +52 -52
- package/src/change-risk-profile.js +352 -352
- package/src/classify-change.js +73 -73
- package/src/constants.js +70 -70
- package/src/contract-matrix.js +278 -278
- package/src/db.js +201 -201
- package/src/endpoint-extractor.js +315 -315
- package/src/hooks/claude-pre-tool-use.cjs +125 -125
- package/src/hooks/worktree-guard.js +653 -653
- package/src/index.js +922 -900
- package/src/init.js +431 -431
- package/src/knowledge-match.js +130 -130
- package/src/migrate.js +117 -117
- package/src/modules.js +482 -482
- package/src/progress.js +1734 -1734
- package/src/run.js +3465 -3358
- package/src/scan-postcheck.js +387 -383
- package/src/setup.js +398 -398
- package/src/stage-contract.js +700 -700
- package/src/stages/archive.js +160 -160
- package/src/stages/brainstorm-auto.js +229 -229
- package/src/stages/brainstorm.js +645 -645
- package/src/stages/doctor.js +365 -365
- package/src/stages/execute.js +625 -625
- package/src/stages/explore.js +34 -34
- package/src/stages/index.js +29 -29
- package/src/stages/knowledge.js +498 -498
- package/src/stages/plan-postcheck.js +511 -513
- package/src/stages/plan.js +582 -582
- package/src/stages/propose.js +174 -174
- package/src/stages/quick.js +82 -82
- package/src/stages/scan.js +558 -558
- package/src/stages/status.js +65 -65
- package/src/stages/verify.js +322 -322
- package/src/sync.js +497 -497
- package/src/task-review.js +346 -346
- package/src/workflow.js +785 -785
- package/src/worktree-apply.js +549 -549
- package/src/worktree-deps.js +185 -0
- package/src/worktree.js +982 -932
- package/templates/workflows/archive-impact.yaml +79 -79
- package/templates/workflows/scan-docs.yaml +132 -132
- package/test/brainstorm-plan-contract.test.mjs +273 -273
- package/test/check-syntax.mjs +26 -26
- package/test/contract-artifacts.test.mjs +323 -323
- package/test/decision-supersede.test.mjs +277 -277
- package/test/knowledge-match.test.mjs +231 -231
- package/test/plan-execute-contract.test.mjs +330 -330
- package/test/plan-optimization.test.mjs +572 -572
- package/test/platform-artifacts.test.mjs +166 -166
- package/test/platform-failure-samples.test.mjs +199 -199
- package/test/platform-recovery-chain.test.mjs +167 -167
- package/test/platform-recovery.test.mjs +136 -136
- package/test/platform-scan-p0.test.mjs +168 -168
- package/test/revision-v1.test.mjs +1145 -1145
- package/test/run-scan-project-parse.test.mjs +200 -200
- package/test/run-tests.mjs +48 -48
- package/test/scan-knowledge.test.mjs +175 -175
- package/test/scan-paths.test.mjs +68 -68
- package/test/scan-postcheck.test.mjs +197 -197
- package/test/spec-dir.test.mjs +206 -206
- package/test/stage-contract.test.mjs +299 -299
- package/test/stage-definitions.test.mjs +39 -39
- package/test/wait-gates.test.mjs +496 -496
- package/test/worktree-deps-provision.test.mjs +148 -0
- package/test/worktree-guard.test.mjs +71 -71
- 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>
|