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.
Files changed (41) hide show
  1. package/.sillyspec/changes/dashboard/design.md +219 -0
  2. package/.sillyspec/plans/2026-04-05-dashboard.md +737 -0
  3. package/.sillyspec/specs/2026-04-05-dashboard-design.md +206 -0
  4. package/bin/sillyspec.js +0 -0
  5. package/package.json +5 -2
  6. package/packages/dashboard/dist/assets/index-Bh-GPjKY.css +1 -0
  7. package/packages/dashboard/dist/assets/index-CrCn5Gg6.js +17 -0
  8. package/packages/dashboard/dist/index.html +16 -0
  9. package/packages/dashboard/index.html +15 -0
  10. package/packages/dashboard/package-lock.json +2164 -0
  11. package/packages/dashboard/package.json +22 -0
  12. package/packages/dashboard/server/executor.js +86 -0
  13. package/packages/dashboard/server/index.js +359 -0
  14. package/packages/dashboard/server/parser.js +154 -0
  15. package/packages/dashboard/server/watcher.js +277 -0
  16. package/packages/dashboard/src/App.vue +154 -0
  17. package/packages/dashboard/src/components/ActionBar.vue +100 -0
  18. package/packages/dashboard/src/components/CommandPalette.vue +117 -0
  19. package/packages/dashboard/src/components/DetailPanel.vue +122 -0
  20. package/packages/dashboard/src/components/LogStream.vue +85 -0
  21. package/packages/dashboard/src/components/PipelineStage.vue +75 -0
  22. package/packages/dashboard/src/components/PipelineView.vue +94 -0
  23. package/packages/dashboard/src/components/ProjectList.vue +152 -0
  24. package/packages/dashboard/src/components/StageBadge.vue +53 -0
  25. package/packages/dashboard/src/components/StepCard.vue +89 -0
  26. package/packages/dashboard/src/composables/useDashboard.js +171 -0
  27. package/packages/dashboard/src/composables/useKeyboard.js +117 -0
  28. package/packages/dashboard/src/composables/useWebSocket.js +129 -0
  29. package/packages/dashboard/src/main.js +5 -0
  30. package/packages/dashboard/src/style.css +132 -0
  31. package/packages/dashboard/vite.config.js +18 -0
  32. package/src/index.js +68 -8
  33. package/src/init.js +23 -1
  34. package/src/progress.js +422 -0
  35. package/templates/archive.md +56 -0
  36. package/templates/brainstorm.md +82 -26
  37. package/templates/commit.md +2 -0
  38. package/templates/execute.md +2 -1
  39. package/templates/progress-format.md +90 -0
  40. package/templates/quick.md +36 -3
  41. 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;') }
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>