agkan 2.14.3 → 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.
- package/README.ja.md +62 -238
- package/README.md +48 -241
- package/dist/board/boardFavicon.d.ts +2 -0
- package/dist/board/boardFavicon.d.ts.map +1 -0
- package/dist/board/boardFavicon.js +5 -0
- package/dist/board/boardFavicon.js.map +1 -0
- package/dist/board/boardRenderer.d.ts +22 -6
- package/dist/board/boardRenderer.d.ts.map +1 -1
- package/dist/board/boardRenderer.js +67 -26
- package/dist/board/boardRenderer.js.map +1 -1
- package/dist/board/boardRoutes.d.ts +4 -2
- package/dist/board/boardRoutes.d.ts.map +1 -1
- package/dist/board/boardRoutes.js +140 -13
- package/dist/board/boardRoutes.js.map +1 -1
- package/dist/board/boardStyles.d.ts +1 -1
- package/dist/board/boardStyles.d.ts.map +1 -1
- package/dist/board/boardStyles.js +62 -8
- package/dist/board/boardStyles.js.map +1 -1
- package/dist/board/client/board.js +1007 -29
- package/dist/board/server.d.ts +3 -2
- package/dist/board/server.d.ts.map +1 -1
- package/dist/board/server.js +9 -7
- package/dist/board/server.js.map +1 -1
- package/dist/cli/commands/agent-guide.d.ts.map +1 -1
- package/dist/cli/commands/agent-guide.js +6 -0
- package/dist/cli/commands/agent-guide.js.map +1 -1
- package/dist/cli/commands/board.d.ts.map +1 -1
- package/dist/cli/commands/board.js +202 -15
- package/dist/cli/commands/board.js.map +1 -1
- package/dist/cli/commands/ps.d.ts +7 -0
- package/dist/cli/commands/ps.d.ts.map +1 -0
- package/dist/cli/commands/ps.js +83 -0
- package/dist/cli/commands/ps.js.map +1 -0
- package/dist/cli/commands/tag/add.d.ts.map +1 -1
- package/dist/cli/commands/tag/add.js +10 -11
- package/dist/cli/commands/tag/add.js.map +1 -1
- package/dist/cli/commands/tag/attach.d.ts.map +1 -1
- package/dist/cli/commands/tag/attach.js +10 -11
- package/dist/cli/commands/tag/attach.js.map +1 -1
- package/dist/cli/commands/tag/rename.d.ts.map +1 -1
- package/dist/cli/commands/tag/rename.js +10 -11
- package/dist/cli/commands/tag/rename.js.map +1 -1
- package/dist/cli/commands/task/add.js +1 -1
- package/dist/cli/commands/task/add.js.map +1 -1
- package/dist/cli/commands/task/copy.d.ts +6 -0
- package/dist/cli/commands/task/copy.d.ts.map +1 -0
- package/dist/cli/commands/task/copy.js +118 -0
- package/dist/cli/commands/task/copy.js.map +1 -0
- package/dist/cli/commands/task/list.d.ts.map +1 -1
- package/dist/cli/commands/task/list.js +37 -17
- package/dist/cli/commands/task/list.js.map +1 -1
- package/dist/cli/commands/task/update-parent.d.ts.map +1 -1
- package/dist/cli/commands/task/update-parent.js +10 -11
- package/dist/cli/commands/task/update-parent.js.map +1 -1
- package/dist/cli/index.js +6 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/utils/board-daemon.d.ts +7 -0
- package/dist/cli/utils/board-daemon.d.ts.map +1 -0
- package/dist/cli/utils/board-daemon.js +83 -0
- package/dist/cli/utils/board-daemon.js.map +1 -0
- package/dist/db/adapters/sqlite-storage-backend.d.ts +27 -0
- package/dist/db/adapters/sqlite-storage-backend.d.ts.map +1 -0
- package/dist/db/adapters/sqlite-storage-backend.js +498 -0
- package/dist/db/adapters/sqlite-storage-backend.js.map +1 -0
- package/dist/db/connection.d.ts +19 -0
- package/dist/db/connection.d.ts.map +1 -1
- package/dist/db/connection.js +37 -2
- package/dist/db/connection.js.map +1 -1
- package/dist/db/migrations/20260328000000_initial_schema.d.ts +3 -0
- package/dist/db/migrations/20260328000000_initial_schema.d.ts.map +1 -0
- package/dist/db/migrations/20260328000000_initial_schema.js +218 -0
- package/dist/db/migrations/20260328000000_initial_schema.js.map +1 -0
- package/dist/db/migrations/20260329000000_add_session_id_to_task_run_logs.d.ts +3 -0
- package/dist/db/migrations/20260329000000_add_session_id_to_task_run_logs.d.ts.map +1 -0
- package/dist/db/migrations/20260329000000_add_session_id_to_task_run_logs.js +7 -0
- package/dist/db/migrations/20260329000000_add_session_id_to_task_run_logs.js.map +1 -0
- package/dist/db/migrations/index.d.ts +4 -0
- package/dist/db/migrations/index.d.ts.map +1 -0
- package/dist/db/migrations/index.js +18 -0
- package/dist/db/migrations/index.js.map +1 -0
- package/dist/db/migrations/types.d.ts +17 -0
- package/dist/db/migrations/types.d.ts.map +1 -0
- package/dist/{board/client → db/migrations}/types.js +0 -1
- package/dist/db/migrations/types.js.map +1 -0
- package/dist/db/reset.d.ts.map +1 -1
- package/dist/db/reset.js +8 -3
- package/dist/db/reset.js.map +1 -1
- package/dist/db/schema.d.ts +4 -4
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +22 -207
- package/dist/db/schema.js.map +1 -1
- package/dist/db/types/repository.d.ts +226 -0
- package/dist/db/types/repository.d.ts.map +1 -0
- package/dist/db/types/repository.js +15 -0
- package/dist/db/types/repository.js.map +1 -0
- package/dist/errors.d.ts +27 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +51 -0
- package/dist/errors.js.map +1 -0
- package/dist/models/Task.d.ts +2 -0
- package/dist/models/Task.d.ts.map +1 -1
- package/dist/services/ClaudeProcessService.d.ts +100 -0
- package/dist/services/ClaudeProcessService.d.ts.map +1 -0
- package/dist/services/ClaudeProcessService.js +279 -0
- package/dist/services/ClaudeProcessService.js.map +1 -0
- package/dist/services/CommentService.d.ts +3 -3
- package/dist/services/CommentService.d.ts.map +1 -1
- package/dist/services/CommentService.js +13 -72
- package/dist/services/CommentService.js.map +1 -1
- package/dist/services/ExportImportService.d.ts +3 -3
- package/dist/services/ExportImportService.d.ts.map +1 -1
- package/dist/services/ExportImportService.js +29 -31
- package/dist/services/ExportImportService.js.map +1 -1
- package/dist/services/MetadataService.d.ts +3 -3
- package/dist/services/MetadataService.d.ts.map +1 -1
- package/dist/services/MetadataService.js +9 -69
- package/dist/services/MetadataService.js.map +1 -1
- package/dist/services/ProcessService.d.ts +54 -0
- package/dist/services/ProcessService.d.ts.map +1 -0
- package/dist/services/ProcessService.js +147 -0
- package/dist/services/ProcessService.js.map +1 -0
- package/dist/services/TagService.d.ts +3 -3
- package/dist/services/TagService.d.ts.map +1 -1
- package/dist/services/TagService.js +16 -41
- package/dist/services/TagService.js.map +1 -1
- package/dist/services/TaskBlockService.d.ts +3 -3
- package/dist/services/TaskBlockService.d.ts.map +1 -1
- package/dist/services/TaskBlockService.js +14 -40
- package/dist/services/TaskBlockService.js.map +1 -1
- package/dist/services/TaskService.d.ts +5 -23
- package/dist/services/TaskService.d.ts.map +1 -1
- package/dist/services/TaskService.js +57 -191
- package/dist/services/TaskService.js.map +1 -1
- package/dist/services/TaskTagService.d.ts +3 -3
- package/dist/services/TaskTagService.d.ts.map +1 -1
- package/dist/services/TaskTagService.js +23 -86
- package/dist/services/TaskTagService.js.map +1 -1
- package/dist/services/TmuxService.d.ts +2 -0
- package/dist/services/TmuxService.d.ts.map +1 -0
- package/dist/services/TmuxService.js +7 -0
- package/dist/services/TmuxService.js.map +1 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +3 -1
- package/dist/services/index.js.map +1 -1
- package/dist/utils/logger.d.ts +7 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +18 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +12 -5
- package/dist/board/client/addTaskModal.d.ts +0 -2
- package/dist/board/client/addTaskModal.d.ts.map +0 -1
- package/dist/board/client/addTaskModal.js +0 -64
- package/dist/board/client/addTaskModal.js.map +0 -1
- package/dist/board/client/autoScroll.d.ts +0 -4
- package/dist/board/client/autoScroll.d.ts.map +0 -1
- package/dist/board/client/autoScroll.js +0 -59
- package/dist/board/client/autoScroll.js.map +0 -1
- package/dist/board/client/boardPolling.d.ts +0 -15
- package/dist/board/client/boardPolling.d.ts.map +0 -1
- package/dist/board/client/boardPolling.js +0 -144
- package/dist/board/client/boardPolling.js.map +0 -1
- package/dist/board/client/burgerMenu.d.ts +0 -2
- package/dist/board/client/burgerMenu.d.ts.map +0 -1
- package/dist/board/client/burgerMenu.js +0 -80
- package/dist/board/client/burgerMenu.js.map +0 -1
- package/dist/board/client/contextMenu.d.ts +0 -2
- package/dist/board/client/contextMenu.d.ts.map +0 -1
- package/dist/board/client/contextMenu.js +0 -52
- package/dist/board/client/contextMenu.js.map +0 -1
- package/dist/board/client/detailPanel.d.ts +0 -8
- package/dist/board/client/detailPanel.d.ts.map +0 -1
- package/dist/board/client/detailPanel.js +0 -565
- package/dist/board/client/detailPanel.js.map +0 -1
- package/dist/board/client/dragDrop.d.ts +0 -6
- package/dist/board/client/dragDrop.d.ts.map +0 -1
- package/dist/board/client/dragDrop.js +0 -82
- package/dist/board/client/dragDrop.js.map +0 -1
- package/dist/board/client/filters.d.ts +0 -6
- package/dist/board/client/filters.d.ts.map +0 -1
- package/dist/board/client/filters.js +0 -167
- package/dist/board/client/filters.js.map +0 -1
- package/dist/board/client/main.d.ts +0 -2
- package/dist/board/client/main.d.ts.map +0 -1
- package/dist/board/client/main.js +0 -20
- package/dist/board/client/main.js.map +0 -1
- package/dist/board/client/tags.d.ts +0 -6
- package/dist/board/client/tags.d.ts.map +0 -1
- package/dist/board/client/tags.js +0 -198
- package/dist/board/client/tags.js.map +0 -1
- package/dist/board/client/types.d.ts +0 -48
- package/dist/board/client/types.d.ts.map +0 -1
- package/dist/board/client/types.js.map +0 -1
- package/dist/board/client/utils.d.ts +0 -4
- package/dist/board/client/utils.d.ts.map +0 -1
- package/dist/board/client/utils.js +0 -44
- package/dist/board/client/utils.js.map +0 -1
|
@@ -30,9 +30,277 @@
|
|
|
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
|
|
286
|
+
var _redrawDependencies = null;
|
|
287
|
+
function registerDependencyRedrawCallback(callback) {
|
|
288
|
+
_redrawDependencies = callback;
|
|
289
|
+
}
|
|
34
290
|
var draggedCard = null;
|
|
35
291
|
var sourceBody = null;
|
|
292
|
+
var isPendingStatusUpdate = false;
|
|
293
|
+
var _dragMouseX = 0;
|
|
294
|
+
var _dragMouseY = 0;
|
|
295
|
+
var _dragOffsetX = 0;
|
|
296
|
+
var _dragOffsetY = 0;
|
|
297
|
+
function getDraggedCardVirtualRect() {
|
|
298
|
+
if (!draggedCard) return null;
|
|
299
|
+
const rect = draggedCard.getBoundingClientRect();
|
|
300
|
+
const left = _dragMouseX - _dragOffsetX;
|
|
301
|
+
const top = _dragMouseY - _dragOffsetY;
|
|
302
|
+
return new DOMRect(left, top, rect.width, rect.height);
|
|
303
|
+
}
|
|
36
304
|
function updateCount(status) {
|
|
37
305
|
const col = document.querySelector(`.column[data-status="${status}"]`);
|
|
38
306
|
if (!col) return;
|
|
@@ -55,6 +323,8 @@
|
|
|
55
323
|
draggedCard.dataset.status = newStatus;
|
|
56
324
|
updateCount(oldStatus);
|
|
57
325
|
updateCount(newStatus);
|
|
326
|
+
updateCardButton(draggedCard, newStatus);
|
|
327
|
+
isPendingStatusUpdate = true;
|
|
58
328
|
try {
|
|
59
329
|
const res = await fetch("/api/tasks/" + taskId, {
|
|
60
330
|
method: "PATCH",
|
|
@@ -62,6 +332,9 @@
|
|
|
62
332
|
body: JSON.stringify({ status: newStatus })
|
|
63
333
|
});
|
|
64
334
|
if (!res.ok) throw new Error("Server error");
|
|
335
|
+
if (_redrawDependencies) {
|
|
336
|
+
_redrawDependencies();
|
|
337
|
+
}
|
|
65
338
|
} catch {
|
|
66
339
|
if (prevBody && draggedCard) {
|
|
67
340
|
prevBody.appendChild(draggedCard);
|
|
@@ -70,19 +343,37 @@
|
|
|
70
343
|
updateCount(newStatus);
|
|
71
344
|
}
|
|
72
345
|
showToast();
|
|
346
|
+
} finally {
|
|
347
|
+
isPendingStatusUpdate = false;
|
|
73
348
|
}
|
|
74
349
|
}
|
|
350
|
+
var _documentDragOverListener = null;
|
|
75
351
|
function attachDragListeners(card) {
|
|
76
352
|
card.addEventListener("dragstart", (e) => {
|
|
77
353
|
draggedCard = card;
|
|
78
354
|
sourceBody = card.parentElement;
|
|
79
355
|
card.classList.add("dragging");
|
|
80
356
|
if (e.dataTransfer) e.dataTransfer.effectAllowed = "move";
|
|
357
|
+
const rect = card.getBoundingClientRect();
|
|
358
|
+
_dragOffsetX = e.clientX - rect.left;
|
|
359
|
+
_dragOffsetY = e.clientY - rect.top;
|
|
360
|
+
_dragMouseX = e.clientX;
|
|
361
|
+
_dragMouseY = e.clientY;
|
|
362
|
+
_documentDragOverListener = (ev) => {
|
|
363
|
+
_dragMouseX = ev.clientX;
|
|
364
|
+
_dragMouseY = ev.clientY;
|
|
365
|
+
if (_redrawDependencies) _redrawDependencies();
|
|
366
|
+
};
|
|
367
|
+
document.addEventListener("dragover", _documentDragOverListener);
|
|
81
368
|
});
|
|
82
369
|
card.addEventListener("dragend", () => {
|
|
83
370
|
card.classList.remove("dragging");
|
|
84
371
|
draggedCard = null;
|
|
85
372
|
sourceBody = null;
|
|
373
|
+
if (_documentDragOverListener) {
|
|
374
|
+
document.removeEventListener("dragover", _documentDragOverListener);
|
|
375
|
+
_documentDragOverListener = null;
|
|
376
|
+
}
|
|
86
377
|
});
|
|
87
378
|
}
|
|
88
379
|
function initDragDrop() {
|
|
@@ -219,14 +510,43 @@
|
|
|
219
510
|
});
|
|
220
511
|
input.placeholder = currentTags.length === 0 ? "Add tags..." : "";
|
|
221
512
|
}
|
|
513
|
+
async function createAndAddTag(name) {
|
|
514
|
+
const detailTaskId2 = _getDetailTaskId ? _getDetailTaskId() : null;
|
|
515
|
+
try {
|
|
516
|
+
const createRes = await fetch("/api/tags", {
|
|
517
|
+
method: "POST",
|
|
518
|
+
headers: { "Content-Type": "application/json" },
|
|
519
|
+
body: JSON.stringify({ name })
|
|
520
|
+
});
|
|
521
|
+
if (!createRes.ok) throw new Error("Server error");
|
|
522
|
+
const newTag = await createRes.json();
|
|
523
|
+
allAvailableTags.push(newTag);
|
|
524
|
+
const taskRes = await fetch("/api/tasks/" + detailTaskId2 + "/tags", {
|
|
525
|
+
method: "POST",
|
|
526
|
+
headers: { "Content-Type": "application/json" },
|
|
527
|
+
body: JSON.stringify({ tagId: newTag.id })
|
|
528
|
+
});
|
|
529
|
+
if (!taskRes.ok) throw new Error("Server error");
|
|
530
|
+
currentTags.push(newTag);
|
|
531
|
+
input.value = "";
|
|
532
|
+
inputValue = "";
|
|
533
|
+
renderPills();
|
|
534
|
+
renderDropdown();
|
|
535
|
+
} catch {
|
|
536
|
+
showToast("Failed to create tag");
|
|
537
|
+
}
|
|
538
|
+
}
|
|
222
539
|
function renderDropdown() {
|
|
223
540
|
const filtered = getFilteredTags();
|
|
224
541
|
dropdown.innerHTML = "";
|
|
225
542
|
focusedOptionIndex = -1;
|
|
226
|
-
|
|
543
|
+
const hasInput = inputValue.trim() !== "";
|
|
544
|
+
const exactMatch = hasInput && allAvailableTags.some((t) => t.name.toLowerCase() === inputValue.trim().toLowerCase());
|
|
545
|
+
const showCreate = hasInput && !exactMatch;
|
|
546
|
+
if (filtered.length === 0 && !showCreate) {
|
|
227
547
|
const noOpt = document.createElement("div");
|
|
228
548
|
noOpt.className = "tag-select-no-options";
|
|
229
|
-
noOpt.textContent =
|
|
549
|
+
noOpt.textContent = hasInput ? "No matching tags" : "No tags available";
|
|
230
550
|
dropdown.appendChild(noOpt);
|
|
231
551
|
} else {
|
|
232
552
|
filtered.forEach((t, i) => {
|
|
@@ -241,6 +561,18 @@
|
|
|
241
561
|
});
|
|
242
562
|
dropdown.appendChild(opt);
|
|
243
563
|
});
|
|
564
|
+
if (showCreate) {
|
|
565
|
+
const createOpt = document.createElement("div");
|
|
566
|
+
createOpt.className = "tag-select-option tag-select-create-option";
|
|
567
|
+
createOpt.dataset.create = "true";
|
|
568
|
+
createOpt.textContent = `Create "${inputValue.trim()}"`;
|
|
569
|
+
createOpt.addEventListener("mouseover", () => setFocusedOption(filtered.length));
|
|
570
|
+
createOpt.addEventListener("mousedown", async (e) => {
|
|
571
|
+
e.preventDefault();
|
|
572
|
+
await createAndAddTag(inputValue.trim());
|
|
573
|
+
});
|
|
574
|
+
dropdown.appendChild(createOpt);
|
|
575
|
+
}
|
|
244
576
|
}
|
|
245
577
|
}
|
|
246
578
|
function setFocusedOption(index) {
|
|
@@ -296,6 +628,8 @@
|
|
|
296
628
|
e.preventDefault();
|
|
297
629
|
if (focusedOptionIndex >= 0 && filtered[focusedOptionIndex]) {
|
|
298
630
|
await addTag(String(filtered[focusedOptionIndex].id));
|
|
631
|
+
} else if (focusedOptionIndex >= 0 && inputValue.trim()) {
|
|
632
|
+
await createAndAddTag(inputValue.trim());
|
|
299
633
|
}
|
|
300
634
|
} else if (e.key === "Escape") {
|
|
301
635
|
closeDropdown();
|
|
@@ -346,14 +680,20 @@
|
|
|
346
680
|
var _renderDetailPanel = null;
|
|
347
681
|
var _showUpdateWarning = null;
|
|
348
682
|
var _getDetailTaskId2 = null;
|
|
683
|
+
var _getDetailActiveTab = null;
|
|
349
684
|
var _setActiveCard = null;
|
|
685
|
+
var _redrawDependencies2 = null;
|
|
350
686
|
function registerDetailPanelCallbacks(callbacks) {
|
|
351
687
|
_openTaskDetail = callbacks.openTaskDetail;
|
|
352
688
|
_renderDetailPanel = callbacks.renderDetailPanel;
|
|
353
689
|
_showUpdateWarning = callbacks.showUpdateWarning;
|
|
354
690
|
_getDetailTaskId2 = callbacks.getDetailTaskId;
|
|
691
|
+
_getDetailActiveTab = callbacks.getDetailActiveTab;
|
|
355
692
|
_setActiveCard = callbacks.setActiveCard;
|
|
356
693
|
}
|
|
694
|
+
function registerDependencyRedrawCallback2(callback) {
|
|
695
|
+
_redrawDependencies2 = callback;
|
|
696
|
+
}
|
|
357
697
|
function attachCardListeners(body) {
|
|
358
698
|
body.querySelectorAll(".card").forEach((card) => {
|
|
359
699
|
attachDragListeners(card);
|
|
@@ -416,6 +756,8 @@
|
|
|
416
756
|
}
|
|
417
757
|
attachCardListeners(body);
|
|
418
758
|
attachAutoScrollToBody(body);
|
|
759
|
+
attachClaudeButtonListeners(body);
|
|
760
|
+
updateButtonStates(getRunningTaskIds());
|
|
419
761
|
}
|
|
420
762
|
function isEditingDetailPanel() {
|
|
421
763
|
const editableFields = ["detail-edit-title", "detail-edit-body", "detail-edit-status", "detail-edit-priority"];
|
|
@@ -426,6 +768,9 @@
|
|
|
426
768
|
if (_showUpdateWarning) _showUpdateWarning();
|
|
427
769
|
return;
|
|
428
770
|
}
|
|
771
|
+
if (_getDetailActiveTab && _getDetailActiveTab() === "run-logs") {
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
429
774
|
try {
|
|
430
775
|
const taskRes = await fetch("/api/tasks/" + detailTaskId2);
|
|
431
776
|
if (taskRes.ok) {
|
|
@@ -447,6 +792,9 @@
|
|
|
447
792
|
if (detailTaskId2 !== null && _setActiveCard) {
|
|
448
793
|
_setActiveCard(detailTaskId2);
|
|
449
794
|
}
|
|
795
|
+
if (_redrawDependencies2) {
|
|
796
|
+
_redrawDependencies2();
|
|
797
|
+
}
|
|
450
798
|
if (detailTaskId2 !== null) {
|
|
451
799
|
await refreshOpenDetailPanel(detailTaskId2);
|
|
452
800
|
}
|
|
@@ -454,7 +802,7 @@
|
|
|
454
802
|
}
|
|
455
803
|
}
|
|
456
804
|
async function pollBoardUpdates() {
|
|
457
|
-
if (draggedCard !== null) return;
|
|
805
|
+
if (draggedCard !== null || isPendingStatusUpdate) return;
|
|
458
806
|
try {
|
|
459
807
|
const res = await fetch("/api/board/updated-at");
|
|
460
808
|
if (!res.ok) return;
|
|
@@ -512,10 +860,13 @@
|
|
|
512
860
|
const filtered = getFilteredAddTags();
|
|
513
861
|
dropdown.innerHTML = "";
|
|
514
862
|
tagFocusedIndex = -1;
|
|
515
|
-
|
|
863
|
+
const hasInput = tagInputValue.trim() !== "";
|
|
864
|
+
const exactMatch = hasInput && allAvailableTags.some((t) => t.name.toLowerCase() === tagInputValue.trim().toLowerCase());
|
|
865
|
+
const showCreate = hasInput && !exactMatch;
|
|
866
|
+
if (filtered.length === 0 && !showCreate) {
|
|
516
867
|
const noOpt = document.createElement("div");
|
|
517
868
|
noOpt.className = "tag-select-no-options";
|
|
518
|
-
noOpt.textContent =
|
|
869
|
+
noOpt.textContent = hasInput ? "No matching tags" : "No tags available";
|
|
519
870
|
dropdown.appendChild(noOpt);
|
|
520
871
|
} else {
|
|
521
872
|
filtered.forEach((t, i) => {
|
|
@@ -530,6 +881,19 @@
|
|
|
530
881
|
});
|
|
531
882
|
dropdown.appendChild(opt);
|
|
532
883
|
});
|
|
884
|
+
if (showCreate) {
|
|
885
|
+
const createOpt = document.createElement("div");
|
|
886
|
+
createOpt.className = "tag-select-option tag-select-create-option";
|
|
887
|
+
createOpt.dataset.create = "true";
|
|
888
|
+
createOpt.textContent = `Create "${tagInputValue.trim()}"`;
|
|
889
|
+
createOpt.addEventListener("mouseover", () => setAddTagFocused(dropdown, filtered.length));
|
|
890
|
+
createOpt.addEventListener("mousedown", async (e) => {
|
|
891
|
+
e.preventDefault();
|
|
892
|
+
const input = document.getElementById("add-tag-input");
|
|
893
|
+
await createAndSelectAddTag(tagInputValue.trim(), dropdown, input);
|
|
894
|
+
});
|
|
895
|
+
dropdown.appendChild(createOpt);
|
|
896
|
+
}
|
|
533
897
|
}
|
|
534
898
|
}
|
|
535
899
|
function setAddTagFocused(dropdown, index) {
|
|
@@ -547,6 +911,21 @@
|
|
|
547
911
|
renderAddTagPills(control, input);
|
|
548
912
|
renderAddTagDropdown(dropdown);
|
|
549
913
|
}
|
|
914
|
+
async function createAndSelectAddTag(name, dropdown, input) {
|
|
915
|
+
try {
|
|
916
|
+
const res = await fetch("/api/tags", {
|
|
917
|
+
method: "POST",
|
|
918
|
+
headers: { "Content-Type": "application/json" },
|
|
919
|
+
body: JSON.stringify({ name })
|
|
920
|
+
});
|
|
921
|
+
if (!res.ok) throw new Error("Server error");
|
|
922
|
+
const newTag = await res.json();
|
|
923
|
+
allAvailableTags.push(newTag);
|
|
924
|
+
selectAddTag(newTag.id, dropdown, input);
|
|
925
|
+
} catch {
|
|
926
|
+
showToast("Failed to create tag");
|
|
927
|
+
}
|
|
928
|
+
}
|
|
550
929
|
function initAddTagSelector() {
|
|
551
930
|
const control = document.getElementById("add-tag-select-control");
|
|
552
931
|
const dropdown = document.getElementById("add-tag-select-dropdown");
|
|
@@ -574,7 +953,7 @@
|
|
|
574
953
|
renderAddTagDropdown(dropdown);
|
|
575
954
|
if (!dropdown.classList.contains("open")) dropdown.classList.add("open");
|
|
576
955
|
});
|
|
577
|
-
input.addEventListener("keydown", (e) => {
|
|
956
|
+
input.addEventListener("keydown", async (e) => {
|
|
578
957
|
const filtered = getFilteredAddTags();
|
|
579
958
|
const opts = dropdown.querySelectorAll(".tag-select-option");
|
|
580
959
|
if (e.key === "ArrowDown") {
|
|
@@ -587,6 +966,8 @@
|
|
|
587
966
|
e.preventDefault();
|
|
588
967
|
if (tagFocusedIndex >= 0 && filtered[tagFocusedIndex]) {
|
|
589
968
|
selectAddTag(filtered[tagFocusedIndex].id, dropdown, input);
|
|
969
|
+
} else if (tagFocusedIndex >= 0 && tagInputValue.trim()) {
|
|
970
|
+
await createAndSelectAddTag(tagInputValue.trim(), dropdown, input);
|
|
590
971
|
}
|
|
591
972
|
} else if (e.key === "Escape") {
|
|
592
973
|
dropdown.classList.remove("open");
|
|
@@ -636,7 +1017,7 @@
|
|
|
636
1017
|
function resetAddModal(elements) {
|
|
637
1018
|
elements.addTitle.value = "";
|
|
638
1019
|
elements.addBody.value = "";
|
|
639
|
-
elements.addPriority.value = "";
|
|
1020
|
+
elements.addPriority.value = "medium";
|
|
640
1021
|
selectedTags = [];
|
|
641
1022
|
tagInputValue = "";
|
|
642
1023
|
tagFocusedIndex = -1;
|
|
@@ -794,8 +1175,8 @@
|
|
|
794
1175
|
});
|
|
795
1176
|
if (!res.ok) throw new Error("Server error");
|
|
796
1177
|
}
|
|
797
|
-
async function fetchTaskDetail(taskId) {
|
|
798
|
-
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);
|
|
799
1180
|
if (!res.ok) throw new Error("Server error");
|
|
800
1181
|
return res.json();
|
|
801
1182
|
}
|
|
@@ -841,6 +1222,12 @@
|
|
|
841
1222
|
}).catch(function() {
|
|
842
1223
|
});
|
|
843
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
|
+
}
|
|
844
1231
|
|
|
845
1232
|
// src/board/client/detailPanelHtml.ts
|
|
846
1233
|
function renderCommentItemHtml(comment, taskId) {
|
|
@@ -903,14 +1290,14 @@
|
|
|
903
1290
|
if (parent) {
|
|
904
1291
|
html += '<div class="detail-relation-row">';
|
|
905
1292
|
html += '<span class="detail-relation-label">Parent</span>';
|
|
906
|
-
html += '<div class="detail-relation-ids"><span class="detail-relation-id">#' + parent.id + " " + escapeHtmlClient(parent.title) + "</span></div>";
|
|
1293
|
+
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>";
|
|
907
1294
|
html += "</div>";
|
|
908
1295
|
}
|
|
909
1296
|
if (blockedBy.length > 0) {
|
|
910
1297
|
html += '<div class="detail-relation-row"><span class="detail-relation-label">Blocked by</span>';
|
|
911
1298
|
html += '<div class="detail-relation-ids">';
|
|
912
1299
|
blockedBy.forEach((t) => {
|
|
913
|
-
html += '<span class="detail-relation-id">#' + t.id + "</span>";
|
|
1300
|
+
html += '<span class="detail-relation-id detail-relation-link" data-task-id="' + t.id + '">#' + t.id + "</span>";
|
|
914
1301
|
});
|
|
915
1302
|
html += "</div></div>";
|
|
916
1303
|
}
|
|
@@ -918,7 +1305,7 @@
|
|
|
918
1305
|
html += '<div class="detail-relation-row"><span class="detail-relation-label">Blocking</span>';
|
|
919
1306
|
html += '<div class="detail-relation-ids">';
|
|
920
1307
|
blocking.forEach((t) => {
|
|
921
|
-
html += '<span class="detail-relation-id">#' + t.id + "</span>";
|
|
1308
|
+
html += '<span class="detail-relation-id detail-relation-link" data-task-id="' + t.id + '">#' + t.id + "</span>";
|
|
922
1309
|
});
|
|
923
1310
|
html += "</div></div>";
|
|
924
1311
|
}
|
|
@@ -926,11 +1313,10 @@
|
|
|
926
1313
|
return html;
|
|
927
1314
|
}
|
|
928
1315
|
function renderMetadataTable(metadata) {
|
|
929
|
-
|
|
930
|
-
if (otherMeta.length === 0) return "";
|
|
1316
|
+
if (metadata.length === 0) return "";
|
|
931
1317
|
let html = '<div class="detail-field"><div class="detail-field-label">Metadata</div>';
|
|
932
1318
|
html += '<table class="detail-meta-table">';
|
|
933
|
-
|
|
1319
|
+
metadata.forEach((m) => {
|
|
934
1320
|
html += "<tr><td>" + escapeHtmlClient(m.key) + "</td><td>" + escapeHtmlClient(m.value) + "</td></tr>";
|
|
935
1321
|
});
|
|
936
1322
|
html += "</table></div>";
|
|
@@ -968,8 +1354,36 @@
|
|
|
968
1354
|
html += renderEditableTextFields(task);
|
|
969
1355
|
return html;
|
|
970
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">▶</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
|
+
}
|
|
971
1385
|
function buildDetailPanelHtml() {
|
|
972
|
-
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">×</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">⎘</button><button class="detail-panel-close" id="detail-panel-close" title="Close">×</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>';
|
|
973
1387
|
}
|
|
974
1388
|
function autoResizeTextarea(el) {
|
|
975
1389
|
const scrollContainer = el.closest(".detail-tab-content");
|
|
@@ -982,9 +1396,22 @@
|
|
|
982
1396
|
// src/board/client/detailPanel.ts
|
|
983
1397
|
var detailTaskId = null;
|
|
984
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
|
+
}
|
|
985
1409
|
function getDetailTaskId() {
|
|
986
1410
|
return detailTaskId;
|
|
987
1411
|
}
|
|
1412
|
+
function getDetailActiveTab() {
|
|
1413
|
+
return lastTab;
|
|
1414
|
+
}
|
|
988
1415
|
function setActiveCard(taskId) {
|
|
989
1416
|
document.querySelectorAll(".card.active").forEach((card) => {
|
|
990
1417
|
card.classList.remove("active");
|
|
@@ -995,6 +1422,13 @@
|
|
|
995
1422
|
}
|
|
996
1423
|
}
|
|
997
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
|
+
}
|
|
998
1432
|
const detailPanel = document.getElementById("detail-panel");
|
|
999
1433
|
detailPanel.classList.remove("open");
|
|
1000
1434
|
detailPanel.style.width = "";
|
|
@@ -1002,26 +1436,41 @@
|
|
|
1002
1436
|
detailTaskId = null;
|
|
1003
1437
|
}
|
|
1004
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
|
+
}
|
|
1005
1445
|
lastTab = tabName;
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
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
|
+
}
|
|
1012
1454
|
const footer = document.getElementById("detail-panel-footer");
|
|
1013
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
|
+
}
|
|
1014
1461
|
}
|
|
1015
1462
|
async function loadComments(taskId) {
|
|
1016
|
-
const tabBtn = document.getElementById("detail-tab-comments");
|
|
1017
1463
|
const pane = document.getElementById("detail-tab-content-comments");
|
|
1018
1464
|
if (!pane) return;
|
|
1019
1465
|
try {
|
|
1020
1466
|
const comments = await fetchComments(taskId);
|
|
1467
|
+
if (detailTaskId !== taskId) return;
|
|
1468
|
+
const tabBtn = document.getElementById("detail-tab-comments");
|
|
1021
1469
|
if (tabBtn) tabBtn.textContent = "Comments (" + comments.length + ")";
|
|
1022
1470
|
renderComments(taskId, comments);
|
|
1023
1471
|
} catch (err) {
|
|
1024
1472
|
console.error("[agkan] loadComments failed for task", taskId, err);
|
|
1473
|
+
if (detailTaskId !== taskId) return;
|
|
1025
1474
|
if (pane) pane.innerHTML = '<div style="padding:20px;font-size:12px;color:#94a3b8;">Failed to load comments</div>';
|
|
1026
1475
|
}
|
|
1027
1476
|
}
|
|
@@ -1146,6 +1595,99 @@
|
|
|
1146
1595
|
const taskId = target.dataset.taskId ? Number(target.dataset.taskId) : NaN;
|
|
1147
1596
|
dispatchCommentAction(action, commentId, taskId);
|
|
1148
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
|
+
}
|
|
1149
1691
|
function renderDetailPanel(data) {
|
|
1150
1692
|
document.getElementById("detail-panel-update-warning")?.remove();
|
|
1151
1693
|
const detailPanelTitle = document.getElementById("detail-panel-title");
|
|
@@ -1157,6 +1699,13 @@
|
|
|
1157
1699
|
if (detailsPane) {
|
|
1158
1700
|
detailsPane.innerHTML = renderDetailPanelHtml(data);
|
|
1159
1701
|
detailsPane.style.padding = "20px";
|
|
1702
|
+
detailsPane.querySelectorAll(".detail-relation-link[data-task-id]").forEach((el) => {
|
|
1703
|
+
el.style.cursor = "pointer";
|
|
1704
|
+
el.addEventListener("click", () => {
|
|
1705
|
+
const tid = el.dataset.taskId;
|
|
1706
|
+
if (tid) void openTaskDetail(tid);
|
|
1707
|
+
});
|
|
1708
|
+
});
|
|
1160
1709
|
}
|
|
1161
1710
|
const footer = document.getElementById("detail-panel-footer");
|
|
1162
1711
|
if (footer) {
|
|
@@ -1165,9 +1714,36 @@
|
|
|
1165
1714
|
}
|
|
1166
1715
|
const textarea = document.getElementById("detail-edit-body");
|
|
1167
1716
|
if (textarea) {
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
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
|
+
}
|
|
1171
1747
|
textarea.addEventListener("input", () => {
|
|
1172
1748
|
autoResizeTextarea(textarea);
|
|
1173
1749
|
});
|
|
@@ -1182,16 +1758,23 @@
|
|
|
1182
1758
|
async function openTaskDetail(taskId) {
|
|
1183
1759
|
const detailPanel = document.getElementById("detail-panel");
|
|
1184
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;
|
|
1185
1767
|
try {
|
|
1186
|
-
const data = await fetchTaskDetail(taskId);
|
|
1768
|
+
const data = await fetchTaskDetail(taskId, signal);
|
|
1769
|
+
currentFetchController = null;
|
|
1187
1770
|
renderDetailPanel(data);
|
|
1188
|
-
setActiveCard(Number(taskId));
|
|
1189
1771
|
if (!detailPanel.classList.contains("open")) {
|
|
1190
1772
|
const preferredWidth = detailPanel.dataset.preferredWidth || String(PANEL_DEFAULT_WIDTH2);
|
|
1191
1773
|
detailPanel.style.width = preferredWidth + "px";
|
|
1192
1774
|
detailPanel.classList.add("open");
|
|
1193
1775
|
}
|
|
1194
1776
|
} catch (err) {
|
|
1777
|
+
if (err instanceof DOMException && err.name === "AbortError") return;
|
|
1195
1778
|
console.error("[agkan] openTaskDetail failed for task", taskId, err);
|
|
1196
1779
|
showToast("Failed to load task details");
|
|
1197
1780
|
}
|
|
@@ -1216,7 +1799,7 @@
|
|
|
1216
1799
|
const reloadBtn = document.createElement("button");
|
|
1217
1800
|
reloadBtn.title = "Reload latest data";
|
|
1218
1801
|
reloadBtn.textContent = "\u21BA";
|
|
1219
|
-
reloadBtn.style.cssText = "background: none; border: none; cursor: pointer; font-size: 1.
|
|
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;";
|
|
1220
1803
|
reloadBtn.addEventListener("click", async () => {
|
|
1221
1804
|
try {
|
|
1222
1805
|
if (detailTaskId !== null) {
|
|
@@ -1303,6 +1886,12 @@
|
|
|
1303
1886
|
boardContainer.insertAdjacentHTML("beforeend", buildDetailPanelHtml());
|
|
1304
1887
|
const detailPanel = document.getElementById("detail-panel");
|
|
1305
1888
|
document.getElementById("detail-panel-close")?.addEventListener("click", closeDetailPanel);
|
|
1889
|
+
document.getElementById("detail-panel-copy-id")?.addEventListener("click", () => {
|
|
1890
|
+
if (detailTaskId === null) return;
|
|
1891
|
+
navigator.clipboard.writeText(String(detailTaskId)).then(() => {
|
|
1892
|
+
showToast("Copied task ID: " + detailTaskId);
|
|
1893
|
+
});
|
|
1894
|
+
});
|
|
1306
1895
|
document.addEventListener("keydown", (e) => {
|
|
1307
1896
|
if (e.key === "Escape" && detailPanel.classList.contains("open")) {
|
|
1308
1897
|
closeDetailPanel();
|
|
@@ -1325,6 +1914,7 @@
|
|
|
1325
1914
|
renderDetailPanel,
|
|
1326
1915
|
showUpdateWarning,
|
|
1327
1916
|
getDetailTaskId,
|
|
1917
|
+
getDetailActiveTab,
|
|
1328
1918
|
setActiveCard
|
|
1329
1919
|
});
|
|
1330
1920
|
registerGetDetailTaskId(getDetailTaskId);
|
|
@@ -1714,6 +2304,387 @@
|
|
|
1714
2304
|
initDarkMode();
|
|
1715
2305
|
}
|
|
1716
2306
|
|
|
2307
|
+
// src/board/client/dependencyVisualization.ts
|
|
2308
|
+
var isDependencyVisible = false;
|
|
2309
|
+
var arrowMarkers = /* @__PURE__ */ new Map();
|
|
2310
|
+
var scrollListener = null;
|
|
2311
|
+
var columnScrollListener = null;
|
|
2312
|
+
var resizeListener = null;
|
|
2313
|
+
function getOrCreateArrowMarker(svg, color) {
|
|
2314
|
+
const markerId = `arrow-${color.substring(1)}`;
|
|
2315
|
+
if (!arrowMarkers.has(markerId)) {
|
|
2316
|
+
const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
|
|
2317
|
+
const marker = document.createElementNS("http://www.w3.org/2000/svg", "marker");
|
|
2318
|
+
marker.setAttribute("id", markerId);
|
|
2319
|
+
marker.setAttribute("markerWidth", "10");
|
|
2320
|
+
marker.setAttribute("markerHeight", "10");
|
|
2321
|
+
marker.setAttribute("refX", "8");
|
|
2322
|
+
marker.setAttribute("refY", "3");
|
|
2323
|
+
marker.setAttribute("orient", "auto");
|
|
2324
|
+
marker.setAttribute("markerUnits", "strokeWidth");
|
|
2325
|
+
const polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
|
|
2326
|
+
polygon.setAttribute("points", "0 0, 10 3, 0 6");
|
|
2327
|
+
polygon.setAttribute("fill", color);
|
|
2328
|
+
marker.appendChild(polygon);
|
|
2329
|
+
defs.appendChild(marker);
|
|
2330
|
+
svg.appendChild(defs);
|
|
2331
|
+
arrowMarkers.set(markerId, { svg });
|
|
2332
|
+
}
|
|
2333
|
+
return markerId;
|
|
2334
|
+
}
|
|
2335
|
+
function drawBezierLine(svg, x1, y1, x2, y2, color, isHovered) {
|
|
2336
|
+
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
2337
|
+
const dx = Math.abs(x2 - x1);
|
|
2338
|
+
const isSameSide = dx < 10;
|
|
2339
|
+
let cp1x;
|
|
2340
|
+
let cp2x;
|
|
2341
|
+
if (isSameSide) {
|
|
2342
|
+
const cpOffset = 80;
|
|
2343
|
+
cp1x = x1 + cpOffset;
|
|
2344
|
+
cp2x = x2 + cpOffset;
|
|
2345
|
+
} else {
|
|
2346
|
+
const cpOffset = Math.max(dx * 0.5, 60);
|
|
2347
|
+
cp1x = x1 < x2 ? x1 + cpOffset : x1 - cpOffset;
|
|
2348
|
+
cp2x = x1 < x2 ? x2 - cpOffset : x2 + cpOffset;
|
|
2349
|
+
}
|
|
2350
|
+
const pathData = `M ${x1} ${y1} C ${cp1x} ${y1}, ${cp2x} ${y2}, ${x2} ${y2}`;
|
|
2351
|
+
path.setAttribute("d", pathData);
|
|
2352
|
+
path.setAttribute("stroke", color);
|
|
2353
|
+
path.setAttribute("stroke-width", isHovered ? "2.5" : "1.5");
|
|
2354
|
+
path.setAttribute("fill", "none");
|
|
2355
|
+
path.setAttribute("stroke-linecap", "round");
|
|
2356
|
+
path.setAttribute("marker-end", `url(#${getOrCreateArrowMarker(svg, color)})`);
|
|
2357
|
+
path.setAttribute("class", "dependency-line");
|
|
2358
|
+
return path;
|
|
2359
|
+
}
|
|
2360
|
+
function getCardRect(card) {
|
|
2361
|
+
if (card === draggedCard) {
|
|
2362
|
+
return getDraggedCardVirtualRect() ?? card.getBoundingClientRect();
|
|
2363
|
+
}
|
|
2364
|
+
return card.getBoundingClientRect();
|
|
2365
|
+
}
|
|
2366
|
+
function getCardEdgePoints(fromCard, toCard, boardRect) {
|
|
2367
|
+
const fromRect = getCardRect(fromCard);
|
|
2368
|
+
const toRect = getCardRect(toCard);
|
|
2369
|
+
const fromCenterX = fromRect.left + fromRect.width / 2;
|
|
2370
|
+
const toCenterX = toRect.left + toRect.width / 2;
|
|
2371
|
+
const columnThreshold = 50;
|
|
2372
|
+
let fromX;
|
|
2373
|
+
let toX;
|
|
2374
|
+
if (Math.abs(fromCenterX - toCenterX) < columnThreshold) {
|
|
2375
|
+
fromX = fromRect.right - boardRect.left;
|
|
2376
|
+
toX = toRect.right - boardRect.left;
|
|
2377
|
+
} else if (fromCenterX <= toCenterX) {
|
|
2378
|
+
fromX = fromRect.right - boardRect.left;
|
|
2379
|
+
toX = toRect.left - boardRect.left;
|
|
2380
|
+
} else {
|
|
2381
|
+
fromX = fromRect.left - boardRect.left;
|
|
2382
|
+
toX = toRect.right - boardRect.left;
|
|
2383
|
+
}
|
|
2384
|
+
return {
|
|
2385
|
+
x1: fromX,
|
|
2386
|
+
y1: fromRect.top - boardRect.top + fromRect.height / 2,
|
|
2387
|
+
x2: toX,
|
|
2388
|
+
y2: toRect.top - boardRect.top + toRect.height / 2
|
|
2389
|
+
};
|
|
2390
|
+
}
|
|
2391
|
+
function createSVGOverlay() {
|
|
2392
|
+
const boardContainer = document.querySelector(".board-container");
|
|
2393
|
+
const existing = boardContainer.querySelector("#dependency-svg");
|
|
2394
|
+
if (existing) {
|
|
2395
|
+
existing.remove();
|
|
2396
|
+
}
|
|
2397
|
+
arrowMarkers.clear();
|
|
2398
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
2399
|
+
svg.setAttribute("id", "dependency-svg");
|
|
2400
|
+
svg.setAttribute("width", "100%");
|
|
2401
|
+
svg.setAttribute("height", "100%");
|
|
2402
|
+
svg.setAttribute("viewBox", `0 0 ${boardContainer.offsetWidth} ${boardContainer.offsetHeight}`);
|
|
2403
|
+
svg.style.position = "absolute";
|
|
2404
|
+
svg.style.top = "0";
|
|
2405
|
+
svg.style.left = "0";
|
|
2406
|
+
svg.style.pointerEvents = "none";
|
|
2407
|
+
svg.style.zIndex = "5";
|
|
2408
|
+
boardContainer.style.position = "relative";
|
|
2409
|
+
boardContainer.appendChild(svg);
|
|
2410
|
+
return svg;
|
|
2411
|
+
}
|
|
2412
|
+
function redrawDependencies() {
|
|
2413
|
+
if (!isDependencyVisible) return;
|
|
2414
|
+
const svg = createSVGOverlay();
|
|
2415
|
+
const boardContainer = document.querySelector(".board-container");
|
|
2416
|
+
svg.querySelectorAll(".dependency-line").forEach((line) => line.remove());
|
|
2417
|
+
const cards = Array.from(document.querySelectorAll(".card"));
|
|
2418
|
+
const cardMap = /* @__PURE__ */ new Map();
|
|
2419
|
+
cards.forEach((card) => {
|
|
2420
|
+
const id = Number(card.getAttribute("data-id"));
|
|
2421
|
+
cardMap.set(id, card);
|
|
2422
|
+
});
|
|
2423
|
+
const hoveredCard = document.querySelector(".card:hover");
|
|
2424
|
+
const hoveredCardId = hoveredCard ? Number(hoveredCard.getAttribute("data-id")) : null;
|
|
2425
|
+
const hoveredBlockedBySet = /* @__PURE__ */ new Set();
|
|
2426
|
+
const hoveredBlockingSet = /* @__PURE__ */ new Set();
|
|
2427
|
+
if (hoveredCardId) {
|
|
2428
|
+
const hoveredElement = cardMap.get(hoveredCardId);
|
|
2429
|
+
if (hoveredElement) {
|
|
2430
|
+
const blockedBy = hoveredElement.getAttribute("data-blocked-by");
|
|
2431
|
+
const blocking = hoveredElement.getAttribute("data-blocking");
|
|
2432
|
+
if (blockedBy) {
|
|
2433
|
+
blockedBy.split(",").forEach((id) => {
|
|
2434
|
+
const numId = Number(id.trim());
|
|
2435
|
+
if (!isNaN(numId)) hoveredBlockedBySet.add(numId);
|
|
2436
|
+
});
|
|
2437
|
+
}
|
|
2438
|
+
if (blocking) {
|
|
2439
|
+
blocking.split(",").forEach((id) => {
|
|
2440
|
+
const numId = Number(id.trim());
|
|
2441
|
+
if (!isNaN(numId)) hoveredBlockingSet.add(numId);
|
|
2442
|
+
});
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
const boardRect = boardContainer.getBoundingClientRect();
|
|
2447
|
+
cards.forEach((card) => {
|
|
2448
|
+
const cardId = Number(card.getAttribute("data-id"));
|
|
2449
|
+
const blockedByStr = card.getAttribute("data-blocked-by");
|
|
2450
|
+
const blockingStr = card.getAttribute("data-blocking");
|
|
2451
|
+
if (!blockedByStr && !blockingStr) return;
|
|
2452
|
+
const isHovered = cardId === hoveredCardId || hoveredBlockedBySet.has(cardId) || hoveredBlockingSet.has(cardId);
|
|
2453
|
+
if (blockingStr) {
|
|
2454
|
+
const blockingIds = blockingStr.split(",").map((s) => Number(s.trim()));
|
|
2455
|
+
blockingIds.forEach((blockedId) => {
|
|
2456
|
+
const blockedCard = cardMap.get(blockedId);
|
|
2457
|
+
if (blockedCard) {
|
|
2458
|
+
const { x1, y1, x2, y2 } = getCardEdgePoints(card, blockedCard, boardRect);
|
|
2459
|
+
const color = isHovered || hoveredBlockedBySet.has(blockedId) ? "#ef4444" : "#cbd5e1";
|
|
2460
|
+
const line = drawBezierLine(svg, x1, y1, x2, y2, color, isHovered);
|
|
2461
|
+
svg.appendChild(line);
|
|
2462
|
+
}
|
|
2463
|
+
});
|
|
2464
|
+
}
|
|
2465
|
+
});
|
|
2466
|
+
svg.setAttribute("viewBox", `0 0 ${boardContainer.offsetWidth} ${boardContainer.offsetHeight}`);
|
|
2467
|
+
}
|
|
2468
|
+
function handleCardHoverEvents() {
|
|
2469
|
+
const cards = document.querySelectorAll(".card");
|
|
2470
|
+
cards.forEach((card) => {
|
|
2471
|
+
card.addEventListener("mouseenter", () => {
|
|
2472
|
+
redrawDependencies();
|
|
2473
|
+
});
|
|
2474
|
+
card.addEventListener("mouseleave", () => {
|
|
2475
|
+
redrawDependencies();
|
|
2476
|
+
});
|
|
2477
|
+
});
|
|
2478
|
+
}
|
|
2479
|
+
function initDependencyVisualization() {
|
|
2480
|
+
const toggleBtn = document.getElementById("dependency-toggle");
|
|
2481
|
+
if (!toggleBtn) return;
|
|
2482
|
+
const redrawIfVisible = () => {
|
|
2483
|
+
if (isDependencyVisible) {
|
|
2484
|
+
handleCardHoverEvents();
|
|
2485
|
+
redrawDependencies();
|
|
2486
|
+
}
|
|
2487
|
+
};
|
|
2488
|
+
registerDependencyRedrawCallback2(redrawIfVisible);
|
|
2489
|
+
registerDependencyRedrawCallback(redrawIfVisible);
|
|
2490
|
+
toggleBtn.addEventListener("click", () => {
|
|
2491
|
+
isDependencyVisible = !isDependencyVisible;
|
|
2492
|
+
if (isDependencyVisible) {
|
|
2493
|
+
toggleBtn.classList.add("active");
|
|
2494
|
+
redrawDependencies();
|
|
2495
|
+
handleCardHoverEvents();
|
|
2496
|
+
const board = document.querySelector(".board");
|
|
2497
|
+
const boardContainer = document.querySelector(".board-container");
|
|
2498
|
+
if (board) {
|
|
2499
|
+
scrollListener = () => redrawDependencies();
|
|
2500
|
+
board.addEventListener("scroll", scrollListener, { passive: true });
|
|
2501
|
+
}
|
|
2502
|
+
columnScrollListener = () => redrawDependencies();
|
|
2503
|
+
document.querySelectorAll(".column-body").forEach((col) => {
|
|
2504
|
+
col.addEventListener("scroll", columnScrollListener, { passive: true });
|
|
2505
|
+
});
|
|
2506
|
+
if (boardContainer) {
|
|
2507
|
+
resizeListener = () => redrawDependencies();
|
|
2508
|
+
window.addEventListener("resize", resizeListener, { passive: true });
|
|
2509
|
+
}
|
|
2510
|
+
} else {
|
|
2511
|
+
toggleBtn.classList.remove("active");
|
|
2512
|
+
const svg = document.querySelector("#dependency-svg");
|
|
2513
|
+
if (svg) svg.remove();
|
|
2514
|
+
const board = document.querySelector(".board");
|
|
2515
|
+
if (board && scrollListener) {
|
|
2516
|
+
board.removeEventListener("scroll", scrollListener);
|
|
2517
|
+
scrollListener = null;
|
|
2518
|
+
}
|
|
2519
|
+
if (columnScrollListener) {
|
|
2520
|
+
document.querySelectorAll(".column-body").forEach((col) => {
|
|
2521
|
+
col.removeEventListener("scroll", columnScrollListener);
|
|
2522
|
+
});
|
|
2523
|
+
columnScrollListener = null;
|
|
2524
|
+
}
|
|
2525
|
+
if (resizeListener) {
|
|
2526
|
+
window.removeEventListener("resize", resizeListener);
|
|
2527
|
+
resizeListener = null;
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
});
|
|
2531
|
+
}
|
|
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
|
+
|
|
1717
2688
|
// src/board/client/main.ts
|
|
1718
2689
|
initDragDrop();
|
|
1719
2690
|
initAutoScroll();
|
|
@@ -1723,4 +2694,11 @@
|
|
|
1723
2694
|
initBoardPolling();
|
|
1724
2695
|
initFilters();
|
|
1725
2696
|
initBurgerMenu();
|
|
2697
|
+
initDependencyVisualization();
|
|
2698
|
+
initClaudeButton();
|
|
2699
|
+
initClaudeStreamModal();
|
|
2700
|
+
registerClaudeModalCallback(openClaudeStreamModal);
|
|
2701
|
+
registerClaudeButtonUpdateCallback(() => {
|
|
2702
|
+
updateButtonStates(/* @__PURE__ */ new Set());
|
|
2703
|
+
});
|
|
1726
2704
|
})();
|