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.
Files changed (68) hide show
  1. package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -0
  2. package/.claude/skills/sillyspec-execute/SKILL.md +1 -0
  3. package/.claude/skills/sillyspec-plan/SKILL.md +1 -0
  4. package/.claude/skills/sillyspec-quick/SKILL.md +18 -4
  5. package/package.json +1 -1
  6. package/src/db.js +11 -0
  7. package/src/dispatch/backends/local-agent.js +3 -0
  8. package/src/dispatch/backends/sillyhub-mcp.js +3 -0
  9. package/src/git-helper.js +21 -8
  10. package/src/index.js +2 -0
  11. package/src/quicklog.js +2 -0
  12. package/src/run/command.js +59 -7
  13. package/src/run/complete-handlers.js +95 -1
  14. package/src/run/complete.js +23 -5
  15. package/src/run/gates.js +1 -1
  16. package/src/run/prompt.js +34 -20
  17. package/src/run/quick-audit.js +5 -6
  18. package/src/run/shared.js +45 -11
  19. package/src/run/stage.js +10 -0
  20. package/src/run.js +5 -5
  21. package/src/stage-contract-spec.js +11 -0
  22. package/src/stage-contract.js +11 -1
  23. package/src/stage-review.js +9 -1
  24. package/src/stages/archive.js +12 -26
  25. package/src/stages/brainstorm.js +22 -20
  26. package/src/stages/execute.js +48 -0
  27. package/src/stages/plan.js +17 -1
  28. package/src/stages/quick.js +5 -1
  29. package/src/stages/scan.js +2 -2
  30. package/src/stages/verify.js +2 -1
  31. package/src/sync.js +1 -1
  32. package/src/worktree-apply.js +3 -3
  33. package/src/worktree.js +83 -8
  34. package/templates/prompts/testcase-design.md +12 -0
  35. package/templates/prompts/verify-probes.md +1 -0
  36. package/templates/skills/sillyspec-onboard/SKILL.md +92 -0
  37. package/packages/dashboard/index.html +0 -15
  38. package/packages/dashboard/package-lock.json +0 -2384
  39. package/packages/dashboard/public/favicon.jpg +0 -0
  40. package/packages/dashboard/public/logo.jpg +0 -0
  41. package/packages/dashboard/public/prototype-dashboard.html +0 -836
  42. package/packages/dashboard/public/prototype-overview.html +0 -256
  43. package/packages/dashboard/src/App.vue +0 -558
  44. package/packages/dashboard/src/components/ActionBar.vue +0 -93
  45. package/packages/dashboard/src/components/CommandPalette.vue +0 -96
  46. package/packages/dashboard/src/components/DetailPanel.vue +0 -137
  47. package/packages/dashboard/src/components/DocPreview.vue +0 -257
  48. package/packages/dashboard/src/components/DocTree.vue +0 -114
  49. package/packages/dashboard/src/components/HResizeHandle.vue +0 -48
  50. package/packages/dashboard/src/components/LogStream.vue +0 -65
  51. package/packages/dashboard/src/components/PipelineStage.vue +0 -95
  52. package/packages/dashboard/src/components/PipelineView.vue +0 -156
  53. package/packages/dashboard/src/components/ProjectCard.vue +0 -187
  54. package/packages/dashboard/src/components/ProjectList.vue +0 -210
  55. package/packages/dashboard/src/components/ProjectOverview.vue +0 -152
  56. package/packages/dashboard/src/components/StageBadge.vue +0 -67
  57. package/packages/dashboard/src/components/StepCard.vue +0 -94
  58. package/packages/dashboard/src/components/VResizeHandle.vue +0 -61
  59. package/packages/dashboard/src/components/detail/DocsDetail.vue +0 -48
  60. package/packages/dashboard/src/components/detail/GitDetail.vue +0 -61
  61. package/packages/dashboard/src/components/detail/TechDetail.vue +0 -43
  62. package/packages/dashboard/src/composables/useDashboard.js +0 -213
  63. package/packages/dashboard/src/composables/useKeyboard.js +0 -119
  64. package/packages/dashboard/src/composables/useLayout.js +0 -131
  65. package/packages/dashboard/src/composables/useWebSocket.js +0 -129
  66. package/packages/dashboard/src/main.js +0 -8
  67. package/packages/dashboard/src/style.css +0 -132
  68. package/packages/dashboard/vite.config.js +0 -18
