@andypai/agent-kanban 0.2.0 → 0.3.1
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/README.md +120 -24
- package/package.json +4 -2
- package/src/__tests__/activity.test.ts +16 -10
- package/src/__tests__/api.test.ts +99 -3
- package/src/__tests__/board-utils.test.ts +100 -0
- package/src/__tests__/commands/board.test.ts +7 -14
- package/src/__tests__/commands/bulk.test.ts +3 -3
- package/src/__tests__/commands/column.test.ts +4 -4
- package/src/__tests__/conflict.test.ts +64 -0
- package/src/__tests__/db.test.ts +2 -2
- package/src/__tests__/id.test.ts +1 -1
- package/src/__tests__/index.test.ts +233 -56
- package/src/__tests__/jira-adf.test.ts +180 -0
- package/src/__tests__/jira-cache.test.ts +304 -0
- package/src/__tests__/jira-client.test.ts +169 -0
- package/src/__tests__/jira-provider-comment.test.ts +281 -0
- package/src/__tests__/jira-provider-mutations.test.ts +771 -0
- package/src/__tests__/jira-provider-read.test.ts +594 -0
- package/src/__tests__/jira-wiring.test.ts +187 -0
- package/src/__tests__/linear-cache-description-activity.test.ts +142 -0
- package/src/__tests__/linear-provider-comment.test.ts +243 -0
- package/src/__tests__/linear-provider-sync.test.ts +488 -0
- package/src/__tests__/local-provider-comment.test.ts +60 -0
- package/src/__tests__/mcp-core.test.ts +164 -0
- package/src/__tests__/mcp-server.test.ts +252 -0
- package/src/__tests__/metrics.test.ts +2 -2
- package/src/__tests__/output.test.ts +1 -1
- package/src/__tests__/provider-capabilities.test.ts +40 -0
- package/src/__tests__/server.test.ts +291 -0
- package/src/__tests__/webhooks.test.ts +604 -0
- package/src/activity.ts +2 -12
- package/src/api.ts +156 -21
- package/src/commands/board.ts +4 -14
- package/src/commands/bulk.ts +4 -4
- package/src/commands/column.ts +4 -4
- package/src/commands/mcp.ts +87 -0
- package/src/config.ts +1 -1
- package/src/db.ts +118 -6
- package/src/errors.ts +2 -0
- package/src/id.ts +1 -1
- package/src/index.ts +83 -35
- package/src/mcp/core.ts +193 -0
- package/src/mcp/errors.ts +109 -0
- package/src/mcp/index.ts +13 -0
- package/src/mcp/server.ts +512 -0
- package/src/mcp/types.ts +72 -0
- package/src/metrics.ts +1 -1
- package/src/output.ts +1 -1
- package/src/providers/capabilities.ts +22 -17
- package/src/providers/errors.ts +1 -1
- package/src/providers/index.ts +36 -6
- package/src/providers/jira-adf.ts +275 -0
- package/src/providers/jira-cache.ts +625 -0
- package/src/providers/jira-client.ts +390 -0
- package/src/providers/jira.ts +773 -0
- package/src/providers/linear-cache.ts +250 -71
- package/src/providers/linear-client.ts +255 -15
- package/src/providers/linear.ts +338 -20
- package/src/providers/local.ts +74 -23
- package/src/providers/types.ts +19 -3
- package/src/server.ts +141 -13
- package/src/tunnel.ts +79 -0
- package/src/types.ts +19 -2
- package/src/webhooks.ts +36 -0
- package/ui/dist/assets/index-DBnoKL_k.css +1 -0
- package/ui/dist/assets/index-qNVJ6clH.js +40 -0
- package/ui/dist/index.html +2 -2
- package/src/__tests__/commands/task.test.ts +0 -144
- package/src/commands/task.ts +0 -117
- package/src/fixtures.ts +0 -128
- package/ui/dist/assets/index-B8f9NB4z.css +0 -1
- package/ui/dist/assets/index-zWp-rB7b.js +0 -40
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test, beforeEach } from 'bun:test'
|
|
2
|
-
import { Database } from 'bun:sqlite'
|
|
3
|
-
import { initSchema, seedDefaultColumns } from '../../db.ts'
|
|
4
|
-
import {
|
|
5
|
-
taskAdd,
|
|
6
|
-
taskList,
|
|
7
|
-
taskView,
|
|
8
|
-
taskUpdate,
|
|
9
|
-
taskDelete,
|
|
10
|
-
taskMove,
|
|
11
|
-
taskAssign,
|
|
12
|
-
taskPrioritize,
|
|
13
|
-
} from '../../commands/task.ts'
|
|
14
|
-
import { KanbanError } from '../../errors.ts'
|
|
15
|
-
import type { TaskWithColumn } from '../../types.ts'
|
|
16
|
-
|
|
17
|
-
let db: Database
|
|
18
|
-
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
db = new Database(':memory:')
|
|
21
|
-
db.run('PRAGMA foreign_keys = ON')
|
|
22
|
-
initSchema(db)
|
|
23
|
-
seedDefaultColumns(db)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
describe('taskAdd', () => {
|
|
27
|
-
test('adds a task with all options', () => {
|
|
28
|
-
const result = taskAdd(db, {
|
|
29
|
-
title: 'Build feature',
|
|
30
|
-
description: 'Do the thing',
|
|
31
|
-
column: 'recurring',
|
|
32
|
-
priority: 'high',
|
|
33
|
-
assignee: 'alice',
|
|
34
|
-
metadata: '{"sprint": 5}',
|
|
35
|
-
})
|
|
36
|
-
expect(result.ok).toBe(true)
|
|
37
|
-
if (result.ok) {
|
|
38
|
-
const task = result.data as TaskWithColumn
|
|
39
|
-
expect(task.title).toBe('Build feature')
|
|
40
|
-
expect(task.description).toBe('Do the thing')
|
|
41
|
-
expect(task.priority).toBe('high')
|
|
42
|
-
expect(task.assignee).toBe('alice')
|
|
43
|
-
expect(task.metadata).toBe('{"sprint": 5}')
|
|
44
|
-
}
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
test('throws without title', () => {
|
|
48
|
-
expect(() => taskAdd(db, {})).toThrow(KanbanError)
|
|
49
|
-
})
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
describe('taskList', () => {
|
|
53
|
-
test('lists all tasks', () => {
|
|
54
|
-
taskAdd(db, { title: 'A' })
|
|
55
|
-
taskAdd(db, { title: 'B' })
|
|
56
|
-
const result = taskList(db, {})
|
|
57
|
-
expect(result.ok).toBe(true)
|
|
58
|
-
if (result.ok) {
|
|
59
|
-
expect(result.data as TaskWithColumn[]).toHaveLength(2)
|
|
60
|
-
}
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
test('filters by column', () => {
|
|
64
|
-
taskAdd(db, { title: 'A', column: 'recurring' })
|
|
65
|
-
taskAdd(db, { title: 'B', column: 'done' })
|
|
66
|
-
const result = taskList(db, { column: 'recurring' })
|
|
67
|
-
if (result.ok) {
|
|
68
|
-
expect(result.data as TaskWithColumn[]).toHaveLength(1)
|
|
69
|
-
}
|
|
70
|
-
})
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
describe('taskView', () => {
|
|
74
|
-
test('returns task details', () => {
|
|
75
|
-
const addResult = taskAdd(db, { title: 'View me' })
|
|
76
|
-
if (!addResult.ok) throw new Error('unexpected')
|
|
77
|
-
const task = addResult.data as TaskWithColumn
|
|
78
|
-
const result = taskView(db, { id: task.id })
|
|
79
|
-
expect(result.ok).toBe(true)
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
test('throws for missing id', () => {
|
|
83
|
-
expect(() => taskView(db, {})).toThrow(KanbanError)
|
|
84
|
-
})
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
describe('taskUpdate', () => {
|
|
88
|
-
test('updates task fields', () => {
|
|
89
|
-
const addResult = taskAdd(db, { title: 'Original' })
|
|
90
|
-
if (!addResult.ok) throw new Error('unexpected')
|
|
91
|
-
const task = addResult.data as TaskWithColumn
|
|
92
|
-
const result = taskUpdate(db, { id: task.id, title: 'Modified' })
|
|
93
|
-
if (result.ok) {
|
|
94
|
-
expect((result.data as TaskWithColumn).title).toBe('Modified')
|
|
95
|
-
}
|
|
96
|
-
})
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
describe('taskDelete', () => {
|
|
100
|
-
test('deletes task', () => {
|
|
101
|
-
const addResult = taskAdd(db, { title: 'Delete me' })
|
|
102
|
-
if (!addResult.ok) throw new Error('unexpected')
|
|
103
|
-
const task = addResult.data as TaskWithColumn
|
|
104
|
-
const result = taskDelete(db, { id: task.id })
|
|
105
|
-
expect(result.ok).toBe(true)
|
|
106
|
-
expect(() => taskView(db, { id: task.id })).toThrow(KanbanError)
|
|
107
|
-
})
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
describe('taskMove', () => {
|
|
111
|
-
test('moves task to new column', () => {
|
|
112
|
-
const addResult = taskAdd(db, { title: 'Move me', column: 'recurring' })
|
|
113
|
-
if (!addResult.ok) throw new Error('unexpected')
|
|
114
|
-
const task = addResult.data as TaskWithColumn
|
|
115
|
-
const result = taskMove(db, { id: task.id, column: 'in-progress' })
|
|
116
|
-
if (result.ok) {
|
|
117
|
-
expect((result.data as TaskWithColumn).column_name).toBe('in-progress')
|
|
118
|
-
}
|
|
119
|
-
})
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
describe('taskAssign', () => {
|
|
123
|
-
test('assigns task', () => {
|
|
124
|
-
const addResult = taskAdd(db, { title: 'Assign me' })
|
|
125
|
-
if (!addResult.ok) throw new Error('unexpected')
|
|
126
|
-
const task = addResult.data as TaskWithColumn
|
|
127
|
-
const result = taskAssign(db, { id: task.id, assignee: 'bob' })
|
|
128
|
-
if (result.ok) {
|
|
129
|
-
expect((result.data as TaskWithColumn).assignee).toBe('bob')
|
|
130
|
-
}
|
|
131
|
-
})
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
describe('taskPrioritize', () => {
|
|
135
|
-
test('sets priority', () => {
|
|
136
|
-
const addResult = taskAdd(db, { title: 'Prioritize me' })
|
|
137
|
-
if (!addResult.ok) throw new Error('unexpected')
|
|
138
|
-
const task = addResult.data as TaskWithColumn
|
|
139
|
-
const result = taskPrioritize(db, { id: task.id, priority: 'urgent' })
|
|
140
|
-
if (result.ok) {
|
|
141
|
-
expect((result.data as TaskWithColumn).priority).toBe('urgent')
|
|
142
|
-
}
|
|
143
|
-
})
|
|
144
|
-
})
|
package/src/commands/task.ts
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import type { Database } from 'bun:sqlite'
|
|
2
|
-
import { addTask, getTask, listTasks, updateTask, deleteTask, moveTask } from '../db.ts'
|
|
3
|
-
import { ErrorCode, KanbanError } from '../errors.ts'
|
|
4
|
-
import { success } from '../output.ts'
|
|
5
|
-
import type { CliOutput, Priority } from '../types.ts'
|
|
6
|
-
|
|
7
|
-
export function taskAdd(
|
|
8
|
-
db: Database,
|
|
9
|
-
args: {
|
|
10
|
-
title?: string
|
|
11
|
-
description?: string
|
|
12
|
-
column?: string
|
|
13
|
-
priority?: string
|
|
14
|
-
assignee?: string
|
|
15
|
-
project?: string
|
|
16
|
-
metadata?: string
|
|
17
|
-
},
|
|
18
|
-
): CliOutput {
|
|
19
|
-
if (!args.title) {
|
|
20
|
-
throw new KanbanError(ErrorCode.MISSING_ARGUMENT, 'Task title is required')
|
|
21
|
-
}
|
|
22
|
-
return success(
|
|
23
|
-
addTask(db, args.title, {
|
|
24
|
-
description: args.description,
|
|
25
|
-
column: args.column,
|
|
26
|
-
priority: args.priority as Priority | undefined,
|
|
27
|
-
assignee: args.assignee,
|
|
28
|
-
project: args.project,
|
|
29
|
-
metadata: args.metadata,
|
|
30
|
-
}),
|
|
31
|
-
)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function taskList(
|
|
35
|
-
db: Database,
|
|
36
|
-
opts: {
|
|
37
|
-
column?: string
|
|
38
|
-
priority?: string
|
|
39
|
-
assignee?: string
|
|
40
|
-
project?: string
|
|
41
|
-
limit?: string
|
|
42
|
-
sort?: string
|
|
43
|
-
},
|
|
44
|
-
): CliOutput {
|
|
45
|
-
return success(
|
|
46
|
-
listTasks(db, {
|
|
47
|
-
column: opts.column,
|
|
48
|
-
priority: opts.priority,
|
|
49
|
-
assignee: opts.assignee,
|
|
50
|
-
project: opts.project,
|
|
51
|
-
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
52
|
-
sort: opts.sort,
|
|
53
|
-
}),
|
|
54
|
-
)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function taskView(db: Database, args: { id?: string }): CliOutput {
|
|
58
|
-
if (!args.id) {
|
|
59
|
-
throw new KanbanError(ErrorCode.MISSING_ARGUMENT, 'Task ID is required')
|
|
60
|
-
}
|
|
61
|
-
return success(getTask(db, args.id))
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function taskUpdate(
|
|
65
|
-
db: Database,
|
|
66
|
-
args: {
|
|
67
|
-
id?: string
|
|
68
|
-
title?: string
|
|
69
|
-
description?: string
|
|
70
|
-
priority?: string
|
|
71
|
-
assignee?: string
|
|
72
|
-
project?: string
|
|
73
|
-
metadata?: string
|
|
74
|
-
},
|
|
75
|
-
): CliOutput {
|
|
76
|
-
if (!args.id) {
|
|
77
|
-
throw new KanbanError(ErrorCode.MISSING_ARGUMENT, 'Task ID is required')
|
|
78
|
-
}
|
|
79
|
-
return success(
|
|
80
|
-
updateTask(db, args.id, {
|
|
81
|
-
title: args.title,
|
|
82
|
-
description: args.description,
|
|
83
|
-
priority: args.priority as Priority | undefined,
|
|
84
|
-
assignee: args.assignee,
|
|
85
|
-
project: args.project,
|
|
86
|
-
metadata: args.metadata,
|
|
87
|
-
}),
|
|
88
|
-
)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function taskDelete(db: Database, args: { id?: string }): CliOutput {
|
|
92
|
-
if (!args.id) {
|
|
93
|
-
throw new KanbanError(ErrorCode.MISSING_ARGUMENT, 'Task ID is required')
|
|
94
|
-
}
|
|
95
|
-
return success(deleteTask(db, args.id))
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function taskMove(db: Database, args: { id?: string; column?: string }): CliOutput {
|
|
99
|
-
if (!args.id || !args.column) {
|
|
100
|
-
throw new KanbanError(ErrorCode.MISSING_ARGUMENT, 'Usage: kanban task move <id> <column>')
|
|
101
|
-
}
|
|
102
|
-
return success(moveTask(db, args.id, args.column))
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function taskAssign(db: Database, args: { id?: string; assignee?: string }): CliOutput {
|
|
106
|
-
if (!args.id || !args.assignee) {
|
|
107
|
-
throw new KanbanError(ErrorCode.MISSING_ARGUMENT, 'Usage: kanban task assign <id> <assignee>')
|
|
108
|
-
}
|
|
109
|
-
return success(updateTask(db, args.id, { assignee: args.assignee }))
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function taskPrioritize(db: Database, args: { id?: string; priority?: string }): CliOutput {
|
|
113
|
-
if (!args.id || !args.priority) {
|
|
114
|
-
throw new KanbanError(ErrorCode.MISSING_ARGUMENT, 'Usage: kanban task prioritize <id> <level>')
|
|
115
|
-
}
|
|
116
|
-
return success(updateTask(db, args.id, { priority: args.priority as Priority }))
|
|
117
|
-
}
|
package/src/fixtures.ts
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import type { Database } from 'bun:sqlite'
|
|
2
|
-
import type { Priority } from './types.ts'
|
|
3
|
-
import { addTask, moveTask, updateTask, listTasks } from './db.ts'
|
|
4
|
-
|
|
5
|
-
interface FixtureTask {
|
|
6
|
-
title: string
|
|
7
|
-
description?: string
|
|
8
|
-
column: string
|
|
9
|
-
priority: Priority
|
|
10
|
-
assignee?: string
|
|
11
|
-
project?: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const FIXTURE_TASKS: FixtureTask[] = [
|
|
15
|
-
// recurring
|
|
16
|
-
{
|
|
17
|
-
title: 'Daily standup notes',
|
|
18
|
-
description: 'Capture blockers and progress each morning',
|
|
19
|
-
column: 'recurring',
|
|
20
|
-
priority: 'medium',
|
|
21
|
-
assignee: 'Alex',
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
title: 'Weekly metrics review',
|
|
25
|
-
description: 'Review board throughput and cycle time every Friday',
|
|
26
|
-
column: 'recurring',
|
|
27
|
-
priority: 'low',
|
|
28
|
-
assignee: 'BuildBot',
|
|
29
|
-
project: 'Platform',
|
|
30
|
-
},
|
|
31
|
-
|
|
32
|
-
// backlog
|
|
33
|
-
{
|
|
34
|
-
title: 'Add search functionality',
|
|
35
|
-
description: 'Full-text search across task titles and descriptions',
|
|
36
|
-
column: 'backlog',
|
|
37
|
-
priority: 'high',
|
|
38
|
-
assignee: 'Alex',
|
|
39
|
-
project: 'Platform',
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
title: 'Write API docs',
|
|
43
|
-
description: 'Document all CLI commands with examples',
|
|
44
|
-
column: 'backlog',
|
|
45
|
-
priority: 'medium',
|
|
46
|
-
assignee: 'BuildBot',
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
title: 'Refactor error handling',
|
|
50
|
-
description: 'Consolidate error codes and improve user-facing messages',
|
|
51
|
-
column: 'backlog',
|
|
52
|
-
priority: 'low',
|
|
53
|
-
project: 'Platform',
|
|
54
|
-
},
|
|
55
|
-
|
|
56
|
-
// in-progress
|
|
57
|
-
{
|
|
58
|
-
title: 'Implement board export',
|
|
59
|
-
description: 'Export board state to JSON and CSV formats',
|
|
60
|
-
column: 'in-progress',
|
|
61
|
-
priority: 'high',
|
|
62
|
-
assignee: 'Alex',
|
|
63
|
-
project: 'Platform',
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
title: 'Fix column reorder bug',
|
|
67
|
-
description: 'Columns shift incorrectly when moving to position 0',
|
|
68
|
-
column: 'in-progress',
|
|
69
|
-
priority: 'high',
|
|
70
|
-
assignee: 'BuildBot',
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
// review
|
|
74
|
-
{
|
|
75
|
-
title: 'Add bulk delete command',
|
|
76
|
-
description: 'Allow deleting multiple tasks by ID or filter',
|
|
77
|
-
column: 'review',
|
|
78
|
-
priority: 'medium',
|
|
79
|
-
assignee: 'Alex',
|
|
80
|
-
project: 'Platform',
|
|
81
|
-
},
|
|
82
|
-
|
|
83
|
-
// done
|
|
84
|
-
{
|
|
85
|
-
title: 'Set up CI pipeline',
|
|
86
|
-
description: 'GitHub Actions for lint, typecheck, and test on every PR',
|
|
87
|
-
column: 'done',
|
|
88
|
-
priority: 'high',
|
|
89
|
-
assignee: 'BuildBot',
|
|
90
|
-
project: 'Platform',
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
title: 'Add activity logging',
|
|
94
|
-
description: 'Track task creates, moves, updates, and deletes',
|
|
95
|
-
column: 'done',
|
|
96
|
-
priority: 'medium',
|
|
97
|
-
assignee: 'Alex',
|
|
98
|
-
},
|
|
99
|
-
]
|
|
100
|
-
|
|
101
|
-
export function seedFixtures(db: Database): { taskCount: number; movedCount: number } {
|
|
102
|
-
let movedCount = 0
|
|
103
|
-
|
|
104
|
-
for (const fixture of FIXTURE_TASKS) {
|
|
105
|
-
// Create all tasks in backlog first (addTask defaults to backlog)
|
|
106
|
-
const task = addTask(db, fixture.title, {
|
|
107
|
-
description: fixture.description,
|
|
108
|
-
priority: fixture.priority,
|
|
109
|
-
assignee: fixture.assignee,
|
|
110
|
-
project: fixture.project,
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
// Move to target column if not already in backlog
|
|
114
|
-
if (fixture.column !== 'backlog') {
|
|
115
|
-
moveTask(db, task.id, fixture.column)
|
|
116
|
-
movedCount++
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Escalate the bug to urgent — generates a realistic "prioritized" activity entry
|
|
121
|
-
const inProgress = listTasks(db, { column: 'in-progress' })
|
|
122
|
-
const bug = inProgress.find((t) => t.title === 'Fix column reorder bug')
|
|
123
|
-
if (bug) {
|
|
124
|
-
updateTask(db, bug.id, { priority: 'urgent' })
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return { taskCount: FIXTURE_TASKS.length, movedCount }
|
|
128
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}:root{--black: #000000;--surface: #111111;--surface-raised: #1a1a1a;--border: #222222;--border-visible: #333333;--text-disabled: #666666;--text-secondary: #999999;--text-muted: var(--text-disabled);--text-primary: #e8e8e8;--text-display: #ffffff;--accent: #d71921;--accent-subtle: rgba(215, 25, 33, .15);--success: #4a9e5c;--warning: #d4a843;--interactive: #5b9bf6;--priority-urgent: #d71921;--priority-high: #d4a843;--priority-medium: #999999;--priority-low: #4a9e5c;--space-xs: 4px;--space-sm: 8px;--space-md: 16px;--space-lg: 24px;--space-xl: 32px;--space-2xl: 48px;--section-radius: 12px;--page-gutter: clamp(16px, 3vw, 32px);--safe-top: env(safe-area-inset-top, 0px);--safe-right: env(safe-area-inset-right, 0px);--safe-bottom: env(safe-area-inset-bottom, 0px);--safe-left: env(safe-area-inset-left, 0px)}html{background:var(--black)}body{font-family:Space Grotesk,system-ui,sans-serif;background:var(--black);color:var(--text-primary);min-height:100vh;overflow-x:hidden;font-weight:400;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}button,select,input,textarea{font-family:inherit}#root{width:100%;max-width:1800px;margin:0 auto;padding-top:calc(var(--space-lg) + var(--safe-top));padding-right:max(var(--page-gutter),var(--safe-right));padding-bottom:calc(var(--space-lg) + var(--safe-bottom));padding-left:max(var(--page-gutter),var(--safe-left))}.header{padding:0 0 var(--space-xl);border-bottom:1px solid var(--border);margin-bottom:var(--space-lg)}.headerTop{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--space-md);margin-bottom:var(--space-lg)}.headerIdentity{display:flex;flex-direction:column;gap:var(--space-sm);min-width:0}.headerTitleRow{display:flex;align-items:center;flex-wrap:wrap;gap:var(--space-sm)}.header h1{font-family:Space Grotesk,sans-serif;font-size:24px;font-weight:500;letter-spacing:-.02em;line-height:1.1;color:var(--text-display)}.header h1 span{color:var(--text-display)}.liveStatus,.providerBadge{display:inline-flex;align-items:center;gap:6px;min-height:28px;padding:4px 12px;border-radius:999px;border:1px solid var(--border-visible);background:transparent;color:var(--text-secondary);font-family:Space Mono,monospace;font-size:11px;font-weight:400;letter-spacing:.04em;text-transform:uppercase}.providerBadge{max-width:fit-content}.newTaskBtn{min-height:44px;background:var(--text-display);color:var(--black);border:none;padding:10px 24px;border-radius:999px;font-family:Space Mono,monospace;font-size:13px;font-weight:400;letter-spacing:.06em;text-transform:uppercase;cursor:pointer;transition:opacity .15s;white-space:nowrap}.newTaskBtn:hover{opacity:.85}.statsBar{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--space-sm);margin-bottom:var(--space-lg)}.statCard{background:var(--surface);border:1px solid var(--border);border-radius:var(--section-radius);padding:var(--space-md);display:flex;flex-direction:column;gap:var(--space-xs);min-width:0}.statValue{font-family:Doto,Space Mono,monospace;font-size:36px;line-height:1;font-weight:400;color:var(--text-display);letter-spacing:-.02em}.statLabel{font-family:Space Mono,monospace;font-size:11px;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.08em}.filterBar{display:grid;grid-template-columns:minmax(0,1fr) minmax(180px,240px);gap:var(--space-md);align-items:end}.filterSection{display:flex;flex-direction:column;gap:var(--space-sm);min-width:0}.filterSectionCompact{justify-self:end;width:100%}.filterLabel{font-family:Space Mono,monospace;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--text-disabled)}.filterGroup{display:flex;gap:6px;align-items:center;min-width:0}.filterScroller{display:flex;flex-wrap:wrap;gap:6px}.filterBtn{min-height:36px;background:transparent;border:1px solid var(--border-visible);color:var(--text-secondary);padding:6px 16px;border-radius:999px;font-family:Space Mono,monospace;font-size:12px;letter-spacing:.04em;text-transform:uppercase;cursor:pointer;transition:all .15s;white-space:nowrap}.filterBtn:hover{border-color:var(--text-secondary);color:var(--text-primary)}.filterBtn.active{background:var(--text-display);border-color:var(--text-display);color:var(--black)}.projectDropdown{width:100%;min-height:36px;background:transparent;border:1px solid var(--border-visible);color:var(--text-primary);padding:6px 34px 6px 14px;border-radius:999px;font-family:Space Mono,monospace;font-size:12px;letter-spacing:.04em;cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-image:url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%23666666' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center}.projectDropdown:focus{outline:none;border-color:var(--text-primary)}.boardShell{min-height:calc(100vh - 280px - var(--safe-bottom))}.board{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(280px,1fr);gap:var(--space-sm);overflow-x:auto;overflow-y:visible;padding:var(--space-xs) 0 calc(var(--space-sm) + var(--safe-bottom));min-height:calc(100vh - 300px - var(--safe-bottom));scrollbar-width:thin;scrollbar-color:var(--border-visible) transparent;scroll-padding-inline:2px}.board:after{content:"";width:2px}.column{min-width:0;background:var(--surface);border:1px solid var(--border);border-radius:var(--section-radius);display:flex;flex-direction:column;min-height:100%}.columnHeader{padding:14px var(--space-md);border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;gap:var(--space-sm)}.columnHeaderMain,.columnHeaderActions{display:flex;align-items:center;gap:var(--space-sm);min-width:0}.columnDot{width:6px;height:6px;border-radius:50%;flex-shrink:0;opacity:.7}.columnName{font-family:Space Mono,monospace;font-size:11px;font-weight:400;text-transform:uppercase;letter-spacing:.08em;color:var(--text-secondary);flex:1;min-width:0}.columnCount{background:transparent;border:1px solid var(--border);color:var(--text-disabled);padding:2px 8px;border-radius:999px;font-family:Space Mono,monospace;font-size:11px;min-width:24px;text-align:center}.columnAddBtn{min-width:32px;min-height:32px;background:transparent;border:1px solid var(--border);border-radius:999px;color:var(--text-disabled);cursor:pointer;font-size:16px;line-height:1;transition:all .15s}.columnAddBtn:hover{color:var(--text-primary);border-color:var(--text-secondary)}.columnBody{padding:var(--space-sm);display:flex;flex-direction:column;gap:6px;min-height:60px;flex:1}.emptyColumn{display:flex;align-items:center;justify-content:center;color:var(--text-disabled);font-family:Space Mono,monospace;font-size:11px;letter-spacing:.04em;text-transform:uppercase;flex:1;min-height:96px}.taskCard{width:100%;text-align:left;background:var(--surface-raised);border:1px solid var(--border);border-radius:8px;padding:14px var(--space-md);cursor:pointer;transition:border-color .15s}.taskCard:hover{border-color:var(--border-visible)}.taskCard.selected{border-color:var(--text-secondary)}.taskCardHeader{display:flex;align-items:flex-start;gap:var(--space-sm);margin-bottom:var(--space-xs)}.taskTitle{font-family:Space Grotesk,sans-serif;font-size:14px;font-weight:500;line-height:1.4;color:var(--text-primary);flex:1}.taskDescription{font-size:13px;color:var(--text-disabled);line-height:1.45;margin-bottom:var(--space-sm);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.taskFooter{display:flex;align-items:center;justify-content:space-between;gap:var(--space-sm)}.taskFooterLeft{display:flex;align-items:center;gap:var(--space-sm);min-width:0;flex-wrap:wrap}.assigneeAvatar{width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-family:Space Mono,monospace;font-size:10px;font-weight:700;color:var(--black);background:var(--text-disabled);flex-shrink:0;border:1px solid var(--border-visible)}.assigneeName{font-family:Space Mono,monospace;font-size:11px;color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.projectTag{display:inline-flex;align-items:center;min-height:22px;font-family:Space Mono,monospace;font-size:10px;letter-spacing:.06em;text-transform:uppercase;padding:2px 10px;border-radius:999px;border:1px solid var(--border-visible);background:transparent;color:var(--text-secondary);white-space:nowrap}.timestamp{font-family:Space Mono,monospace;font-size:11px;color:var(--text-disabled);white-space:nowrap;flex-shrink:0}.priorityDot{width:6px;height:6px;border-radius:50%;flex-shrink:0;margin-top:5px}.priorityDot.urgent{background:var(--priority-urgent)}.priorityDot.high{background:var(--priority-high)}.priorityDot.medium{background:var(--priority-medium)}.priorityDot.low{background:var(--priority-low)}.taskDetailOverlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#000000b3;z-index:9}.taskDetail{position:fixed;top:0;right:0;width:min(420px,100vw);height:100vh;background:var(--surface);border-left:1px solid var(--border-visible);padding:calc(var(--space-xl) + var(--safe-top)) max(var(--space-lg),var(--safe-right)) calc(var(--space-xl) + var(--safe-bottom)) var(--space-lg);overflow-y:auto;z-index:10}.taskDetail .closeBtn{position:absolute;top:calc(var(--space-md) + var(--safe-top));right:max(var(--space-md),var(--safe-right));min-width:40px;min-height:40px;background:var(--surface-raised);border:1px solid var(--border-visible);color:var(--text-secondary);cursor:pointer;padding:4px 10px;border-radius:999px;font-size:18px;transition:all .15s}.taskDetail .closeBtn:hover{border-color:var(--text-secondary);color:var(--text-primary)}.detailTitle{font-family:Space Grotesk,sans-serif;font-size:18px;font-weight:500;color:var(--text-display);margin-bottom:var(--space-lg);padding-right:56px;line-height:1.4}.detailField{margin-bottom:var(--space-md)}.detailLabel{font-family:Space Mono,monospace;font-size:11px;color:var(--text-disabled);text-transform:uppercase;letter-spacing:.08em;margin-bottom:var(--space-xs)}.detailValue{font-family:Space Grotesk,sans-serif;font-size:14px;line-height:1.5;color:var(--text-primary)}.detailActions{display:flex;gap:var(--space-sm);margin-top:var(--space-xl);padding-top:var(--space-md);border-top:1px solid var(--border)}.detailSelect{background:var(--surface-raised);border:1px solid var(--border-visible);color:var(--text-primary);min-height:42px;padding:8px 14px;border-radius:8px;font-family:Space Grotesk,sans-serif;font-size:13px;flex:1}.detailSelect:focus{outline:none;border-color:var(--text-secondary)}.deleteBtn{background:transparent;border:1px solid var(--accent);color:var(--accent);min-height:42px;padding:8px 20px;border-radius:999px;font-family:Space Mono,monospace;font-size:12px;letter-spacing:.04em;text-transform:uppercase;cursor:pointer;transition:all .15s}.deleteBtn:hover{background:var(--accent-subtle)}.modalOverlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#000c;display:flex;align-items:center;justify-content:center;padding:calc(var(--space-md) + var(--safe-top)) max(var(--space-md),var(--safe-right)) calc(var(--space-md) + var(--safe-bottom)) max(var(--space-md),var(--safe-left));z-index:20}.modal{background:var(--surface);border:1px solid var(--border-visible);border-radius:16px;padding:var(--space-lg);width:480px;max-width:100%;max-height:100%;overflow-y:auto}.modal h2{font-family:Space Grotesk,sans-serif;font-size:18px;font-weight:500;color:var(--text-display);margin-bottom:var(--space-lg)}.formField{margin-bottom:var(--space-md)}.formLabel{display:block;font-family:Space Mono,monospace;font-size:11px;color:var(--text-disabled);text-transform:uppercase;letter-spacing:.08em;margin-bottom:6px}.formInput{width:100%;background:var(--surface-raised);border:1px solid var(--border-visible);color:var(--text-primary);min-height:42px;padding:10px 14px;border-radius:8px;font-family:Space Grotesk,sans-serif;font-size:14px}.formInput::placeholder{color:var(--text-disabled)}.formInput:focus{outline:none;border-color:var(--text-secondary)}textarea.formInput{resize:vertical;min-height:96px}select.formInput{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding-right:28px;background-image:url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%23666666' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center}.formRow{display:grid;grid-template-columns:1fr 1fr;gap:var(--space-sm)}.modalActions{display:flex;justify-content:flex-end;gap:var(--space-sm);margin-top:var(--space-lg)}.btnSecondary{background:transparent;border:1px solid var(--border-visible);color:var(--text-secondary);min-height:42px;padding:8px 24px;border-radius:999px;font-family:Space Mono,monospace;font-size:12px;letter-spacing:.04em;text-transform:uppercase;cursor:pointer;transition:all .15s}.btnSecondary:hover{border-color:var(--text-secondary);color:var(--text-primary)}.btnPrimary{background:var(--text-display);border:none;color:var(--black);min-height:42px;padding:8px 24px;border-radius:999px;font-family:Space Mono,monospace;font-size:12px;font-weight:400;letter-spacing:.04em;text-transform:uppercase;cursor:pointer;transition:opacity .15s}.btnPrimary:hover{opacity:.85}.btnPrimary:disabled{opacity:.3;cursor:not-allowed}.appLayout{display:grid;grid-template-columns:minmax(0,1fr);gap:0;min-width:0}.loading{display:flex;align-items:center;justify-content:center;padding:var(--space-2xl);color:var(--text-disabled);font-family:Space Mono,monospace;font-size:12px;letter-spacing:.04em;text-transform:uppercase}.errorBanner{background:var(--accent-subtle);border:1px solid var(--accent);color:var(--accent);padding:10px var(--space-md);border-radius:8px;font-family:Space Mono,monospace;font-size:12px;margin-bottom:var(--space-md)}.wsIndicator{width:6px;height:6px;border-radius:50%;display:inline-block;flex-shrink:0}.wsIndicator.connected{background:var(--success)}.wsIndicator.disconnected{background:var(--text-disabled)}.mobileList{display:none}.mobileGroup{border-bottom:1px solid var(--border)}.mobileGroupHeader{display:flex;align-items:center;gap:var(--space-sm);min-height:48px;padding:0 var(--space-md);background:var(--surface)}.mobileGroupToggle{display:flex;align-items:center;gap:var(--space-sm);flex:1;min-height:48px;padding:12px 0;background:transparent;border:none;color:var(--text-primary);font:inherit;cursor:pointer;text-align:left}.mobileGroupToggle:hover,.mobileGroupToggle:focus-visible{color:var(--text-display)}.mobileGroupChevron{font-size:12px;color:var(--text-disabled);transition:transform .15s;width:16px;text-align:center;flex-shrink:0}.mobileGroupChevron.collapsed{transform:rotate(-90deg)}.mobileGroupDot{width:6px;height:6px;border-radius:50%;flex-shrink:0;opacity:.6}.mobileGroupName{font-family:Space Mono,monospace;font-size:11px;font-weight:400;text-transform:uppercase;letter-spacing:.08em;color:var(--text-secondary)}.mobileGroupCount{font-family:Space Mono,monospace;font-size:11px;color:var(--text-disabled);margin-right:auto}.mobileGroupAdd{width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;padding:0;background:transparent;border:none;border-radius:999px;font-size:16px;color:var(--text-disabled);cursor:pointer;flex-shrink:0;transition:color .15s}.mobileGroupAdd:hover{color:var(--text-primary)}.mobileGroupBody{padding-bottom:var(--space-xs)}.mobileGroupEmpty{padding:var(--space-md) var(--space-md) var(--space-md) 46px;font-family:Space Mono,monospace;font-size:11px;color:var(--text-disabled);letter-spacing:.04em;text-transform:uppercase}.mobileTaskRow{display:flex;align-items:center;gap:10px;width:100%;min-height:48px;padding:12px var(--space-md);background:none;border:none;border-top:1px solid var(--border);color:var(--text-primary);font:inherit;cursor:pointer;text-align:left;transition:background .1s}.mobileTaskRow:hover,.mobileTaskRow:focus-visible{background:var(--surface)}.mobileTaskRow.selected{background:var(--surface);border-left:2px solid var(--text-display)}.mobileTaskRow .priorityDot{flex-shrink:0}.mobileTaskTitle{flex:1;min-width:0;font-family:Space Grotesk,sans-serif;font-size:14px;font-weight:400;line-height:1.35;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.mobileTaskProject{font-family:Space Mono,monospace;font-size:10px;letter-spacing:.06em;text-transform:uppercase;color:var(--text-disabled);border:1px solid var(--border-visible);border-radius:999px;padding:2px 10px;white-space:nowrap;flex-shrink:0}.mobileTaskAvatar{width:22px;height:22px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-family:Space Mono,monospace;font-size:10px;font-weight:700;color:var(--black);background:var(--text-disabled);flex-shrink:0;border:1px solid var(--border-visible)}@media(max-width:960px){.statsBar{grid-template-columns:repeat(2,minmax(0,1fr))}.filterBar{grid-template-columns:1fr}.filterSectionCompact{justify-self:stretch}}@media(max-width:720px){:root{--page-gutter: 20px}#root{padding-top:max(var(--space-sm),var(--safe-top));padding-bottom:calc(var(--space-lg) + var(--safe-bottom));overflow-x:hidden}.header{padding:0;border-bottom:none;margin-bottom:var(--space-md)}.headerTop{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:start;gap:var(--space-sm);margin-bottom:var(--space-md)}.headerIdentity{gap:6px}.headerTitleRow{align-items:center;gap:6px}.header h1{font-size:18px}.liveStatus,.providerBadge{min-height:24px;padding:3px 10px;font-size:10px}.newTaskBtn{min-width:44px;min-height:44px;padding:0 16px;align-self:start}.statsBar{display:grid;grid-template-columns:repeat(2,1fr);gap:6px;margin-bottom:var(--space-md)}.statCard{padding:12px;gap:var(--space-xs)}.statValue{font-size:28px}.statLabel{font-size:10px}.filterBar{display:flex;flex-direction:column;gap:var(--space-sm)}.filterSection,.filterSectionCompact{width:100%}.filterSection{gap:6px}.filterLabel{font-size:10px}.filterGroup{gap:6px}.filterBtn{min-height:34px;padding:6px 12px;font-size:11px}.projectDropdown{min-height:34px;font-size:11px}.boardShell{display:none}.mobileList{display:block;border-top:1px solid var(--border)}.taskDetailOverlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:9}.taskDetail{position:fixed;top:0;right:0;bottom:0;left:0;width:100vw;height:100vh;height:100dvh;border-left:none;padding:calc(var(--space-lg) + var(--safe-top)) max(var(--space-md),var(--safe-right)) calc(var(--space-lg) + var(--safe-bottom)) var(--space-md);z-index:10}.detailTitle{font-size:17px;margin-bottom:var(--space-md)}.detailActions{flex-direction:column}.deleteBtn,.detailSelect{width:100%}.modalOverlay{position:fixed;top:0;right:0;bottom:0;left:0;align-items:flex-end;padding:max(12px,var(--safe-top)) max(12px,var(--safe-right)) max(12px,var(--safe-bottom)) max(12px,var(--safe-left));z-index:20}.modal{width:100%;max-height:85vh;max-height:85dvh;overflow-y:auto;border-radius:16px;padding:var(--space-lg)}.formRow{grid-template-columns:1fr;gap:0}.modalActions{flex-direction:column-reverse}.modalActions .btnPrimary,.modalActions .btnSecondary{width:100%}}@media(max-width:520px){.mobileTaskProject{display:none}.headerTop{grid-template-columns:minmax(0,1fr)}.newTaskBtn{width:100%;text-align:center}}
|