agkan 2.14.1 → 2.15.0

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 (140) hide show
  1. package/README.ja.md +13 -13
  2. package/README.md +13 -13
  3. package/dist/board/boardFavicon.d.ts +2 -0
  4. package/dist/board/boardFavicon.d.ts.map +1 -0
  5. package/dist/board/boardFavicon.js +5 -0
  6. package/dist/board/boardFavicon.js.map +1 -0
  7. package/dist/board/boardRenderer.d.ts +22 -6
  8. package/dist/board/boardRenderer.d.ts.map +1 -1
  9. package/dist/board/boardRenderer.js +40 -24
  10. package/dist/board/boardRenderer.js.map +1 -1
  11. package/dist/board/boardRoutes.d.ts +2 -2
  12. package/dist/board/boardRoutes.d.ts.map +1 -1
  13. package/dist/board/boardRoutes.js +18 -3
  14. package/dist/board/boardRoutes.js.map +1 -1
  15. package/dist/board/boardStyles.d.ts +1 -1
  16. package/dist/board/boardStyles.d.ts.map +1 -1
  17. package/dist/board/boardStyles.js +15 -7
  18. package/dist/board/boardStyles.js.map +1 -1
  19. package/dist/board/client/board.js +590 -207
  20. package/dist/board/server.d.ts +2 -2
  21. package/dist/board/server.d.ts.map +1 -1
  22. package/dist/board/server.js +5 -5
  23. package/dist/board/server.js.map +1 -1
  24. package/dist/cli/commands/agent-guide.d.ts.map +1 -1
  25. package/dist/cli/commands/agent-guide.js +6 -0
  26. package/dist/cli/commands/agent-guide.js.map +1 -1
  27. package/dist/cli/commands/board.d.ts.map +1 -1
  28. package/dist/cli/commands/board.js +202 -15
  29. package/dist/cli/commands/board.js.map +1 -1
  30. package/dist/cli/commands/ps.d.ts +7 -0
  31. package/dist/cli/commands/ps.d.ts.map +1 -0
  32. package/dist/cli/commands/ps.js +83 -0
  33. package/dist/cli/commands/ps.js.map +1 -0
  34. package/dist/cli/commands/task/add.js +1 -1
  35. package/dist/cli/commands/task/add.js.map +1 -1
  36. package/dist/cli/commands/task/copy.d.ts +6 -0
  37. package/dist/cli/commands/task/copy.d.ts.map +1 -0
  38. package/dist/cli/commands/task/copy.js +118 -0
  39. package/dist/cli/commands/task/copy.js.map +1 -0
  40. package/dist/cli/commands/task/list.d.ts.map +1 -1
  41. package/dist/cli/commands/task/list.js +37 -17
  42. package/dist/cli/commands/task/list.js.map +1 -1
  43. package/dist/cli/index.js +2 -0
  44. package/dist/cli/index.js.map +1 -1
  45. package/dist/cli/utils/board-daemon.d.ts +7 -0
  46. package/dist/cli/utils/board-daemon.d.ts.map +1 -0
  47. package/dist/cli/utils/board-daemon.js +77 -0
  48. package/dist/cli/utils/board-daemon.js.map +1 -0
  49. package/dist/db/adapters/sqlite-storage-backend.d.ts +26 -0
  50. package/dist/db/adapters/sqlite-storage-backend.d.ts.map +1 -0
  51. package/dist/db/adapters/sqlite-storage-backend.js +447 -0
  52. package/dist/db/adapters/sqlite-storage-backend.js.map +1 -0
  53. package/dist/db/connection.d.ts +14 -0
  54. package/dist/db/connection.d.ts.map +1 -1
  55. package/dist/db/connection.js +28 -2
  56. package/dist/db/connection.js.map +1 -1
  57. package/dist/db/migrations/20260328000000_initial_schema.d.ts +3 -0
  58. package/dist/db/migrations/20260328000000_initial_schema.d.ts.map +1 -0
  59. package/dist/db/migrations/20260328000000_initial_schema.js +218 -0
  60. package/dist/db/migrations/20260328000000_initial_schema.js.map +1 -0
  61. package/dist/db/migrations/20260329000000_add_session_id_to_task_run_logs.d.ts +3 -0
  62. package/dist/db/migrations/20260329000000_add_session_id_to_task_run_logs.d.ts.map +1 -0
  63. package/dist/db/migrations/20260329000000_add_session_id_to_task_run_logs.js +7 -0
  64. package/dist/db/migrations/20260329000000_add_session_id_to_task_run_logs.js.map +1 -0
  65. package/dist/db/migrations/index.d.ts +4 -0
  66. package/dist/db/migrations/index.d.ts.map +1 -0
  67. package/dist/db/migrations/index.js +18 -0
  68. package/dist/db/migrations/index.js.map +1 -0
  69. package/dist/db/migrations/types.d.ts +17 -0
  70. package/dist/db/migrations/types.d.ts.map +1 -0
  71. package/dist/db/migrations/types.js +3 -0
  72. package/dist/db/migrations/types.js.map +1 -0
  73. package/dist/db/reset.d.ts.map +1 -1
  74. package/dist/db/reset.js +8 -3
  75. package/dist/db/reset.js.map +1 -1
  76. package/dist/db/schema.d.ts +4 -4
  77. package/dist/db/schema.d.ts.map +1 -1
  78. package/dist/db/schema.js +22 -207
  79. package/dist/db/schema.js.map +1 -1
  80. package/dist/db/types/repository.d.ts +192 -0
  81. package/dist/db/types/repository.d.ts.map +1 -0
  82. package/dist/db/types/repository.js +15 -0
  83. package/dist/db/types/repository.js.map +1 -0
  84. package/dist/models/Attachment.d.ts +25 -0
  85. package/dist/models/Attachment.d.ts.map +1 -0
  86. package/dist/models/Attachment.js +7 -0
  87. package/dist/models/Attachment.js.map +1 -0
  88. package/dist/services/AttachmentService.d.ts +62 -0
  89. package/dist/services/AttachmentService.d.ts.map +1 -0
  90. package/dist/services/AttachmentService.js +95 -0
  91. package/dist/services/AttachmentService.js.map +1 -0
  92. package/dist/services/ClaudeProcessService.d.ts +100 -0
  93. package/dist/services/ClaudeProcessService.d.ts.map +1 -0
  94. package/dist/services/ClaudeProcessService.js +278 -0
  95. package/dist/services/ClaudeProcessService.js.map +1 -0
  96. package/dist/services/CommentService.d.ts +3 -3
  97. package/dist/services/CommentService.d.ts.map +1 -1
  98. package/dist/services/CommentService.js +10 -70
  99. package/dist/services/CommentService.js.map +1 -1
  100. package/dist/services/ExportImportService.d.ts +3 -3
  101. package/dist/services/ExportImportService.d.ts.map +1 -1
  102. package/dist/services/ExportImportService.js +12 -16
  103. package/dist/services/ExportImportService.js.map +1 -1
  104. package/dist/services/MetadataService.d.ts +3 -3
  105. package/dist/services/MetadataService.d.ts.map +1 -1
  106. package/dist/services/MetadataService.js +9 -69
  107. package/dist/services/MetadataService.js.map +1 -1
  108. package/dist/services/TagService.d.ts +3 -3
  109. package/dist/services/TagService.d.ts.map +1 -1
  110. package/dist/services/TagService.js +9 -35
  111. package/dist/services/TagService.js.map +1 -1
  112. package/dist/services/TaskBlockService.d.ts +3 -3
  113. package/dist/services/TaskBlockService.d.ts.map +1 -1
  114. package/dist/services/TaskBlockService.js +9 -36
  115. package/dist/services/TaskBlockService.js.map +1 -1
  116. package/dist/services/TaskService.d.ts +3 -23
  117. package/dist/services/TaskService.d.ts.map +1 -1
  118. package/dist/services/TaskService.js +34 -186
  119. package/dist/services/TaskService.js.map +1 -1
  120. package/dist/services/TaskTagService.d.ts +3 -3
  121. package/dist/services/TaskTagService.d.ts.map +1 -1
  122. package/dist/services/TaskTagService.js +19 -83
  123. package/dist/services/TaskTagService.js.map +1 -1
  124. package/dist/utils/logger.d.ts +7 -0
  125. package/dist/utils/logger.d.ts.map +1 -0
  126. package/dist/utils/logger.js +18 -0
  127. package/dist/utils/logger.js.map +1 -0
  128. package/package.json +12 -5
  129. package/dist/board/boardScript.d.ts +0 -2
  130. package/dist/board/boardScript.d.ts.map +0 -1
  131. package/dist/board/boardScript.js +0 -1202
  132. package/dist/board/boardScript.js.map +0 -1
  133. package/dist/services/ProcessService.d.ts +0 -54
  134. package/dist/services/ProcessService.d.ts.map +0 -1
  135. package/dist/services/ProcessService.js +0 -147
  136. package/dist/services/ProcessService.js.map +0 -1
  137. package/dist/services/TmuxService.d.ts +0 -2
  138. package/dist/services/TmuxService.d.ts.map +0 -1
  139. package/dist/services/TmuxService.js +0 -7
  140. package/dist/services/TmuxService.js.map +0 -1
