kanban-lite 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/.editorconfig +9 -0
  2. package/.github/workflows/ci.yml +59 -0
  3. package/.github/workflows/release.yml +75 -0
  4. package/.prettierignore +6 -0
  5. package/.prettierrc.yaml +4 -0
  6. package/.vscode/extensions.json +3 -0
  7. package/.vscode/launch.json +17 -0
  8. package/.vscode/settings.json +21 -0
  9. package/.vscode/tasks.json +22 -0
  10. package/.vscodeignore +11 -0
  11. package/CHANGELOG.md +184 -0
  12. package/CLAUDE.md +58 -0
  13. package/CONTRIBUTING.md +114 -0
  14. package/LICENSE +22 -0
  15. package/README.md +482 -0
  16. package/SKILL.md +237 -0
  17. package/dist/cli.js +8716 -0
  18. package/dist/extension.js +8463 -0
  19. package/dist/mcp-server.js +1327 -0
  20. package/dist/standalone-webview/icons-Dx9MGYqN.js +180 -0
  21. package/dist/standalone-webview/icons-Dx9MGYqN.js.map +1 -0
  22. package/dist/standalone-webview/index.js +85 -0
  23. package/dist/standalone-webview/index.js.map +1 -0
  24. package/dist/standalone-webview/react-vendor-DkYdDBET.js +25 -0
  25. package/dist/standalone-webview/react-vendor-DkYdDBET.js.map +1 -0
  26. package/dist/standalone-webview/style.css +1 -0
  27. package/dist/standalone.js +7513 -0
  28. package/dist/webview/icons-Dx9MGYqN.js +180 -0
  29. package/dist/webview/icons-Dx9MGYqN.js.map +1 -0
  30. package/dist/webview/index.js +85 -0
  31. package/dist/webview/index.js.map +1 -0
  32. package/dist/webview/react-vendor-DkYdDBET.js +25 -0
  33. package/dist/webview/react-vendor-DkYdDBET.js.map +1 -0
  34. package/dist/webview/style.css +1 -0
  35. package/docs/images/board-overview.png +0 -0
  36. package/docs/images/editor-view.png +0 -0
  37. package/docs/plans/2026-02-20-kanban-json-config-design.md +74 -0
  38. package/docs/plans/2026-02-20-kanban-json-config.md +690 -0
  39. package/eslint.config.mjs +31 -0
  40. package/package.json +161 -0
  41. package/postcss.config.js +6 -0
  42. package/resources/icon-light.png +0 -0
  43. package/resources/icon-light.svg +105 -0
  44. package/resources/icon.png +0 -0
  45. package/resources/icon.svg +105 -0
  46. package/resources/kanban-dark.svg +21 -0
  47. package/resources/kanban-light.svg +21 -0
  48. package/resources/kanban.svg +21 -0
  49. package/src/cli/index.ts +846 -0
  50. package/src/extension/FeatureHeaderProvider.ts +370 -0
  51. package/src/extension/KanbanPanel.ts +973 -0
  52. package/src/extension/SidebarViewProvider.ts +507 -0
  53. package/src/extension/featureFileUtils.ts +82 -0
  54. package/src/extension/index.ts +234 -0
  55. package/src/mcp-server/index.ts +632 -0
  56. package/src/sdk/KanbanSDK.ts +349 -0
  57. package/src/sdk/__tests__/KanbanSDK.test.ts +468 -0
  58. package/src/sdk/__tests__/parser.test.ts +170 -0
  59. package/src/sdk/fileUtils.ts +76 -0
  60. package/src/sdk/index.ts +6 -0
  61. package/src/sdk/parser.ts +70 -0
  62. package/src/sdk/types.ts +15 -0
  63. package/src/shared/config.ts +113 -0
  64. package/src/shared/editorTypes.ts +14 -0
  65. package/src/shared/types.ts +120 -0
  66. package/src/standalone/__tests__/server.integration.test.ts +1916 -0
  67. package/src/standalone/__tests__/webhooks.test.ts +357 -0
  68. package/src/standalone/fileUtils.ts +70 -0
  69. package/src/standalone/index.ts +71 -0
  70. package/src/standalone/server.ts +1046 -0
  71. package/src/standalone/webhooks.ts +135 -0
  72. package/src/webview/App.tsx +469 -0
  73. package/src/webview/assets/main.css +329 -0
  74. package/src/webview/assets/standalone-theme.css +130 -0
  75. package/src/webview/components/ColumnDialog.tsx +119 -0
  76. package/src/webview/components/CreateFeatureDialog.tsx +524 -0
  77. package/src/webview/components/DatePicker.tsx +185 -0
  78. package/src/webview/components/FeatureCard.tsx +186 -0
  79. package/src/webview/components/FeatureEditor.tsx +623 -0
  80. package/src/webview/components/KanbanBoard.tsx +144 -0
  81. package/src/webview/components/KanbanColumn.tsx +159 -0
  82. package/src/webview/components/MarkdownEditor.tsx +291 -0
  83. package/src/webview/components/PrioritySelect.tsx +39 -0
  84. package/src/webview/components/QuickAddInput.tsx +72 -0
  85. package/src/webview/components/SettingsPanel.tsx +284 -0
  86. package/src/webview/components/Toolbar.tsx +175 -0
  87. package/src/webview/components/UndoToast.tsx +70 -0
  88. package/src/webview/index.html +12 -0
  89. package/src/webview/lib/utils.ts +6 -0
  90. package/src/webview/main.tsx +11 -0
  91. package/src/webview/standalone-main.tsx +13 -0
  92. package/src/webview/standalone-shim.ts +132 -0
  93. package/src/webview/standalone.html +12 -0
  94. package/src/webview/store/index.ts +241 -0
  95. package/tailwind.config.js +53 -0
  96. package/tsconfig.json +22 -0
  97. package/vite.config.ts +36 -0
  98. package/vite.standalone.config.ts +62 -0
  99. package/vitest.config.ts +15 -0
