agkan 2.15.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.ja.md +60 -236
  2. package/README.md +46 -239
  3. package/dist/board/boardRenderer.d.ts.map +1 -1
  4. package/dist/board/boardRenderer.js +27 -2
  5. package/dist/board/boardRenderer.js.map +1 -1
  6. package/dist/board/boardRoutes.d.ts +2 -0
  7. package/dist/board/boardRoutes.d.ts.map +1 -1
  8. package/dist/board/boardRoutes.js +122 -10
  9. package/dist/board/boardRoutes.js.map +1 -1
  10. package/dist/board/boardScript.d.ts +2 -0
  11. package/dist/board/boardScript.d.ts.map +1 -0
  12. package/dist/board/boardScript.js +1202 -0
  13. package/dist/board/boardScript.js.map +1 -0
  14. package/dist/board/boardStyles.d.ts +1 -1
  15. package/dist/board/boardStyles.d.ts.map +1 -1
  16. package/dist/board/boardStyles.js +49 -3
  17. package/dist/board/boardStyles.js.map +1 -1
  18. package/dist/board/client/board.js +639 -17
  19. package/dist/board/server.d.ts +2 -1
  20. package/dist/board/server.d.ts.map +1 -1
  21. package/dist/board/server.js +4 -2
  22. package/dist/board/server.js.map +1 -1
  23. package/dist/cli/commands/tag/add.d.ts.map +1 -1
  24. package/dist/cli/commands/tag/add.js +10 -11
  25. package/dist/cli/commands/tag/add.js.map +1 -1
  26. package/dist/cli/commands/tag/attach.d.ts.map +1 -1
  27. package/dist/cli/commands/tag/attach.js +10 -11
  28. package/dist/cli/commands/tag/attach.js.map +1 -1
  29. package/dist/cli/commands/tag/rename.d.ts.map +1 -1
  30. package/dist/cli/commands/tag/rename.js +10 -11
  31. package/dist/cli/commands/tag/rename.js.map +1 -1
  32. package/dist/cli/commands/task/update-parent.d.ts.map +1 -1
  33. package/dist/cli/commands/task/update-parent.js +10 -11
  34. package/dist/cli/commands/task/update-parent.js.map +1 -1
  35. package/dist/cli/index.js +4 -0
  36. package/dist/cli/index.js.map +1 -1
  37. package/dist/cli/utils/board-daemon.d.ts.map +1 -1
  38. package/dist/cli/utils/board-daemon.js +10 -4
  39. package/dist/cli/utils/board-daemon.js.map +1 -1
  40. package/dist/db/adapters/sqlite-storage-backend.d.ts +2 -1
  41. package/dist/db/adapters/sqlite-storage-backend.d.ts.map +1 -1
  42. package/dist/db/adapters/sqlite-storage-backend.js +53 -2
  43. package/dist/db/adapters/sqlite-storage-backend.js.map +1 -1
  44. package/dist/db/connection.d.ts +5 -0
  45. package/dist/db/connection.d.ts.map +1 -1
  46. package/dist/db/connection.js +9 -0
  47. package/dist/db/connection.js.map +1 -1
  48. package/dist/db/types/repository.d.ts +34 -0
  49. package/dist/db/types/repository.d.ts.map +1 -1
  50. package/dist/errors.d.ts +27 -0
  51. package/dist/errors.d.ts.map +1 -0
  52. package/dist/errors.js +51 -0
  53. package/dist/errors.js.map +1 -0
  54. package/dist/models/Task.d.ts +2 -0
  55. package/dist/models/Task.d.ts.map +1 -1
  56. package/dist/services/ClaudeProcessService.d.ts.map +1 -1
  57. package/dist/services/ClaudeProcessService.js +2 -1
  58. package/dist/services/ClaudeProcessService.js.map +1 -1
  59. package/dist/services/CommentService.d.ts.map +1 -1
  60. package/dist/services/CommentService.js +3 -2
  61. package/dist/services/CommentService.js.map +1 -1
  62. package/dist/services/ExportImportService.d.ts.map +1 -1
  63. package/dist/services/ExportImportService.js +17 -15
  64. package/dist/services/ExportImportService.js.map +1 -1
  65. package/dist/services/ProcessService.d.ts +54 -0
  66. package/dist/services/ProcessService.d.ts.map +1 -0
  67. package/dist/services/ProcessService.js +147 -0
  68. package/dist/services/ProcessService.js.map +1 -0
  69. package/dist/services/TagService.d.ts.map +1 -1
  70. package/dist/services/TagService.js +7 -6
  71. package/dist/services/TagService.js.map +1 -1
  72. package/dist/services/TaskBlockService.d.ts.map +1 -1
  73. package/dist/services/TaskBlockService.js +5 -4
  74. package/dist/services/TaskBlockService.js.map +1 -1
  75. package/dist/services/TaskService.d.ts +2 -0
  76. package/dist/services/TaskService.d.ts.map +1 -1
  77. package/dist/services/TaskService.js +28 -10
  78. package/dist/services/TaskService.js.map +1 -1
  79. package/dist/services/TaskTagService.d.ts.map +1 -1
  80. package/dist/services/TaskTagService.js +4 -3
  81. package/dist/services/TaskTagService.js.map +1 -1
  82. package/dist/services/TmuxService.d.ts +2 -0
  83. package/dist/services/TmuxService.d.ts.map +1 -0
  84. package/dist/services/TmuxService.js +7 -0
  85. package/dist/services/TmuxService.js.map +1 -0
  86. package/dist/services/index.d.ts +2 -0
  87. package/dist/services/index.d.ts.map +1 -1
  88. package/dist/services/index.js +3 -1
  89. package/dist/services/index.js.map +1 -1
  90. package/package.json +1 -1
  91. package/dist/models/Attachment.d.ts +0 -25
  92. package/dist/models/Attachment.d.ts.map +0 -1
  93. package/dist/models/Attachment.js +0 -7
  94. package/dist/models/Attachment.js.map +0 -1
  95. package/dist/services/AttachmentService.d.ts +0 -62
  96. package/dist/services/AttachmentService.d.ts.map +0 -1
  97. package/dist/services/AttachmentService.js +0 -95
  98. package/dist/services/AttachmentService.js.map +0 -1
