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.
- package/.editorconfig +9 -0
- package/.github/workflows/ci.yml +59 -0
- package/.github/workflows/release.yml +75 -0
- package/.prettierignore +6 -0
- package/.prettierrc.yaml +4 -0
- package/.vscode/extensions.json +3 -0
- package/.vscode/launch.json +17 -0
- package/.vscode/settings.json +21 -0
- package/.vscode/tasks.json +22 -0
- package/.vscodeignore +11 -0
- package/CHANGELOG.md +184 -0
- package/CLAUDE.md +58 -0
- package/CONTRIBUTING.md +114 -0
- package/LICENSE +22 -0
- package/README.md +482 -0
- package/SKILL.md +237 -0
- package/dist/cli.js +8716 -0
- package/dist/extension.js +8463 -0
- package/dist/mcp-server.js +1327 -0
- package/dist/standalone-webview/icons-Dx9MGYqN.js +180 -0
- package/dist/standalone-webview/icons-Dx9MGYqN.js.map +1 -0
- package/dist/standalone-webview/index.js +85 -0
- package/dist/standalone-webview/index.js.map +1 -0
- package/dist/standalone-webview/react-vendor-DkYdDBET.js +25 -0
- package/dist/standalone-webview/react-vendor-DkYdDBET.js.map +1 -0
- package/dist/standalone-webview/style.css +1 -0
- package/dist/standalone.js +7513 -0
- package/dist/webview/icons-Dx9MGYqN.js +180 -0
- package/dist/webview/icons-Dx9MGYqN.js.map +1 -0
- package/dist/webview/index.js +85 -0
- package/dist/webview/index.js.map +1 -0
- package/dist/webview/react-vendor-DkYdDBET.js +25 -0
- package/dist/webview/react-vendor-DkYdDBET.js.map +1 -0
- package/dist/webview/style.css +1 -0
- package/docs/images/board-overview.png +0 -0
- package/docs/images/editor-view.png +0 -0
- package/docs/plans/2026-02-20-kanban-json-config-design.md +74 -0
- package/docs/plans/2026-02-20-kanban-json-config.md +690 -0
- package/eslint.config.mjs +31 -0
- package/package.json +161 -0
- package/postcss.config.js +6 -0
- package/resources/icon-light.png +0 -0
- package/resources/icon-light.svg +105 -0
- package/resources/icon.png +0 -0
- package/resources/icon.svg +105 -0
- package/resources/kanban-dark.svg +21 -0
- package/resources/kanban-light.svg +21 -0
- package/resources/kanban.svg +21 -0
- package/src/cli/index.ts +846 -0
- package/src/extension/FeatureHeaderProvider.ts +370 -0
- package/src/extension/KanbanPanel.ts +973 -0
- package/src/extension/SidebarViewProvider.ts +507 -0
- package/src/extension/featureFileUtils.ts +82 -0
- package/src/extension/index.ts +234 -0
- package/src/mcp-server/index.ts +632 -0
- package/src/sdk/KanbanSDK.ts +349 -0
- package/src/sdk/__tests__/KanbanSDK.test.ts +468 -0
- package/src/sdk/__tests__/parser.test.ts +170 -0
- package/src/sdk/fileUtils.ts +76 -0
- package/src/sdk/index.ts +6 -0
- package/src/sdk/parser.ts +70 -0
- package/src/sdk/types.ts +15 -0
- package/src/shared/config.ts +113 -0
- package/src/shared/editorTypes.ts +14 -0
- package/src/shared/types.ts +120 -0
- package/src/standalone/__tests__/server.integration.test.ts +1916 -0
- package/src/standalone/__tests__/webhooks.test.ts +357 -0
- package/src/standalone/fileUtils.ts +70 -0
- package/src/standalone/index.ts +71 -0
- package/src/standalone/server.ts +1046 -0
- package/src/standalone/webhooks.ts +135 -0
- package/src/webview/App.tsx +469 -0
- package/src/webview/assets/main.css +329 -0
- package/src/webview/assets/standalone-theme.css +130 -0
- package/src/webview/components/ColumnDialog.tsx +119 -0
- package/src/webview/components/CreateFeatureDialog.tsx +524 -0
- package/src/webview/components/DatePicker.tsx +185 -0
- package/src/webview/components/FeatureCard.tsx +186 -0
- package/src/webview/components/FeatureEditor.tsx +623 -0
- package/src/webview/components/KanbanBoard.tsx +144 -0
- package/src/webview/components/KanbanColumn.tsx +159 -0
- package/src/webview/components/MarkdownEditor.tsx +291 -0
- package/src/webview/components/PrioritySelect.tsx +39 -0
- package/src/webview/components/QuickAddInput.tsx +72 -0
- package/src/webview/components/SettingsPanel.tsx +284 -0
- package/src/webview/components/Toolbar.tsx +175 -0
- package/src/webview/components/UndoToast.tsx +70 -0
- package/src/webview/index.html +12 -0
- package/src/webview/lib/utils.ts +6 -0
- package/src/webview/main.tsx +11 -0
- package/src/webview/standalone-main.tsx +13 -0
- package/src/webview/standalone-shim.ts +132 -0
- package/src/webview/standalone.html +12 -0
- package/src/webview/store/index.ts +241 -0
- package/tailwind.config.js +53 -0
- package/tsconfig.json +22 -0
- package/vite.config.ts +36 -0
- package/vite.standalone.config.ts +62 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as path from 'path'
|
|
2
|
+
import * as fs from 'fs/promises'
|
|
3
|
+
|
|
4
|
+
export function getFeatureFilePath(featuresDir: string, status: string, filename: string): string {
|
|
5
|
+
return path.join(featuresDir, status, `${filename}.md`)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function ensureDirectories(featuresDir: string): Promise<void> {
|
|
9
|
+
await fs.mkdir(featuresDir, { recursive: true })
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function moveFeatureFile(
|
|
13
|
+
currentPath: string,
|
|
14
|
+
featuresDir: string,
|
|
15
|
+
newStatus: string,
|
|
16
|
+
attachments?: string[]
|
|
17
|
+
): Promise<string> {
|
|
18
|
+
const filename = path.basename(currentPath)
|
|
19
|
+
const targetDir = path.join(featuresDir, newStatus)
|
|
20
|
+
let targetPath = path.join(targetDir, filename)
|
|
21
|
+
|
|
22
|
+
if (currentPath === targetPath) return currentPath
|
|
23
|
+
|
|
24
|
+
const ext = path.extname(filename)
|
|
25
|
+
const base = path.basename(filename, ext)
|
|
26
|
+
let counter = 1
|
|
27
|
+
while (await fileExists(targetPath)) {
|
|
28
|
+
targetPath = path.join(targetDir, `${base}-${counter}${ext}`)
|
|
29
|
+
counter++
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await fs.mkdir(targetDir, { recursive: true })
|
|
33
|
+
await fs.rename(currentPath, targetPath)
|
|
34
|
+
|
|
35
|
+
if (attachments && attachments.length > 0) {
|
|
36
|
+
const sourceDir = path.dirname(currentPath)
|
|
37
|
+
for (const attachment of attachments) {
|
|
38
|
+
const srcAttach = path.join(sourceDir, attachment)
|
|
39
|
+
const destAttach = path.join(targetDir, attachment)
|
|
40
|
+
try {
|
|
41
|
+
await fs.access(srcAttach)
|
|
42
|
+
await fs.rename(srcAttach, destAttach)
|
|
43
|
+
} catch {
|
|
44
|
+
// Best effort -- skip failed attachment moves
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return targetPath
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function renameFeatureFile(currentPath: string, newFilename: string): Promise<string> {
|
|
53
|
+
const dir = path.dirname(currentPath)
|
|
54
|
+
const newPath = path.join(dir, `${newFilename}.md`)
|
|
55
|
+
if (currentPath === newPath) return currentPath
|
|
56
|
+
await fs.rename(currentPath, newPath)
|
|
57
|
+
return newPath
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getStatusFromPath(filePath: string, featuresDir: string): string | null {
|
|
61
|
+
const relative = path.relative(featuresDir, filePath)
|
|
62
|
+
const parts = relative.split(path.sep)
|
|
63
|
+
if (parts.length === 2) {
|
|
64
|
+
return parts[0]
|
|
65
|
+
}
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
70
|
+
try {
|
|
71
|
+
await fs.access(filePath)
|
|
72
|
+
return true
|
|
73
|
+
} catch {
|
|
74
|
+
return false
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/sdk/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { KanbanSDK } from './KanbanSDK'
|
|
2
|
+
export { parseFeatureFile, serializeFeature } from './parser'
|
|
3
|
+
export { getFeatureFilePath, ensureDirectories, moveFeatureFile, getStatusFromPath } from './fileUtils'
|
|
4
|
+
export type { CreateCardInput, BoardConfig } from './types'
|
|
5
|
+
export type { Feature, FeatureStatus, Priority, KanbanColumn } from '../shared/types'
|
|
6
|
+
export { getTitleFromContent, generateFeatureFilename, DEFAULT_COLUMNS } from '../shared/types'
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as path from 'path'
|
|
2
|
+
import type { Feature, FeatureStatus, Priority } from '../shared/types'
|
|
3
|
+
|
|
4
|
+
function extractIdFromFilename(filePath: string): string {
|
|
5
|
+
const basename = path.basename(filePath, '.md')
|
|
6
|
+
// New format: "42-some-slug" → "42"
|
|
7
|
+
const numericMatch = basename.match(/^(\d+)-/)
|
|
8
|
+
if (numericMatch) return numericMatch[1]
|
|
9
|
+
// Legacy format: full basename is the ID
|
|
10
|
+
return basename
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function parseFeatureFile(content: string, filePath: string): Feature | null {
|
|
14
|
+
content = content.replace(/\r\n/g, '\n')
|
|
15
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/)
|
|
16
|
+
if (!frontmatterMatch) return null
|
|
17
|
+
|
|
18
|
+
const frontmatter = frontmatterMatch[1]
|
|
19
|
+
const body = frontmatterMatch[2] || ''
|
|
20
|
+
|
|
21
|
+
const getValue = (key: string): string => {
|
|
22
|
+
const match = frontmatter.match(new RegExp(`^${key}:\\s*(.*)$`, 'm'))
|
|
23
|
+
if (!match) return ''
|
|
24
|
+
const value = match[1].trim().replace(/^["']|["']$/g, '')
|
|
25
|
+
return value === 'null' ? '' : value
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const getArrayValue = (key: string): string[] => {
|
|
29
|
+
const match = frontmatter.match(new RegExp(`^${key}:\\s*\\[([^\\]]*)\\]`, 'm'))
|
|
30
|
+
if (!match) return []
|
|
31
|
+
return match[1].split(',').map(s => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
id: getValue('id') || extractIdFromFilename(filePath),
|
|
36
|
+
status: (getValue('status') as FeatureStatus) || 'backlog',
|
|
37
|
+
priority: (getValue('priority') as Priority) || 'medium',
|
|
38
|
+
assignee: getValue('assignee') || null,
|
|
39
|
+
dueDate: getValue('dueDate') || null,
|
|
40
|
+
created: getValue('created') || new Date().toISOString(),
|
|
41
|
+
modified: getValue('modified') || new Date().toISOString(),
|
|
42
|
+
completedAt: getValue('completedAt') || null,
|
|
43
|
+
labels: getArrayValue('labels'),
|
|
44
|
+
attachments: getArrayValue('attachments'),
|
|
45
|
+
order: getValue('order') || 'a0',
|
|
46
|
+
content: body.trim(),
|
|
47
|
+
filePath
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function serializeFeature(feature: Feature): string {
|
|
52
|
+
const frontmatter = [
|
|
53
|
+
'---',
|
|
54
|
+
`id: "${feature.id}"`,
|
|
55
|
+
`status: "${feature.status}"`,
|
|
56
|
+
`priority: "${feature.priority}"`,
|
|
57
|
+
`assignee: ${feature.assignee ? `"${feature.assignee}"` : 'null'}`,
|
|
58
|
+
`dueDate: ${feature.dueDate ? `"${feature.dueDate}"` : 'null'}`,
|
|
59
|
+
`created: "${feature.created}"`,
|
|
60
|
+
`modified: "${feature.modified}"`,
|
|
61
|
+
`completedAt: ${feature.completedAt ? `"${feature.completedAt}"` : 'null'}`,
|
|
62
|
+
`labels: [${feature.labels.map(l => `"${l}"`).join(', ')}]`,
|
|
63
|
+
`attachments: [${(feature.attachments || []).map(a => `"${a}"`).join(', ')}]`,
|
|
64
|
+
`order: "${feature.order}"`,
|
|
65
|
+
'---',
|
|
66
|
+
''
|
|
67
|
+
].join('\n')
|
|
68
|
+
|
|
69
|
+
return frontmatter + feature.content
|
|
70
|
+
}
|
package/src/sdk/types.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { FeatureStatus, KanbanColumn, Priority } from '../shared/types'
|
|
2
|
+
|
|
3
|
+
export interface CreateCardInput {
|
|
4
|
+
content: string
|
|
5
|
+
status?: FeatureStatus
|
|
6
|
+
priority?: Priority
|
|
7
|
+
assignee?: string | null
|
|
8
|
+
dueDate?: string | null
|
|
9
|
+
labels?: string[]
|
|
10
|
+
attachments?: string[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface BoardConfig {
|
|
14
|
+
columns: KanbanColumn[]
|
|
15
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
import type { KanbanColumn, CardDisplaySettings, Priority, FeatureStatus } from './types'
|
|
4
|
+
|
|
5
|
+
export interface KanbanConfig {
|
|
6
|
+
featuresDirectory: string
|
|
7
|
+
defaultPriority: Priority
|
|
8
|
+
defaultStatus: FeatureStatus
|
|
9
|
+
columns: KanbanColumn[]
|
|
10
|
+
aiAgent: string
|
|
11
|
+
nextCardId: number
|
|
12
|
+
showPriorityBadges: boolean
|
|
13
|
+
showAssignee: boolean
|
|
14
|
+
showDueDate: boolean
|
|
15
|
+
showLabels: boolean
|
|
16
|
+
showBuildWithAI: boolean
|
|
17
|
+
showFileName: boolean
|
|
18
|
+
compactMode: boolean
|
|
19
|
+
markdownEditorMode: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const DEFAULT_CONFIG: KanbanConfig = {
|
|
23
|
+
featuresDirectory: '.kanban',
|
|
24
|
+
defaultPriority: 'medium',
|
|
25
|
+
defaultStatus: 'backlog',
|
|
26
|
+
columns: [
|
|
27
|
+
{ id: 'backlog', name: 'Backlog', color: '#6b7280' },
|
|
28
|
+
{ id: 'todo', name: 'To Do', color: '#3b82f6' },
|
|
29
|
+
{ id: 'in-progress', name: 'In Progress', color: '#f59e0b' },
|
|
30
|
+
{ id: 'review', name: 'Review', color: '#8b5cf6' },
|
|
31
|
+
{ id: 'done', name: 'Done', color: '#22c55e' }
|
|
32
|
+
],
|
|
33
|
+
aiAgent: 'claude',
|
|
34
|
+
nextCardId: 1,
|
|
35
|
+
showPriorityBadges: true,
|
|
36
|
+
showAssignee: true,
|
|
37
|
+
showDueDate: true,
|
|
38
|
+
showLabels: true,
|
|
39
|
+
showBuildWithAI: true,
|
|
40
|
+
showFileName: false,
|
|
41
|
+
compactMode: false,
|
|
42
|
+
markdownEditorMode: false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const CONFIG_FILENAME = '.kanban.json'
|
|
46
|
+
|
|
47
|
+
export function configPath(workspaceRoot: string): string {
|
|
48
|
+
return path.join(workspaceRoot, CONFIG_FILENAME)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function readConfig(workspaceRoot: string): KanbanConfig {
|
|
52
|
+
const filePath = configPath(workspaceRoot)
|
|
53
|
+
try {
|
|
54
|
+
const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
|
|
55
|
+
return { ...DEFAULT_CONFIG, ...raw }
|
|
56
|
+
} catch {
|
|
57
|
+
return { ...DEFAULT_CONFIG }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function writeConfig(workspaceRoot: string, config: KanbanConfig): void {
|
|
62
|
+
const filePath = configPath(workspaceRoot)
|
|
63
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n', 'utf-8')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Read and increment the nextCardId counter, returning the allocated ID */
|
|
67
|
+
export function allocateCardId(workspaceRoot: string): number {
|
|
68
|
+
const config = readConfig(workspaceRoot)
|
|
69
|
+
const id = config.nextCardId
|
|
70
|
+
writeConfig(workspaceRoot, { ...config, nextCardId: id + 1 })
|
|
71
|
+
return id
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Ensure nextCardId is ahead of all existing numeric IDs */
|
|
75
|
+
export function syncCardIdCounter(workspaceRoot: string, existingIds: number[]): void {
|
|
76
|
+
if (existingIds.length === 0) return
|
|
77
|
+
const maxId = Math.max(...existingIds)
|
|
78
|
+
const config = readConfig(workspaceRoot)
|
|
79
|
+
if (config.nextCardId <= maxId) {
|
|
80
|
+
writeConfig(workspaceRoot, { ...config, nextCardId: maxId + 1 })
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Extract CardDisplaySettings from a KanbanConfig */
|
|
85
|
+
export function configToSettings(config: KanbanConfig): CardDisplaySettings {
|
|
86
|
+
return {
|
|
87
|
+
showPriorityBadges: config.showPriorityBadges,
|
|
88
|
+
showAssignee: config.showAssignee,
|
|
89
|
+
showDueDate: config.showDueDate,
|
|
90
|
+
showLabels: config.showLabels,
|
|
91
|
+
showBuildWithAI: config.showBuildWithAI,
|
|
92
|
+
showFileName: config.showFileName,
|
|
93
|
+
compactMode: config.compactMode,
|
|
94
|
+
markdownEditorMode: config.markdownEditorMode,
|
|
95
|
+
defaultPriority: config.defaultPriority,
|
|
96
|
+
defaultStatus: config.defaultStatus
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Merge CardDisplaySettings back into a KanbanConfig */
|
|
101
|
+
export function settingsToConfig(config: KanbanConfig, settings: CardDisplaySettings): KanbanConfig {
|
|
102
|
+
return {
|
|
103
|
+
...config,
|
|
104
|
+
showPriorityBadges: settings.showPriorityBadges,
|
|
105
|
+
showAssignee: settings.showAssignee,
|
|
106
|
+
showDueDate: settings.showDueDate,
|
|
107
|
+
showLabels: settings.showLabels,
|
|
108
|
+
showFileName: settings.showFileName,
|
|
109
|
+
compactMode: settings.compactMode,
|
|
110
|
+
defaultPriority: settings.defaultPriority,
|
|
111
|
+
defaultStatus: settings.defaultStatus
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FeatureFrontmatter } from './types'
|
|
2
|
+
|
|
3
|
+
export type { FeatureFrontmatter }
|
|
4
|
+
|
|
5
|
+
// Messages from the extension to the editor webview
|
|
6
|
+
export type EditorExtensionMessage =
|
|
7
|
+
| { type: 'init'; content: string; frontmatter: FeatureFrontmatter | null; fileName: string }
|
|
8
|
+
|
|
9
|
+
// Messages from the editor webview to the extension
|
|
10
|
+
export type EditorWebviewMessage =
|
|
11
|
+
| { type: 'ready' }
|
|
12
|
+
| { type: 'frontmatterUpdate'; frontmatter: FeatureFrontmatter }
|
|
13
|
+
| { type: 'requestSave' }
|
|
14
|
+
| { type: 'startWithAI'; agent?: 'claude' | 'codex' | 'opencode'; permissionMode?: 'default' | 'plan' | 'acceptEdits' | 'bypassPermissions' }
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Kanban types
|
|
2
|
+
|
|
3
|
+
export type Priority = 'critical' | 'high' | 'medium' | 'low'
|
|
4
|
+
export type FeatureStatus = 'backlog' | 'todo' | 'in-progress' | 'review' | 'done'
|
|
5
|
+
|
|
6
|
+
export interface Feature {
|
|
7
|
+
id: string
|
|
8
|
+
status: FeatureStatus
|
|
9
|
+
priority: Priority
|
|
10
|
+
assignee: string | null
|
|
11
|
+
dueDate: string | null
|
|
12
|
+
created: string
|
|
13
|
+
modified: string
|
|
14
|
+
completedAt: string | null
|
|
15
|
+
labels: string[]
|
|
16
|
+
attachments: string[]
|
|
17
|
+
order: string
|
|
18
|
+
content: string
|
|
19
|
+
filePath: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Parse title from the first # heading in markdown content, falling back to the first line
|
|
23
|
+
export function getTitleFromContent(content: string): string {
|
|
24
|
+
const match = content.match(/^#\s+(.+)$/m)
|
|
25
|
+
if (match) return match[1].trim()
|
|
26
|
+
const firstLine = content.split('\n').map(l => l.trim()).find(l => l.length > 0)
|
|
27
|
+
return firstLine || 'Untitled'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Generate a filename-safe slug from a title
|
|
31
|
+
export function generateSlug(title: string): string {
|
|
32
|
+
return title
|
|
33
|
+
.toLowerCase()
|
|
34
|
+
.replace(/[^a-z0-9\s-]/g, '') // Remove special characters
|
|
35
|
+
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
|
36
|
+
.replace(/-+/g, '-') // Replace multiple hyphens with single
|
|
37
|
+
.replace(/^-|-$/g, '') // Trim hyphens from start/end
|
|
38
|
+
.slice(0, 50) || 'feature' // Limit length, fallback
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Generate a filename from an incremental ID and a title
|
|
42
|
+
export function generateFeatureFilename(id: number, title: string): string {
|
|
43
|
+
const slug = generateSlug(title)
|
|
44
|
+
return `${id}-${slug}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Extract the numeric ID prefix from a filename or ID string like "42-build-dashboard"
|
|
48
|
+
export function extractNumericId(filenameOrId: string): number | null {
|
|
49
|
+
const match = filenameOrId.match(/^(\d+)(?:-|$)/)
|
|
50
|
+
return match ? parseInt(match[1], 10) : null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface KanbanColumn {
|
|
54
|
+
id: string
|
|
55
|
+
name: string
|
|
56
|
+
color: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const DEFAULT_COLUMNS: KanbanColumn[] = [
|
|
60
|
+
{ id: 'backlog', name: 'Backlog', color: '#6b7280' },
|
|
61
|
+
{ id: 'todo', name: 'To Do', color: '#3b82f6' },
|
|
62
|
+
{ id: 'in-progress', name: 'In Progress', color: '#f59e0b' },
|
|
63
|
+
{ id: 'review', name: 'Review', color: '#8b5cf6' },
|
|
64
|
+
{ id: 'done', name: 'Done', color: '#22c55e' }
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
export interface CardDisplaySettings {
|
|
68
|
+
showPriorityBadges: boolean
|
|
69
|
+
showAssignee: boolean
|
|
70
|
+
showDueDate: boolean
|
|
71
|
+
showLabels: boolean
|
|
72
|
+
showBuildWithAI: boolean
|
|
73
|
+
showFileName: boolean
|
|
74
|
+
compactMode: boolean
|
|
75
|
+
markdownEditorMode: boolean
|
|
76
|
+
defaultPriority: Priority
|
|
77
|
+
defaultStatus: FeatureStatus
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Messages between extension and webview
|
|
81
|
+
export type ExtensionMessage =
|
|
82
|
+
| { type: 'init'; features: Feature[]; columns: KanbanColumn[]; settings: CardDisplaySettings }
|
|
83
|
+
| { type: 'featuresUpdated'; features: Feature[] }
|
|
84
|
+
| { type: 'triggerCreateDialog' }
|
|
85
|
+
| { type: 'featureContent'; featureId: string; content: string; frontmatter: FeatureFrontmatter }
|
|
86
|
+
| { type: 'showSettings'; settings: CardDisplaySettings }
|
|
87
|
+
|
|
88
|
+
// Frontmatter for editing
|
|
89
|
+
export interface FeatureFrontmatter {
|
|
90
|
+
id: string
|
|
91
|
+
status: FeatureStatus
|
|
92
|
+
priority: Priority
|
|
93
|
+
assignee: string | null
|
|
94
|
+
dueDate: string | null
|
|
95
|
+
created: string
|
|
96
|
+
modified: string
|
|
97
|
+
completedAt: string | null
|
|
98
|
+
labels: string[]
|
|
99
|
+
attachments: string[]
|
|
100
|
+
order: string
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type WebviewMessage =
|
|
104
|
+
| { type: 'ready' }
|
|
105
|
+
| { type: 'createFeature'; data: { status: FeatureStatus; priority: Priority; content: string; assignee: string | null; dueDate: string | null; labels: string[] } }
|
|
106
|
+
| { type: 'moveFeature'; featureId: string; newStatus: string; newOrder: number }
|
|
107
|
+
| { type: 'deleteFeature'; featureId: string }
|
|
108
|
+
| { type: 'updateFeature'; featureId: string; updates: Partial<Feature> }
|
|
109
|
+
| { type: 'openFeature'; featureId: string }
|
|
110
|
+
| { type: 'saveFeatureContent'; featureId: string; content: string; frontmatter: FeatureFrontmatter }
|
|
111
|
+
| { type: 'closeFeature' }
|
|
112
|
+
| { type: 'openFile'; featureId: string }
|
|
113
|
+
| { type: 'addAttachment'; featureId: string }
|
|
114
|
+
| { type: 'openAttachment'; featureId: string; attachment: string }
|
|
115
|
+
| { type: 'removeAttachment'; featureId: string; attachment: string }
|
|
116
|
+
| { type: 'openSettings' }
|
|
117
|
+
| { type: 'saveSettings'; settings: CardDisplaySettings }
|
|
118
|
+
| { type: 'addColumn'; column: { name: string; color: string } }
|
|
119
|
+
| { type: 'editColumn'; columnId: string; updates: { name: string; color: string } }
|
|
120
|
+
| { type: 'removeColumn'; columnId: string }
|