@@ -0,0 +1,132 @@
1
+ // WebSocket bridge that provides acquireVsCodeApi() for standalone mode.
2
+ // Must be imported BEFORE App.tsx so the global is available at module load time.
3
+
4
+ const ws = new WebSocket(`ws://${window.location.host}/ws`)
5
+ const pendingMessages: string[] = []
6
+ let connected = false
7
+
8
+ ws.addEventListener('open', () => {
9
+ connected = true
10
+ for (const msg of pendingMessages) {
11
+ ws.send(msg)
12
+ }
13
+ pendingMessages.length = 0
14
+ })
15
+
16
+ ws.addEventListener('message', (event) => {
17
+ try {
18
+ const data = JSON.parse(event.data)
19
+ window.postMessage(data, '*')
20
+ } catch {
21
+ // ignore malformed messages
22
+ }
23
+ })
24
+
25
+ ws.addEventListener('close', () => {
26
+ connected = false
27
+ })
28
+
29
+ // State persistence via sessionStorage
30
+ let savedState: unknown = null
31
+ try {
32
+ const stored = sessionStorage.getItem('kanban-standalone-state')
33
+ if (stored) savedState = JSON.parse(stored)
34
+ } catch {
35
+ // ignore
36
+ }
37
+
38
+ // --- Standalone attachment handling ---
39
+ function handleAddAttachment(featureId: string) {
40
+ const input = document.createElement('input')
41
+ input.type = 'file'
42
+ input.multiple = true
43
+ input.style.display = 'none'
44
+ document.body.appendChild(input)
45
+
46
+ const cleanup = () => {
47
+ input.remove()
48
+ }
49
+
50
+ input.onchange = async () => {
51
+ if (!input.files || input.files.length === 0) {
52
+ cleanup()
53
+ return
54
+ }
55
+ const files: { name: string; data: string }[] = []
56
+ for (const file of Array.from(input.files)) {
57
+ const buf = await file.arrayBuffer()
58
+ const bytes = new Uint8Array(buf)
59
+ // Encode in chunks to avoid stack overflow on large files
60
+ let binary = ''
61
+ for (let i = 0; i < bytes.length; i++) {
62
+ binary += String.fromCharCode(bytes[i])
63
+ }
64
+ files.push({ name: file.name, data: btoa(binary) })
65
+ }
66
+ try {
67
+ await fetch('/api/upload-attachment', {
68
+ method: 'POST',
69
+ headers: { 'Content-Type': 'application/json' },
70
+ body: JSON.stringify({ featureId, files })
71
+ })
72
+ } catch (err) {
73
+ console.error('Failed to upload attachment:', err)
74
+ }
75
+ cleanup()
76
+ }
77
+
78
+ input.click()
79
+ }
80
+
81
+ function handleOpenAttachment(featureId: string, attachment: string) {
82
+ const url = `/api/attachment?featureId=${encodeURIComponent(featureId)}&filename=${encodeURIComponent(attachment)}`
83
+ window.open(url, '_blank')
84
+ }
85
+
86
+ // Provide the same API shape as acquireVsCodeApi()
87
+ ;(window as unknown as Record<string, unknown>).acquireVsCodeApi = () => ({
88
+ postMessage(message: unknown) {
89
+ const msg = message as Record<string, unknown>
90
+ // Intercept attachment messages — handle browser-side in standalone
91
+ if (msg.type === 'addAttachment') {
92
+ handleAddAttachment(msg.featureId as string)
93
+ return
94
+ }
95
+ if (msg.type === 'openAttachment') {
96
+ handleOpenAttachment(msg.featureId as string, msg.attachment as string)
97
+ return
98
+ }
99
+
100
+ const json = JSON.stringify(message)
101
+ if (connected) {
102
+ ws.send(json)
103
+ } else {
104
+ pendingMessages.push(json)
105
+ }
106
+ },
107
+ getState() {
108
+ return savedState
109
+ },
110
+ setState(state: unknown) {
111
+ savedState = state
112
+ try {
113
+ sessionStorage.setItem('kanban-standalone-state', JSON.stringify(state))
114
+ } catch {
115
+ // ignore
116
+ }
117
+ }
118
+ })
119
+
120
+ // Set dark mode class based on system preference (replaces VSCode's vscode-dark class)
121
+ const darkMq = window.matchMedia('(prefers-color-scheme: dark)')
122
+ function applyTheme(dark: boolean) {
123
+ document.body.classList.toggle('vscode-dark', dark)
124
+ document.body.classList.toggle('vscode-light', !dark)
125
+ }
126
+ darkMq.addEventListener('change', (e) => applyTheme(e.matches))
127
+ // Apply immediately (body may not exist yet, so also apply on DOMContentLoaded)
128
+ if (document.body) {
129
+ applyTheme(darkMq.matches)
130
+ } else {
131
+ document.addEventListener('DOMContentLoaded', () => applyTheme(darkMq.matches))
132
+ }
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Kanban Board</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="./standalone-main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,241 @@
1
+ import { create } from 'zustand'
2
+ import type { Feature, FeatureStatus, KanbanColumn, Priority, CardDisplaySettings } from '../../shared/types'
3
+
4
+ export type DueDateFilter = 'all' | 'overdue' | 'today' | 'this-week' | 'no-date'
5
+ export type LayoutMode = 'horizontal' | 'vertical'
6
+
7
+ interface KanbanState {
8
+ features: Feature[]
9
+ columns: KanbanColumn[]
10
+ isDarkMode: boolean
11
+ searchQuery: string
12
+ priorityFilter: Priority | 'all'
13
+ assigneeFilter: string | 'all'
14
+ labelFilter: string | 'all'
15
+ dueDateFilter: DueDateFilter
16
+ layout: LayoutMode
17
+ cardSettings: CardDisplaySettings
18
+ settingsOpen: boolean
19
+
20
+ setFeatures: (features: Feature[]) => void
21
+ setColumns: (columns: KanbanColumn[]) => void
22
+ setIsDarkMode: (dark: boolean) => void
23
+ setCardSettings: (settings: CardDisplaySettings) => void
24
+ setSettingsOpen: (open: boolean) => void
25
+ setSearchQuery: (query: string) => void
26
+ setPriorityFilter: (priority: Priority | 'all') => void
27
+ setAssigneeFilter: (assignee: string | 'all') => void
28
+ setLabelFilter: (label: string | 'all') => void
29
+ setDueDateFilter: (filter: DueDateFilter) => void
30
+ setLayout: (layout: LayoutMode) => void
31
+ toggleLayout: () => void
32
+ clearAllFilters: () => void
33
+
34
+ addFeature: (feature: Feature) => void
35
+ updateFeature: (id: string, updates: Partial<Feature>) => void
36
+ removeFeature: (id: string) => void
37
+ getFeaturesByStatus: (status: FeatureStatus) => Feature[]
38
+ getFilteredFeaturesByStatus: (status: FeatureStatus) => Feature[]
39
+ getUniqueAssignees: () => string[]
40
+ getUniqueLabels: () => string[]
41
+ hasActiveFilters: () => boolean
42
+ }
43
+
44
+ const getInitialDarkMode = (): boolean => {
45
+ // Check for VSCode theme
46
+ if (typeof document !== 'undefined') {
47
+ return document.body.classList.contains('vscode-dark') ||
48
+ document.body.classList.contains('vscode-high-contrast')
49
+ }
50
+ return false
51
+ }
52
+
53
+ const isToday = (date: Date): boolean => {
54
+ const today = new Date()
55
+ return (
56
+ date.getFullYear() === today.getFullYear() &&
57
+ date.getMonth() === today.getMonth() &&
58
+ date.getDate() === today.getDate()
59
+ )
60
+ }
61
+
62
+ const isThisWeek = (date: Date): boolean => {
63
+ const today = new Date()
64
+ const startOfWeek = new Date(today)
65
+ startOfWeek.setDate(today.getDate() - today.getDay())
66
+ startOfWeek.setHours(0, 0, 0, 0)
67
+
68
+ const endOfWeek = new Date(startOfWeek)
69
+ endOfWeek.setDate(startOfWeek.getDate() + 7)
70
+
71
+ return date >= startOfWeek && date < endOfWeek
72
+ }
73
+
74
+ const isOverdue = (date: Date): boolean => {
75
+ const today = new Date()
76
+ today.setHours(0, 0, 0, 0)
77
+ return date < today
78
+ }
79
+
80
+ export const useStore = create<KanbanState>((set, get) => ({
81
+ features: [],
82
+ columns: [],
83
+ isDarkMode: getInitialDarkMode(),
84
+ searchQuery: '',
85
+ priorityFilter: 'all',
86
+ assigneeFilter: 'all',
87
+ labelFilter: 'all',
88
+ dueDateFilter: 'all',
89
+ layout: 'horizontal',
90
+ cardSettings: {
91
+ showPriorityBadges: true,
92
+ showAssignee: true,
93
+ showDueDate: true,
94
+ showLabels: true,
95
+ showBuildWithAI: true,
96
+ showFileName: false,
97
+ compactMode: false,
98
+ markdownEditorMode: false,
99
+ defaultPriority: 'medium',
100
+ defaultStatus: 'backlog'
101
+ },
102
+ settingsOpen: false,
103
+
104
+ setFeatures: (features) => set({ features }),
105
+ setColumns: (columns) => set({ columns }),
106
+ setIsDarkMode: (dark) => set({ isDarkMode: dark }),
107
+ setCardSettings: (settings) => set({ cardSettings: settings }),
108
+ setSettingsOpen: (open) => set({ settingsOpen: open }),
109
+ setSearchQuery: (query) => set({ searchQuery: query }),
110
+ setPriorityFilter: (priority) => set({ priorityFilter: priority }),
111
+ setAssigneeFilter: (assignee) => set({ assigneeFilter: assignee }),
112
+ setLabelFilter: (label) => set({ labelFilter: label }),
113
+ setDueDateFilter: (filter) => set({ dueDateFilter: filter }),
114
+ setLayout: (layout) => set({ layout }),
115
+ toggleLayout: () => set((state) => ({ layout: state.layout === 'horizontal' ? 'vertical' : 'horizontal' })),
116
+
117
+ clearAllFilters: () =>
118
+ set({
119
+ searchQuery: '',
120
+ priorityFilter: 'all',
121
+ assigneeFilter: 'all',
122
+ labelFilter: 'all',
123
+ dueDateFilter: 'all'
124
+ }),
125
+
126
+ addFeature: (feature) =>
127
+ set((state) => ({
128
+ features: [...state.features, feature]
129
+ })),
130
+
131
+ updateFeature: (id, updates) =>
132
+ set((state) => ({
133
+ features: state.features.map((f) => (f.id === id ? { ...f, ...updates } : f))
134
+ })),
135
+
136
+ removeFeature: (id) =>
137
+ set((state) => ({
138
+ features: state.features.filter((f) => f.id !== id)
139
+ })),
140
+
141
+ getFeaturesByStatus: (status) => {
142
+ const { features } = get()
143
+ return features
144
+ .filter((f) => f.status === status)
145
+ .sort((a, b) => (a.order < b.order ? -1 : a.order > b.order ? 1 : 0))
146
+ },
147
+
148
+ getFilteredFeaturesByStatus: (status) => {
149
+ const {
150
+ features,
151
+ searchQuery,
152
+ priorityFilter,
153
+ assigneeFilter,
154
+ labelFilter,
155
+ dueDateFilter
156
+ } = get()
157
+
158
+ return features
159
+ .filter((f) => {
160
+ if (f.status !== status) return false
161
+
162
+ // Priority filter
163
+ if (priorityFilter !== 'all' && f.priority !== priorityFilter) return false
164
+
165
+ // Assignee filter
166
+ if (assigneeFilter !== 'all') {
167
+ if (assigneeFilter === 'unassigned') {
168
+ if (f.assignee) return false
169
+ } else if (f.assignee !== assigneeFilter) {
170
+ return false
171
+ }
172
+ }
173
+
174
+ // Label filter
175
+ if (labelFilter !== 'all' && !f.labels.includes(labelFilter)) return false
176
+
177
+ // Due date filter
178
+ if (dueDateFilter !== 'all') {
179
+ if (dueDateFilter === 'no-date') {
180
+ if (f.dueDate) return false
181
+ } else if (!f.dueDate) {
182
+ return false
183
+ } else {
184
+ const dueDate = new Date(f.dueDate)
185
+ if (dueDateFilter === 'overdue' && !isOverdue(dueDate)) return false
186
+ if (dueDateFilter === 'today' && !isToday(dueDate)) return false
187
+ if (dueDateFilter === 'this-week' && !isThisWeek(dueDate)) return false
188
+ }
189
+ }
190
+
191
+ // Search query
192
+ if (searchQuery) {
193
+ const query = searchQuery.toLowerCase()
194
+ return (
195
+ f.content.toLowerCase().includes(query) ||
196
+ f.id.toLowerCase().includes(query) ||
197
+ (f.assignee && f.assignee.toLowerCase().includes(query)) ||
198
+ f.labels.some((l) => l.toLowerCase().includes(query))
199
+ )
200
+ }
201
+
202
+ return true
203
+ })
204
+ .sort((a, b) => (a.order < b.order ? -1 : a.order > b.order ? 1 : 0))
205
+ },
206
+
207
+ getUniqueAssignees: () => {
208
+ const { features } = get()
209
+ const assignees = new Set<string>()
210
+ features.forEach((f) => {
211
+ if (f.assignee) assignees.add(f.assignee)
212
+ })
213
+ return Array.from(assignees).sort()
214
+ },
215
+
216
+ getUniqueLabels: () => {
217
+ const { features } = get()
218
+ const labels = new Set<string>()
219
+ features.forEach((f) => {
220
+ f.labels.forEach((l) => labels.add(l))
221
+ })
222
+ return Array.from(labels).sort()
223
+ },
224
+
225
+ hasActiveFilters: () => {
226
+ const {
227
+ searchQuery,
228
+ priorityFilter,
229
+ assigneeFilter,
230
+ labelFilter,
231
+ dueDateFilter
232
+ } = get()
233
+ return (
234
+ searchQuery !== '' ||
235
+ priorityFilter !== 'all' ||
236
+ assigneeFilter !== 'all' ||
237
+ labelFilter !== 'all' ||
238
+ dueDateFilter !== 'all'
239
+ )
240
+ }
241
+ }))
@@ -0,0 +1,53 @@
1
+ import typography from '@tailwindcss/typography'
2
+
3
+ /** @type {import('tailwindcss').Config} */
4
+ export default {
5
+ content: [
6
+ "./src/webview/**/*.{js,ts,jsx,tsx}",
7
+ ],
8
+ darkMode: 'class',
9
+ theme: {
10
+ extend: {
11
+ typography: () => ({
12
+ DEFAULT: {
13
+ css: {
14
+ '--tw-prose-body': 'var(--vscode-foreground)',
15
+ '--tw-prose-headings': 'var(--vscode-foreground)',
16
+ '--tw-prose-lead': 'var(--vscode-foreground)',
17
+ '--tw-prose-links': 'var(--vscode-textLink-foreground)',
18
+ '--tw-prose-bold': 'var(--vscode-foreground)',
19
+ '--tw-prose-counters': 'var(--vscode-descriptionForeground)',
20
+ '--tw-prose-bullets': 'var(--vscode-descriptionForeground)',
21
+ '--tw-prose-hr': 'var(--vscode-panel-border)',
22
+ '--tw-prose-quotes': 'var(--vscode-foreground)',
23
+ '--tw-prose-quote-borders': 'var(--vscode-textBlockQuote-border)',
24
+ '--tw-prose-captions': 'var(--vscode-descriptionForeground)',
25
+ '--tw-prose-code': 'var(--vscode-textPreformat-foreground)',
26
+ '--tw-prose-pre-code': 'var(--vscode-editor-foreground)',
27
+ '--tw-prose-pre-bg': 'var(--vscode-textBlockQuote-background)',
28
+ '--tw-prose-th-borders': 'var(--vscode-panel-border)',
29
+ '--tw-prose-td-borders': 'var(--vscode-panel-border)',
30
+ // Invert colors for dark mode handled by CSS variables
31
+ '--tw-prose-invert-body': 'var(--vscode-foreground)',
32
+ '--tw-prose-invert-headings': 'var(--vscode-foreground)',
33
+ '--tw-prose-invert-lead': 'var(--vscode-foreground)',
34
+ '--tw-prose-invert-links': 'var(--vscode-textLink-foreground)',
35
+ '--tw-prose-invert-bold': 'var(--vscode-foreground)',
36
+ '--tw-prose-invert-counters': 'var(--vscode-descriptionForeground)',
37
+ '--tw-prose-invert-bullets': 'var(--vscode-descriptionForeground)',
38
+ '--tw-prose-invert-hr': 'var(--vscode-panel-border)',
39
+ '--tw-prose-invert-quotes': 'var(--vscode-foreground)',
40
+ '--tw-prose-invert-quote-borders': 'var(--vscode-textBlockQuote-border)',
41
+ '--tw-prose-invert-captions': 'var(--vscode-descriptionForeground)',
42
+ '--tw-prose-invert-code': 'var(--vscode-textPreformat-foreground)',
43
+ '--tw-prose-invert-pre-code': 'var(--vscode-editor-foreground)',
44
+ '--tw-prose-invert-pre-bg': 'var(--vscode-textBlockQuote-background)',
45
+ '--tw-prose-invert-th-borders': 'var(--vscode-panel-border)',
46
+ '--tw-prose-invert-td-borders': 'var(--vscode-panel-border)',
47
+ },
48
+ },
49
+ }),
50
+ },
51
+ },
52
+ plugins: [typography],
53
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
15
+ "baseUrl": ".",
16
+ "paths": {
17
+ "@/*": ["src/*"]
18
+ }
19
+ },
20
+ "include": ["src/**/*"],
21
+ "exclude": ["node_modules", "dist"]
22
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+ import { resolve } from 'path'
4
+
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ root: resolve(__dirname, 'src/webview'),
8
+ build: {
9
+ outDir: resolve(__dirname, 'dist/webview'),
10
+ emptyOutDir: true,
11
+ rollupOptions: {
12
+ input: {
13
+ index: resolve(__dirname, 'src/webview/main.tsx')
14
+ },
15
+ output: {
16
+ entryFileNames: '[name].js',
17
+ chunkFileNames: '[name]-[hash].js',
18
+ assetFileNames: '[name].[ext]',
19
+ // Split vendor deps into separate chunks for parallel loading
20
+ manualChunks: {
21
+ 'react-vendor': ['react', 'react-dom'],
22
+ 'icons': ['lucide-react']
23
+ }
24
+ }
25
+ },
26
+ cssCodeSplit: false,
27
+ sourcemap: true,
28
+ // Optimize chunk size
29
+ chunkSizeWarningLimit: 300
30
+ },
31
+ resolve: {
32
+ alias: {
33
+ '@': resolve(__dirname, 'src')
34
+ }
35
+ }
36
+ })
@@ -0,0 +1,62 @@
1
+ import { resolve } from 'node:path'
2
+
3
+ import react from '@vitejs/plugin-react'
4
+ import { defineConfig, type Plugin } from 'vite'
5
+
6
+ // In dev mode, serve standalone.html as the default page
7
+ function standaloneHtmlPlugin(): Plugin {
8
+ return {
9
+ name: 'standalone-html',
10
+ configureServer(server) {
11
+ server.middlewares.use((req, _res, next) => {
12
+ if (req.url === '/' || req.url === '/index.html') {
13
+ req.url = '/standalone.html'
14
+ }
15
+ next()
16
+ })
17
+ }
18
+ }
19
+ }
20
+
21
+ export default defineConfig({
22
+ plugins: [react(), standaloneHtmlPlugin()],
23
+ root: resolve(__dirname, 'src/webview'),
24
+ server: {
25
+ port: 3000,
26
+ proxy: {
27
+ '/ws': {
28
+ target: 'ws://localhost:3001',
29
+ ws: true
30
+ },
31
+ '/api': {
32
+ target: 'http://localhost:3001'
33
+ }
34
+ }
35
+ },
36
+ build: {
37
+ outDir: resolve(__dirname, 'dist/standalone-webview'),
38
+ emptyOutDir: true,
39
+ rollupOptions: {
40
+ input: {
41
+ index: resolve(__dirname, 'src/webview/standalone-main.tsx')
42
+ },
43
+ output: {
44
+ entryFileNames: '[name].js',
45
+ chunkFileNames: '[name]-[hash].js',
46
+ assetFileNames: '[name].[ext]',
47
+ manualChunks: {
48
+ 'react-vendor': ['react', 'react-dom'],
49
+ 'icons': ['lucide-react']
50
+ }
51
+ }
52
+ },
53
+ cssCodeSplit: false,
54
+ sourcemap: true,
55
+ chunkSizeWarningLimit: 300
56
+ },
57
+ resolve: {
58
+ alias: {
59
+ '@': resolve(__dirname, 'src')
60
+ }
61
+ }
62
+ })
@@ -0,0 +1,15 @@
1
+ import { defineConfig } from 'vitest/config'
2
+ import { resolve } from 'path'
3
+
4
+ export default defineConfig({
5
+ test: {
6
+ root: resolve(__dirname),
7
+ include: ['src/**/*.{test,spec}.{ts,tsx}'],
8
+ testTimeout: 30000
9
+ },
10
+ resolve: {
11
+ alias: {
12
+ '@': resolve(__dirname, 'src')
13
+ }
14
+ }
15
+ })