@@ -30,6 +30,258 @@
30
30
  setTimeout(() => toast.classList.remove("show"), 3e3);
31
31
  }
32
32
 
33
+ // src/board/client/claudeButton.ts
34
+ var _claudeModalCallback = null;
35
+ var _runningTaskIds = /* @__PURE__ */ new Set();
36
+ var _planningTaskIds = /* @__PURE__ */ new Set();
37
+ var _inFlightTaskIds = /* @__PURE__ */ new Set();
38
+ function registerClaudeModalCallback(callback) {
39
+ _claudeModalCallback = callback;
40
+ }
41
+ function getRunningTaskIds() {
42
+ return _runningTaskIds;
43
+ }
44
+ function updateButtonStates(runningTaskIds, planningTaskIds = /* @__PURE__ */ new Set()) {
45
+ _runningTaskIds = runningTaskIds;
46
+ const onlyPlanningRunning = runningTaskIds.size > 0 && [...runningTaskIds].every((id) => planningTaskIds.has(id));
47
+ const anyRunning = runningTaskIds.size > 0 && !onlyPlanningRunning;
48
+ const indicator = document.getElementById("header-running-indicator");
49
+ if (indicator) {
50
+ indicator.style.display = runningTaskIds.size > 0 ? "" : "none";
51
+ }
52
+ document.querySelectorAll(".claude-run-split").forEach((split) => {
53
+ const taskId = Number(split.dataset.taskId);
54
+ if (runningTaskIds.has(taskId)) {
55
+ replaceWithDetailBtn(split, taskId);
56
+ } else {
57
+ split.querySelectorAll(".claude-run-btn, .claude-run-toggle, .claude-run-menu-item").forEach((btn) => {
58
+ btn.disabled = anyRunning;
59
+ });
60
+ }
61
+ });
62
+ document.querySelectorAll(".claude-plan-btn").forEach((btn) => {
63
+ const taskId = Number(btn.dataset.taskId);
64
+ if (runningTaskIds.has(taskId)) {
65
+ replaceWithDetailBtn(btn, taskId);
66
+ }
67
+ });
68
+ document.querySelectorAll(".claude-detail-btn").forEach((btn) => {
69
+ const taskId = Number(btn.dataset.taskId);
70
+ if (!runningTaskIds.has(taskId)) {
71
+ const card = btn.closest(".card");
72
+ const status = card?.dataset.status;
73
+ replaceWithRunOrPlanBtn(btn, taskId, status);
74
+ }
75
+ });
76
+ }
77
+ function replaceWithDetailBtn(btn, taskId) {
78
+ const detailBtn = document.createElement("button");
79
+ detailBtn.className = "claude-detail-btn";
80
+ detailBtn.dataset.taskId = String(taskId);
81
+ detailBtn.textContent = "\u25CF Details";
82
+ attachDetailBtnListener(detailBtn);
83
+ btn.replaceWith(detailBtn);
84
+ }
85
+ function replaceWithRunOrPlanBtn(btn, taskId, status) {
86
+ _planningTaskIds.delete(taskId);
87
+ if (["review", "done", "closed"].includes(status ?? "")) {
88
+ btn.remove();
89
+ return;
90
+ }
91
+ if (["ready", "in_progress"].includes(status ?? "")) {
92
+ const split = createRunSplitElement(taskId);
93
+ btn.replaceWith(split);
94
+ } else {
95
+ const newBtn = document.createElement("button");
96
+ newBtn.className = "claude-plan-btn";
97
+ newBtn.dataset.taskId = String(taskId);
98
+ newBtn.innerHTML = "📋 Planning";
99
+ attachPlanBtnListener(newBtn);
100
+ btn.replaceWith(newBtn);
101
+ }
102
+ }
103
+ function createRunSplitElement(taskId) {
104
+ const split = document.createElement("div");
105
+ split.className = "claude-run-split";
106
+ split.dataset.taskId = String(taskId);
107
+ const mainBtn = document.createElement("button");
108
+ mainBtn.className = "claude-run-btn";
109
+ mainBtn.dataset.taskId = String(taskId);
110
+ mainBtn.innerHTML = "▶ Run";
111
+ const toggleBtn = document.createElement("button");
112
+ toggleBtn.className = "claude-run-toggle";
113
+ toggleBtn.dataset.taskId = String(taskId);
114
+ toggleBtn.title = "More options";
115
+ toggleBtn.innerHTML = "▼";
116
+ const menu = document.createElement("div");
117
+ menu.className = "claude-run-menu";
118
+ const directItem = document.createElement("button");
119
+ directItem.className = "claude-run-menu-item";
120
+ directItem.dataset.taskId = String(taskId);
121
+ directItem.dataset.command = "direct";
122
+ directItem.innerHTML = "▶ Run (current branch)";
123
+ const prItem = document.createElement("button");
124
+ prItem.className = "claude-run-menu-item";
125
+ prItem.dataset.taskId = String(taskId);
126
+ prItem.dataset.command = "pr";
127
+ prItem.innerHTML = "▶ Run (create PR)";
128
+ menu.appendChild(directItem);
129
+ menu.appendChild(prItem);
130
+ split.appendChild(mainBtn);
131
+ split.appendChild(toggleBtn);
132
+ split.appendChild(menu);
133
+ attachRunSplitListeners(split, mainBtn, toggleBtn, directItem, prItem, taskId);
134
+ return split;
135
+ }
136
+ function attachRunSplitListeners(split, mainBtn, toggleBtn, directItem, prItem, taskId) {
137
+ split.dataset.listenersAttached = "true";
138
+ mainBtn.addEventListener("click", async (e) => {
139
+ e.stopPropagation();
140
+ await triggerRunTask(taskId, split, {});
141
+ });
142
+ toggleBtn.addEventListener("click", (e) => {
143
+ e.stopPropagation();
144
+ split.classList.toggle("open");
145
+ });
146
+ directItem.addEventListener("click", async (e) => {
147
+ e.stopPropagation();
148
+ split.classList.remove("open");
149
+ await triggerRunTask(taskId, split, {});
150
+ });
151
+ prItem.addEventListener("click", async (e) => {
152
+ e.stopPropagation();
153
+ split.classList.remove("open");
154
+ await triggerRunTask(taskId, split, { command: "pr" });
155
+ });
156
+ }
157
+ function attachPlanBtnListener(btn) {
158
+ btn.dataset.listenersAttached = "true";
159
+ btn.addEventListener("click", async (e) => {
160
+ e.stopPropagation();
161
+ const taskId = Number(btn.dataset.taskId);
162
+ await triggerRunTask(taskId, btn, { command: "planning" });
163
+ });
164
+ }
165
+ function attachDetailBtnListener(btn) {
166
+ btn.dataset.listenersAttached = "true";
167
+ btn.addEventListener("click", (e) => {
168
+ e.stopPropagation();
169
+ const taskId = Number(btn.dataset.taskId);
170
+ if (_claudeModalCallback) {
171
+ _claudeModalCallback(taskId);
172
+ }
173
+ });
174
+ }
175
+ async function triggerRunTask(taskId, btn, body) {
176
+ if (_inFlightTaskIds.has(taskId)) return;
177
+ _inFlightTaskIds.add(taskId);
178
+ btn.disabled = true;
179
+ btn.querySelectorAll("button").forEach((b) => b.disabled = true);
180
+ try {
181
+ const res = await fetch(`/api/claude/tasks/${taskId}/run`, {
182
+ method: "POST",
183
+ headers: { "Content-Type": "application/json" },
184
+ body: JSON.stringify(body)
185
+ });
186
+ if (res.ok) {
187
+ _runningTaskIds = new Set(_runningTaskIds).add(taskId);
188
+ if (body.command === "planning") {
189
+ _planningTaskIds = new Set(_planningTaskIds).add(taskId);
190
+ }
191
+ replaceWithDetailBtn(btn, taskId);
192
+ } else {
193
+ btn.disabled = false;
194
+ btn.querySelectorAll("button").forEach((b) => b.disabled = false);
195
+ let errorDetail = `HTTP ${res.status}`;
196
+ try {
197
+ const data = await res.json();
198
+ if (data.error) errorDetail += `: ${data.error}`;
199
+ } catch {
200
+ }
201
+ console.error(`[claude] Failed to start task ${taskId}: ${errorDetail}`);
202
+ if (res.status === 409) {
203
+ alert(`\u3053\u306E\u30BF\u30B9\u30AF\u306F\u3059\u3067\u306B\u5B9F\u884C\u4E2D\u3067\u3059 (task ${taskId})`);
204
+ } else {
205
+ alert(`Claude\u8D77\u52D5\u30A8\u30E9\u30FC (task ${taskId}): ${errorDetail}`);
206
+ }
207
+ }
208
+ } catch (err) {
209
+ btn.disabled = false;
210
+ btn.querySelectorAll("button").forEach((b) => b.disabled = false);
211
+ console.error(`[claude] Network error starting task ${taskId}:`, err);
212
+ } finally {
213
+ _inFlightTaskIds.delete(taskId);
214
+ }
215
+ }
216
+ async function pollRunningTasks() {
217
+ try {
218
+ const res = await fetch("/api/running-tasks");
219
+ if (!res.ok) return;
220
+ const data = await res.json();
221
+ const allIds = new Set(data.tasks.map((t) => t.taskId));
222
+ const planningIds = new Set(data.tasks.filter((t) => t.command === "planning").map((t) => t.taskId));
223
+ updateButtonStates(allIds, planningIds);
224
+ } catch {
225
+ }
226
+ }
227
+ function updateCardButton(card, newStatus) {
228
+ const taskId = Number(card.dataset.id);
229
+ if (_runningTaskIds.has(taskId)) return;
230
+ const existingEl = card.querySelector(".claude-run-split, .claude-plan-btn, .claude-detail-btn");
231
+ if (!existingEl) return;
232
+ if (["review", "done", "closed"].includes(newStatus)) {
233
+ existingEl.remove();
234
+ } else if (["ready", "in_progress"].includes(newStatus)) {
235
+ if (!existingEl.classList.contains("claude-run-split")) {
236
+ const split = createRunSplitElement(taskId);
237
+ existingEl.replaceWith(split);
238
+ }
239
+ } else {
240
+ if (!existingEl.classList.contains("claude-plan-btn")) {
241
+ const newBtn = document.createElement("button");
242
+ newBtn.className = "claude-plan-btn";
243
+ newBtn.dataset.taskId = String(taskId);
244
+ newBtn.innerHTML = "📋 Planning";
245
+ attachPlanBtnListener(newBtn);
246
+ existingEl.replaceWith(newBtn);
247
+ }
248
+ }
249
+ }
250
+ function attachClaudeButtonListeners(body) {
251
+ body.querySelectorAll(".claude-run-split").forEach((split) => {
252
+ if (split.dataset.listenersAttached) return;
253
+ const taskId = Number(split.dataset.taskId);
254
+ const mainBtn = split.querySelector(".claude-run-btn");
255
+ const toggleBtn = split.querySelector(".claude-run-toggle");
256
+ const directItem = split.querySelector('[data-command="direct"]');
257
+ const prItem = split.querySelector('[data-command="pr"]');
258
+ if (mainBtn && toggleBtn && directItem && prItem) {
259
+ attachRunSplitListeners(split, mainBtn, toggleBtn, directItem, prItem, taskId);
260
+ }
261
+ });
262
+ body.querySelectorAll(".claude-plan-btn").forEach((btn) => {
263
+ if (btn.dataset.listenersAttached) return;
264
+ attachPlanBtnListener(btn);
265
+ });
266
+ body.querySelectorAll(".claude-detail-btn").forEach((btn) => {
267
+ if (btn.dataset.listenersAttached) return;
268
+ attachDetailBtnListener(btn);
269
+ });
270
+ updateButtonStates(_runningTaskIds, _planningTaskIds);
271
+ }
272
+ function initClaudeButton() {
273
+ document.querySelectorAll(".column-body").forEach((body) => {
274
+ attachClaudeButtonListeners(body);
275
+ });
276
+ document.addEventListener("click", () => {
277
+ document.querySelectorAll(".claude-run-split.open").forEach((el) => {
278
+ el.classList.remove("open");
279
+ });
280
+ });
281
+ setInterval(pollRunningTasks, 2500);
282
+ pollRunningTasks();
283
+ }
284
+
33
285
  // src/board/client/dragDrop.ts
