@soleone/pi-tasks 0.2.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/CONTRIBUTING.md +95 -0
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/package.json +46 -0
- package/src/backend/adapters/beads.ts +305 -0
- package/src/backend/adapters/todo-md.ts +446 -0
- package/src/backend/api.ts +40 -0
- package/src/backend/resolver.ts +86 -0
- package/src/controllers/list.ts +208 -0
- package/src/controllers/show.ts +81 -0
- package/src/extension.ts +358 -0
- package/src/index.ts +7 -0
- package/src/lib/task-serialization.ts +45 -0
- package/src/models/list-item.ts +44 -0
- package/src/models/task.ts +117 -0
- package/src/ui/components/blur-editor.ts +78 -0
- package/src/ui/components/keyboard-help.ts +25 -0
- package/src/ui/components/min-height.ts +21 -0
- package/src/ui/pages/list.ts +540 -0
- package/src/ui/pages/show.ts +425 -0
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## UI
|
|
4
|
+
|
|
5
|
+
### Color hierarchy (Beads pages)
|
|
6
|
+
|
|
7
|
+
Use these text colors consistently, from strongest to weakest emphasis:
|
|
8
|
+
|
|
9
|
+
1. `accent`
|
|
10
|
+
2. `warning` (only for attention/error states)
|
|
11
|
+
3. default/plain text
|
|
12
|
+
4. `muted`
|
|
13
|
+
5. `dim`
|
|
14
|
+
|
|
15
|
+
### Color inventory by page
|
|
16
|
+
|
|
17
|
+
#### List page
|
|
18
|
+
|
|
19
|
+
- `accent`
|
|
20
|
+
- selected row prefix/text
|
|
21
|
+
- selected preview title
|
|
22
|
+
- `muted`
|
|
23
|
+
- page title (`Tasks`)
|
|
24
|
+
- global top/bottom borders
|
|
25
|
+
- row metadata (status/type)
|
|
26
|
+
- `dim`
|
|
27
|
+
- keyboard helper text (primary + secondary)
|
|
28
|
+
- footer divider
|
|
29
|
+
- select-list scroll info
|
|
30
|
+
- `warning`
|
|
31
|
+
- no-match/search warning text
|
|
32
|
+
- default/plain
|
|
33
|
+
- non-selected row title and summary body
|
|
34
|
+
|
|
35
|
+
#### Show page
|
|
36
|
+
|
|
37
|
+
- `accent`
|
|
38
|
+
- focused field labels (`Title`, `Description`)
|
|
39
|
+
- editing status in title area
|
|
40
|
+
- save success status (`✓ Saved`)
|
|
41
|
+
- selected-task arrow prefix
|
|
42
|
+
- focused field border color
|
|
43
|
+
- `muted`
|
|
44
|
+
- page title base (`Tasks`)
|
|
45
|
+
- unfocused field labels
|
|
46
|
+
- blurred field borders
|
|
47
|
+
- `dim`
|
|
48
|
+
- keyboard helper lines
|
|
49
|
+
- global top/bottom borders
|
|
50
|
+
- footer divider
|
|
51
|
+
- `warning`
|
|
52
|
+
- save failure status
|
|
53
|
+
- default/plain
|
|
54
|
+
- form field content text
|
|
55
|
+
|
|
56
|
+
### UX rules for list/show flow
|
|
57
|
+
|
|
58
|
+
- Keep list/show visual hierarchy aligned.
|
|
59
|
+
- Keep keyboard helper spacing/padding symmetric across pages.
|
|
60
|
+
- Keep selected task identity formatting shared via view-model helpers (avoid duplicate formatting logic).
|
|
61
|
+
- Prefer extracting reusable rendering/formatting primitives over repeating inline style logic.
|
|
62
|
+
|
|
63
|
+
## Task adapter quick guide
|
|
64
|
+
|
|
65
|
+
To add a backend adapter, create one file in `src/backend/adapters/` with a **default export** that satisfies `TaskAdapterInitializer`.
|
|
66
|
+
|
|
67
|
+
Required shape:
|
|
68
|
+
|
|
69
|
+
- `id: string`
|
|
70
|
+
- `isApplicable(): boolean` (detect if adapter should be used in current workspace)
|
|
71
|
+
- `initialize(pi)` returning a `TaskAdapter` with:
|
|
72
|
+
- `statusMap` (internal camelCase status -> backend status)
|
|
73
|
+
- `taskTypes` (toggle order, first is default)
|
|
74
|
+
- `priorities` (highest-first order, middle is default)
|
|
75
|
+
- optional `priorityHotkeys` (`key -> priority`, overrides default `1..N` rank hotkeys)
|
|
76
|
+
- `list`, `show`, `update`, `create`
|
|
77
|
+
|
|
78
|
+
Resolution behavior:
|
|
79
|
+
|
|
80
|
+
1. `PI_TASKS_BACKEND` selects adapter by `id`
|
|
81
|
+
2. otherwise the first adapter with `isApplicable() === true` is used
|
|
82
|
+
3. if none apply, first loaded adapter is used as fallback
|
|
83
|
+
|
|
84
|
+
Keep backend-specific field mapping inside adapter files only (e.g. beads `issue_type`, `in_progress`, `created_at`, `due_at`), and keep app-level types in task-oriented camelCase (`taskType`, `inProgress`, `createdAt`, `dueAt`).
|
|
85
|
+
|
|
86
|
+
## Implementation details
|
|
87
|
+
|
|
88
|
+
- stable list layout with aligned task identity/meta and fixed-height description preview
|
|
89
|
+
- unified task form architecture for both Edit and Create pages
|
|
90
|
+
- create flow that keeps editing context after initial save and updates the same task on subsequent saves
|
|
91
|
+
- keyboard-first interaction model with intent-based shortcuts and consistent list/show navigation behavior
|
|
92
|
+
- mvc-like architecture: control flow designed for concise, maintainable extension code
|
|
93
|
+
- inline header save feedback states with optional status icons
|
|
94
|
+
- shared view-model formatting primitives to keep list/show/create rendering in sync
|
|
95
|
+
- task serialization and work handoff prompt generation
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Soleone
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# @soleone/pi-tasks
|
|
2
|
+
|
|
3
|
+
Task management extension for the [pi coding agent](https://github.com/badlogic/pi-mono), designed for pluggable task backends.
|
|
4
|
+
|
|
5
|
+
<img width="2373" height="1305" alt="image" src="https://github.com/user-attachments/assets/af210b63-f993-447d-9668-3308874d493c" />
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
1. Installation: `pi install npm:@soleone/pi-tasks`
|
|
10
|
+
2. Toggle the Tasks UI with `ctrl + x`, or use `/tasks`.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
- Navigate up with `w` and `s` (arrows also work)
|
|
15
|
+
- `space` to change status
|
|
16
|
+
- `0` to `4` to change priority
|
|
17
|
+
- `t` to change task type
|
|
18
|
+
- `f` for keyword search (title, description)
|
|
19
|
+
- `q` or `Esc` to go back
|
|
20
|
+
|
|
21
|
+
### List view
|
|
22
|
+
|
|
23
|
+
- `e` to edit a task
|
|
24
|
+
- `Enter` to work off a task
|
|
25
|
+
- `Tab` to insert task details in prompt and close Tasks UI
|
|
26
|
+
- `c` to create a new task
|
|
27
|
+
|
|
28
|
+
### Edit view
|
|
29
|
+
|
|
30
|
+
- `Tab` to switch focus between inputs
|
|
31
|
+
- `Enter` to save
|
|
32
|
+
|
|
33
|
+
## Backend selection
|
|
34
|
+
|
|
35
|
+
By default, the extension auto-detects the first applicable backend. If none are applicable, it falls back to `todo-md`.
|
|
36
|
+
|
|
37
|
+
Set `PI_TASKS_BACKEND` to explicitly choose a backend implementation.
|
|
38
|
+
Currently supported values:
|
|
39
|
+
|
|
40
|
+
- `beads`
|
|
41
|
+
- `todo-md`
|
|
42
|
+
|
|
43
|
+
### TODO.md backend
|
|
44
|
+
|
|
45
|
+
The `todo-md` backend reads/writes a markdown task file (default: `TODO.md`; if `todo.md` already exists, it is used).
|
|
46
|
+
|
|
47
|
+
Optional env var:
|
|
48
|
+
|
|
49
|
+
- `PI_TASKS_TODO_PATH` — override the TODO file path
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@soleone/pi-tasks",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Task management extension for the pi coding agent",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/soleone/pi-tasks.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/soleone/pi-tasks/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/soleone/pi-tasks#readme",
|
|
15
|
+
"pi": {
|
|
16
|
+
"extensions": [
|
|
17
|
+
"./src/index.ts"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
".": "./src/index.ts"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"src",
|
|
25
|
+
"README.md",
|
|
26
|
+
"CONTRIBUTING.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"keywords": [
|
|
30
|
+
"pi",
|
|
31
|
+
"tasks",
|
|
32
|
+
"task-management",
|
|
33
|
+
"tui"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"release": "release-it",
|
|
37
|
+
"release:dry": "release-it --dry-run"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@release-it/conventional-changelog": "^10.0.5",
|
|
44
|
+
"release-it": "^19.2.4"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"
|
|
2
|
+
import { spawnSync } from "node:child_process"
|
|
3
|
+
import { existsSync } from "node:fs"
|
|
4
|
+
import { resolve } from "node:path"
|
|
5
|
+
import type { Task, TaskStatus } from "../../models/task.ts"
|
|
6
|
+
import type { CreateTaskInput, TaskAdapter, TaskAdapterInitializer, TaskStatusMap, TaskUpdate } from "../api.ts"
|
|
7
|
+
|
|
8
|
+
const MAX_LIST_RESULTS = 100
|
|
9
|
+
const STATUS_MAP = {
|
|
10
|
+
open: "open",
|
|
11
|
+
inProgress: "in_progress",
|
|
12
|
+
closed: "closed",
|
|
13
|
+
} satisfies TaskStatusMap
|
|
14
|
+
const TASK_TYPES = ["task", "feature", "bug", "chore", "epic"]
|
|
15
|
+
const PRIORITIES = ["p0", "p1", "p2", "p3", "p4"]
|
|
16
|
+
const PRIORITY_HOTKEYS: Record<string, string> = {
|
|
17
|
+
"0": "p0",
|
|
18
|
+
"1": "p1",
|
|
19
|
+
"2": "p2",
|
|
20
|
+
"3": "p3",
|
|
21
|
+
"4": "p4",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const OPEN_TASK_LIST_ARGS = [
|
|
25
|
+
"list",
|
|
26
|
+
"--status", STATUS_MAP.open,
|
|
27
|
+
"--limit", String(MAX_LIST_RESULTS),
|
|
28
|
+
"--sort", "priority",
|
|
29
|
+
"--json",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
const IN_PROGRESS_TASK_LIST_ARGS = [
|
|
33
|
+
"list",
|
|
34
|
+
"--status", STATUS_MAP.inProgress,
|
|
35
|
+
"--limit", String(MAX_LIST_RESULTS),
|
|
36
|
+
"--sort", "priority",
|
|
37
|
+
"--json",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
interface BeadsIssue {
|
|
41
|
+
id: string
|
|
42
|
+
title: string
|
|
43
|
+
description?: string
|
|
44
|
+
status: string
|
|
45
|
+
priority?: number
|
|
46
|
+
issue_type?: string
|
|
47
|
+
owner?: string
|
|
48
|
+
created_at?: string
|
|
49
|
+
due_at?: string
|
|
50
|
+
due?: string
|
|
51
|
+
updated_at?: string
|
|
52
|
+
dependency_count?: number
|
|
53
|
+
dependent_count?: number
|
|
54
|
+
comment_count?: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function toPriorityLabel(value: number | undefined): string | undefined {
|
|
58
|
+
if (value === undefined) return undefined
|
|
59
|
+
const label = `p${value}`
|
|
60
|
+
return PRIORITIES.includes(label) ? label : undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function toPriorityValue(label: string | undefined): number | undefined {
|
|
64
|
+
if (!label) return undefined
|
|
65
|
+
const match = label.toLowerCase().match(/^p(\d)$/)
|
|
66
|
+
if (!match) return undefined
|
|
67
|
+
return Number(match[1])
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function toRequiredPriorityValue(label: string): number {
|
|
71
|
+
const value = toPriorityValue(label)
|
|
72
|
+
if (value === undefined) {
|
|
73
|
+
throw new Error(`Unsupported priority for beads backend: ${label}`)
|
|
74
|
+
}
|
|
75
|
+
return value
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function fromBackendStatus(status: string): TaskStatus {
|
|
79
|
+
for (const [internalStatus, backendStatus] of Object.entries(STATUS_MAP)) {
|
|
80
|
+
if (backendStatus === status) return internalStatus as TaskStatus
|
|
81
|
+
}
|
|
82
|
+
return "open"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function toBackendStatus(status: TaskStatus): string {
|
|
86
|
+
const mapped = STATUS_MAP[status]
|
|
87
|
+
if (!mapped) throw new Error(`Unsupported status for beads backend: ${status}`)
|
|
88
|
+
return mapped
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function toTask(beadsIssue: BeadsIssue): Task {
|
|
92
|
+
const task: Task = {
|
|
93
|
+
ref: beadsIssue.id,
|
|
94
|
+
id: beadsIssue.id,
|
|
95
|
+
title: beadsIssue.title,
|
|
96
|
+
description: beadsIssue.description ?? "",
|
|
97
|
+
status: fromBackendStatus(beadsIssue.status),
|
|
98
|
+
owner: beadsIssue.owner,
|
|
99
|
+
priority: toPriorityLabel(beadsIssue.priority),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (beadsIssue.issue_type !== undefined) task.taskType = beadsIssue.issue_type
|
|
103
|
+
if (beadsIssue.created_at !== undefined) task.createdAt = beadsIssue.created_at
|
|
104
|
+
if (beadsIssue.due_at !== undefined) task.dueAt = beadsIssue.due_at
|
|
105
|
+
if (beadsIssue.due !== undefined) task.dueAt = beadsIssue.due
|
|
106
|
+
if (beadsIssue.updated_at !== undefined) task.updatedAt = beadsIssue.updated_at
|
|
107
|
+
if (beadsIssue.dependency_count !== undefined) task.dependencyCount = beadsIssue.dependency_count
|
|
108
|
+
if (beadsIssue.dependent_count !== undefined) task.dependentCount = beadsIssue.dependent_count
|
|
109
|
+
if (beadsIssue.comment_count !== undefined) task.commentCount = beadsIssue.comment_count
|
|
110
|
+
|
|
111
|
+
return task
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function taskStatusSortRank(status: Task["status"]): number {
|
|
115
|
+
if (status === "inProgress") return 0
|
|
116
|
+
if (status === "open") return 1
|
|
117
|
+
return 2
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function taskPrioritySortRank(priority: string | undefined): number {
|
|
121
|
+
if (!priority) return PRIORITIES.length + 1
|
|
122
|
+
const index = PRIORITIES.indexOf(priority)
|
|
123
|
+
return index >= 0 ? index : PRIORITIES.length
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function sortActiveTasks(tasks: Task[]): Task[] {
|
|
127
|
+
return [...tasks].sort((left, right) => {
|
|
128
|
+
const statusOrder = taskStatusSortRank(left.status) - taskStatusSortRank(right.status)
|
|
129
|
+
if (statusOrder !== 0) return statusOrder
|
|
130
|
+
|
|
131
|
+
const priorityOrder = taskPrioritySortRank(left.priority) - taskPrioritySortRank(right.priority)
|
|
132
|
+
if (priorityOrder !== 0) return priorityOrder
|
|
133
|
+
|
|
134
|
+
return left.ref.localeCompare(right.ref)
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function fromTaskUpdateToBeadsArgs(update: TaskUpdate): string[] {
|
|
139
|
+
const args: string[] = []
|
|
140
|
+
|
|
141
|
+
if (update.title !== undefined) {
|
|
142
|
+
args.push("--title", update.title.trim())
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (update.description !== undefined) {
|
|
146
|
+
args.push("--description", update.description)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (update.status !== undefined) {
|
|
150
|
+
args.push("--status", toBackendStatus(update.status))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (update.priority !== undefined) {
|
|
154
|
+
args.push("--priority", String(toRequiredPriorityValue(update.priority)))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (update.taskType !== undefined) {
|
|
158
|
+
args.push("--type", update.taskType || TASK_TYPES[0])
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (update.dueAt !== undefined) {
|
|
162
|
+
args.push("--due", update.dueAt)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return args
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function parseJsonArray<T>(stdout: string, context: string): T[] {
|
|
169
|
+
try {
|
|
170
|
+
const parsed = JSON.parse(stdout)
|
|
171
|
+
if (!Array.isArray(parsed)) throw new Error("expected JSON array")
|
|
172
|
+
return parsed as T[]
|
|
173
|
+
} catch (e) {
|
|
174
|
+
const msg = e instanceof Error ? e.message : String(e)
|
|
175
|
+
throw new Error(`Failed to parse bd output (${context}): ${msg}`)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function parseJsonObject<T>(stdout: string, context: string): T {
|
|
180
|
+
try {
|
|
181
|
+
const parsed = JSON.parse(stdout)
|
|
182
|
+
if (parsed === null || Array.isArray(parsed) || typeof parsed !== "object") {
|
|
183
|
+
throw new Error("expected JSON object")
|
|
184
|
+
}
|
|
185
|
+
return parsed as T
|
|
186
|
+
} catch (e) {
|
|
187
|
+
const msg = e instanceof Error ? e.message : String(e)
|
|
188
|
+
throw new Error(`Failed to parse bd output (${context}): ${msg}`)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isApplicable(): boolean {
|
|
193
|
+
if (!existsSync(resolve(process.cwd(), ".beads"))) return false
|
|
194
|
+
|
|
195
|
+
const result = spawnSync("bd", ["--version"], {
|
|
196
|
+
stdio: "ignore",
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
return !result.error
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function initialize(pi: ExtensionAPI): TaskAdapter {
|
|
203
|
+
async function execBd(args: string[], timeout = 30_000): Promise<string> {
|
|
204
|
+
const result = await pi.exec("bd", args, { timeout })
|
|
205
|
+
if (result.code !== 0) {
|
|
206
|
+
const details = (result.stderr || result.stdout || "").trim()
|
|
207
|
+
throw new Error(details.length > 0 ? details : `bd ${args.join(" ")} failed (code ${result.code})`)
|
|
208
|
+
}
|
|
209
|
+
return result.stdout
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function update(ref: string, update: TaskUpdate): Promise<void> {
|
|
213
|
+
const args = fromTaskUpdateToBeadsArgs(update)
|
|
214
|
+
if (args.length === 0) return
|
|
215
|
+
|
|
216
|
+
await execBd(["update", ref, ...args])
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
id: "beads",
|
|
221
|
+
statusMap: STATUS_MAP,
|
|
222
|
+
taskTypes: TASK_TYPES,
|
|
223
|
+
priorities: PRIORITIES,
|
|
224
|
+
priorityHotkeys: PRIORITY_HOTKEYS,
|
|
225
|
+
|
|
226
|
+
async list(): Promise<Task[]> {
|
|
227
|
+
const [openOut, inProgressOut] = await Promise.all([
|
|
228
|
+
execBd(OPEN_TASK_LIST_ARGS),
|
|
229
|
+
execBd(IN_PROGRESS_TASK_LIST_ARGS),
|
|
230
|
+
])
|
|
231
|
+
|
|
232
|
+
const openIssues = parseJsonArray<BeadsIssue>(openOut, "list open")
|
|
233
|
+
const inProgressIssues = parseJsonArray<BeadsIssue>(inProgressOut, "list in_progress")
|
|
234
|
+
|
|
235
|
+
const dedupedById = new Map<string, Task>()
|
|
236
|
+
for (const issue of [...inProgressIssues, ...openIssues]) {
|
|
237
|
+
dedupedById.set(issue.id, toTask(issue))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return sortActiveTasks([...dedupedById.values()]).slice(0, MAX_LIST_RESULTS)
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
async show(ref: string): Promise<Task> {
|
|
244
|
+
const out = await execBd(["show", ref, "--json"])
|
|
245
|
+
const beadsIssues = parseJsonArray<BeadsIssue>(out, `show ${ref}`)
|
|
246
|
+
const task = beadsIssues[0]
|
|
247
|
+
if (!task) throw new Error(`Task not found: ${ref}`)
|
|
248
|
+
return toTask(task)
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
update,
|
|
252
|
+
|
|
253
|
+
async create(input: CreateTaskInput): Promise<Task> {
|
|
254
|
+
const title = input.title.trim()
|
|
255
|
+
const status = input.status ?? "open"
|
|
256
|
+
const selectedPriority = input.priority ?? PRIORITIES[Math.floor(PRIORITIES.length / 2)]
|
|
257
|
+
const createArgs = [
|
|
258
|
+
"create",
|
|
259
|
+
"--title", title,
|
|
260
|
+
"--priority", String(toRequiredPriorityValue(selectedPriority)),
|
|
261
|
+
"--type", input.taskType || TASK_TYPES[0],
|
|
262
|
+
"--json",
|
|
263
|
+
]
|
|
264
|
+
|
|
265
|
+
if (input.description && input.description.length > 0) {
|
|
266
|
+
createArgs.splice(3, 0, "--description", input.description)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (input.dueAt && input.dueAt.length > 0) {
|
|
270
|
+
createArgs.splice(3, 0, "--due", input.dueAt)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const out = await execBd(createArgs)
|
|
274
|
+
const created = toTask(parseJsonObject<BeadsIssue>(out, "create"))
|
|
275
|
+
|
|
276
|
+
if (status !== "open") {
|
|
277
|
+
await update(created.ref, { status })
|
|
278
|
+
created.status = status
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
created.title = title
|
|
282
|
+
created.description = input.description ?? ""
|
|
283
|
+
|
|
284
|
+
if (input.priority !== undefined) {
|
|
285
|
+
created.priority = input.priority
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (input.taskType !== undefined) {
|
|
289
|
+
created.taskType = input.taskType
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (input.dueAt !== undefined) {
|
|
293
|
+
created.dueAt = input.dueAt
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return created
|
|
297
|
+
},
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export default {
|
|
302
|
+
id: "beads",
|
|
303
|
+
isApplicable,
|
|
304
|
+
initialize,
|
|
305
|
+
} satisfies TaskAdapterInitializer
|