claude-code-kanban 4.9.0 → 4.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/public/app.js +4 -207
- package/public/index.html +11 -54
- package/public/style.css +39 -81
- package/server.js +25 -42
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -14,7 +14,6 @@ const collapsedProjectGroups = new Set();
|
|
|
14
14
|
let stableGroupOrder = []; // cached project path order to prevent jumping
|
|
15
15
|
let searchQuery = ''; // Search query for fuzzy search
|
|
16
16
|
let allTasksCache = []; // Cache all tasks for search
|
|
17
|
-
let bulkDeleteSessionId = null; // Track session for bulk delete
|
|
18
17
|
let ownerFilter = '';
|
|
19
18
|
let currentAgents = [];
|
|
20
19
|
let currentWaiting = null;
|
|
@@ -226,172 +225,9 @@ function clearSearch() {
|
|
|
226
225
|
renderSessions();
|
|
227
226
|
}
|
|
228
227
|
|
|
229
|
-
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
230
|
-
function deleteAllSessionTasks(sessionId) {
|
|
231
|
-
const session = sessions.find((s) => s.id === sessionId);
|
|
232
|
-
if (!session) return;
|
|
233
|
-
|
|
234
|
-
// When viewing a single session, currentTasks already contains only that session's tasks
|
|
235
|
-
// When viewing "All Tasks", tasks have sessionId property, so we filter
|
|
236
|
-
const sessionTasks =
|
|
237
|
-
currentSessionId === sessionId ? currentTasks : currentTasks.filter((t) => t.sessionId === sessionId);
|
|
238
|
-
|
|
239
|
-
if (sessionTasks.length === 0) {
|
|
240
|
-
alert('No tasks to delete in this session');
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
bulkDeleteSessionId = sessionId;
|
|
245
|
-
|
|
246
|
-
const displayName = session.name || sessionId;
|
|
247
|
-
const message = `Delete all ${sessionTasks.length} task(s) from session "${displayName}"?`;
|
|
248
|
-
|
|
249
|
-
document.getElementById('delete-session-tasks-message').textContent = message;
|
|
250
|
-
|
|
251
|
-
const modal = document.getElementById('delete-session-tasks-modal');
|
|
252
|
-
modal.classList.add('visible');
|
|
253
|
-
|
|
254
|
-
// Handle ESC key
|
|
255
|
-
const keyHandler = (e) => {
|
|
256
|
-
if (e.key === 'Escape') {
|
|
257
|
-
e.preventDefault();
|
|
258
|
-
closeDeleteSessionTasksModal();
|
|
259
|
-
document.removeEventListener('keydown', keyHandler);
|
|
260
|
-
}
|
|
261
|
-
};
|
|
262
|
-
document.addEventListener('keydown', keyHandler);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function closeDeleteSessionTasksModal() {
|
|
266
|
-
const modal = document.getElementById('delete-session-tasks-modal');
|
|
267
|
-
modal.classList.remove('visible');
|
|
268
|
-
bulkDeleteSessionId = null;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
272
|
-
async function confirmDeleteSessionTasks() {
|
|
273
|
-
if (!bulkDeleteSessionId) return;
|
|
274
|
-
|
|
275
|
-
const sessionId = bulkDeleteSessionId;
|
|
276
|
-
closeDeleteSessionTasksModal();
|
|
277
|
-
|
|
278
|
-
// Get tasks to delete
|
|
279
|
-
const sessionTasks =
|
|
280
|
-
currentSessionId === sessionId ? currentTasks : currentTasks.filter((t) => t.sessionId === sessionId);
|
|
281
|
-
|
|
282
|
-
// Sort tasks by dependency order (blocked tasks first, then blockers)
|
|
283
|
-
const sortedTasks = topologicalSort(sessionTasks);
|
|
284
|
-
|
|
285
|
-
let successCount = 0;
|
|
286
|
-
let failedCount = 0;
|
|
287
|
-
const failedTasks = [];
|
|
288
|
-
|
|
289
|
-
for (const task of sortedTasks) {
|
|
290
|
-
try {
|
|
291
|
-
const res = await fetch(`/api/tasks/${sessionId}/${task.id}`, {
|
|
292
|
-
method: 'DELETE',
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
if (res.ok) {
|
|
296
|
-
successCount++;
|
|
297
|
-
} else {
|
|
298
|
-
failedCount++;
|
|
299
|
-
const error = await res.json();
|
|
300
|
-
failedTasks.push({ id: task.id, subject: task.subject, error: error.error });
|
|
301
|
-
console.error(`Failed to delete task ${task.id}:`, error);
|
|
302
|
-
}
|
|
303
|
-
} catch (error) {
|
|
304
|
-
failedCount++;
|
|
305
|
-
failedTasks.push({ id: task.id, subject: task.subject, error: 'Network error' });
|
|
306
|
-
console.error(`Error deleting task ${task.id}:`, error);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Show result modal
|
|
311
|
-
showDeleteResultModal(successCount, failedCount, failedTasks);
|
|
312
|
-
|
|
313
|
-
// Close detail panel if open
|
|
314
|
-
closeDetailPanel();
|
|
315
|
-
|
|
316
|
-
// Refresh the view
|
|
317
|
-
await refreshCurrentView();
|
|
318
|
-
}
|
|
319
|
-
|
|
320
228
|
//#endregion
|
|
321
229
|
|
|
322
|
-
//#region
|
|
323
|
-
// Topological sort for task deletion order
|
|
324
|
-
function topologicalSort(tasks) {
|
|
325
|
-
const result = [];
|
|
326
|
-
const visited = new Set();
|
|
327
|
-
const visiting = new Set();
|
|
328
|
-
const taskMap = new Map(tasks.map((t) => [t.id, t]));
|
|
329
|
-
|
|
330
|
-
function visit(taskId) {
|
|
331
|
-
if (visited.has(taskId)) return;
|
|
332
|
-
if (visiting.has(taskId)) return; // Cycle - skip
|
|
333
|
-
|
|
334
|
-
visiting.add(taskId);
|
|
335
|
-
const task = taskMap.get(taskId);
|
|
336
|
-
|
|
337
|
-
if (task?.blocks && task.blocks.length > 0) {
|
|
338
|
-
// Visit all tasks that this task blocks (dependencies first)
|
|
339
|
-
for (const blockedId of task.blocks) {
|
|
340
|
-
if (taskMap.has(blockedId)) {
|
|
341
|
-
visit(blockedId);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
visiting.delete(taskId);
|
|
347
|
-
visited.add(taskId);
|
|
348
|
-
if (task) result.push(task);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Visit all tasks
|
|
352
|
-
for (const task of tasks) {
|
|
353
|
-
visit(task.id);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
return result;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function showDeleteResultModal(successCount, failedCount, failedTasks) {
|
|
360
|
-
const modal = document.getElementById('delete-result-modal');
|
|
361
|
-
const messageEl = document.getElementById('delete-result-message');
|
|
362
|
-
const detailsEl = document.getElementById('delete-result-details');
|
|
363
|
-
|
|
364
|
-
if (failedCount === 0) {
|
|
365
|
-
messageEl.textContent = `Successfully deleted all ${successCount} task(s).`;
|
|
366
|
-
detailsEl.style.display = 'none';
|
|
367
|
-
} else {
|
|
368
|
-
messageEl.textContent = `Deleted ${successCount} task(s). Failed to delete ${failedCount} task(s).`;
|
|
369
|
-
|
|
370
|
-
const failedList = failedTasks
|
|
371
|
-
.map((t) => `<li><strong>${escapeHtml(t.subject)}</strong> (#${escapeHtml(t.id)}): ${escapeHtml(t.error)}</li>`)
|
|
372
|
-
.join('');
|
|
373
|
-
detailsEl.innerHTML = `<ul style="margin: 8px 0 0 0; padding-left: 20px;">${failedList}</ul>`;
|
|
374
|
-
detailsEl.style.display = 'block';
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
modal.classList.add('visible');
|
|
378
|
-
|
|
379
|
-
// Handle ESC key
|
|
380
|
-
const keyHandler = (e) => {
|
|
381
|
-
if (e.key === 'Escape') {
|
|
382
|
-
e.preventDefault();
|
|
383
|
-
closeDeleteResultModal();
|
|
384
|
-
document.removeEventListener('keydown', keyHandler);
|
|
385
|
-
}
|
|
386
|
-
};
|
|
387
|
-
document.addEventListener('keydown', keyHandler);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function closeDeleteResultModal() {
|
|
391
|
-
const modal = document.getElementById('delete-result-modal');
|
|
392
|
-
modal.classList.remove('visible');
|
|
393
|
-
}
|
|
394
|
-
|
|
230
|
+
//#region SEARCH
|
|
395
231
|
function fuzzyMatch(text, query) {
|
|
396
232
|
if (!query) return true;
|
|
397
233
|
if (!text) return false;
|
|
@@ -1833,6 +1669,8 @@ function _renderPinToDetail(pin) {
|
|
|
1833
1669
|
}
|
|
1834
1670
|
|
|
1835
1671
|
const SESSION_PIN_SVG = PIN_SVG.replace('width="14" height="14"', 'width="12" height="12"');
|
|
1672
|
+
const SESSION_STAR_SVG =
|
|
1673
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="12" height="12"><path d="M12 2 L15 9 L22 9 L17 14 L19 22 L12 18 L5 22 L7 14 L2 9 L9 9 Z"/></svg>';
|
|
1836
1674
|
const LINK_SVG_PATHS =
|
|
1837
1675
|
'<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>';
|
|
1838
1676
|
const linkSvg = (size) =>
|
|
@@ -2993,7 +2831,7 @@ function renderSessions() {
|
|
|
2993
2831
|
const tempClass = session.hasRecentLog || session.inProgress || session.hasWaitingForUser ? 'warm' : 'stale';
|
|
2994
2832
|
return `
|
|
2995
2833
|
<button onclick="fetchTasks('${session.id}')" data-session-id="${session.id}" class="session-item ${isActive ? 'active' : ''} ${session.hasWaitingForUser ? 'permission-pending' : ''} ${tempClass} ${showCtx ? 'has-context' : ''}" title="${tooltip}">
|
|
2996
|
-
<span class="session-pin-btn${pinClass}" onclick="event.stopPropagation();toggleSessionPin('${escapeHtml(session.id)}')" title="${pinTitle} session">${SESSION_PIN_SVG}</span>
|
|
2834
|
+
<span class="session-pin-btn${pinClass}" onclick="event.stopPropagation();toggleSessionPin('${escapeHtml(session.id)}')" title="${pinTitle} session">${pinState === 'sticky' ? SESSION_STAR_SVG : SESSION_PIN_SVG}</span>
|
|
2997
2835
|
<div class="session-name">${escapeHtml(primaryName)}</div>
|
|
2998
2836
|
${secondaryName ? `<div class="session-secondary">${escapeHtml(secondaryName)}</div>` : ''}
|
|
2999
2837
|
${gitBranch ? `<div class="session-branch">${gitBranch}</div>` : ''}
|
|
@@ -3829,14 +3667,6 @@ async function showTaskDetail(taskId, sessionId = null) {
|
|
|
3829
3667
|
</div>`
|
|
3830
3668
|
: ''
|
|
3831
3669
|
}
|
|
3832
|
-
|
|
3833
|
-
<div class="detail-section note-section">
|
|
3834
|
-
<label for="note-input" class="detail-label">Add Note</label>
|
|
3835
|
-
<form class="note-form" onsubmit="addNote(event, '${task.id}', '${actualSessionId}')">
|
|
3836
|
-
<textarea id="note-input" class="note-input" placeholder="Add a note for Claude..." rows="3"></textarea>
|
|
3837
|
-
<button type="submit" class="note-submit">Add Note</button>
|
|
3838
|
-
</form>
|
|
3839
|
-
</div>
|
|
3840
3670
|
`;
|
|
3841
3671
|
|
|
3842
3672
|
// Setup button handlers (read-only in project view)
|
|
@@ -3845,9 +3675,6 @@ async function showTaskDetail(taskId, sessionId = null) {
|
|
|
3845
3675
|
deleteBtn.style.display = isProjectView ? 'none' : '';
|
|
3846
3676
|
if (!isProjectView) deleteBtn.onclick = () => deleteTask(task.id, actualSessionId);
|
|
3847
3677
|
|
|
3848
|
-
const noteSection = detailContent.querySelector('.note-section');
|
|
3849
|
-
if (noteSection && isProjectView) noteSection.style.display = 'none';
|
|
3850
|
-
|
|
3851
3678
|
if (!isProjectView) {
|
|
3852
3679
|
const titleEl = detailContent.querySelector('.detail-title');
|
|
3853
3680
|
if (titleEl) {
|
|
@@ -3955,36 +3782,6 @@ async function saveTaskField(taskId, sessionId, field, value) {
|
|
|
3955
3782
|
}
|
|
3956
3783
|
}
|
|
3957
3784
|
|
|
3958
|
-
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
3959
|
-
async function addNote(event, taskId, sessionId) {
|
|
3960
|
-
event.preventDefault();
|
|
3961
|
-
const input = document.getElementById('note-input');
|
|
3962
|
-
const note = input.value.trim();
|
|
3963
|
-
if (!note) return;
|
|
3964
|
-
|
|
3965
|
-
try {
|
|
3966
|
-
const res = await fetch(`/api/tasks/${sessionId}/${taskId}/note`, {
|
|
3967
|
-
method: 'POST',
|
|
3968
|
-
headers: { 'Content-Type': 'application/json' },
|
|
3969
|
-
body: JSON.stringify({ note }),
|
|
3970
|
-
});
|
|
3971
|
-
|
|
3972
|
-
if (res.ok) {
|
|
3973
|
-
input.value = '';
|
|
3974
|
-
// Refresh to show updated description
|
|
3975
|
-
if (viewMode === 'all') {
|
|
3976
|
-
const tasksRes = await fetch('/api/tasks/all');
|
|
3977
|
-
currentTasks = await tasksRes.json();
|
|
3978
|
-
} else {
|
|
3979
|
-
await fetchTasks(sessionId);
|
|
3980
|
-
}
|
|
3981
|
-
showTaskDetail(taskId, sessionId);
|
|
3982
|
-
}
|
|
3983
|
-
} catch (error) {
|
|
3984
|
-
console.error('Failed to add note:', error);
|
|
3985
|
-
}
|
|
3986
|
-
}
|
|
3987
|
-
|
|
3988
3785
|
function closeDetailPanel() {
|
|
3989
3786
|
detailPanel.classList.remove('visible');
|
|
3990
3787
|
document.getElementById('delete-task-btn').style.display = 'none';
|
package/public/index.html
CHANGED
|
@@ -221,7 +221,7 @@
|
|
|
221
221
|
<header class="detail-header">
|
|
222
222
|
<h3>Task Details</h3>
|
|
223
223
|
<div style="display: flex; gap: 8px; align-items: center;">
|
|
224
|
-
<button id="delete-task-btn" class="icon-btn" title="Delete task (D)" aria-label="Delete task" style="
|
|
224
|
+
<button id="delete-task-btn" class="icon-btn icon-btn-danger" title="Delete task (D)" aria-label="Delete task" style="display: none;">
|
|
225
225
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
226
226
|
<path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
|
|
227
227
|
</svg>
|
|
@@ -472,50 +472,7 @@
|
|
|
472
472
|
</div>
|
|
473
473
|
<div class="modal-footer">
|
|
474
474
|
<button id="delete-cancel-btn" class="btn btn-secondary" onclick="closeDeleteConfirmModal()">Cancel</button>
|
|
475
|
-
<button id="delete-confirm-btn" class="btn btn-
|
|
476
|
-
</div>
|
|
477
|
-
</div>
|
|
478
|
-
</div>
|
|
479
|
-
|
|
480
|
-
<!-- Delete All Session Tasks Confirmation Modal -->
|
|
481
|
-
<div id="delete-session-tasks-modal" class="modal-overlay" onclick="closeDeleteSessionTasksModal()">
|
|
482
|
-
<div class="modal" onclick="event.stopPropagation()">
|
|
483
|
-
<div class="modal-header">
|
|
484
|
-
<h3 class="modal-title">Delete All Tasks</h3>
|
|
485
|
-
<button class="modal-close" aria-label="Close dialog" onclick="closeDeleteSessionTasksModal()">
|
|
486
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
487
|
-
<path d="M18 6L6 18M6 6l12 12"/>
|
|
488
|
-
</svg>
|
|
489
|
-
</button>
|
|
490
|
-
</div>
|
|
491
|
-
<div class="modal-body">
|
|
492
|
-
<p id="delete-session-tasks-message" style="margin: 0 0 12px 0; color: var(--text-primary);"></p>
|
|
493
|
-
<p style="margin: 0; font-size: 13px; color: var(--text-secondary);">This action cannot be undone.</p>
|
|
494
|
-
</div>
|
|
495
|
-
<div class="modal-footer">
|
|
496
|
-
<button class="btn btn-secondary" onclick="closeDeleteSessionTasksModal()">Cancel</button>
|
|
497
|
-
<button class="btn btn-primary" onclick="confirmDeleteSessionTasks()" style="background: #ef4444; border-color: #ef4444;">Delete All</button>
|
|
498
|
-
</div>
|
|
499
|
-
</div>
|
|
500
|
-
</div>
|
|
501
|
-
|
|
502
|
-
<!-- Delete Result Modal -->
|
|
503
|
-
<div id="delete-result-modal" class="modal-overlay" onclick="closeDeleteResultModal()">
|
|
504
|
-
<div class="modal" onclick="event.stopPropagation()">
|
|
505
|
-
<div class="modal-header">
|
|
506
|
-
<h3 class="modal-title">Deletion Result</h3>
|
|
507
|
-
<button class="modal-close" aria-label="Close dialog" onclick="closeDeleteResultModal()">
|
|
508
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
509
|
-
<path d="M18 6L6 18M6 6l12 12"/>
|
|
510
|
-
</svg>
|
|
511
|
-
</button>
|
|
512
|
-
</div>
|
|
513
|
-
<div class="modal-body">
|
|
514
|
-
<p id="delete-result-message" style="margin: 0; color: var(--text-primary);"></p>
|
|
515
|
-
<div id="delete-result-details" style="margin-top: 12px; font-size: 13px; color: var(--text-secondary);"></div>
|
|
516
|
-
</div>
|
|
517
|
-
<div class="modal-footer">
|
|
518
|
-
<button class="btn btn-primary" onclick="closeDeleteResultModal()">Close</button>
|
|
475
|
+
<button id="delete-confirm-btn" class="btn btn-danger" onclick="confirmDelete()">Delete</button>
|
|
519
476
|
</div>
|
|
520
477
|
</div>
|
|
521
478
|
</div>
|
|
@@ -549,7 +506,7 @@
|
|
|
549
506
|
<div class="modal-footer">
|
|
550
507
|
<button id="session-info-dismiss-btn" class="btn btn-secondary" onclick="toggleDismissSession(_infoModalSessionId); closeTeamModal()">Dismiss</button>
|
|
551
508
|
<button class="btn btn-secondary" onclick="showToolStatsModal(_infoModalSessionId)" title="Tool invocation statistics">Tool Stats</button>
|
|
552
|
-
<button class="btn btn-
|
|
509
|
+
<button class="btn btn-secondary" onclick="closeTeamModal()">Close</button>
|
|
553
510
|
</div>
|
|
554
511
|
</div>
|
|
555
512
|
</div>
|
|
@@ -567,7 +524,7 @@
|
|
|
567
524
|
</div>
|
|
568
525
|
<div id="tool-stats-modal-body" class="modal-body" style="overflow-y:auto;flex:1 1 auto;min-height:0;"></div>
|
|
569
526
|
<div class="modal-footer">
|
|
570
|
-
<button class="btn btn-
|
|
527
|
+
<button class="btn btn-secondary" onclick="closeToolStatsModal()">Close</button>
|
|
571
528
|
</div>
|
|
572
529
|
</div>
|
|
573
530
|
</div>
|
|
@@ -596,7 +553,7 @@
|
|
|
596
553
|
</div>
|
|
597
554
|
<div id="plan-modal-body" class="modal-body detail-desc rendered-md" style="overflow-y: auto; flex: 0 1 auto; min-height: 0;"></div>
|
|
598
555
|
<div class="modal-footer">
|
|
599
|
-
<button class="btn btn-
|
|
556
|
+
<button class="btn btn-secondary" onclick="closePlanModal()">Close</button>
|
|
600
557
|
</div>
|
|
601
558
|
</div>
|
|
602
559
|
</div>
|
|
@@ -606,7 +563,7 @@
|
|
|
606
563
|
<div class="modal modal-sm" onclick="event.stopPropagation()">
|
|
607
564
|
<div class="modal-header">
|
|
608
565
|
<h3 class="modal-title" style="display: flex; align-items: center; gap: 8px;">
|
|
609
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="
|
|
566
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="var(--danger)" stroke-width="2" style="width: 20px; height: 20px;">
|
|
610
567
|
<circle cx="12" cy="12" r="10"/>
|
|
611
568
|
<line x1="15" y1="9" x2="9" y2="15"/>
|
|
612
569
|
<line x1="9" y1="9" x2="15" y2="15"/>
|
|
@@ -623,7 +580,7 @@
|
|
|
623
580
|
<div id="blocked-task-message" style="color: var(--text-primary); line-height: 1.6;"></div>
|
|
624
581
|
</div>
|
|
625
582
|
<div class="modal-footer">
|
|
626
|
-
<button class="btn btn-
|
|
583
|
+
<button class="btn btn-secondary" onclick="closeBlockedTaskModal()">OK</button>
|
|
627
584
|
</div>
|
|
628
585
|
</div>
|
|
629
586
|
</div>
|
|
@@ -642,7 +599,7 @@
|
|
|
642
599
|
</div>
|
|
643
600
|
<div id="loop-modal-body" class="modal-body" style="overflow-y:auto;flex:0 1 auto;min-height:0;"></div>
|
|
644
601
|
<div class="modal-footer">
|
|
645
|
-
<button class="btn btn-
|
|
602
|
+
<button class="btn btn-secondary" onclick="closeLoopModal()">Close</button>
|
|
646
603
|
</div>
|
|
647
604
|
</div>
|
|
648
605
|
</div>
|
|
@@ -674,7 +631,7 @@
|
|
|
674
631
|
<div id="agent-modal-body" class="modal-body" style="overflow-y: auto; overflow-x: hidden; flex: 0 1 auto; min-height: 0;"></div>
|
|
675
632
|
<div class="modal-footer">
|
|
676
633
|
<button id="agent-modal-dismiss-btn" class="btn agent-dismiss-btn" style="display:none" onclick="dismissAgent(currentAgentModalId);closeAgentModal()">Dismiss</button>
|
|
677
|
-
<button class="btn btn-
|
|
634
|
+
<button class="btn btn-secondary" onclick="closeAgentModal()">Close</button>
|
|
678
635
|
</div>
|
|
679
636
|
</div>
|
|
680
637
|
</div>
|
|
@@ -693,7 +650,7 @@
|
|
|
693
650
|
</div>
|
|
694
651
|
<div class="modal-footer scratchpad-footer">
|
|
695
652
|
<span id="scratchpad-charcount" class="scratchpad-charcount">0 chars</span>
|
|
696
|
-
<button class="btn btn-
|
|
653
|
+
<button class="btn btn-secondary" onclick="closeScratchpad()">Close</button>
|
|
697
654
|
</div>
|
|
698
655
|
</div>
|
|
699
656
|
</div>
|
|
@@ -717,7 +674,7 @@
|
|
|
717
674
|
<div class="modal-body" id="storage-modal-body" style="overflow-y:auto;min-height:200px;max-height:60vh;padding-top:16px;padding-right:8px;"></div>
|
|
718
675
|
<div class="modal-footer">
|
|
719
676
|
<button id="storage-cleanup-btn" class="btn btn-secondary" onclick="cleanupOrphanedStorage()">Clean Orphaned</button>
|
|
720
|
-
<button class="btn btn-
|
|
677
|
+
<button class="btn btn-secondary" onclick="closeStorageManager()">Close</button>
|
|
721
678
|
</div>
|
|
722
679
|
</div>
|
|
723
680
|
</div>
|
package/public/style.css
CHANGED
|
@@ -893,13 +893,14 @@ body::before {
|
|
|
893
893
|
}
|
|
894
894
|
|
|
895
895
|
.icon-btn-danger {
|
|
896
|
-
color:
|
|
896
|
+
color: var(--danger);
|
|
897
|
+
border-color: var(--danger);
|
|
897
898
|
}
|
|
898
899
|
|
|
899
900
|
.icon-btn-danger:hover {
|
|
900
|
-
background:
|
|
901
|
-
color:
|
|
902
|
-
border-color:
|
|
901
|
+
background: color-mix(in srgb, var(--danger) 12%, transparent);
|
|
902
|
+
color: color-mix(in srgb, var(--danger) 80%, black);
|
|
903
|
+
border-color: var(--danger);
|
|
903
904
|
}
|
|
904
905
|
|
|
905
906
|
/* theme picker: palette icon button + custom popover with per-theme swatches
|
|
@@ -1167,8 +1168,8 @@ body::before {
|
|
|
1167
1168
|
}
|
|
1168
1169
|
|
|
1169
1170
|
.task-badge.blocked {
|
|
1170
|
-
background:
|
|
1171
|
-
color:
|
|
1171
|
+
background: color-mix(in srgb, var(--accent) 15%, transparent);
|
|
1172
|
+
color: var(--accent);
|
|
1172
1173
|
}
|
|
1173
1174
|
|
|
1174
1175
|
.task-title {
|
|
@@ -1568,61 +1569,6 @@ body::before {
|
|
|
1568
1569
|
|
|
1569
1570
|
/* #endregion */
|
|
1570
1571
|
|
|
1571
|
-
/* #region NOTE_FORM */
|
|
1572
|
-
.note-section {
|
|
1573
|
-
margin-top: 8px;
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
.note-form {
|
|
1577
|
-
display: flex;
|
|
1578
|
-
flex-direction: column;
|
|
1579
|
-
gap: 10px;
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
.note-input {
|
|
1583
|
-
width: 100%;
|
|
1584
|
-
padding: 10px 12px;
|
|
1585
|
-
background: var(--bg-elevated);
|
|
1586
|
-
border: 1px solid var(--border);
|
|
1587
|
-
border-radius: 6px;
|
|
1588
|
-
color: var(--text-primary);
|
|
1589
|
-
font-family: var(--mono);
|
|
1590
|
-
font-size: 12px;
|
|
1591
|
-
line-height: 1.5;
|
|
1592
|
-
resize: vertical;
|
|
1593
|
-
min-height: 60px;
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
.note-input:focus {
|
|
1597
|
-
outline: none;
|
|
1598
|
-
border-color: var(--accent);
|
|
1599
|
-
box-shadow: 0 0 0 2px var(--accent-dim);
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
.note-input::placeholder {
|
|
1603
|
-
color: var(--text-muted);
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
.note-submit {
|
|
1607
|
-
align-self: flex-end;
|
|
1608
|
-
padding: 8px 16px;
|
|
1609
|
-
background: var(--accent);
|
|
1610
|
-
border: none;
|
|
1611
|
-
border-radius: 5px;
|
|
1612
|
-
color: white;
|
|
1613
|
-
font-family: var(--mono);
|
|
1614
|
-
font-size: 11px;
|
|
1615
|
-
font-weight: 500;
|
|
1616
|
-
cursor: pointer;
|
|
1617
|
-
transition: all 0.15s ease;
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
.note-submit:hover {
|
|
1621
|
-
filter: brightness(1.1);
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
/* #endregion */
|
|
1625
|
-
|
|
1626
1572
|
/* #region TEAM_BADGE */
|
|
1627
1573
|
.session-indicators {
|
|
1628
1574
|
display: flex;
|
|
@@ -2512,8 +2458,8 @@ body::before {
|
|
|
2512
2458
|
color: var(--success);
|
|
2513
2459
|
}
|
|
2514
2460
|
.toast.toast-error {
|
|
2515
|
-
border-color:
|
|
2516
|
-
color:
|
|
2461
|
+
border-color: var(--danger);
|
|
2462
|
+
color: var(--danger);
|
|
2517
2463
|
}
|
|
2518
2464
|
.toast.toast-info {
|
|
2519
2465
|
border-color: var(--accent);
|
|
@@ -2847,10 +2793,10 @@ body::before {
|
|
|
2847
2793
|
font-weight: 600;
|
|
2848
2794
|
}
|
|
2849
2795
|
.protocol-bool-true {
|
|
2850
|
-
color:
|
|
2796
|
+
color: var(--success);
|
|
2851
2797
|
}
|
|
2852
2798
|
.protocol-bool-false {
|
|
2853
|
-
color:
|
|
2799
|
+
color: var(--danger);
|
|
2854
2800
|
}
|
|
2855
2801
|
.msg-jump-latest {
|
|
2856
2802
|
position: absolute;
|
|
@@ -3183,8 +3129,8 @@ body.light .msg-assistant .msg-text {
|
|
|
3183
3129
|
|
|
3184
3130
|
/* #region INTERACTIVE */
|
|
3185
3131
|
.icon-btn.delete:hover {
|
|
3186
|
-
background:
|
|
3187
|
-
border-color:
|
|
3132
|
+
background: color-mix(in srgb, var(--danger) 12%, transparent);
|
|
3133
|
+
border-color: var(--danger);
|
|
3188
3134
|
}
|
|
3189
3135
|
|
|
3190
3136
|
.column-header {
|
|
@@ -3560,11 +3506,23 @@ select.form-input option:checked {
|
|
|
3560
3506
|
}
|
|
3561
3507
|
|
|
3562
3508
|
.btn-primary:hover {
|
|
3563
|
-
background:
|
|
3509
|
+
background: color-mix(in srgb, var(--accent) 82%, black);
|
|
3564
3510
|
transform: translateY(-1px);
|
|
3565
3511
|
box-shadow: 0 4px 12px var(--accent-glow);
|
|
3566
3512
|
}
|
|
3567
3513
|
|
|
3514
|
+
.btn-danger {
|
|
3515
|
+
background: var(--danger);
|
|
3516
|
+
border-color: var(--danger);
|
|
3517
|
+
color: white;
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3520
|
+
.btn-danger:hover {
|
|
3521
|
+
background: color-mix(in srgb, var(--danger) 82%, black);
|
|
3522
|
+
transform: translateY(-1px);
|
|
3523
|
+
box-shadow: 0 4px 12px color-mix(in srgb, var(--danger) 45%, transparent);
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3568
3526
|
.btn-secondary {
|
|
3569
3527
|
background: transparent;
|
|
3570
3528
|
color: var(--text-secondary);
|
|
@@ -3685,8 +3643,8 @@ select.form-input option:checked {
|
|
|
3685
3643
|
|
|
3686
3644
|
.badge-success {
|
|
3687
3645
|
display: inline-block;
|
|
3688
|
-
background:
|
|
3689
|
-
color:
|
|
3646
|
+
background: color-mix(in srgb, var(--success) 15%, transparent);
|
|
3647
|
+
color: var(--success);
|
|
3690
3648
|
border-radius: 4px;
|
|
3691
3649
|
padding: 1px 6px;
|
|
3692
3650
|
font-size: 12px;
|
|
@@ -3695,8 +3653,8 @@ select.form-input option:checked {
|
|
|
3695
3653
|
|
|
3696
3654
|
.badge-failed {
|
|
3697
3655
|
display: inline-block;
|
|
3698
|
-
background:
|
|
3699
|
-
color:
|
|
3656
|
+
background: color-mix(in srgb, var(--danger) 15%, transparent);
|
|
3657
|
+
color: var(--danger);
|
|
3700
3658
|
border-radius: 4px;
|
|
3701
3659
|
padding: 1px 6px;
|
|
3702
3660
|
font-size: 12px;
|
|
@@ -3705,8 +3663,8 @@ select.form-input option:checked {
|
|
|
3705
3663
|
|
|
3706
3664
|
.badge-rejected {
|
|
3707
3665
|
display: inline-block;
|
|
3708
|
-
background:
|
|
3709
|
-
color:
|
|
3666
|
+
background: color-mix(in srgb, var(--warning) 15%, transparent);
|
|
3667
|
+
color: var(--warning);
|
|
3710
3668
|
border-radius: 4px;
|
|
3711
3669
|
padding: 1px 6px;
|
|
3712
3670
|
font-size: 12px;
|
|
@@ -3714,7 +3672,7 @@ select.form-input option:checked {
|
|
|
3714
3672
|
}
|
|
3715
3673
|
|
|
3716
3674
|
.tool-stats-chip-val.rejected {
|
|
3717
|
-
color:
|
|
3675
|
+
color: var(--warning);
|
|
3718
3676
|
}
|
|
3719
3677
|
|
|
3720
3678
|
.impact-bar-wrap {
|
|
@@ -3910,8 +3868,8 @@ select.form-input option:checked {
|
|
|
3910
3868
|
}
|
|
3911
3869
|
|
|
3912
3870
|
.storage-item-badge.orphan {
|
|
3913
|
-
background:
|
|
3914
|
-
color:
|
|
3871
|
+
background: color-mix(in srgb, var(--danger) 15%, transparent);
|
|
3872
|
+
color: var(--danger);
|
|
3915
3873
|
}
|
|
3916
3874
|
|
|
3917
3875
|
.storage-item-badge.pinned {
|
|
@@ -3920,8 +3878,8 @@ select.form-input option:checked {
|
|
|
3920
3878
|
}
|
|
3921
3879
|
|
|
3922
3880
|
.storage-item-badge.sticky {
|
|
3923
|
-
background:
|
|
3924
|
-
color:
|
|
3881
|
+
background: color-mix(in srgb, var(--gold) 15%, transparent);
|
|
3882
|
+
color: var(--gold);
|
|
3925
3883
|
}
|
|
3926
3884
|
|
|
3927
3885
|
.storage-item-actions {
|
|
@@ -3948,8 +3906,8 @@ select.form-input option:checked {
|
|
|
3948
3906
|
}
|
|
3949
3907
|
|
|
3950
3908
|
.storage-item-actions button.danger:hover {
|
|
3951
|
-
background:
|
|
3952
|
-
color:
|
|
3909
|
+
background: color-mix(in srgb, var(--danger) 15%, transparent);
|
|
3910
|
+
color: var(--danger);
|
|
3953
3911
|
}
|
|
3954
3912
|
|
|
3955
3913
|
.storage-group-header {
|
package/server.js
CHANGED
|
@@ -270,6 +270,20 @@ function resolveSessionId(sessionId) {
|
|
|
270
270
|
return (teamConfig && teamConfig.leadSessionId) ? teamConfig.leadSessionId : sessionId;
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
// Recent Claude Code releases auto-create a single-member "self-team" for every
|
|
274
|
+
// session: a teams/session-<id>/config.json whose only member is the "team-lead"
|
|
275
|
+
// (the session itself). These are not real multi-agent teams — surfacing them
|
|
276
|
+
// makes every solo session render a team badge, member panel, and (via the empty
|
|
277
|
+
// team-named task dir) a shared-task-list link. Treat them as plain sessions.
|
|
278
|
+
// As soon as a real teammate joins (members.length > 1) it becomes a true team again.
|
|
279
|
+
function isAutoSelfTeam(cfg) {
|
|
280
|
+
if (!cfg || !Array.isArray(cfg.members)) return false;
|
|
281
|
+
const namedSession = typeof cfg.name === 'string' && cfg.name.startsWith('session-');
|
|
282
|
+
const soleLead = cfg.members.length === 0
|
|
283
|
+
|| (cfg.members.length === 1 && cfg.members[0]?.agentType === 'team-lead');
|
|
284
|
+
return namedSession && soleLead;
|
|
285
|
+
}
|
|
286
|
+
|
|
273
287
|
// SSE clients for live updates
|
|
274
288
|
const clients = new Set();
|
|
275
289
|
|
|
@@ -931,8 +945,11 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
931
945
|
const cfg = loadTeamConfig(dir.name);
|
|
932
946
|
if (!cfg?.leadSessionId) continue;
|
|
933
947
|
const leaderId = cfg.leadSessionId;
|
|
934
|
-
//
|
|
948
|
+
// Remove the team-named duplicate before bailing on self-teams. Otherwise an
|
|
949
|
+
// auto-created session-<uuid> self-team dir leaves a duplicate session card whose
|
|
950
|
+
// id (session-<uuid>) resolves no messages, so switching to it shows a stale log.
|
|
935
951
|
if (sessionsMap.has(dir.name) && dir.name !== leaderId) sessionsMap.delete(dir.name);
|
|
952
|
+
if (isAutoSelfTeam(cfg)) continue;
|
|
936
953
|
const existing = sessionsMap.get(leaderId);
|
|
937
954
|
if (existing) {
|
|
938
955
|
existing.isTeam = true;
|
|
@@ -1170,7 +1187,7 @@ app.get('/api/projects/:encodedPath/tasks', (req, res) => {
|
|
|
1170
1187
|
app.get('/api/sessions/:sessionId/plan', async (req, res) => {
|
|
1171
1188
|
try {
|
|
1172
1189
|
const metadata = loadSessionMetadata();
|
|
1173
|
-
const meta = metadata[req.params.sessionId];
|
|
1190
|
+
const meta = metadata[req.params.sessionId] || metadata[resolveSessionId(req.params.sessionId)];
|
|
1174
1191
|
const slug = meta?.slug;
|
|
1175
1192
|
if (!slug) return res.status(404).json({ error: 'No plan found' });
|
|
1176
1193
|
|
|
@@ -1188,7 +1205,7 @@ app.get('/api/sessions/:sessionId/plan', async (req, res) => {
|
|
|
1188
1205
|
app.get('/api/sessions/:sessionId/loop', (req, res) => {
|
|
1189
1206
|
try {
|
|
1190
1207
|
const metadata = loadSessionMetadata();
|
|
1191
|
-
const meta = metadata[req.params.sessionId];
|
|
1208
|
+
const meta = metadata[req.params.sessionId] || metadata[resolveSessionId(req.params.sessionId)];
|
|
1192
1209
|
if (!meta?.jsonlPath) return res.json({ wakeups: [], crons: [] });
|
|
1193
1210
|
const state = refreshLoopInfoState(meta.jsonlPath);
|
|
1194
1211
|
const filtered = filterActiveLoopInfo(buildLoopInfoFromState(state));
|
|
@@ -1211,7 +1228,7 @@ function openInEditor(...targets) {
|
|
|
1211
1228
|
app.post('/api/sessions/:sessionId/plan/open', (req, res) => {
|
|
1212
1229
|
try {
|
|
1213
1230
|
const metadata = loadSessionMetadata();
|
|
1214
|
-
const meta = metadata[req.params.sessionId];
|
|
1231
|
+
const meta = metadata[req.params.sessionId] || metadata[resolveSessionId(req.params.sessionId)];
|
|
1215
1232
|
const slug = meta?.slug;
|
|
1216
1233
|
if (!slug) return res.status(404).json({ error: 'No plan found' });
|
|
1217
1234
|
|
|
@@ -1725,7 +1742,7 @@ app.get('/api/sessions/:sessionId/messages', (req, res) => {
|
|
|
1725
1742
|
const limit = Math.min(parseInt(req.query.limit, 10) || 10, 50);
|
|
1726
1743
|
const before = req.query.before || null;
|
|
1727
1744
|
const metadata = loadSessionMetadata();
|
|
1728
|
-
const meta = metadata[req.params.sessionId];
|
|
1745
|
+
const meta = metadata[req.params.sessionId] || metadata[resolveSessionId(req.params.sessionId)];
|
|
1729
1746
|
const jsonlPath = meta?.jsonlPath;
|
|
1730
1747
|
if (!jsonlPath) return res.json({ messages: [], hasMore: false, sessionId: req.params.sessionId });
|
|
1731
1748
|
let messages, hasMore;
|
|
@@ -1820,7 +1837,7 @@ app.get('/api/sessions/:sessionId/messages', (req, res) => {
|
|
|
1820
1837
|
|
|
1821
1838
|
app.get('/api/sessions/:sessionId/tool-result/:toolUseId', (req, res) => {
|
|
1822
1839
|
const metadata = loadSessionMetadata();
|
|
1823
|
-
const meta = metadata[req.params.sessionId];
|
|
1840
|
+
const meta = metadata[req.params.sessionId] || metadata[resolveSessionId(req.params.sessionId)];
|
|
1824
1841
|
const jsonlPath = meta?.jsonlPath;
|
|
1825
1842
|
if (!jsonlPath) return res.status(404).json({ error: 'session not found' });
|
|
1826
1843
|
const content = readFullToolResult(jsonlPath, req.params.toolUseId);
|
|
@@ -1927,7 +1944,7 @@ function buildToolStats(jsonlPath) {
|
|
|
1927
1944
|
|
|
1928
1945
|
app.get('/api/sessions/:sessionId/tool-stats', (req, res) => {
|
|
1929
1946
|
const metadata = loadSessionMetadata();
|
|
1930
|
-
const meta = metadata[req.params.sessionId];
|
|
1947
|
+
const meta = metadata[req.params.sessionId] || metadata[resolveSessionId(req.params.sessionId)];
|
|
1931
1948
|
const jsonlPath = meta?.jsonlPath;
|
|
1932
1949
|
if (!jsonlPath) return res.status(404).json({ error: 'session not found' });
|
|
1933
1950
|
try {
|
|
@@ -1941,7 +1958,7 @@ app.get('/api/sessions/:sessionId/tool-stats', (req, res) => {
|
|
|
1941
1958
|
|
|
1942
1959
|
app.get('/api/sessions/:sessionId/user-image/:msgUuid/:blockIndex', (req, res) => {
|
|
1943
1960
|
const metadata = loadSessionMetadata();
|
|
1944
|
-
const meta = metadata[req.params.sessionId];
|
|
1961
|
+
const meta = metadata[req.params.sessionId] || metadata[resolveSessionId(req.params.sessionId)];
|
|
1945
1962
|
const jsonlPath = meta?.jsonlPath;
|
|
1946
1963
|
if (!jsonlPath) return res.status(404).end();
|
|
1947
1964
|
const img = readUserImage(jsonlPath, req.params.msgUuid, req.params.blockIndex);
|
|
@@ -2023,40 +2040,6 @@ app.get('/api/tasks/all', async (req, res) => {
|
|
|
2023
2040
|
}
|
|
2024
2041
|
});
|
|
2025
2042
|
|
|
2026
|
-
// API: Add note to a task
|
|
2027
|
-
app.post('/api/tasks/:sessionId/:taskId/note', async (req, res) => {
|
|
2028
|
-
try {
|
|
2029
|
-
const { sessionId, taskId } = req.params;
|
|
2030
|
-
const { note } = req.body;
|
|
2031
|
-
|
|
2032
|
-
if (!note || !note.trim()) {
|
|
2033
|
-
return res.status(400).json({ error: 'Note cannot be empty' });
|
|
2034
|
-
}
|
|
2035
|
-
|
|
2036
|
-
const sessionDir = getCustomTaskDir(sessionId) || path.join(TASKS_DIR, sessionId);
|
|
2037
|
-
const taskPath = path.join(sessionDir, `${taskId}.json`);
|
|
2038
|
-
|
|
2039
|
-
if (!existsSync(taskPath)) {
|
|
2040
|
-
return res.status(404).json({ error: 'Task not found' });
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
|
-
// Read current task
|
|
2044
|
-
const task = JSON.parse(await fs.readFile(taskPath, 'utf8'));
|
|
2045
|
-
|
|
2046
|
-
// Append note to description
|
|
2047
|
-
const noteBlock = `\n\n---\n\n#### [Note added by user]\n\n${note.trim()}`;
|
|
2048
|
-
task.description = (task.description || '') + noteBlock;
|
|
2049
|
-
|
|
2050
|
-
// Write updated task
|
|
2051
|
-
await fs.writeFile(taskPath, JSON.stringify(task, null, 2));
|
|
2052
|
-
|
|
2053
|
-
res.json({ success: true, task });
|
|
2054
|
-
} catch (error) {
|
|
2055
|
-
console.error('Error adding note:', error);
|
|
2056
|
-
res.status(500).json({ error: 'Failed to add note' });
|
|
2057
|
-
}
|
|
2058
|
-
});
|
|
2059
|
-
|
|
2060
2043
|
// API: Update task fields (subject, description)
|
|
2061
2044
|
app.put('/api/tasks/:sessionId/:taskId', async (req, res) => {
|
|
2062
2045
|
try {
|