34
286
  var _redrawDependencies = null;
35
287
  function registerDependencyRedrawCallback(callback) {
@@ -37,6 +289,7 @@
37
289
  }
38
290
  var draggedCard = null;
39
291
  var sourceBody = null;
292
+ var isPendingStatusUpdate = false;
40
293
  var _dragMouseX = 0;
41
294
  var _dragMouseY = 0;
42
295
  var _dragOffsetX = 0;
@@ -70,6 +323,8 @@
70
323
  draggedCard.dataset.status = newStatus;
71
324
  updateCount(oldStatus);
72
325
  updateCount(newStatus);
326
+ updateCardButton(draggedCard, newStatus);
327
+ isPendingStatusUpdate = true;
73
328
  try {
74
329
  const res = await fetch("/api/tasks/" + taskId, {
75
330
  method: "PATCH",
@@ -88,6 +343,8 @@
88
343
  updateCount(newStatus);
89
344
  }
90
345
  showToast();
346
+ } finally {
347
+ isPendingStatusUpdate = false;
91
348
  }
92
349
  }
93
350
  var _documentDragOverListener = null;
@@ -423,6 +680,7 @@
423
680
  var _renderDetailPanel = null;
424
681
  var _showUpdateWarning = null;
425
682
  var _getDetailTaskId2 = null;
