sillyspec 3.26.3 → 3.26.5
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-brainstorm/SKILL.md +1 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +1 -0
- package/.claude/skills/sillyspec-plan/SKILL.md +1 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +18 -4
- package/package.json +1 -1
- package/src/db.js +11 -0
- package/src/dispatch/backends/local-agent.js +3 -0
- package/src/dispatch/backends/sillyhub-mcp.js +3 -0
- package/src/git-helper.js +21 -8
- package/src/index.js +2 -0
- package/src/quicklog.js +2 -0
- package/src/run/command.js +59 -7
- package/src/run/complete-handlers.js +95 -1
- package/src/run/complete.js +23 -5
- package/src/run/gates.js +1 -1
- package/src/run/prompt.js +34 -20
- package/src/run/quick-audit.js +5 -6
- package/src/run/shared.js +45 -11
- package/src/run/stage.js +10 -0
- package/src/run.js +5 -5
- package/src/stage-contract-spec.js +11 -0
- package/src/stage-contract.js +11 -1
- package/src/stage-review.js +9 -1
- package/src/stages/archive.js +12 -26
- package/src/stages/brainstorm.js +22 -20
- package/src/stages/execute.js +48 -0
- package/src/stages/plan.js +17 -1
- package/src/stages/quick.js +5 -1
- package/src/stages/scan.js +2 -2
- package/src/stages/verify.js +2 -1
- package/src/sync.js +1 -1
- package/src/worktree-apply.js +3 -3
- package/src/worktree.js +83 -8
- package/templates/prompts/testcase-design.md +12 -0
- package/templates/prompts/verify-probes.md +1 -0
- package/templates/skills/sillyspec-onboard/SKILL.md +92 -0
- package/packages/dashboard/index.html +0 -15
- package/packages/dashboard/package-lock.json +0 -2384
- package/packages/dashboard/public/favicon.jpg +0 -0
- package/packages/dashboard/public/logo.jpg +0 -0
- package/packages/dashboard/public/prototype-dashboard.html +0 -836
- package/packages/dashboard/public/prototype-overview.html +0 -256
- package/packages/dashboard/src/App.vue +0 -558
- package/packages/dashboard/src/components/ActionBar.vue +0 -93
- package/packages/dashboard/src/components/CommandPalette.vue +0 -96
- package/packages/dashboard/src/components/DetailPanel.vue +0 -137
- package/packages/dashboard/src/components/DocPreview.vue +0 -257
- package/packages/dashboard/src/components/DocTree.vue +0 -114
- package/packages/dashboard/src/components/HResizeHandle.vue +0 -48
- package/packages/dashboard/src/components/LogStream.vue +0 -65
- package/packages/dashboard/src/components/PipelineStage.vue +0 -95
- package/packages/dashboard/src/components/PipelineView.vue +0 -156
- package/packages/dashboard/src/components/ProjectCard.vue +0 -187
- package/packages/dashboard/src/components/ProjectList.vue +0 -210
- package/packages/dashboard/src/components/ProjectOverview.vue +0 -152
- package/packages/dashboard/src/components/StageBadge.vue +0 -67
- package/packages/dashboard/src/components/StepCard.vue +0 -94
- package/packages/dashboard/src/components/VResizeHandle.vue +0 -61
- package/packages/dashboard/src/components/detail/DocsDetail.vue +0 -48
- package/packages/dashboard/src/components/detail/GitDetail.vue +0 -61
- package/packages/dashboard/src/components/detail/TechDetail.vue +0 -43
- package/packages/dashboard/src/composables/useDashboard.js +0 -213
- package/packages/dashboard/src/composables/useKeyboard.js +0 -119
- package/packages/dashboard/src/composables/useLayout.js +0 -131
- package/packages/dashboard/src/composables/useWebSocket.js +0 -129
- package/packages/dashboard/src/main.js +0 -8
- package/packages/dashboard/src/style.css +0 -132
- package/packages/dashboard/vite.config.js +0 -18
|
@@ -1,210 +0,0 @@
|
|
|
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,152 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="overview-section">
|
|
3
|
-
<!-- 头部 -->
|
|
4
|
-
<div class="section-header">
|
|
5
|
-
<h1 class="section-title">
|
|
6
|
-
项目概览
|
|
7
|
-
<span class="badge">{{ projects.length }} 个项目</span>
|
|
8
|
-
</h1>
|
|
9
|
-
<div class="section-actions">
|
|
10
|
-
<button class="btn btn-icon" title="刷新" @click="refresh">↻</button>
|
|
11
|
-
<button class="btn btn-secondary" @click="resetLayout">重置布局</button>
|
|
12
|
-
</div>
|
|
13
|
-
</div>
|
|
14
|
-
|
|
15
|
-
<!-- 卡片容器 -->
|
|
16
|
-
<div class="cards-container">
|
|
17
|
-
<ProjectCard
|
|
18
|
-
v-for="project in projects"
|
|
19
|
-
:key="project.name"
|
|
20
|
-
:project="project"
|
|
21
|
-
:is-selected="activeProject?.name === project.name"
|
|
22
|
-
@select="handleSelect"
|
|
23
|
-
/>
|
|
24
|
-
</div>
|
|
25
|
-
</div>
|
|
26
|
-
</template>
|
|
27
|
-
|
|
28
|
-
<script setup>
|
|
29
|
-
import { defineProps, defineEmits } from 'vue'
|
|
30
|
-
import ProjectCard from './ProjectCard.vue'
|
|
31
|
-
import { useLayout } from '../composables/useLayout.js'
|
|
32
|
-
|
|
33
|
-
const props = defineProps({
|
|
34
|
-
projects: { type: Array, default: () => [] },
|
|
35
|
-
activeProject: { type: Object, default: null }
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
const emit = defineEmits(['select'])
|
|
39
|
-
|
|
40
|
-
function handleSelect(project) {
|
|
41
|
-
emit('select', project)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function refresh() {
|
|
45
|
-
// TODO: 实现刷新逻辑
|
|
46
|
-
window.location.reload()
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function resetLayout() {
|
|
50
|
-
if (confirm('确定要重置布局吗?')) {
|
|
51
|
-
useLayout().resetLayout()
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
</script>
|
|
55
|
-
|
|
56
|
-
<style scoped>
|
|
57
|
-
.overview-section {
|
|
58
|
-
height: 100%;
|
|
59
|
-
background: #FFFFFF;
|
|
60
|
-
padding: 16px 24px;
|
|
61
|
-
display: flex;
|
|
62
|
-
flex-direction: column;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
.section-header {
|
|
66
|
-
display: flex;
|
|
67
|
-
justify-content: space-between;
|
|
68
|
-
align-items: center;
|
|
69
|
-
margin-bottom: 16px;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.section-title {
|
|
73
|
-
font-size: 18px;
|
|
74
|
-
font-weight: 600;
|
|
75
|
-
color: #1A1A1A;
|
|
76
|
-
display: flex;
|
|
77
|
-
align-items: center;
|
|
78
|
-
gap: 8px;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
.badge {
|
|
82
|
-
font-size: 12px;
|
|
83
|
-
padding: 2px 8px;
|
|
84
|
-
background: #E5E7EB;
|
|
85
|
-
border-radius: 10px;
|
|
86
|
-
color: #6B7280;
|
|
87
|
-
font-weight: 500;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
.section-actions {
|
|
91
|
-
display: flex;
|
|
92
|
-
gap: 8px;
|
|
93
|
-
align-items: center;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
.btn {
|
|
97
|
-
padding: 6px 12px;
|
|
98
|
-
border-radius: 6px;
|
|
99
|
-
border: none;
|
|
100
|
-
cursor: pointer;
|
|
101
|
-
font-size: 13px;
|
|
102
|
-
font-weight: 500;
|
|
103
|
-
transition: all 0.2s;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
.btn:hover {
|
|
107
|
-
opacity: 0.9;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
.btn-secondary {
|
|
111
|
-
background: #E5E7EB;
|
|
112
|
-
color: #374151;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
.btn-icon {
|
|
116
|
-
padding: 6px;
|
|
117
|
-
width: 32px;
|
|
118
|
-
height: 32px;
|
|
119
|
-
display: flex;
|
|
120
|
-
align-items: center;
|
|
121
|
-
justify-content: center;
|
|
122
|
-
background: #F3F4F6;
|
|
123
|
-
border-radius: 6px;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
.btn-icon:hover {
|
|
127
|
-
background: #E5E7EB;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
.cards-container {
|
|
131
|
-
flex: 1;
|
|
132
|
-
display: flex;
|
|
133
|
-
gap: 16px;
|
|
134
|
-
overflow-x: auto;
|
|
135
|
-
overflow-y: hidden;
|
|
136
|
-
padding-bottom: 8px;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
.cards-container::-webkit-scrollbar {
|
|
140
|
-
height: 8px;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
.cards-container::-webkit-scrollbar-track {
|
|
144
|
-
background: #F3F4F6;
|
|
145
|
-
border-radius: 4px;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
.cards-container::-webkit-scrollbar-thumb {
|
|
149
|
-
background: #D1D5DB;
|
|
150
|
-
border-radius: 4px;
|
|
151
|
-
}
|
|
152
|
-
</style>
|
|
@@ -1,67 +0,0 @@
|
|
|
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,94 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div
|
|
3
|
-
:class="['group relative rounded-md cursor-pointer transition-all duration-200']"
|
|
4
|
-
:style="cardStyle"
|
|
5
|
-
@mouseenter="hovered = true"
|
|
6
|
-
@mouseleave="hovered = false"
|
|
7
|
-
@click="handleClick"
|
|
8
|
-
>
|
|
9
|
-
<!-- Left accent bar -->
|
|
10
|
-
<div class="absolute left-0 top-0 bottom-0 w-[2px]" :style="{ background: barColor }" />
|
|
11
|
-
|
|
12
|
-
<div class="pl-4 pr-4 py-3">
|
|
13
|
-
<div class="flex items-center gap-2">
|
|
14
|
-
<h3 class="text-[12px] font-medium transition-colors duration-150" :style="{ color: isActive ? '#D97706' : '#1C1C1E', fontFamily: '\'JetBrains Mono\', monospace' }">
|
|
15
|
-
{{ step.title || step.name }}
|
|
16
|
-
</h3>
|
|
17
|
-
<StageBadge v-if="step.status" :status="step.status" :label="statusLabel" />
|
|
18
|
-
</div>
|
|
19
|
-
|
|
20
|
-
<!-- Hover summary -->
|
|
21
|
-
<div
|
|
22
|
-
v-if="step.summary || step.description"
|
|
23
|
-
class="transition-all duration-200"
|
|
24
|
-
:style="{ maxHeight: hovered ? '80px' : '0', opacity: hovered ? 1 : 0, overflow: 'hidden' }"
|
|
25
|
-
>
|
|
26
|
-
<p class="mt-1.5 text-[11px] line-clamp-2" style="color: #636366;">
|
|
27
|
-
{{ step.summary || step.description }}
|
|
28
|
-
</p>
|
|
29
|
-
</div>
|
|
30
|
-
|
|
31
|
-
<!-- Active details -->
|
|
32
|
-
<div v-if="isActive" class="mt-2 space-y-1">
|
|
33
|
-
<p v-if="step.conclusion" class="text-[11px]" style="color: #1C1C1E;">
|
|
34
|
-
<span style="color: #D97706; font-weight: 600;">结论:</span> {{ step.conclusion }}
|
|
35
|
-
</p>
|
|
36
|
-
<p v-if="step.decision" class="text-[11px]" style="color: #1C1C1E;">
|
|
37
|
-
<span style="color: #D97706; font-weight: 600;">决策:</span> {{ step.decision }}
|
|
38
|
-
</p>
|
|
39
|
-
<p v-if="step.userQuery" class="text-[11px] italic" style="color: #636366;">
|
|
40
|
-
"{{ step.userQuery }}"
|
|
41
|
-
</p>
|
|
42
|
-
</div>
|
|
43
|
-
|
|
44
|
-
<div v-if="step.duration" class="mt-1 text-[10px] font-mono-log" style="color: #6B7280;">
|
|
45
|
-
{{ step.duration }}
|
|
46
|
-
</div>
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
49
|
-
</template>
|
|
50
|
-
|
|
51
|
-
<script setup>
|
|
52
|
-
import { ref, computed } from 'vue'
|
|
53
|
-
import StageBadge from './StageBadge.vue'
|
|
54
|
-
|
|
55
|
-
const props = defineProps({
|
|
56
|
-
step: { type: Object, required: true },
|
|
57
|
-
isActive: { type: Boolean, default: false },
|
|
58
|
-
isClickable: { type: Boolean, default: true }
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
const emit = defineEmits(['select'])
|
|
62
|
-
const hovered = ref(false)
|
|
63
|
-
|
|
64
|
-
const barColor = computed(() => {
|
|
65
|
-
const colors = {
|
|
66
|
-
'completed': '#16A34A', 'in-progress': '#D97706', 'blocked': '#EA580C', 'failed': '#DC2626', 'pending': '#E5E5EA',
|
|
67
|
-
'revising': '#7C3AED', 'stale': '#CA8A04', 'waiting': '#2563EB',
|
|
68
|
-
}
|
|
69
|
-
return colors[props.step.status] || colors.pending
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
const cardStyle = computed(() => ({
|
|
73
|
-
background: props.isActive ? 'rgba(217,119,6,0.08)' : (hovered.value ? 'rgba(0,0,0,0.02)' : '#FFFFFF'),
|
|
74
|
-
border: `1px solid ${props.isActive ? 'rgba(251,191,36,0.2)' : '#F0F0F3'}`,
|
|
75
|
-
}))
|
|
76
|
-
|
|
77
|
-
const statusLabel = computed(() => {
|
|
78
|
-
const labels = { 'completed': '完成', 'in-progress': '进行中', 'blocked': '阻塞', 'failed': '失败', 'pending': '待办',
|
|
79
|
-
'revising': '修订中', 'stale': '已失效', 'waiting': '等待中',
|
|
80
|
-
}
|
|
81
|
-
return labels[props.step.status] || '待办'
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
function handleClick() { if (props.isClickable) emit('select', props.step) }
|
|
85
|
-
</script>
|
|
86
|
-
|
|
87
|
-
<style scoped>
|
|
88
|
-
.line-clamp-2 {
|
|
89
|
-
display: -webkit-box;
|
|
90
|
-
-webkit-line-clamp: 2;
|
|
91
|
-
-webkit-box-orient: vertical;
|
|
92
|
-
overflow: hidden;
|
|
93
|
-
}
|
|
94
|
-
</style>
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div
|
|
3
|
-
class="v-resize-handle"
|
|
4
|
-
:class="{ 'dragging': isDragging }"
|
|
5
|
-
@mousedown="handleMouseDown"
|
|
6
|
-
>
|
|
7
|
-
<div class="drag-indicator"></div>
|
|
8
|
-
</div>
|
|
9
|
-
</template>
|
|
10
|
-
|
|
11
|
-
<script setup>
|
|
12
|
-
const props = defineProps({
|
|
13
|
-
isDragging: { type: Boolean, default: false }
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
const emit = defineEmits(['drag-start', 'drag-end', 'resize'])
|
|
17
|
-
|
|
18
|
-
function handleMouseDown(e) {
|
|
19
|
-
emit('drag-start', { type: 'vertical', startY: e.clientY })
|
|
20
|
-
|
|
21
|
-
const onMove = (ev) => {
|
|
22
|
-
emit('resize', { deltaY: ev.clientY - e.clientY })
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const onUp = () => {
|
|
26
|
-
emit('drag-end')
|
|
27
|
-
window.removeEventListener('mousemove', onMove)
|
|
28
|
-
window.removeEventListener('mouseup', onUp)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
window.addEventListener('mousemove', onMove)
|
|
32
|
-
window.addEventListener('mouseup', onUp)
|
|
33
|
-
}
|
|
34
|
-
</script>
|
|
35
|
-
|
|
36
|
-
<style scoped>
|
|
37
|
-
.v-resize-handle {
|
|
38
|
-
height: 6px;
|
|
39
|
-
background: #2A3040;
|
|
40
|
-
cursor: row-resize;
|
|
41
|
-
position: relative;
|
|
42
|
-
transition: background 0.2s;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
.v-resize-handle:hover,
|
|
46
|
-
.v-resize-handle.dragging {
|
|
47
|
-
background: #D97706;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
.drag-indicator {
|
|
51
|
-
position: absolute;
|
|
52
|
-
top: 50%;
|
|
53
|
-
left: 50%;
|
|
54
|
-
transform: translate(-50%, -50%);
|
|
55
|
-
width: 40px;
|
|
56
|
-
height: 4px;
|
|
57
|
-
background: rgba(255, 255, 255, 0.3);
|
|
58
|
-
border-radius: 2px;
|
|
59
|
-
pointer-events: none;
|
|
60
|
-
}
|
|
61
|
-
</style>
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div v-if="data?.length" class="px-4 py-3">
|
|
3
|
-
<div v-for="group in data" :key="group.key" class="mb-4">
|
|
4
|
-
<h4 class="text-[9px] font-semibold uppercase tracking-[0.2em] mb-2 font-[JetBrains_Mono,monospace]" style="color: #6B7280;">
|
|
5
|
-
{{ group.icon }} {{ group.label }} ({{ group.files.length }})
|
|
6
|
-
</h4>
|
|
7
|
-
<div class="space-y-1">
|
|
8
|
-
<div
|
|
9
|
-
v-for="f in group.files"
|
|
10
|
-
:key="f.path"
|
|
11
|
-
class="flex items-center gap-2 px-2 py-1.5 rounded text-[11px] cursor-pointer hover:bg-[#FEF3C7] transition-colors"
|
|
12
|
-
@click="$emit('open-file', f)"
|
|
13
|
-
>
|
|
14
|
-
<svg class="w-3 h-3 flex-shrink-0" fill="none" stroke="#9CA3AF" viewBox="0 0 24 24">
|
|
15
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
|
16
|
-
</svg>
|
|
17
|
-
<span class="truncate" style="color: #1C1C1E;">{{ f.name }}</span>
|
|
18
|
-
<span class="ml-auto flex-shrink-0 font-mono text-[10px]" style="color: #9CA3AF;">{{ formatSize(f.size) }}</span>
|
|
19
|
-
<span class="flex-shrink-0 text-[10px]" style="color: #9CA3AF;">{{ relativeTime(f.mtime) }}</span>
|
|
20
|
-
</div>
|
|
21
|
-
</div>
|
|
22
|
-
</div>
|
|
23
|
-
</div>
|
|
24
|
-
<n-empty v-else description="无文档" style="padding: 48px 0;" />
|
|
25
|
-
</template>
|
|
26
|
-
|
|
27
|
-
<script setup>
|
|
28
|
-
defineProps({ data: { type: Array, default: () => [] } })
|
|
29
|
-
defineEmits(['open-file'])
|
|
30
|
-
|
|
31
|
-
function formatSize(bytes) {
|
|
32
|
-
if (!bytes) return '—'
|
|
33
|
-
if (bytes < 1024) return bytes + ' B'
|
|
34
|
-
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'
|
|
35
|
-
return (bytes / 1048576).toFixed(1) + ' MB'
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function relativeTime(iso) {
|
|
39
|
-
if (!iso) return ''
|
|
40
|
-
const d = new Date(iso)
|
|
41
|
-
const diff = Date.now() - d.getTime()
|
|
42
|
-
if (diff < 60000) return '刚刚'
|
|
43
|
-
if (diff < 3600000) return `${Math.floor(diff / 60000)} 分钟前`
|
|
44
|
-
if (diff < 86400000) return `${Math.floor(diff / 3600000)} 小时前`
|
|
45
|
-
if (diff < 604800000) return `${Math.floor(diff / 86400000)} 天前`
|
|
46
|
-
return `${d.getMonth() + 1}/${d.getDate()}`
|
|
47
|
-
}
|
|
48
|
-
</script>
|