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,370 @@
1
+ import * as vscode from 'vscode'
2
+ import * as path from 'path'
3
+ import type { FeatureFrontmatter, EditorExtensionMessage, EditorWebviewMessage } from '../shared/editorTypes'
4
+ import type { FeatureStatus, Priority } from '../shared/types'
5
+ import { readConfig, CONFIG_FILENAME } from '../shared/config'
6
+
7
+ /**
8
+ * Provides a webview panel that shows feature metadata (frontmatter) as a header.
9
+ * The actual markdown editing is done by VSCode's native text editor.
10
+ */
11
+ export class FeatureHeaderProvider implements vscode.WebviewViewProvider {
12
+ public static readonly viewType = 'kanban-lite.featureHeader'
13
+
14
+ private _view?: vscode.WebviewView
15
+ private _currentDocument?: vscode.TextDocument
16
+ private _disposables: vscode.Disposable[] = []
17
+
18
+ constructor(private readonly _extensionUri: vscode.Uri) {}
19
+
20
+ public static register(context: vscode.ExtensionContext): vscode.Disposable {
21
+ const provider = new FeatureHeaderProvider(context.extensionUri)
22
+
23
+ const disposables: vscode.Disposable[] = []
24
+
25
+ // Register the webview view provider
26
+ disposables.push(
27
+ vscode.window.registerWebviewViewProvider(
28
+ FeatureHeaderProvider.viewType,
29
+ provider,
30
+ {
31
+ webviewOptions: {
32
+ retainContextWhenHidden: true
33
+ }
34
+ }
35
+ )
36
+ )
37
+
38
+ // Listen for active editor changes
39
+ disposables.push(
40
+ vscode.window.onDidChangeActiveTextEditor(editor => {
41
+ provider._onActiveEditorChanged(editor)
42
+ })
43
+ )
44
+
45
+ // Listen for document changes
46
+ disposables.push(
47
+ vscode.workspace.onDidChangeTextDocument(e => {
48
+ provider._onDocumentChanged(e)
49
+ })
50
+ )
51
+
52
+ // Listen for .kanban.json changes
53
+ const workspaceFolders = vscode.workspace.workspaceFolders
54
+ if (workspaceFolders) {
55
+ const root = workspaceFolders[0].uri.fsPath
56
+ const configPattern = new vscode.RelativePattern(root, CONFIG_FILENAME)
57
+ const configWatcher = vscode.workspace.createFileSystemWatcher(configPattern)
58
+
59
+ const handleConfigChange = () => {
60
+ provider._onActiveEditorChanged(vscode.window.activeTextEditor)
61
+ }
62
+
63
+ configWatcher.onDidChange(handleConfigChange)
64
+ configWatcher.onDidCreate(handleConfigChange)
65
+ configWatcher.onDidDelete(handleConfigChange)
66
+ disposables.push(configWatcher)
67
+ }
68
+
69
+ return vscode.Disposable.from(...disposables)
70
+ }
71
+
72
+ public resolveWebviewView(
73
+ webviewView: vscode.WebviewView,
74
+ _context: vscode.WebviewViewResolveContext,
75
+ _token: vscode.CancellationToken
76
+ ): void {
77
+ this._view = webviewView
78
+
79
+ webviewView.webview.options = {
80
+ enableScripts: true,
81
+ localResourceRoots: [
82
+ vscode.Uri.joinPath(this._extensionUri, 'dist'),
83
+ vscode.Uri.joinPath(this._extensionUri, 'dist', 'webview')
84
+ ]
85
+ }
86
+
87
+ webviewView.webview.html = this._getHtmlForWebview(webviewView.webview)
88
+
89
+ // Handle messages from the webview
90
+ webviewView.webview.onDidReceiveMessage(async (message: EditorWebviewMessage) => {
91
+ switch (message.type) {
92
+ case 'ready':
93
+ this._updateViewForCurrentEditor()
94
+ break
95
+
96
+ case 'frontmatterUpdate':
97
+ await this._updateFrontmatter(message.frontmatter)
98
+ break
99
+
100
+ case 'requestSave':
101
+ if (this._currentDocument) {
102
+ await this._currentDocument.save()
103
+ }
104
+ break
105
+
106
+ case 'startWithAI': {
107
+ if (!this._currentDocument) return
108
+ await this._currentDocument.save()
109
+
110
+ const fullText = this._currentDocument.getText()
111
+ const { frontmatter: fm, content: docContent } = this._parseDocument(fullText)
112
+
113
+ // Parse title from the first # heading in content
114
+ const titleMatch = docContent.match(/^#\s+(.+)$/m)
115
+ const title = titleMatch ? titleMatch[1].trim() : 'Untitled'
116
+
117
+ const labels = fm.labels.length > 0 ? ` [${fm.labels.join(', ')}]` : ''
118
+ const description = docContent.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim()
119
+ const shortDesc = description.length > 200 ? description.substring(0, 200) + '...' : description
120
+
121
+ const prompt = `Implement this feature: "${title}" (${fm.priority} priority)${labels}. ${shortDesc} See full details in: ${this._currentDocument.uri.fsPath}`
122
+
123
+ const agent = message.agent || 'claude'
124
+ const permissionMode = message.permissionMode || 'default'
125
+
126
+ let command: string
127
+ const escapedPrompt = prompt.replace(/"/g, '\\"')
128
+
129
+ switch (agent) {
130
+ case 'claude': {
131
+ const permissionFlag = permissionMode !== 'default' ? ` --permission-mode ${permissionMode}` : ''
132
+ command = `claude${permissionFlag} "${escapedPrompt}"`
133
+ break
134
+ }
135
+ case 'codex': {
136
+ const approvalMap: Record<string, string> = {
137
+ 'default': 'suggest',
138
+ 'plan': 'suggest',
139
+ 'acceptEdits': 'auto-edit',
140
+ 'bypassPermissions': 'full-auto'
141
+ }
142
+ const approvalMode = approvalMap[permissionMode] || 'suggest'
143
+ command = `codex --approval-mode ${approvalMode} "${escapedPrompt}"`
144
+ break
145
+ }
146
+ case 'opencode': {
147
+ command = `opencode "${escapedPrompt}"`
148
+ break
149
+ }
150
+ default:
151
+ command = `claude "${escapedPrompt}"`
152
+ }
153
+
154
+ const agentNames: Record<string, string> = {
155
+ 'claude': 'Claude Code',
156
+ 'codex': 'Codex',
157
+ 'opencode': 'OpenCode'
158
+ }
159
+ const terminal = vscode.window.createTerminal({
160
+ name: agentNames[agent] || 'AI Agent',
161
+ cwd: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
162
+ })
163
+ terminal.show()
164
+ terminal.sendText(command)
165
+ break
166
+ }
167
+ }
168
+ })
169
+
170
+ // Update view when it becomes visible
171
+ webviewView.onDidChangeVisibility(() => {
172
+ if (webviewView.visible) {
173
+ this._updateViewForCurrentEditor()
174
+ }
175
+ })
176
+
177
+ // Check current editor
178
+ this._updateViewForCurrentEditor()
179
+ }
180
+
181
+ private _onActiveEditorChanged(editor: vscode.TextEditor | undefined): void {
182
+ if (!editor) {
183
+ this._currentDocument = undefined
184
+ return
185
+ }
186
+
187
+ // Only track .md files in the features directory (including status subfolders)
188
+ const uri = editor.document.uri
189
+ const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
190
+ if (!workspaceRoot) return
191
+ const kanbanConfig = readConfig(workspaceRoot)
192
+ const fullFeaturesDir = path.join(workspaceRoot, kanbanConfig.featuresDirectory)
193
+ if (uri.fsPath.endsWith('.md') && uri.fsPath.startsWith(fullFeaturesDir + path.sep)) {
194
+ this._currentDocument = editor.document
195
+ this._updateViewForCurrentEditor()
196
+ } else {
197
+ this._currentDocument = undefined
198
+ this._hideView()
199
+ }
200
+ }
201
+
202
+ private _onDocumentChanged(e: vscode.TextDocumentChangeEvent): void {
203
+ if (this._currentDocument && e.document.uri.toString() === this._currentDocument.uri.toString()) {
204
+ this._updateViewForCurrentEditor()
205
+ }
206
+ }
207
+
208
+ private _updateViewForCurrentEditor(): void {
209
+ if (!this._view || !this._currentDocument) return
210
+
211
+ const { frontmatter } = this._parseDocument(this._currentDocument.getText())
212
+ const fileName = this._currentDocument.uri.path.split('/').pop()?.replace(/\.md$/, '') || 'Untitled'
213
+
214
+ const message: EditorExtensionMessage = {
215
+ type: 'init',
216
+ content: '', // Not used anymore
217
+ frontmatter,
218
+ fileName
219
+ }
220
+ this._view.webview.postMessage(message)
221
+ }
222
+
223
+ private _hideView(): void {
224
+ // Send empty state to hide content
225
+ if (this._view) {
226
+ this._view.webview.postMessage({
227
+ type: 'init',
228
+ content: '',
229
+ frontmatter: null,
230
+ fileName: ''
231
+ })
232
+ }
233
+ }
234
+
235
+ private async _updateFrontmatter(frontmatter: FeatureFrontmatter): Promise<void> {
236
+ if (!this._currentDocument) return
237
+
238
+ const { content } = this._parseDocument(this._currentDocument.getText())
239
+ const newText = this._serializeDocument(frontmatter, content)
240
+
241
+ const edit = new vscode.WorkspaceEdit()
242
+ edit.replace(
243
+ this._currentDocument.uri,
244
+ new vscode.Range(0, 0, this._currentDocument.lineCount, 0),
245
+ newText
246
+ )
247
+ await vscode.workspace.applyEdit(edit)
248
+ }
249
+
250
+ private _parseDocument(text: string): { frontmatter: FeatureFrontmatter; content: string } {
251
+ text = text.replace(/\r\n/g, '\n')
252
+ const frontmatterMatch = text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/)
253
+
254
+ if (!frontmatterMatch) {
255
+ return {
256
+ frontmatter: this._getDefaultFrontmatter(),
257
+ content: text
258
+ }
259
+ }
260
+
261
+ const frontmatterText = frontmatterMatch[1]
262
+ const content = frontmatterMatch[2] || ''
263
+
264
+ const getValue = (key: string): string => {
265
+ const match = frontmatterText.match(new RegExp(`^${key}:\\s*(.*)$`, 'm'))
266
+ if (!match) return ''
267
+ const value = match[1].trim().replace(/^["']|["']$/g, '')
268
+ return value === 'null' ? '' : value
269
+ }
270
+
271
+ const getArrayValue = (key: string): string[] => {
272
+ const match = frontmatterText.match(new RegExp(`^${key}:\\s*\\[([^\\]]*)\\]`, 'm'))
273
+ if (!match) return []
274
+ return match[1].split(',').map(s => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean)
275
+ }
276
+
277
+ const frontmatter: FeatureFrontmatter = {
278
+ id: getValue('id') || 'unknown',
279
+ status: (getValue('status') as FeatureStatus) || 'backlog',
280
+ priority: (getValue('priority') as Priority) || 'medium',
281
+ assignee: getValue('assignee') || null,
282
+ dueDate: getValue('dueDate') || null,
283
+ created: getValue('created') || new Date().toISOString(),
284
+ modified: getValue('modified') || new Date().toISOString(),
285
+ completedAt: getValue('completedAt') || null,
286
+ labels: getArrayValue('labels'),
287
+ attachments: getArrayValue('attachments'),
288
+ order: getValue('order') || 'a0'
289
+ }
290
+
291
+ return { frontmatter, content: content.trim() }
292
+ }
293
+
294
+ private _getDefaultFrontmatter(): FeatureFrontmatter {
295
+ const now = new Date().toISOString()
296
+ return {
297
+ id: 'unknown',
298
+ status: 'backlog',
299
+ priority: 'medium',
300
+ assignee: null,
301
+ dueDate: null,
302
+ created: now,
303
+ modified: now,
304
+ completedAt: null,
305
+ labels: [],
306
+ attachments: [],
307
+ order: 'a0'
308
+ }
309
+ }
310
+
311
+ private _serializeDocument(frontmatter: FeatureFrontmatter, content: string): string {
312
+ const updatedFrontmatter = {
313
+ ...frontmatter,
314
+ modified: new Date().toISOString()
315
+ }
316
+
317
+ const frontmatterLines = [
318
+ '---',
319
+ `id: "${updatedFrontmatter.id}"`,
320
+ `status: "${updatedFrontmatter.status}"`,
321
+ `priority: "${updatedFrontmatter.priority}"`,
322
+ `assignee: ${updatedFrontmatter.assignee ? `"${updatedFrontmatter.assignee}"` : 'null'}`,
323
+ `dueDate: ${updatedFrontmatter.dueDate ? `"${updatedFrontmatter.dueDate}"` : 'null'}`,
324
+ `created: "${updatedFrontmatter.created}"`,
325
+ `modified: "${updatedFrontmatter.modified}"`,
326
+ `completedAt: ${updatedFrontmatter.completedAt ? `"${updatedFrontmatter.completedAt}"` : 'null'}`,
327
+ `labels: [${frontmatter.labels.map((l: string) => `"${l}"`).join(', ')}]`,
328
+ `order: "${frontmatter.order}"`,
329
+ '---',
330
+ ''
331
+ ].join('\n')
332
+
333
+ return frontmatterLines + content
334
+ }
335
+
336
+ private _getNonce(): string {
337
+ let text = ''
338
+ const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
339
+ for (let i = 0; i < 32; i++) {
340
+ text += possible.charAt(Math.floor(Math.random() * possible.length))
341
+ }
342
+ return text
343
+ }
344
+
345
+ private _getHtmlForWebview(webview: vscode.Webview): string {
346
+ const scriptUri = webview.asWebviewUri(
347
+ vscode.Uri.joinPath(this._extensionUri, 'dist', 'webview', 'editor.js')
348
+ )
349
+ const styleUri = webview.asWebviewUri(
350
+ vscode.Uri.joinPath(this._extensionUri, 'dist', 'webview', 'style.css')
351
+ )
352
+
353
+ const nonce = this._getNonce()
354
+
355
+ return `<!DOCTYPE html>
356
+ <html lang="en">
357
+ <head>
358
+ <meta charset="UTF-8">
359
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
360
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}';">
361
+ <link href="${styleUri}" rel="stylesheet">
362
+ <title>Feature Header</title>
363
+ </head>
364
+ <body>
365
+ <div id="root"></div>
366
+ <script type="module" nonce="${nonce}" src="${scriptUri}"></script>
367
+ </body>
368
+ </html>`
369
+ }
370
+ }