683
+ var _getDetailActiveTab = null;
426
684
  var _setActiveCard = null;
427
685
  var _redrawDependencies2 = null;
428
686
  function registerDetailPanelCallbacks(callbacks) {
@@ -430,6 +688,7 @@
430
688
  _renderDetailPanel = callbacks.renderDetailPanel;
431
689
  _showUpdateWarning = callbacks.showUpdateWarning;
432
690
  _getDetailTaskId2 = callbacks.getDetailTaskId;
691
+ _getDetailActiveTab = callbacks.getDetailActiveTab;
433
692
  _setActiveCard = callbacks.setActiveCard;
434
693
  }
435
694
  function registerDependencyRedrawCallback2(callback) {
@@ -497,6 +756,8 @@
497
756
  }
498
757
  attachCardListeners(body);
499
758
  attachAutoScrollToBody(body);
759
+ attachClaudeButtonListeners(body);
760
+ updateButtonStates(getRunningTaskIds());
500
761
  }
501
762
  function isEditingDetailPanel() {
502
763
  const editableFields = ["detail-edit-title", "detail-edit-body", "detail-edit-status", "detail-edit-priority"];
@@ -507,6 +768,9 @@
507
768
  if (_showUpdateWarning) _showUpdateWarning();
508
769
  return;
509
770
  }
771
+ if (_getDetailActiveTab && _getDetailActiveTab() === "run-logs") {
772
+ return;
773
+ }
510
774
  try {
511
775
  const taskRes = await fetch("/api/tasks/" + detailTaskId2);
512
776
  if (taskRes.ok) {
@@ -538,7 +802,7 @@
538
802
  }
539
803
  }
540
804
  async function pollBoardUpdates() {
541
- if (draggedCard !== null) return;
805
+ if (draggedCard !== null || isPendingStatusUpdate) return;
542
806
  try {
543
807
  const res = await fetch("/api/board/updated-at");
544
808
  if (!res.ok) return;
@@ -911,8 +1175,8 @@
911
1175
  });
912
1176
  if (!res.ok) throw new Error("Server error");
913
1177
  }
914
- async function fetchTaskDetail(taskId) {
915
- const res = await fetch("/api/tasks/" + taskId);
1178
+ async function fetchTaskDetail(taskId, signal) {
1179
+ const res = await fetch("/api/tasks/" + taskId, signal ? { signal } : void 0);
916
1180
  if (!res.ok) throw new Error("Server error");
917
1181
  return res.json();
918
1182
  }
@@ -958,6 +1222,12 @@
958
1222
  }).catch(function() {
959
1223
  });
960
1224
  }
1225
+ async function fetchRunLogs(taskId) {
1226
+ const res = await fetch("/api/claude/tasks/" + taskId + "/run-logs");
1227
+ if (!res.ok) throw new Error("Server error");
1228
+ const data = await res.json();
1229
+ return data.logs || [];
1230
+ }
961
1231
 
962
1232
  // src/board/client/detailPanelHtml.ts
963
1233
  function renderCommentItemHtml(comment, taskId) {
@@ -1084,8 +1354,36 @@
1084
1354
  html += renderEditableTextFields(task);
1085
1355
  return html;
1086
1356
  }
