@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.
@@ -0,0 +1,208 @@
1
+ import { Key, matchesKey } from "@mariozechner/pi-tui"
2
+
3
+ export type ListIntent =
4
+ | { type: "cancel" }
5
+ | { type: "searchStart" }
6
+ | { type: "searchCancel" }
7
+ | { type: "searchApply" }
8
+ | { type: "searchBackspace" }
9
+ | { type: "searchAppend"; value: string }
10
+ | { type: "moveSelection"; delta: number }
11
+ | { type: "work" }
12
+ | { type: "edit" }
13
+ | { type: "toggleStatus" }
14
+ | { type: "setPriority"; priority: string }
15
+ | { type: "scrollDescription"; delta: number }
16
+ | { type: "toggleType" }
17
+ | { type: "create" }
18
+ | { type: "insert" }
19
+ | { type: "delegate" }
20
+
21
+ export interface ListControllerState {
22
+ searching: boolean
23
+ filtered: boolean
24
+ allowSearch: boolean
25
+ allowPriority: boolean
26
+ ctrlQ: string
27
+ priorities: string[]
28
+ priorityHotkeys?: Record<string, string>
29
+ }
30
+
31
+ type ShortcutContext = "default" | "search"
32
+
33
+ interface ShortcutDefinition {
34
+ context: ShortcutContext
35
+ help?: string | ((state: ListControllerState) => string)
36
+ showInHelp?: (state: ListControllerState) => boolean
37
+ match: (data: string, state: ListControllerState) => boolean
38
+ intent: (data: string, state: ListControllerState) => ListIntent
39
+ }
40
+
41
+ function parsePriorityKey(
42
+ data: string,
43
+ priorities: string[],
44
+ priorityHotkeys?: Record<string, string>,
45
+ ): string | null {
46
+ if (data.length !== 1) return null
47
+
48
+ const hotkeyPriority = priorityHotkeys?.[data]
49
+ if (hotkeyPriority && priorities.includes(hotkeyPriority)) return hotkeyPriority
50
+
51
+ const rank = parseInt(data, 10)
52
+ if (isNaN(rank) || rank < 1 || rank > priorities.length) return null
53
+ return priorities[rank - 1] ?? null
54
+ }
55
+
56
+ function buildPriorityHelpText(priorities: string[], priorityHotkeys?: Record<string, string>): string {
57
+ const hotkeyKeys = priorityHotkeys ? Object.keys(priorityHotkeys).sort((a, b) => a.localeCompare(b)) : []
58
+ if (hotkeyKeys.length > 0) {
59
+ return `${hotkeyKeys.join("/")} priority`
60
+ }
61
+
62
+ if (priorities.length === 0) return "priority"
63
+ if (priorities.length === 1) return "1 priority"
64
+ return `1-${priorities.length} priority`
65
+ }
66
+
67
+ function isPrintable(data: string): boolean {
68
+ return data.length === 1 && data.charCodeAt(0) >= 32 && data.charCodeAt(0) < 127
69
+ }
70
+
71
+ const MOVE_KEYS: Record<string, number> = {
72
+ w: -1,
73
+ W: -1,
74
+ s: 1,
75
+ S: 1,
76
+ }
77
+
78
+ const SCROLL_KEYS: Record<string, number> = {
79
+ j: 1,
80
+ k: -1,
81
+ }
82
+
83
+ const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
84
+ {
85
+ context: "search",
86
+ match: (data, state) => data === state.ctrlQ,
87
+ intent: () => ({ type: "cancel" }),
88
+ },
89
+ {
90
+ context: "search",
91
+ help: "esc cancel",
92
+ match: (data) => matchesKey(data, Key.escape),
93
+ intent: () => ({ type: "searchCancel" }),
94
+ },
95
+ {
96
+ context: "search",
97
+ help: "enter apply",
98
+ match: (data) => matchesKey(data, Key.enter),
99
+ intent: () => ({ type: "searchApply" }),
100
+ },
101
+ {
102
+ context: "search",
103
+ match: (data) => matchesKey(data, Key.backspace),
104
+ intent: () => ({ type: "searchBackspace" }),
105
+ },
106
+ {
107
+ context: "search",
108
+ help: "type to search",
109
+ match: (data) => isPrintable(data),
110
+ intent: (data) => ({ type: "searchAppend", value: data }),
111
+ },
112
+ {
113
+ context: "default",
114
+ help: "w/s navigate",
115
+ match: (data) => data in MOVE_KEYS,
116
+ intent: (data) => ({ type: "moveSelection", delta: MOVE_KEYS[data] ?? 1 }),
117
+ },
118
+ {
119
+ context: "default",
120
+ help: "enter work",
121
+ match: (data) => matchesKey(data, Key.enter),
122
+ intent: () => ({ type: "work" }),
123
+ },
124
+ {
125
+ context: "default",
126
+ help: "e edit",
127
+ match: (data) => data === "e" || data === "E" || matchesKey(data, Key.right),
128
+ intent: () => ({ type: "edit" }),
129
+ },
130
+ {
131
+ context: "default",
132
+ help: (state) => buildPriorityHelpText(state.priorities, state.priorityHotkeys),
133
+ showInHelp: (state) => state.allowPriority,
134
+ match: (data, state) => state.allowPriority && parsePriorityKey(data, state.priorities, state.priorityHotkeys) !== null,
135
+ intent: (data, state) => ({
136
+ type: "setPriority",
137
+ priority: parsePriorityKey(data, state.priorities, state.priorityHotkeys) ?? state.priorities[0] ?? "",
138
+ }),
139
+ },
140
+ {
141
+ context: "default",
142
+ help: "f find",
143
+ showInHelp: (state) => state.allowSearch,
144
+ match: (data, state) => state.allowSearch && (data === "f" || data === "F"),
145
+ intent: () => ({ type: "searchStart" }),
146
+ },
147
+ {
148
+ context: "default",
149
+ match: (data) => data === " ",
150
+ intent: () => ({ type: "toggleStatus" }),
151
+ },
152
+ {
153
+ context: "default",
154
+ match: (data) => data in SCROLL_KEYS,
155
+ intent: (data) => ({ type: "scrollDescription", delta: SCROLL_KEYS[data] ?? 1 }),
156
+ },
157
+ {
158
+ context: "default",
159
+ help: "t type",
160
+ match: (data) => data === "t" || data === "T",
161
+ intent: () => ({ type: "toggleType" }),
162
+ },
163
+ {
164
+ context: "default",
165
+ help: "c create",
166
+ match: (data) => data === "c" || data === "C",
167
+ intent: () => ({ type: "create" }),
168
+ },
169
+ {
170
+ context: "default",
171
+ help: "tab insert",
172
+ match: (data) => matchesKey(data, Key.tab),
173
+ intent: () => ({ type: "insert" }),
174
+ },
175
+ {
176
+ context: "default",
177
+ match: (data, state) => data === state.ctrlQ,
178
+ intent: () => ({ type: "cancel" }),
179
+ },
180
+ ]
181
+
182
+ export function resolveListIntent(data: string, state: ListControllerState): ListIntent {
183
+ const context: ShortcutContext = state.searching ? "search" : "default"
184
+ for (const shortcut of SHORTCUT_DEFINITIONS) {
185
+ if (shortcut.context !== context) continue
186
+ if (shortcut.match(data, state)) return shortcut.intent(data, state)
187
+ }
188
+ return { type: "delegate" }
189
+ }
190
+
191
+ export function buildListPrimaryHelpText(state: ListControllerState): string {
192
+ const context: ShortcutContext = state.searching ? "search" : "default"
193
+ const parts = SHORTCUT_DEFINITIONS
194
+ .filter(s => s.context === context)
195
+ .filter(s => !!s.help)
196
+ .filter(s => (s.showInHelp ? s.showInHelp(state) : true))
197
+ .map(s => (typeof s.help === "function" ? s.help(state) : s.help as string))
198
+
199
+ if (context === "default") {
200
+ parts.push(state.filtered ? "esc clear filter" : "esc cancel")
201
+ }
202
+
203
+ return parts.join(" • ")
204
+ }
205
+
206
+ export function buildListSecondaryHelpText(): string {
207
+ return "space status • j/k scroll"
208
+ }
@@ -0,0 +1,81 @@
1
+ import type { TaskStatus } from "../models/task.ts"
2
+
3
+ export type FormFocus = "nav" | "title" | "desc"
4
+ export type FormMode = "edit" | "create"
5
+
6
+ export interface FormDraft {
7
+ title: string
8
+ description: string
9
+ status: TaskStatus
10
+ priority: string | undefined
11
+ taskType: string | undefined
12
+ }
13
+
14
+ type HeaderStatusColor = "dim" | "accent" | "warning"
15
+
16
+ export interface HeaderStatus {
17
+ message: string
18
+ icon?: string
19
+ color: HeaderStatusColor
20
+ }
21
+
22
+ const FOCUS_LABELS: Record<Exclude<FormFocus, "nav">, string> = {
23
+ title: "Title",
24
+ desc: "Description",
25
+ }
26
+
27
+ export function normalizeDraft(draft: FormDraft): FormDraft {
28
+ return {
29
+ ...draft,
30
+ title: draft.title.trim(),
31
+ }
32
+ }
33
+
34
+ export function isSameDraft(a: FormDraft, b: FormDraft): boolean {
35
+ const left = normalizeDraft(a)
36
+ const right = normalizeDraft(b)
37
+ return (
38
+ left.title === right.title &&
39
+ left.description === right.description &&
40
+ left.status === right.status &&
41
+ left.priority === right.priority &&
42
+ left.taskType === right.taskType
43
+ )
44
+ }
45
+
46
+ export function getHeaderStatus(
47
+ saveIndicator: "saving" | "saved" | "error" | undefined,
48
+ focus: FormFocus,
49
+ ): HeaderStatus | undefined {
50
+ if (saveIndicator === "saving") return { message: "Saving…", icon: "⟳", color: "dim" }
51
+ if (saveIndicator === "saved") return { message: "Saved", icon: "âś“", color: "accent" }
52
+ if (saveIndicator === "error") return { message: "Save failed", color: "warning" }
53
+ if (focus === "title" || focus === "desc") return { message: `Editing ${FOCUS_LABELS[focus].toLowerCase()}`, color: "accent" }
54
+ return undefined
55
+ }
56
+
57
+ export function buildPrimaryHelpText(focus: FormFocus): string {
58
+ if (focus === "title") return "enter save • tab description • esc back"
59
+ if (focus === "desc") return "enter newline • tab save • esc back"
60
+ return "tab title • enter save • esc/q back • ctrl+x close"
61
+ }
62
+
63
+ function buildPriorityHelpText(priorities: string[], priorityHotkeys?: Record<string, string>): string {
64
+ const hotkeyKeys = priorityHotkeys ? Object.keys(priorityHotkeys).sort((a, b) => a.localeCompare(b)) : []
65
+ if (hotkeyKeys.length > 0) {
66
+ return `${hotkeyKeys.join("/")} priority`
67
+ }
68
+
69
+ if (priorities.length === 0) return "priority"
70
+ if (priorities.length === 1) return "1 priority"
71
+ return `1-${priorities.length} priority`
72
+ }
73
+
74
+ export function buildSecondaryHelpText(
75
+ focus: FormFocus,
76
+ priorities: string[],
77
+ priorityHotkeys?: Record<string, string>,
78
+ ): string {
79
+ if (focus !== "nav") return ""
80
+ return `space status • ${buildPriorityHelpText(priorities, priorityHotkeys)} • t type`
81
+ }
@@ -0,0 +1,358 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent"
2
+ import initializeAdapter from "./backend/resolver.ts"
3
+ import type { Task, TaskStatus } from "./models/task.ts"
4
+ import { buildTaskWorkPrompt, serializeTask } from "./lib/task-serialization.ts"
5
+ import { showTaskList } from "./ui/pages/list.ts"
6
+ import { showTaskForm } from "./ui/pages/show.ts"
7
+ import type { TaskUpdate } from "./backend/api.ts"
8
+
9
+ const CTRL_Q = "\x11"
10
+
11
+ function parsePriorityKey(
12
+ data: string,
13
+ priorities: string[],
14
+ priorityHotkeys?: Record<string, string>,
15
+ ): string | null {
16
+ if (data.length !== 1) return null
17
+
18
+ const hotkeyPriority = priorityHotkeys?.[data]
19
+ if (hotkeyPriority && priorities.includes(hotkeyPriority)) return hotkeyPriority
20
+
21
+ const rank = parseInt(data, 10)
22
+ if (isNaN(rank) || rank < 1 || rank > priorities.length) return null
23
+ return priorities[rank - 1] ?? null
24
+ }
25
+
26
+ function cycleStatus(current: TaskStatus, statusMap: Record<string, string>): TaskStatus {
27
+ const statusCycle = Object.keys(statusMap) as TaskStatus[]
28
+ if (statusCycle.length === 0) return "open"
29
+ const idx = statusCycle.indexOf(current)
30
+ if (idx === -1) return statusCycle[0]
31
+ return statusCycle[(idx + 1) % statusCycle.length]
32
+ }
33
+
34
+ function cycleTaskType(current: string | undefined, taskTypes: string[]): string {
35
+ if (taskTypes.length === 0) return "task"
36
+ const normalized = current || taskTypes[0]
37
+ const idx = taskTypes.indexOf(normalized)
38
+ if (idx === -1) return taskTypes[0]
39
+ return taskTypes[(idx + 1) % taskTypes.length]
40
+ }
41
+
42
+ function defaultPriority(priorities: string[]): string | undefined {
43
+ if (priorities.length === 0) return undefined
44
+ return priorities[Math.floor(priorities.length / 2)]
45
+ }
46
+
47
+ function defaultTaskType(taskTypes: string[]): string | undefined {
48
+ return taskTypes[0]
49
+ }
50
+
51
+ function hasUniqueValues(values: string[]): boolean {
52
+ return new Set(values).size === values.length
53
+ }
54
+
55
+ function validateBackendConfiguration(backend: {
56
+ id: string
57
+ statusMap: Record<string, string>
58
+ taskTypes: string[]
59
+ priorities: string[]
60
+ priorityHotkeys?: Record<string, string>
61
+ }): void {
62
+ const statusKeys = Object.keys(backend.statusMap)
63
+ if (statusKeys.length === 0) {
64
+ throw new Error(`Invalid backend config (${backend.id}): statusMap must not be empty`)
65
+ }
66
+
67
+ if (!statusKeys.includes("open") || !statusKeys.includes("closed")) {
68
+ throw new Error(`Invalid backend config (${backend.id}): statusMap must include open and closed`)
69
+ }
70
+
71
+ if (backend.taskTypes.length === 0) {
72
+ throw new Error(`Invalid backend config (${backend.id}): taskTypes must not be empty`)
73
+ }
74
+
75
+ if (!hasUniqueValues(backend.taskTypes)) {
76
+ throw new Error(`Invalid backend config (${backend.id}): taskTypes must be unique`)
77
+ }
78
+
79
+ if (backend.priorities.length < 3 || backend.priorities.length > 5) {
80
+ throw new Error(`Invalid backend config (${backend.id}): priorities must contain 3 to 5 values`)
81
+ }
82
+
83
+ if (!hasUniqueValues(backend.priorities)) {
84
+ throw new Error(`Invalid backend config (${backend.id}): priorities must be unique`)
85
+ }
86
+
87
+ if (backend.priorityHotkeys) {
88
+ for (const [key, priority] of Object.entries(backend.priorityHotkeys)) {
89
+ if (key.length !== 1) {
90
+ throw new Error(`Invalid backend config (${backend.id}): priority hotkey keys must be a single character`)
91
+ }
92
+
93
+ if (!backend.priorities.includes(priority)) {
94
+ throw new Error(`Invalid backend config (${backend.id}): priority hotkey ${key} points to unsupported priority ${priority}`)
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ interface EditTaskResult {
101
+ updatedTask: Task | null
102
+ closeList: boolean
103
+ }
104
+
105
+ function buildTaskUpdate(previous: Task, next: {
106
+ title: string
107
+ description: string
108
+ status: TaskStatus
109
+ priority: string | undefined
110
+ taskType: string | undefined
111
+ }): TaskUpdate {
112
+ const update: TaskUpdate = {}
113
+
114
+ const nextTitle = next.title.trim()
115
+ if (nextTitle !== previous.title.trim()) {
116
+ update.title = nextTitle
117
+ }
118
+
119
+ if (next.description !== (previous.description ?? "")) {
120
+ update.description = next.description
121
+ }
122
+
123
+ if (next.status !== previous.status) {
124
+ update.status = next.status
125
+ }
126
+
127
+ if (next.priority !== previous.priority && next.priority !== undefined) {
128
+ update.priority = next.priority
129
+ }
130
+
131
+ if (next.taskType !== previous.taskType) {
132
+ update.taskType = next.taskType || "task"
133
+ }
134
+
135
+ return update
136
+ }
137
+
138
+ function hasTaskUpdate(update: TaskUpdate): boolean {
139
+ return Object.keys(update).length > 0
140
+ }
141
+
142
+ function applyDraftToTask(
143
+ task: Task,
144
+ draft: {
145
+ title: string
146
+ description: string
147
+ status: TaskStatus
148
+ priority: string | undefined
149
+ taskType: string | undefined
150
+ },
151
+ ): Task {
152
+ const nextTask: Task = {
153
+ ...task,
154
+ title: draft.title.trim(),
155
+ description: draft.description,
156
+ status: draft.status,
157
+ }
158
+
159
+ if (draft.priority !== undefined) {
160
+ nextTask.priority = draft.priority
161
+ } else {
162
+ delete nextTask.priority
163
+ }
164
+
165
+ if (draft.taskType !== undefined) {
166
+ nextTask.taskType = draft.taskType
167
+ } else {
168
+ delete nextTask.taskType
169
+ }
170
+
171
+ return nextTask
172
+ }
173
+
174
+ export default function registerExtension(pi: ExtensionAPI) {
175
+ const backend = initializeAdapter(pi)
176
+ validateBackendConfiguration(backend)
177
+
178
+ const nextStatus = (status: TaskStatus): TaskStatus => cycleStatus(status, backend.statusMap)
179
+ const nextTaskType = (current: string | undefined): string => cycleTaskType(current, backend.taskTypes)
180
+ const nextPriorityFromKey = (data: string): string | null => parsePriorityKey(
181
+ data,
182
+ backend.priorities,
183
+ backend.priorityHotkeys,
184
+ )
185
+
186
+ async function listTasks(): Promise<Task[]> {
187
+ return backend.list()
188
+ }
189
+
190
+ async function showTask(ref: string): Promise<Task> {
191
+ return backend.show(ref)
192
+ }
193
+
194
+ function needsTaskDetailsForEdit(task: Task): boolean {
195
+ return task.description === undefined
196
+ }
197
+
198
+ async function getTaskForEdit(ref: string, fromList?: Task): Promise<Task> {
199
+ if (!fromList) return showTask(ref)
200
+ if (needsTaskDetailsForEdit(fromList)) return showTask(ref)
201
+ return { ...fromList }
202
+ }
203
+
204
+ async function updateTask(ref: string, update: TaskUpdate): Promise<void> {
205
+ await backend.update(ref, update)
206
+ }
207
+
208
+ async function editTask(ctx: ExtensionCommandContext, ref: string, fromList?: Task): Promise<EditTaskResult> {
209
+ let task = await getTaskForEdit(ref, fromList)
210
+
211
+ const formResult = await showTaskForm(ctx, {
212
+ mode: "edit",
213
+ subtitle: "Edit",
214
+ task,
215
+ ctrlQ: CTRL_Q,
216
+ cycleStatus: nextStatus,
217
+ cycleTaskType: nextTaskType,
218
+ parsePriorityKey: nextPriorityFromKey,
219
+ priorities: backend.priorities,
220
+ priorityHotkeys: backend.priorityHotkeys,
221
+ onSave: async (draft) => {
222
+ const update = buildTaskUpdate(task, {
223
+ title: draft.title,
224
+ description: draft.description,
225
+ status: draft.status,
226
+ priority: draft.priority,
227
+ taskType: draft.taskType,
228
+ })
229
+
230
+ if (!hasTaskUpdate(update)) return false
231
+
232
+ await updateTask(ref, update)
233
+ task = applyDraftToTask(task, {
234
+ title: draft.title,
235
+ description: draft.description,
236
+ status: draft.status,
237
+ priority: draft.priority,
238
+ taskType: draft.taskType,
239
+ })
240
+ return true
241
+ },
242
+ })
243
+
244
+ return {
245
+ updatedTask: task,
246
+ closeList: formResult.action === "close_list",
247
+ }
248
+ }
249
+
250
+ async function createTask(ctx: ExtensionCommandContext): Promise<Task | null> {
251
+ let createdTask: Task | null = null
252
+
253
+ await showTaskForm(ctx, {
254
+ mode: "create",
255
+ subtitle: "Create",
256
+ task: {
257
+ ref: "new",
258
+ title: "",
259
+ description: "",
260
+ status: "open",
261
+ priority: defaultPriority(backend.priorities),
262
+ taskType: defaultTaskType(backend.taskTypes),
263
+ },
264
+ ctrlQ: CTRL_Q,
265
+ cycleStatus: nextStatus,
266
+ cycleTaskType: nextTaskType,
267
+ parsePriorityKey: nextPriorityFromKey,
268
+ priorities: backend.priorities,
269
+ priorityHotkeys: backend.priorityHotkeys,
270
+ onSave: async (draft) => {
271
+ const title = draft.title.trim()
272
+ if (title.length === 0) {
273
+ throw new Error("Title is required")
274
+ }
275
+
276
+ if (!createdTask) {
277
+ createdTask = await backend.create({
278
+ title,
279
+ description: draft.description,
280
+ status: draft.status,
281
+ priority: draft.priority,
282
+ taskType: draft.taskType,
283
+ })
284
+ return true
285
+ }
286
+
287
+ const update = buildTaskUpdate(createdTask, {
288
+ title,
289
+ description: draft.description,
290
+ status: draft.status,
291
+ priority: draft.priority,
292
+ taskType: draft.taskType,
293
+ })
294
+
295
+ if (!hasTaskUpdate(update)) return false
296
+
297
+ await updateTask(createdTask.ref, update)
298
+ createdTask = applyDraftToTask(createdTask, {
299
+ title,
300
+ description: draft.description,
301
+ status: draft.status,
302
+ priority: draft.priority,
303
+ taskType: draft.taskType,
304
+ })
305
+ return true
306
+ },
307
+ })
308
+
309
+ return createdTask
310
+ }
311
+
312
+ async function browseTasks(ctx: ExtensionCommandContext): Promise<void> {
313
+ const pageTitle = "Tasks"
314
+ const backendLabel = backend.id
315
+
316
+ try {
317
+ backend.invalidateCache?.()
318
+ ctx.ui.setStatus("tasks", "Loading…")
319
+ const tasks = await listTasks()
320
+ ctx.ui.setStatus("tasks", undefined)
321
+
322
+ await showTaskList(ctx, {
323
+ title: pageTitle,
324
+ subtitle: backendLabel,
325
+ tasks,
326
+ ctrlQ: CTRL_Q,
327
+ priorities: backend.priorities,
328
+ priorityHotkeys: backend.priorityHotkeys,
329
+ cycleStatus: nextStatus,
330
+ cycleTaskType: nextTaskType,
331
+ onUpdateTask: updateTask,
332
+ onWork: (task) => pi.sendUserMessage(buildTaskWorkPrompt(task)),
333
+ onInsert: (task) => ctx.ui.pasteToEditor(`${serializeTask(task)} `),
334
+ onEdit: (ref, task) => editTask(ctx, ref, task),
335
+ onCreate: () => createTask(ctx),
336
+ })
337
+ } catch (e) {
338
+ ctx.ui.setStatus("tasks", undefined)
339
+ ctx.ui.notify(e instanceof Error ? e.message : String(e), "error")
340
+ }
341
+ }
342
+
343
+ pi.registerCommand("tasks", {
344
+ description: "Open task list",
345
+ handler: async (_rawArgs, ctx) => {
346
+ if (!ctx.hasUI) return
347
+ await browseTasks(ctx)
348
+ },
349
+ })
350
+
351
+ pi.registerShortcut("ctrl+x", {
352
+ description: "Open task list",
353
+ handler: async (ctx) => {
354
+ if (!ctx.hasUI) return
355
+ await browseTasks(ctx as ExtensionCommandContext)
356
+ },
357
+ })
358
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"
2
+
3
+ import registerExtension from "./extension.ts"
4
+
5
+ export default function (pi: ExtensionAPI) {
6
+ registerExtension(pi)
7
+ }
@@ -0,0 +1,45 @@
1
+ import type { Task } from "../models/task.ts"
2
+ import { toKebabCase } from "../models/task.ts"
3
+
4
+ export function serializeTask(task: Task): string {
5
+ const parts = [
6
+ `title="${task.title}"`,
7
+ `status=${toKebabCase(task.status)}`,
8
+ `priority=${task.priority ?? "unknown"}`,
9
+ `type=${task.taskType || "task"}`,
10
+ ]
11
+
12
+ if (task.id) {
13
+ parts.unshift(`id=${task.id}`)
14
+ }
15
+
16
+ const description = task.description?.trim()
17
+ if (description) {
18
+ parts.push(`description="${description.replaceAll("\n", "\\n")}"`)
19
+ }
20
+
21
+ if (task.dueAt) {
22
+ parts.push(`due="${task.dueAt}"`)
23
+ }
24
+
25
+ return `task(${parts.join(", ")})`
26
+ }
27
+
28
+ export function buildTaskWorkPrompt(task: Task): string {
29
+ const leadLine = task.id
30
+ ? `Work on task ${task.id}: ${task.title}`
31
+ : `Work on task: ${task.title}`
32
+
33
+ const lines = [
34
+ leadLine,
35
+ "",
36
+ `Status: ${toKebabCase(task.status)}`,
37
+ `Priority: ${task.priority ?? "unknown"}`,
38
+ ]
39
+
40
+ if (task.description && task.description.trim()) {
41
+ lines.push("", "Context:", task.description.trim())
42
+ }
43
+
44
+ return lines.join("\n")
45
+ }