@nestos/kanban 0.5.1 → 0.5.2

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 (147) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +73 -29
  3. package/dist/{activity-C6xDM-cg.d.cts → activity-BbC4sgKL.d.cts} +28 -2
  4. package/dist/{activity-Dw8DOw0L.d.ts → activity-Czlan5xW.d.ts} +28 -2
  5. package/dist/activity-module-DaO1Rry0.d.cts +22 -0
  6. package/dist/activity-module-T22wliZu.d.ts +22 -0
  7. package/dist/activity-ports-Dqxa86zT.d.cts +12 -0
  8. package/dist/activity-ports-cxfrelaL.d.ts +12 -0
  9. package/dist/activity.cjs +10 -282
  10. package/dist/activity.cjs.map +1 -1
  11. package/dist/activity.d.cts +8 -30
  12. package/dist/activity.d.ts +8 -30
  13. package/dist/activity.js +9 -281
  14. package/dist/activity.js.map +1 -1
  15. package/dist/{chunk-O4OGAAV4.js → chunk-3JYLJPE5.js} +29 -5
  16. package/dist/chunk-3JYLJPE5.js.map +1 -0
  17. package/dist/{chunk-ATUFFNLV.js → chunk-3LEIHB7Y.js} +5 -5
  18. package/dist/chunk-4XF6LQSU.cjs.map +1 -1
  19. package/dist/chunk-5Q7WB63A.cjs +282 -0
  20. package/dist/chunk-5Q7WB63A.cjs.map +1 -0
  21. package/dist/{chunk-PSKQAFDF.cjs → chunk-7KDOTBRX.cjs} +1 -1
  22. package/dist/chunk-7KDOTBRX.cjs.map +1 -0
  23. package/dist/{chunk-5DLZWW5Y.js → chunk-AHHEGTUA.js} +3 -3
  24. package/dist/chunk-AOHQCX7K.cjs +95 -0
  25. package/dist/chunk-AOHQCX7K.cjs.map +1 -0
  26. package/dist/{chunk-IUI5CI2S.cjs → chunk-DINLR6E6.cjs} +96 -92
  27. package/dist/chunk-DINLR6E6.cjs.map +1 -0
  28. package/dist/{chunk-L44TM4EE.cjs → chunk-EYTVC2H5.cjs} +568 -454
  29. package/dist/chunk-EYTVC2H5.cjs.map +1 -0
  30. package/dist/chunk-FEE2YBZ7.js +1972 -0
  31. package/dist/chunk-FEE2YBZ7.js.map +1 -0
  32. package/dist/chunk-FNO5K3A7.js +282 -0
  33. package/dist/chunk-FNO5K3A7.js.map +1 -0
  34. package/dist/chunk-IHHDXZPZ.js +382 -0
  35. package/dist/chunk-IHHDXZPZ.js.map +1 -0
  36. package/dist/chunk-J3EAIVA3.js +280 -0
  37. package/dist/chunk-J3EAIVA3.js.map +1 -0
  38. package/dist/{chunk-DFTOTBGF.js → chunk-KH3WF3K4.js} +20 -16
  39. package/dist/chunk-KH3WF3K4.js.map +1 -0
  40. package/dist/{chunk-TB2RZGDV.js → chunk-M35X5PHO.js} +14 -1
  41. package/dist/chunk-M35X5PHO.js.map +1 -0
  42. package/dist/chunk-QRGZD2JU.cjs +1972 -0
  43. package/dist/chunk-QRGZD2JU.cjs.map +1 -0
  44. package/dist/{chunk-4KF7KUAI.cjs → chunk-RBOER6EW.cjs} +6 -6
  45. package/dist/chunk-RBOER6EW.cjs.map +1 -0
  46. package/dist/{chunk-6MN3H3YF.cjs → chunk-RSH6QSEC.cjs} +16 -3
  47. package/dist/chunk-RSH6QSEC.cjs.map +1 -0
  48. package/dist/{chunk-T7P5EIC4.cjs → chunk-VMRDXOLX.cjs} +33 -33
  49. package/dist/chunk-VMRDXOLX.cjs.map +1 -0
  50. package/dist/{chunk-5XKOCKY2.js → chunk-X4UHZX6F.js} +1 -1
  51. package/dist/chunk-XBSGUVCY.cjs +382 -0
  52. package/dist/chunk-XBSGUVCY.cjs.map +1 -0
  53. package/dist/{chunk-OHQTKGQN.js → chunk-XKDZHAUE.js} +242 -128
  54. package/dist/chunk-XKDZHAUE.js.map +1 -0
  55. package/dist/chunk-ZUZGAO6G.cjs +280 -0
  56. package/dist/chunk-ZUZGAO6G.cjs.map +1 -0
  57. package/dist/{content-CAIp1njn.d.cts → content-C5aJZFlw.d.cts} +1 -1
  58. package/dist/{content-CGMJdyH_.d.ts → content-hLH5BNud.d.ts} +1 -1
  59. package/dist/contracts/index.cjs +13 -3
  60. package/dist/contracts/index.cjs.map +1 -1
  61. package/dist/contracts/index.d.cts +5 -4
  62. package/dist/contracts/index.d.ts +5 -4
  63. package/dist/contracts/index.js +14 -4
  64. package/dist/{discovery-DxpirOz3.d.cts → discovery-9E-LfwIw.d.cts} +1 -1
  65. package/dist/{discovery-gSqWpuRm.d.ts → discovery-D60yrjej.d.ts} +1 -1
  66. package/dist/discovery.cjs +63 -65
  67. package/dist/discovery.cjs.map +1 -1
  68. package/dist/discovery.d.cts +4 -4
  69. package/dist/discovery.d.ts +4 -4
  70. package/dist/discovery.js +5 -7
  71. package/dist/discovery.js.map +1 -1
  72. package/dist/execution-bridge-fpCnH6z5.d.cts +52 -0
  73. package/dist/execution-bridge-rk0uVy_n.d.ts +52 -0
  74. package/dist/execution.cjs +6 -356
  75. package/dist/execution.cjs.map +1 -1
  76. package/dist/execution.d.cts +4 -51
  77. package/dist/execution.d.ts +4 -51
  78. package/dist/execution.js +9 -359
  79. package/dist/execution.js.map +1 -1
  80. package/dist/index.cjs +10 -12
  81. package/dist/index.cjs.map +1 -1
  82. package/dist/index.d.cts +9 -16
  83. package/dist/index.d.ts +9 -16
  84. package/dist/index.js +10 -12
  85. package/dist/index.js.map +1 -1
  86. package/dist/{module-CWNK2BMj.d.ts → module-CYfE7YhM.d.ts} +2 -2
  87. package/dist/{module-BbvwdCDF.d.cts → module-Ca9L3qCn.d.cts} +2 -2
  88. package/dist/{planning-CcFZYdIL.d.cts → planning-CHrOqQ6b.d.cts} +45 -10
  89. package/dist/{planning-CcFZYdIL.d.ts → planning-CHrOqQ6b.d.ts} +45 -10
  90. package/dist/{ports-Bn6ro2FB.d.ts → ports-B0McVg7B.d.ts} +4 -3
  91. package/dist/ports-BeGO1VRI.d.cts +49 -0
  92. package/dist/{ports-CaL6geqR.d.cts → ports-GQy0x-RI.d.cts} +4 -3
  93. package/dist/ports-ykGiNUSx.d.ts +49 -0
  94. package/dist/runtime/index.cjs +205 -0
  95. package/dist/runtime/index.cjs.map +1 -0
  96. package/dist/runtime/index.d.cts +75 -0
  97. package/dist/runtime/index.d.ts +75 -0
  98. package/dist/runtime/index.js +205 -0
  99. package/dist/runtime/index.js.map +1 -0
  100. package/dist/runtime/integration.cjs +423 -0
  101. package/dist/runtime/integration.cjs.map +1 -0
  102. package/dist/runtime/integration.d.cts +658 -0
  103. package/dist/runtime/integration.d.ts +658 -0
  104. package/dist/runtime/integration.js +423 -0
  105. package/dist/runtime/integration.js.map +1 -0
  106. package/dist/storage.cjs +9 -277
  107. package/dist/storage.cjs.map +1 -1
  108. package/dist/storage.d.cts +6 -6
  109. package/dist/storage.d.ts +6 -6
  110. package/dist/storage.js +11 -279
  111. package/dist/storage.js.map +1 -1
  112. package/dist/{store-q63eSMoT.d.cts → store-DPwHzGiz.d.cts} +2 -2
  113. package/dist/{store-Ct0gOAAg.d.ts → store-DgdrPCV9.d.ts} +2 -2
  114. package/dist/task-projection-CSorlMws.d.ts +11 -0
  115. package/dist/task-projection-DKKB_ueb.d.cts +11 -0
  116. package/examples/standalone.ts +22 -0
  117. package/package.json +117 -87
  118. package/dist/chunk-2YFPV5GF.cjs +0 -16
  119. package/dist/chunk-2YFPV5GF.cjs.map +0 -1
  120. package/dist/chunk-4KF7KUAI.cjs.map +0 -1
  121. package/dist/chunk-6MN3H3YF.cjs.map +0 -1
  122. package/dist/chunk-DFTOTBGF.js.map +0 -1
  123. package/dist/chunk-G6FCDLDF.js +0 -16
  124. package/dist/chunk-G6FCDLDF.js.map +0 -1
  125. package/dist/chunk-IUI5CI2S.cjs.map +0 -1
  126. package/dist/chunk-KYNYPKKH.cjs +0 -71
  127. package/dist/chunk-KYNYPKKH.cjs.map +0 -1
  128. package/dist/chunk-L44TM4EE.cjs.map +0 -1
  129. package/dist/chunk-LSE6JG5I.cjs +0 -20
  130. package/dist/chunk-LSE6JG5I.cjs.map +0 -1
  131. package/dist/chunk-O4OGAAV4.js.map +0 -1
  132. package/dist/chunk-OHQTKGQN.js.map +0 -1
  133. package/dist/chunk-PSKQAFDF.cjs.map +0 -1
  134. package/dist/chunk-QTMTBEBA.js +0 -20
  135. package/dist/chunk-QTMTBEBA.js.map +0 -1
  136. package/dist/chunk-T7P5EIC4.cjs.map +0 -1
  137. package/dist/chunk-TB2RZGDV.js.map +0 -1
  138. package/dist/ui/index.cjs +0 -1961
  139. package/dist/ui/index.cjs.map +0 -1
  140. package/dist/ui/index.d.cts +0 -209
  141. package/dist/ui/index.d.ts +0 -209
  142. package/dist/ui/index.js +0 -1961
  143. package/dist/ui/index.js.map +0 -1
  144. package/dist/ui/kanban.css +0 -194
  145. /package/dist/{chunk-ATUFFNLV.js.map → chunk-3LEIHB7Y.js.map} +0 -0
  146. /package/dist/{chunk-5DLZWW5Y.js.map → chunk-AHHEGTUA.js.map} +0 -0
  147. /package/dist/{chunk-5XKOCKY2.js.map → chunk-X4UHZX6F.js.map} +0 -0
package/dist/ui/index.js DELETED
@@ -1,1961 +0,0 @@
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