1357
+ function renderRunLogsHtml(logs) {
1358
+ if (logs.length === 0) {
1359
+ return '<div class="run-log-empty">No run logs yet.</div>';
1360
+ }
1361
+ let html = '<div class="run-log-list">';
1362
+ logs.forEach((log, index) => {
1363
+ const date = log.started_at ? log.started_at.replace("T", " ").replace(/\.\d+Z$/, "") : "";
1364
+ const exitOk = log.exit_code === 0;
1365
+ const exitLabel = log.exit_code !== null ? "exit: " + String(log.exit_code) : "running";
1366
+ const exitClass = exitOk ? "success" : "failure";
1367
+ const isFirst = index === 0;
1368
+ html += '<div class="run-log-item' + (isFirst ? " open" : "") + '" data-log-id="' + log.id + '"><div class="run-log-header" data-action="toggle-run-log"><span class="run-log-toggle">&#9654;</span><span class="run-log-date">' + escapeHtmlClient(date) + '</span><span class="run-log-exit ' + exitClass + '">' + escapeHtmlClient(exitLabel) + '</span></div><div class="run-log-body">';
1369
+ log.events.forEach((evt) => {
1370
+ if (evt.kind === "text" && evt.text) {
1371
+ html += escapeHtmlClient(evt.text);
1372
+ } else if (evt.kind === "tool_use" && evt.name) {
1373
+ const mainArg = evt.input && typeof evt.input === "object" ? String(
1374
+ evt.input.path ?? evt.input.command ?? ""
1375
+ ) : "";
1376
+ const display = mainArg ? "\u{1F527} " + evt.name + ": " + mainArg : "\u{1F527} " + evt.name;
1377
+ html += '<span class="run-log-tool-use">' + escapeHtmlClient(display) + "\n</span>";
1378
+ }
1379
+ });
1380
+ html += "</div></div>";
1381
+ });
1382
+ html += "</div>";
1383
+ return html;
1384
+ }
1087
1385
  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>';
1386
+ 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><button class="detail-tab" data-tab="run-logs" id="detail-tab-run-logs">Run Logs</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 class="detail-tab-content" id="detail-tab-content-run-logs"></div></div><div class="detail-panel-footer" id="detail-panel-footer"><button id="detail-save-btn">Save</button></div></div>';
1089
1387
  }