@@ -1,61 +0,0 @@
1
- <template>
2
- <div v-if="data" class="px-4 py-3">
3
- <!-- Branch -->
4
- <div class="mb-4">
5
- <h4 class="text-[9px] font-semibold uppercase tracking-[0.2em] mb-1.5 font-[JetBrains_Mono,monospace]" style="color: #6B7280;">当前分支</h4>
6
- <div class="text-[18px] font-bold font-[JetBrains_Mono,monospace]" style="color: #D97706;">{{ data.branch || '—' }}</div>
7
- </div>
8
-
9
- <!-- Commits -->
10
- <div v-if="data.commits?.length" class="mb-4">
11
- <h4 class="text-[9px] font-semibold uppercase tracking-[0.2em] mb-2 font-[JetBrains_Mono,monospace]" style="color: #6B7280;">最近提交</h4>
12
- <div class="space-y-2">
13
- <div v-for="c in data.commits" :key="c.hash" class="px-3 py-2 rounded-md" style="background: #F5F5F7;">
14
- <div class="flex items-center gap-2">
15
- <span class="text-[11px] font-mono font-semibold" style="color: #D97706;">{{ c.hash }}</span>
16
- <span class="text-[11px] flex-1 truncate" style="color: #1C1C1E;">{{ c.message }}</span>
17
- </div>
18
- <div class="flex items-center gap-2 mt-1 text-[10px]" style="color: #6B7280;">
19
- <span>{{ c.author }}</span>
20
- <span>·</span>
21
- <span>{{ relativeTime(c.date) }}</span>
22
- </div>
23
- </div>
24
- </div>
25
- </div>
26
-
27
- <!-- Untracked -->
28
- <div v-if="data.untracked?.length">
29
- <h4 class="text-[9px] font-semibold uppercase tracking-[0.2em] mb-2 font-[JetBrains_Mono,monospace]" style="color: #6B7280;">未提交文件</h4>
30
- <div class="space-y-1">
31
- <div v-for="f in data.untracked" :key="f.file" class="flex items-center gap-2 px-2 py-1 rounded text-[11px] font-[JetBrains_Mono,monospace]">
32
- <span class="font-semibold w-5 text-center" :style="{ color: statusColor(f.status) }">{{ f.status }}</span>
33
- <span class="truncate" style="color: #1C1C1E;">{{ f.file }}</span>
34
- </div>
35
- </div>
36
- </div>
37
- </div>
38
- <n-empty v-else description="无 Git 信息" style="padding: 48px 0;" />
39
- </template>
40
-
41
- <script setup>
42
- defineProps({ data: { type: Object, default: null } })
43
-
44
- function statusColor(s) {
45
- if (s.includes('M')) return '#F59E0B'
46
- if (s.includes('A') || s === '?') return '#10B981'
47
- if (s.includes('D')) return '#EF4444'
48
- return '#9CA3AF'
49
- }
50
-
51
- function relativeTime(iso) {
52
- if (!iso) return ''
53
- const d = new Date(iso)
54
- const diff = Date.now() - d.getTime()
55
- if (diff < 60000) return '刚刚'
56
- if (diff < 3600000) return `${Math.floor(diff / 60000)} 分钟前`
57
- if (diff < 86400000) return `${Math.floor(diff / 3600000)} 小时前`
58
- if (diff < 604800000) return `${Math.floor(diff / 86400000)} 天前`
59
- return `${d.getMonth() + 1}/${d.getDate()}`
60
- }
61
- </script>
@@ -1,43 +0,0 @@
1
- <template>
2
- <div v-if="data" class="px-4 py-3">
3
- <!-- Frameworks -->
4
- <div v-if="data.frameworks?.length" class="mb-4">
5
- <h4 class="text-[9px] font-semibold uppercase tracking-[0.2em] mb-2 font-[JetBrains_Mono,monospace]" style="color: #6B7280;">识别框架</h4>
6
- <div class="flex flex-wrap gap-2">
7
- <n-tag v-for="f in data.frameworks" :key="f" type="warning" size="small" round>{{ f }}</n-tag>
8
- </div>
9
- </div>
10
-
11
- <!-- Dependencies -->
12
- <div v-if="depEntries.length" class="mb-4">
13
- <h4 class="text-[9px] font-semibold uppercase tracking-[0.2em] mb-2 font-[JetBrains_Mono,monospace]" style="color: #6B7280;">dependencies ({{ depEntries.length }})</h4>
14
- <div class="space-y-1">
15
- <div v-for="[name, ver] in depEntries" :key="name" class="flex items-center gap-2 px-2 py-1 rounded text-[11px]" style="background: #F5F5F7;">
16
- <span class="font-mono font-medium truncate" style="color: #1C1C1E;">{{ name }}</span>
17
- <span class="font-mono ml-auto flex-shrink-0" style="color: #9CA3AF;">{{ ver }}</span>
18
- </div>
19
- </div>
20
- </div>
21
-
22
- <!-- Dev Dependencies -->
23
- <div v-if="devDepEntries.length">
24
- <h4 class="text-[9px] font-semibold uppercase tracking-[0.2em] mb-2 font-[JetBrains_Mono,monospace]" style="color: #6B7280;">devDependencies ({{ devDepEntries.length }})</h4>
25
- <div class="space-y-1">
26
- <div v-for="[name, ver] in devDepEntries" :key="name" class="flex items-center gap-2 px-2 py-1 rounded text-[11px]" style="background: #F5F5F7;">
27
- <span class="font-mono font-medium truncate" style="color: #1C1C1E;">{{ name }}</span>
28
- <span class="font-mono ml-auto flex-shrink-0" style="color: #9CA3AF;">{{ ver }}</span>
29
- </div>
30
- </div>
31
- </div>
32
- </div>
33
- <n-empty v-else description="无依赖信息" style="padding: 48px 0;" />
34
- </template>
35
-
36
- <script setup>
37
- import { computed } from 'vue'
38
-
39
- const props = defineProps({ data: { type: Object, default: null } })
40
-
41
- const depEntries = computed(() => props.data ? Object.entries(props.data.dependencies || {}) : [])
42
- const devDepEntries = computed(() => props.data ? Object.entries(props.data.devDependencies || {}) : [])
43
- </script>
@@ -1,213 +0,0 @@
1
- import { reactive, computed } from 'vue'
2
-
3
- /**
4
- * Dashboard state management composable
5
- * @returns {object} Dashboard state and methods
6
- */
7
- export function useDashboard() {
8
- // Reactive state
9
- const state = reactive({
10
- projects: [],
11
- activeProject: null,
12
- activeStep: null,
13
- logs: [],
14
- isPanelOpen: true,
15
- executingProject: null,
16
- isLoading: true,
17
- activeTab: 'pipeline',
18
- docs: { groups: [] },
19
- selectedDocFile: null,
20
- docContent: '',
21
- docLoading: false
22
- })
23
-
24
- /**
25
- * Get project by name
26
- * @param {string} name - Project name
27
- * @returns {object|null} Project or null
28
- */
29
- function getProject(path) {
30
- return state.projects.find(p => p.path === path) || null
31
- }
32
-
33
- /**
34
- * Select a project as active
35
- * @param {object|string} project - Project object or path
36
- */
37
- function selectProject(project) {
38
- const proj = typeof project === 'string'
39
- ? getProject(project)
40
- : project
41
-
42
- if (proj) {
43
- state.activeProject = proj
44
- state.activeStep = null
45
- state.logs = []
46
- state.selectedDocFile = null
47
- state.docContent = ''
48
-
49
- // Load initial logs from project state if available
50
- if (proj.state?.progress?.currentLogs) {
51
- appendLog(proj.state.progress.currentLogs)
52
- }
53
- }
54
- }
55
-
56
- /**
57
- * Select a step within active project
58
- * @param {object} step - Step object
59
- */
60
- function selectStep(step) {
61
- state.activeStep = step
62
- }
63
-
64
- /**
65
- * Append log lines to logs buffer
66
- * @param {string|string[]} lines - Log line(s) to append
67
- */
68
- function appendLog(lines) {
69
- const newLines = Array.isArray(lines) ? lines : [lines]
70
-
71
- for (const line of newLines) {
72
- const timestamp = new Date().toISOString()
73
- state.logs.push({
74
- id: `${timestamp}-${Math.random().toString(36).substr(2, 9)}`,
75
- timestamp,
76
- content: line,
77
- type: 'info'
78
- })
79
- }
80
-
81
- // Keep only last 500 lines
82
- if (state.logs.length > 500) {
83
- state.logs = state.logs.slice(-500)
84
- }
85
- }
86
-
87
- /**
88
- * Clear logs buffer
89
- */
90
- function clearLogs() {
91
- state.logs = []
92
- }
93
-
94
- /**
95
- * Toggle detail panel open/closed
96
- */
97
- function togglePanel() {
98
- state.isPanelOpen = !state.isPanelOpen
99
- }
100
-
101
- /**
102
- * Open detail panel
103
- */
104
- function openPanel() {
105
- state.isPanelOpen = true
106
- }
107
-
108
- /**
109
- * Close detail panel
110
- */
111
- function closePanel() {
112
- state.isPanelOpen = false
113
- }
114
-
115
- /**
116
- * Update projects list
117
- * @param {array} projects - New projects array
118
- */
119
- function updateProjects(projects) {
120
- state.isLoading = false
121
- state.projects = projects
122
-
123
- // Restore active project if it still exists
124
- if (state.activeProject) {
125
- const updated = getProject(state.activeProject.path)
126
- if (updated) {
127
- state.activeProject = updated
128
- return
129
- }
130
- }
131
-
132
- state.activeProject = state.projects[0] || null
133
- }
134
-
135
- /**
136
- * Merge a single project update into the current project list.
137
- * @param {object} project - Partial project payload containing at least path
138
- */
139
- function updateProject(project) {
140
- if (!project?.path) return
141
-
142
- const index = state.projects.findIndex(p => p.path === project.path)
143
- if (index === -1) return
144
-
145
- const updated = {
146
- ...state.projects[index],
147
- ...project,
148
- state: project.state ?? state.projects[index].state,
149
- overview: project.overview ?? state.projects[index].overview
150
- }
151
-
152
- state.projects.splice(index, 1, updated)
153
-
154
- if (state.activeProject?.path === project.path) {
155
- state.activeProject = updated
156
- }
157
- }
158
-
159
- /**
160
- * Set executing project state
161
- * @param {string|null} projectName - Project name or null
162
- */
163
- function setExecuting(projectName) {
164
- state.executingProject = projectName
165
- }
166
-
167
- /**
168
- * Check if a project is currently executing
169
- * @param {string} projectName - Project name
170
- * @returns {boolean}
171
- */
172
- function isExecuting(projectName) {
173
- return state.executingProject === projectName
174
- }
175
-
176
- // Computed properties
177
- const activeProjectName = computed(() => state.activeProject?.name || null)
178
- const activeProjectPath = computed(() => state.activeProject?.path || null)
179
- const activeProjectStage = computed(() => state.activeProject?.state?.currentStage || null)
180
- const hasProjects = computed(() => state.projects.length > 0)
181
- const activeProjectSteps = computed(() => {
182
- if (!state.activeProject?.state?.progress?.stages) return []
183
- const currentStage = state.activeProject.state.currentStage
184
- const stageData = state.activeProject.state.progress.stages[currentStage]
185
- return stageData?.steps || []
186
- })
187
-
188
- return {
189
- state,
190
- getProject,
191
- selectProject,
192
- selectStep,
193
- appendLog,
194
- clearLogs,
195
- togglePanel,
196
- openPanel,
197
- closePanel,
198
- updateProjects,
199
- updateProject,
200
- setExecuting,
201
- isExecuting,
202
- activeProjectName,
203
- activeProjectPath,
204
- activeProjectStage,
205
- hasProjects,
206
- activeProjectSteps,
207
- setActiveTab(tab) { state.activeTab = tab },
208
- updateDocs(docs) { state.docs = docs },
209
- selectDocFile(file) { state.selectedDocFile = file },
210
- setDocContent(content) { state.docContent = content },
211
- setDocLoading(loading) { state.docLoading = loading }
212
- }
213
- }
@@ -1,119 +0,0 @@
1
- import { onMounted, onUnmounted } from 'vue'
2
-
3
- /**
4
- * Keyboard shortcuts composable
5
- * @param {object} options - Configuration options
6
- * @returns {object} Keyboard API
7
- */
8
- export function useKeyboard(options = {}) {
9
- const {
10
- onCmdK = null,
11
- onJ = null,
12
- onK = null,
13
- onEscape = null,
14
- onEnter = null,
15
- onArrowUp = null,
16
- onArrowDown = null,
17
- disabled: initialDisabled = false
18
- } = options
19
-
20
- let isDisabled = initialDisabled
21
-
22
- /**
23
- * Handle keyboard events
24
- * @param {KeyboardEvent} event
25
- */
26
- function handleKeyDown(event) {
27
- if (isDisabled) return
28
-
29
- // Ignore if in input field
30
- const target = event.target
31
- if (target.tagName === 'INPUT' ||
32
- target.tagName === 'TEXTAREA' ||
33
- target.contentEditable === 'true') {
34
- return
35
- }
36
-
37
- const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0
38
- const cmdOrCtrl = isMac ? event.metaKey : event.ctrlKey
39
-
40
- // Cmd/Ctrl + K
41
- if (cmdOrCtrl && event.key === 'k') {
42
- event.preventDefault()
43
- if (onCmdK) onCmdK()
44
- }
45
-
46
- // J - navigate down
47
- if (event.key === 'j' && !cmdOrCtrl) {
48
- event.preventDefault()
49
- if (onJ) onJ()
50
- }
51
-
52
- // K - navigate up
53
- if (event.key === 'k' && !cmdOrCtrl) {
54
- event.preventDefault()
55
- if (onK) onK()
56
- }
57
-
58
- // Escape
59
- if (event.key === 'Escape') {
60
- event.preventDefault()
61
- if (onEscape) onEscape()
62
- }
63
-
64
- // Enter
65
- if (event.key === 'Enter' && !cmdOrCtrl) {
66
- event.preventDefault()
67
- if (onEnter) onEnter()
68
- }
69
-
70
- // Arrow Up
71
- if (event.key === 'ArrowUp' && !cmdOrCtrl) {
72
- event.preventDefault()
73
- if (onArrowUp) onArrowUp()
74
- }
75
-
76
- // Arrow Down
77
- if (event.key === 'ArrowDown' && !cmdOrCtrl) {
78
- event.preventDefault()
79
- if (onArrowDown) onArrowDown()
80
- }
81
- }
82
-
83
- onMounted(() => {
84
- window.addEventListener('keydown', handleKeyDown)
85
- })
86
-
87
- onUnmounted(() => {
88
- window.removeEventListener('keydown', handleKeyDown)
89
- })
90
-
91
- return {
92
- disable: () => { isDisabled = true },
93
- enable: () => { isDisabled = false }
94
- }
95
- }
96
-
97
- /**
98
- * Global keyboard shortcuts for dashboard
99
- * @param {object} callbacks - Callback functions for each shortcut
100
- * @returns {object} Keyboard API
101
- */
102
- export function useDashboardKeyboard(callbacks = {}) {
103
- const {
104
- onOpenCommandPalette,
105
- onNavigateDown,
106
- onNavigateUp,
107
- onClose,
108
- onSelect,
109
- onPanelToggle
110
- } = callbacks
111
-
112
- return useKeyboard({
113
- onCmdK: onOpenCommandPalette,
114
- onJ: onNavigateDown,
115
- onK: onNavigateUp,
116
- onEscape: onClose,
117
- onEnter: onSelect
118
- })
119
- }
@@ -1,131 +0,0 @@
1
- /**
2
- * 布局管理 Composable
3
- * 管理双层布局的概览/详情比例和三栏宽度
4
- */
5
- import { reactive, ref, onMounted } from 'vue'
6
-
7
- const STORAGE_KEY = 'dashboard-layout-v2'
8
-
9
- // 默认值
10
- const DEFAULT_OVERVIEW_HEIGHT = 30 // 概览区域百分比
11
- const DEFAULT_COLUMN_WIDTHS = [33.33, 33.33, 33.33] // 三栏宽度百分比
12
-
13
- // 最小值限制
14
- const MIN_OVERVIEW_HEIGHT = 15
15
- const MAX_OVERVIEW_HEIGHT = 75
16
- const MIN_COLUMN_WIDTH = 10
17
-
18
- /**
19
- * 布局管理 Hook
20
- */
21
- export function useLayout() {
22
- // 布局状态
23
- const layout = reactive({
24
- overviewHeight: DEFAULT_OVERVIEW_HEIGHT,
25
- columnWidths: [...DEFAULT_COLUMN_WIDTHS]
26
- })
27
-
28
- // 拖动状态
29
- const isDragging = ref(false)
30
- const dragType = ref(null) // 'vertical' | 'horizontal'
31
-
32
- /**
33
- * 加载保存的布局
34
- */
35
- function loadLayout() {
36
- try {
37
- const saved = localStorage.getItem(STORAGE_KEY)
38
- if (saved) {
39
- const parsed = JSON.parse(saved)
40
- if (parsed.overviewHeight !== undefined) {
41
- layout.overviewHeight = Math.max(MIN_OVERVIEW_HEIGHT, Math.min(MAX_OVERVIEW_HEIGHT, parsed.overviewHeight))
42
- }
43
- if (Array.isArray(parsed.columnWidths) && parsed.columnWidths.length === 3) {
44
- layout.columnWidths = parsed.columnWidths
45
- }
46
- }
47
- } catch (e) {
48
- console.error('Failed to load layout:', e)
49
- // 使用默认值
50
- resetLayout()
51
- }
52
- }
53
-
54
- /**
55
- * 保存布局到 localStorage
56
- */
57
- function saveLayout() {
58
- try {
59
- localStorage.setItem(STORAGE_KEY, JSON.stringify({
60
- overviewHeight: layout.overviewHeight,
61
- columnWidths: layout.columnWidths
62
- }))
63
- } catch (e) {
64
- console.error('Failed to save layout:', e)
65
- }
66
- }
67
-
68
- /**
69
- * 重置布局为默认值
70
- */
71
- function resetLayout() {
72
- layout.overviewHeight = DEFAULT_OVERVIEW_HEIGHT
73
- layout.columnWidths = [...DEFAULT_COLUMN_WIDTHS]
74
- saveLayout()
75
- }
76
-
77
- /**
78
- * 设置概览区域高度(百分比)
79
- */
80
- function setOverviewHeight(percent) {
81
- layout.overviewHeight = Math.max(MIN_OVERVIEW_HEIGHT, Math.min(MAX_OVERVIEW_HEIGHT, percent))
82
- saveLayout()
83
- }
84
-
85
- /**
86
- * 设置列宽度(索引,百分比)
87
- */
88
- function setColumnWidth(index, percent) {
89
- if (index >= 0 && index < 3) {
90
- layout.columnWidths[index] = Math.max(MIN_COLUMN_WIDTH, percent)
91
- saveLayout()
92
- }
93
- }
94
-
95
- /**
96
- * 开始拖动
97
- */
98
- function startDrag(type) {
99
- isDragging.value = true
100
- dragType.value = type
101
- document.body.classList.add('resizing')
102
- }
103
-
104
- /**
105
- * 结束拖动
106
- */
107
- function endDrag() {
108
- isDragging.value = false
109
- dragType.value = null
110
- document.body.classList.remove('resizing')
111
- saveLayout()
112
- }
113
-
114
- // 组件挂载时加载布局
115
- onMounted(() => {
116
- loadLayout()
117
- })
118
-
119
- return {
120
- layout,
121
- isDragging,
122
- dragType,
123
- loadLayout,
124
- saveLayout,
125
- resetLayout,
126
- setOverviewHeight,
127
- setColumnWidth,
128
- startDrag,
129
- endDrag
130
- }
131
- }
@@ -1,129 +0,0 @@
1
- import { ref, onMounted, onUnmounted } from 'vue'
2
-
3
- /**
4
- * WebSocket connection composable with auto-reconnect
5
- * @returns {object} WebSocket API
6
- */
7
- export function useWebSocket() {
8
- const ws = ref(null)
9
- const connected = ref(false)
10
- const reconnectTimeout = ref(null)
11
- const handlers = new Map()
12
-
13
- /**
14
- * Connect to WebSocket server
15
- */
16
- function connect() {
17
- const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
18
- const wsUrl = `${protocol}//${location.host}`
19
-
20
- ws.value = new WebSocket(wsUrl)
21
-
22
- ws.value.onopen = () => {
23
- connected.value = true
24
- console.log('WebSocket connected')
25
- }
26
-
27
- ws.value.onclose = () => {
28
- connected.value = false
29
- console.log('WebSocket disconnected, reconnecting in 3s...')
30
-
31
- // Auto-reconnect after 3 seconds
32
- reconnectTimeout.value = setTimeout(() => {
33
- connect()
34
- }, 3000)
35
- }
36
-
37
- ws.value.onerror = (err) => {
38
- console.error('WebSocket error:', err)
39
- }
40
-
41
- ws.value.onmessage = (event) => {
42
- try {
43
- const message = JSON.parse(event.data)
44
- const { type, data } = message
45
-
46
- // Call registered handlers for this message type
47
- const typeHandlers = handlers.get(type) || []
48
- typeHandlers.forEach(handler => handler(data))
49
-
50
- // Call wildcard handlers
51
- const wildcardHandlers = handlers.get('*') || []
52
- wildcardHandlers.forEach(handler => handler(message))
53
- } catch (err) {
54
- console.error('Error parsing WebSocket message:', err)
55
- }
56
- }
57
- }
58
-
59
- /**
60
- * Register event handler for a message type
61
- * @param {string} type - Message type
62
- * @param {function} handler - Handler function
63
- * @returns {function} Unsubscribe function
64
- */
65
- function on(type, handler) {
66
- if (!handlers.has(type)) {
67
- handlers.set(type, [])
68
- }
69
- handlers.get(type).push(handler)
70
-
71
- // Return unsubscribe function
72
- return () => {
73
- const typeHandlers = handlers.get(type)
74
- if (typeHandlers) {
75
- const index = typeHandlers.indexOf(handler)
76
- if (index > -1) {
77
- typeHandlers.splice(index, 1)
78
- }
79
- }
80
- }
81
- }
82
-
83
- /**
84
- * Send message to server
85
- * @param {object} data - Message data
86
- */
87
- function send(data) {
88
- if (ws.value && connected.value) {
89
- ws.value.send(JSON.stringify(data))
90
- } else {
91
- console.warn('Cannot send message: WebSocket not connected')
92
- }
93
- }
94
-
95
- /**
96
- * Disconnect WebSocket
97
- */
98
- function disconnect() {
99
- if (reconnectTimeout.value) {
100
- clearTimeout(reconnectTimeout.value)
101
- reconnectTimeout.value = null
102
- }
103
-
104
- if (ws.value) {
105
- ws.value.close()
106
- ws.value = null
107
- }
108
-
109
- connected.value = false
110
- }
111
-
112
- // Auto-connect on mount
113
- onMounted(() => {
114
- connect()
115
- })
116
-
117
- // Cleanup on unmount
118
- onUnmounted(() => {
119
- disconnect()
120
- })
121
-
122
- return {
123
- connected,
124
- on,
125
- send,
126
- disconnect,
127
- reconnect: connect
128
- }
129
- }
@@ -1,8 +0,0 @@
1
- import { createApp } from 'vue'
2
- import naive from 'naive-ui'
3
- import App from './App.vue'
4
- import './style.css'
5
-
6
- const app = createApp(App)
7
- app.use(naive)
8
- app.mount('#app')