@nestos/kanban 0.0.0-stage → 0.5.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.
Files changed (99) hide show
  1. package/README.md +121 -2
  2. package/dist/activity-C6xDM-cg.d.cts +232 -0
  3. package/dist/activity-Dw8DOw0L.d.ts +232 -0
  4. package/dist/activity.cjs +288 -0
  5. package/dist/activity.cjs.map +1 -0
  6. package/dist/activity.d.cts +34 -0
  7. package/dist/activity.d.ts +34 -0
  8. package/dist/activity.js +288 -0
  9. package/dist/activity.js.map +1 -0
  10. package/dist/chunk-2YFPV5GF.cjs +16 -0
  11. package/dist/chunk-2YFPV5GF.cjs.map +1 -0
  12. package/dist/chunk-4KF7KUAI.cjs +221 -0
  13. package/dist/chunk-4KF7KUAI.cjs.map +1 -0
  14. package/dist/chunk-4XF6LQSU.cjs +9 -0
  15. package/dist/chunk-4XF6LQSU.cjs.map +1 -0
  16. package/dist/chunk-5DLZWW5Y.js +148 -0
  17. package/dist/chunk-5DLZWW5Y.js.map +1 -0
  18. package/dist/chunk-5XKOCKY2.js +58 -0
  19. package/dist/chunk-5XKOCKY2.js.map +1 -0
  20. package/dist/chunk-6MN3H3YF.cjs +183 -0
  21. package/dist/chunk-6MN3H3YF.cjs.map +1 -0
  22. package/dist/chunk-ATUFFNLV.js +221 -0
  23. package/dist/chunk-ATUFFNLV.js.map +1 -0
  24. package/dist/chunk-DFTOTBGF.js +439 -0
  25. package/dist/chunk-DFTOTBGF.js.map +1 -0
  26. package/dist/chunk-G6FCDLDF.js +16 -0
  27. package/dist/chunk-G6FCDLDF.js.map +1 -0
  28. package/dist/chunk-IUI5CI2S.cjs +439 -0
  29. package/dist/chunk-IUI5CI2S.cjs.map +1 -0
  30. package/dist/chunk-KYNYPKKH.cjs +71 -0
  31. package/dist/chunk-KYNYPKKH.cjs.map +1 -0
  32. package/dist/chunk-L44TM4EE.cjs +1498 -0
  33. package/dist/chunk-L44TM4EE.cjs.map +1 -0
  34. package/dist/chunk-LSE6JG5I.cjs +20 -0
  35. package/dist/chunk-LSE6JG5I.cjs.map +1 -0
  36. package/dist/chunk-O4OGAAV4.js +71 -0
  37. package/dist/chunk-O4OGAAV4.js.map +1 -0
  38. package/dist/chunk-OHQTKGQN.js +1498 -0
  39. package/dist/chunk-OHQTKGQN.js.map +1 -0
  40. package/dist/chunk-PSKQAFDF.cjs +58 -0
  41. package/dist/chunk-PSKQAFDF.cjs.map +1 -0
  42. package/dist/chunk-QTMTBEBA.js +20 -0
  43. package/dist/chunk-QTMTBEBA.js.map +1 -0
  44. package/dist/chunk-RQP77ZTY.js +9 -0
  45. package/dist/chunk-RQP77ZTY.js.map +1 -0
  46. package/dist/chunk-T7P5EIC4.cjs +148 -0
  47. package/dist/chunk-T7P5EIC4.cjs.map +1 -0
  48. package/dist/chunk-TB2RZGDV.js +183 -0
  49. package/dist/chunk-TB2RZGDV.js.map +1 -0
  50. package/dist/content-CAIp1njn.d.cts +35 -0
  51. package/dist/content-CGMJdyH_.d.ts +35 -0
  52. package/dist/contracts/index.cjs +21 -0
  53. package/dist/contracts/index.cjs.map +1 -0
  54. package/dist/contracts/index.d.cts +4 -0
  55. package/dist/contracts/index.d.ts +4 -0
  56. package/dist/contracts/index.js +21 -0
  57. package/dist/contracts/index.js.map +1 -0
  58. package/dist/discovery-DxpirOz3.d.cts +61 -0
  59. package/dist/discovery-gSqWpuRm.d.ts +61 -0
  60. package/dist/discovery.cjs +379 -0
  61. package/dist/discovery.cjs.map +1 -0
  62. package/dist/discovery.d.cts +22 -0
  63. package/dist/discovery.d.ts +22 -0
  64. package/dist/discovery.js +379 -0
  65. package/dist/discovery.js.map +1 -0
  66. package/dist/execution.cjs +368 -0
  67. package/dist/execution.cjs.map +1 -0
  68. package/dist/execution.d.cts +89 -0
  69. package/dist/execution.d.ts +89 -0
  70. package/dist/execution.js +368 -0
  71. package/dist/execution.js.map +1 -0
  72. package/dist/index.cjs +76 -0
  73. package/dist/index.cjs.map +1 -0
  74. package/dist/index.d.cts +61 -0
  75. package/dist/index.d.ts +61 -0
  76. package/dist/index.js +76 -0
  77. package/dist/index.js.map +1 -0
  78. package/dist/module-BbvwdCDF.d.cts +11 -0
  79. package/dist/module-CWNK2BMj.d.ts +11 -0
  80. package/dist/planning-CcFZYdIL.d.cts +844 -0
  81. package/dist/planning-CcFZYdIL.d.ts +844 -0
  82. package/dist/ports-Bn6ro2FB.d.ts +142 -0
  83. package/dist/ports-CaL6geqR.d.cts +142 -0
  84. package/dist/storage.cjs +289 -0
  85. package/dist/storage.cjs.map +1 -0
  86. package/dist/storage.d.cts +71 -0
  87. package/dist/storage.d.ts +71 -0
  88. package/dist/storage.js +289 -0
  89. package/dist/storage.js.map +1 -0
  90. package/dist/store-Ct0gOAAg.d.ts +28 -0
  91. package/dist/store-q63eSMoT.d.cts +28 -0
  92. package/dist/ui/index.cjs +1961 -0
  93. package/dist/ui/index.cjs.map +1 -0
  94. package/dist/ui/index.d.cts +209 -0
  95. package/dist/ui/index.d.ts +209 -0
  96. package/dist/ui/index.js +1961 -0
  97. package/dist/ui/index.js.map +1 -0
  98. package/dist/ui/kanban.css +194 -0
  99. package/package.json +113 -4