@@ -31,8 +31,23 @@
31
31
  }
32
32
 
33
33
  // src/board/client/dragDrop.ts
34
+ var _redrawDependencies = null;
35
+ function registerDependencyRedrawCallback(callback) {
36
+ _redrawDependencies = callback;
37
+ }
34
38
  var draggedCard = null;
35
39
  var sourceBody = null;
40
+ var _dragMouseX = 0;
41
+ var _dragMouseY = 0;
42
+ var _dragOffsetX = 0;
43
+ var _dragOffsetY = 0;
44
+ function getDraggedCardVirtualRect() {
45
+ if (!draggedCard) return null;
46
+ const rect = draggedCard.getBoundingClientRect();
47
+ const left = _dragMouseX - _dragOffsetX;
48
+ const top = _dragMouseY - _dragOffsetY;
49
+ return new DOMRect(left, top, rect.width, rect.height);
50
+ }
36
51
  function updateCount(status) {
37
52
  const col = document.querySelector(`.column[data-status="${status}"]`);
38
53
  if (!col) return;
@@ -62,6 +77,9 @@
62
77
  body: JSON.stringify({ status: newStatus })
63
78
  });
64
79
  if (!res.ok) throw new Error("Server error");
80
+ if (_redrawDependencies) {
81
+ _redrawDependencies();
82
+ }
65
83
  } catch {
66
84
  if (prevBody && draggedCard) {
67
85
  prevBody.appendChild(draggedCard);
@@ -72,17 +90,33 @@
72
90
  showToast();
73
91
  }
74
92
  }
93
+ var _documentDragOverListener = null;
75
94
  function attachDragListeners(card) {
76
95
  card.addEventListener("dragstart", (e) => {
77
96
  draggedCard = card;
78
97
  sourceBody = card.parentElement;
79
98
  card.classList.add("dragging");
80
99
  if (e.dataTransfer) e.dataTransfer.effectAllowed = "move";
100
+ const rect = card.getBoundingClientRect();
101
+ _dragOffsetX = e.clientX - rect.left;
102
+ _dragOffsetY = e.clientY - rect.top;
103
+ _dragMouseX = e.clientX;
104
+ _dragMouseY = e.clientY;
105
+ _documentDragOverListener = (ev) => {
106
+ _dragMouseX = ev.clientX;
107
+ _dragMouseY = ev.clientY;
108
+ if (_redrawDependencies) _redrawDependencies();
109
+ };
110
+ document.addEventListener("dragover", _documentDragOverListener);
81
111
  });
82
112
  card.addEventListener("dragend", () => {
83
113
  card.classList.remove("dragging");
84
114
  draggedCard = null;
85
115
  sourceBody = null;
116
+ if (_documentDragOverListener) {
117
+ document.removeEventListener("dragover", _documentDragOverListener);
118
+ _documentDragOverListener = null;
119
+ }
86
120
  });
87
121
  }
88
122
  function initDragDrop() {
@@ -219,14 +253,43 @@
219
253
  });
220
254
  input.placeholder = currentTags.length === 0 ? "Add tags..." : "";
221
255
  }
256
+ async function createAndAddTag(name) {
257
+ const detailTaskId2 = _getDetailTaskId ? _getDetailTaskId() : null;
258
+ try {
259
+ const createRes = await fetch("/api/tags", {
260
+ method: "POST",
261
+ headers: { "Content-Type": "application/json" },
262
+ body: JSON.stringify({ name })
263
+ });
264
+ if (!createRes.ok) throw new Error("Server error");
265
+ const newTag = await createRes.json();
266
+ allAvailableTags.push(newTag);
267
+ const taskRes = await fetch("/api/tasks/" + detailTaskId2 + "/tags", {
268
+ method: "POST",
269
+ headers: { "Content-Type": "application/json" },
270
+ body: JSON.stringify({ tagId: newTag.id })
271
+ });
272
+ if (!taskRes.ok) throw new Error("Server error");
273
+ currentTags.push(newTag);
274
+ input.value = "";
275
+ inputValue = "";
276
+ renderPills();
277
+ renderDropdown();
278
+ } catch {
279
+ showToast("Failed to create tag");
280
+ }
281
+ }
222
282
  function renderDropdown() {
223
283
  const filtered = getFilteredTags();
224
284
  dropdown.innerHTML = "";
225
285
  focusedOptionIndex = -1;
226
- if (filtered.length === 0) {
286
+ const hasInput = inputValue.trim() !== "";
287
+ const exactMatch = hasInput && allAvailableTags.some((t) => t.name.toLowerCase() === inputValue.trim().toLowerCase());
288
+ const showCreate = hasInput && !exactMatch;
289
+ if (filtered.length === 0 && !showCreate) {
227
290
  const noOpt = document.createElement("div");
228
291
  noOpt.className = "tag-select-no-options";
229
- noOpt.textContent = inputValue ? "No matching tags" : "No tags available";
292
+ noOpt.textContent = hasInput ? "No matching tags" : "No tags available";
230
293
  dropdown.appendChild(noOpt);
231
294
  } else {
232
295
  filtered.forEach((t, i) => {
@@ -241,6 +304,18 @@
241
304
  });
242
305
  dropdown.appendChild(opt);
243
306
  });
307
+ if (showCreate) {
308
+ const createOpt = document.createElement("div");
309
+ createOpt.className = "tag-select-option tag-select-create-option";
310
+ createOpt.dataset.create = "true";
311
+ createOpt.textContent = `Create "${inputValue.trim()}"`;
312
+ createOpt.addEventListener("mouseover", () => setFocusedOption(filtered.length));
313
+ createOpt.addEventListener("mousedown", async (e) => {
314
+ e.preventDefault();
315
+ await createAndAddTag(inputValue.trim());
316
+ });
317
+ dropdown.appendChild(createOpt);
318
+ }
244
319
  }
245
320
  }
