sillyspec 3.7.8 → 3.7.10
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/.sillyspec/changes/dashboard/design.md +219 -0
- package/.sillyspec/plans/2026-04-05-dashboard.md +737 -0
- package/.sillyspec/specs/2026-04-05-dashboard-design.md +206 -0
- package/bin/sillyspec.js +0 -0
- package/package.json +5 -2
- package/packages/dashboard/dist/assets/index-Bh-GPjKY.css +1 -0
- package/packages/dashboard/dist/assets/index-CrCn5Gg6.js +17 -0
- package/packages/dashboard/dist/index.html +16 -0
- package/packages/dashboard/index.html +15 -0
- package/packages/dashboard/package-lock.json +2164 -0
- package/packages/dashboard/package.json +22 -0
- package/packages/dashboard/server/executor.js +86 -0
- package/packages/dashboard/server/index.js +359 -0
- package/packages/dashboard/server/parser.js +154 -0
- package/packages/dashboard/server/watcher.js +277 -0
- package/packages/dashboard/src/App.vue +154 -0
- package/packages/dashboard/src/components/ActionBar.vue +100 -0
- package/packages/dashboard/src/components/CommandPalette.vue +117 -0
- package/packages/dashboard/src/components/DetailPanel.vue +122 -0
- package/packages/dashboard/src/components/LogStream.vue +85 -0
- package/packages/dashboard/src/components/PipelineStage.vue +75 -0
- package/packages/dashboard/src/components/PipelineView.vue +94 -0
- package/packages/dashboard/src/components/ProjectList.vue +152 -0
- package/packages/dashboard/src/components/StageBadge.vue +53 -0
- package/packages/dashboard/src/components/StepCard.vue +89 -0
- package/packages/dashboard/src/composables/useDashboard.js +171 -0
- package/packages/dashboard/src/composables/useKeyboard.js +117 -0
- package/packages/dashboard/src/composables/useWebSocket.js +129 -0
- package/packages/dashboard/src/main.js +5 -0
- package/packages/dashboard/src/style.css +132 -0
- package/packages/dashboard/vite.config.js +18 -0
- package/src/index.js +68 -8
- package/src/init.js +23 -1
- package/src/progress.js +422 -0
- package/templates/archive.md +56 -0
- package/templates/brainstorm.md +82 -26
- package/templates/commit.md +2 -0
- package/templates/execute.md +2 -1
- package/templates/progress-format.md +90 -0
- package/templates/quick.md +36 -3
- package/templates/resume-dialog.md +55 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
:class="['flex flex-col transition-all duration-300', isOpen ? 'w-[340px]' : 'w-0 opacity-0 overflow-hidden']"
|
|
4
|
+
style="background: #111113;"
|
|
5
|
+
>
|
|
6
|
+
<!-- Header -->
|
|
7
|
+
<div class="px-4 py-3 flex items-center justify-between flex-shrink-0" style="border-bottom: 1px solid #1F1F22;">
|
|
8
|
+
<h2 class="text-[11px] font-semibold uppercase tracking-[0.2em] font-[JetBrains_Mono,monospace]" style="color: #525252;">Detail</h2>
|
|
9
|
+
<button
|
|
10
|
+
@click="$emit('close')"
|
|
11
|
+
class="p-1 rounded-sm transition-colors duration-100 hover:bg-white/5"
|
|
12
|
+
style="color: #525252;"
|
|
13
|
+
>
|
|
14
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
15
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
16
|
+
</svg>
|
|
17
|
+
</button>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<!-- Content -->
|
|
21
|
+
<div class="flex-1 overflow-y-auto">
|
|
22
|
+
<!-- Empty state -->
|
|
23
|
+
<div v-if="!activeStep" class="flex items-center justify-center h-full">
|
|
24
|
+
<div class="text-center">
|
|
25
|
+
<div class="w-10 h-10 mx-auto mb-3 rounded-md flex items-center justify-center" style="border: 1px dashed #2A2A2D; transform: rotate(45deg);">
|
|
26
|
+
<svg class="w-4 h-4" style="color: #3A3A3D; transform: rotate(-45deg);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
27
|
+
<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"/>
|
|
28
|
+
</svg>
|
|
29
|
+
</div>
|
|
30
|
+
<p class="text-[11px] font-[JetBrains_Mono,monospace]" style="color: #3A3A3D;">Select a step</p>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<!-- Step detail -->
|
|
35
|
+
<div v-else>
|
|
36
|
+
<!-- Title -->
|
|
37
|
+
<div class="px-4 py-3" style="border-bottom: 1px solid #1F1F22;">
|
|
38
|
+
<h3 class="text-[13px] font-semibold font-[JetBrains_Mono,monospace]" style="color: #FBBF24;">
|
|
39
|
+
{{ activeStep.title || activeStep.name }}
|
|
40
|
+
</h3>
|
|
41
|
+
<div v-if="activeStep.status" class="mt-2">
|
|
42
|
+
<StageBadge :status="activeStep.status" />
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<!-- Description -->
|
|
47
|
+
<div v-if="activeStep.description || activeStep.summary" class="px-4 py-3" style="border-bottom: 1px solid #1F1F22;">
|
|
48
|
+
<h4 class="text-[9px] font-semibold uppercase tracking-[0.2em] mb-1.5 font-[JetBrains_Mono,monospace]" style="color: #525252;">Description</h4>
|
|
49
|
+
<p class="text-[11px] leading-relaxed" style="color: #8B8B8E;">{{ activeStep.description || activeStep.summary }}</p>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<!-- Conclusion -->
|
|
53
|
+
<div v-if="activeStep.conclusion" class="px-4 py-3" style="border-bottom: 1px solid #1F1F22;">
|
|
54
|
+
<h4 class="text-[9px] font-semibold uppercase tracking-[0.2em] mb-1.5 font-[JetBrains_Mono,monospace]" style="color: #525252;">Conclusion</h4>
|
|
55
|
+
<p class="text-[11px] leading-relaxed" style="color: #E4E4E7;">{{ activeStep.conclusion }}</p>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<!-- Decision -->
|
|
59
|
+
<div v-if="activeStep.decision" class="px-4 py-3" style="border-bottom: 1px solid #1F1F22;">
|
|
60
|
+
<h4 class="text-[9px] font-semibold uppercase tracking-[0.2em] mb-1.5 font-[JetBrains_Mono,monospace]" style="color: #525252;">Decision</h4>
|
|
61
|
+
<p class="text-[11px] leading-relaxed" style="color: #E4E4E7;">{{ activeStep.decision }}</p>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<!-- User Query -->
|
|
65
|
+
<div v-if="activeStep.userQuery" class="px-4 py-3" style="border-bottom: 1px solid #1F1F22;">
|
|
66
|
+
<h4 class="text-[9px] font-semibold uppercase tracking-[0.2em] mb-1.5 font-[JetBrains_Mono,monospace]" style="color: #525252;">User Query</h4>
|
|
67
|
+
<div class="px-3 py-2 rounded-md" style="background: #0E0E10; border: 1px solid #1F1F22;">
|
|
68
|
+
<p class="text-[11px] italic" style="color: #8B8B8E;">"{{ activeStep.userQuery }}"</p>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<!-- Metadata -->
|
|
73
|
+
<div v-if="activeStep.duration || activeStep.timestamp" class="px-4 py-3" style="border-bottom: 1px solid #1F1F22;">
|
|
74
|
+
<h4 class="text-[9px] font-semibold uppercase tracking-[0.2em] mb-1.5 font-[JetBrains_Mono,monospace]" style="color: #525252;">Meta</h4>
|
|
75
|
+
<div class="space-y-1 text-[11px]" style="color: #525252;">
|
|
76
|
+
<div v-if="activeStep.duration"><span style="color: #8B8B8E;">Time:</span> {{ activeStep.duration }}</div>
|
|
77
|
+
<div v-if="activeStep.timestamp"><span style="color: #8B8B8E;">At:</span> {{ formatTimestamp(activeStep.timestamp) }}</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<!-- Output -->
|
|
82
|
+
<div v-if="activeStep.output || activeStep.files" class="px-4 py-3" style="border-bottom: 1px solid #1F1F22;">
|
|
83
|
+
<h4 class="text-[9px] font-semibold uppercase tracking-[0.2em] mb-1.5 font-[JetBrains_Mono,monospace]" style="color: #525252;">Output</h4>
|
|
84
|
+
<div v-if="activeStep.output" class="px-3 py-2 rounded-md max-h-40 overflow-y-auto" style="background: #0E0E10; border: 1px solid #1F1F22;">
|
|
85
|
+
<pre class="text-[10px] whitespace-pre-wrap font-mono-log" style="color: #8B8B8E;">{{ activeStep.output }}</pre>
|
|
86
|
+
</div>
|
|
87
|
+
<div v-if="activeStep.files" class="mt-2 space-y-1">
|
|
88
|
+
<div v-for="(file, i) in activeStep.files" :key="i" class="flex items-center gap-2 text-[10px]" style="color: #525252;">
|
|
89
|
+
<svg class="w-3 h-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
90
|
+
<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"/>
|
|
91
|
+
</svg>
|
|
92
|
+
<span class="truncate">{{ file }}</span>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<!-- Log Stream -->
|
|
100
|
+
<div class="flex-shrink-0" style="height: 200px; border-top: 1px solid #1F1F22;">
|
|
101
|
+
<LogStream :logs="logs" @clear="$emit('clear-logs')" />
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</template>
|
|
105
|
+
|
|
106
|
+
<script setup>
|
|
107
|
+
import StageBadge from './StageBadge.vue'
|
|
108
|
+
import LogStream from './LogStream.vue'
|
|
109
|
+
|
|
110
|
+
const props = defineProps({
|
|
111
|
+
isOpen: { type: Boolean, default: true },
|
|
112
|
+
activeStep: { type: Object, default: null },
|
|
113
|
+
logs: { type: Array, default: () => [] }
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const emit = defineEmits(['close', 'clear-logs'])
|
|
117
|
+
|
|
118
|
+
function formatTimestamp(ts) {
|
|
119
|
+
if (!ts) return ''
|
|
120
|
+
return new Date(ts).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
|
|
121
|
+
}
|
|
122
|
+
</script>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex flex-col h-full" style="background: #0E0E10;">
|
|
3
|
+
<!-- Header -->
|
|
4
|
+
<div class="px-3 py-2 flex items-center gap-2" style="border-bottom: 1px solid #1F1F22;">
|
|
5
|
+
<input
|
|
6
|
+
v-model="searchQuery"
|
|
7
|
+
type="text"
|
|
8
|
+
placeholder="filter logs..."
|
|
9
|
+
class="flex-1 px-2 py-1 rounded-sm text-[10px] font-mono-log outline-none transition-colors duration-100"
|
|
10
|
+
style="background: #141416; border: 1px solid #1F1F22; color: #8B8B8E;"
|
|
11
|
+
/>
|
|
12
|
+
<button
|
|
13
|
+
@click="clearLogs"
|
|
14
|
+
class="px-2 py-1 text-[10px] rounded-sm transition-colors duration-100"
|
|
15
|
+
style="color: #525252; border: 1px solid #1F1F22;"
|
|
16
|
+
>
|
|
17
|
+
clear
|
|
18
|
+
</button>
|
|
19
|
+
<button
|
|
20
|
+
@click="toggleAutoScroll"
|
|
21
|
+
class="px-2 py-1 text-[10px] rounded-sm font-mono-log transition-colors duration-100"
|
|
22
|
+
:style="{
|
|
23
|
+
color: autoScroll ? '#FBBF24' : '#525252',
|
|
24
|
+
background: autoScroll ? 'rgba(251,191,36,0.08)' : 'transparent',
|
|
25
|
+
border: autoScroll ? '1px solid rgba(251,191,36,0.2)' : '1px solid #1F1F22'
|
|
26
|
+
}"
|
|
27
|
+
>
|
|
28
|
+
{{ autoScroll ? 'auto' : 'pause' }}
|
|
29
|
+
</button>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<!-- Log output -->
|
|
33
|
+
<div ref="logContainer" class="flex-1 overflow-y-auto px-2 py-1.5 font-mono-log text-[10px]" style="background: #0A0A0B;" @scroll="handleScroll">
|
|
34
|
+
<div v-if="filteredLogs.length === 0" class="flex items-center justify-center h-full">
|
|
35
|
+
<span class="font-mono-log" style="color: #2A2A2D;">{{ logs.length === 0 ? 'no logs' : 'no match' }}</span>
|
|
36
|
+
</div>
|
|
37
|
+
<div v-else class="space-y-px">
|
|
38
|
+
<div v-for="log in filteredLogs" :key="log.id" class="px-1.5 py-px rounded-sm" :style="{ background: logBg(log.type) }">
|
|
39
|
+
<span style="color: #3A3A3D;" class="select-none">[{{ formatTime(log.timestamp) }}]</span>
|
|
40
|
+
<span :style="{ color: logColor(log.type) }">{{ escapeHtml(log.content) }}</span>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<!-- Footer -->
|
|
46
|
+
<div class="px-3 py-1 flex items-center justify-between text-[9px] font-mono-log" style="border-top: 1px solid #1F1F22; background: #0E0E10; color: #3A3A3D;">
|
|
47
|
+
<span>{{ filteredLogs.length }}/{{ logs.length }}</span>
|
|
48
|
+
<span v-if="!autoScroll" style="color: #FB923C;">paused</span>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</template>
|
|
52
|
+
|
|
53
|
+
<script setup>
|
|
54
|
+
import { ref, computed, watch, nextTick } from 'vue'
|
|
55
|
+
|
|
56
|
+
const props = defineProps({ logs: { type: Array, default: () => [] } })
|
|
57
|
+
const emit = defineEmits(['clear'])
|
|
58
|
+
|
|
59
|
+
const searchQuery = ref('')
|
|
60
|
+
const autoScroll = ref(true)
|
|
61
|
+
const logContainer = ref(null)
|
|
62
|
+
|
|
63
|
+
const filteredLogs = computed(() => {
|
|
64
|
+
if (!searchQuery.value) return props.logs
|
|
65
|
+
const q = searchQuery.value.toLowerCase()
|
|
66
|
+
return props.logs.filter(l => l.content.toLowerCase().includes(q))
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
function formatTime(ts) { if (!ts) return ''; const d = new Date(ts); return d.toLocaleTimeString('zh-CN', { hour12: false }) }
|
|
70
|
+
function escapeHtml(t) { if (!t) return ''; return t.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') }
|
|
71
|
+
function logBg(t) { return t === 'error' ? 'rgba(239,68,68,0.05)' : t === 'warn' ? 'rgba(251,146,60,0.05)' : 'transparent' }
|
|
72
|
+
function logColor(t) { return t === 'error' ? '#EF4444' : t === 'warn' ? '#FB923C' : t === 'debug' ? '#525252' : '#8B8B8E' }
|
|
73
|
+
function clearLogs() { emit('clear') }
|
|
74
|
+
function toggleAutoScroll() { autoScroll.value = !autoScroll.value; if (autoScroll.value) scrollToBottom() }
|
|
75
|
+
function handleScroll() {
|
|
76
|
+
if (!logContainer.value) return
|
|
77
|
+
const { scrollTop, scrollHeight, clientHeight } = logContainer.value
|
|
78
|
+
const atBottom = scrollHeight - scrollTop - clientHeight < 50
|
|
79
|
+
if (!atBottom && autoScroll.value) autoScroll.value = false
|
|
80
|
+
else if (atBottom && !autoScroll.value) autoScroll.value = true
|
|
81
|
+
}
|
|
82
|
+
function scrollToBottom() { nextTick(() => { if (logContainer.value && autoScroll.value) logContainer.value.scrollTop = logContainer.value.scrollHeight }) }
|
|
83
|
+
watch(() => props.logs.length, () => { scrollToBottom() }, { flush: 'post' })
|
|
84
|
+
watch(logContainer, () => { scrollToBottom() }, { once: true })
|
|
85
|
+
</script>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="relative">
|
|
3
|
+
<div class="flex items-start gap-3">
|
|
4
|
+
<!-- Node indicator -->
|
|
5
|
+
<div class="flex flex-col items-center flex-shrink-0 pt-1.5">
|
|
6
|
+
<div
|
|
7
|
+
class="w-2 h-2 transition-colors duration-200"
|
|
8
|
+
:style="nodeStyle"
|
|
9
|
+
:class="{ 'animate-pulse-dot': isActive || status === 'in-progress' }"
|
|
10
|
+
/>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<!-- Content -->
|
|
14
|
+
<div class="flex-1 min-w-0 -mt-0.5">
|
|
15
|
+
<!-- Stage Header -->
|
|
16
|
+
<div class="flex items-center gap-2.5 mb-3">
|
|
17
|
+
<span
|
|
18
|
+
class="text-[12px] font-semibold font-[JetBrains_Mono,monospace] tracking-tight"
|
|
19
|
+
:style="{ color: isActive ? '#FBBF24' : '#E4E4E7' }"
|
|
20
|
+
>
|
|
21
|
+
{{ title }}
|
|
22
|
+
</span>
|
|
23
|
+
<StageBadge :status="status" />
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<!-- Steps -->
|
|
27
|
+
<div class="space-y-1">
|
|
28
|
+
<div v-if="steps.length === 0" class="text-[11px] italic py-1" style="color: #525252;">
|
|
29
|
+
No steps yet
|
|
30
|
+
</div>
|
|
31
|
+
<StepCard
|
|
32
|
+
v-for="step in steps"
|
|
33
|
+
:key="step.id || step.name"
|
|
34
|
+
:step="step"
|
|
35
|
+
:is-active="isActiveStep(step)"
|
|
36
|
+
@select="$emit('select-step', $event)"
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</template>
|
|
43
|
+
|
|
44
|
+
<script setup>
|
|
45
|
+
import { computed } from 'vue'
|
|
46
|
+
import StageBadge from './StageBadge.vue'
|
|
47
|
+
import StepCard from './StepCard.vue'
|
|
48
|
+
|
|
49
|
+
const props = defineProps({
|
|
50
|
+
name: { type: String, required: true },
|
|
51
|
+
title: { type: String, required: true },
|
|
52
|
+
steps: { type: Array, default: () => [] },
|
|
53
|
+
status: { type: String, default: 'pending' },
|
|
54
|
+
isActive: { type: Boolean, default: false },
|
|
55
|
+
activeStep: { type: Object, default: null }
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const emit = defineEmits(['select-step'])
|
|
59
|
+
|
|
60
|
+
const nodeStyle = computed(() => {
|
|
61
|
+
if (props.isActive) return { background: '#FBBF24', boxShadow: '0 0 8px rgba(251,191,36,0.4)' }
|
|
62
|
+
const colors = {
|
|
63
|
+
'completed': '#34D399',
|
|
64
|
+
'in-progress': '#FBBF24',
|
|
65
|
+
'blocked': '#FB923C',
|
|
66
|
+
'failed': '#EF4444',
|
|
67
|
+
'pending': '#2A2A2D'
|
|
68
|
+
}
|
|
69
|
+
return { background: colors[props.status] || colors.pending }
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
function isActiveStep(step) {
|
|
73
|
+
return props.activeStep?.id === step.id || props.activeStep?.name === step.name
|
|
74
|
+
}
|
|
75
|
+
</script>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex flex-col h-full" style="background: #0E0E10;">
|
|
3
|
+
<!-- Header -->
|
|
4
|
+
<div class="px-6 pt-6 pb-4" style="border-bottom: 1px solid #1F1F22;">
|
|
5
|
+
<h2 class="text-[11px] font-semibold uppercase tracking-[0.2em] font-[JetBrains_Mono,monospace]" style="color: #525252;">
|
|
6
|
+
Pipeline
|
|
7
|
+
</h2>
|
|
8
|
+
<p v-if="project" class="text-[12px] mt-1.5 font-[JetBrains_Mono,monospace]" style="color: #8B8B8E;">
|
|
9
|
+
{{ project.name }} <span style="color: #2A2A2D;">/</span> <span style="color: #FBBF24;">{{ currentStage }}</span>
|
|
10
|
+
</p>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<!-- Stages -->
|
|
14
|
+
<div class="flex-1 overflow-y-auto px-6 py-5">
|
|
15
|
+
<!-- Empty state -->
|
|
16
|
+
<div v-if="!project || !project.state" class="flex items-center justify-center h-full">
|
|
17
|
+
<div class="text-center">
|
|
18
|
+
<div class="w-14 h-14 mx-auto mb-4 rounded-md flex items-center justify-center" style="border: 1px dashed #2A2A2D; transform: rotate(45deg);">
|
|
19
|
+
<svg class="w-5 h-5" style="color: #525252; transform: rotate(-45deg);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
20
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
|
|
21
|
+
</svg>
|
|
22
|
+
</div>
|
|
23
|
+
<p class="text-[12px] font-[JetBrains_Mono,monospace]" style="color: #525252;">Select a project</p>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div v-else class="space-y-5">
|
|
28
|
+
<PipelineStage name="brainstorm" title="BRAINSTORM" :steps="getStageSteps('brainstorm')" :status="getStageStatus('brainstorm')" :is-active="currentStage === 'brainstorm'" :active-step="activeStep" @select-step="handleSelectStep" />
|
|
29
|
+
<div v-if="hasStage('plan')" class="flex items-center pl-[3px]"><div class="w-px h-4" style="background: #1F1F22;" /></div>
|
|
30
|
+
<PipelineStage name="plan" title="PLAN" :steps="getStageSteps('plan')" :status="getStageStatus('plan')" :is-active="currentStage === 'plan'" :active-step="activeStep" @select-step="handleSelectStep" />
|
|
31
|
+
<div v-if="hasStage('execute')" class="flex items-center pl-[3px]"><div class="w-px h-4" style="background: #1F1F22;" /></div>
|
|
32
|
+
<PipelineStage name="execute" title="EXECUTE" :steps="getStageSteps('execute')" :status="getStageStatus('execute')" :is-active="currentStage === 'execute'" :active-step="activeStep" @select-step="handleSelectStep" />
|
|
33
|
+
<div v-if="hasStage('verify')" class="flex items-center pl-[3px]"><div class="w-px h-4" style="background: #1F1F22;" /></div>
|
|
34
|
+
<PipelineStage name="verify" title="VERIFY" :steps="getStageSteps('verify')" :status="getStageStatus('verify')" :is-active="currentStage === 'verify'" :active-step="activeStep" @select-step="handleSelectStep" />
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<!-- Activity Log -->
|
|
39
|
+
<div v-if="project?.state?.progress" style="border-top: 1px solid #1F1F22; background: rgba(10,10,11,0.6);">
|
|
40
|
+
<div class="px-6 py-2.5 flex items-center justify-between">
|
|
41
|
+
<div class="text-[9px] font-semibold uppercase tracking-[0.25em] font-[JetBrains_Mono,monospace]" style="color: #525252;">Activity</div>
|
|
42
|
+
<div class="text-[10px] font-mono-log" style="color: #3A3A3D;">{{ activityLogs.length }}</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="px-6 pb-3 space-y-0.5 max-h-32 overflow-y-auto">
|
|
45
|
+
<div v-for="(log, i) in activityLogs" :key="i" class="flex items-start gap-2.5 text-[11px] py-0.5">
|
|
46
|
+
<span class="w-10 font-mono-log flex-shrink-0" style="color: #3A3A3D;">{{ log.time }}</span>
|
|
47
|
+
<span :style="{ color: log.status === 'completed' ? '#34D399' : '#FBBF24' }">›</span>
|
|
48
|
+
<span style="color: #8B8B8E;">{{ log.description }}</span>
|
|
49
|
+
</div>
|
|
50
|
+
<div v-if="activityLogs.length === 0" class="text-[10px] py-1" style="color: #3A3A3D;">No activity</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</template>
|
|
55
|
+
|
|
56
|
+
<script setup>
|
|
57
|
+
import { computed } from 'vue'
|
|
58
|
+
import PipelineStage from './PipelineStage.vue'
|
|
59
|
+
|
|
60
|
+
const props = defineProps({
|
|
61
|
+
project: { type: Object, default: null },
|
|
62
|
+
activeStep: { type: Object, default: null }
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const emit = defineEmits(['select-step'])
|
|
66
|
+
|
|
67
|
+
const currentStage = computed(() => props.project?.state?.currentStage || 'unknown')
|
|
68
|
+
const progress = computed(() => props.project?.state?.progress || {})
|
|
69
|
+
const stages = computed(() => progress.value.stages || {})
|
|
70
|
+
|
|
71
|
+
const activityLogs = computed(() => {
|
|
72
|
+
if (!props.project?.state) return []
|
|
73
|
+
const logs = []
|
|
74
|
+
for (const [name, data] of Object.entries(progress.value.stages || {})) {
|
|
75
|
+
if (data.status === 'completed') logs.push({ time: data.completedAt ? formatTime(data.completedAt) : '--:--', status: 'completed', description: `${name} completed` })
|
|
76
|
+
else if (data.status === 'in-progress') logs.push({ time: '--:--', status: 'in-progress', description: `${name} running` })
|
|
77
|
+
}
|
|
78
|
+
return logs.reverse()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
function formatTime(iso) { try { const d = new Date(iso); return `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}` } catch { return '--:--' } }
|
|
82
|
+
function hasStage(n) { return !!stages.value[n] }
|
|
83
|
+
function getStageSteps(n) { return stages.value[n]?.steps || [] }
|
|
84
|
+
function getStageStatus(n) {
|
|
85
|
+
const s = stages.value[n]; if (!s) return 'pending'
|
|
86
|
+
const steps = s.steps || []
|
|
87
|
+
if (steps.some(x => x.status === 'failed')) return 'failed'
|
|
88
|
+
if (steps.some(x => x.status === 'blocked')) return 'blocked'
|
|
89
|
+
if (steps.some(x => x.status === 'in-progress')) return 'in-progress'
|
|
90
|
+
if (steps.every(x => x.status === 'completed')) return 'completed'
|
|
91
|
+
return 'pending'
|
|
92
|
+
}
|
|
93
|
+
function handleSelectStep(step) { emit('select-step', step) }
|
|
94
|
+
</script>
|
|
@@ -0,0 +1,152 @@
|
|
|
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, #FBBF24 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
|
+
<div>
|
|
10
|
+
<h1 class="text-[13px] font-semibold tracking-tight font-[JetBrains_Mono,monospace]" style="color: #E4E4E7;">
|
|
11
|
+
SillySpec
|
|
12
|
+
</h1>
|
|
13
|
+
<p class="text-[10px] tracking-widest uppercase" style="color: #525252;">Dashboard</p>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<!-- Divider -->
|
|
19
|
+
<div class="mx-4 h-px" style="background: linear-gradient(90deg, transparent, #2A2A2D, transparent);"></div>
|
|
20
|
+
|
|
21
|
+
<!-- Projects List -->
|
|
22
|
+
<div class="flex-1 overflow-y-auto py-3 relative z-10">
|
|
23
|
+
<!-- Loading skeleton -->
|
|
24
|
+
<div v-if="isLoading" class="px-4 space-y-2">
|
|
25
|
+
<div v-for="i in 4" :key="i" class="rounded-lg p-3" style="background: #141416;">
|
|
26
|
+
<div class="h-3 rounded w-20 skeleton-shimmer mb-2"></div>
|
|
27
|
+
<div class="h-2 rounded w-32 skeleton-shimmer"></div>
|
|
28
|
+
</div>
|
|
29
|
+
<p class="text-center text-[10px] mt-4 font-[JetBrains_Mono,monospace]" style="color: #525252;">
|
|
30
|
+
scanning projects...
|
|
31
|
+
</p>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<!-- Empty state -->
|
|
35
|
+
<div v-else-if="projects.length === 0" class="px-4 py-12 text-center">
|
|
36
|
+
<div class="w-10 h-10 mx-auto mb-3 rounded-full flex items-center justify-center" style="border: 1px dashed #2A2A2D;">
|
|
37
|
+
<svg class="w-4 h-4" style="color: #525252;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
38
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
|
39
|
+
</svg>
|
|
40
|
+
</div>
|
|
41
|
+
<p class="text-[11px]" style="color: #8B8B8E;">No projects found</p>
|
|
42
|
+
<p class="text-[10px] mt-1" style="color: #525252;">Looking for .sillyspec dirs</p>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<!-- Projects -->
|
|
46
|
+
<div v-else class="px-3 space-y-0.5">
|
|
47
|
+
<div
|
|
48
|
+
v-for="project in projects"
|
|
49
|
+
:key="project.name"
|
|
50
|
+
:class="[
|
|
51
|
+
'relative rounded-md cursor-pointer transition-all duration-150 overflow-hidden group',
|
|
52
|
+
]"
|
|
53
|
+
:style="{
|
|
54
|
+
background: isActive(project) ? 'rgba(251,191,36,0.06)' : 'transparent',
|
|
55
|
+
borderLeft: isActive(project) ? '2px solid #FBBF24' : '2px solid transparent',
|
|
56
|
+
}"
|
|
57
|
+
@mouseenter="$event.currentTarget.style.background = isActive(project) ? 'rgba(251,191,36,0.08)' : 'rgba(255,255,255,0.02)'"
|
|
58
|
+
@mouseleave="$event.currentTarget.style.background = isActive(project) ? 'rgba(251,191,36,0.06)' : 'transparent'"
|
|
59
|
+
@click="$emit('select', project)"
|
|
60
|
+
>
|
|
61
|
+
<div class="px-3 py-2.5">
|
|
62
|
+
<div class="flex items-center justify-between gap-2">
|
|
63
|
+
<div class="flex-1 min-w-0">
|
|
64
|
+
<h3
|
|
65
|
+
:class="['text-[12px] font-medium truncate transition-colors duration-150 font-[JetBrains_Mono,monospace]']"
|
|
66
|
+
:style="{ color: isActive(project) ? '#FBBF24' : '#E4E4E7' }"
|
|
67
|
+
>
|
|
68
|
+
{{ project.name }}
|
|
69
|
+
</h3>
|
|
70
|
+
<p class="text-[10px] mt-0.5 truncate font-mono-log" style="color: #525252;">
|
|
71
|
+
{{ project.path }}
|
|
72
|
+
</p>
|
|
73
|
+
</div>
|
|
74
|
+
<StageBadge
|
|
75
|
+
v-if="project.state?.currentStage"
|
|
76
|
+
:status="getProjectStatus(project)"
|
|
77
|
+
:label="stageLabel(project)"
|
|
78
|
+
size="sm"
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<!-- Progress -->
|
|
83
|
+
<div v-if="project.state?.progress" class="mt-2 h-[2px] rounded-full overflow-hidden" style="background: #1C1C1F;">
|
|
84
|
+
<div
|
|
85
|
+
class="h-full rounded-full transition-all duration-500 progress-gradient"
|
|
86
|
+
:style="{ width: getProjectProgress(project) + '%' }"
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<!-- Footer -->
|
|
95
|
+
<div class="relative z-10 px-4 py-2.5" style="border-top: 1px solid #1F1F22;">
|
|
96
|
+
<div class="flex items-center justify-between">
|
|
97
|
+
<span class="text-[10px] font-[JetBrains_Mono,monospace]" style="color: #525252;">{{ projects.length }} proj</span>
|
|
98
|
+
<kbd class="text-[9px] px-1.5 py-0.5 rounded font-mono-log" style="color: #525252; background: #141416; border: 1px solid #2A2A2D;">⌘K</kbd>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</template>
|
|
103
|
+
|
|
104
|
+
<script setup>
|
|
105
|
+
import { computed } from 'vue'
|
|
106
|
+
import StageBadge from './StageBadge.vue'
|
|
107
|
+
|
|
108
|
+
const props = defineProps({
|
|
109
|
+
projects: { type: Array, default: () => [] },
|
|
110
|
+
activeProject: { type: Object, default: null },
|
|
111
|
+
isLoading: { type: Boolean, default: false }
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const emit = defineEmits(['select'])
|
|
115
|
+
|
|
116
|
+
function isActive(project) {
|
|
117
|
+
return props.activeProject?.name === project.name
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getProjectStatus(project) {
|
|
121
|
+
const stage = project.state?.currentStage
|
|
122
|
+
if (!stage) return 'pending'
|
|
123
|
+
const stageData = project.state?.progress?.stages?.[stage]
|
|
124
|
+
if (!stageData) return 'pending'
|
|
125
|
+
const steps = stageData.steps || []
|
|
126
|
+
if (steps.length === 0) return 'pending'
|
|
127
|
+
if (steps.some(s => s.status === 'failed')) return 'failed'
|
|
128
|
+
if (steps.some(s => s.status === 'blocked')) return 'blocked'
|
|
129
|
+
if (steps.some(s => s.status === 'in-progress')) return 'in-progress'
|
|
130
|
+
if (steps.every(s => s.status === 'completed')) return 'completed'
|
|
131
|
+
return 'pending'
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function stageLabel(project) {
|
|
135
|
+
const stage = project.state?.currentStage
|
|
136
|
+
const labels = { 'brainstorm': '头脑风暴', 'plan': '规划', 'execute': '执行', 'verify': '验证' }
|
|
137
|
+
return labels[stage] || stage || '未知'
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getProjectProgress(project) {
|
|
141
|
+
const progress = project.state?.progress
|
|
142
|
+
if (!progress) return 0
|
|
143
|
+
const stages = progress.stages || {}
|
|
144
|
+
const stageNames = ['brainstorm', 'plan', 'execute', 'verify']
|
|
145
|
+
let total = 0, done = 0
|
|
146
|
+
for (const s of stageNames) {
|
|
147
|
+
const st = stages[s]
|
|
148
|
+
if (st?.steps) { total += st.steps.length; done += st.steps.filter(x => x.status === 'completed').length }
|
|
149
|
+
}
|
|
150
|
+
return total === 0 ? 0 : Math.round((done / total) * 100)
|
|
151
|
+
}
|
|
152
|
+
</script>
|
|
@@ -0,0 +1,53 @@
|
|
|
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 = { 'completed': 'done', 'in-progress': 'running', 'blocked': 'blocked', 'failed': 'error', 'pending': 'idle' }
|
|
23
|
+
return labels[props.status] || 'idle'
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const sizeClass = computed(() => props.size === 'sm' ? 'px-1.5 py-0.5' : 'px-2 py-1')
|
|
27
|
+
|
|
28
|
+
const badgeStyle = computed(() => {
|
|
29
|
+
const styles = {
|
|
30
|
+
'completed': { background: 'rgba(52,211,153,0.1)', color: '#34D399' },
|
|
31
|
+
'in-progress': { background: 'rgba(251,191,36,0.1)', color: '#FBBF24' },
|
|
32
|
+
'blocked': { background: 'rgba(251,146,60,0.1)', color: '#FB923C' },
|
|
33
|
+
'failed': { background: 'rgba(239,68,68,0.1)', color: '#EF4444' },
|
|
34
|
+
'pending': { background: 'rgba(82,82,82,0.15)', color: '#525252' }
|
|
35
|
+
}
|
|
36
|
+
return styles[props.status] || styles.pending
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const dotClass = computed(() => {
|
|
40
|
+
return props.status === 'in-progress' ? 'animate-pulse-dot' : ''
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const dotStyle = computed(() => {
|
|
44
|
+
const colors = {
|
|
45
|
+
'completed': '#34D399',
|
|
46
|
+
'in-progress': '#FBBF24',
|
|
47
|
+
'blocked': '#FB923C',
|
|
48
|
+
'failed': '#EF4444',
|
|
49
|
+
'pending': '#525252'
|
|
50
|
+
}
|
|
51
|
+
return { background: colors[props.status] || '#525252' }
|
|
52
|
+
})
|
|
53
|
+
</script>
|