@hachej/boring-tasks 0.1.79
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 +27 -0
- package/dist/front/index.d.ts +42 -0
- package/dist/front/index.js +1134 -0
- package/dist/server/index.d.ts +161 -0
- package/dist/server/index.js +469 -0
- package/dist/shared/index.d.ts +69 -0
- package/dist/shared/index.js +9 -0
- package/package.json +73 -0
|
@@ -0,0 +1,1134 @@
|
|
|
1
|
+
// src/front/index.tsx
|
|
2
|
+
import { definePlugin } from "@hachej/boring-workspace/plugin";
|
|
3
|
+
|
|
4
|
+
// src/shared/index.ts
|
|
5
|
+
var TASKS_PLUGIN_ID = "tasks";
|
|
6
|
+
var TASKS_PLUGIN_LABEL = "Tasks";
|
|
7
|
+
|
|
8
|
+
// src/front/TasksOverlay.tsx
|
|
9
|
+
import { useEffect as useEffect2, useMemo as useMemo2, useState as useState3 } from "react";
|
|
10
|
+
import { IconButton } from "@hachej/boring-ui-kit";
|
|
11
|
+
import { useWorkspacePluginClient as useWorkspacePluginClient2 } from "@hachej/boring-workspace";
|
|
12
|
+
import { useAppLeftOverlayChrome } from "@hachej/boring-workspace/plugin";
|
|
13
|
+
import { X } from "lucide-react";
|
|
14
|
+
|
|
15
|
+
// src/front/httpTaskAdapter.ts
|
|
16
|
+
var ROUTE_PREFIX = "/api/boring-tasks";
|
|
17
|
+
async function readError(response) {
|
|
18
|
+
try {
|
|
19
|
+
const body = await response.json();
|
|
20
|
+
const message = body.error ?? body.message;
|
|
21
|
+
if (typeof message === "string" && message.length > 0) return message;
|
|
22
|
+
} catch {
|
|
23
|
+
}
|
|
24
|
+
return `${response.status} ${response.statusText}`.trim();
|
|
25
|
+
}
|
|
26
|
+
async function fetchJson(path, init) {
|
|
27
|
+
const response = await fetch(`${ROUTE_PREFIX}${path}`, {
|
|
28
|
+
credentials: "include",
|
|
29
|
+
...init,
|
|
30
|
+
headers: {
|
|
31
|
+
...init?.body ? { "Content-Type": "application/json" } : {},
|
|
32
|
+
...init?.headers
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) throw new Error(await readError(response));
|
|
36
|
+
return await response.json();
|
|
37
|
+
}
|
|
38
|
+
function getJson(client, path) {
|
|
39
|
+
return client ? client.getJson(`${ROUTE_PREFIX}${path}`) : fetchJson(path);
|
|
40
|
+
}
|
|
41
|
+
function postJson(client, path, body) {
|
|
42
|
+
return client ? client.postJson(`${ROUTE_PREFIX}${path}`, body) : fetchJson(path, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
body: JSON.stringify(body)
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async function listHttpTaskSources(client) {
|
|
48
|
+
const body = await getJson(client, "/sources");
|
|
49
|
+
return body.sources ?? [];
|
|
50
|
+
}
|
|
51
|
+
function createHttpTaskAdapter(source, client) {
|
|
52
|
+
return {
|
|
53
|
+
...source,
|
|
54
|
+
async getBoardConfig() {
|
|
55
|
+
const body = await postJson(client, "/sources/tasks/list", { sourceIds: [source.id] });
|
|
56
|
+
const config = body.configs?.[source.id];
|
|
57
|
+
if (!config) throw new Error(`Task source did not return board config: ${source.id}`);
|
|
58
|
+
return config;
|
|
59
|
+
},
|
|
60
|
+
async listTasks() {
|
|
61
|
+
const body = await postJson(client, "/sources/tasks/list", { sourceIds: [source.id] });
|
|
62
|
+
return body.tasks ?? [];
|
|
63
|
+
},
|
|
64
|
+
moveTask: source.capabilities.move ? async ({ taskId, statusId }) => {
|
|
65
|
+
const body = await postJson(client, "/sources/tasks/move", { sourceId: source.id, taskId, statusId });
|
|
66
|
+
if (!body.task) throw new Error(`Task source did not return moved task: ${source.id}`);
|
|
67
|
+
return body.task;
|
|
68
|
+
} : void 0,
|
|
69
|
+
deleteTask: source.capabilities.delete ? async ({ taskId }) => {
|
|
70
|
+
await postJson(client, "/sources/tasks/delete", { sourceId: source.id, taskId });
|
|
71
|
+
} : void 0
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/front/TaskKanbanBoard.tsx
|
|
76
|
+
import { useCallback, useEffect, useMemo, useRef, useState as useState2 } from "react";
|
|
77
|
+
import { Columns3, List } from "lucide-react";
|
|
78
|
+
|
|
79
|
+
// src/front/taskBoardModel.ts
|
|
80
|
+
var UNMAPPED_COLUMN_ID = "__unmapped__";
|
|
81
|
+
function groupTasksByColumn(board, tasks) {
|
|
82
|
+
const configuredIds = new Set(board.columns.map((column) => column.id));
|
|
83
|
+
const columns = board.columns.map((column) => ({ ...column, tasks: [] }));
|
|
84
|
+
const byId = new Map(columns.map((column) => [column.id, column]));
|
|
85
|
+
const unmapped = [];
|
|
86
|
+
for (const task of tasks) {
|
|
87
|
+
const column = byId.get(task.statusId);
|
|
88
|
+
if (column) column.tasks.push(task);
|
|
89
|
+
else unmapped.push(task);
|
|
90
|
+
}
|
|
91
|
+
if (unmapped.length > 0) {
|
|
92
|
+
columns.push({
|
|
93
|
+
id: UNMAPPED_COLUMN_ID,
|
|
94
|
+
title: "Unmapped",
|
|
95
|
+
description: "Tasks whose adapter status is not in this board config",
|
|
96
|
+
acceptsDrop: false,
|
|
97
|
+
tasks: unmapped,
|
|
98
|
+
unmapped: true
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return columns.filter((column) => column.tasks.length > 0 || configuredIds.has(column.id));
|
|
102
|
+
}
|
|
103
|
+
function canDropInColumn(column) {
|
|
104
|
+
return column.id !== UNMAPPED_COLUMN_ID && column.acceptsDrop !== false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/front/TaskCard.tsx
|
|
108
|
+
import { useState } from "react";
|
|
109
|
+
import { MessageSquare, MoreHorizontal, Trash2 } from "lucide-react";
|
|
110
|
+
import { useWorkspacePluginClient } from "@hachej/boring-workspace";
|
|
111
|
+
import { useWorkspaceShellCapabilities } from "@hachej/boring-workspace/plugin";
|
|
112
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
113
|
+
function ExternalLinkGlyph({ className }) {
|
|
114
|
+
return /* @__PURE__ */ jsxs("svg", { className, viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: [
|
|
115
|
+
/* @__PURE__ */ jsx("path", { d: "M14 5h5v5M19 5l-9 9", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }),
|
|
116
|
+
/* @__PURE__ */ jsx("path", { d: "M11 6H7a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-4", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round" })
|
|
117
|
+
] });
|
|
118
|
+
}
|
|
119
|
+
function rectFromElement(element) {
|
|
120
|
+
const rect = element.getBoundingClientRect();
|
|
121
|
+
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height, top: rect.top, right: rect.right, bottom: rect.bottom, left: rect.left };
|
|
122
|
+
}
|
|
123
|
+
function taskChatDraft(task) {
|
|
124
|
+
return [
|
|
125
|
+
`Let's work on ${task.number}: ${task.title}`,
|
|
126
|
+
"",
|
|
127
|
+
`Task ID: ${task.number}`,
|
|
128
|
+
`Status: ${task.statusId}`,
|
|
129
|
+
task.url ? `URL: ${task.url}` : null
|
|
130
|
+
].filter(Boolean).join("\n");
|
|
131
|
+
}
|
|
132
|
+
function TaskCard({ task, draggable, unmapped = false, deleteEnabled = false, compact = false, onDelete, onDragStart, onDragEnd }) {
|
|
133
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
134
|
+
const [openingChat, setOpeningChat] = useState(false);
|
|
135
|
+
const shell = useWorkspaceShellCapabilities();
|
|
136
|
+
const pluginClient = useWorkspacePluginClient();
|
|
137
|
+
const tags = task.tags?.slice(0, 4) ?? [];
|
|
138
|
+
const hiddenTagCount = Math.max((task.tags?.length ?? 0) - tags.length, 0);
|
|
139
|
+
const pullRequests = task.pullRequests?.slice(0, 2) ?? [];
|
|
140
|
+
const hiddenPullRequestCount = Math.max((task.pullRequests?.length ?? 0) - pullRequests.length, 0);
|
|
141
|
+
const stopCardAction = (event) => event.stopPropagation();
|
|
142
|
+
const openTaskChat = (event) => {
|
|
143
|
+
stopCardAction(event);
|
|
144
|
+
const anchor = rectFromElement(event.currentTarget);
|
|
145
|
+
const title = `${task.number}: ${task.title}`;
|
|
146
|
+
setOpeningChat(true);
|
|
147
|
+
void pluginClient.postJson("/api/v1/agent/pi-chat/sessions", { title }).then((session) => {
|
|
148
|
+
if (typeof session.id !== "string" || session.id.length === 0) throw new Error("Chat session was not created.");
|
|
149
|
+
const initialDraft = taskChatDraft(task);
|
|
150
|
+
shell.openDetachedChat(session.id, { anchor, title, initialDraft, composingEnabled: true });
|
|
151
|
+
window.dispatchEvent(new CustomEvent("boring-workspace:open-detached-chat", { detail: { sessionId: session.id, title, initialDraft, composingEnabled: true } }));
|
|
152
|
+
}).catch((error) => console.error("Failed to open task chat", error)).finally(() => setOpeningChat(false));
|
|
153
|
+
};
|
|
154
|
+
const deleteTask = (event) => {
|
|
155
|
+
stopCardAction(event);
|
|
156
|
+
setMenuOpen(false);
|
|
157
|
+
onDelete?.(task);
|
|
158
|
+
};
|
|
159
|
+
if (compact) {
|
|
160
|
+
return /* @__PURE__ */ jsxs(
|
|
161
|
+
"article",
|
|
162
|
+
{
|
|
163
|
+
draggable,
|
|
164
|
+
onDragStart: (event) => onDragStart(event, task),
|
|
165
|
+
onDragEnd,
|
|
166
|
+
className: [
|
|
167
|
+
"group flex min-w-0 items-center gap-2 rounded-xl border bg-background px-3 py-2 shadow-sm transition",
|
|
168
|
+
draggable ? "cursor-grab hover:border-foreground/30 hover:shadow-md active:cursor-grabbing" : "cursor-default",
|
|
169
|
+
unmapped ? "border-dashed border-amber-400/60 bg-amber-500/5" : "border-border"
|
|
170
|
+
].join(" "),
|
|
171
|
+
"data-task-id": task.id,
|
|
172
|
+
children: [
|
|
173
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 rounded-full border border-border bg-muted/50 px-2 py-0.5 font-mono text-[10px] font-semibold uppercase tracking-wide text-muted-foreground", children: task.number }),
|
|
174
|
+
/* @__PURE__ */ jsx("h3", { className: "min-w-0 flex-1 truncate text-sm font-semibold text-foreground", title: task.title, children: task.title }),
|
|
175
|
+
task.epic ? /* @__PURE__ */ jsx("span", { className: "hidden max-w-36 shrink-0 truncate rounded-full border border-primary/20 bg-primary/10 px-2 py-0.5 text-[10px] font-medium text-primary sm:inline-block", children: task.epic.title }) : null,
|
|
176
|
+
tags.slice(0, 2).map((tag) => /* @__PURE__ */ jsx("span", { className: "hidden max-w-28 shrink-0 truncate rounded-full border border-border bg-muted/30 px-2 py-0.5 text-[10px] font-medium text-muted-foreground md:inline-block", children: tag }, tag)),
|
|
177
|
+
pullRequests.slice(0, 1).map((pr) => pr.url ? /* @__PURE__ */ jsxs("a", { href: pr.url, target: "_blank", rel: "noreferrer", draggable: false, onClick: (event) => event.stopPropagation(), className: "hidden max-w-64 shrink-0 truncate rounded-full border border-emerald-500/30 bg-emerald-500/10 px-2 py-0.5 text-[10px] font-semibold text-emerald-700 hover:bg-emerald-500/15 dark:text-emerald-300 lg:inline-block", title: pr.title, "aria-label": `Open pull request ${pr.number}: ${pr.title}`, children: [
|
|
178
|
+
"PR ",
|
|
179
|
+
pr.number,
|
|
180
|
+
" ",
|
|
181
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: pr.title })
|
|
182
|
+
] }, pr.id) : /* @__PURE__ */ jsxs("span", { className: "hidden max-w-64 shrink-0 truncate rounded-full border border-emerald-500/30 bg-emerald-500/10 px-2 py-0.5 text-[10px] font-semibold text-emerald-700 dark:text-emerald-300 lg:inline-block", title: pr.title, children: [
|
|
183
|
+
"PR ",
|
|
184
|
+
pr.number,
|
|
185
|
+
" ",
|
|
186
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: pr.title })
|
|
187
|
+
] }, pr.id)),
|
|
188
|
+
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-0.5", children: [
|
|
189
|
+
/* @__PURE__ */ jsx("button", { type: "button", draggable: false, onClick: openTaskChat, className: "grid size-7 place-items-center rounded-lg text-muted-foreground opacity-80 hover:bg-muted hover:text-foreground group-hover:opacity-100", "aria-label": `Open chat for ${task.number}`, title: "Open task chat", disabled: openingChat, children: /* @__PURE__ */ jsx(MessageSquare, { className: ["size-3.5", openingChat ? "animate-pulse" : ""].join(" "), strokeWidth: 1.75 }) }),
|
|
190
|
+
task.url ? /* @__PURE__ */ jsx("a", { href: task.url, target: "_blank", rel: "noreferrer", draggable: false, onClick: (event) => event.stopPropagation(), className: "grid size-7 place-items-center rounded-lg text-muted-foreground opacity-80 hover:bg-muted hover:text-foreground group-hover:opacity-100", "aria-label": `Open ${task.number} in native task system`, title: "Open in native task system", children: /* @__PURE__ */ jsx(ExternalLinkGlyph, { className: "size-3.5" }) }) : null,
|
|
191
|
+
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
192
|
+
/* @__PURE__ */ jsx("button", { type: "button", draggable: false, onClick: (event) => {
|
|
193
|
+
stopCardAction(event);
|
|
194
|
+
setMenuOpen((current) => !current);
|
|
195
|
+
}, className: "grid size-7 place-items-center rounded-lg text-muted-foreground opacity-80 hover:bg-muted hover:text-foreground group-hover:opacity-100", "aria-label": `Open actions for ${task.number}`, "aria-expanded": menuOpen, title: "Task actions", children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "size-3.5", strokeWidth: 1.75 }) }),
|
|
196
|
+
menuOpen ? /* @__PURE__ */ jsx("div", { className: "absolute right-0 z-30 mt-1 w-40 overflow-hidden rounded-xl border border-border bg-popover p-1 text-sm text-popover-foreground shadow-xl", children: /* @__PURE__ */ jsxs("button", { type: "button", className: "flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-destructive hover:bg-destructive/10 disabled:cursor-not-allowed disabled:opacity-50", disabled: !deleteEnabled, onClick: deleteTask, title: deleteEnabled ? "Delete issue" : "This task source cannot delete issues", children: [
|
|
197
|
+
/* @__PURE__ */ jsx(Trash2, { className: "size-3.5", strokeWidth: 1.75 }),
|
|
198
|
+
"Delete issue"
|
|
199
|
+
] }) }) : null
|
|
200
|
+
] })
|
|
201
|
+
] })
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return /* @__PURE__ */ jsxs(
|
|
207
|
+
"article",
|
|
208
|
+
{
|
|
209
|
+
draggable,
|
|
210
|
+
onDragStart: (event) => onDragStart(event, task),
|
|
211
|
+
onDragEnd,
|
|
212
|
+
className: [
|
|
213
|
+
"group rounded-xl border bg-background p-3 shadow-sm transition",
|
|
214
|
+
draggable ? "cursor-grab hover:-translate-y-0.5 hover:border-foreground/30 hover:shadow-md active:cursor-grabbing" : "cursor-default",
|
|
215
|
+
unmapped ? "border-dashed border-amber-400/60 bg-amber-500/5" : "border-border"
|
|
216
|
+
].join(" "),
|
|
217
|
+
"data-task-id": task.id,
|
|
218
|
+
children: [
|
|
219
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
|
|
220
|
+
/* @__PURE__ */ jsx("span", { className: "mt-0.5 shrink-0 rounded-full border border-border bg-muted/50 px-2 py-0.5 font-mono text-[10px] font-semibold uppercase tracking-wide text-muted-foreground", children: task.number }),
|
|
221
|
+
/* @__PURE__ */ jsx("h3", { className: "min-w-0 flex-1 truncate text-sm font-semibold leading-snug text-foreground", title: task.title, children: task.title }),
|
|
222
|
+
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-0.5", children: [
|
|
223
|
+
/* @__PURE__ */ jsx("button", { type: "button", draggable: false, onClick: openTaskChat, className: "grid size-7 place-items-center rounded-lg text-muted-foreground opacity-80 hover:bg-muted hover:text-foreground group-hover:opacity-100", "aria-label": `Open chat for ${task.number}`, title: "Open task chat", disabled: openingChat, children: /* @__PURE__ */ jsx(MessageSquare, { className: ["size-3.5", openingChat ? "animate-pulse" : ""].join(" "), strokeWidth: 1.75 }) }),
|
|
224
|
+
task.url ? /* @__PURE__ */ jsx("a", { href: task.url, target: "_blank", rel: "noreferrer", draggable: false, onClick: (event) => event.stopPropagation(), className: "grid size-7 place-items-center rounded-lg text-muted-foreground opacity-80 hover:bg-muted hover:text-foreground group-hover:opacity-100", "aria-label": `Open ${task.number} in native task system`, title: "Open in native task system", children: /* @__PURE__ */ jsx(ExternalLinkGlyph, { className: "size-3.5" }) }) : null,
|
|
225
|
+
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
226
|
+
/* @__PURE__ */ jsx("button", { type: "button", draggable: false, onClick: (event) => {
|
|
227
|
+
stopCardAction(event);
|
|
228
|
+
setMenuOpen((current) => !current);
|
|
229
|
+
}, className: "grid size-7 place-items-center rounded-lg text-muted-foreground opacity-80 hover:bg-muted hover:text-foreground group-hover:opacity-100", "aria-label": `Open actions for ${task.number}`, "aria-expanded": menuOpen, title: "Task actions", children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "size-3.5", strokeWidth: 1.75 }) }),
|
|
230
|
+
menuOpen ? /* @__PURE__ */ jsx("div", { className: "absolute right-0 z-30 mt-1 w-40 overflow-hidden rounded-xl border border-border bg-popover p-1 text-sm text-popover-foreground shadow-xl", children: /* @__PURE__ */ jsxs("button", { type: "button", className: "flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-destructive hover:bg-destructive/10 disabled:cursor-not-allowed disabled:opacity-50", disabled: !deleteEnabled, onClick: deleteTask, title: deleteEnabled ? "Delete issue" : "This task source cannot delete issues", children: [
|
|
231
|
+
/* @__PURE__ */ jsx(Trash2, { className: "size-3.5", strokeWidth: 1.75 }),
|
|
232
|
+
"Delete issue"
|
|
233
|
+
] }) }) : null
|
|
234
|
+
] })
|
|
235
|
+
] })
|
|
236
|
+
] }),
|
|
237
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-3 flex flex-wrap gap-1.5", children: [
|
|
238
|
+
unmapped ? /* @__PURE__ */ jsx("span", { className: "rounded-full border border-amber-400/50 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-amber-600 dark:text-amber-300", children: "Unmapped" }) : null,
|
|
239
|
+
task.epic ? /* @__PURE__ */ jsx("span", { className: "rounded-full border border-primary/20 bg-primary/10 px-2 py-0.5 text-[10px] font-medium text-primary", children: task.epic.title }) : null,
|
|
240
|
+
tags.map((tag) => /* @__PURE__ */ jsx("span", { className: "rounded-full border border-border bg-muted/30 px-2 py-0.5 text-[10px] font-medium text-muted-foreground", children: tag }, tag)),
|
|
241
|
+
hiddenTagCount > 0 ? /* @__PURE__ */ jsxs("span", { className: "rounded-full border border-border bg-muted/30 px-2 py-0.5 text-[10px] font-medium text-muted-foreground", children: [
|
|
242
|
+
"+",
|
|
243
|
+
hiddenTagCount
|
|
244
|
+
] }) : null
|
|
245
|
+
] }),
|
|
246
|
+
pullRequests.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap gap-1.5 border-t border-border/60 pt-2", children: [
|
|
247
|
+
pullRequests.map((pr) => pr.url ? /* @__PURE__ */ jsxs("a", { href: pr.url, target: "_blank", rel: "noreferrer", draggable: false, onClick: (event) => event.stopPropagation(), className: "inline-flex max-w-full items-center gap-1 rounded-full border border-emerald-500/30 bg-emerald-500/10 px-2 py-0.5 text-[10px] font-semibold text-emerald-700 hover:bg-emerald-500/15 dark:text-emerald-300", title: pr.title, "aria-label": `Open pull request ${pr.number}: ${pr.title}`, children: [
|
|
248
|
+
/* @__PURE__ */ jsxs("span", { className: "shrink-0", children: [
|
|
249
|
+
"PR ",
|
|
250
|
+
pr.number
|
|
251
|
+
] }),
|
|
252
|
+
/* @__PURE__ */ jsx("span", { className: "truncate font-medium", children: pr.title })
|
|
253
|
+
] }, pr.id) : /* @__PURE__ */ jsxs("span", { className: "inline-flex max-w-full items-center gap-1 rounded-full border border-emerald-500/30 bg-emerald-500/10 px-2 py-0.5 text-[10px] font-semibold text-emerald-700 dark:text-emerald-300", title: pr.title, children: [
|
|
254
|
+
/* @__PURE__ */ jsxs("span", { className: "shrink-0", children: [
|
|
255
|
+
"PR ",
|
|
256
|
+
pr.number
|
|
257
|
+
] }),
|
|
258
|
+
/* @__PURE__ */ jsx("span", { className: "truncate font-medium", children: pr.title })
|
|
259
|
+
] }, pr.id)),
|
|
260
|
+
hiddenPullRequestCount > 0 ? /* @__PURE__ */ jsxs("span", { className: "rounded-full border border-border bg-muted/30 px-2 py-0.5 text-[10px] font-medium text-muted-foreground", children: [
|
|
261
|
+
"+",
|
|
262
|
+
hiddenPullRequestCount,
|
|
263
|
+
" PR"
|
|
264
|
+
] }) : null
|
|
265
|
+
] }) : null
|
|
266
|
+
]
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/front/TaskKanbanColumn.tsx
|
|
272
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
273
|
+
function TaskKanbanColumn({
|
|
274
|
+
column,
|
|
275
|
+
moveEnabled,
|
|
276
|
+
activeTaskRef,
|
|
277
|
+
onTaskDragStart,
|
|
278
|
+
onTaskDragEnd,
|
|
279
|
+
onTaskDrop,
|
|
280
|
+
onTaskDelete,
|
|
281
|
+
canDragTask = () => moveEnabled,
|
|
282
|
+
canDeleteTask = () => false
|
|
283
|
+
}) {
|
|
284
|
+
const acceptsDrop = moveEnabled && canDropInColumn(column);
|
|
285
|
+
const hasTaskDragPayload = (event) => {
|
|
286
|
+
const types = Array.from(event.dataTransfer.types);
|
|
287
|
+
return types.includes("application/x-boring-task-ref") || types.includes("application/x-boring-task-id");
|
|
288
|
+
};
|
|
289
|
+
const handleDragOver = (event) => {
|
|
290
|
+
if (!acceptsDrop || !hasTaskDragPayload(event)) return;
|
|
291
|
+
event.preventDefault();
|
|
292
|
+
event.dataTransfer.dropEffect = "move";
|
|
293
|
+
};
|
|
294
|
+
const handleDrop = (event) => {
|
|
295
|
+
if (!acceptsDrop || !hasTaskDragPayload(event)) return;
|
|
296
|
+
event.preventDefault();
|
|
297
|
+
const rawRef = event.dataTransfer.getData("application/x-boring-task-ref");
|
|
298
|
+
const fallbackTaskId = event.dataTransfer.getData("application/x-boring-task-id");
|
|
299
|
+
let ref;
|
|
300
|
+
try {
|
|
301
|
+
ref = rawRef ? JSON.parse(rawRef) : void 0;
|
|
302
|
+
} catch {
|
|
303
|
+
ref = void 0;
|
|
304
|
+
}
|
|
305
|
+
const taskId = typeof ref?.taskId === "string" ? ref.taskId : fallbackTaskId || activeTaskRef?.taskId;
|
|
306
|
+
const adapterId = typeof ref?.adapterId === "string" ? ref.adapterId : activeTaskRef?.adapterId;
|
|
307
|
+
if (taskId && adapterId) onTaskDrop(taskId, adapterId, column.id);
|
|
308
|
+
};
|
|
309
|
+
return /* @__PURE__ */ jsxs2(
|
|
310
|
+
"section",
|
|
311
|
+
{
|
|
312
|
+
className: [
|
|
313
|
+
"flex min-h-0 w-72 shrink-0 flex-col rounded-2xl border bg-muted/20",
|
|
314
|
+
column.unmapped ? "border-dashed border-amber-400/50" : "border-border/80"
|
|
315
|
+
].join(" "),
|
|
316
|
+
onDragOver: handleDragOver,
|
|
317
|
+
onDrop: handleDrop,
|
|
318
|
+
"aria-label": `${column.title} column`,
|
|
319
|
+
children: [
|
|
320
|
+
/* @__PURE__ */ jsxs2("header", { className: "border-b border-border/70 p-3", children: [
|
|
321
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between gap-2", children: [
|
|
322
|
+
/* @__PURE__ */ jsxs2("div", { className: "min-w-0", children: [
|
|
323
|
+
/* @__PURE__ */ jsx2("h2", { className: "truncate text-sm font-semibold text-foreground", children: column.title }),
|
|
324
|
+
column.description ? /* @__PURE__ */ jsx2("p", { className: "mt-0.5 line-clamp-2 text-[11px] leading-4 text-muted-foreground", children: column.description }) : null
|
|
325
|
+
] }),
|
|
326
|
+
/* @__PURE__ */ jsx2("span", { className: "rounded-full border border-border bg-background px-2 py-0.5 text-xs tabular-nums text-muted-foreground", children: column.tasks.length })
|
|
327
|
+
] }),
|
|
328
|
+
column.color ? /* @__PURE__ */ jsx2("div", { className: "mt-3 h-1 rounded-full", style: { backgroundColor: column.color } }) : null
|
|
329
|
+
] }),
|
|
330
|
+
/* @__PURE__ */ jsx2("div", { className: "flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto p-2", children: column.tasks.length === 0 ? /* @__PURE__ */ jsx2("div", { className: "rounded-xl border border-dashed border-border/80 p-3 text-center text-xs text-muted-foreground", children: "Drop tasks here" }) : column.tasks.map((task) => /* @__PURE__ */ jsx2(
|
|
331
|
+
TaskCard,
|
|
332
|
+
{
|
|
333
|
+
task,
|
|
334
|
+
draggable: !column.unmapped && canDragTask(task),
|
|
335
|
+
unmapped: column.unmapped,
|
|
336
|
+
deleteEnabled: canDeleteTask(task),
|
|
337
|
+
onDelete: onTaskDelete,
|
|
338
|
+
onDragStart: onTaskDragStart,
|
|
339
|
+
onDragEnd: onTaskDragEnd
|
|
340
|
+
},
|
|
341
|
+
`${task.adapterId}:${task.id}`
|
|
342
|
+
)) })
|
|
343
|
+
]
|
|
344
|
+
}
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/front/TaskKanbanBoard.tsx
|
|
349
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
350
|
+
var TASK_BOARD_CACHE_TTL_MS = 2 * 60 * 1e3;
|
|
351
|
+
function adapterSummary(adapters, selectedCount) {
|
|
352
|
+
if (selectedCount === adapters.length) return "All sources";
|
|
353
|
+
if (selectedCount === 1) return adapters.find((adapter) => adapter.id)?.description ?? "1 source";
|
|
354
|
+
return `${selectedCount} sources`;
|
|
355
|
+
}
|
|
356
|
+
function uniqueTags(tasks) {
|
|
357
|
+
return [...new Set(tasks.flatMap((task) => task.tags ?? []))].sort((a, b) => a.localeCompare(b));
|
|
358
|
+
}
|
|
359
|
+
function uniqueEpics(tasks) {
|
|
360
|
+
const byId = /* @__PURE__ */ new Map();
|
|
361
|
+
for (const task of tasks) {
|
|
362
|
+
if (!task.epic) continue;
|
|
363
|
+
const id = `${task.adapterId}:${task.epic.id}`;
|
|
364
|
+
if (!byId.has(id)) byId.set(id, { id, title: task.epic.title });
|
|
365
|
+
}
|
|
366
|
+
return [...byId.values()].sort((a, b) => a.title.localeCompare(b.title));
|
|
367
|
+
}
|
|
368
|
+
function epicFilterId(task) {
|
|
369
|
+
return task.epic ? `${task.adapterId}:${task.epic.id}` : void 0;
|
|
370
|
+
}
|
|
371
|
+
function mergeColumns(configs, visibleColumnIds) {
|
|
372
|
+
const byId = /* @__PURE__ */ new Map();
|
|
373
|
+
for (const config of configs) {
|
|
374
|
+
for (const column of config.columns) {
|
|
375
|
+
if (!visibleColumnIds.has(column.id) || byId.has(column.id)) continue;
|
|
376
|
+
byId.set(column.id, column);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return [...byId.values()];
|
|
380
|
+
}
|
|
381
|
+
function readCachedBoardState(cacheKey) {
|
|
382
|
+
if (typeof window === "undefined") return null;
|
|
383
|
+
try {
|
|
384
|
+
const raw = window.localStorage.getItem(cacheKey);
|
|
385
|
+
if (!raw) return null;
|
|
386
|
+
const parsed = JSON.parse(raw);
|
|
387
|
+
if (!parsed || typeof parsed !== "object" || !parsed.configs || !Array.isArray(parsed.tasks)) return null;
|
|
388
|
+
return {
|
|
389
|
+
configs: parsed.configs,
|
|
390
|
+
tasks: parsed.tasks,
|
|
391
|
+
cachedAt: typeof parsed.cachedAt === "number" ? parsed.cachedAt : 0
|
|
392
|
+
};
|
|
393
|
+
} catch {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function writeCachedBoardState(cacheKey, state) {
|
|
398
|
+
if (typeof window === "undefined") return;
|
|
399
|
+
try {
|
|
400
|
+
window.localStorage.setItem(cacheKey, JSON.stringify({ ...state, cachedAt: Date.now() }));
|
|
401
|
+
} catch {
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function readViewMode(cacheKey) {
|
|
405
|
+
if (typeof window === "undefined") return "kanban";
|
|
406
|
+
try {
|
|
407
|
+
return window.localStorage.getItem(`${cacheKey}:view`) === "list" ? "list" : "kanban";
|
|
408
|
+
} catch {
|
|
409
|
+
return "kanban";
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function writeViewMode(cacheKey, mode) {
|
|
413
|
+
if (typeof window === "undefined") return;
|
|
414
|
+
try {
|
|
415
|
+
window.localStorage.setItem(`${cacheKey}:view`, mode);
|
|
416
|
+
} catch {
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function TaskKanbanBoard({ adapters }) {
|
|
420
|
+
const allAdapterIds = useMemo(() => adapters.map((adapter) => adapter.id), [adapters]);
|
|
421
|
+
const cacheKey = useMemo(() => `boring-tasks:board-cache:v1:${allAdapterIds.join("|")}`, [allAdapterIds]);
|
|
422
|
+
const cachedState = useMemo(() => readCachedBoardState(cacheKey), [cacheKey]);
|
|
423
|
+
const cachedColumnIds = useMemo(
|
|
424
|
+
() => cachedState ? new Set(Object.values(cachedState.configs).flatMap((config) => config.columns.map((column) => column.id))) : /* @__PURE__ */ new Set(),
|
|
425
|
+
[cachedState]
|
|
426
|
+
);
|
|
427
|
+
const [selectedAdapterIds, setSelectedAdapterIds] = useState2(() => new Set(allAdapterIds));
|
|
428
|
+
const [state, setState] = useState2(() => cachedState ? { configs: cachedState.configs, tasks: cachedState.tasks } : null);
|
|
429
|
+
const [visibleColumnIds, setVisibleColumnIds] = useState2(cachedColumnIds);
|
|
430
|
+
const [tagFilter, setTagFilter] = useState2("all");
|
|
431
|
+
const [epicFilter, setEpicFilter] = useState2("all");
|
|
432
|
+
const [loading, setLoading] = useState2(!cachedState);
|
|
433
|
+
const [error, setError] = useState2(null);
|
|
434
|
+
const [activeTaskRef, setActiveTaskRef] = useState2(null);
|
|
435
|
+
const [movingTaskId, setMovingTaskId] = useState2(null);
|
|
436
|
+
const [deletingTaskId, setDeletingTaskId] = useState2(null);
|
|
437
|
+
const [openMenu, setOpenMenu] = useState2(null);
|
|
438
|
+
const [viewMode, setViewModeState] = useState2(() => readViewMode(cacheKey));
|
|
439
|
+
const [collapsedSectionIds, setCollapsedSectionIds] = useState2(/* @__PURE__ */ new Set());
|
|
440
|
+
const requestSeq = useRef(0);
|
|
441
|
+
const toolbarRef = useRef(null);
|
|
442
|
+
const adaptersById = useMemo(() => new Map(adapters.map((adapter) => [adapter.id, adapter])), [adapters]);
|
|
443
|
+
const setViewMode = (mode) => {
|
|
444
|
+
setViewModeState(mode);
|
|
445
|
+
writeViewMode(cacheKey, mode);
|
|
446
|
+
};
|
|
447
|
+
useEffect(() => {
|
|
448
|
+
setSelectedAdapterIds((current) => {
|
|
449
|
+
const next = new Set([...current].filter((id) => adaptersById.has(id)));
|
|
450
|
+
if (next.size === 0) for (const id of allAdapterIds) next.add(id);
|
|
451
|
+
return next;
|
|
452
|
+
});
|
|
453
|
+
}, [adaptersById, allAdapterIds]);
|
|
454
|
+
const load = useCallback(async (options = {}) => {
|
|
455
|
+
if (adapters.length === 0) {
|
|
456
|
+
setState(null);
|
|
457
|
+
setLoading(false);
|
|
458
|
+
setError("No task adapters are registered.");
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const cached = readCachedBoardState(cacheKey);
|
|
462
|
+
const cacheFresh = cached && Date.now() - cached.cachedAt < TASK_BOARD_CACHE_TTL_MS;
|
|
463
|
+
if (cached && !options.force) {
|
|
464
|
+
const columnIds = new Set(Object.values(cached.configs).flatMap((config) => config.columns.map((column) => column.id)));
|
|
465
|
+
setState({ configs: cached.configs, tasks: cached.tasks });
|
|
466
|
+
setVisibleColumnIds((current) => current.size > 0 ? current : columnIds);
|
|
467
|
+
if (cacheFresh) {
|
|
468
|
+
setLoading(false);
|
|
469
|
+
setError(null);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const requestId = requestSeq.current + 1;
|
|
474
|
+
requestSeq.current = requestId;
|
|
475
|
+
setLoading(true);
|
|
476
|
+
setError(null);
|
|
477
|
+
try {
|
|
478
|
+
const entries = await Promise.all(adapters.map(async (adapter) => {
|
|
479
|
+
const [config, tasks2] = await Promise.all([adapter.getBoardConfig(), adapter.listTasks()]);
|
|
480
|
+
return { adapterId: adapter.id, config, tasks: tasks2 };
|
|
481
|
+
}));
|
|
482
|
+
if (requestSeq.current !== requestId) return;
|
|
483
|
+
const configs = Object.fromEntries(entries.map((entry) => [entry.adapterId, entry.config]));
|
|
484
|
+
const tasks = entries.flatMap((entry) => entry.tasks);
|
|
485
|
+
const columnIds = new Set(entries.flatMap((entry) => entry.config.columns.map((column) => column.id)));
|
|
486
|
+
const nextState = { configs, tasks };
|
|
487
|
+
setState(nextState);
|
|
488
|
+
writeCachedBoardState(cacheKey, nextState);
|
|
489
|
+
setVisibleColumnIds(columnIds);
|
|
490
|
+
setTagFilter("all");
|
|
491
|
+
setEpicFilter("all");
|
|
492
|
+
} catch (cause) {
|
|
493
|
+
if (requestSeq.current === requestId) {
|
|
494
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
495
|
+
setState((current) => current ?? null);
|
|
496
|
+
}
|
|
497
|
+
} finally {
|
|
498
|
+
if (requestSeq.current === requestId) setLoading(false);
|
|
499
|
+
}
|
|
500
|
+
}, [adapters, cacheKey]);
|
|
501
|
+
useEffect(() => {
|
|
502
|
+
void load();
|
|
503
|
+
}, [load]);
|
|
504
|
+
useEffect(() => {
|
|
505
|
+
const onPointerDown = (event) => {
|
|
506
|
+
if (!toolbarRef.current?.contains(event.target)) setOpenMenu(null);
|
|
507
|
+
};
|
|
508
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
509
|
+
return () => document.removeEventListener("pointerdown", onPointerDown);
|
|
510
|
+
}, []);
|
|
511
|
+
const selectedTasks = useMemo(() => {
|
|
512
|
+
if (!state) return [];
|
|
513
|
+
return state.tasks.filter((task) => selectedAdapterIds.has(task.adapterId));
|
|
514
|
+
}, [selectedAdapterIds, state]);
|
|
515
|
+
const tags = useMemo(() => uniqueTags(selectedTasks), [selectedTasks]);
|
|
516
|
+
const epics = useMemo(() => uniqueEpics(selectedTasks), [selectedTasks]);
|
|
517
|
+
const selectedConfigs = useMemo(() => {
|
|
518
|
+
if (!state) return [];
|
|
519
|
+
return [...selectedAdapterIds].map((id) => state.configs[id]).filter((config) => Boolean(config));
|
|
520
|
+
}, [selectedAdapterIds, state]);
|
|
521
|
+
const allColumns = useMemo(() => mergeColumns(selectedConfigs, new Set(selectedConfigs.flatMap((config) => config.columns.map((column) => column.id)))), [selectedConfigs]);
|
|
522
|
+
const filteredTasks = useMemo(() => {
|
|
523
|
+
const configuredStatusIds = new Set(allColumns.map((column) => column.id));
|
|
524
|
+
return selectedTasks.filter((task) => {
|
|
525
|
+
if (tagFilter !== "all" && !(task.tags ?? []).includes(tagFilter)) return false;
|
|
526
|
+
if (epicFilter !== "all" && epicFilterId(task) !== epicFilter) return false;
|
|
527
|
+
if (!configuredStatusIds.has(task.statusId)) return true;
|
|
528
|
+
return visibleColumnIds.has(task.statusId);
|
|
529
|
+
});
|
|
530
|
+
}, [allColumns, epicFilter, selectedTasks, tagFilter, visibleColumnIds]);
|
|
531
|
+
const visibleConfig = useMemo(() => {
|
|
532
|
+
if (!state) return null;
|
|
533
|
+
return {
|
|
534
|
+
adapterId: "combined",
|
|
535
|
+
columns: mergeColumns(selectedConfigs, visibleColumnIds)
|
|
536
|
+
};
|
|
537
|
+
}, [selectedConfigs, state, visibleColumnIds]);
|
|
538
|
+
const columns = useMemo(
|
|
539
|
+
() => visibleConfig ? groupTasksByColumn(visibleConfig, filteredTasks) : [],
|
|
540
|
+
[filteredTasks, visibleConfig]
|
|
541
|
+
);
|
|
542
|
+
const handleTaskDragStart = (event, task) => {
|
|
543
|
+
setActiveTaskRef({ taskId: task.id, adapterId: task.adapterId });
|
|
544
|
+
event.dataTransfer.effectAllowed = "move";
|
|
545
|
+
event.dataTransfer.setData("application/x-boring-task-ref", JSON.stringify({ taskId: task.id, adapterId: task.adapterId }));
|
|
546
|
+
event.dataTransfer.setData("application/x-boring-task-id", task.id);
|
|
547
|
+
event.dataTransfer.setData("text/plain", task.number);
|
|
548
|
+
};
|
|
549
|
+
const moveTask = async (taskId, adapterId, statusId) => {
|
|
550
|
+
if (!state) return;
|
|
551
|
+
const task = state.tasks.find((candidate) => candidate.id === taskId && candidate.adapterId === adapterId);
|
|
552
|
+
if (!task || task.statusId === statusId) {
|
|
553
|
+
setActiveTaskRef(null);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const adapter = adaptersById.get(task.adapterId);
|
|
557
|
+
if (!adapter?.capabilities.move || !adapter.moveTask) {
|
|
558
|
+
setActiveTaskRef(null);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const previous = state.tasks;
|
|
562
|
+
const movingAdapterId = task.adapterId;
|
|
563
|
+
setMovingTaskId(taskId);
|
|
564
|
+
setError(null);
|
|
565
|
+
setState((current) => current ? {
|
|
566
|
+
...current,
|
|
567
|
+
tasks: current.tasks.map((candidate) => candidate.id === taskId && candidate.adapterId === adapterId ? { ...candidate, statusId } : candidate)
|
|
568
|
+
} : current);
|
|
569
|
+
try {
|
|
570
|
+
const moved = await adapter.moveTask({ taskId, statusId });
|
|
571
|
+
setState((current) => current ? {
|
|
572
|
+
...current,
|
|
573
|
+
tasks: current.tasks.map((candidate) => candidate.id === taskId && candidate.adapterId === adapterId ? moved : candidate)
|
|
574
|
+
} : current);
|
|
575
|
+
} catch (cause) {
|
|
576
|
+
setState((current) => current ? { ...current, tasks: previous } : current);
|
|
577
|
+
if (selectedAdapterIds.has(movingAdapterId)) {
|
|
578
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
579
|
+
}
|
|
580
|
+
} finally {
|
|
581
|
+
setMovingTaskId(null);
|
|
582
|
+
setActiveTaskRef(null);
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
const deleteTask = async (task) => {
|
|
586
|
+
const adapter = adaptersById.get(task.adapterId);
|
|
587
|
+
if (!adapter?.capabilities.delete || !adapter.deleteTask) {
|
|
588
|
+
setError(`Task source does not support issue deletion: ${task.adapterId}`);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const previous = state?.tasks ?? [];
|
|
592
|
+
setDeletingTaskId(task.id);
|
|
593
|
+
setError(null);
|
|
594
|
+
setState((current) => current ? {
|
|
595
|
+
...current,
|
|
596
|
+
tasks: current.tasks.filter((candidate) => !(candidate.id === task.id && candidate.adapterId === task.adapterId))
|
|
597
|
+
} : current);
|
|
598
|
+
try {
|
|
599
|
+
await adapter.deleteTask({ taskId: task.id });
|
|
600
|
+
} catch (cause) {
|
|
601
|
+
setState((current) => current ? { ...current, tasks: previous } : current);
|
|
602
|
+
if (selectedAdapterIds.has(task.adapterId)) setError(cause instanceof Error ? cause.message : String(cause));
|
|
603
|
+
} finally {
|
|
604
|
+
setDeletingTaskId(null);
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
const toggleColumn = (columnId) => {
|
|
608
|
+
setVisibleColumnIds((current) => {
|
|
609
|
+
const next = new Set(current);
|
|
610
|
+
if (next.has(columnId)) next.delete(columnId);
|
|
611
|
+
else next.add(columnId);
|
|
612
|
+
return next;
|
|
613
|
+
});
|
|
614
|
+
};
|
|
615
|
+
const toggleSource = (adapterId) => {
|
|
616
|
+
setSelectedAdapterIds((current) => {
|
|
617
|
+
const next = new Set(current);
|
|
618
|
+
if (next.has(adapterId)) next.delete(adapterId);
|
|
619
|
+
else next.add(adapterId);
|
|
620
|
+
return next.size === 0 ? current : next;
|
|
621
|
+
});
|
|
622
|
+
};
|
|
623
|
+
const showAllColumns = () => {
|
|
624
|
+
setVisibleColumnIds(new Set(allColumns.map((column) => column.id)));
|
|
625
|
+
};
|
|
626
|
+
const showAllSources = () => {
|
|
627
|
+
setSelectedAdapterIds(new Set(allAdapterIds));
|
|
628
|
+
};
|
|
629
|
+
const toggleSection = (sectionId) => {
|
|
630
|
+
setCollapsedSectionIds((current) => {
|
|
631
|
+
const next = new Set(current);
|
|
632
|
+
if (next.has(sectionId)) next.delete(sectionId);
|
|
633
|
+
else next.add(sectionId);
|
|
634
|
+
return next;
|
|
635
|
+
});
|
|
636
|
+
};
|
|
637
|
+
const selectedCount = selectedAdapterIds.size;
|
|
638
|
+
const visibleCount = visibleColumnIds.size;
|
|
639
|
+
const totalCount = allColumns.length;
|
|
640
|
+
return /* @__PURE__ */ jsxs3("div", { className: "flex h-full min-h-0 flex-col gap-3 p-3", children: [
|
|
641
|
+
/* @__PURE__ */ jsxs3("div", { ref: toolbarRef, className: "flex flex-wrap items-center gap-2 rounded-xl border border-border bg-card/70 p-2 shadow-sm", children: [
|
|
642
|
+
/* @__PURE__ */ jsxs3("div", { className: "relative", children: [
|
|
643
|
+
/* @__PURE__ */ jsxs3(
|
|
644
|
+
"button",
|
|
645
|
+
{
|
|
646
|
+
type: "button",
|
|
647
|
+
className: "flex h-8 items-center rounded-lg border border-border bg-background px-3 text-sm font-medium text-foreground shadow-sm hover:bg-muted",
|
|
648
|
+
onClick: () => setOpenMenu((current) => current === "sources" ? null : "sources"),
|
|
649
|
+
"aria-expanded": openMenu === "sources",
|
|
650
|
+
children: [
|
|
651
|
+
"Sources ",
|
|
652
|
+
selectedCount,
|
|
653
|
+
"/",
|
|
654
|
+
adapters.length
|
|
655
|
+
]
|
|
656
|
+
}
|
|
657
|
+
),
|
|
658
|
+
openMenu === "sources" ? /* @__PURE__ */ jsxs3("div", { className: "absolute left-0 z-20 mt-2 w-72 rounded-xl border border-border bg-popover p-2 text-sm text-popover-foreground shadow-xl", children: [
|
|
659
|
+
/* @__PURE__ */ jsxs3("div", { className: "mb-2 flex items-center justify-between gap-2 border-b border-border pb-2", children: [
|
|
660
|
+
/* @__PURE__ */ jsx3("span", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Task sources" }),
|
|
661
|
+
/* @__PURE__ */ jsx3("button", { type: "button", className: "text-xs font-medium text-primary hover:underline", onClick: showAllSources, children: "All" })
|
|
662
|
+
] }),
|
|
663
|
+
/* @__PURE__ */ jsx3("div", { className: "flex max-h-72 flex-col gap-1 overflow-y-auto", children: adapters.map((adapter) => /* @__PURE__ */ jsxs3("label", { className: "flex items-start gap-2 rounded-lg px-2 py-1.5 hover:bg-muted/70", children: [
|
|
664
|
+
/* @__PURE__ */ jsx3("input", { type: "checkbox", className: "mt-0.5", checked: selectedAdapterIds.has(adapter.id), onChange: () => toggleSource(adapter.id) }),
|
|
665
|
+
/* @__PURE__ */ jsxs3("span", { className: "min-w-0", children: [
|
|
666
|
+
/* @__PURE__ */ jsx3("span", { className: "block truncate font-medium text-foreground", children: adapter.label }),
|
|
667
|
+
/* @__PURE__ */ jsx3("span", { className: "block truncate text-xs text-muted-foreground", children: adapter.description ?? adapter.id })
|
|
668
|
+
] })
|
|
669
|
+
] }, adapter.id)) })
|
|
670
|
+
] }) : null
|
|
671
|
+
] }),
|
|
672
|
+
/* @__PURE__ */ jsxs3("label", { className: "flex items-center gap-2 text-xs font-medium text-muted-foreground", children: [
|
|
673
|
+
"Tag",
|
|
674
|
+
/* @__PURE__ */ jsxs3(
|
|
675
|
+
"select",
|
|
676
|
+
{
|
|
677
|
+
className: "h-8 rounded-lg border border-border bg-background px-2 text-sm text-foreground shadow-sm outline-none focus:border-foreground/40",
|
|
678
|
+
value: tagFilter,
|
|
679
|
+
onChange: (event) => setTagFilter(event.target.value),
|
|
680
|
+
children: [
|
|
681
|
+
/* @__PURE__ */ jsx3("option", { value: "all", children: "All tags" }),
|
|
682
|
+
tags.map((tag) => /* @__PURE__ */ jsx3("option", { value: tag, children: tag }, tag))
|
|
683
|
+
]
|
|
684
|
+
}
|
|
685
|
+
)
|
|
686
|
+
] }),
|
|
687
|
+
/* @__PURE__ */ jsxs3("label", { className: "flex items-center gap-2 text-xs font-medium text-muted-foreground", children: [
|
|
688
|
+
"Epic",
|
|
689
|
+
/* @__PURE__ */ jsxs3(
|
|
690
|
+
"select",
|
|
691
|
+
{
|
|
692
|
+
className: "h-8 rounded-lg border border-border bg-background px-2 text-sm text-foreground shadow-sm outline-none focus:border-foreground/40",
|
|
693
|
+
value: epicFilter,
|
|
694
|
+
onChange: (event) => setEpicFilter(event.target.value),
|
|
695
|
+
children: [
|
|
696
|
+
/* @__PURE__ */ jsx3("option", { value: "all", children: "All epics" }),
|
|
697
|
+
epics.map((epic) => /* @__PURE__ */ jsx3("option", { value: epic.id, children: epic.title }, epic.id))
|
|
698
|
+
]
|
|
699
|
+
}
|
|
700
|
+
)
|
|
701
|
+
] }),
|
|
702
|
+
/* @__PURE__ */ jsxs3("div", { className: "relative", children: [
|
|
703
|
+
/* @__PURE__ */ jsxs3(
|
|
704
|
+
"button",
|
|
705
|
+
{
|
|
706
|
+
type: "button",
|
|
707
|
+
className: "flex h-8 items-center rounded-lg border border-border bg-background px-3 text-sm font-medium text-foreground shadow-sm hover:bg-muted",
|
|
708
|
+
onClick: () => setOpenMenu((current) => current === "columns" ? null : "columns"),
|
|
709
|
+
"aria-expanded": openMenu === "columns",
|
|
710
|
+
children: [
|
|
711
|
+
"Columns ",
|
|
712
|
+
visibleCount,
|
|
713
|
+
"/",
|
|
714
|
+
totalCount
|
|
715
|
+
]
|
|
716
|
+
}
|
|
717
|
+
),
|
|
718
|
+
openMenu === "columns" ? /* @__PURE__ */ jsxs3("div", { className: "absolute left-0 z-20 mt-2 w-64 rounded-xl border border-border bg-popover p-2 text-sm text-popover-foreground shadow-xl", children: [
|
|
719
|
+
/* @__PURE__ */ jsxs3("div", { className: "mb-2 flex items-center justify-between gap-2 border-b border-border pb-2", children: [
|
|
720
|
+
/* @__PURE__ */ jsx3("span", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Visible columns" }),
|
|
721
|
+
/* @__PURE__ */ jsx3("button", { type: "button", className: "text-xs font-medium text-primary hover:underline", onClick: showAllColumns, children: "All" })
|
|
722
|
+
] }),
|
|
723
|
+
/* @__PURE__ */ jsx3("div", { className: "flex max-h-72 flex-col gap-1 overflow-y-auto", children: allColumns.map((column) => /* @__PURE__ */ jsxs3("label", { className: "flex items-start gap-2 rounded-lg px-2 py-1.5 hover:bg-muted/70", children: [
|
|
724
|
+
/* @__PURE__ */ jsx3("input", { type: "checkbox", className: "mt-0.5", checked: visibleColumnIds.has(column.id), onChange: () => toggleColumn(column.id) }),
|
|
725
|
+
/* @__PURE__ */ jsxs3("span", { className: "min-w-0", children: [
|
|
726
|
+
/* @__PURE__ */ jsx3("span", { className: "block truncate font-medium text-foreground", children: column.title }),
|
|
727
|
+
column.description ? /* @__PURE__ */ jsx3("span", { className: "block truncate text-xs text-muted-foreground", children: column.description }) : null
|
|
728
|
+
] })
|
|
729
|
+
] }, column.id)) })
|
|
730
|
+
] }) : null
|
|
731
|
+
] }),
|
|
732
|
+
/* @__PURE__ */ jsx3(
|
|
733
|
+
"button",
|
|
734
|
+
{
|
|
735
|
+
type: "button",
|
|
736
|
+
className: "h-8 rounded-lg border border-border bg-background px-3 text-sm font-medium text-foreground shadow-sm hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50",
|
|
737
|
+
onClick: () => void load({ force: true }),
|
|
738
|
+
disabled: loading,
|
|
739
|
+
children: loading ? "Refreshing\u2026" : "Refresh"
|
|
740
|
+
}
|
|
741
|
+
),
|
|
742
|
+
/* @__PURE__ */ jsxs3("div", { className: "inline-flex h-8 overflow-hidden rounded-lg border border-border bg-background shadow-sm", "aria-label": "Task view mode", children: [
|
|
743
|
+
/* @__PURE__ */ jsx3(
|
|
744
|
+
"button",
|
|
745
|
+
{
|
|
746
|
+
type: "button",
|
|
747
|
+
className: ["grid w-8 place-items-center", viewMode === "kanban" ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted/70 hover:text-foreground"].join(" "),
|
|
748
|
+
onClick: () => setViewMode("kanban"),
|
|
749
|
+
"aria-label": "Show kanban view",
|
|
750
|
+
title: "Kanban view",
|
|
751
|
+
children: /* @__PURE__ */ jsx3(Columns3, { className: "size-3.5", strokeWidth: 1.75 })
|
|
752
|
+
}
|
|
753
|
+
),
|
|
754
|
+
/* @__PURE__ */ jsx3(
|
|
755
|
+
"button",
|
|
756
|
+
{
|
|
757
|
+
type: "button",
|
|
758
|
+
className: ["grid w-8 place-items-center", viewMode === "list" ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted/70 hover:text-foreground"].join(" "),
|
|
759
|
+
onClick: () => setViewMode("list"),
|
|
760
|
+
"aria-label": "Show list view",
|
|
761
|
+
title: "List view",
|
|
762
|
+
children: /* @__PURE__ */ jsx3(List, { className: "size-3.5", strokeWidth: 1.75 })
|
|
763
|
+
}
|
|
764
|
+
)
|
|
765
|
+
] }),
|
|
766
|
+
/* @__PURE__ */ jsxs3("div", { className: "ml-auto flex flex-wrap items-center gap-1.5 text-xs text-muted-foreground", children: [
|
|
767
|
+
/* @__PURE__ */ jsx3("span", { className: "rounded-full border border-border bg-muted/40 px-2 py-1", children: adapterSummary(adapters, selectedCount) }),
|
|
768
|
+
movingTaskId ? /* @__PURE__ */ jsx3("span", { className: "rounded-full border border-border bg-muted/40 px-2 py-1", children: "Moving\u2026" }) : null,
|
|
769
|
+
deletingTaskId ? /* @__PURE__ */ jsx3("span", { className: "rounded-full border border-border bg-muted/40 px-2 py-1", children: "Deleting\u2026" }) : null
|
|
770
|
+
] })
|
|
771
|
+
] }),
|
|
772
|
+
error ? /* @__PURE__ */ jsx3("div", { className: "rounded-xl border border-destructive/40 bg-destructive/10 p-3 text-sm text-destructive", children: error }) : null,
|
|
773
|
+
/* @__PURE__ */ jsx3("div", { className: "min-h-0 flex-1 overflow-hidden", children: loading && !state ? /* @__PURE__ */ jsx3("div", { className: "grid h-full place-items-center rounded-2xl border border-dashed border-border text-sm text-muted-foreground", children: "Loading task board\u2026" }) : columns.length === 0 ? /* @__PURE__ */ jsx3("div", { className: "grid h-full place-items-center rounded-2xl border border-dashed border-border text-sm text-muted-foreground", children: "No tasks match the current filters." }) : viewMode === "list" ? /* @__PURE__ */ jsx3("div", { className: "boring-scrollbar-discreet flex h-full flex-col gap-3 overflow-y-auto pr-1", children: columns.map((column) => {
|
|
774
|
+
const collapsed = collapsedSectionIds.has(column.id);
|
|
775
|
+
return /* @__PURE__ */ jsxs3(
|
|
776
|
+
"section",
|
|
777
|
+
{
|
|
778
|
+
className: ["rounded-2xl border bg-muted/20", column.unmapped ? "border-dashed border-amber-400/50" : "border-border/80"].join(" "),
|
|
779
|
+
children: [
|
|
780
|
+
/* @__PURE__ */ jsxs3(
|
|
781
|
+
"button",
|
|
782
|
+
{
|
|
783
|
+
type: "button",
|
|
784
|
+
className: "flex w-full items-center justify-between gap-3 border-b border-border/70 p-3 text-left hover:bg-muted/50",
|
|
785
|
+
onClick: () => toggleSection(column.id),
|
|
786
|
+
"aria-expanded": !collapsed,
|
|
787
|
+
children: [
|
|
788
|
+
/* @__PURE__ */ jsxs3("span", { className: "flex min-w-0 items-start gap-2", children: [
|
|
789
|
+
column.color ? /* @__PURE__ */ jsx3("span", { className: "mt-1.5 h-2 w-2 shrink-0 rounded-full", style: { backgroundColor: column.color }, "aria-hidden": "true" }) : null,
|
|
790
|
+
/* @__PURE__ */ jsxs3("span", { className: "min-w-0", children: [
|
|
791
|
+
/* @__PURE__ */ jsxs3("span", { className: "block truncate text-sm font-semibold text-foreground", children: [
|
|
792
|
+
collapsed ? "\u25B8" : "\u25BE",
|
|
793
|
+
" ",
|
|
794
|
+
column.title
|
|
795
|
+
] }),
|
|
796
|
+
column.description ? /* @__PURE__ */ jsx3("span", { className: "mt-0.5 block line-clamp-2 text-[11px] leading-4 text-muted-foreground", children: column.description }) : null
|
|
797
|
+
] })
|
|
798
|
+
] }),
|
|
799
|
+
/* @__PURE__ */ jsx3("span", { className: "shrink-0 rounded-full border border-border bg-background px-2 py-0.5 text-xs tabular-nums text-muted-foreground", children: column.tasks.length })
|
|
800
|
+
]
|
|
801
|
+
}
|
|
802
|
+
),
|
|
803
|
+
!collapsed ? /* @__PURE__ */ jsx3("div", { className: "flex flex-col gap-2 p-2", children: column.tasks.length === 0 ? /* @__PURE__ */ jsx3("div", { className: "rounded-xl border border-dashed border-border/80 p-3 text-center text-xs text-muted-foreground", children: "No tasks" }) : column.tasks.map((task) => /* @__PURE__ */ jsx3(
|
|
804
|
+
TaskCard,
|
|
805
|
+
{
|
|
806
|
+
task,
|
|
807
|
+
draggable: false,
|
|
808
|
+
unmapped: column.unmapped,
|
|
809
|
+
compact: true,
|
|
810
|
+
deleteEnabled: Boolean(adaptersById.get(task.adapterId)?.capabilities.delete && adaptersById.get(task.adapterId)?.deleteTask),
|
|
811
|
+
onDelete: (task2) => void deleteTask(task2),
|
|
812
|
+
onDragStart: handleTaskDragStart,
|
|
813
|
+
onDragEnd: () => setActiveTaskRef(null)
|
|
814
|
+
},
|
|
815
|
+
`${task.adapterId}:${task.id}`
|
|
816
|
+
)) }) : null
|
|
817
|
+
]
|
|
818
|
+
},
|
|
819
|
+
column.id
|
|
820
|
+
);
|
|
821
|
+
}) }) : /* @__PURE__ */ jsx3("div", { className: "flex h-full gap-3 overflow-x-auto pb-2", children: columns.map((column) => /* @__PURE__ */ jsx3(
|
|
822
|
+
TaskKanbanColumn,
|
|
823
|
+
{
|
|
824
|
+
column,
|
|
825
|
+
moveEnabled: true,
|
|
826
|
+
activeTaskRef,
|
|
827
|
+
onTaskDragStart: handleTaskDragStart,
|
|
828
|
+
onTaskDragEnd: () => setActiveTaskRef(null),
|
|
829
|
+
onTaskDrop: (taskId, adapterId, statusId) => void moveTask(taskId, adapterId, statusId),
|
|
830
|
+
onTaskDelete: (task) => void deleteTask(task),
|
|
831
|
+
canDragTask: (task) => Boolean(adaptersById.get(task.adapterId)?.capabilities.move && adaptersById.get(task.adapterId)?.moveTask),
|
|
832
|
+
canDeleteTask: (task) => Boolean(adaptersById.get(task.adapterId)?.capabilities.delete && adaptersById.get(task.adapterId)?.deleteTask)
|
|
833
|
+
},
|
|
834
|
+
column.id
|
|
835
|
+
)) }) })
|
|
836
|
+
] });
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// src/front/TasksOverlay.tsx
|
|
840
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
841
|
+
function TasksGlyph({ className }) {
|
|
842
|
+
return /* @__PURE__ */ jsxs4("svg", { className, viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: [
|
|
843
|
+
/* @__PURE__ */ jsx4("path", { d: "M7 7h10M7 12h10M7 17h6", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round" }),
|
|
844
|
+
/* @__PURE__ */ jsx4("path", { d: "M4.75 6.9l.45.45 1.05-1.2M4.75 11.9l.45.45 1.05-1.2M4.75 16.9l.45.45 1.05-1.2", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round" })
|
|
845
|
+
] });
|
|
846
|
+
}
|
|
847
|
+
function TasksOverlay({ onClose }) {
|
|
848
|
+
const { headerInsetStart, headerInsetEnd } = useAppLeftOverlayChrome();
|
|
849
|
+
const pluginClient = useWorkspacePluginClient2();
|
|
850
|
+
const [httpAdapters, setHttpAdapters] = useState3(null);
|
|
851
|
+
useEffect2(() => {
|
|
852
|
+
let cancelled = false;
|
|
853
|
+
const loadSources = async () => {
|
|
854
|
+
try {
|
|
855
|
+
const sources = await listHttpTaskSources(pluginClient);
|
|
856
|
+
if (!cancelled) setHttpAdapters(sources.map((source) => createHttpTaskAdapter(source, pluginClient)));
|
|
857
|
+
} catch {
|
|
858
|
+
if (!cancelled) setHttpAdapters([]);
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
void loadSources();
|
|
862
|
+
return () => {
|
|
863
|
+
cancelled = true;
|
|
864
|
+
};
|
|
865
|
+
}, [pluginClient]);
|
|
866
|
+
const adapters = useMemo2(() => {
|
|
867
|
+
if (httpAdapters === null) return null;
|
|
868
|
+
return httpAdapters;
|
|
869
|
+
}, [httpAdapters]);
|
|
870
|
+
return /* @__PURE__ */ jsxs4("div", { "data-boring-workspace-part": "tasks-overlay", className: "flex h-full min-h-0 flex-col bg-background", children: [
|
|
871
|
+
/* @__PURE__ */ jsxs4(
|
|
872
|
+
"header",
|
|
873
|
+
{
|
|
874
|
+
className: [
|
|
875
|
+
"flex h-12 shrink-0 items-center justify-between border-b border-border/60",
|
|
876
|
+
headerInsetStart ? "pl-12" : "pl-4",
|
|
877
|
+
headerInsetEnd ? "pr-16" : "pr-4"
|
|
878
|
+
].join(" "),
|
|
879
|
+
children: [
|
|
880
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex min-w-0 items-center gap-2", children: [
|
|
881
|
+
/* @__PURE__ */ jsx4("span", { className: "grid size-7 place-items-center rounded-lg bg-primary/10 text-primary", children: /* @__PURE__ */ jsx4(TasksGlyph, { className: "size-4" }) }),
|
|
882
|
+
/* @__PURE__ */ jsxs4("div", { className: "min-w-0", children: [
|
|
883
|
+
/* @__PURE__ */ jsx4("h2", { className: "truncate text-sm font-semibold tracking-tight text-foreground", children: "Tasks" }),
|
|
884
|
+
/* @__PURE__ */ jsx4("p", { className: "truncate text-xs text-muted-foreground", children: "Adapter-mapped Kanban board" })
|
|
885
|
+
] })
|
|
886
|
+
] }),
|
|
887
|
+
/* @__PURE__ */ jsx4("div", { className: "flex shrink-0 items-center gap-0.5", children: /* @__PURE__ */ jsx4(
|
|
888
|
+
IconButton,
|
|
889
|
+
{
|
|
890
|
+
type: "button",
|
|
891
|
+
variant: "ghost",
|
|
892
|
+
size: "icon-xs",
|
|
893
|
+
onClick: onClose,
|
|
894
|
+
"aria-label": "Close tasks",
|
|
895
|
+
title: "Close",
|
|
896
|
+
className: "text-muted-foreground hover:text-foreground",
|
|
897
|
+
children: /* @__PURE__ */ jsx4(X, { className: "size-3", strokeWidth: 1.75 })
|
|
898
|
+
}
|
|
899
|
+
) })
|
|
900
|
+
]
|
|
901
|
+
}
|
|
902
|
+
),
|
|
903
|
+
adapters ? /* @__PURE__ */ jsx4(TaskKanbanBoard, { adapters }) : /* @__PURE__ */ jsx4("div", { className: "grid min-h-0 flex-1 place-items-center p-4 text-sm text-muted-foreground", children: "Loading task sources\u2026" })
|
|
904
|
+
] });
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// src/front/githubIssuesAdapter.ts
|
|
908
|
+
var GITHUB_COLUMNS = [
|
|
909
|
+
{ id: "needs-triage", title: "Needs triage", description: "Fresh issues that need a first pass", color: "#8b5cf6" },
|
|
910
|
+
{ id: "needs-info", title: "Needs info", description: "Blocked on clarification or missing context", color: "#ef4444" },
|
|
911
|
+
{ id: "ready-for-agent", title: "Ready for agent", description: "Clear agent-pickable work", color: "#0ea5e9" },
|
|
912
|
+
{ id: "ready-for-human", title: "Ready for human", description: "Waiting for owner review or human decision", color: "#f59e0b" },
|
|
913
|
+
{ id: "done", title: "Done", description: "Closed GitHub issues", color: "#64748b", acceptsDrop: false }
|
|
914
|
+
];
|
|
915
|
+
var WORKFLOW_LABELS = ["needs-triage", "needs-info", "ready-for-agent", "ready-for-human", "done"];
|
|
916
|
+
function issueLabels(issue) {
|
|
917
|
+
return (issue.labels ?? []).map((label) => label.name?.trim()).filter((label) => Boolean(label));
|
|
918
|
+
}
|
|
919
|
+
function issueStatus(issue) {
|
|
920
|
+
if (issue.state === "closed") return "done";
|
|
921
|
+
const labels = issueLabels(issue).map((label) => label.toLowerCase());
|
|
922
|
+
return WORKFLOW_LABELS.find((label) => labels.includes(label)) ?? "needs-triage";
|
|
923
|
+
}
|
|
924
|
+
function descriptionFromBody(body) {
|
|
925
|
+
if (!body) return void 0;
|
|
926
|
+
const compact = body.replace(/```[\s\S]*?```/g, " ").replace(/`([^`]+)`/g, "$1").replace(/[#>*_\-[\]()]/g, " ").replace(/\s+/g, " ").trim();
|
|
927
|
+
return compact.length > 180 ? `${compact.slice(0, 177)}\u2026` : compact || void 0;
|
|
928
|
+
}
|
|
929
|
+
function associatedPullRequests(issue, pullRequests) {
|
|
930
|
+
const issueRef = `#${issue.number}`;
|
|
931
|
+
const issueUrl = issue.html_url?.toLowerCase();
|
|
932
|
+
return pullRequests.filter((pr) => {
|
|
933
|
+
const haystack = `${pr.title}
|
|
934
|
+
${pr.body ?? ""}
|
|
935
|
+
${pr.html_url ?? ""}`.toLowerCase();
|
|
936
|
+
return haystack.includes(issueRef.toLowerCase()) || Boolean(issueUrl && haystack.includes(issueUrl));
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
function taskFromIssue(issue, adapterId, pullRequests = []) {
|
|
940
|
+
const prs = associatedPullRequests(issue, pullRequests);
|
|
941
|
+
return {
|
|
942
|
+
id: String(issue.id),
|
|
943
|
+
number: `#${issue.number}`,
|
|
944
|
+
title: issue.title,
|
|
945
|
+
description: descriptionFromBody(issue.body),
|
|
946
|
+
statusId: issueStatus(issue),
|
|
947
|
+
tags: issueLabels(issue).filter((label) => !WORKFLOW_LABELS.includes(label.toLowerCase())),
|
|
948
|
+
epic: issue.milestone ? {
|
|
949
|
+
id: String(issue.milestone.id),
|
|
950
|
+
title: issue.milestone.title,
|
|
951
|
+
url: issue.milestone.html_url
|
|
952
|
+
} : void 0,
|
|
953
|
+
adapterId,
|
|
954
|
+
pullRequests: prs.map((pr) => ({
|
|
955
|
+
id: String(pr.id),
|
|
956
|
+
number: `#${pr.number}`,
|
|
957
|
+
title: pr.title,
|
|
958
|
+
url: pr.html_url,
|
|
959
|
+
state: pr.state
|
|
960
|
+
})),
|
|
961
|
+
url: issue.html_url
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
function createGitHubIssuesAdapter({ owner, repo, limit = 200, state = "open", moveIssue }) {
|
|
965
|
+
const adapterId = `github:${owner}/${repo}`;
|
|
966
|
+
const board = {
|
|
967
|
+
adapterId,
|
|
968
|
+
defaultColumnId: "needs-triage",
|
|
969
|
+
columns: GITHUB_COLUMNS
|
|
970
|
+
};
|
|
971
|
+
const taskCache = /* @__PURE__ */ new Map();
|
|
972
|
+
const issueNumberByTaskId = /* @__PURE__ */ new Map();
|
|
973
|
+
return {
|
|
974
|
+
id: adapterId,
|
|
975
|
+
label: `GitHub ${owner}/${repo}`,
|
|
976
|
+
description: moveIssue ? "GitHub Issues adapter; Boring v2 labels drive columns" : "Read-only GitHub Issues adapter; Boring v2 labels drive columns",
|
|
977
|
+
capabilities: { move: Boolean(moveIssue) },
|
|
978
|
+
getBoardConfig: () => board,
|
|
979
|
+
async listTasks() {
|
|
980
|
+
const params = new URLSearchParams({
|
|
981
|
+
state,
|
|
982
|
+
per_page: String(Math.min(Math.max(limit, 1), 100)),
|
|
983
|
+
sort: "updated",
|
|
984
|
+
direction: "desc"
|
|
985
|
+
});
|
|
986
|
+
const issues = [];
|
|
987
|
+
const maxPages = Math.ceil(Math.min(Math.max(limit, 1), 300) / 100);
|
|
988
|
+
for (let page = 1; page <= maxPages; page += 1) {
|
|
989
|
+
params.set("page", String(page));
|
|
990
|
+
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/issues?${params.toString()}`, {
|
|
991
|
+
headers: { Accept: "application/vnd.github+json" }
|
|
992
|
+
});
|
|
993
|
+
if (!response.ok) {
|
|
994
|
+
throw new Error(`GitHub issues request failed (${response.status})`);
|
|
995
|
+
}
|
|
996
|
+
const pageIssues = await response.json();
|
|
997
|
+
issues.push(...pageIssues);
|
|
998
|
+
if (pageIssues.length < Number(params.get("per_page"))) break;
|
|
999
|
+
}
|
|
1000
|
+
const tasks = issues.filter((issue) => !issue.pull_request).map((issue) => {
|
|
1001
|
+
const task = taskFromIssue(issue, adapterId);
|
|
1002
|
+
taskCache.set(task.id, task);
|
|
1003
|
+
issueNumberByTaskId.set(task.id, issue.number);
|
|
1004
|
+
return task;
|
|
1005
|
+
});
|
|
1006
|
+
return tasks;
|
|
1007
|
+
},
|
|
1008
|
+
moveTask: moveIssue ? async ({ taskId, statusId }) => {
|
|
1009
|
+
const issueNumber = issueNumberByTaskId.get(taskId);
|
|
1010
|
+
const cached = taskCache.get(taskId);
|
|
1011
|
+
if (!issueNumber || !cached) throw new Error("GitHub issue is not loaded; refresh tasks and try again.");
|
|
1012
|
+
const moved = await moveIssue({ owner, repo, issueNumber, statusId });
|
|
1013
|
+
const next = moved ?? { ...cached, statusId };
|
|
1014
|
+
taskCache.set(taskId, next);
|
|
1015
|
+
return next;
|
|
1016
|
+
} : void 0
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// src/front/mockAdapter.ts
|
|
1021
|
+
var mockBoardConfig = {
|
|
1022
|
+
adapterId: "mock",
|
|
1023
|
+
defaultColumnId: "needs-triage",
|
|
1024
|
+
columns: [
|
|
1025
|
+
{ id: "needs-triage", title: "Needs triage", description: "Not evaluated yet", color: "#d4c5f9" },
|
|
1026
|
+
{ id: "needs-info", title: "Needs info", description: "Waiting on specific answers", color: "#f9d0c4" },
|
|
1027
|
+
{ id: "ready-for-agent", title: "Ready for agent", description: "Agent can plan or implement safely", color: "#0e8a16" },
|
|
1028
|
+
{ id: "ready-for-human", title: "Ready for human", description: "Human judgment, access, approval, review, or merge needed", color: "#f9a825" },
|
|
1029
|
+
{ id: "done", title: "Done", description: "Completed or closed", color: "#22c55e" }
|
|
1030
|
+
]
|
|
1031
|
+
};
|
|
1032
|
+
var mockTasks = [
|
|
1033
|
+
{
|
|
1034
|
+
id: "task-101",
|
|
1035
|
+
number: "TASK-101",
|
|
1036
|
+
title: "Standard task card contract",
|
|
1037
|
+
description: "Keep number, title, description, and status small enough for every adapter to map into.",
|
|
1038
|
+
statusId: "needs-triage",
|
|
1039
|
+
tags: ["contract", "adapter"],
|
|
1040
|
+
epic: { id: "adapter-platform", title: "Adapter platform" },
|
|
1041
|
+
adapterId: "mock"
|
|
1042
|
+
},
|
|
1043
|
+
{
|
|
1044
|
+
id: "task-124",
|
|
1045
|
+
number: "TASK-124",
|
|
1046
|
+
title: "Adapter-supplied columns",
|
|
1047
|
+
description: "Columns are data from the adapter; the board does not hardcode Todo/In Progress/Done.",
|
|
1048
|
+
statusId: "ready-for-agent",
|
|
1049
|
+
tags: ["columns", "config"],
|
|
1050
|
+
epic: { id: "board-ux", title: "Board UX" },
|
|
1051
|
+
adapterId: "mock"
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
id: "task-138",
|
|
1055
|
+
number: "TASK-138",
|
|
1056
|
+
title: "Drag status through adapter boundary",
|
|
1057
|
+
description: "A drop calls moveTask(taskId, statusId). The adapter decides what that means for GitHub, Linear, Kata, or DB tasks.",
|
|
1058
|
+
statusId: "ready-for-agent",
|
|
1059
|
+
tags: ["drag-drop", "action-loop"],
|
|
1060
|
+
epic: { id: "adapter-platform", title: "Adapter platform" },
|
|
1061
|
+
adapterId: "mock"
|
|
1062
|
+
},
|
|
1063
|
+
{
|
|
1064
|
+
id: "task-147",
|
|
1065
|
+
number: "TASK-147",
|
|
1066
|
+
title: "No tracker-specific UI actions",
|
|
1067
|
+
description: "Create, close, comment, and assign belong to adapter capabilities, not hardcoded board buttons.",
|
|
1068
|
+
statusId: "ready-for-human",
|
|
1069
|
+
tags: ["actions", "capabilities"],
|
|
1070
|
+
epic: { id: "adapter-platform", title: "Adapter platform" },
|
|
1071
|
+
adapterId: "mock"
|
|
1072
|
+
},
|
|
1073
|
+
{
|
|
1074
|
+
id: "task-152",
|
|
1075
|
+
number: "TASK-152",
|
|
1076
|
+
title: "Unmapped statuses never disappear",
|
|
1077
|
+
description: "If a task status has no matching column, render it in a safe non-droppable overflow lane.",
|
|
1078
|
+
statusId: "external-new-state",
|
|
1079
|
+
tags: ["unmapped"],
|
|
1080
|
+
epic: { id: "board-ux", title: "Board UX" },
|
|
1081
|
+
adapterId: "mock"
|
|
1082
|
+
}
|
|
1083
|
+
];
|
|
1084
|
+
function createMockTaskAdapter(initialTasks = mockTasks) {
|
|
1085
|
+
let tasks = initialTasks.map((task) => ({ ...task }));
|
|
1086
|
+
return {
|
|
1087
|
+
id: "mock",
|
|
1088
|
+
label: "Mock tasks",
|
|
1089
|
+
description: "Local demo adapter for the generic Kanban UI",
|
|
1090
|
+
capabilities: { move: true, delete: true },
|
|
1091
|
+
getBoardConfig: () => mockBoardConfig,
|
|
1092
|
+
listTasks: () => tasks.map((task) => ({ ...task })),
|
|
1093
|
+
moveTask: ({ taskId, statusId }) => {
|
|
1094
|
+
const next = tasks.map((task) => task.id === taskId ? { ...task, statusId } : task);
|
|
1095
|
+
const moved = next.find((task) => task.id === taskId);
|
|
1096
|
+
if (!moved) throw new Error(`Task not found: ${taskId}`);
|
|
1097
|
+
tasks = next;
|
|
1098
|
+
return { ...moved };
|
|
1099
|
+
},
|
|
1100
|
+
deleteTask: ({ taskId }) => {
|
|
1101
|
+
if (!tasks.some((task) => task.id === taskId)) throw new Error(`Task not found: ${taskId}`);
|
|
1102
|
+
tasks = tasks.filter((task) => task.id !== taskId);
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// src/front/index.tsx
|
|
1108
|
+
function createTasksPlugin() {
|
|
1109
|
+
return definePlugin({
|
|
1110
|
+
id: TASKS_PLUGIN_ID,
|
|
1111
|
+
label: TASKS_PLUGIN_LABEL,
|
|
1112
|
+
appLeftActions: [
|
|
1113
|
+
{
|
|
1114
|
+
id: "tasks",
|
|
1115
|
+
label: TASKS_PLUGIN_LABEL,
|
|
1116
|
+
icon: TasksGlyph,
|
|
1117
|
+
overlay: TasksOverlay,
|
|
1118
|
+
order: 40
|
|
1119
|
+
}
|
|
1120
|
+
]
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
var tasksPlugin = createTasksPlugin();
|
|
1124
|
+
var front_default = tasksPlugin;
|
|
1125
|
+
export {
|
|
1126
|
+
TaskKanbanBoard,
|
|
1127
|
+
TasksOverlay,
|
|
1128
|
+
createGitHubIssuesAdapter,
|
|
1129
|
+
createHttpTaskAdapter,
|
|
1130
|
+
createMockTaskAdapter,
|
|
1131
|
+
createTasksPlugin,
|
|
1132
|
+
front_default as default,
|
|
1133
|
+
listHttpTaskSources
|
|
1134
|
+
};
|