1090
1388
  function autoResizeTextarea(el) {
1091
1389
  const scrollContainer = el.closest(".detail-tab-content");
@@ -1098,9 +1396,22 @@
1098
1396
  // src/board/client/detailPanel.ts
1099
1397
  var detailTaskId = null;
1100
1398
  var lastTab = "details";
1399
+ var runLogPollingInterval = null;
1400
+ var currentFetchController = null;
1401
+ var runLogLoadSeq = 0;
1402
+ var runLogsLoadedTaskId = null;
1403
+ function stopRunLogPolling() {
1404
+ if (runLogPollingInterval !== null) {
1405
+ clearInterval(runLogPollingInterval);
1406
+ runLogPollingInterval = null;
1407
+ }
1408
+ }
1101
1409
  function getDetailTaskId() {
1102
1410
  return detailTaskId;
1103
1411
  }
1412
+ function getDetailActiveTab() {
1413
+ return lastTab;
1414
+ }
1104
1415
  function setActiveCard(taskId) {
1105
1416
  document.querySelectorAll(".card.active").forEach((card) => {
1106
1417
  card.classList.remove("active");
@@ -1111,6 +1422,13 @@
1111
1422
  }
1112
1423
  }
1113
1424
  function closeDetailPanel() {
1425
+ stopRunLogPolling();
1426
+ runLogsLoadedTaskId = null;
1427
+ const runLogsPane = document.getElementById("detail-tab-content-run-logs");
1428
+ if (runLogsPane) {
1429
+ delete runLogsPane.dataset.runLogsTaskId;
1430
+ delete runLogsPane.dataset.runLogsSignature;
1431
+ }
1114
1432
  const detailPanel = document.getElementById("detail-panel");
1115
1433
  detailPanel.classList.remove("open");
1116
1434
  detailPanel.style.width = "";
@@ -1118,26 +1436,41 @@
1118
1436
  detailTaskId = null;
1119
1437
  }
1120
1438
  function switchTab(tabName) {
1439
+ const activeTabBtn = document.querySelector(".detail-tab.active");
1440
+ const currentTab = activeTabBtn?.dataset.tab ?? null;
1441
+ const isSameTab = currentTab === tabName;
1442
+ if (tabName !== "run-logs") {
1443
+ stopRunLogPolling();
1444
+ }
1121
1445
  lastTab = tabName;
1122
- document.querySelectorAll(".detail-tab").forEach((btn) => {
1123
- btn.classList.toggle("active", btn.dataset.tab === tabName);
1124
- });
1125
- document.querySelectorAll(".detail-tab-content").forEach((el) => {
1126
- el.classList.toggle("active", el.id === "detail-tab-content-" + tabName);
1127
- });
1446
+ if (!isSameTab) {
1447
+ document.querySelectorAll(".detail-tab").forEach((btn) => {
1448
+ btn.classList.toggle("active", btn.dataset.tab === tabName);
1449
+ });
1450
+ document.querySelectorAll(".detail-tab-content").forEach((el) => {
1451
+ el.classList.toggle("active", el.id === "detail-tab-content-" + tabName);
1452
+ });
1453
+ }
1128
1454
  const footer = document.getElementById("detail-panel-footer");
1129
1455
  if (footer) footer.style.display = tabName === "details" ? "" : "none";
1456
+ if (tabName === "run-logs" && detailTaskId !== null && (!isSameTab || runLogsLoadedTaskId !== detailTaskId)) {
1457
+ loadRunLogs(detailTaskId).catch((err) => {
1458
+ console.error("[agkan] switchTab loadRunLogs failed", err);
1459
+ });
1460
+ }
1130
1461
  }
1131
1462
  async function loadComments(taskId) {
1132
- const tabBtn = document.getElementById("detail-tab-comments");
1133
1463
  const pane = document.getElementById("detail-tab-content-comments");
1134
1464
  if (!pane) return;
1135
1465
  try {
1136
1466
  const comments = await fetchComments(taskId);
1467
+ if (detailTaskId !== taskId) return;
1468
+ const tabBtn = document.getElementById("detail-tab-comments");
1137
1469
  if (tabBtn) tabBtn.textContent = "Comments (" + comments.length + ")";
1138
1470
  renderComments(taskId, comments);
1139
1471
  } catch (err) {
1140
1472
  console.error("[agkan] loadComments failed for task", taskId, err);
1473
+ if (detailTaskId !== taskId) return;
1141
1474
  if (pane) pane.innerHTML = '<div style="padding:20px;font-size:12px;color:#94a3b8;">Failed to load comments</div>';
1142
1475
  }
1143
1476
  }
@@ -1262,6 +1595,99 @@
1262
1595
  const taskId = target.dataset.taskId ? Number(target.dataset.taskId) : NaN;
1263
1596
  dispatchCommentAction(action, commentId, taskId);
1264
1597
  }
1598
+ function buildRunLogsSignature(logs) {
1599
+ return JSON.stringify(logs);
1600
+ }
1601
+ function renderRunLogsInPane(pane, logs) {
1602
+ const nextSignature = buildRunLogsSignature(logs);
1603
+ if (pane.dataset.runLogsSignature === nextSignature) return;
1604
+ pane.dataset.runLogsSignature = nextSignature;
1605
+ const bodyScrollState = /* @__PURE__ */ new Map();
1606
+ const openLogIds = /* @__PURE__ */ new Set();
1607
+ const hadPreviousItems = pane.querySelector(".run-log-item") !== null;
1608
+ const paneScrollTop = pane.scrollTop;
1609
+ const paneIsNearBottom = pane.scrollHeight - pane.scrollTop - pane.clientHeight <= 50;
1610
+ pane.querySelectorAll(".run-log-item.open").forEach((item) => {
1611
+ const logId = Number(item.dataset.logId);
1612
+ if (!logId) return;
1613
+ openLogIds.add(logId);
1614
+ const body = item.querySelector(".run-log-body");
1615
+ if (body) {
1616
+ const isNearBottom = body.scrollHeight - body.scrollTop - body.clientHeight <= 50;
1617
+ bodyScrollState.set(logId, { scrollTop: body.scrollTop, isNearBottom });
1618
+ }
1619
+ });
1620
+ pane.innerHTML = renderRunLogsHtml(logs);
1621
+ requestAnimationFrame(() => {
1622
+ if (hadPreviousItems) {
1623
+ pane.scrollTop = paneIsNearBottom ? pane.scrollHeight : paneScrollTop;
1624
+ }
1625
+ pane.querySelectorAll(".run-log-item").forEach((item) => {
1626
+ const logId = Number(item.dataset.logId);
1627
+ if (!logId) return;
1628
+ const body = item.querySelector(".run-log-body");
1629
+ if (!body) return;
1630
+ if (hadPreviousItems) {
1631
+ if (openLogIds.has(logId)) {
1632
+ item.classList.add("open");
1633
+ const state = bodyScrollState.get(logId);
1634
+ if (state) {
1635
+ body.scrollTop = state.isNearBottom ? body.scrollHeight : state.scrollTop;
1636
+ } else {
1637
+ body.scrollTop = body.scrollHeight;
1638
+ }
1639
+ } else {
1640
+ item.classList.remove("open");
1641
+ }
1642
+ } else {
1643
+ if (item.classList.contains("open")) {
1644
+ body.scrollTop = body.scrollHeight;
1645
+ }
1646
+ }
1647
+ });
1648
+ });
1649
+ }
1650
+ async function loadRunLogs(taskId) {
1651
+ stopRunLogPolling();
1652
+ const seq = ++runLogLoadSeq;
1653
+ const pane = document.getElementById("detail-tab-content-run-logs");
1654
+ if (!pane) return;
1655
+ runLogsLoadedTaskId = taskId;
1656
+ if (pane.dataset.runLogsTaskId !== String(taskId)) {
1657
+ pane.dataset.runLogsTaskId = String(taskId);
1658
+ delete pane.dataset.runLogsSignature;
1659
+ }
1660
+ pane.removeEventListener("click", handleRunLogToggle);
1661
+ pane.addEventListener("click", handleRunLogToggle);
1662
+ try {
1663
+ const logs = await fetchRunLogs(taskId);
1664
+ if (seq !== runLogLoadSeq) return;
1665
+ renderRunLogsInPane(pane, logs);
1666
+ runLogPollingInterval = setInterval(() => {
1667
+ if (detailTaskId !== taskId) {
1668
+ stopRunLogPolling();
1669
+ return;
1670
+ }
1671
+ fetchRunLogs(taskId).then((updated) => {
1672
+ if (seq !== runLogLoadSeq) return;
1673
+ const p = document.getElementById("detail-tab-content-run-logs");
1674
+ if (p) renderRunLogsInPane(p, updated);
1675
+ }).catch(() => stopRunLogPolling());
1676
+ }, 2e3);
1677
+ } catch (err) {
1678
+ console.error("[agkan] loadRunLogs failed for task", taskId, err);
1679
+ if (seq !== runLogLoadSeq) return;
1680
+ runLogsLoadedTaskId = null;
1681
+ delete pane.dataset.runLogsSignature;
1682
+ pane.innerHTML = '<div style="padding:20px;font-size:12px;color:#94a3b8;">Failed to load run logs</div>';
1683
+ }
1684
+ }
1685
+ function handleRunLogToggle(e) {
1686
+ const target = e.target.closest('[data-action="toggle-run-log"]');
1687
+ if (!target) return;
1688
+ const item = target.closest(".run-log-item");
1689
+ if (item) item.classList.toggle("open");
1690
+ }
1265
1691
  function renderDetailPanel(data) {
1266
1692
  document.getElementById("detail-panel-update-warning")?.remove();
1267
1693
  const detailPanelTitle = document.getElementById("detail-panel-title");
@@ -1288,9 +1714,36 @@
1288
1714
  }
1289
1715
  const textarea = document.getElementById("detail-edit-body");
1290
1716
  if (textarea) {
1291
- requestAnimationFrame(() => {
1292
- autoResizeTextarea(textarea);
1293
- });
1717
+ const detailPanel = document.getElementById("detail-panel");
1718
+ if (detailPanel && !detailPanel.classList.contains("open")) {
1719
+ const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1720
+ if (prefersReducedMotion) {
1721
+ autoResizeTextarea(textarea);
1722
+ } else {
1723
+ let done = false;
1724
+ const finish = () => {
1725
+ if (done) return;
1726
+ done = true;
1727
+ detailPanel.removeEventListener("transitionend", onTransitionEnd);
1728
+ requestAnimationFrame(() => {
1729
+ requestAnimationFrame(() => {
1730
+ autoResizeTextarea(textarea);
1731
+ });
1732
+ });
1733
+ };
1734
+ const onTransitionEnd = (e) => {
1735
+ if (e.propertyName === "width") finish();
1736
+ };
1737
+ detailPanel.addEventListener("transitionend", onTransitionEnd);
1738
+ setTimeout(finish, 260);
1739
+ }
1740
+ } else {
1741
+ requestAnimationFrame(() => {
1742
+ requestAnimationFrame(() => {
1743
+ autoResizeTextarea(textarea);
1744
+ });
1745
+ });
1746
+ }
1294
1747
  textarea.addEventListener("input", () => {
1295
1748
  autoResizeTextarea(textarea);
1296
1749
  });
@@ -1305,16 +1758,23 @@
1305
1758
  async function openTaskDetail(taskId) {
1306
1759
  const detailPanel = document.getElementById("detail-panel");
1307
1760
  const PANEL_DEFAULT_WIDTH2 = 400;
1761
+ setActiveCard(Number(taskId));
1762
+ if (currentFetchController) {
1763
+ currentFetchController.abort();
1764
+ }
1765
+ currentFetchController = new AbortController();
1766
+ const { signal } = currentFetchController;
1308
1767
  try {
1309
- const data = await fetchTaskDetail(taskId);
1768
+ const data = await fetchTaskDetail(taskId, signal);
1769
+ currentFetchController = null;
1310
1770
  renderDetailPanel(data);
1311
- setActiveCard(Number(taskId));
1312
1771
  if (!detailPanel.classList.contains("open")) {
1313
1772
  const preferredWidth = detailPanel.dataset.preferredWidth || String(PANEL_DEFAULT_WIDTH2);
1314
1773
  detailPanel.style.width = preferredWidth + "px";
1315
1774
  detailPanel.classList.add("open");
1316
1775
  }
1317
1776
  } catch (err) {
1777
+ if (err instanceof DOMException && err.name === "AbortError") return;
1318
1778
  console.error("[agkan] openTaskDetail failed for task", taskId, err);
1319
1779
  showToast("Failed to load task details");
1320
1780
  }
@@ -1339,7 +1799,7 @@
1339
1799
  const reloadBtn = document.createElement("button");
1340
1800
  reloadBtn.title = "Reload latest data";
1341
1801
  reloadBtn.textContent = "\u21BA";
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;";
1802
+ reloadBtn.style.cssText = "background: none; border: none; cursor: pointer; font-size: 1.5em; color: red; padding: 0 4px; line-height: 1; flex-shrink: 0;";
1343
1803
  reloadBtn.addEventListener("click", async () => {
1344
1804
  try {
1345
1805
  if (detailTaskId !== null) {
@@ -1454,6 +1914,7 @@
1454
1914
  renderDetailPanel,
1455
1915
  showUpdateWarning,
1456
1916
  getDetailTaskId,
1917
+ getDetailActiveTab,
1457
1918
  setActiveCard
1458
1919
  });
1459
1920
  registerGetDetailTaskId(getDetailTaskId);
@@ -2069,6 +2530,161 @@
2069
2530
  });
2070
2531
  }
2071
2532
 
2533
+ // src/board/client/claudeStreamModal.ts
2534
+ var _currentEventSource = null;
2535
+ var _currentTaskId = null;
2536
+ var _claudeButtonUpdateCallback = null;
2537
+ function registerClaudeButtonUpdateCallback(cb) {
2538
+ _claudeButtonUpdateCallback = cb;
2539
+ }
2540
+ function stripAnsi(text) {
2541
+ return text.replace(/\x1b\[[0-9;]*m/g, "");
2542
+ }
2543
+ function getModalElements() {
2544
+ return {
2545
+ overlay: document.getElementById("claude-stream-modal"),
2546
+ title: document.getElementById("claude-stream-modal-title"),
2547
+ log: document.getElementById("claude-stream-log"),
2548
+ status: document.getElementById("claude-stream-status"),
2549
+ stopBtn: document.getElementById("claude-stream-stop-btn"),
2550
+ closeBtn: document.getElementById("claude-stream-close-btn"),
2551
+ modalClose: document.getElementById("claude-stream-modal-close")
2552
+ };
2553
+ }
2554
+ function isAutoScrollEnabled(log) {
2555
+ const threshold = 50;
2556
+ return log.scrollHeight - log.scrollTop - log.clientHeight <= threshold;
2557
+ }
2558
+ function appendToLog(log, text, className) {
2559
+ const shouldScroll = isAutoScrollEnabled(log);
2560
+ const line = document.createElement("div");
2561
+ line.textContent = text;
2562
+ if (className) {
2563
+ line.className = className;
2564
+ }
2565
+ log.appendChild(line);
2566
+ if (shouldScroll) {
2567
+ log.scrollTop = log.scrollHeight;
2568
+ }
2569
+ }
2570
+ function closeEventSource() {
2571
+ if (_currentEventSource) {
2572
+ _currentEventSource.close();
2573
+ _currentEventSource = null;
2574
+ }
2575
+ }
2576
+ function closeClaudeStreamModal() {
2577
+ closeEventSource();
2578
+ const { overlay } = getModalElements();
2579
+ overlay?.classList.remove("show");
2580
+ }
2581
+ function openClaudeStreamModal(taskId) {
2582
+ closeEventSource();
2583
+ _currentTaskId = taskId;
2584
+ const { overlay, title, log, status, stopBtn } = getModalElements();
2585
+ if (!overlay || !title || !log || !status || !stopBtn) return;
2586
+ title.textContent = `Claude Output #${taskId}`;
2587
+ log.innerHTML = "";
2588
+ status.textContent = "Connecting...";
2589
+ stopBtn.disabled = false;
2590
+ stopBtn.textContent = "Stop";
2591
+ overlay.classList.add("show");
2592
+ const es = new EventSource(`/api/claude/tasks/${taskId}/stream`);
2593
+ _currentEventSource = es;
2594
+ es.onopen = () => {
2595
+ if (_currentEventSource === es) {
2596
+ status.textContent = "Running";
2597
+ }
2598
+ };
2599
+ es.addEventListener("text", (event) => {
2600
+ const msgEvent = event;
2601
+ try {
2602
+ const data = JSON.parse(msgEvent.data);
2603
+ appendToLog(log, stripAnsi(data.text));
2604
+ } catch {
2605
+ }
2606
+ });
2607
+ es.addEventListener("tool_use", (event) => {
2608
+ const msgEvent = event;
2609
+ try {
2610
+ const data = JSON.parse(msgEvent.data);
2611
+ const mainArg = data.input?.path ?? data.input?.command ?? "";
2612
+ const displayText = mainArg ? `\u{1F527} ${data.name}: ${mainArg}` : `\u{1F527} ${data.name}`;
2613
+ appendToLog(log, displayText, "claude-stream-tool-use");
2614
+ } catch {
2615
+ }
2616
+ });
2617
+ es.addEventListener("end", (event) => {
2618
+ const msgEvent = event;
2619
+ try {
2620
+ const data = JSON.parse(msgEvent.data);
2621
+ status.textContent = `Done (exit ${data.exitCode})`;
2622
+ } catch {
2623
+ status.textContent = "Done";
2624
+ }
2625
+ closeEventSource();
2626
+ stopBtn.disabled = true;
2627
+ });
2628
+ es.addEventListener("error", (event) => {
2629
+ const msgEvent = event;
2630
+ try {
2631
+ const data = JSON.parse(msgEvent.data);
2632
+ status.textContent = `Error: ${data.message}`;
2633
+ } catch {
2634
+ status.textContent = "Error";
2635
+ }
2636
+ closeEventSource();
2637
+ stopBtn.disabled = true;
2638
+ });
2639
+ es.onerror = () => {
2640
+ if (_currentEventSource === es) {
2641
+ closeEventSource();
2642
+ status.textContent = "Disconnected";
2643
+ stopBtn.disabled = true;
2644
+ }
2645
+ };
2646
+ }
2647
+ async function handleStop() {
2648
+ if (_currentTaskId === null) return;
2649
+ const { stopBtn, status } = getModalElements();
2650
+ if (!stopBtn || !status) return;
2651
+ const taskId = _currentTaskId;
2652
+ closeEventSource();
2653
+ stopBtn.disabled = true;
2654
+ stopBtn.textContent = "Stopping...";
2655
+ try {
2656
+ await fetch(`/api/claude/tasks/${taskId}/run`, { method: "DELETE" });
2657
+ } catch {
2658
+ }
2659
+ stopBtn.textContent = "Stopped";
2660
+ status.textContent = "Stopped";
2661
+ if (_claudeButtonUpdateCallback) {
2662
+ _claudeButtonUpdateCallback();
2663
+ }
2664
+ }
2665
+ function initClaudeStreamModal() {
2666
+ const { overlay, stopBtn, closeBtn, modalClose } = getModalElements();
2667
+ modalClose?.addEventListener("click", () => {
2668
+ closeClaudeStreamModal();
2669
+ });
2670
+ closeBtn?.addEventListener("click", () => {
2671
+ closeClaudeStreamModal();
2672
+ });
2673
+ stopBtn?.addEventListener("click", () => {
2674
+ void handleStop();
2675
+ });
2676
+ overlay?.addEventListener("click", (e) => {
2677
+ if (e.target === overlay) {
2678
+ closeClaudeStreamModal();
2679
+ }
2680
+ });
2681
+ document.addEventListener("keydown", (e) => {
2682
+ if (e.key === "Escape" && overlay?.classList.contains("show")) {
2683
+ closeClaudeStreamModal();
2684
+ }
2685
+ });
2686
+ }
2687
+
2072
2688
  // src/board/client/main.ts
2073
2689
  initDragDrop();
2074
2690
  initAutoScroll();
@@ -2079,4 +2695,10 @@
2079
2695
  initFilters();
2080
2696
  initBurgerMenu();
2081
2697
  initDependencyVisualization();
2698
+ initClaudeButton();
2699
+ initClaudeStreamModal();
2700
+ registerClaudeModalCallback(openClaudeStreamModal);
2701
+ registerClaudeButtonUpdateCallback(() => {
2702
+ updateButtonStates(/* @__PURE__ */ new Set());
2703
+ });
2082
2704
  })();