@@ -0,0 +1,1961 @@
1
+ import {
2
+ projectKanbanTask
3
+ } from "../chunk-O4OGAAV4.js";
4
+ import {
5
+ DEFAULT_KANBAN_WORK_TYPES,
6
+ executableWorkItem,
7
+ kanbanReference
8
+ } from "../chunk-5XKOCKY2.js";
9
+ import {
10
+ randomUUID
11
+ } from "../chunk-TB2RZGDV.js";
12
+ import "../chunk-QTMTBEBA.js";
13
+
14
+ // src/ui/html.ts
15
+ function escapeHTML(value2) {
16
+ return String(value2 ?? "").replace(/[&<>"']/g, (character) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character]);
17
+ }
18
+ var escapeAttr = escapeHTML;
19
+
20
+ // src/ui/task-state.ts
21
+ function taskStatus(ui, task) {
22
+ if (!task) return "";
23
+ return `<span class="kanban__task-state" data-task-phase="${escapeAttr(task.phase)}">${escapeHTML(ui.text("taskPhase." + task.phase))}${task.reason ? `<small>${escapeHTML(taskReason(ui, task.reason))}</small>` : ""}${task.phase === "waiting-start" && task.reason !== "MANUAL_START_REQUIRED" && task.startAt !== void 0 ? `<time data-task-start-at="${task.startAt}" datetime="${new Date(task.startAt).toISOString()}">${escapeHTML(startTimeText(ui, task.startAt))}</time>` : ""}</span>`;
24
+ }
25
+ function taskControls(ui, task, fixAvailable = false) {
26
+ return task.actions.filter((action) => ["run", "pause", "resume", "edit", "cancel"].includes(action) || action === "fix" && fixAvailable).map((action) => {
27
+ const label = ui.text("taskAction." + action), icon = action === "cancel" ? ui.icons.close : ui.icons[action];
28
+ return `<button type="button" data-action="task-${action}" title="${escapeAttr(label)}" aria-label="${escapeAttr(label)}">${icon ?? escapeHTML(label)}</button>`;
29
+ }).join("");
30
+ }
31
+ function phaseFilter(ui, selected) {
32
+ const phases = ["waiting-start", "analyzing", "queued", "waiting-dependency", "preparing", "running", "pausing", "paused", "resuming", "waiting-input", "error", "review", "completed", "cancelled", "unknown"];
33
+ return `<label>${escapeHTML(ui.text("taskState"))}<select name="phase"><option value="">${escapeHTML(ui.text("all"))}</option>${phases.map((phase) => `<option value="${phase}" ${phase === selected ? "selected" : ""}>${escapeHTML(ui.text("taskPhase." + phase))}</option>`).join("")}</select></label>`;
34
+ }
35
+ function startTimeText(ui, startAt) {
36
+ return ui.text("taskStartAt") + " " + new Date(startAt).toLocaleString() + " \xB7 " + Math.max(0, Math.ceil((startAt - Date.now()) / 1e3)) + "s";
37
+ }
38
+ function updateTaskCountdowns(editor, ui) {
39
+ for (const node of editor.querySelectorAll("[data-task-start-at]")) {
40
+ const at = Number(node.dataset.taskStartAt);
41
+ if (Number.isFinite(at)) node.textContent = startTimeText(ui, at);
42
+ }
43
+ }
44
+ function taskReason(ui, reason) {
45
+ const key = "taskReason." + reason, value2 = ui.text(key);
46
+ return value2 === key || value2 === "kanban." + key ? reason : value2;
47
+ }
48
+
49
+ // src/ui/session-links.ts
50
+ function sessionLink(ui, id, label = id, url) {
51
+ if (!url) return `<button type="button" data-session="${escapeAttr(id)}">${escapeHTML(label)}</button>`;
52
+ return linkPair(ui, url, label, id);
53
+ }
54
+ function linkPair(ui, href, label, sessionId) {
55
+ return `<span class="kanban__session-links"><a href="${escapeAttr(href)}"${sessionId ? ` data-session="${escapeAttr(sessionId)}" title="${escapeAttr(ui.text("openSessionInTab"))}"` : ""}>${escapeHTML(label)}</a></span>`;
56
+ }
57
+
58
+ // src/ui/references.ts
59
+ function referenceMenu(ui, board, entity, actions = "") {
60
+ const reference = kanbanReference(board, entity);
61
+ const path = `/board/${encodeURIComponent(board.id)}${entity ? entity.kind ? "/@record/" + encodeURIComponent(entity.id) : "/" + encodeURIComponent(entity.id) : ""}`;
62
+ return `<details class="kanban__menu" data-popover><summary aria-label="${escapeAttr(ui.text("referenceActions"))}" title="${escapeAttr(ui.text("referenceActions"))}">${ui.icons.more}</summary>
63
+ <div class="kanban__menu-content"><code class="kanban__menu-reference">${escapeHTML(reference)}</code>${actions}<button type="button" data-action="copy-reference" data-copy="${escapeAttr(reference)}">${escapeHTML(ui.text("copyReference"))}</button>
64
+ <button type="button" data-action="copy-link" data-copy="${escapeAttr(path)}">${escapeHTML(ui.text("copyLink"))}</button></div></details>`;
65
+ }
66
+ async function copyReference(ui, button2) {
67
+ const value2 = button2.dataset.copy ?? "";
68
+ const text = button2.dataset.action === "copy-link" ? ui.referenceUrl?.(value2) ?? value2 : value2;
69
+ try {
70
+ await navigator.clipboard.writeText(text);
71
+ if (button2.dataset.action === "copy-resource") {
72
+ button2.title = ui.text("copied");
73
+ button2.setAttribute("aria-label", ui.text("copied"));
74
+ } else button2.textContent = ui.text("copied");
75
+ } catch {
76
+ throw Object.assign(new Error(ui.text("copyFailed")), { code: "COPY_FAILED" });
77
+ }
78
+ }
79
+
80
+ // src/ui/suggestion-field.ts
81
+ function suggestionField(options) {
82
+ return `<div class="kanban__suggest" data-kanban-suggest="${escapeAttr(JSON.stringify(options))}"></div>`;
83
+ }
84
+ function selectedItems(board, ids) {
85
+ return ids.map((id) => ({ id, label: board.items.find((item) => item.id === id)?.title ?? id }));
86
+ }
87
+
88
+ // src/ui/session-view.ts
89
+ function executionBadge(ui, session, sessionUrl) {
90
+ if (!session) return "";
91
+ const resource = session.resourceId ?? session.sessionId;
92
+ return `<span class="kanban__execution" data-execution-state="${escapeAttr(session.state)}" data-stale="${session.stale}"><span>${sessionUrl ? sessionLink(ui, resource, ui.text("executionSession"), sessionUrl(resource)) : escapeHTML(ui.text("executionSession"))} \xB7 ${escapeHTML(ui.text("sessionState." + session.state))}${session.stale ? " \xB7 " + escapeHTML(ui.text("activityStale")) : ""}</span><small>${escapeHTML(activityTime(ui, session))}</small></span>`;
93
+ }
94
+ function activityTime(ui, session) {
95
+ if (session.lastActivityAt === void 0) return ui.text("activityUnknown");
96
+ const minutes = Math.max(0, Math.floor((Date.now() - session.lastActivityAt) / 6e4));
97
+ return ui.text("lastActivity") + " \xB7 " + (minutes < 1 ? ui.text("justNow") : minutes < 60 ? ui.text("minutesAgo").replace("{count}", String(minutes)) : new Date(session.lastActivityAt).toLocaleString());
98
+ }
99
+ function executionSessionView(ui, detail, editable, sessionUrl) {
100
+ const session = detail.current, resource = session?.resourceId ?? session?.sessionId;
101
+ return `<section class="kanban__execution-detail"><h4>${escapeHTML(ui.text("executionSession"))}</h4><p>${escapeHTML(ui.text("executionSessionHelp"))}</p>
102
+ ${detail.continuation ? `<button type="button" data-action="continue-execution" data-dispatch="${escapeAttr(detail.continuation.previousDispatchId)}">${escapeHTML(ui.text("continueExecution"))}</button>` : ""}
103
+ ${session ? `${executionBadge(ui, session)}${sessionLink(ui, resource, session.title ?? session.sessionId, sessionUrl?.(resource))}<small>${escapeHTML(session.sessionId)}</small>${session.observedAt ? `<small>${escapeHTML(ui.text("observedAt"))} \xB7 ${escapeHTML(new Date(session.observedAt).toLocaleString())}</small>` : ""}${session.code ? `<p>${escapeHTML(ui.text("activityReadFailed"))} (${escapeHTML(session.code)})</p>` : ""}` : `<p>${escapeHTML(ui.text("noExecutionSession"))}</p>`}
104
+ ${editable ? `<details><summary>${escapeHTML(ui.text(session ? "changeExecutionSession" : "bindExecutionSession"))}</summary><form data-form="execution-session">${suggestionField({ name: "executionSession", label: "executionSession", kind: "session", manual: true, required: true })}<button>${escapeHTML(ui.text("bindExecutionSession"))}</button><p data-execution-error role="alert" hidden></p></form></details>` : ""}
105
+ </section>`;
106
+ }
107
+ function relatedSessionsView(ui, detail, sessionUrl) {
108
+ const sessions = detail.sessions ?? legacySessions(detail);
109
+ return `${detail.sessionsCode ? `<p role="alert">${escapeHTML(ui.text("sessionRelationsIncomplete"))} (${escapeHTML(detail.sessionsCode)})</p>` : ""}
110
+ ${sessions.length ? `<ul class="kanban__session-list">${sessions.map((session) => relatedSession(ui, session, sessionUrl)).join("")}</ul>` : `<p>${escapeHTML(ui.text("noRelatedSessions"))}</p>`}`;
111
+ }
112
+ function relatedSession(ui, session, sessionUrl) {
113
+ const resource = session.resourceId ?? session.sessionId, title = session.title ?? session.sessionId;
114
+ return `<li class="kanban__related-session" data-related-session="${escapeAttr(session.sessionId)}"><div class="kanban__session-heading" title="${escapeAttr(title)}">${sessionLink(ui, resource, title, sessionUrl?.(resource))}</div>
115
+ <small>${escapeHTML(session.sessionId)}</small>${session.createdAt !== void 0 ? `<small>${escapeHTML(ui.text("sessionCreatedAt"))} \xB7 ${escapeHTML(date(session.createdAt))}</small>` : ""}<p>${session.current ? `<strong>${escapeHTML(ui.text("currentExecution"))}</strong> \xB7 ` : ""}${session.purposes.map((purpose) => escapeHTML(ui.text("sessionPurpose." + purpose))).join(" \xB7 ")} \xB7 ${escapeHTML(ui.text("sessionState." + session.state))}${session.stale ? " \xB7 " + escapeHTML(ui.text("activityStale")) : ""}</p>
116
+ ${session.code ? `<p role="alert">${escapeHTML(ui.text("activityReadFailed"))} (${escapeHTML(session.code)})</p>` : ""}
117
+ ${session.executions.length ? `<details><summary>${escapeHTML(ui.text("sessionExecutions"))} (${session.executions.length})</summary><ol>${session.executions.map((run) => executionView(ui, run)).join("")}</ol></details>` : ""}</li>`;
118
+ }
119
+ function executionView(ui, run) {
120
+ const label = run.previousDispatchId ? ui.text("sessionContinuation") : ui.text("sessionPurpose." + run.purpose);
121
+ const showState = run.state && run.state !== run.outcome && !(run.state === "completed" && ["succeeded", "accepted", "rejected"].includes(run.outcome ?? ""));
122
+ const fields = [
123
+ [ui.text("sessionExecutionId"), run.id],
124
+ [ui.text("sessionRoundId"), run.roundId],
125
+ [ui.text("sessionTurnId"), run.turnId],
126
+ [ui.text("sessionPreviousExecution"), run.previousDispatchId],
127
+ [ui.text(run.roundId ? "sessionExecutionStarted" : "sessionExecutionPrepared"), date(run.createdAt)],
128
+ [ui.text("sessionExecutionEnded"), date(run.completedAt)],
129
+ ...run.resources?.map((resource) => [resource.label, resource.value]) ?? []
130
+ ];
131
+ return `<li class="kanban__session-run"><p>${escapeHTML(label)}${run.attempt ? " \xB7 #" + run.attempt : ""}${run.outcome ? " \xB7 " + escapeHTML(ui.text("sessionRunOutcome." + run.outcome)) : ""}${showState ? " \xB7 " + escapeHTML(ui.text("sessionState." + run.state)) : ""}</p>
132
+ <dl>${fields.filter((field2) => field2[1] !== void 0).map(([key, value2]) => `<dt>${escapeHTML(key)}</dt><dd>${escapeHTML(value2)}</dd>`).join("")}</dl>${run.code ? `<p role="alert">${escapeHTML(run.code)}</p>` : ""}</li>`;
133
+ }
134
+ function date(value2) {
135
+ return value2 === void 0 ? void 0 : new Date(value2).toLocaleString();
136
+ }
137
+ function legacySessions(detail) {
138
+ return [...new Map(detail.history.map((row) => [row.sessionId, row])).values()].map((row) => ({
139
+ ...detail.current?.sessionId === row.sessionId ? detail.current : { sessionId: row.sessionId, state: "unknown", stale: true, recent: false },
140
+ current: detail.current?.sessionId === row.sessionId,
141
+ purposes: ["execution"],
142
+ executions: []
143
+ }));
144
+ }
145
+ function executionFilter(ui, value2 = "all") {
146
+ return `<select name="execution" aria-label="${escapeAttr(ui.text("executionFilter"))}">${["all", "running", "recent", "active"].map((filter) => `<option value="${filter}" ${filter === value2 ? "selected" : ""}>${escapeHTML(ui.text("executionFilter." + filter))}</option>`).join("")}</select>`;
147
+ }
148
+
149
+ // src/ui/view.ts
150
+ var states = ["proposed", "backlog", "open", "claimed", "review", "completed", "cancelled"];
151
+ var button = (ui, action, key, primary = false) => `<button type="button"${primary ? ' class="kanban__primary"' : ""} data-action="${escapeAttr(action)}">${escapeHTML(ui.text(key))}</button>`;
152
+ function contextTabs(ui, active = "board") {
153
+ return `<nav class="kanban__context-tabs" aria-label="${escapeAttr(ui.text("recordContext"))}">${[["board", "tabBoard"], ["log", "tabLog"], ["decision", "tabDecision"]].map(([value2, key]) => `<button type="button" data-context="${value2}" ${active === value2 ? 'aria-current="page"' : ""}>${escapeHTML(ui.text(key))}</button>`).join("")}</nav>`;
154
+ }
155
+ function field(ui, name, key, value2 = "", required = true, type = "text") {
156
+ return `<label>${escapeHTML(ui.text(key))}<input name="${name}" type="${type}" value="${escapeAttr(value2)}" ${required ? "required" : ""}></label>`;
157
+ }
158
+ function textarea(ui, name, key, value2 = "") {
159
+ return `<label>${escapeHTML(ui.text(key))}<textarea name="${name}" required>${escapeHTML(value2)}</textarea></label>`;
160
+ }
161
+ function owns(item, actor) {
162
+ return !!actor && !!item.owner && (item.owner.kind ?? "agent") === actor.kind && (item.owner.id ?? item.owner.agentId) === actor.id;
163
+ }
164
+ function navigation(ui, boards, selected) {
165
+ return `<header class="kanban__sidebar-header"><h2>${escapeHTML(ui.text("title"))}</h2>${button(ui, "new-board", "createShort")}</header>
166
+ <section class="kanban__board-list"><p class="kanban__caption">${escapeHTML(ui.text("scope"))}</p><nav aria-label="${escapeAttr(ui.text("title"))}">${boards.map((board) => `<button type="button" data-board="${escapeAttr(board.id)}" ${board.id === selected ? 'aria-current="page"' : ""}><small>${escapeHTML(kanbanReference(board))}</small><span>${escapeHTML(board.title)}</span></button>`).join("")}</nav></section>
167
+ <footer class="kanban__sidebar-tools"><details class="kanban__reference-lookup"><summary>${escapeHTML(ui.text("openReference"))}</summary>${referenceLookup(ui)}</details>
168
+ <details class="kanban__scope-note"><summary>${escapeHTML(ui.text("scopeInfo"))}</summary><p>${escapeHTML(ui.text("scopeHelp"))}</p></details></footer>`;
169
+ }
170
+ function referenceLookup(ui) {
171
+ return `<form data-form="reference"><input name="reference" aria-label="${escapeAttr(ui.text("reference"))}" placeholder="${escapeAttr(ui.text("referencePlaceholder"))}" required><button>${escapeHTML(ui.text("openShort"))}</button></form>`;
172
+ }
173
+ function boardHeader(ui, board, active = "board", automation = false) {
174
+ const [action, key] = active === "log" ? ["new-log", "newLog"] : active === "decision" ? ["new-decision", "newDecision"] : ["new-item", "newItem"];
175
+ const actions = `<div class="kanban__mobile-create">${button(ui, action, key)}</div>${button(ui, "refresh", "refresh")}${automation ? button(ui, "automation", "automationTitle") : ""}`;
176
+ return `<header class="kanban__page-header" data-titlebar><span class="kanban__navigation" data-navigation></span><div class="kanban__board-heading"><h2 data-titlebar-title title="${escapeAttr(board.title)}">${escapeHTML(board.title)}</h2></div>
177
+ <div class="kanban__header-actions"><span class="kanban__desktop-create">${button(ui, action, key, true)}</span><span class="kanban__window-navigation" data-window-navigation></span>${referenceMenu(ui, board, void 0, actions)}</div></header>`;
178
+ }
179
+ function catalogView(ui, boards) {
180
+ return `<header class="kanban__page-header" data-titlebar><span class="kanban__navigation" data-navigation></span><div class="kanban__board-heading"><h2 data-titlebar-title>${escapeHTML(ui.text("title"))}</h2></div><span class="kanban__window-navigation" data-window-navigation></span>${button(ui, "new-board", "createShort")}</header>
181
+ <div class="kanban__record-list">${boards.map((board) => `<button type="button" class="kanban__card" data-board="${escapeAttr(board.id)}"><strong>${escapeHTML(board.title)}</strong><small>${escapeHTML(kanbanReference(board))}</small></button>`).join("") || `<p>${escapeHTML(ui.text("emptyBoards"))}</p>`}</div>`;
182
+ }
183
+ function boardView(ui, board, page, query, activity = false, automation = false, sessionUrl) {
184
+ const columns = query.view === "archive" ? [{ id: "archive", label: ui.text("archive"), states }] : board.columns ?? states.map((state) => ({ id: state, label: ui.text(state), states: [state] }));
185
+ return `${boardHeader(ui, board, "board", automation)}${contextTabs(ui)}<form data-form="filter" class="kanban__filters"><input name="search" aria-label="${escapeAttr(ui.text("search"))}" placeholder="${escapeAttr(ui.text("search"))}" value="${escapeAttr(query.search ?? "")}">
186
+ <details class="kanban__filter-more" data-popover><summary>${escapeHTML(ui.text("moreFilters"))}${query.area || query.milestone || query.view !== "all" || query.execution && query.execution !== "all" ? '<span class="kanban__filter-dot" aria-hidden="true"></span>' : ""}</summary><div class="kanban__filter-fields"><label>${escapeHTML(ui.text("filter"))}<select name="view" aria-label="${escapeAttr(ui.text("filter"))}">${["all", "backlog", "ready", "in-progress", "review", "closed", "proposed", "archive"].map((view) => `<option value="${view}" ${view === query.view ? "selected" : ""}>${escapeHTML(ui.text(view === "ready" ? "open" : view === "in-progress" ? "claimed" : view === "closed" ? "completed" : view))}</option>`).join("")}</select></label>${phaseFilter(ui, query.phase)}${activity ? executionFilter(ui, query.execution) : ""}${suggestionField({ name: "area", label: "area", kind: "area", free: true, selected: query.area ? [{ id: query.area, label: query.area }] : [] })}
187
+ ${suggestionField({ name: "milestone", label: "milestone", kind: "milestone", free: true, selected: query.milestone ? [{ id: query.milestone, label: query.milestone }] : [] })}</div></details><button>${escapeHTML(ui.text("filter"))}</button></form>
188
+ <p class="kanban__count" role="status">${escapeHTML(ui.text("total", { count: page.total }))}</p><div class="kanban__columns" data-has-items="${page.items.length > 0}">${columns.map((column) => {
189
+ const items = page.items.filter((item) => column.states.includes(item.status));
190
+ return `<section class="kanban__column" data-empty="${items.length === 0}"><h3>${escapeHTML(column.label)} <small>${items.length}</small></h3><div class="kanban__column-items" data-column="${escapeAttr(column.id)}" tabindex="0" aria-label="${escapeAttr(column.label + " \xB7 " + ui.text("columnItems"))}">${items.map((item) => itemCard(ui, board, item, sessionUrl)).join("") || `<p>${escapeHTML(ui.text("emptyItems"))}</p>`}</div></section>`;
191
+ }).join("")}</div><footer class="kanban__toolbar"><small>${escapeHTML(ui.text("scrollHint"))}</small>${button(ui, "scroll-left", "previousColumns")}${button(ui, "scroll-right", "nextColumns")}${query.cursor ? button(ui, "first", "first") : ""}${page.nextCursor ? button(ui, "next", "next") : ""}</footer>`;
192
+ }
193
+ function itemCard(ui, board, item, sessionUrl) {
194
+ const content = `<strong>${escapeHTML(item.title)}</strong><small>${escapeHTML(kanbanReference(board, item))}</small><small>P${item.planning.priority} \xB7 ${escapeHTML(item.planning.area ?? "")} \xB7 ${escapeHTML(item.planning.milestone ?? "")}</small>${taskStatus(ui, item.task)}${item.pendingAmendments ? `<small class="kanban__badge">${escapeHTML(ui.text("pendingAmendments"))} (${item.pendingAmendments})</small>` : ""}${item.progress ? `<small>${item.progress.completed}/${item.progress.total}</small>` : ""}`;
195
+ const identity = item.task?.sessionId ? `data-session="${escapeAttr(item.task.sessionId)}"` : `data-item="${escapeAttr(item.id)}"`;
196
+ return `<article class="kanban__card"><button type="button" class="kanban__item-open" ${identity}>${content}</button>${item.task?.sessionId ? `<button type="button" data-item="${escapeAttr(item.id)}" title="${escapeAttr(ui.text("details"))}" aria-label="${escapeAttr(ui.text("details"))}">${escapeHTML(ui.text("details"))}</button>` : ""}<span data-item-execution="${escapeAttr(item.id)}">${executionBadge(ui, item.execution, sessionUrl)}</span></article>`;
197
+ }
198
+
199
+ // src/ui/children.ts
200
+ function childType(board, parent) {
201
+ const types = board.workTypes ?? DEFAULT_KANBAN_WORK_TYPES;
202
+ const rank = types.find((type) => type.id === (parent.planning?.kind ?? board.defaultWorkType ?? "task"))?.rank;
203
+ const candidates = types.filter((type) => rank !== void 0 && type.rank > rank);
204
+ return (candidates.find((type) => type.id === (board.defaultWorkType ?? "task")) ?? candidates.sort((a, b) => a.rank - b.rank)[0])?.id;
205
+ }
206
+ function childrenView(ui, page, cursor, canCreate, sessionUrl) {
207
+ return `<h4>${escapeHTML(ui.text("children"))} (${page.total})</h4><div class="kanban__actions">${canCreate ? button(ui, "new-child", "newChild") : ""}</div>
208
+ <div class="kanban__record-list">${page.items.map((item) => `<article class="kanban__card"><button type="button" data-item="${escapeAttr(item.id)}"><strong>${escapeHTML(item.title)}</strong></button>${taskStatus(ui, item.task)}${item.task?.sessionId ? sessionLink(ui, item.task.sessionId, ui.text("executionSession"), sessionUrl?.(item.task.sessionId)) : ""}${item.execution ? sessionLink(ui, item.execution.resourceId ?? item.execution.sessionId, ui.text("executionSession"), sessionUrl?.(item.execution.resourceId ?? item.execution.sessionId)) : ""}</article>`).join("")}</div>
209
+ <div class="kanban__actions">${cursor ? button(ui, "children-first", "first") : ""}${page.nextCursor ? `<button type="button" data-action="children-next" data-cursor="${escapeAttr(page.nextCursor)}">${escapeHTML(ui.text("next"))}</button>` : ""}</div>`;
210
+ }
211
+
212
+ // src/ui/panel-header.ts
213
+ function panelHeader(ui, title, board, entity) {
214
+ const label = escapeAttr(ui.text("close"));
215
+ return `<header class="kanban__panel-header" data-titlebar><button type="button" class="kanban__panel-back" data-titlebar-back data-action="close-panel" aria-label="${label}" title="${label}">${ui.icons.back ?? ui.icons.close}</button>
216
+ <h3 data-titlebar-title title="${escapeAttr(title)}">${escapeHTML(title)}</h3><span class="kanban__window-navigation" data-window-navigation></span>${board ? referenceMenu(ui, board, entity, `<p class="kanban__menu-title">${escapeHTML(title)}</p>`) : ""}</header>`;
217
+ }
218
+
219
+ // src/ui/context-view.ts
220
+ function recordsView(ui, page, kind, hasPrevious, board) {
221
+ return `${board ? boardHeader(ui, board, kind) : `<header class="kanban__toolbar"><h2>${escapeHTML(ui.text(kind))}</h2></header>`}
222
+ ${contextTabs(ui, kind)}<p>${escapeHTML(ui.text("recordHelp"))}</p><div class="kanban__record-list" tabindex="0" aria-label="${escapeAttr(ui.text(kind))}">${recordLinks(ui, page, board)}${!page.total ? `<p>${escapeHTML(ui.text("emptyRecords"))}</p>` : ""}</div>
223
+ <footer class="kanban__toolbar">${hasPrevious ? button(ui, "records-first", "first") : ""}${page.nextCursor ? button(ui, "records-next", "next") : ""}</footer>`;
224
+ }
225
+ function recordLinks(ui, page, board) {
226
+ return page.items.map((record) => `<button type="button" class="kanban__card" data-record="${escapeAttr(record.id)}"><strong>${escapeHTML(record.title)}</strong>${board && !record.activity ? `<small>${escapeHTML(kanbanReference(board, record))}</small>` : ""}<small>${escapeHTML(ui.text(record.activity ? "automaticLog" : record.kind))} \xB7 ${escapeHTML(ui.text(record.status))} \xB7 ${escapeHTML(new Date(record.createdAt).toLocaleString())}</small></button>`).join("");
227
+ }
228
+ function sourcesView(ui, sources) {
229
+ return sources.length ? `<ul>${sources.map((source) => `<li><button type="button" data-session="${escapeAttr(source.sessionId)}">${escapeHTML(source.sessionId)}</button><small>${escapeHTML(source.messageId)} \xB7 ${escapeHTML(source.revision)} \xB7 ${escapeHTML(source.role)}</small></li>`).join("")}</ul>` : `<p>${escapeHTML(ui.text("noSources"))}</p>`;
230
+ }
231
+ function recordForm(ui, kind, board, itemId = "", resources = []) {
232
+ return `${panelHeader(ui, ui.text(kind === "log" ? "newLog" : "newDecision"))}<form data-form="record">
233
+ <input type="hidden" name="kind" value="${kind}">${field(ui, "title", "name")}${textarea(ui, "content", "content")}${kind === "decision" ? textarea(ui, "rationale", "rationale") + suggestionField({ name: "supersedes", label: "supersedes", kind: "decision" }) : ""}
234
+ ${kind === "decision" && resources.length ? `<label>${escapeHTML(ui.text("recordResources"))}<select name="recordResources" multiple>${resources.map((resource) => `<option value="${escapeAttr(JSON.stringify(resource.reference))}">${escapeHTML(resource.label)}</option>`).join("")}</select></label>` : ""}
235
+ ${itemChoices(board, itemId)}<details><summary>${escapeHTML(ui.text("sources"))}</summary>${suggestionField({ name: "sourceSession", label: "sourceSession", kind: "session", manual: true })}${field(ui, "sourceMessage", "sourceMessage", "", false)}${field(ui, "sourceRevision", "sourceRevision", "", false)}</details><button>${escapeHTML(ui.text("save"))}</button></form>`;
236
+ }
237
+ function recordDetail(ui, record, board, executions) {
238
+ return `${panelHeader(ui, record.title, board, record)}
239
+ <p>${escapeHTML(ui.text(record.kind))} \xB7 ${escapeHTML(ui.text(record.status))} \xB7 ${escapeHTML(ui.text(record.origin))}</p>${record.activity ? "" : `<small>${escapeHTML(ui.text("recordId"))}: ${escapeHTML(record.id)}</small>`}<p>${escapeHTML(ui.text("author"))}: ${escapeHTML(record.author.id)} \xB7 ${escapeHTML(new Date(record.createdAt).toLocaleString())}</p>
240
+ <section data-reference-links></section><section class="kanban__prose"><h4>${escapeHTML(ui.text("content"))}</h4><p>${escapeHTML(record.content)}</p>${record.rationale ? `<h4>${escapeHTML(ui.text("rationale"))}</h4><p>${escapeHTML(record.rationale)}</p>` : ""}</section>
241
+ ${record.status === "draft" ? `<div class="kanban__actions">${button(ui, "confirm-record", "confirmRecord")}${button(ui, "reject-record", "rejectRecord")}</div>` : ""}
242
+ ${record.kind === "decision" && record.status === "confirmed" && executions ? recordExecutionView(ui, board, executions) : ""}
243
+ ${(record.externalRefs ?? []).map((ref) => `<p>${escapeHTML(ref.system)}: ${escapeHTML(ref.id)}</p>`).join("")}
244
+ ${record.supersedes ? recordLink(ui, record.supersedes, "supersedes") : ""}${record.supersededBy ? recordLink(ui, record.supersededBy, "superseded") : ""}
245
+ <h4>${escapeHTML(ui.text("itemIds"))}</h4>${record.itemIds.map((id) => `<button type="button" data-item="${escapeAttr(id)}">${escapeHTML(board.items.find((item) => item.id === id)?.title ?? id)}</button>`).join("")}
246
+ ${record.activity ? "" : `<form data-form="record-link">${suggestionField({ name: "itemId", label: "itemIds", kind: "item", required: true })}<button>${escapeHTML(ui.text("linkRecord"))}</button></form>`}<h4>${escapeHTML(ui.text("sources"))}</h4>${sourcesView(ui, record.sources)}`;
247
+ }
248
+ function recordExecutionView(ui, board, executions) {
249
+ const completed = executions.some((run) => run.state === "completed");
250
+ const actions = completed ? `<p>${escapeHTML(ui.text("completedFollowup"))}</p>` : `${button(ui, "run-record", "runRecord")}<details><summary>${escapeHTML(ui.text("referenceActions"))}</summary>${button(ui, "run-record-separate", "runRecordSeparate")}</details>`;
251
+ return `<div class="kanban__actions">${actions}</div>${completed ? "" : `<p>${escapeHTML(ui.text("runRecordHelp"))}</p>`}
252
+ ${executions.map((run) => `<p><button type="button" ${run.rootItemId && run.boardId === board.id ? `data-item="${escapeAttr(run.rootItemId)}"` : `data-board="${escapeAttr(run.boardId)}"`}>${escapeHTML(ui.text(run.rootItemId && run.boardId === board.id ? "executionWork" : "executionBoard"))}</button> \xB7 ${escapeHTML(ui.text(run.state))}${run.code ? ` \xB7 ${escapeHTML(run.code)}` : ""}</p>`).join("")}`;
253
+ }
254
+ function recordLink(ui, id, key) {
255
+ return `<p>${escapeHTML(ui.text(key))}: <button type="button" data-record="${escapeAttr(id)}">${escapeHTML(id)}</button></p>`;
256
+ }
257
+ function amendmentsView(ui, item) {
258
+ const pending = item.amendments?.filter((proposal) => proposal.status === "pending") ?? [];
259
+ if (!pending.length) return "";
260
+ return `<section class="kanban__amendments"><h4>${escapeHTML(ui.text("amendments"))}</h4><p>${escapeHTML(ui.text("amendmentHint"))}</p>${pending.map((proposal) => `<article><strong>${escapeHTML(proposal.proposedBy.id)}</strong><small> v${proposal.baseRevision}</small>
261
+ <details><summary>${escapeHTML(ui.text("currentText"))}</summary><p>${escapeHTML(item.title)}</p><p>${escapeHTML(item.responsibility)}</p><p>${escapeHTML(item.acceptance)}</p></details>
262
+ <h5>${escapeHTML(ui.text("proposedText"))}</h5><p>${escapeHTML(proposal.proposed.title)}</p><p>${escapeHTML(proposal.proposed.responsibility)}</p><p>${escapeHTML(proposal.proposed.acceptance)}</p>
263
+ ${sourcesView(ui, proposal.sources)}<form data-form="amendment" data-amendment="${escapeAttr(proposal.id)}"><select name="decision" aria-label="${escapeAttr(ui.text("amendments"))}"><option value="accept">${escapeHTML(ui.text("applyAmendment"))}</option><option value="reject">${escapeHTML(ui.text("rejectAmendment"))}</option></select>${field(ui, "reason", "reason", "", false)}<button>${escapeHTML(ui.text("save"))}</button></form></article>`).join("")}</section>`;
264
+ }
265
+ function itemChoices(board, itemId) {
266
+ return suggestionField({ name: "itemIds", label: "itemIds", kind: "item", multiple: true, selected: selectedItems(board, itemId ? [itemId] : []) });
267
+ }
268
+
269
+ // src/ui/event-history.ts
270
+ function meaningfulItemEvents(events) {
271
+ const last = /* @__PURE__ */ new Map(), hidden = /* @__PURE__ */ new Set();
272
+ for (const event of [...events].sort((a, b) => a.sequence - b.sequence)) {
273
+ if (event.kind === "renew") {
274
+ hidden.add(event);
275
+ continue;
276
+ }
277
+ if (event.kind !== "observe-execution" || !event.data.dispatchId || !event.data.reason) continue;
278
+ const key = JSON.stringify([event.boardId, event.itemId, event.data.dispatchId]);
279
+ const state = JSON.stringify([event.data.reason, event.data.code, event.data.status]);
280
+ if (last.get(key) === state) hidden.add(event);
281
+ last.set(key, state);
282
+ }
283
+ return events.filter((event) => !hidden.has(event));
284
+ }
285
+
286
+ // src/ui/forms.ts
287
+ function newBoardForm(ui) {
288
+ return `${panelHeader(ui, ui.text("newBoard"))}<form data-form="board">${field(ui, "title", "boardName")}
289
+ <label><input name="requireEstimate" type="checkbox">${escapeHTML(ui.text("requireEstimate"))}</label>
290
+ ${save(ui)}</form>`;
291
+ }
292
+ function newItemForm(ui, board, parent, resources) {
293
+ const preset = parent ? { kind: childType(board, parent), parentId: parent.id, priority: parent.planning?.priority ?? 2, dependencies: [], references: [] } : void 0;
294
+ return `${panelHeader(ui, ui.text("newItem"))}<form data-form="item">${field(ui, "title", "name")}
295
+ ${textarea(ui, "responsibility", "responsibility")}${textarea(ui, "acceptance", "acceptance")}${launchModeField(ui)}${planningFields(ui, board, void 0, preset)}${itemResourceField(ui, resources, (board.workTypes ?? DEFAULT_KANBAN_WORK_TYPES).some((type) => type.id === (preset?.kind ?? board.defaultWorkType ?? "task") && type.executable))}${save(ui)}</form>`;
296
+ }
297
+ function planningFields(ui, board, item, preset) {
298
+ const plan = preset ?? item?.planning, types = board.workTypes ?? DEFAULT_KANBAN_WORK_TYPES;
299
+ return `<label>${escapeHTML(ui.text("type"))}<select name="kind">${types.map((type) => `<option data-executable="${type.executable}" value="${escapeAttr(type.id)}" ${type.id === (plan?.kind ?? board.defaultWorkType ?? "task") ? "selected" : ""}>${escapeHTML(type.label)}</option>`).join("")}</select></label>
300
+ ${field(ui, "priority", "priority", String(plan?.priority ?? 2), true, "number")}
301
+ ${suggestionField({ name: "area", label: "area", kind: "area", free: true, selected: plan?.area ? [{ id: plan.area, label: plan.area }] : [] })}${suggestionField({ name: "milestone", label: "milestone", kind: "milestone", free: true, selected: plan?.milestone ? [{ id: plan.milestone, label: plan.milestone }] : [] })}
302
+ ${suggestionField({ name: "parentId", label: "parent", kind: "parent", excludeItemId: item?.id, selected: selectedItems(board, plan?.parentId ? [plan.parentId] : []) })}${suggestionField({ name: "dependencies", label: "dependencies", kind: "dependency", multiple: true, excludeItemId: item?.id, selected: selectedItems(board, plan?.dependencies ?? []) })}
303
+ ${field(ui, "dueAt", "dueAt", dateInput(plan?.dueAt), false, "datetime-local")}`;
304
+ }
305
+ function dateInput(at) {
306
+ if (at === void 0) return "";
307
+ const date2 = new Date(at);
308
+ return new Date(at - date2.getTimezoneOffset() * 6e4).toISOString().slice(0, 16);
309
+ }
310
+ var save = (ui) => `<div class="kanban__actions"><button>${escapeHTML(ui.text("save"))}</button>${button(ui, "close-panel", "close")}</div>`;
311
+ function detailView(ui, board, item, actor, archived, events, relatedSessions = false, guidance, interactive = false, task = projectKanbanTask(item), fixAvailable = false) {
312
+ return `${panelHeader(ui, item.title, board, item)}${taskStatus(ui, task)}${archived ? "" : `<div class="kanban__actions">${taskControls(ui, task, fixAvailable)}</div>`}
313
+ <p>${escapeHTML(ui.text("origin"))}: ${escapeHTML(item.origin ? ui.text(item.origin.kind === "discovery" ? "flow" : "manual") + " \xB7 " + item.origin.principal.id : ui.text("unknownOrigin"))}</p>
314
+ <dl><dt>${escapeHTML(ui.text("itemId"))}</dt><dd>${escapeHTML(item.id)}</dd><dt>${escapeHTML(ui.text("responsibility"))}</dt><dd>${escapeHTML(item.responsibility)}</dd>
315
+ <dt>${escapeHTML(ui.text("acceptance"))}</dt><dd>${escapeHTML(item.acceptance)}</dd><dt>${escapeHTML(ui.text("owner"))}</dt><dd>${escapeHTML(item.owner?.id ?? item.owner?.agentId ?? ui.text("none"))}</dd></dl>
316
+ <p>${escapeHTML(ui.text(item.status))}${item.blocked ? " \xB7 " + escapeHTML(ui.text("blocked") + ": " + item.blocked.reason) : ""}</p>
317
+ ${interactive ? "<section data-item-operations></section>" : ""}
318
+ ${item.status === "review" ? interactive ? `<details><summary>${escapeHTML(ui.text("reviewFlow"))}</summary>${reviewGuidance(ui, guidance)}</details>` : reviewGuidance(ui, guidance) : ""}
319
+ <div class="kanban__actions">${controls(ui, board, item, actor, archived, interactive)}</div>
320
+ ${item.issues?.filter((issue) => !issue.resolvedBy).map((issue) => `<form data-form="task-reply"><input type="hidden" name="attemptId" value="${escapeAttr(issue.attemptId)}"><input type="hidden" name="replyTo" value="${escapeAttr(issue.id)}">${textarea(ui, "content", "message")}<button>${escapeHTML(ui.text("send"))}</button></form>`).join("") ?? ""}
321
+ ${amendmentsView(ui, item)}
322
+ ${archived ? "" : detailForms(ui, board, item, actor)}
323
+ ${(item.planning?.dependencies ?? []).length ? `<p>${escapeHTML(ui.text("dependencies"))}: ${(item.planning?.dependencies ?? []).map((id) => `<button type="button" data-item="${escapeAttr(id)}">${escapeHTML(board.items.find((i) => i.id === id)?.title ?? id)}</button>`).join(" ")}</p>` : ""}
324
+ ${item.planning?.parentId ? `<p>${escapeHTML(ui.text("parent"))}: <button type="button" data-item="${escapeAttr(item.planning.parentId)}">${escapeHTML(board.items.find((parent) => parent.id === item.planning?.parentId)?.title ?? item.planning.parentId)}</button></p>` : ""}
325
+ <section data-children></section><section data-related-records></section><h4>${escapeHTML(ui.text("sources"))}</h4>${sourcesView(ui, item.sources ?? [])}
326
+ <div data-analysis></div><div data-execution-session></div><section data-related-sessions-section></section><section data-reference-links></section><section data-work-resource-section></section>
327
+ ${relatedSessions ? "" : `<h4>${escapeHTML(ui.text("sessions"))}</h4>${item.participants.map((p) => `<button type="button" data-session="${escapeAttr(p.sessionId)}">${escapeHTML(p.sessionId)} \xB7 ${escapeHTML(p.roles.join(", "))}</button>`).join("") || `<p>${escapeHTML(ui.text("none"))}</p>`}`}
328
+ ${archived ? "" : `<details><summary>${escapeHTML(ui.text("registerSession"))}</summary><p>${escapeHTML(ui.text("registerSessionHelp"))}</p><form data-form="session">${suggestionField({ name: "sessionId", label: "session", kind: "session", manual: true, required: true })}<button>${escapeHTML(ui.text("registerSession"))}</button></form></details>`}
329
+ <section data-item-events>${itemEventsView(ui, events)}</section>`;
330
+ }
331
+ function reviewGuidance(ui, guidance) {
332
+ const [summary, ...steps] = guidance?.paragraphs ?? [ui.text("reviewHelp")];
333
+ return `<section class="kanban__review-guide"><strong>${escapeHTML(guidance?.title ?? ui.text("reviewHelpTitle"))}</strong><p>${escapeHTML(summary)}</p>
334
+ ${steps.length ? `<details><summary>${escapeHTML(ui.text("reviewFlow"))}</summary><ol>${steps.map((text) => `<li>${escapeHTML(text)}</li>`).join("")}</ol></details>` : ""}</section>`;
335
+ }
336
+ function itemEventsView(ui, events) {
337
+ return `<details><summary>${escapeHTML(ui.text("eventsHelp"))}</summary><ol>${meaningfulItemEvents(events).map((event) => `<li>${escapeHTML(event.kind)} \xB7 ${escapeHTML(event.actor.id ?? event.actor.agentId ?? "")} \xB7 ${escapeHTML(new Date(event.at).toLocaleString())}${event.data.message ? `<p>${escapeHTML(event.data.message)}</p>` : ""}</li>`).join("")}</ol></details>`;
338
+ }
339
+ function controls(ui, board, item, actor, archived, interactive = false) {
340
+ if (archived) return button(ui, "restore", "restore");
341
+ if (["completed", "cancelled"].includes(item.status)) return button(ui, "archive", "archiveItem");
342
+ const executable = executableWorkItem(board, item);
343
+ let result = item.status === "proposed" ? button(ui, "publish", "publish") : item.status === "backlog" && executable ? button(ui, "ready", "ready") : item.status === "open" ? button(ui, "defer", "defer") : "";
344
+ if (owns(item, actor) && item.status === "claimed") result += button(ui, "renew", "renew");
345
+ if (item.status === "review" && !owns(item, actor)) result += interactive ? "" : button(ui, "accept", "accept") + button(ui, "reject", "reject");
346
+ if (item.blocked) result += button(ui, "unblock", "unblock");
347
+ if (item.status === "review" && owns(item, actor) && board.policy.review !== "assignee") result += `<p>${escapeHTML(ui.text("noReview"))}</p>`;
348
+ return result;
349
+ }
350
+ function detailForms(ui, board, item, actor) {
351
+ if (["completed", "cancelled"].includes(item.status)) return "";
352
+ let forms = "";
353
+ const executable = executableWorkItem(board, item);
354
+ if (!executable && ["backlog", "proposed", "open"].includes(item.status)) forms += `<form data-form="close-plan">${field(ui, "resultRef", "result")}<button>${escapeHTML(ui.text("closePlan"))}</button></form>`;
355
+ if (!board.automation && executable && item.status === "open" && !item.blocked) forms += `<form data-form="claim">${board.policy.requireEstimate !== false ? estimates(ui) : ""}<button>${escapeHTML(ui.text("claim"))}</button></form>`;
356
+ if (item.status === "claimed" && owns(item, actor)) forms += `<form data-form="submit">${field(ui, "resultRef", "result")}<button>${escapeHTML(ui.text("submit"))}</button></form>`;
357
+ const automatic = item.eligiblePrincipals?.some((principal) => principal.kind === board.automation?.worker.kind && principal.id === board.automation.worker.id);
358
+ let secondary = "";
359
+ if (["proposed", "backlog", "open"].includes(item.status) && !automatic && !board.automation) secondary += `<details><summary>${escapeHTML(ui.text("editItem"))}</summary><form data-form="edit-item">${field(ui, "title", "name", item.title)}${textarea(ui, "responsibility", "responsibility", item.responsibility)}${textarea(ui, "acceptance", "acceptance", item.acceptance)}<button>${escapeHTML(ui.text("save"))}</button></form></details>`;
360
+ if (!board.automation && item.launch?.admittedAt === void 0 && ["proposed", "backlog", "open"].includes(item.status)) secondary += `<details><summary>${escapeHTML(ui.text("plan"))}</summary><form data-form="plan">${planningFields(ui, board, item)}<button>${escapeHTML(ui.text("save"))}</button></form></details><form data-form="cancel">${field(ui, "reason", "reason")}<button>${escapeHTML(ui.text("cancel"))}</button></form>`;
361
+ if (owns(item, actor)) secondary += `<form data-form="release">${field(ui, "reason", "reason")}<button>${escapeHTML(ui.text("release"))}</button></form>`;
362
+ if (!item.blocked && (!item.owner || owns(item, actor))) secondary += `<form data-form="block">${field(ui, "reason", "reason")}<button>${escapeHTML(ui.text("block"))}</button></form>`;
363
+ return forms + `<details><summary>${escapeHTML(ui.text("moreActions"))}</summary>` + secondary + `<form data-form="message">${textarea(ui, "message", "message")}<button>${escapeHTML(ui.text("send"))}</button></form></details>`;
364
+ }
365
+ function estimates(ui) {
366
+ return `${field(ui, "complexity", "complexity", "1", true, "number")}${field(ui, "workUnits", "workUnits", "1", true, "number")}
367
+ ${field(ui, "duration", "duration", "30", true, "number")}<label>${escapeHTML(ui.text("confidence"))}<select name="confidence">${["low", "medium", "high"].map((value2) => `<option value="${value2}">${escapeHTML(ui.text(value2))}</option>`).join("")}</select></label>`;
368
+ }
369
+ function itemResourceField(ui, catalog, executable) {
370
+ if (!catalog || !catalog.resources.length && !catalog.selected) return "";
371
+ const options = [...catalog.resources], selected = catalog.selected ? JSON.stringify(catalog.selected) : "";
372
+ if (catalog.selected && !options.some((option) => option.reference.system === catalog.selected.system && option.reference.id === catalog.selected.id)) options.push({ reference: catalog.selected, label: catalog.selected.id });
373
+ return `<label>${escapeHTML(ui.text("itemResource"))}<select name="itemResource" data-resource-required="${catalog.required === true}" ${catalog.required && executable ? "required" : ""} ${catalog.readonly ? "disabled" : ""}>
374
+ <option value="">${escapeHTML(ui.text("none"))}</option>${options.map((option) => `<option value="${escapeAttr(JSON.stringify(option.reference))}" ${JSON.stringify(option.reference) === selected ? "selected" : ""}>${escapeHTML(option.label)}</option>`).join("")}</select>
375
+ ${catalog.readonly ? `<input type="hidden" name="itemResource" value="${escapeAttr(selected)}">` : ""}</label>`;
376
+ }
377
+ function editTaskForm(ui, board, item, editId, resources) {
378
+ return `${panelHeader(ui, item.title, board, item)}<p>${escapeHTML(ui.text("editLeaseHelp"))}</p><form data-form="save-edit"><input type="hidden" name="editId" value="${escapeAttr(editId)}">${field(ui, "title", "name", item.title)}${textarea(ui, "responsibility", "responsibility", item.responsibility)}${textarea(ui, "acceptance", "acceptance", item.acceptance)}${launchModeField(ui, item.launch?.mode)}${planningFields(ui, board, item)}${itemResourceField(ui, resources, true)}${save(ui)}</form>`;
379
+ }
380
+ function launchModeField(ui, mode = "automatic") {
381
+ return `<label>${escapeHTML(ui.text("taskStartMode"))}<select name="launchMode">${["automatic", "manual"].map((value2) => `<option value="${value2}" ${mode === value2 ? "selected" : ""}>${escapeHTML(ui.text("taskMode." + value2))}</option>`).join("")}</select></label>`;
382
+ }
383
+
384
+ // src/ui/automation.ts
385
+ function automationForm(ui, board, catalog) {
386
+ const settings = board.automation;
387
+ const select = (name, label) => {
388
+ const selected = settings?.[name], targets = [...name === "preparation" ? catalog.preparations ?? [] : catalog.targets];
389
+ if (settings?.[name] && !targets.some((entry) => sameReference(entry.reference, selected)))
390
+ targets.push({ reference: settings[name], label: ui.text("automationTargetUnavailable") });
391
+ return `<label>${escapeHTML(ui.text(label))}<select name="${name}" ${name === "preparation" ? "" : "required"}><option value="">${escapeHTML(ui.text("automationChoose"))}</option>${targets.map((entry) => {
392
+ const value2 = JSON.stringify(entry.reference);
393
+ return `<option value="${escapeAttr(value2)}" ${sameReference(entry.reference, selected) ? "selected" : ""}>${escapeHTML(entry.label)}</option>`;
394
+ }).join("")}</select></label>`;
395
+ };
396
+ return `${panelHeader(ui, ui.text("automationTitle"))}<form data-form="automation">
397
+ <p>${escapeHTML(ui.text("automationHelp"))}</p>
398
+ <label><input type="checkbox" name="enabled" ${settings?.enabled ? "checked" : ""}>${escapeHTML(ui.text("automationEnabled"))}</label>
399
+ ${select("planner", "automationPlanner")}${select("executor", "automationExecutor")}${select("preparation", "automationPreparation")}<div data-preparation-options>${preparationFields(board, catalog)}</div>
400
+ <label><input type="checkbox" name="autoAccept" ${settings?.autoAccept ? "checked" : ""}>${escapeHTML(ui.text("automationAutoAccept"))}</label>
401
+ <p>${escapeHTML(ui.text("automationReviewHelp"))}</p>
402
+ <div class="kanban__actions"><button>${escapeHTML(ui.text("save"))}</button>${button(ui, "close-panel", "close")}</div></form>`;
403
+ }
404
+ function analysisView(ui, status) {
405
+ if (!status) return `<p>${escapeHTML(ui.text("automationPending"))}</p>`;
406
+ return `<section><h4>${escapeHTML(ui.text("automationAnalysis"))}</h4><p>${escapeHTML(ui.text("automationState." + status.state))}</p>
407
+ ${status.reason ? `<p>${escapeHTML(status.reason)}</p>` : ""}${status.code ? `<p>${escapeHTML(ui.text("errors." + status.code))}</p>` : ""}
408
+ ${["failed", "clarification"].includes(status.state) ? button(ui, "reanalyze", "automationReanalyze") : ""}</section>`;
409
+ }
410
+ function preparationFields(board, catalog, reference = board.automation?.preparation) {
411
+ const selected = reference;
412
+ const entries = catalog.preparations?.filter((entry) => sameReference(entry.reference, selected)) ?? [];
413
+ return entries.flatMap((entry) => (entry.options ?? []).map((option) => {
414
+ const value2 = board.automation?.preparationOptions?.[option.name] ?? option.defaultValue ?? "";
415
+ const name = "preparation-option-" + option.name;
416
+ const control = option.choices ? `<select name="${escapeAttr(name)}">${option.choices.map((choice) => `<option value="${escapeAttr(choice.value)}" ${choice.value === value2 ? "selected" : ""}>${escapeHTML(choice.label)}</option>`).join("")}</select>` : `<input name="${escapeAttr(name)}" value="${escapeAttr(value2)}" maxlength="4096">`;
417
+ return `<label>${escapeHTML(option.label)}${control}</label>`;
418
+ })).join("");
419
+ }
420
+ function sameReference(left, right) {
421
+ const a = left;
422
+ const b = right;
423
+ return !!a && !!b && a.system === b.system && a.id === b.id && a.revision === b.revision;
424
+ }
425
+
426
+ // src/ui/suggestion-picker.ts
427
+ var KanbanSuggestionPicker = class {
428
+ constructor(root, config, fetch, presentation) {
429
+ this.root = root;
430
+ this.config = config;
431
+ this.fetch = fetch;
432
+ this.presentation = presentation;
433
+ this.selected = [...config.selected ?? []];
434
+ this.mount();
435
+ this.bind();
436
+ this.renderValues();
437
+ this.close();
438
+ }
439
+ input = document.createElement("input");
440
+ list = document.createElement("div");
441
+ chips = document.createElement("div");
442
+ values = document.createElement("div");
443
+ hint = document.createElement("small");
444
+ selected;
445
+ rows = [];
446
+ active = -1;
447
+ loading = false;
448
+ generation = 0;
449
+ timer;
450
+ abort = new AbortController();
451
+ mount() {
452
+ const caption = document.createElement("label"), id = "kanban-suggest-" + randomUUID();
453
+ caption.textContent = this.presentation.text(this.config.label);
454
+ caption.htmlFor = this.input.id = id;
455
+ this.input.type = "text";
456
+ this.input.autocomplete = "off";
457
+ this.input.maxLength = 256;
458
+ this.input.required = !!this.config.required;
459
+ this.input.setAttribute("role", "combobox");
460
+ this.input.setAttribute("aria-autocomplete", "list");
461
+ this.input.setAttribute("aria-controls", this.list.id = id + "-list");
462
+ this.input.setAttribute("aria-describedby", this.hint.id = id + "-hint");
463
+ this.input.placeholder = this.config.free ? this.presentation.text(this.config.label) + " \xB7 " + this.presentation.text("suggestFreePlaceholder") : this.presentation.text("suggestPlaceholder");
464
+ if (this.config.free) this.input.name = this.config.name;
465
+ this.input.value = this.config.multiple ? "" : this.selected[0]?.label ?? "";
466
+ this.list.className = "kanban__suggest-list";
467
+ this.list.setAttribute("role", "listbox");
468
+ this.list.setAttribute("aria-label", caption.textContent);
469
+ this.chips.className = "kanban__suggest-chips";
470
+ this.hint.setAttribute("aria-live", "polite");
471
+ this.hint.textContent = this.presentation.text(this.config.free ? "suggestFreeHint" : "suggestHint");
472
+ this.root.replaceChildren(caption, this.chips, this.input, this.values, this.list, this.hint);
473
+ }
474
+ bind() {
475
+ const signal = this.abort.signal;
476
+ this.input.addEventListener("focus", () => {
477
+ if (!this.config.multiple && this.selected.length) this.input.select();
478
+ if (!this.loading) void this.search();
479
+ }, { signal });
480
+ this.input.addEventListener("click", () => {
481
+ if (this.list.hidden && !this.loading) void this.search();
482
+ }, { signal });
483
+ this.input.addEventListener("input", () => {
484
+ if (!this.config.multiple && !this.config.free) {
485
+ this.selected = [];
486
+ this.renderValues();
487
+ }
488
+ this.validate();
489
+ clearTimeout(this.timer);
490
+ this.generation++;
491
+ this.rows = [];
492
+ this.active = -1;
493
+ this.list.replaceChildren();
494
+ this.hint.textContent = this.presentation.text("loading");
495
+ this.open();
496
+ this.timer = setTimeout(() => {
497
+ void this.search();
498
+ }, 150);
499
+ }, { signal });
500
+ this.input.addEventListener("keydown", (event) => this.keydown(event), { signal });
501
+ this.list.addEventListener("pointerdown", (event) => event.preventDefault(), { signal });
502
+ this.root.addEventListener("click", (event) => this.click(event), { signal });
503
+ document.addEventListener("scroll", () => this.position(), { signal, capture: true });
504
+ window.addEventListener("resize", () => this.position(), { signal });
505
+ this.root.addEventListener("focusout", (event) => {
506
+ if (!this.root.contains(event.relatedTarget)) this.close();
507
+ }, { signal });
508
+ }
509
+ async search() {
510
+ const generation = ++this.generation, operationId = randomUUID(), started = Date.now();
511
+ const search = !this.config.multiple && this.selected[0]?.label === this.input.value ? "" : this.input.value.trim();
512
+ const kind = this.root.closest("form")?.querySelector("[name=kind]")?.value;
513
+ this.revealInput();
514
+ this.loading = true;
515
+ this.hint.textContent = this.presentation.text("loading");
516
+ try {
517
+ const page = await this.fetch({
518
+ kind: this.config.kind,
519
+ search,
520
+ limit: 12,
521
+ ...this.config.multiple ? { excludeIds: this.selected.map((row) => row.id) } : {},
522
+ ...this.config.excludeItemId ? { excludeItemId: this.config.excludeItemId } : {},
523
+ ...this.config.kind === "parent" && kind ? { childKind: kind } : {}
524
+ });
525
+ if (generation !== this.generation || this.abort.signal.aborted || !this.root.isConnected) return;
526
+ this.rows = page.items.filter((row) => !this.config.multiple || !this.selected.some((value2) => value2.id === row.id));
527
+ if (this.config.manual && search && !this.rows.some((row) => row.id === search)) this.rows.push({ id: search, label: search, detail: this.presentation.text("suggestManualId") });
528
+ this.active = this.rows.length ? 0 : -1;
529
+ this.renderOptions();
530
+ this.hint.textContent = this.presentation.text(page.hasMore ? "suggestRefine" : this.rows.length ? this.config.free ? "suggestFreeHint" : "suggestHint" : "suggestEmpty");
531
+ this.log({ operationId, stage: this.config.kind, result: "success", count: page.items.length, durationMs: Date.now() - started });
532
+ } catch (error) {
533
+ if (generation !== this.generation || this.abort.signal.aborted) return;
534
+ this.rows = [];
535
+ this.list.replaceChildren();
536
+ this.hint.textContent = this.presentation.text("suggestFailed");
537
+ const retry = document.createElement("button");
538
+ retry.type = "button";
539
+ retry.textContent = this.presentation.text("retry");
540
+ retry.dataset.suggestRetry = "";
541
+ this.list.append(retry);
542
+ this.open();
543
+ const raw = error?.code, code = typeof raw === "string" && /^[A-Z_0-9]{1,80}$/.test(raw) ? raw : "KANBAN_SUGGEST_FAILED";
544
+ this.log({ operationId, stage: this.config.kind, result: "failure", code, durationMs: Date.now() - started });
545
+ } finally {
546
+ if (generation === this.generation) this.loading = false;
547
+ }
548
+ }
549
+ log(event) {
550
+ try {
551
+ this.presentation.log?.(event);
552
+ } catch {
553
+ }
554
+ }
555
+ renderOptions() {
556
+ this.list.replaceChildren();
557
+ this.rows.forEach((row, index) => {
558
+ const button2 = document.createElement("button");
559
+ button2.type = "button";
560
+ button2.tabIndex = -1;
561
+ button2.dataset.suggestIndex = String(index);
562
+ button2.id = this.list.id + "-" + index;
563
+ button2.setAttribute("role", "option");
564
+ const title = document.createElement("strong"), detail = document.createElement("small");
565
+ title.textContent = row.label;
566
+ detail.textContent = [row.status ? this.presentation.text(row.status) : "", row.archived ? this.presentation.text("archive") : "", row.detail, this.config.free ? "" : row.id].filter(Boolean).join(" \xB7 ");
567
+ button2.append(title, detail);
568
+ this.list.append(button2);
569
+ });
570
+ this.open();
571
+ this.markActive();
572
+ }
573
+ click(event) {
574
+ const button2 = event.target.closest("button");
575
+ if (!button2) return;
576
+ if (button2.dataset.suggestIndex !== void 0) this.choose(this.rows[Number(button2.dataset.suggestIndex)]);
577
+ else if (button2.dataset.suggestRemove !== void 0) {
578
+ this.selected = this.selected.filter((row) => row.id !== button2.dataset.suggestRemove);
579
+ this.renderValues();
580
+ this.changed();
581
+ this.input.focus();
582
+ } else if (button2.hasAttribute("data-suggest-retry")) {
583
+ void this.search();
584
+ this.input.focus();
585
+ }
586
+ }
587
+ choose(row) {
588
+ if (!row) return;
589
+ this.selected = this.config.multiple ? [...this.selected, row] : [row];
590
+ this.input.value = this.config.multiple ? "" : row.label;
591
+ this.renderValues();
592
+ this.input.setCustomValidity("");
593
+ this.hint.textContent = this.presentation.text(this.config.free ? "suggestFreeHint" : "suggestHint");
594
+ this.changed();
595
+ this.close();
596
+ this.input.focus();
597
+ }
598
+ renderValues() {
599
+ this.values.replaceChildren();
600
+ this.chips.replaceChildren();
601
+ if (!this.config.free) for (const row of this.selected.length ? this.selected : this.config.multiple ? [] : [{ id: "", label: "" }]) {
602
+ const input = document.createElement("input");
603
+ input.type = "hidden";
604
+ input.name = this.config.name;
605
+ input.value = row.id;
606
+ this.values.append(input);
607
+ }
608
+ if (this.config.multiple) for (const row of this.selected) {
609
+ const chip = document.createElement("button");
610
+ chip.type = "button";
611
+ chip.dataset.suggestRemove = row.id;
612
+ const name = document.createElement("span"), remove = document.createElement("span");
613
+ name.textContent = row.label;
614
+ remove.innerHTML = this.presentation.icons.close;
615
+ remove.setAttribute("aria-hidden", "true");
616
+ chip.append(name, remove);
617
+ chip.setAttribute("aria-label", this.presentation.text("suggestRemove") + ": " + row.label);
618
+ this.chips.append(chip);
619
+ }
620
+ }
621
+ validate() {
622
+ const unselected = !this.config.free && !!this.input.value.trim() && (this.config.multiple || !this.selected.length);
623
+ this.input.setCustomValidity(unselected ? this.presentation.text("suggestPickRequired") : "");
624
+ }
625
+ changed() {
626
+ this.validate();
627
+ this.root.dispatchEvent(new Event("input", { bubbles: true }));
628
+ }
629
+ keydown(event) {
630
+ if (event.key === "Escape" && !this.list.hidden) {
631
+ event.preventDefault();
632
+ event.stopPropagation();
633
+ this.close();
634
+ return;
635
+ }
636
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
637
+ event.preventDefault();
638
+ event.stopPropagation();
639
+ if (this.list.hidden) {
640
+ void this.search();
641
+ return;
642
+ }
643
+ this.active = this.rows.length ? (this.active + (event.key === "ArrowDown" ? 1 : -1) + this.rows.length) % this.rows.length : -1;
644
+ this.markActive();
645
+ } else if (event.key === "Enter" && !this.list.hidden) {
646
+ event.preventDefault();
647
+ event.stopPropagation();
648
+ this.choose(this.rows[this.active]);
649
+ } else if (event.key === "Tab" && !this.list.querySelector("[data-suggest-retry]")) this.close();
650
+ else if (event.key === "Backspace" && this.config.multiple && !this.input.value && this.selected.length) {
651
+ this.selected.pop();
652
+ this.renderValues();
653
+ this.changed();
654
+ }
655
+ }
656
+ markActive() {
657
+ [...this.list.children].forEach((node, index) => node.setAttribute("aria-selected", String(index === this.active)));
658
+ if (this.active >= 0) {
659
+ this.input.setAttribute("aria-activedescendant", this.list.id + "-" + this.active);
660
+ const row = this.list.children[this.active];
661
+ if (row && row.offsetTop < this.list.scrollTop) this.list.scrollTop = row.offsetTop;
662
+ else if (row && row.offsetTop + row.offsetHeight > this.list.scrollTop + this.list.clientHeight) this.list.scrollTop = row.offsetTop + row.offsetHeight - this.list.clientHeight;
663
+ } else this.input.removeAttribute("aria-activedescendant");
664
+ }
665
+ open() {
666
+ this.list.hidden = false;
667
+ this.input.setAttribute("aria-expanded", "true");
668
+ this.position();
669
+ }
670
+ revealInput() {
671
+ const panel = this.root.closest("[data-panel]");
672
+ if (!panel) return;
673
+ const input = this.input.getBoundingClientRect(), bounds = panel.getBoundingClientRect();
674
+ const top = (panel.querySelector(".kanban__panel-header")?.getBoundingClientRect().bottom ?? bounds.top) + 8, bottom = bounds.bottom - 12;
675
+ if (input.bottom > bottom) panel.scrollTop += input.bottom - bottom;
676
+ else if (input.top < top) panel.scrollTop -= top - input.top;
677
+ }
678
+ position() {
679
+ if (this.list.hidden || !this.root.isConnected) return;
680
+ const input = this.input.getBoundingClientRect(), scope = (this.root.closest("[data-panel]") ?? this.root.closest(".kanban") ?? document.documentElement).getBoundingClientRect();
681
+ const below = Math.max(0, scope.bottom - input.bottom - 8), above = Math.max(0, input.top - scope.top - 8), up = above > below && below < 160;
682
+ const height = Math.min(210, up ? above : below), width = Math.min(Math.max(input.width, 220), Math.max(0, scope.width - 16));
683
+ const left = Math.max(scope.left + 8, Math.min(input.left, scope.right - width - 8));
684
+ Object.assign(this.list.style, {
685
+ position: "fixed",
686
+ zIndex: "30",
687
+ left: `${left}px`,
688
+ width: `${width}px`,
689
+ maxHeight: `${height}px`,
690
+ top: up ? "auto" : `${input.bottom + 4}px`,
691
+ bottom: up ? `${innerHeight - input.top + 4}px` : "auto"
692
+ });
693
+ }
694
+ close() {
695
+ this.loading = false;
696
+ if (this.hint.textContent === this.presentation.text("loading")) this.hint.textContent = this.presentation.text(this.config.free ? "suggestFreeHint" : "suggestHint");
697
+ clearTimeout(this.timer);
698
+ this.generation++;
699
+ this.list.hidden = true;
700
+ this.input.setAttribute("aria-expanded", "false");
701
+ this.input.removeAttribute("aria-activedescendant");
702
+ }
703
+ destroy() {
704
+ clearTimeout(this.timer);
705
+ this.generation++;
706
+ this.abort.abort();
707
+ }
708
+ };
709
+
710
+ // src/ui/suggestions.ts
711
+ var KanbanSuggestions = class {
712
+ constructor(root, service, boardId, presentation) {
713
+ this.root = root;
714
+ this.service = service;
715
+ this.boardId = boardId;
716
+ this.presentation = presentation;
717
+ this.observer = new MutationObserver(() => this.sync());
718
+ this.observer.observe(root, { childList: true, subtree: true });
719
+ this.sync();
720
+ }
721
+ observer;
722
+ fields = /* @__PURE__ */ new Map();
723
+ sync() {
724
+ for (const [node, field2] of this.fields) if (!this.root.contains(node)) {
725
+ field2.destroy();
726
+ this.fields.delete(node);
727
+ }
728
+ if (!this.service) return;
729
+ for (const node of this.root.querySelectorAll("[data-kanban-suggest]")) {
730
+ if (this.fields.has(node)) continue;
731
+ const options = JSON.parse(node.dataset.kanbanSuggest);
732
+ const picker = new KanbanSuggestionPicker(node, options, (query) => this.service.suggest(this.boardId(), query), this.presentation);
733
+ this.fields.set(node, picker);
734
+ }
735
+ }
736
+ destroy() {
737
+ this.observer.disconnect();
738
+ for (const field2 of this.fields.values()) field2.destroy();
739
+ this.fields.clear();
740
+ }
741
+ };
742
+
743
+ // src/ui/index.ts
744
+ import { mountTitleBar as mountSharedTitleBar } from "@nestos/ui-common";
745
+
746
+ // src/ui/drawer.ts
747
+ var KanbanDrawer = class {
748
+ constructor(editor, sidebar) {
749
+ this.editor = editor;
750
+ this.sidebar = sidebar;
751
+ }
752
+ key = "";
753
+ focusRequested = false;
754
+ restoreTarget;
755
+ restoreItem;
756
+ restoreRecord;
757
+ restoreRequested = false;
758
+ get element() {
759
+ return this.editor.querySelector("[data-panel]");
760
+ }
761
+ get open() {
762
+ return !this.element.hidden;
763
+ }
764
+ show(html, key) {
765
+ const panel = this.element, same = this.open && this.key === key, top = same ? panel.scrollTop : 0;
766
+ if (!this.open) {
767
+ const active = document.activeElement;
768
+ this.restoreTarget = active ?? void 0;
769
+ this.restoreItem = active?.dataset.item;
770
+ this.restoreRecord = active?.dataset.record;
771
+ }
772
+ this.focusRequested ||= same && panel.contains(document.activeElement);
773
+ const retained = same ? ["[data-work-resource-section]", "[data-related-sessions-section]"].map((selector) => ({ selector, node: panel.querySelector(selector) })) : [];
774
+ panel.innerHTML = html;
775
+ for (const { selector, node } of retained) if (node?.hasChildNodes()) panel.querySelector(selector)?.replaceWith(node);
776
+ const error = document.createElement("p");
777
+ error.dataset.panelError = "";
778
+ error.setAttribute("role", "alert");
779
+ const header = panel.querySelector(".kanban__panel-header");
780
+ if (header) header.after(error);
781
+ else panel.prepend(error);
782
+ panel.hidden = false;
783
+ panel.scrollTop = top;
784
+ this.key = key;
785
+ this.focusRequested ||= !same;
786
+ this.editor.querySelector("[data-content]").inert = true;
787
+ if (this.sidebar) this.sidebar.inert = true;
788
+ this.editor.querySelector("[data-backdrop]").hidden = false;
789
+ this.editor.classList.add("kanban--detail");
790
+ }
791
+ close() {
792
+ this.element.hidden = true;
793
+ this.element.replaceChildren();
794
+ this.key = "";
795
+ this.focusRequested = false;
796
+ this.restoreRequested = true;
797
+ this.editor.querySelector("[data-content]").inert = false;
798
+ if (this.sidebar) this.sidebar.inert = false;
799
+ this.editor.querySelector("[data-backdrop]").hidden = true;
800
+ this.editor.classList.remove("kanban--detail");
801
+ }
802
+ flushFocus() {
803
+ if (this.focusRequested && this.open) {
804
+ this.focusRequested = false;
805
+ this.element.focus({ preventScroll: true });
806
+ } else if (this.restoreRequested) {
807
+ this.restoreRequested = false;
808
+ const matches = [...this.editor.querySelectorAll("[data-item], [data-record]")];
809
+ const target = this.restoreTarget?.isConnected ? this.restoreTarget : matches.find((node) => this.restoreItem ? node.dataset.item === this.restoreItem : this.restoreRecord ? node.dataset.record === this.restoreRecord : false);
810
+ (target ?? this.editor.querySelector("button"))?.focus({ preventScroll: true });
811
+ }
812
+ }
813
+ keydown(event) {
814
+ if (!this.open || event.key !== "Tab") return;
815
+ const nodes = [...this.element.querySelectorAll('button, input:not([type=hidden]), select, textarea, a[href], summary, [tabindex="0"]')].filter((node) => !node.matches(":disabled") && node.tabIndex >= 0 && !node.closest("[hidden], [inert]") && !node.hidden && !node.closest("details:not([open]) :not(summary)"));
816
+ const first = nodes[0], last = nodes[nodes.length - 1], active = document.activeElement;
817
+ if (!first) {
818
+ event.preventDefault();
819
+ this.element.focus();
820
+ return;
821
+ }
822
+ if (event.shiftKey && (active === first || active === this.element)) {
823
+ event.preventDefault();
824
+ last.focus();
825
+ } else if (!event.shiftKey && (active === last || !this.element.contains(active))) {
826
+ event.preventDefault();
827
+ first.focus();
828
+ }
829
+ }
830
+ destroy() {
831
+ if (this.sidebar) this.sidebar.inert = false;
832
+ this.editor.classList.remove("kanban--detail");
833
+ }
834
+ };
835
+ function drawerMarkup(ui) {
836
+ return `<button type="button" class="kanban__backdrop" data-backdrop data-action="dismiss-panel" aria-label="${escapeAttr(ui.text("closeDetail"))}" tabindex="-1" hidden></button>
837
+ <section data-panel class="kanban__panel" role="dialog" aria-modal="true" aria-label="${escapeAttr(ui.text("details"))}" tabindex="-1" hidden></section>`;
838
+ }
839
+ function preserveBoardScroll(container, render) {
840
+ const columns = [...container.querySelectorAll("[data-column]")].map((node) => ({ id: node.dataset.column, top: node.scrollTop }));
841
+ const left = container.querySelector(".kanban__columns")?.scrollLeft ?? 0;
842
+ render();
843
+ for (const node of container.querySelectorAll("[data-column]")) node.scrollTop = columns.find((column) => column.id === node.dataset.column)?.top ?? 0;
844
+ const board = container.querySelector(".kanban__columns");
845
+ if (board) board.scrollLeft = left;
846
+ }
847
+
848
+ // src/ui/diagnostics.ts
849
+ function failureDetails(error) {
850
+ if (!error || typeof error !== "object") return {};
851
+ const value2 = error;
852
+ const known = /^(?:Error|TypeError|ReferenceError|SyntaxError|RangeError|SecurityError|QuotaExceededError|NotSupportedError|InvalidStateError|DataCloneError|AbortError|NetworkError)$/;
853
+ const errorName = typeof value2.name === "string" && known.test(value2.name) ? value2.name : void 0;
854
+ const frame = typeof value2.stack === "string" ? value2.stack.match(/(?:^|\/)([A-Za-z0-9_.-]+\.m?js):(\d+):(\d+)/m) : null;
855
+ return { ...errorName ? { errorName } : {}, ...frame ? { source: `${frame[1]}:${frame[2]}:${frame[3]}` } : {} };
856
+ }
857
+
858
+ // src/ui/operations.ts
859
+ function itemOperationsView(ui, view, sessionUrl) {
860
+ return `<button type="button" data-action="refresh-item-operations">${escapeHTML(ui.text("operationRefresh"))}</button><h4>${escapeHTML(ui.text("operationsTitle"))}</h4>
861
+ <ol class="kanban__operations">${view.operations.map((operation) => operationCard(ui, view, operation, sessionUrl)).join("")}</ol>`;
862
+ }
863
+ function operationCard(ui, view, operation, url) {
864
+ const reason = operation.code ? ui.text("errors." + operation.code) : "";
865
+ return `<li data-operation-id="${escapeAttr(operation.id)}"><strong>${escapeHTML(operation.label)}</strong> \xB7 ${escapeHTML(ui.text("operationState." + operation.state))}
866
+ ${operation.reason ? `<p>${escapeHTML(operation.reason)}</p>` : ""}${operation.code ? `<p role="alert">${escapeHTML(reason.endsWith("errors." + operation.code) ? operation.code : reason)}</p>` : ""}
867
+ ${operation.summary ? `<pre class="kanban__operation-summary">${escapeHTML(operation.summary)}</pre>` : ""}
868
+ ${operation.sessionId ? sessionLink(ui, operation.sessionId, ui.text("operationSession"), url?.(operation.sessionId)) : ""}
869
+ <form data-form="item-operation" data-operation="${escapeAttr(operation.id)}" data-revision="${escapeAttr(view.revision)}">
870
+ ${choices(ui, operation)}${operation.confirmation ? `<label><input type="checkbox" name="confirmed" required>${escapeHTML(operation.confirmation)}</label>` : ""}
871
+ <button type="submit"${operation.enabled ? "" : " disabled"}>${escapeHTML(operation.label)}</button></form>${configuration(ui, view, operation)}</li>`;
872
+ }
873
+ function choices(ui, operation) {
874
+ return (operation.fields ?? []).map((field2) => `<label>${escapeHTML(field2.label)}<select name="choice-${escapeAttr(field2.id)}" required>
875
+ <option value="">${escapeHTML(ui.text("operationChoose"))}</option>${field2.choices.map((choice) => `<option value="${escapeAttr(choice.id)}"${field2.selected === choice.id ? " selected" : ""}>${escapeHTML(choice.label + (choice.detail ? " \xB7 " + choice.detail : ""))}</option>`).join("")}</select></label>`).join("");
876
+ }
877
+ function configuration(ui, view, operation) {
878
+ if (!operation.configurable) return "";
879
+ return `<details><summary>${escapeHTML(ui.text("operationConfigure"))}</summary><form data-form="operation-binding" data-operation="${escapeAttr(operation.id)}" data-revision="${escapeAttr(view.revision)}">
880
+ <label>${escapeHTML(ui.text("operationFlow"))}<select name="target" required><option value="">${escapeHTML(ui.text("operationChoose"))}</option>
881
+ ${(view.targets ?? []).map((target) => `<option value="${escapeAttr(JSON.stringify(target.reference))}"${JSON.stringify(target.reference) === JSON.stringify(operation.binding) ? " selected" : ""}>${escapeHTML(target.label)}</option>`).join("")}</select></label>
882
+ <button type="submit">${escapeHTML(ui.text("save"))}</button>${view.targets?.length ? "" : `<p>${escapeHTML(ui.text("operationNoFlows"))}</p>`}</form></details>`;
883
+ }
884
+
885
+ // src/ui/work-resources.ts
886
+ function workResourcesView(ui, resources) {
887
+ if (!resources.length) return `<p>${escapeHTML(ui.text("none"))}</p>`;
888
+ return resources.map((resource) => `<article class="kanban__work-resource" data-work-resource="${escapeAttr(resource.id)}">
889
+ <strong>${escapeHTML(resource.title)}</strong>${copy(ui, resource.title, ui.text("copyWorkResourceName"))} \xB7 ${escapeHTML(ui.text("workResourceState." + resource.state))}
890
+ <p><code>${escapeHTML(resource.location)}</code>${copy(ui, resource.location, ui.text("copyWorkResourcePath"))}</p>
891
+ <dl>${resource.properties.map((row) => `<dt>${escapeHTML(row.label)}</dt><dd>${escapeHTML(row.value)}${copy(ui, row.value, ui.text("copyValue") + " \xB7 " + row.label)}</dd>`).join("")}</dl>
892
+ ${resource.reasons.map((reason) => `<p>${escapeHTML(reasonLabel(ui, reason))}</p>`).join("")}
893
+ <button type="button" data-action="cleanup-work-resource" data-resource="${escapeAttr(resource.id)}" data-version="${escapeAttr(resource.version)}" ${resource.canCleanup ? "" : "disabled"}>${escapeHTML(ui.text("cleanupWorkResource"))}</button>
894
+ </article>`).join("");
895
+ }
896
+ function copy(ui, value2, label) {
897
+ return `<button type="button" class="kanban__copy-button" data-action="copy-resource" data-copy="${escapeAttr(value2)}" title="${escapeAttr(label)}" aria-label="${escapeAttr(label)}">${ui.icons.copy ?? escapeHTML(ui.text("copyValue"))}</button>`;
898
+ }
899
+ function reasonLabel(ui, reason) {
900
+ const key = "workResourceReason." + reason, label = ui.text(key);
901
+ return label.includes(key) ? reason : label;
902
+ }
903
+
904
+ // src/ui/renderer.ts
905
+ function createRenderer(ui) {
906
+ const bind = (fn) => (...args) => fn(ui, ...args);
907
+ return {
908
+ label: ui.text.bind(ui),
909
+ boardView: bind(boardView),
910
+ catalogView: bind(catalogView),
911
+ navigation: bind(navigation),
912
+ button: bind(button),
913
+ detailView: bind(detailView),
914
+ itemEventsView: bind(itemEventsView),
915
+ newBoardForm: bind(newBoardForm),
916
+ newItemForm: bind(newItemForm),
917
+ recordLinks: bind(recordLinks),
918
+ recordsView: bind(recordsView),
919
+ recordDetail: bind(recordDetail),
920
+ recordForm: bind(recordForm),
921
+ executionSessionView: bind(executionSessionView),
922
+ executionBadge: bind(executionBadge),
923
+ copyReference: bind(copyReference)
924
+ };
925
+ }
926
+
927
+ // src/ui/commands.ts
928
+ var value = (data, key) => String(data.get(key) ?? "").trim();
929
+ function readPlan(data, previous) {
930
+ const area = value(data, "area"), milestone = value(data, "milestone"), parentId = value(data, "parentId"), dueAt = value(data, "dueAt");
931
+ return {
932
+ kind: value(data, "kind"),
933
+ ...previous?.execution ? { execution: previous.execution } : {},
934
+ priority: Number(value(data, "priority")),
935
+ references: previous?.references ?? [],
936
+ dependencies: [...new Set(data.getAll("dependencies").flatMap((value2) => String(value2).split(",")).map((id) => id.trim()).filter(Boolean))],
937
+ ...area ? { area } : {},
938
+ ...milestone ? { milestone } : {},
939
+ ...parentId ? { parentId } : {},
940
+ ...dueAt ? { dueAt: new Date(dueAt).getTime() } : {}
941
+ };
942
+ }
943
+ function boardInput(data, actor, id) {
944
+ return {
945
+ id,
946
+ title: value(data, "title"),
947
+ scope: { kind: "system" },
948
+ members: [{ ...actor, responsibility: "Manage personal work", roles: ["worker", "reviewer", "coordinator"] }],
949
+ policy: { requireEstimate: data.has("requireEstimate"), review: "assignee" }
950
+ };
951
+ }
952
+ function formCommand(kind, data, board, item, id = randomUUID()) {
953
+ if (kind === "item") return { type: "create-item", ...value(data, "launchMode") ? { launch: { mode: value(data, "launchMode") } } : {}, item: { id, title: value(data, "title"), responsibility: value(data, "responsibility"), acceptance: value(data, "acceptance"), ...value(data, "itemResource") ? { externalRefs: [JSON.parse(value(data, "itemResource"))] } : {} }, planning: readPlan(data) };
954
+ if (kind === "record") {
955
+ const sessionId = value(data, "sourceSession"), messageId = value(data, "sourceMessage"), revision = value(data, "sourceRevision");
956
+ if ([sessionId, messageId, revision].some(Boolean) && ![sessionId, messageId, revision].every(Boolean)) throw Object.assign(new Error("Missing source fields"), { code: "SOURCE_FIELDS_REQUIRED" });
957
+ return { type: "append-record", record: {
958
+ id,
959
+ kind: value(data, "kind"),
960
+ title: value(data, "title"),
961
+ content: value(data, "content"),
962
+ externalRefs: data.getAll("recordResources").map((value2) => JSON.parse(String(value2))),
963
+ itemIds: [...new Set(data.getAll("itemIds").flatMap((value2) => String(value2).split(",")).map((id2) => id2.trim()).filter(Boolean))],
964
+ sources: sessionId ? [{ sessionId, messageId, revision, role: "user" }] : [],
965
+ ...value(data, "rationale") ? { rationale: value(data, "rationale") } : {},
966
+ ...value(data, "supersedes") ? { supersedes: value(data, "supersedes") } : {}
967
+ } };
968
+ }
969
+ if (!item) throw new Error("ITEM_NOT_FOUND");
970
+ const itemId = item.id, token = item.owner?.token ?? "";
971
+ if (kind === "edit-item") return { type: "edit-item", itemId, title: value(data, "title"), responsibility: value(data, "responsibility"), acceptance: value(data, "acceptance") };
972
+ if (kind === "amendment") return { type: "review-amendment", itemId, amendmentId: value(data, "amendmentId"), accept: value(data, "decision") === "accept", reason: value(data, "reason") || void 0 };
973
+ if (kind === "claim") return { type: "claim", itemId, ...board.policy.requireEstimate !== false ? { estimate: estimate(data) } : {} };
974
+ if (kind === "close-plan") return { type: "close-plan", itemId, resultRef: value(data, "resultRef") };
975
+ if (kind === "submit") return { type: "submit", itemId, token, resultRef: value(data, "resultRef") };
976
+ if (kind === "plan") return { type: "plan-item", itemId, planning: readPlan(data, item.planning) };
977
+ if (kind === "cancel") return { type: "cancel-item", itemId, reason: value(data, "reason") };
978
+ if (kind === "release") return { type: "release-item", itemId, token, next: "backlog", reason: value(data, "reason") };
979
+ if (kind === "block") return { type: "block-item", itemId, token, reason: value(data, "reason") };
980
+ if (kind === "session") return { type: "register-session", itemId, sessionId: value(data, "sessionId"), role: "observer", subscribed: true };
981
+ if (kind === "message") return { type: "message", itemId, message: value(data, "message"), recipientSessionIds: item.participants.filter((p) => p.subscribed).map((p) => p.sessionId) };
982
+ throw new Error("UNKNOWN_COMMAND");
983
+ }
984
+ function estimate(data) {
985
+ return { complexity: Number(value(data, "complexity")), workUnits: Number(value(data, "workUnits")), durationMs: Number(value(data, "duration")) * 6e4, confidence: value(data, "confidence") };
986
+ }
987
+ function actionCommand(action, item) {
988
+ const itemId = item.id, token = item.owner?.token ?? "";
989
+ if (action === "ready" || action === "defer") return { type: "set-stage", itemId, stage: action === "ready" ? "open" : "backlog" };
990
+ if (action === "publish") return { type: "publish-proposal", itemId };
991
+ if (action === "renew") return { type: "renew", itemId, token };
992
+ if (action === "accept" || action === "reject") return { type: "review", itemId, accept: action === "accept" };
993
+ if (action === "archive") return { type: "archive-items", itemIds: [itemId], reason: "manual" };
994
+ if (action === "restore") return { type: "restore-item", itemId };
995
+ if (action === "unblock") return { type: "unblock-item", itemId, token };
996
+ throw new Error("UNKNOWN_COMMAND");
997
+ }
998
+
999
+ // src/ui/index.ts
1000
+ var KanbanWorkbench = class {
1001
+ constructor(options, container) {
1002
+ this.options = options;
1003
+ this.container = container;
1004
+ this.ui = createRenderer(options.presentation);
1005
+ }
1006
+ board;
1007
+ page;
1008
+ actor;
1009
+ automationCatalog;
1010
+ item;
1011
+ editing;
1012
+ itemArchived = false;
1013
+ operations;
1014
+ boardId = "";
1015
+ recordKind;
1016
+ recordId = "";
1017
+ recordPage;
1018
+ recordCursor;
1019
+ suggestions;
1020
+ drawer;
1021
+ itemId = "";
1022
+ query = { view: "all", limit: 50 };
1023
+ activityTimer;
1024
+ generation = 0;
1025
+ eventGeneration = 0;
1026
+ abort = new AbortController();
1027
+ busy = false;
1028
+ background;
1029
+ operationStage = "ui.operation";
1030
+ dirty = false;
1031
+ visible = true;
1032
+ invalid = false;
1033
+ pending;
1034
+ ui;
1035
+ releaseNavigation;
1036
+ windowMounts = /* @__PURE__ */ new Map();
1037
+ async start() {
1038
+ const { sidebar, editor } = this.options, signal = this.abort.signal;
1039
+ sidebar?.classList.add("kanban__sidebar");
1040
+ editor.classList.add("kanban");
1041
+ editor.innerHTML = `<p data-status role="status"></p><p data-error role="alert"></p><div data-retry hidden>${this.ui.button("refresh", "retry")}</div><div data-content class="kanban__content"></div>${drawerMarkup(this.options.presentation)}`;
1042
+ this.drawer = new KanbanDrawer(editor, sidebar);
1043
+ this.suggestions = new KanbanSuggestions(editor, this.options.service, () => this.boardId, this.options.presentation);
1044
+ document.addEventListener("pointerdown", (event) => {
1045
+ for (const menu of editor.querySelectorAll("[data-popover][open]")) if (!menu.contains(event.target)) menu.open = false;
1046
+ }, { signal });
1047
+ editor.addEventListener("toggle", (event) => {
1048
+ const opened = event.target;
1049
+ if (opened instanceof HTMLDetailsElement && opened.open && opened.hasAttribute("data-related-sessions")) {
1050
+ void this.loadRelatedSessions();
1051
+ return;
1052
+ }
1053
+ if (opened instanceof HTMLDetailsElement && opened.open && opened.hasAttribute("data-work-resources")) {
1054
+ void this.loadWorkResources();
1055
+ return;
1056
+ }
1057
+ if (!(opened instanceof HTMLDetailsElement) || !opened.open || !opened.hasAttribute("data-popover")) return;
1058
+ for (const menu of editor.querySelectorAll("[data-popover][open]")) if (menu !== opened) menu.open = false;
1059
+ }, { capture: true, signal });
1060
+ editor.addEventListener("change", (event) => {
1061
+ const target = event.target;
1062
+ if (!(target instanceof HTMLSelectElement) || target.name !== "kind") return;
1063
+ const resource = target.closest("form")?.querySelector('select[name="itemResource"]');
1064
+ if (resource) resource.required = resource.dataset.resourceRequired === "true" && target.selectedOptions[0]?.dataset.executable === "true";
1065
+ }, { signal });
1066
+ editor.addEventListener("keydown", (event) => {
1067
+ const menu = editor.querySelector("[data-popover][open]");
1068
+ if (event.key === "Escape" && menu) {
1069
+ event.preventDefault();
1070
+ event.stopPropagation();
1071
+ menu.open = false;
1072
+ menu.querySelector("summary")?.focus();
1073
+ return;
1074
+ }
1075
+ this.drawer.keydown(event);
1076
+ if (event.key === "Escape" && this.drawer.open) {
1077
+ event.preventDefault();
1078
+ void this.run(() => this.requestClose(false));
1079
+ }
1080
+ }, { signal });
1081
+ for (const element of sidebar ? [sidebar, editor] : [editor]) element.addEventListener("click", (event) => {
1082
+ if (event instanceof MouseEvent && (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey || event.button !== 0)) return;
1083
+ if (!event.target.closest("[data-action], [data-board], [data-item], [data-session], [data-record], [data-context]")) return;
1084
+ void this.run(async () => {
1085
+ await this.click(event);
1086
+ if (element === sidebar) this.options.activated?.();
1087
+ });
1088
+ }, { signal });
1089
+ sidebar?.addEventListener("submit", (event) => {
1090
+ event.preventDefault();
1091
+ void this.run(() => this.submit(event));
1092
+ }, { signal });
1093
+ editor.addEventListener("submit", (event) => {
1094
+ event.preventDefault();
1095
+ void this.run(() => this.submit(event));
1096
+ }, { signal });
1097
+ editor.addEventListener("change", (event) => {
1098
+ const target = event.target;
1099
+ if (target.name === "preparation" && this.board && this.automationCatalog) {
1100
+ const container = editor.querySelector("[data-preparation-options]");
1101
+ if (container) container.innerHTML = preparationFields(this.board, this.automationCatalog, target.value ? JSON.parse(target.value) : null);
1102
+ }
1103
+ }, { signal });
1104
+ editor.addEventListener("input", (event) => {
1105
+ const target = event.target;
1106
+ if (target.closest("[data-form=reference]")) return;
1107
+ if (target.closest("form")) this.generation++;
1108
+ if (target.closest("[data-panel]")) {
1109
+ this.dirty = true;
1110
+ this.options.changed?.();
1111
+ }
1112
+ }, { signal });
1113
+ this.activityTimer = setInterval(() => {
1114
+ if (!this.visible || document.hidden) return;
1115
+ updateTaskCountdowns(this.options.editor, this.options.presentation);
1116
+ if (this.options.service?.queryActivity && (this.board?.automation?.enabled || this.page?.items.some((item) => item.execution) || this.query.execution && this.query.execution !== "all")) this.refreshInBackground(true);
1117
+ }, 15e3);
1118
+ if (this.options.initialResource !== void 0) await this.openResource(this.options.initialResource);
1119
+ else await this.run(() => this.refresh());
1120
+ }
1121
+ open(target) {
1122
+ const path = target.boardId ? `/board/${encodeURIComponent(target.boardId)}${target.recordId ? "/@record/" + encodeURIComponent(target.recordId) : target.itemId ? "/" + encodeURIComponent(target.itemId) : ""}` : "/";
1123
+ return this.openResource(path);
1124
+ }
1125
+ dispose() {
1126
+ return this.destroy();
1127
+ }
1128
+ async openResource(path) {
1129
+ if (this.busy || this.dirty) throw new Error(this.ui.label("unsaved"));
1130
+ this.closePanel();
1131
+ const parts = path.split("/");
1132
+ this.boardId = decodeURIComponent(parts[2] ?? "");
1133
+ this.itemId = decodeURIComponent(parts[3] ?? "");
1134
+ this.recordId = parts[3] === "@record" ? decodeURIComponent(parts[4] ?? "") : "";
1135
+ if (this.recordId) this.itemId = "";
1136
+ this.recordKind = ["@log", "@decision"].includes(parts[3] ?? "") ? parts[3].slice(1) : void 0;
1137
+ if (this.recordKind) this.itemId = "";
1138
+ this.recordCursor = void 0;
1139
+ this.query = { view: "all", limit: 50 };
1140
+ await this.run(() => this.refresh());
1141
+ }
1142
+ createBoard() {
1143
+ return this.run(async () => {
1144
+ if (this.dirty) throw new Error(this.ui.label("unsaved"));
1145
+ this.showBoardForm();
1146
+ });
1147
+ }
1148
+ showBoardForm() {
1149
+ this.itemId = "";
1150
+ this.drawer.show(this.ui.newBoardForm(), "new-board");
1151
+ this.dirty = true;
1152
+ }
1153
+ getActiveResourceId() {
1154
+ return this.boardId ? `/board/${encodeURIComponent(this.boardId)}${this.recordId ? "/@record/" + encodeURIComponent(this.recordId) : this.itemId ? "/" + encodeURIComponent(this.itemId) : this.recordKind ? "/@" + this.recordKind : ""}` : "/";
1155
+ }
1156
+ getViewState() {
1157
+ return {
1158
+ title: this.drawer?.open ? this.panel().querySelector("h3")?.textContent ?? this.board?.title : this.board?.title ?? this.ui.label("title"),
1159
+ context: this.board?.title,
1160
+ dirty: this.dirty,
1161
+ busy: this.busy
1162
+ };
1163
+ }
1164
+ captureRecovery() {
1165
+ if (this.busy || this.dirty) throw new Error(this.ui.label("unsaved"));
1166
+ return null;
1167
+ }
1168
+ restoreRecovery() {
1169
+ }
1170
+ suspendForRecovery() {
1171
+ this.generation++;
1172
+ this.abort.abort();
1173
+ this.suggestions?.destroy();
1174
+ if (this.activityTimer) clearInterval(this.activityTimer);
1175
+ }
1176
+ async destroy() {
1177
+ for (const release of this.windowMounts.values()) release();
1178
+ this.windowMounts.clear();
1179
+ this.suspendForRecovery();
1180
+ this.releaseNavigation?.();
1181
+ this.releaseNavigation = void 0;
1182
+ this.suggestions?.destroy();
1183
+ this.drawer?.destroy();
1184
+ this.options.sidebar?.replaceChildren();
1185
+ this.options.editor.replaceChildren();
1186
+ this.options.editor.inert = false;
1187
+ this.options.editor.classList.remove("kanban");
1188
+ this.options.sidebar?.classList.remove("kanban__sidebar");
1189
+ this.container?.replaceChildren();
1190
+ this.container?.classList.remove("kanban-layout");
1191
+ }
1192
+ invalidateFromEvents() {
1193
+ this.invalid = true;
1194
+ this.eventGeneration++;
1195
+ this.refreshInBackground();
1196
+ }
1197
+ setVisible(visible) {
1198
+ this.visible = visible;
1199
+ if (visible && this.invalid) this.invalidateFromEvents();
1200
+ }
1201
+ refreshInBackground(activityOnly = false) {
1202
+ if (this.background || this.busy || this.dirty || !this.visible || this.abort.signal.aborted) return;
1203
+ if (document.activeElement?.closest("form") && this.options.editor.contains(document.activeElement)) return;
1204
+ const started = Date.now(), operationId = randomUUID();
1205
+ const reading = activityOnly ? this.refreshActivity(this.generation) : this.refresh(), generation = this.generation;
1206
+ this.background = reading.catch((error) => {
1207
+ if (!this.alive(generation)) return;
1208
+ this.invalid = false;
1209
+ if (!this.abort.signal.aborted) {
1210
+ this.reportRead("ui.background", error, operationId, started);
1211
+ this.error(errorText(error, this.ui.label, "ui.background"));
1212
+ }
1213
+ }).finally(() => {
1214
+ this.background = void 0;
1215
+ if (this.invalid && !this.busy && !this.dirty && this.visible && !this.abort.signal.aborted) this.refreshInBackground();
1216
+ });
1217
+ }
1218
+ queryPage() {
1219
+ const service = this.options.service;
1220
+ return this.query.execution && this.query.execution !== "all" && service.queryActivity ? service.queryActivity(this.boardId, this.query) : service.query(this.boardId, this.query);
1221
+ }
1222
+ async refreshActivity(generation) {
1223
+ if (this.item && this.itemId) {
1224
+ await Promise.all([this.loadExecutionSections(generation, this.item, this.itemArchived), this.operations?.operations.some((operation) => ["running", "unknown"].includes(operation.state)) ? this.loadOperations(generation) : void 0]);
1225
+ return;
1226
+ }
1227
+ if (!this.board || this.recordId || this.recordKind || !this.options.service?.queryActivity) return;
1228
+ await this.supplement("ui.activity", generation, () => this.options.service.queryActivity(this.boardId, this.query), (page) => {
1229
+ if (page.revision !== this.page?.revision) {
1230
+ this.invalid = true;
1231
+ this.refreshInBackground();
1232
+ return;
1233
+ }
1234
+ if (JSON.stringify(page) === JSON.stringify(this.page)) return;
1235
+ const sameItems = page.total === this.page.total && page.nextCursor === this.page.nextCursor && JSON.stringify(page.items.map((item) => item.id)) === JSON.stringify(this.page.items.map((item) => item.id));
1236
+ this.page = page;
1237
+ if (sameItems) {
1238
+ const sessions = new Map(page.items.map((item) => [item.id, item.execution]));
1239
+ for (const slot of this.content().querySelectorAll("[data-item-execution]")) {
1240
+ const html = this.ui.executionBadge(sessions.get(slot.dataset.itemExecution), this.options.sessionUrl);
1241
+ if (slot.innerHTML !== html) slot.innerHTML = html;
1242
+ }
1243
+ return;
1244
+ }
1245
+ preserveBoardScroll(this.content(), () => {
1246
+ this.content().innerHTML = this.ui.boardView(this.board, page, this.query, true, !!this.options.service?.automationCatalog, this.options.sessionUrl);
1247
+ });
1248
+ this.mountHeaderActions();
1249
+ });
1250
+ }
1251
+ async refresh() {
1252
+ const service = this.options.service, generation = ++this.generation, events = this.eventGeneration;
1253
+ if (!service) {
1254
+ this.content().innerHTML = `<p>${escapeHTML(this.ui.label("unavailable"))}</p>`;
1255
+ return;
1256
+ }
1257
+ this.operationStage = "ui.snapshot";
1258
+ const pageRequest = this.boardId ? this.queryPage() : void 0;
1259
+ const itemRequest = this.boardId && this.itemId ? service.item(this.boardId, this.itemId) : void 0;
1260
+ const [boards, actor, initialPage, initialItem] = await Promise.all([service.catalog?.() ?? service.list(), service.currentPrincipal(), pageRequest, itemRequest]);
1261
+ if (!this.alive(generation)) return;
1262
+ this.actor = actor;
1263
+ if (!this.boardId && this.options.initialResource === void 0) this.boardId = boards[0]?.id ?? "";
1264
+ this.board = boards.find((board) => board.id === this.boardId);
1265
+ if (this.boardId && !this.board) throw Object.assign(new Error("Board unavailable"), { code: "BOARD_NOT_FOUND" });
1266
+ if (this.query.execution && this.query.execution !== "all" && !service.queryActivity) throw Object.assign(new Error("Execution Session projection unavailable"), { code: "EXECUTION_SESSION_UNAVAILABLE" });
1267
+ const page = this.board ? initialPage ?? await this.queryPage() : void 0;
1268
+ if (!this.alive(generation)) return;
1269
+ this.page = page;
1270
+ this.invalid = events !== this.eventGeneration;
1271
+ if (this.options.sidebar) this.options.sidebar.innerHTML = this.ui.navigation(boards, this.boardId);
1272
+ this.operationStage = "ui.render";
1273
+ preserveBoardScroll(this.content(), () => {
1274
+ this.content().innerHTML = this.board && page ? this.ui.boardView(this.board, page, this.query, !!service.queryActivity, !!service.automationCatalog, this.options.sessionUrl) : this.ui.catalogView(boards);
1275
+ });
1276
+ if (this.recordKind) await this.showRecords(generation);
1277
+ if (!this.alive(generation)) return;
1278
+ this.releaseNavigation?.();
1279
+ this.releaseNavigation = void 0;
1280
+ const navigation2 = this.content().querySelector("[data-navigation]");
1281
+ if (navigation2 && !this.options.mountTitleBar) this.releaseNavigation = this.options.mountNavigation?.(navigation2);
1282
+ if (this.recordId) await this.showRecord(this.recordId, generation);
1283
+ else if (this.itemId) await this.showItem(generation, initialItem);
1284
+ else if (!this.recordKind) void this.refreshActivity(generation);
1285
+ this.mountHeaderActions();
1286
+ this.operationStage = "ui.selection";
1287
+ this.options.selected(this.getActiveResourceId());
1288
+ }
1289
+ async showItem(generation = this.generation, loaded) {
1290
+ const service = this.options.service;
1291
+ if (!service || !this.board || !this.itemId) return;
1292
+ this.operationStage = "ui.item.load";
1293
+ const { item, archive, task } = loaded ?? await service.item(this.boardId, this.itemId);
1294
+ if (!this.alive(generation)) return;
1295
+ this.operationStage = "ui.item.render";
1296
+ const referencesChanged = this.item?.id === item.id && JSON.stringify(this.item.externalRefs) !== JSON.stringify(item.externalRefs);
1297
+ if (!this.drawer.open || this.itemArchived !== !!archive || JSON.stringify(this.item) !== JSON.stringify(item)) this.drawer.show(this.ui.detailView(this.board, item, this.actor, !!archive, [], !!service.executionSession, this.options.reviewGuidance?.(this.board, item), !!service.itemOperations && !archive && (this.options.itemOperationsAvailable?.(this.board, item) ?? true), task ?? projectKanbanTask(item), !!service.fixTask), "item:" + item.id);
1298
+ this.item = item;
1299
+ this.itemArchived = !!archive;
1300
+ this.loadItemSections(generation, item, !!archive);
1301
+ const resources = this.panel().querySelector("[data-work-resources]");
1302
+ if (referencesChanged && resources) {
1303
+ resources.dataset.associationRevision = String(Number(resources.dataset.associationRevision ?? 0) + 1);
1304
+ delete resources.dataset.loaded;
1305
+ if (resources.open) void this.loadWorkResources(false, true);
1306
+ }
1307
+ this.options.selected(this.getActiveResourceId());
1308
+ }
1309
+ loadItemSections(generation, item, archived) {
1310
+ const service = this.options.service, boardId = this.boardId;
1311
+ if (service.executionSession && !this.panel().querySelector("[data-related-sessions]"))
1312
+ this.setSection("[data-related-sessions-section]", `<details data-related-sessions><summary>${escapeHTML(this.ui.label("relatedSessions"))}</summary><button type="button" data-action="refresh-related-sessions">${escapeHTML(this.ui.label("refresh"))}</button><div data-related-sessions-body></div></details>`);
1313
+ if (service.workResources) {
1314
+ const resources = this.panel().querySelector("[data-work-resources]");
1315
+ if (!resources) this.setSection("[data-work-resource-section]", `<details data-work-resources><summary>${escapeHTML(this.ui.label("workResourceTitle"))}</summary><p>${escapeHTML(this.ui.label("workResourceHelp"))}</p><div class="kanban__actions"><button type="button" data-action="refresh-work-resources">${escapeHTML(this.ui.label("refresh"))}</button><button type="button" data-action="inspect-work-resources">${escapeHTML(this.ui.label("inspectWorkResources"))}</button></div><div data-work-resource-body></div></details>`);
1316
+ }
1317
+ void this.supplement("ui.item.events", generation, () => service.events(boardId, 0, 200), (events) => {
1318
+ this.setSection("[data-item-events]", this.ui.itemEventsView(events.filter((event) => event.itemId === item.id).reverse()));
1319
+ }, "[data-item-events]");
1320
+ void this.supplement("ui.item.records", generation, () => service.records(boardId, { itemId: item.id, limit: 10 }), (records) => {
1321
+ this.setSection("[data-related-records]", `<h4>${escapeHTML(this.ui.label("relatedRecords"))} (${records.total})</h4>${this.ui.recordLinks(records, this.board)}<div class="kanban__actions">${this.ui.button("new-log", "newLog")}${this.ui.button("new-decision", "newDecision")}</div>`);
1322
+ }, "[data-related-records]");
1323
+ void this.loadOperations(generation);
1324
+ void this.loadExecutionSections(generation, item, archived, true);
1325
+ if (!service.executionSession) this.loadReferenceSection(generation, item);
1326
+ void this.supplement("ui.item.children", generation, () => this.showChildren(generation), () => {
1327
+ }, "[data-children]");
1328
+ }
1329
+ async loadExecutionSections(generation, item, archived, references = false) {
1330
+ const service = this.options.service, boardId = this.boardId, reads = [];
1331
+ if (service.executionSession) reads.push(this.supplement("ui.item.session", generation, () => service.executionSession(boardId, item.id), (execution) => {
1332
+ this.setSection("[data-execution-session]", this.ui.executionSessionView(execution, !!this.options.executionReference && !archived && !["completed", "cancelled"].includes(item.status) && item.launch?.admittedAt === void 0, this.options.sessionUrl));
1333
+ if (references) this.loadReferenceSection(generation, item, new Set(execution.history.map((binding) => binding.sessionId)));
1334
+ }, "[data-execution-session]"));
1335
+ if (this.board?.automation && service.analysisStatus) reads.push(this.supplement("ui.item.analysis", generation, () => service.analysisStatus(boardId, item.id), (analysis) => {
1336
+ this.setSection("[data-analysis]", analysisView(this.options.presentation, analysis ?? null));
1337
+ }, "[data-analysis]"));
1338
+ await Promise.all(reads);
1339
+ }
1340
+ async loadOperations(generation) {
1341
+ const service = this.options.service, item = this.item;
1342
+ if (!service?.itemOperations || !item || !this.panel().querySelector("[data-item-operations]")) return;
1343
+ await this.supplement("ui.item.operations", generation, () => service.itemOperations(this.boardId, item.id), (view) => {
1344
+ this.operations = view;
1345
+ this.setSection("[data-item-operations]", itemOperationsView(this.options.presentation, view, this.options.sessionUrl));
1346
+ }, "[data-item-operations]");
1347
+ }
1348
+ async submitOperation(form, data, binding) {
1349
+ const service = this.options.service, action = form.dataset.operation, itemId = this.item.id;
1350
+ const request = { operationId: form.dataset.resourceId, itemId, action, expectedRevision: form.dataset.revision };
1351
+ const entry = this.operations?.operations.find((operation) => operation.id === action);
1352
+ this.operationStage = binding ? "ui.operation.bind" : "ui.operation.start";
1353
+ const view = binding ? await service.bindItemOperation(this.boardId, { ...request, target: JSON.parse(value(data, "target")) }) : await service.runItemOperation(this.boardId, {
1354
+ ...request,
1355
+ confirmed: data.has("confirmed"),
1356
+ selections: Object.fromEntries((entry?.fields ?? []).map((field2) => [field2.id, value(data, "choice-" + field2.id)]))
1357
+ });
1358
+ this.dirty = false;
1359
+ this.operations = view;
1360
+ this.options.committed?.();
1361
+ this.setSection("[data-item-operations]", itemOperationsView(this.options.presentation, view, this.options.sessionUrl));
1362
+ this.options.changed?.();
1363
+ const result = view.operations.find((operation) => operation.id === action);
1364
+ if (!binding && result?.state === "succeeded") await this.refresh();
1365
+ if (!binding && entry?.openSessionOnRun && result?.sessionId) await this.options.openSession(result.sessionId);
1366
+ }
1367
+ loadReferenceSection(generation, item, skip) {
1368
+ void this.supplement("ui.item.references", generation, () => this.showReferenceLinks(item.externalRefs ?? [], generation, skip), () => {
1369
+ }, "[data-reference-links]");
1370
+ }
1371
+ async supplement(stage, generation, read, apply, selector) {
1372
+ const started = Date.now(), operationId = randomUUID();
1373
+ try {
1374
+ const value2 = await read();
1375
+ if (!this.alive(generation) || this.dirty) return;
1376
+ apply(value2);
1377
+ try {
1378
+ this.options.presentation.log?.({ operationId, stage, result: "success", durationMs: Date.now() - started });
1379
+ } catch {
1380
+ }
1381
+ } catch (error) {
1382
+ if (!this.alive(generation)) return;
1383
+ this.reportRead(stage, error, operationId, started);
1384
+ const slot = selector ? this.panel().querySelector(selector) : void 0;
1385
+ if (slot) {
1386
+ const notice = slot.querySelector("[data-read-error]") ?? document.createElement("p");
1387
+ notice.setAttribute("role", "alert");
1388
+ notice.setAttribute("data-read-error", "");
1389
+ notice.textContent = errorText(error, this.ui.label, stage);
1390
+ if (!notice.parentElement) slot.append(notice);
1391
+ } else this.error(errorText(error, this.ui.label, stage));
1392
+ }
1393
+ }
1394
+ setSection(selector, html) {
1395
+ const slot = this.panel().querySelector(selector);
1396
+ if (slot && slot.innerHTML !== html) slot.innerHTML = html;
1397
+ }
1398
+ reportRead(stage, error, operationId = randomUUID(), started = Date.now()) {
1399
+ const raw = error?.code;
1400
+ try {
1401
+ this.options.presentation.log?.({ operationId, stage, result: "failure", ...failureDetails(error), code: typeof raw === "string" && /^[A-Z][A-Z0-9_]{0,79}$/.test(raw) ? raw : "KANBAN_OPERATION_FAILED", durationMs: Date.now() - started });
1402
+ } catch {
1403
+ }
1404
+ }
1405
+ async showChildren(generation, cursor) {
1406
+ const { service } = this.options, item = this.item, board = this.board;
1407
+ if (!service || !item || !board || !childType(board, item)) return;
1408
+ const query = { parentId: item.id, view: "all", limit: 50, ...cursor ? { cursor } : {} };
1409
+ const page = await (service.queryActivity?.(board.id, query) ?? service.query(board.id, query));
1410
+ if (!this.alive(generation)) return;
1411
+ this.panel().querySelector("[data-children]").innerHTML = childrenView(this.options.presentation, page, cursor, !["completed", "cancelled"].includes(item.status) && !item.closedAt, this.options.sessionUrl);
1412
+ }
1413
+ async showRecords(generation) {
1414
+ if (!this.recordKind || !this.options.service) return;
1415
+ let page;
1416
+ try {
1417
+ page = await this.options.service.records(this.boardId, { kind: this.recordKind, limit: 50, ...this.recordCursor ? { cursor: this.recordCursor } : {} });
1418
+ } catch (error) {
1419
+ if (error?.code !== "CURSOR_STALE" || !this.recordCursor) throw error;
1420
+ this.recordCursor = void 0;
1421
+ page = await this.options.service.records(this.boardId, { kind: this.recordKind, limit: 50 });
1422
+ }
1423
+ if (!this.alive(generation)) return;
1424
+ this.recordPage = page;
1425
+ this.content().innerHTML = this.ui.recordsView(page, this.recordKind, !!this.recordCursor, this.board);
1426
+ }
1427
+ async showRecord(id, generation = this.generation) {
1428
+ if (!this.options.service) return;
1429
+ const record = await this.options.service.record(this.boardId, id);
1430
+ const executions = record.kind === "decision" ? await this.options.service.recordExecutions?.(this.boardId, id) : void 0;
1431
+ if (!this.alive(generation)) return;
1432
+ this.recordId = id;
1433
+ this.drawer.show(this.ui.recordDetail(record, this.board, executions), "record:" + id);
1434
+ await this.showReferenceLinks(record.externalRefs ?? [], generation);
1435
+ this.options.selected(this.getActiveResourceId());
1436
+ }
1437
+ async showReferenceLinks(references, generation, skip = /* @__PURE__ */ new Set()) {
1438
+ if (!this.options.referenceLink) return;
1439
+ const started = Date.now();
1440
+ const results = await Promise.allSettled(references.filter((ref) => !skip.has(ref.id)).map((ref) => this.options.referenceLink(ref)));
1441
+ if (!this.alive(generation)) return;
1442
+ const resolved = results.flatMap((result) => result.status === "fulfilled" && result.value ? [result.value] : []);
1443
+ const links = [...new Map(resolved.map((link) => [link.href, link])).values()];
1444
+ const slot = this.panel().querySelector("[data-reference-links]");
1445
+ if (slot) slot.innerHTML = links.map((link) => linkPair(this.options.presentation, link.href, link.label, link.sessionId)).join("");
1446
+ const errors = results.filter((result) => result.status === "rejected");
1447
+ if (errors.length) {
1448
+ const raw = errors[0].reason?.code, code = typeof raw === "string" && /^[A-Z][A-Z0-9_]{0,79}$/.test(raw) ? raw : "SESSION_LINK_UNAVAILABLE";
1449
+ if (slot) {
1450
+ const error = document.createElement("p");
1451
+ error.setAttribute("role", "alert");
1452
+ error.textContent = this.ui.label("sessionLinkUnavailable") + " (" + code + ")";
1453
+ slot.append(error);
1454
+ }
1455
+ this.options.presentation.log?.({ operationId: randomUUID(), stage: "ui.reference-links", result: "failure", code, ...failureDetails(errors[0].reason), durationMs: Date.now() - started });
1456
+ }
1457
+ }
1458
+ async loadWorkResources(inspect = false, refresh = false) {
1459
+ const service = this.options.service, boardId = this.boardId, itemId = this.item?.id;
1460
+ const section = this.panel().querySelector("[data-work-resources]");
1461
+ if (!service?.workResources || !itemId || !section || section.dataset.loading || !refresh && section.dataset.loaded) return;
1462
+ const operationId = randomUUID(), started = Date.now(), revision = section.dataset.associationRevision;
1463
+ section.dataset.loading = "true";
1464
+ if (!section.dataset.loaded) this.setSection("[data-work-resource-body]", `<p role="status">${escapeHTML(this.ui.label("loading"))}</p>`);
1465
+ try {
1466
+ const resources = await service.workResources(boardId, itemId, { inspect });
1467
+ if (this.abort.signal.aborted || !section.isConnected || this.item?.id !== itemId || this.boardId !== boardId || revision !== section.dataset.associationRevision) return;
1468
+ this.setSection("[data-work-resource-body]", workResourcesView(this.options.presentation, resources));
1469
+ section.dataset.loaded = "true";
1470
+ } catch (error) {
1471
+ if (!section.isConnected || this.abort.signal.aborted) return;
1472
+ this.reportRead("ui.item.work-resources", error, operationId, started);
1473
+ const body = section.querySelector("[data-work-resource-body]"), notice = document.createElement("p");
1474
+ notice.setAttribute("role", "alert");
1475
+ notice.textContent = errorText(error, this.ui.label, "ui.item.work-resources");
1476
+ body?.append(notice);
1477
+ } finally {
1478
+ delete section.dataset.loading;
1479
+ if (!this.abort.signal.aborted && section.isConnected && section.open && revision !== section.dataset.associationRevision) void this.loadWorkResources(false, true);
1480
+ }
1481
+ }
1482
+ async loadRelatedSessions(refresh = false) {
1483
+ const service = this.options.service, boardId = this.boardId, itemId = this.item?.id;
1484
+ const details = this.panel().querySelector("[data-related-sessions]");
1485
+ if (!service?.executionSession || !itemId || !details || details.dataset.loading || !refresh && details.dataset.loaded) return;
1486
+ details.dataset.loading = "true";
1487
+ this.setSection("[data-related-sessions-body]", `<p role="status">${escapeHTML(this.ui.label("loading"))}</p>`);
1488
+ try {
1489
+ await this.supplement("ui.item.related-sessions", this.generation, () => service.executionSession(boardId, itemId, { includeRelated: true }), (detail) => {
1490
+ if (this.item?.id !== itemId || this.boardId !== boardId) return;
1491
+ this.setSection("[data-related-sessions-body]", relatedSessionsView(this.options.presentation, detail, this.options.sessionUrl));
1492
+ details.dataset.loaded = "true";
1493
+ }, "[data-related-sessions-body]");
1494
+ } finally {
1495
+ delete details.dataset.loading;
1496
+ }
1497
+ }
1498
+ async click(event) {
1499
+ const target = event.target.closest("button, a");
1500
+ if (!target || target.matches(":disabled")) return;
1501
+ if (target.dataset.action === "refresh-related-sessions") {
1502
+ await this.loadRelatedSessions(true);
1503
+ return;
1504
+ }
1505
+ for (const menu of this.options.editor.querySelectorAll("[data-popover][open]")) if (!menu.contains(target)) menu.open = false;
1506
+ if (target.dataset.session) {
1507
+ event.preventDefault();
1508
+ await this.options.openSession(target.dataset.session);
1509
+ return;
1510
+ }
1511
+ if (this.dirty && !["close-panel", "copy-reference", "copy-link", "copy-resource"].includes(target.dataset.action ?? "")) throw new Error(this.ui.label("unsaved"));
1512
+ const path = this.navigationTarget(target);
1513
+ if (path && this.options.navigate) {
1514
+ await this.options.navigate(path);
1515
+ return;
1516
+ }
1517
+ if (target.dataset.board) {
1518
+ await this.navigate(target.dataset.board);
1519
+ return;
1520
+ }
1521
+ if (target.dataset.context) {
1522
+ this.recordKind = target.dataset.context === "board" ? void 0 : target.dataset.context;
1523
+ this.recordCursor = void 0;
1524
+ this.closePanel();
1525
+ await this.refresh();
1526
+ return;
1527
+ }
1528
+ if (target.dataset.record) {
1529
+ this.itemId = "";
1530
+ this.item = void 0;
1531
+ await this.showRecord(target.dataset.record);
1532
+ return;
1533
+ }
1534
+ if (target.dataset.item) {
1535
+ this.recordId = "";
1536
+ this.itemId = target.dataset.item;
1537
+ await this.showItem();
1538
+ return;
1539
+ }
1540
+ const action = target.dataset.action;
1541
+ if (action === "continue-execution" && this.item && target.dataset.dispatch) {
1542
+ await this.execute({ type: "continue-execution", previousDispatchId: target.dataset.dispatch, dispatchId: randomUUID() });
1543
+ return;
1544
+ }
1545
+ if (action === "refresh-work-resources" || action === "inspect-work-resources") {
1546
+ await this.loadWorkResources(action === "inspect-work-resources", true);
1547
+ return;
1548
+ }
1549
+ if (action === "cleanup-work-resource" && this.item && this.options.service?.cleanupWorkResource) {
1550
+ if (!globalThis.confirm(this.ui.label("cleanupWorkResourceConfirm"))) return;
1551
+ try {
1552
+ await this.options.service.cleanupWorkResource(this.boardId, { operationId: randomUUID(), itemId: this.item.id, resourceId: target.dataset.resource, version: target.dataset.version });
1553
+ } finally {
1554
+ await this.loadWorkResources(false, true);
1555
+ }
1556
+ return;
1557
+ }
1558
+ if (action === "close-panel" || action === "dismiss-panel") {
1559
+ await this.requestClose(action === "close-panel");
1560
+ return;
1561
+ }
1562
+ if (action === "copy-reference" || action === "copy-link" || action === "copy-resource") {
1563
+ await this.ui.copyReference(target.closest("[data-action]"));
1564
+ return;
1565
+ }
1566
+ if (action === "automation" && this.board && this.options.service?.automationCatalog) {
1567
+ this.automationCatalog = await this.options.service.automationCatalog();
1568
+ this.itemId = "";
1569
+ this.drawer.show(automationForm(this.options.presentation, this.board, this.automationCatalog), "automation");
1570
+ this.dirty = true;
1571
+ return;
1572
+ }
1573
+ if (action === "task-edit" && this.item && this.board) {
1574
+ const editId = randomUUID(), itemId = this.item.id;
1575
+ await this.execute({ type: "begin-edit", itemId, editId, leaseMs: 3e5 });
1576
+ this.editing = { itemId, editId };
1577
+ const resources = await this.options.service?.itemResources?.(this.boardId);
1578
+ if (resources) resources.selected = this.item.externalRefs?.find((ref) => resources.resources.some((r) => r.reference.system === ref.system && r.reference.id === ref.id));
1579
+ this.drawer.show(editTaskForm(this.options.presentation, this.board, this.item, editId, resources), "edit:" + itemId);
1580
+ this.dirty = true;
1581
+ return;
1582
+ }
1583
+ if (action === "task-fix" && this.item && this.options.service?.fixTask) {
1584
+ target.dataset.operationId ??= randomUUID();
1585
+ const fix = await this.options.service.fixTask(this.boardId, { itemId: this.item.id, operationId: target.dataset.operationId });
1586
+ await this.options.openSession(fix.sessionId);
1587
+ return;
1588
+ }
1589
+ if (action?.startsWith("task-") && this.item) {
1590
+ if (action === "task-cancel") await this.execute({ type: "cancel-item", itemId: this.item.id, reason: this.ui.label("taskAction.cancel") });
1591
+ else if (action === "task-run") await this.execute({ type: "request-run", itemId: this.item.id });
1592
+ else if (action === "task-pause" || action === "task-resume") await this.execute({ type: "request-control", itemId: this.item.id, controlId: randomUUID(), action: action === "task-pause" ? "pause" : "resume" });
1593
+ return;
1594
+ }
1595
+ if (action === "reanalyze" && this.item?.planning) {
1596
+ await this.execute({ type: "retry-analysis", itemId: this.item.id, retryId: randomUUID() });
1597
+ return;
1598
+ }
1599
+ if (action === "new-board") {
1600
+ this.showBoardForm();
1601
+ return;
1602
+ }
1603
+ if (action === "children-next" || action === "children-first") {
1604
+ await this.showChildren(this.generation, action === "children-next" ? target.dataset.cursor : void 0);
1605
+ return;
1606
+ }
1607
+ if (action === "new-child" && this.board && this.item && childType(this.board, this.item)) {
1608
+ await this.showItemForm(this.item);
1609
+ return;
1610
+ }
1611
+ if (action === "new-item" && this.board) {
1612
+ await this.showItemForm();
1613
+ return;
1614
+ }
1615
+ if (action === "new-log" || action === "new-decision") {
1616
+ const resources = action === "new-decision" ? await this.options.service?.recordResources?.() : [];
1617
+ this.recordId = "";
1618
+ this.drawer.show(this.ui.recordForm(action === "new-log" ? "log" : "decision", this.board, this.itemId, resources), action);
1619
+ this.dirty = true;
1620
+ return;
1621
+ }
1622
+ if ((action === "run-record" || action === "run-record-separate") && this.options.service?.runRecord) {
1623
+ const record = await this.options.service.record(this.boardId, this.recordId);
1624
+ target.dataset.operationId ??= randomUUID();
1625
+ await this.options.service.runRecord(this.boardId, { operationId: target.dataset.operationId, recordId: record.id, expectedRevision: record.revision, placement: action === "run-record-separate" ? "new-board" : "current-board" });
1626
+ this.options.committed?.();
1627
+ await this.refresh();
1628
+ return;
1629
+ }
1630
+ if (action === "scroll-left" || action === "scroll-right") {
1631
+ const board = this.content().querySelector(".kanban__columns");
1632
+ if (board) board.scrollLeft += board.clientWidth * (action === "scroll-left" ? -0.8 : 0.8);
1633
+ return;
1634
+ }
1635
+ if (action === "records-next" || action === "records-first") {
1636
+ this.recordCursor = action === "records-next" ? this.recordPage?.nextCursor : void 0;
1637
+ await this.refresh();
1638
+ return;
1639
+ }
1640
+ if ((action === "confirm-record" || action === "reject-record") && this.recordId) {
1641
+ await this.execute({ type: "review-record", recordId: this.recordId, accept: action === "confirm-record" });
1642
+ return;
1643
+ }
1644
+ if (action === "next" || action === "first") {
1645
+ this.query.cursor = action === "next" ? this.page?.nextCursor : void 0;
1646
+ await this.refresh();
1647
+ return;
1648
+ }
1649
+ if (action === "refresh-item-operations") {
1650
+ this.dirty = false;
1651
+ await this.loadOperations(this.generation);
1652
+ this.options.changed?.();
1653
+ return;
1654
+ }
1655
+ if (action === "refresh") {
1656
+ await this.refresh();
1657
+ return;
1658
+ }
1659
+ if (action && this.item) await this.execute(actionCommand(action, this.item));
1660
+ }
1661
+ async openReference(reference) {
1662
+ if (this.dirty) throw new Error(this.ui.label("unsaved"));
1663
+ const target = await this.options.service.resolve(reference);
1664
+ if (this.options.navigate) {
1665
+ const leaf = target.recordId ? "/@record/" + encodeURIComponent(target.recordId) : target.itemId ? "/" + encodeURIComponent(target.itemId) : "";
1666
+ await this.options.navigate(`/board/${encodeURIComponent(target.boardId)}${leaf}`);
1667
+ return;
1668
+ }
1669
+ this.closePanel();
1670
+ this.boardId = target.boardId;
1671
+ this.itemId = target.itemId ?? "";
1672
+ this.recordId = target.recordId ?? "";
1673
+ this.recordKind = void 0;
1674
+ this.recordCursor = void 0;
1675
+ this.query = { view: "all", limit: 50 };
1676
+ await this.refresh();
1677
+ this.options.activated?.();
1678
+ }
1679
+ async navigate(id) {
1680
+ this.boardId = id;
1681
+ this.itemId = "";
1682
+ this.recordKind = void 0;
1683
+ this.recordCursor = void 0;
1684
+ this.query = { view: "all", limit: 50 };
1685
+ this.closePanel();
1686
+ await this.refresh();
1687
+ }
1688
+ async submit(event) {
1689
+ const form = event.target, kind = form.dataset.form, data = new FormData(form);
1690
+ form.dataset.resourceId ??= randomUUID();
1691
+ if (!form.reportValidity()) return;
1692
+ if (kind === "item-operation" || kind === "operation-binding") {
1693
+ await this.submitOperation(form, data, kind === "operation-binding");
1694
+ return;
1695
+ }
1696
+ if (kind === "reference") {
1697
+ await this.openReference(value(data, "reference"));
1698
+ return;
1699
+ }
1700
+ if (kind === "filter") {
1701
+ if (this.dirty) throw new Error(this.ui.label("unsaved"));
1702
+ this.query = { phase: value(data, "phase") || void 0, view: value(data, "view"), execution: value(data, "execution") || "all", search: value(data, "search"), area: value(data, "area") || void 0, milestone: value(data, "milestone") || void 0, limit: 50 };
1703
+ await this.refresh();
1704
+ return;
1705
+ }
1706
+ if (kind === "board" && this.actor && this.options.service) {
1707
+ const created = await this.options.service.create(boardInput(data, this.actor, form.dataset.resourceId));
1708
+ this.options.committed?.();
1709
+ this.dirty = false;
1710
+ await this.navigate(created.id);
1711
+ return;
1712
+ }
1713
+ if (kind === "automation" && this.automationCatalog) {
1714
+ await this.execute({ type: "configure-automation", settings: {
1715
+ enabled: data.has("enabled"),
1716
+ autoAccept: data.has("autoAccept"),
1717
+ ...value(data, "preparation") ? {
1718
+ preparation: JSON.parse(value(data, "preparation")),
1719
+ preparationOptions: Object.fromEntries([...data.entries()].filter(([key]) => key.startsWith("preparation-option-")).map(([key, value2]) => [key.slice("preparation-option-".length), String(value2)]))
1720
+ } : {},
1721
+ planner: JSON.parse(value(data, "planner")),
1722
+ executor: JSON.parse(value(data, "executor")),
1723
+ coordinator: this.automationCatalog.coordinator,
1724
+ worker: this.automationCatalog.worker
1725
+ } });
1726
+ return;
1727
+ }
1728
+ if (kind === "task-reply" && this.item && this.options.service?.replyTask) {
1729
+ await this.options.service.replyTask(this.boardId, { itemId: this.item.id, attemptId: value(data, "attemptId"), replyTo: value(data, "replyTo"), operationId: form.dataset.resourceId, content: value(data, "content") });
1730
+ this.dirty = false;
1731
+ await this.refresh();
1732
+ return;
1733
+ }
1734
+ if (kind === "save-edit" && this.item) {
1735
+ const planning = formCommand("plan", data, this.board, this.item);
1736
+ if (planning.type !== "plan-item") return;
1737
+ await this.execute({ type: "save-edit", launchMode: value(data, "launchMode"), itemId: this.item.id, editId: value(data, "editId"), title: value(data, "title"), responsibility: value(data, "responsibility"), acceptance: value(data, "acceptance"), planning: planning.planning, externalRefs: [...(this.item.externalRefs ?? []).filter((ref) => !value(data, "itemResource") || ref.system !== JSON.parse(value(data, "itemResource")).system), ...value(data, "itemResource") ? [JSON.parse(value(data, "itemResource"))] : []] });
1738
+ this.editing = void 0;
1739
+ return;
1740
+ }
1741
+ if (kind === "amendment") data.set("amendmentId", form.dataset.amendment ?? "");
1742
+ if (kind === "record-link" && this.recordId) {
1743
+ await this.execute({ type: "link-record", recordId: this.recordId, itemId: value(data, "itemId") });
1744
+ return;
1745
+ }
1746
+ if (kind && this.board) {
1747
+ if (kind === "execution-session" && this.item && this.options.executionReference) {
1748
+ await this.execute({ type: "link-reference", itemId: this.item.id, reference: this.options.executionReference(value(data, "executionSession"), form.dataset.resourceId) });
1749
+ return;
1750
+ }
1751
+ await this.execute(formCommand(kind, data, this.board, this.item, form.dataset.resourceId));
1752
+ }
1753
+ }
1754
+ async showItemForm(parent) {
1755
+ const board = this.board, generation = this.generation;
1756
+ if (!board) return;
1757
+ const resources = await this.options.service?.itemResources?.(board.id, parent?.id);
1758
+ if (this.abort.signal.aborted || generation !== this.generation) return;
1759
+ this.itemId = "";
1760
+ this.drawer.show(this.ui.newItemForm(board, parent, resources), parent ? "new-child" : "new-item");
1761
+ this.dirty = true;
1762
+ }
1763
+ async execute(command) {
1764
+ if (!this.board || !this.options.service) return;
1765
+ if (command.type === "review") {
1766
+ const workflow = await this.options.service.workflow(this.board.id);
1767
+ const dispatch = Object.values(workflow.dispatches).find((dispatch2) => dispatch2.itemId === ("itemId" in command ? command.itemId : "") && dispatch2.outcome === "succeeded");
1768
+ if (dispatch) command = { type: "accept-execution", dispatchId: dispatch.id, accept: command.accept };
1769
+ }
1770
+ const fingerprint = JSON.stringify(command);
1771
+ if (this.pending && (this.pending.boardId !== this.board.id || this.pending.fingerprint !== fingerprint)) throw Object.assign(new Error("Pending write"), { code: "WRITE_OUTCOME_UNKNOWN" });
1772
+ this.pending ??= { boardId: this.board.id, fingerprint, request: { operationId: randomUUID(), expectedRevision: this.board.revision, command } };
1773
+ try {
1774
+ await this.options.service.execute(this.board.id, this.pending.request);
1775
+ } catch (error) {
1776
+ if (error?.name === "KanbanError" || error?.outcome === "not-committed") this.pending = void 0;
1777
+ throw error;
1778
+ }
1779
+ this.pending = void 0;
1780
+ this.options.committed?.();
1781
+ if (command.type === "configure-automation") this.closePanel();
1782
+ this.dirty = false;
1783
+ this.query.cursor = void 0;
1784
+ this.recordCursor = void 0;
1785
+ if (command.type === "create-item") this.itemId = command.item.id;
1786
+ if (command.type === "append-record") {
1787
+ this.recordId = command.record.id;
1788
+ this.itemId = "";
1789
+ this.item = void 0;
1790
+ }
1791
+ await this.refresh();
1792
+ }
1793
+ async run(action) {
1794
+ if (this.busy || this.abort.signal.aborted) return;
1795
+ const operationId = randomUUID(), started = Date.now();
1796
+ this.operationStage = "ui.operation";
1797
+ this.generation++;
1798
+ this.busy = true;
1799
+ this.status(this.ui.label("loading"));
1800
+ this.error("");
1801
+ this.options.editor.setAttribute("aria-busy", "true");
1802
+ let succeeded = false;
1803
+ try {
1804
+ await action();
1805
+ succeeded = true;
1806
+ } catch (error) {
1807
+ const raw = error?.code;
1808
+ try {
1809
+ this.options.presentation.log?.({ operationId, stage: this.operationStage, result: "failure", ...failureDetails(error), code: typeof raw === "string" && /^[A-Z][A-Z0-9_]{0,79}$/.test(raw) ? raw : "KANBAN_OPERATION_FAILED", durationMs: Date.now() - started });
1810
+ } catch {
1811
+ }
1812
+ if (!this.abort.signal.aborted) this.error(errorText(error, this.ui.label, this.operationStage));
1813
+ } finally {
1814
+ this.busy = false;
1815
+ if (!this.abort.signal.aborted) {
1816
+ this.status("");
1817
+ this.options.editor.removeAttribute("aria-busy");
1818
+ this.options.editor.inert = false;
1819
+ this.mountHeaderActions();
1820
+ this.drawer.flushFocus();
1821
+ this.options.changed?.();
1822
+ if (succeeded && this.invalid && this.visible && !this.dirty) this.refreshInBackground();
1823
+ }
1824
+ }
1825
+ }
1826
+ navigationTarget(target) {
1827
+ if (target.dataset.board) return `/board/${encodeURIComponent(target.dataset.board)}`;
1828
+ const board = `/board/${encodeURIComponent(this.boardId)}`;
1829
+ if (target.dataset.item) return `${board}/${encodeURIComponent(target.dataset.item)}`;
1830
+ if (target.dataset.record) return `${board}/@record/${encodeURIComponent(target.dataset.record)}`;
1831
+ if (target.dataset.context) return board + (target.dataset.context === "board" ? "" : "/@" + target.dataset.context);
1832
+ }
1833
+ mountHeaderActions() {
1834
+ for (const [slot, release] of this.windowMounts) if (!this.options.editor.contains(slot)) {
1835
+ release();
1836
+ this.windowMounts.delete(slot);
1837
+ }
1838
+ if (this.options.mountTitleBar) {
1839
+ for (const header of this.options.editor.querySelectorAll("[data-titlebar]")) if (!this.windowMounts.has(header)) this.mountTitleBar(header);
1840
+ return;
1841
+ }
1842
+ for (const slot of this.options.editor.querySelectorAll("[data-window-navigation]")) {
1843
+ if (!this.windowMounts.has(slot) && this.options.mountWindowList) this.windowMounts.set(slot, this.options.mountWindowList(slot));
1844
+ }
1845
+ for (const header of this.options.editor.querySelectorAll("[data-titlebar]")) if (!this.windowMounts.has(header)) this.mountDefaultTitleBar(header);
1846
+ }
1847
+ mountDefaultTitleBar(header) {
1848
+ const back = header.querySelector("[data-titlebar-back]") ?? void 0;
1849
+ const title = header.querySelector("[data-titlebar-title]") ?? void 0;
1850
+ back?.classList.add("ui-nav-button");
1851
+ header.querySelector(".kanban__menu > summary")?.classList.add("ui-nav-button");
1852
+ const view = mountSharedTitleBar(header, {
1853
+ title,
1854
+ back,
1855
+ leading: [...header.querySelectorAll("[data-navigation] > button")],
1856
+ trailing: [...header.querySelectorAll("[data-window-navigation]")],
1857
+ actions: [...header.querySelectorAll(".kanban__desktop-create, :scope > [data-action=new-board]")],
1858
+ menu: header.querySelector(".kanban__menu") ?? void 0
1859
+ });
1860
+ view.element.dataset.titlebarSlot = "";
1861
+ header.dataset.titlebarHost = "true";
1862
+ view.update({ title: title?.textContent ?? "" });
1863
+ this.windowMounts.set(header, () => {
1864
+ view.destroy();
1865
+ delete header.dataset.titlebarHost;
1866
+ });
1867
+ }
1868
+ mountTitleBar(header) {
1869
+ const slot = document.createElement("div");
1870
+ slot.dataset.titlebarSlot = "";
1871
+ header.prepend(slot);
1872
+ header.dataset.titlebarHost = "true";
1873
+ const release = this.options.mountTitleBar(slot, {
1874
+ title: header.querySelector("[data-titlebar-title]") ?? void 0,
1875
+ back: header.querySelector("[data-titlebar-back]") ?? void 0,
1876
+ menu: header.querySelector(".kanban__menu") ?? void 0,
1877
+ actions: [...header.querySelectorAll(".kanban__desktop-create, :scope > [data-action=new-board]")]
1878
+ });
1879
+ this.windowMounts.set(header, () => {
1880
+ release();
1881
+ slot.remove();
1882
+ delete header.dataset.titlebarHost;
1883
+ });
1884
+ }
1885
+ closePanel() {
1886
+ this.dirty = false;
1887
+ this.itemId = "";
1888
+ this.recordId = "";
1889
+ this.item = void 0;
1890
+ this.drawer.close();
1891
+ }
1892
+ async requestClose(explicit) {
1893
+ if (this.editing && explicit) {
1894
+ await this.execute({ type: "end-edit", ...this.editing });
1895
+ this.editing = void 0;
1896
+ }
1897
+ if (this.dirty && !explicit) throw new Error(this.ui.label("unsaved"));
1898
+ if (this.options.navigate && this.boardId) {
1899
+ if (this.dirty) this.closePanel();
1900
+ await this.options.navigate(`/board/${encodeURIComponent(this.boardId)}`);
1901
+ return;
1902
+ }
1903
+ this.closePanel();
1904
+ void this.refreshActivity(this.generation);
1905
+ this.options.selected(this.getActiveResourceId());
1906
+ }
1907
+ alive(generation) {
1908
+ return generation === this.generation && !this.abort.signal.aborted;
1909
+ }
1910
+ content() {
1911
+ return this.options.editor.querySelector("[data-content]");
1912
+ }
1913
+ panel() {
1914
+ return this.options.editor.querySelector("[data-panel]");
1915
+ }
1916
+ status(message) {
1917
+ this.options.editor.querySelector("[data-status]").textContent = message;
1918
+ }
1919
+ error(message) {
1920
+ const error = this.options.editor.querySelector("[data-error]");
1921
+ error.textContent = message;
1922
+ error.hidden = this.drawer?.open ?? false;
1923
+ const panelError = this.options.editor.querySelector("[data-panel-error]");
1924
+ if (panelError) panelError.textContent = message;
1925
+ const executionError = this.options.editor.querySelector("[data-execution-error]");
1926
+ if (executionError) {
1927
+ executionError.textContent = message;
1928
+ executionError.hidden = !message;
1929
+ if (message) {
1930
+ const panel = this.panel(), bottom = executionError.getBoundingClientRect().bottom, edge = panel.getBoundingClientRect().bottom;
1931
+ if (bottom > edge) panel.scrollTop += bottom - edge + 12;
1932
+ }
1933
+ }
1934
+ this.options.editor.querySelector("[data-retry]").hidden = !message || (this.drawer?.open ?? false);
1935
+ }
1936
+ };
1937
+ function errorText(error, label, stage) {
1938
+ const code = error?.outcome === "unknown" ? "WRITE_OUTCOME_UNKNOWN" : error?.code;
1939
+ if (code === "REVISION_CONFLICT" || code === "CURSOR_STALE") return label("conflict");
1940
+ if (error instanceof Error && error.message === label("unsaved")) return error.message;
1941
+ const safe = typeof code === "string" && /^[A-Z_]{1,80}$/.test(code) ? code : "KANBAN_OPERATION_FAILED";
1942
+ const translated = label("errors." + safe);
1943
+ const details = safe === "KANBAN_OPERATION_FAILED" ? failureDetails(error) : {};
1944
+ const diagnostic = details.errorName ? `
1945
+ ${label("diagnostic")}: ${stage} \xB7 ${details.errorName}${details.source ? " \xB7 " + details.source : ""}` : "";
1946
+ return (translated !== "kanban.errors." + safe && translated !== "errors." + safe ? `${translated} (${safe})` : `${label("failed")}: ${safe}`) + diagnostic;
1947
+ }
1948
+ async function mountKanban(container, options) {
1949
+ const sidebar = document.createElement("aside"), editor = document.createElement("main");
1950
+ container.classList.add("kanban-layout");
1951
+ container.replaceChildren(sidebar, editor);
1952
+ const view = new KanbanWorkbench({ ...options, sidebar, editor }, container);
1953
+ await view.start();
1954
+ return view;
1955
+ }
1956
+ export {
1957
+ KanbanSuggestionPicker,
1958
+ KanbanWorkbench,
1959
+ mountKanban
1960
+ };
1961
+ //# sourceMappingURL=index.js.map