246
321
  function setFocusedOption(index) {
@@ -296,6 +371,8 @@
296
371
  e.preventDefault();
297
372
  if (focusedOptionIndex >= 0 && filtered[focusedOptionIndex]) {
298
373
  await addTag(String(filtered[focusedOptionIndex].id));
374
+ } else if (focusedOptionIndex >= 0 && inputValue.trim()) {
375
+ await createAndAddTag(inputValue.trim());
299
376
  }
300
377
  } else if (e.key === "Escape") {
301
378
  closeDropdown();
@@ -347,6 +424,7 @@
347
424
  var _showUpdateWarning = null;
348
425
  var _getDetailTaskId2 = null;
349
426
  var _setActiveCard = null;
427
+ var _redrawDependencies2 = null;
350
428
  function registerDetailPanelCallbacks(callbacks) {
351
429
  _openTaskDetail = callbacks.openTaskDetail;
352
430
  _renderDetailPanel = callbacks.renderDetailPanel;
@@ -354,6 +432,9 @@
354
432
  _getDetailTaskId2 = callbacks.getDetailTaskId;
355
433
  _setActiveCard = callbacks.setActiveCard;
356
434
  }
435
+ function registerDependencyRedrawCallback2(callback) {
436
+ _redrawDependencies2 = callback;
437
+ }
357
438
  function attachCardListeners(body) {
358
439
  body.querySelectorAll(".card").forEach((card) => {
359
440
  attachDragListeners(card);
@@ -447,6 +528,9 @@
447
528
  if (detailTaskId2 !== null && _setActiveCard) {
448
529
  _setActiveCard(detailTaskId2);
449
530
  }
531
+ if (_redrawDependencies2) {
532
+ _redrawDependencies2();
533
+ }
450
534
  if (detailTaskId2 !== null) {
451
535
  await refreshOpenDetailPanel(detailTaskId2);
452
536
  }
@@ -512,10 +596,13 @@
512
596
  const filtered = getFilteredAddTags();
513
597
  dropdown.innerHTML = "";
514
598
  tagFocusedIndex = -1;
515
- if (filtered.length === 0) {
599
+ const hasInput = tagInputValue.trim() !== "";
600
+ const exactMatch = hasInput && allAvailableTags.some((t) => t.name.toLowerCase() === tagInputValue.trim().toLowerCase());
601
+ const showCreate = hasInput && !exactMatch;
602
+ if (filtered.length === 0 && !showCreate) {
516
603
  const noOpt = document.createElement("div");
517
604
  noOpt.className = "tag-select-no-options";
518
- noOpt.textContent = tagInputValue ? "No matching tags" : "No tags available";
605
+ noOpt.textContent = hasInput ? "No matching tags" : "No tags available";
519
606
  dropdown.appendChild(noOpt);
520
607
  } else {
521
608
  filtered.forEach((t, i) => {
@@ -530,6 +617,19 @@
530
617
  });
531
618
  dropdown.appendChild(opt);
532
619
  });
620
+ if (showCreate) {
621
+ const createOpt = document.createElement("div");
622
+ createOpt.className = "tag-select-option tag-select-create-option";
623
+ createOpt.dataset.create = "true";
624
+ createOpt.textContent = `Create "${tagInputValue.trim()}"`;
625
+ createOpt.addEventListener("mouseover", () => setAddTagFocused(dropdown, filtered.length));
626
+ createOpt.addEventListener("mousedown", async (e) => {
627
+ e.preventDefault();
628
+ const input = document.getElementById("add-tag-input");
629
+ await createAndSelectAddTag(tagInputValue.trim(), dropdown, input);
630
+ });
631
+ dropdown.appendChild(createOpt);
632
+ }
533
633
  }
534
634
  }
535
635
  function setAddTagFocused(dropdown, index) {
@@ -547,6 +647,21 @@
547
647
  renderAddTagPills(control, input);
548
648
  renderAddTagDropdown(dropdown);
549
649
  }
650
+ async function createAndSelectAddTag(name, dropdown, input) {
651
+ try {
652
+ const res = await fetch("/api/tags", {
653
+ method: "POST",
654
+ headers: { "Content-Type": "application/json" },
655
+ body: JSON.stringify({ name })
656
+ });
657
+ if (!res.ok) throw new Error("Server error");
658
+ const newTag = await res.json();
659
+ allAvailableTags.push(newTag);
660
+ selectAddTag(newTag.id, dropdown, input);
661
+ } catch {
662
+ showToast("Failed to create tag");
663
+ }
664
+ }
550
665
  function initAddTagSelector() {
551
666
  const control = document.getElementById("add-tag-select-control");
552
667
  const dropdown = document.getElementById("add-tag-select-dropdown");
@@ -574,7 +689,7 @@
574
689
  renderAddTagDropdown(dropdown);
575
690
  if (!dropdown.classList.contains("open")) dropdown.classList.add("open");
576
691
  });
577
- input.addEventListener("keydown", (e) => {
692
+ input.addEventListener("keydown", async (e) => {
578
693
  const filtered = getFilteredAddTags();
579
694
  const opts = dropdown.querySelectorAll(".tag-select-option");
580
695
  if (e.key === "ArrowDown") {
@@ -587,6 +702,8 @@
587
702
  e.preventDefault();
588
703
  if (tagFocusedIndex >= 0 && filtered[tagFocusedIndex]) {
589
704
  selectAddTag(filtered[tagFocusedIndex].id, dropdown, input);
705
+ } else if (tagFocusedIndex >= 0 && tagInputValue.trim()) {
706
+ await createAndSelectAddTag(tagInputValue.trim(), dropdown, input);
590
707
  }
591
708
  } else if (e.key === "Escape") {
592
709
  dropdown.classList.remove("open");
@@ -636,7 +753,7 @@
636
753
  function resetAddModal(elements) {
637
754
  elements.addTitle.value = "";
638
755
  elements.addBody.value = "";
639
- elements.addPriority.value = "";
756
+ elements.addPriority.value = "medium";
640
757
  selectedTags = [];
641
758
  tagInputValue = "";
642
759
  tagFocusedIndex = -1;
@@ -764,6 +881,220 @@
764
881
  });
765
882
  }
766
883
 
884
+ // src/board/client/detailPanelApi.ts
885
+ var PANEL_MIN_WIDTH = 280;
886
+ var PANEL_MAX_WIDTH = 800;
887
+ var PANEL_DEFAULT_WIDTH = 400;
888
+ async function fetchComments(taskId) {
889
+ const res = await fetch("/api/tasks/" + taskId + "/comments");
890
+ if (!res.ok) throw new Error("Server error");
891
+ const data = await res.json();
892
+ return data.comments || [];
893
+ }
894
+ async function patchComment(commentId, content) {
895
+ const res = await fetch("/api/comments/" + commentId, {
896
+ method: "PATCH",
897
+ headers: { "Content-Type": "application/json" },
898
+ body: JSON.stringify({ content })
899
+ });
900
+ if (!res.ok) throw new Error("Server error");
901
+ }
902
+ async function deleteCommentRequest(commentId) {
903
+ const res = await fetch("/api/comments/" + commentId, { method: "DELETE" });
904
+ if (!res.ok) throw new Error("Server error");
905
+ }
906
+ async function postComment(taskId, content) {
907
+ const res = await fetch("/api/tasks/" + taskId + "/comments", {
908
+ method: "POST",
909
+ headers: { "Content-Type": "application/json" },
910
+ body: JSON.stringify({ content })
911
+ });
912
+ if (!res.ok) throw new Error("Server error");
913
+ }
914
+ async function fetchTaskDetail(taskId) {
915
+ const res = await fetch("/api/tasks/" + taskId);
916
+ if (!res.ok) throw new Error("Server error");
917
+ return res.json();
918
+ }
919
+ async function patchTask(taskId, fields) {
920
+ const res = await fetch("/api/tasks/" + taskId, {
921
+ method: "PATCH",
922
+ headers: { "Content-Type": "application/json" },
923
+ body: JSON.stringify(fields)
924
+ });
925
+ if (!res.ok) throw new Error("Server error");
926
+ return fetchTaskDetail(taskId);
927
+ }
928
+ async function syncTimestampAfterSave() {
929
+ try {
930
+ const tsRes = await fetch("/api/board/updated-at");
931
+ if (tsRes.ok) {
932
+ const tsData = await tsRes.json();
933
+ setLastUpdatedAt(tsData.updatedAt);
934
+ }
935
+ } catch {
936
+ }
937
+ }
938
+ async function fetchPanelWidthFromConfig() {
939
+ let targetWidth = PANEL_DEFAULT_WIDTH;
940
+ try {
941
+ const res = await fetch("/api/config");
942
+ if (res.ok) {
943
+ const data = await res.json();
944
+ const savedWidth = data && data.board && data.board.detailPaneWidth;
945
+ if (typeof savedWidth === "number" && savedWidth >= PANEL_MIN_WIDTH && savedWidth <= PANEL_MAX_WIDTH) {
946
+ targetWidth = savedWidth;
947
+ }
948
+ }
949
+ } catch {
950
+ }
951
+ return targetWidth;
952
+ }
953
+ function savePanelWidthToConfig(width) {
954
+ fetch("/api/config", {
955
+ method: "PUT",
956
+ headers: { "Content-Type": "application/json" },
957
+ body: JSON.stringify({ board: { detailPaneWidth: width } })
958
+ }).catch(function() {
959
+ });
960
+ }
961
+
962
+ // src/board/client/detailPanelHtml.ts
963
+ function renderCommentItemHtml(comment, taskId) {
964
+ const authorText = comment.author ? escapeHtmlClient(comment.author) : "Anonymous";
965
+ const dateRel = relativeTime(comment.created_at);
966
+ const dateAbs = escapeHtmlClient(comment.created_at);
967
+ const contentText = escapeHtmlClient(comment.content);
968
+ let html = '<div class="comment-item" data-comment-id="' + comment.id + '">';
969
+ html += '<div class="comment-meta">';
970
+ html += '<span class="comment-author">' + authorText + "</span>";
971
+ html += '<span class="comment-date" title="' + dateAbs + '">' + dateRel + "</span>";
972
+ html += '<span class="comment-actions">';
973
+ html += '<button class="comment-action-btn" title="Edit" data-action="start-comment-edit" data-comment-id="' + comment.id + '">&#9998;</button>';
974
+ html += '<button class="comment-action-btn danger" title="Delete" data-action="delete-comment" data-comment-id="' + comment.id + '" data-task-id="' + taskId + '">&#128465;</button>';
975
+ html += "</span></div>";
976
+ html += '<div class="comment-content" id="comment-content-' + comment.id + '">' + contentText + "</div>";
977
+ html += '<div id="comment-edit-' + comment.id + '" style="display:none;">';
978
+ html += '<textarea class="comment-edit-area" id="comment-edit-area-' + comment.id + '">' + contentText + "</textarea>";
979
+ html += '<div class="comment-edit-actions">';
980
+ html += '<button class="comment-btn" data-action="save-comment-edit" data-comment-id="' + comment.id + '" data-task-id="' + taskId + '">Save</button>';
981
+ html += '<button class="comment-btn" data-action="cancel-comment-edit" data-comment-id="' + comment.id + '">Cancel</button>';
982
+ html += "</div></div></div>";
983
+ return html;
984
+ }
985
+ function renderAddCommentFormHtml(taskId) {
986
+ let html = '<button class="add-comment-trigger" id="add-comment-trigger" data-action="open-add-comment">+ Add comment...</button>';
987
+ html += '<div class="add-comment-form" id="add-comment-form">';
988
+ html += '<textarea class="add-comment-textarea" id="add-comment-text" placeholder="Write a comment..."></textarea>';
989
+ html += "<div>";
990
+ html += '<button class="add-comment-submit" data-action="submit-comment" data-task-id="' + taskId + '">Add Comment</button>';
991
+ html += '<button class="add-comment-cancel" data-action="close-add-comment">Cancel</button>';
992
+ html += "</div></div>";
993
+ return html;
994
+ }
995
+ function renderStatusField(currentStatus, allStatuses, statusLabels) {
996
+ let html = '<div class="detail-field">';
997
+ html += '<div class="detail-field-label">Status</div>';
998
+ html += '<select id="detail-edit-status" class="detail-edit-select">';
999
+ allStatuses.forEach((s) => {
1000
+ const selected = s === currentStatus ? " selected" : "";
1001
+ html += '<option value="' + s + '"' + selected + ">" + statusLabels[s] + "</option>";
1002
+ });
1003
+ html += "</select></div>";
1004
+ return html;
1005
+ }
1006
+ function renderPriorityField(currentPriority, allPriorities) {
1007
+ let html = '<div class="detail-field">';
1008
+ html += '<div class="detail-field-label">Priority</div>';
1009
+ html += '<select id="detail-edit-priority" class="detail-edit-select">';
1010
+ html += '<option value="">None</option>';
1011
+ allPriorities.forEach((p) => {
1012
+ const selected = currentPriority === p ? " selected" : "";
1013
+ html += '<option value="' + p + '"' + selected + ">" + p.charAt(0).toUpperCase() + p.slice(1) + "</option>";
1014
+ });
1015
+ html += "</select></div>";
1016
+ return html;
1017
+ }
1018
+ function renderRelationsHtml(parent, blockedBy, blocking) {
1019
+ let html = '<div class="detail-relations">';
1020
+ if (parent) {
1021
+ html += '<div class="detail-relation-row">';
1022
+ html += '<span class="detail-relation-label">Parent</span>';
1023
+ html += '<div class="detail-relation-ids"><span class="detail-relation-id detail-relation-link" data-task-id="' + parent.id + '">#' + parent.id + " " + escapeHtmlClient(parent.title) + "</span></div>";
1024
+ html += "</div>";
1025
+ }
1026
+ if (blockedBy.length > 0) {
1027
+ html += '<div class="detail-relation-row"><span class="detail-relation-label">Blocked by</span>';
1028
+ html += '<div class="detail-relation-ids">';
1029
+ blockedBy.forEach((t) => {
1030
+ html += '<span class="detail-relation-id detail-relation-link" data-task-id="' + t.id + '">#' + t.id + "</span>";
1031
+ });
1032
+ html += "</div></div>";
1033
+ }
1034
+ if (blocking.length > 0) {
1035
+ html += '<div class="detail-relation-row"><span class="detail-relation-label">Blocking</span>';
1036
+ html += '<div class="detail-relation-ids">';
1037
+ blocking.forEach((t) => {
1038
+ html += '<span class="detail-relation-id detail-relation-link" data-task-id="' + t.id + '">#' + t.id + "</span>";
1039
+ });
1040
+ html += "</div></div>";
1041
+ }
1042
+ html += "</div>";
1043
+ return html;
1044
+ }
1045
+ function renderMetadataTable(metadata) {
1046
+ if (metadata.length === 0) return "";
1047
+ let html = '<div class="detail-field"><div class="detail-field-label">Metadata</div>';
1048
+ html += '<table class="detail-meta-table">';
1049
+ metadata.forEach((m) => {
1050
+ html += "<tr><td>" + escapeHtmlClient(m.key) + "</td><td>" + escapeHtmlClient(m.value) + "</td></tr>";
1051
+ });
1052
+ html += "</table></div>";
1053
+ return html;
1054
+ }
1055
+ function renderEditableTextFields(task) {
1056
+ let html = '<div class="detail-field"><div class="detail-field-label">Title</div>';
1057
+ html += '<input id="detail-edit-title" class="detail-edit-input" type="text" value="' + escapeHtmlClient(task.title) + '">';
1058
+ html += "</div>";
1059
+ html += '<div class="detail-field description-field-wrapper"><div class="detail-field-label">Description</div>';
1060
+ html += '<textarea id="detail-edit-body" class="detail-edit-textarea">' + escapeHtmlClient(task.body || "") + "</textarea>";
1061
+ html += "</div>";
1062
+ return html;
1063
+ }
1064
+ function renderDetailPanelHtml(data) {
1065
+ const task = data.task;
1066
+ const metadata = data.metadata || [];
1067
+ const blockedBy = data.blockedBy || [];
1068
+ const blocking = data.blocking || [];
1069
+ const parent = data.parent || null;
1070
+ const win = window;
1071
+ const allStatuses = win.allStatuses;
1072
+ const statusLabels = win.statusLabels;
1073
+ const allPriorities = win.allPriorities;
1074
+ let html = "";
1075
+ html += renderStatusField(task.status, allStatuses, statusLabels);
1076
+ html += renderPriorityField(task.priority, allPriorities);
1077
+ html += '<div class="detail-field"><div class="detail-field-label">Tags</div>';
1078
+ html += '<div id="detail-tags-container"></div></div>';
1079
+ const hasRelations = parent || blockedBy.length > 0 || blocking.length > 0;
1080
+ if (hasRelations) {
1081
+ html += renderRelationsHtml(parent, blockedBy, blocking);
1082
+ }
1083
+ html += renderMetadataTable(metadata);
1084
+ html += renderEditableTextFields(task);
1085
+ return html;
1086
+ }
1087
+ function buildDetailPanelHtml() {
1088
+ return '<div class="detail-panel" id="detail-panel"><div class="detail-panel-resize-handle" id="detail-panel-resize-handle"></div><div class="detail-panel-header"><h2 id="detail-panel-title">Task Detail</h2><button class="detail-panel-copy-id" id="detail-panel-copy-id" title="Copy task ID">&#x2398;</button><button class="detail-panel-close" id="detail-panel-close" title="Close">&times;</button></div><div class="detail-tabs" id="detail-tabs"><button class="detail-tab active" data-tab="details">Details</button><button class="detail-tab" data-tab="comments" id="detail-tab-comments">Comments</button></div><div class="detail-panel-body" id="detail-panel-body"><div class="detail-tab-content active" id="detail-tab-content-details"></div><div class="detail-tab-content" id="detail-tab-content-comments"></div></div><div class="detail-panel-footer" id="detail-panel-footer"><button id="detail-save-btn">Save</button></div></div>';
1089
+ }
1090
+ function autoResizeTextarea(el) {
1091
+ const scrollContainer = el.closest(".detail-tab-content");
1092
+ const scrollTop = scrollContainer?.scrollTop ?? 0;
1093
+ el.style.height = "auto";
1094
+ el.style.height = el.scrollHeight + "px";
1095
+ if (scrollContainer) scrollContainer.scrollTop = scrollTop;
1096
+ }
1097
+
767
1098
  // src/board/client/detailPanel.ts
768
1099
  var detailTaskId = null;
769
1100
  var lastTab = "details";
@@ -802,10 +1133,7 @@
802
1133
  const pane = document.getElementById("detail-tab-content-comments");
803
1134
  if (!pane) return;
804
1135
  try {
805
- const res = await fetch("/api/tasks/" + taskId + "/comments");
806
- if (!res.ok) throw new Error("Server error");
807
- const data = await res.json();
808
- const comments = data.comments || [];
1136
+ const comments = await fetchComments(taskId);
809
1137
  if (tabBtn) tabBtn.textContent = "Comments (" + comments.length + ")";
810
1138
  renderComments(taskId, comments);
811
1139
  } catch (err) {
@@ -813,38 +1141,6 @@
813
1141
  if (pane) pane.innerHTML = '<div style="padding:20px;font-size:12px;color:#94a3b8;">Failed to load comments</div>';
814
1142
  }
815
1143
  }
816
- function renderCommentItemHtml(comment, taskId) {
817
- const authorText = comment.author ? escapeHtmlClient(comment.author) : "Anonymous";
818
- const dateRel = relativeTime(comment.created_at);
819
- const dateAbs = escapeHtmlClient(comment.created_at);
820
- const contentText = escapeHtmlClient(comment.content);
821
- let html = '<div class="comment-item" data-comment-id="' + comment.id + '">';
822
- html += '<div class="comment-meta">';
823
- html += '<span class="comment-author">' + authorText + "</span>";
824
- html += '<span class="comment-date" title="' + dateAbs + '">' + dateRel + "</span>";
825
- html += '<span class="comment-actions">';
826
- html += '<button class="comment-action-btn" title="Edit" data-action="start-comment-edit" data-comment-id="' + comment.id + '">&#9998;</button>';
827
- html += '<button class="comment-action-btn danger" title="Delete" data-action="delete-comment" data-comment-id="' + comment.id + '" data-task-id="' + taskId + '">&#128465;</button>';
828
- html += "</span></div>";
829
- html += '<div class="comment-content" id="comment-content-' + comment.id + '">' + contentText + "</div>";
830
- html += '<div id="comment-edit-' + comment.id + '" style="display:none;">';
831
- html += '<textarea class="comment-edit-area" id="comment-edit-area-' + comment.id + '">' + contentText + "</textarea>";
832
- html += '<div class="comment-edit-actions">';
833
- html += '<button class="comment-btn" data-action="save-comment-edit" data-comment-id="' + comment.id + '" data-task-id="' + taskId + '">Save</button>';
834
- html += '<button class="comment-btn" data-action="cancel-comment-edit" data-comment-id="' + comment.id + '">Cancel</button>';
835
- html += "</div></div></div>";
836
- return html;
837
- }
838
- function renderAddCommentFormHtml(taskId) {
839
- let html = '<button class="add-comment-trigger" id="add-comment-trigger" data-action="open-add-comment">+ Add comment...</button>';
840
- html += '<div class="add-comment-form" id="add-comment-form">';
841
- html += '<textarea class="add-comment-textarea" id="add-comment-text" placeholder="Write a comment..."></textarea>';
842
- html += "<div>";
843
- html += '<button class="add-comment-submit" data-action="submit-comment" data-task-id="' + taskId + '">Add Comment</button>';
844
- html += '<button class="add-comment-cancel" data-action="close-add-comment">Cancel</button>';
845
- html += "</div></div>";
846
- return html;
847
- }
848
1144
  function renderComments(taskId, comments) {
849
1145
  const pane = document.getElementById("detail-tab-content-comments");
850
1146
  if (!pane) return;
@@ -903,12 +1199,7 @@
903
1199
  return;
904
1200
  }
905
1201
  try {
906
- const res = await fetch("/api/comments/" + commentId, {
907
- method: "PATCH",
908
- headers: { "Content-Type": "application/json" },
909
- body: JSON.stringify({ content })
910
- });
911
- if (!res.ok) throw new Error("Server error");
1202
+ await patchComment(commentId, content);
912
1203
  await loadComments(taskId);
913
1204
  } catch {
914
1205
  showToast("Failed to update comment");
@@ -917,8 +1208,7 @@
917
1208
  async function deleteComment(commentId, taskId) {
918
1209
  if (!confirm("Delete this comment?")) return;
919
1210
  try {
920
- const res = await fetch("/api/comments/" + commentId, { method: "DELETE" });
921
- if (!res.ok) throw new Error("Server error");
1211
+ await deleteCommentRequest(commentId);
922
1212
  await loadComments(taskId);
923
1213
  } catch {
924
1214
  showToast("Failed to delete comment");
@@ -933,12 +1223,7 @@
933
1223
  return;
934
1224
  }
935
1225
  try {
936
- const res = await fetch("/api/tasks/" + taskId + "/comments", {
937
- method: "POST",
938
- headers: { "Content-Type": "application/json" },
939
- body: JSON.stringify({ content })
940
- });
941
- if (!res.ok) throw new Error("Server error");
1226
+ await postComment(taskId, content);
942
1227
  await loadComments(taskId);
943
1228
  } catch {
944
1229
  showToast("Failed to add comment");
@@ -977,103 +1262,6 @@
977
1262
  const taskId = target.dataset.taskId ? Number(target.dataset.taskId) : NaN;
978
1263
  dispatchCommentAction(action, commentId, taskId);
979
1264
  }
980
- function renderStatusField(currentStatus, allStatuses, statusLabels) {
981
- let html = '<div class="detail-field">';
982
- html += '<div class="detail-field-label">Status</div>';
983
- html += '<select id="detail-edit-status" class="detail-edit-select">';
984
- allStatuses.forEach((s) => {
985
- const selected = s === currentStatus ? " selected" : "";
986
- html += '<option value="' + s + '"' + selected + ">" + statusLabels[s] + "</option>";
987
- });
988
- html += "</select></div>";
989
- return html;
990
- }
991
- function renderPriorityField(currentPriority, allPriorities) {
992
- let html = '<div class="detail-field">';
993
- html += '<div class="detail-field-label">Priority</div>';
994
- html += '<select id="detail-edit-priority" class="detail-edit-select">';
995
- html += '<option value="">None</option>';
996
- allPriorities.forEach((p) => {
997
- const selected = currentPriority === p ? " selected" : "";
998
- html += '<option value="' + p + '"' + selected + ">" + p.charAt(0).toUpperCase() + p.slice(1) + "</option>";
999
- });
1000
- html += "</select></div>";
1001
- return html;
1002
- }
1003
- function renderRelationsHtml(parent, blockedBy, blocking) {
1004
- let html = '<div class="detail-relations">';
1005
- if (parent) {
1006
- html += '<div class="detail-relation-row">';
1007
- html += '<span class="detail-relation-label">Parent</span>';
1008
- html += '<div class="detail-relation-ids"><span class="detail-relation-id">#' + parent.id + " " + escapeHtmlClient(parent.title) + "</span></div>";
1009
- html += "</div>";
1010
- }
1011
- if (blockedBy.length > 0) {
1012
- html += '<div class="detail-relation-row"><span class="detail-relation-label">Blocked by</span>';
1013
- html += '<div class="detail-relation-ids">';
1014
- blockedBy.forEach((t) => {
1015
- html += '<span class="detail-relation-id">#' + t.id + "</span>";
1016
- });
1017
- html += "</div></div>";
1018
- }
1019
- if (blocking.length > 0) {
1020
- html += '<div class="detail-relation-row"><span class="detail-relation-label">Blocking</span>';
1021
- html += '<div class="detail-relation-ids">';
1022
- blocking.forEach((t) => {
1023
- html += '<span class="detail-relation-id">#' + t.id + "</span>";
1024
- });
1025
- html += "</div></div>";
1026
- }
1027
- html += "</div>";
1028
- return html;
1029
- }
1030
- function autoResizeTextarea(el) {
1031
- el.style.height = "auto";
1032
- el.style.height = el.scrollHeight + "px";
1033
- }
1034
- function renderMetadataTable(metadata) {
1035
- const otherMeta = metadata.filter((m) => m.key !== "priority");
1036
- if (otherMeta.length === 0) return "";
1037
- let html = '<div class="detail-field"><div class="detail-field-label">Metadata</div>';
1038
- html += '<table class="detail-meta-table">';
1039
- otherMeta.forEach((m) => {
1040
- html += "<tr><td>" + escapeHtmlClient(m.key) + "</td><td>" + escapeHtmlClient(m.value) + "</td></tr>";
1041
- });
1042
- html += "</table></div>";
1043
- return html;
1044
- }
1045
- function renderEditableTextFields(task) {
1046
- let html = '<div class="detail-field"><div class="detail-field-label">Title</div>';
1047
- html += '<input id="detail-edit-title" class="detail-edit-input" type="text" value="' + escapeHtmlClient(task.title) + '">';
1048
- html += "</div>";
1049
- html += '<div class="detail-field description-field-wrapper"><div class="detail-field-label">Description</div>';
1050
- html += '<textarea id="detail-edit-body" class="detail-edit-textarea">' + escapeHtmlClient(task.body || "") + "</textarea>";
1051
- html += "</div>";
1052
- return html;
1053
- }
1054
- function renderDetailPanelHtml(data) {
1055
- const task = data.task;
1056
- const metadata = data.metadata || [];
1057
- const blockedBy = data.blockedBy || [];
1058
- const blocking = data.blocking || [];
1059
- const parent = data.parent || null;
1060
- const win = window;
1061
- const allStatuses = win.allStatuses;
1062
- const statusLabels = win.statusLabels;
1063
- const allPriorities = win.allPriorities;
1064
- let html = "";
1065
- html += renderStatusField(task.status, allStatuses, statusLabels);
1066
- html += renderPriorityField(task.priority, allPriorities);
1067
- html += '<div class="detail-field"><div class="detail-field-label">Tags</div>';
1068
- html += '<div id="detail-tags-container"></div></div>';
1069
- const hasRelations = parent || blockedBy.length > 0 || blocking.length > 0;
1070
- if (hasRelations) {
1071
- html += renderRelationsHtml(parent, blockedBy, blocking);
1072
- }
1073
- html += renderMetadataTable(metadata);
1074
- html += renderEditableTextFields(task);
1075
- return html;
1076
- }
1077
1265
  function renderDetailPanel(data) {
1078
1266
  document.getElementById("detail-panel-update-warning")?.remove();
1079
1267
  const detailPanelTitle = document.getElementById("detail-panel-title");
@@ -1085,6 +1273,13 @@
1085
1273
  if (detailsPane) {
1086
1274
  detailsPane.innerHTML = renderDetailPanelHtml(data);
1087
1275
  detailsPane.style.padding = "20px";
1276
+ detailsPane.querySelectorAll(".detail-relation-link[data-task-id]").forEach((el) => {
1277
+ el.style.cursor = "pointer";
1278
+ el.addEventListener("click", () => {
1279
+ const tid = el.dataset.taskId;
1280
+ if (tid) void openTaskDetail(tid);
1281
+ });
1282
+ });
1088
1283
  }
1089
1284
  const footer = document.getElementById("detail-panel-footer");
1090
1285
  if (footer) {
@@ -1100,7 +1295,8 @@
1100
1295
  autoResizeTextarea(textarea);
1101
1296
  });
1102
1297
  }
1103
- loadAllTags().then(() => renderTagsSection([...tags])).catch((err) => {
1298
+ renderTagsSection([...tags]);
1299
+ loadAllTags().catch((err) => {
1104
1300
  console.error("[agkan] renderDetailPanel tags failed", err);
1105
1301
  });
1106
1302
  loadComments(task.id);
@@ -1110,9 +1306,7 @@
1110
1306
  const detailPanel = document.getElementById("detail-panel");
1111
1307
  const PANEL_DEFAULT_WIDTH2 = 400;
1112
1308
  try {
1113
- const res = await fetch("/api/tasks/" + taskId);
1114
- if (!res.ok) throw new Error("Server error");
1115
- const data = await res.json();
1309
+ const data = await fetchTaskDetail(taskId);
1116
1310
  renderDetailPanel(data);
1117
1311
  setActiveCard(Number(taskId));
1118
1312
  if (!detailPanel.classList.contains("open")) {
@@ -1148,9 +1342,8 @@
1148
1342
  reloadBtn.style.cssText = "background: none; border: none; cursor: pointer; font-size: 1.1em; color: red; padding: 0 2px; line-height: 1; flex-shrink: 0;";
1149
1343
  reloadBtn.addEventListener("click", async () => {
1150
1344
  try {
1151
- const taskRes = await fetch("/api/tasks/" + detailTaskId);
1152
- if (taskRes.ok) {
1153
- const taskData = await taskRes.json();
1345
+ if (detailTaskId !== null) {
1346
+ const taskData = await fetchTaskDetail(detailTaskId);
1154
1347
  renderDetailPanel(taskData);
1155
1348
  }
1156
1349
  } catch {
@@ -1175,24 +1368,13 @@
1175
1368
  priority: priorityEl ? priorityEl.value || null : null
1176
1369
  };
1177
1370
  }
1178
- async function patchAndReloadDetail(taskId, fields) {
1179
- const res = await fetch("/api/tasks/" + taskId, {
1180
- method: "PATCH",
1181
- headers: { "Content-Type": "application/json" },
1182
- body: JSON.stringify(fields)
1183
- });
1184
- if (!res.ok) throw new Error("Server error");
1185
- const getRes = await fetch("/api/tasks/" + taskId);
1186
- if (!getRes.ok) throw new Error("Failed to fetch updated task");
1187
- const data = await getRes.json();
1188
- renderDetailPanel(data);
1189
- }
1190
1371
  async function saveDetailTask() {
1191
1372
  if (detailTaskId === null) return;
1192
1373
  const fields = collectEditedTaskFields();
1193
1374
  if (!fields) return;
1194
1375
  try {
1195
- await patchAndReloadDetail(detailTaskId, fields);
1376
+ const data = await patchTask(detailTaskId, fields);
1377
+ renderDetailPanel(data);
1196
1378
  showToast("Task saved successfully");
1197
1379
  await syncTimestampAfterSave();
1198
1380
  refreshBoardCards();
@@ -1200,32 +1382,8 @@
1200
1382
  showToast("Failed to update task");
1201
1383
  }
1202
1384
  }
1203
- async function syncTimestampAfterSave() {
1204
- try {
1205
- const tsRes = await fetch("/api/board/updated-at");
1206
- if (tsRes.ok) {
1207
- const tsData = await tsRes.json();
1208
- setLastUpdatedAt(tsData.updatedAt);
1209
- }
1210
- } catch {
1211
- }
1212
- }
1213
- var PANEL_MIN_WIDTH = 280;
1214
- var PANEL_MAX_WIDTH = 800;
1215
- var PANEL_DEFAULT_WIDTH = 400;
1216
1385
  async function initPanelWidthFromConfig(detailPanel) {
1217
- let targetWidth = PANEL_DEFAULT_WIDTH;
1218
- try {
1219
- const res = await fetch("/api/config");
1220
- if (res.ok) {
1221
- const data = await res.json();
1222
- const savedWidth = data && data.board && data.board.detailPaneWidth;
1223
- if (typeof savedWidth === "number" && savedWidth >= PANEL_MIN_WIDTH && savedWidth <= PANEL_MAX_WIDTH) {
1224
- targetWidth = savedWidth;
1225
- }
1226
- }
1227
- } catch {
1228
- }
1386
+ const targetWidth = await fetchPanelWidthFromConfig();
1229
1387
  detailPanel.dataset.preferredWidth = String(targetWidth);
1230
1388
  }
1231
1389
  function attachResizeMousedown(resizeHandle, detailPanel) {
@@ -1250,12 +1408,7 @@
1250
1408
  detailPanel.style.transition = "";
1251
1409
  const currentWidth = detailPanel.offsetWidth;
1252
1410
  detailPanel.dataset.preferredWidth = String(currentWidth);
1253
- fetch("/api/config", {
1254
- method: "PUT",
1255
- headers: { "Content-Type": "application/json" },
1256
- body: JSON.stringify({ board: { detailPaneWidth: currentWidth } })
1257
- }).catch(function() {
1258
- });
1411
+ savePanelWidthToConfig(currentWidth);
1259
1412
  document.removeEventListener("mousemove", onMouseMove);
1260
1413
  document.removeEventListener("mouseup", onMouseUp);
1261
1414
  }
@@ -1268,14 +1421,17 @@
1268
1421
  initPanelWidthFromConfig(detailPanel);
1269
1422
  attachResizeMousedown(resizeHandle, detailPanel);
1270
1423
  }
1271
- function buildDetailPanelHtml() {
1272
- return '<div class="detail-panel" id="detail-panel"><div class="detail-panel-resize-handle" id="detail-panel-resize-handle"></div><div class="detail-panel-header"><h2 id="detail-panel-title">Task Detail</h2><button class="detail-panel-close" id="detail-panel-close" title="Close">&times;</button></div><div class="detail-tabs" id="detail-tabs"><button class="detail-tab active" data-tab="details">Details</button><button class="detail-tab" data-tab="comments" id="detail-tab-comments">Comments</button></div><div class="detail-panel-body" id="detail-panel-body"><div class="detail-tab-content active" id="detail-tab-content-details"></div><div class="detail-tab-content" id="detail-tab-content-comments"></div></div><div class="detail-panel-footer" id="detail-panel-footer"><button id="detail-save-btn">Save</button></div></div>';
1273
- }
1274
1424
  function initDetailPanel() {
1275
1425
  const boardContainer = document.querySelector(".board-container");
1276
1426
  boardContainer.insertAdjacentHTML("beforeend", buildDetailPanelHtml());
1277
1427
  const detailPanel = document.getElementById("detail-panel");
1278
1428
  document.getElementById("detail-panel-close")?.addEventListener("click", closeDetailPanel);
1429
+ document.getElementById("detail-panel-copy-id")?.addEventListener("click", () => {
1430
+ if (detailTaskId === null) return;
1431
+ navigator.clipboard.writeText(String(detailTaskId)).then(() => {
1432
+ showToast("Copied task ID: " + detailTaskId);
1433
+ });
1434
+ });
1279
1435
  document.addEventListener("keydown", (e) => {
1280
1436
  if (e.key === "Escape" && detailPanel.classList.contains("open")) {
1281
1437
  closeDetailPanel();
@@ -1687,6 +1843,232 @@
1687
1843
  initDarkMode();
1688
1844
  }
1689
1845
 
1846
+ // src/board/client/dependencyVisualization.ts
1847
+ var isDependencyVisible = false;
1848
+ var arrowMarkers = /* @__PURE__ */ new Map();
1849
+ var scrollListener = null;
1850
+ var columnScrollListener = null;
1851
+ var resizeListener = null;
1852
+ function getOrCreateArrowMarker(svg, color) {
1853
+ const markerId = `arrow-${color.substring(1)}`;
1854
+ if (!arrowMarkers.has(markerId)) {
1855
+ const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
1856
+ const marker = document.createElementNS("http://www.w3.org/2000/svg", "marker");
1857
+ marker.setAttribute("id", markerId);
1858
+ marker.setAttribute("markerWidth", "10");
1859
+ marker.setAttribute("markerHeight", "10");
1860
+ marker.setAttribute("refX", "8");
1861
+ marker.setAttribute("refY", "3");
1862
+ marker.setAttribute("orient", "auto");
1863
+ marker.setAttribute("markerUnits", "strokeWidth");
1864
+ const polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
1865
+ polygon.setAttribute("points", "0 0, 10 3, 0 6");
1866
+ polygon.setAttribute("fill", color);
1867
+ marker.appendChild(polygon);
1868
+ defs.appendChild(marker);
1869
+ svg.appendChild(defs);
1870
+ arrowMarkers.set(markerId, { svg });
1871
+ }
1872
+ return markerId;
1873
+ }
1874
+ function drawBezierLine(svg, x1, y1, x2, y2, color, isHovered) {
1875
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
1876
+ const dx = Math.abs(x2 - x1);
1877
+ const isSameSide = dx < 10;
1878
+ let cp1x;
1879
+ let cp2x;
1880
+ if (isSameSide) {
1881
+ const cpOffset = 80;
1882
+ cp1x = x1 + cpOffset;
1883
+ cp2x = x2 + cpOffset;
1884
+ } else {
1885
+ const cpOffset = Math.max(dx * 0.5, 60);
1886
+ cp1x = x1 < x2 ? x1 + cpOffset : x1 - cpOffset;
1887
+ cp2x = x1 < x2 ? x2 - cpOffset : x2 + cpOffset;
1888
+ }
1889
+ const pathData = `M ${x1} ${y1} C ${cp1x} ${y1}, ${cp2x} ${y2}, ${x2} ${y2}`;
1890
+ path.setAttribute("d", pathData);
1891
+ path.setAttribute("stroke", color);
1892
+ path.setAttribute("stroke-width", isHovered ? "2.5" : "1.5");
1893
+ path.setAttribute("fill", "none");
1894
+ path.setAttribute("stroke-linecap", "round");
1895
+ path.setAttribute("marker-end", `url(#${getOrCreateArrowMarker(svg, color)})`);
1896
+ path.setAttribute("class", "dependency-line");
1897
+ return path;
1898
+ }
1899
+ function getCardRect(card) {
1900
+ if (card === draggedCard) {
1901
+ return getDraggedCardVirtualRect() ?? card.getBoundingClientRect();
1902
+ }
1903
+ return card.getBoundingClientRect();
1904
+ }
1905
+ function getCardEdgePoints(fromCard, toCard, boardRect) {
1906
+ const fromRect = getCardRect(fromCard);
1907
+ const toRect = getCardRect(toCard);
1908
+ const fromCenterX = fromRect.left + fromRect.width / 2;
1909
+ const toCenterX = toRect.left + toRect.width / 2;
1910
+ const columnThreshold = 50;
1911
+ let fromX;
1912
+ let toX;
1913
+ if (Math.abs(fromCenterX - toCenterX) < columnThreshold) {
1914
+ fromX = fromRect.right - boardRect.left;
1915
+ toX = toRect.right - boardRect.left;
1916
+ } else if (fromCenterX <= toCenterX) {
1917
+ fromX = fromRect.right - boardRect.left;
1918
+ toX = toRect.left - boardRect.left;
1919
+ } else {
1920
+ fromX = fromRect.left - boardRect.left;
1921
+ toX = toRect.right - boardRect.left;
1922
+ }
1923
+ return {
1924
+ x1: fromX,
1925
+ y1: fromRect.top - boardRect.top + fromRect.height / 2,
1926
+ x2: toX,
1927
+ y2: toRect.top - boardRect.top + toRect.height / 2
1928
+ };
1929
+ }
1930
+ function createSVGOverlay() {
1931
+ const boardContainer = document.querySelector(".board-container");
1932
+ const existing = boardContainer.querySelector("#dependency-svg");
1933
+ if (existing) {
1934
+ existing.remove();
1935
+ }
1936
+ arrowMarkers.clear();
1937
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1938
+ svg.setAttribute("id", "dependency-svg");
1939
+ svg.setAttribute("width", "100%");
1940
+ svg.setAttribute("height", "100%");
1941
+ svg.setAttribute("viewBox", `0 0 ${boardContainer.offsetWidth} ${boardContainer.offsetHeight}`);
1942
+ svg.style.position = "absolute";
1943
+ svg.style.top = "0";
1944
+ svg.style.left = "0";
1945
+ svg.style.pointerEvents = "none";
1946
+ svg.style.zIndex = "5";
1947
+ boardContainer.style.position = "relative";
1948
+ boardContainer.appendChild(svg);
1949
+ return svg;
1950
+ }
1951
+ function redrawDependencies() {
1952
+ if (!isDependencyVisible) return;
1953
+ const svg = createSVGOverlay();
1954
+ const boardContainer = document.querySelector(".board-container");
1955
+ svg.querySelectorAll(".dependency-line").forEach((line) => line.remove());
1956
+ const cards = Array.from(document.querySelectorAll(".card"));
1957
+ const cardMap = /* @__PURE__ */ new Map();
1958
+ cards.forEach((card) => {
1959
+ const id = Number(card.getAttribute("data-id"));
1960
+ cardMap.set(id, card);
1961
+ });
1962
+ const hoveredCard = document.querySelector(".card:hover");
1963
+ const hoveredCardId = hoveredCard ? Number(hoveredCard.getAttribute("data-id")) : null;
1964
+ const hoveredBlockedBySet = /* @__PURE__ */ new Set();
1965
+ const hoveredBlockingSet = /* @__PURE__ */ new Set();
1966
+ if (hoveredCardId) {
1967
+ const hoveredElement = cardMap.get(hoveredCardId);
1968
+ if (hoveredElement) {
1969
+ const blockedBy = hoveredElement.getAttribute("data-blocked-by");
1970
+ const blocking = hoveredElement.getAttribute("data-blocking");
1971
+ if (blockedBy) {
1972
+ blockedBy.split(",").forEach((id) => {
1973
+ const numId = Number(id.trim());
1974
+ if (!isNaN(numId)) hoveredBlockedBySet.add(numId);
1975
+ });
1976
+ }
1977
+ if (blocking) {
1978
+ blocking.split(",").forEach((id) => {
1979
+ const numId = Number(id.trim());
1980
+ if (!isNaN(numId)) hoveredBlockingSet.add(numId);
1981
+ });
1982
+ }
1983
+ }
1984
+ }
1985
+ const boardRect = boardContainer.getBoundingClientRect();
1986
+ cards.forEach((card) => {
1987
+ const cardId = Number(card.getAttribute("data-id"));
1988
+ const blockedByStr = card.getAttribute("data-blocked-by");
1989
+ const blockingStr = card.getAttribute("data-blocking");
1990
+ if (!blockedByStr && !blockingStr) return;
1991
+ const isHovered = cardId === hoveredCardId || hoveredBlockedBySet.has(cardId) || hoveredBlockingSet.has(cardId);
1992
+ if (blockingStr) {
1993
+ const blockingIds = blockingStr.split(",").map((s) => Number(s.trim()));
1994
+ blockingIds.forEach((blockedId) => {
1995
+ const blockedCard = cardMap.get(blockedId);
1996
+ if (blockedCard) {
1997
+ const { x1, y1, x2, y2 } = getCardEdgePoints(card, blockedCard, boardRect);
1998
+ const color = isHovered || hoveredBlockedBySet.has(blockedId) ? "#ef4444" : "#cbd5e1";
1999
+ const line = drawBezierLine(svg, x1, y1, x2, y2, color, isHovered);
2000
+ svg.appendChild(line);
2001
+ }
2002
+ });
2003
+ }
2004
+ });
2005
+ svg.setAttribute("viewBox", `0 0 ${boardContainer.offsetWidth} ${boardContainer.offsetHeight}`);
2006
+ }
2007
+ function handleCardHoverEvents() {
2008
+ const cards = document.querySelectorAll(".card");
2009
+ cards.forEach((card) => {
2010
+ card.addEventListener("mouseenter", () => {
2011
+ redrawDependencies();
2012
+ });
2013
+ card.addEventListener("mouseleave", () => {
2014
+ redrawDependencies();
2015
+ });
2016
+ });
2017
+ }
2018
+ function initDependencyVisualization() {
2019
+ const toggleBtn = document.getElementById("dependency-toggle");
2020
+ if (!toggleBtn) return;
2021
+ const redrawIfVisible = () => {
2022
+ if (isDependencyVisible) {
2023
+ handleCardHoverEvents();
2024
+ redrawDependencies();
2025
+ }
2026
+ };
2027
+ registerDependencyRedrawCallback2(redrawIfVisible);
2028
+ registerDependencyRedrawCallback(redrawIfVisible);
2029
+ toggleBtn.addEventListener("click", () => {
2030
+ isDependencyVisible = !isDependencyVisible;
2031
+ if (isDependencyVisible) {
2032
+ toggleBtn.classList.add("active");
2033
+ redrawDependencies();
2034
+ handleCardHoverEvents();
2035
+ const board = document.querySelector(".board");
2036
+ const boardContainer = document.querySelector(".board-container");
2037
+ if (board) {
2038
+ scrollListener = () => redrawDependencies();
2039
+ board.addEventListener("scroll", scrollListener, { passive: true });
2040
+ }
2041
+ columnScrollListener = () => redrawDependencies();
2042
+ document.querySelectorAll(".column-body").forEach((col) => {
2043
+ col.addEventListener("scroll", columnScrollListener, { passive: true });
2044
+ });
2045
+ if (boardContainer) {
2046
+ resizeListener = () => redrawDependencies();
2047
+ window.addEventListener("resize", resizeListener, { passive: true });
2048
+ }
2049
+ } else {
2050
+ toggleBtn.classList.remove("active");
2051
+ const svg = document.querySelector("#dependency-svg");
2052
+ if (svg) svg.remove();
2053
+ const board = document.querySelector(".board");
2054
+ if (board && scrollListener) {
2055
+ board.removeEventListener("scroll", scrollListener);
2056
+ scrollListener = null;
2057
+ }
2058
+ if (columnScrollListener) {
2059
+ document.querySelectorAll(".column-body").forEach((col) => {
2060
+ col.removeEventListener("scroll", columnScrollListener);
2061
+ });
2062
+ columnScrollListener = null;
2063
+ }
2064
+ if (resizeListener) {
2065
+ window.removeEventListener("resize", resizeListener);
2066
+ resizeListener = null;
2067
+ }
2068
+ }
2069
+ });
2070
+ }
2071
+
1690
2072
  // src/board/client/main.ts
1691
2073
  initDragDrop();
1692
2074
  initAutoScroll();
@@ -1696,4 +2078,5 @@
1696
2078
  initBoardPolling();
1697
2079
  initFilters();
1698
2080
  initBurgerMenu();
2081
+ initDependencyVisualization();
1699
2082
  })();