claude-code-kanban 1.12.0 → 1.14.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/index.html +571 -118
- package/server.js +155 -4
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
</style>
|
|
14
14
|
<script defer src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
15
15
|
<script defer src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
|
|
16
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/styles/github-dark.min.css" id="hljs-theme-dark">
|
|
17
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/styles/github.min.css" id="hljs-theme-light" disabled>
|
|
18
|
+
<script defer src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/highlight.min.js"></script>
|
|
16
19
|
<style>
|
|
17
20
|
:root {
|
|
18
21
|
--bg-deep: #101114;
|
|
@@ -877,8 +880,8 @@
|
|
|
877
880
|
}
|
|
878
881
|
|
|
879
882
|
.detail-close {
|
|
880
|
-
width:
|
|
881
|
-
height:
|
|
883
|
+
width: 34px;
|
|
884
|
+
height: 34px;
|
|
882
885
|
display: flex;
|
|
883
886
|
align-items: center;
|
|
884
887
|
justify-content: center;
|
|
@@ -896,8 +899,8 @@
|
|
|
896
899
|
}
|
|
897
900
|
|
|
898
901
|
.detail-close svg {
|
|
899
|
-
width:
|
|
900
|
-
height:
|
|
902
|
+
width: 20px;
|
|
903
|
+
height: 20px;
|
|
901
904
|
}
|
|
902
905
|
|
|
903
906
|
.detail-content {
|
|
@@ -933,6 +936,34 @@
|
|
|
933
936
|
font-family: var(--serif);
|
|
934
937
|
font-size: 22px;
|
|
935
938
|
line-height: 1.4;
|
|
939
|
+
cursor: pointer;
|
|
940
|
+
padding: 2px 4px;
|
|
941
|
+
margin: -2px -4px;
|
|
942
|
+
border-radius: 4px;
|
|
943
|
+
border: 1px solid transparent;
|
|
944
|
+
transition: border-color 0.15s ease;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
.detail-title:hover {
|
|
948
|
+
border-color: var(--border);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
.detail-title-input {
|
|
952
|
+
font-family: var(--serif);
|
|
953
|
+
font-size: 22px;
|
|
954
|
+
line-height: 1.4;
|
|
955
|
+
width: 100%;
|
|
956
|
+
padding: 2px 4px;
|
|
957
|
+
margin: -2px -4px;
|
|
958
|
+
background: var(--bg-elevated);
|
|
959
|
+
border: 1px solid var(--accent);
|
|
960
|
+
border-radius: 4px;
|
|
961
|
+
color: var(--text-primary);
|
|
962
|
+
box-shadow: 0 0 0 2px var(--accent-dim);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
.detail-title-input:focus {
|
|
966
|
+
outline: none;
|
|
936
967
|
}
|
|
937
968
|
|
|
938
969
|
.detail-status {
|
|
@@ -1008,15 +1039,99 @@
|
|
|
1008
1039
|
font-size: 14px;
|
|
1009
1040
|
line-height: 1.7;
|
|
1010
1041
|
color: var(--text-secondary);
|
|
1042
|
+
cursor: pointer;
|
|
1043
|
+
padding: 4px 6px;
|
|
1044
|
+
margin: -4px -6px;
|
|
1045
|
+
border-radius: 4px;
|
|
1046
|
+
border: 1px solid transparent;
|
|
1047
|
+
transition: border-color 0.15s ease;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
.detail-desc:hover {
|
|
1051
|
+
border-color: var(--border);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
.detail-desc-textarea {
|
|
1055
|
+
width: 100%;
|
|
1056
|
+
min-height: 120px;
|
|
1057
|
+
padding: 8px 10px;
|
|
1058
|
+
margin: -4px -6px;
|
|
1059
|
+
background: var(--bg-elevated);
|
|
1060
|
+
border: 1px solid var(--accent);
|
|
1061
|
+
border-radius: 4px;
|
|
1062
|
+
color: var(--text-primary);
|
|
1063
|
+
font-family: var(--mono);
|
|
1064
|
+
font-size: 13px;
|
|
1065
|
+
line-height: 1.6;
|
|
1066
|
+
resize: vertical;
|
|
1067
|
+
box-shadow: 0 0 0 2px var(--accent-dim);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
.detail-desc-textarea:focus {
|
|
1071
|
+
outline: none;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
.edit-actions {
|
|
1075
|
+
display: flex;
|
|
1076
|
+
gap: 8px;
|
|
1077
|
+
justify-content: flex-end;
|
|
1078
|
+
margin-top: 8px;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
.edit-actions button {
|
|
1082
|
+
padding: 6px 14px;
|
|
1083
|
+
border: none;
|
|
1084
|
+
border-radius: 4px;
|
|
1085
|
+
font-family: var(--mono);
|
|
1086
|
+
font-size: 11px;
|
|
1087
|
+
font-weight: 500;
|
|
1088
|
+
cursor: pointer;
|
|
1089
|
+
transition: all 0.15s ease;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
.edit-save {
|
|
1093
|
+
background: var(--accent);
|
|
1094
|
+
color: white;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
.edit-save:hover {
|
|
1098
|
+
filter: brightness(1.1);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
.edit-cancel {
|
|
1102
|
+
background: var(--bg-elevated);
|
|
1103
|
+
color: var(--text-secondary);
|
|
1104
|
+
border: 1px solid var(--border) !important;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
.edit-cancel:hover {
|
|
1108
|
+
color: var(--text-primary);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
.detail-deps {
|
|
1112
|
+
font-size: 14px;
|
|
1113
|
+
line-height: 1.7;
|
|
1114
|
+
color: var(--text-secondary);
|
|
1011
1115
|
}
|
|
1012
1116
|
|
|
1013
1117
|
.detail-desc pre {
|
|
1118
|
+
border-radius: 6px;
|
|
1119
|
+
overflow: hidden;
|
|
1120
|
+
margin: 12px 0;
|
|
1121
|
+
font-size: 12px;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
.detail-desc pre code.hljs {
|
|
1125
|
+
padding: 12px;
|
|
1126
|
+
border-radius: 6px;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
.detail-desc pre code {
|
|
1014
1130
|
background: var(--bg-elevated);
|
|
1015
1131
|
padding: 12px;
|
|
1016
1132
|
border-radius: 6px;
|
|
1133
|
+
display: block;
|
|
1017
1134
|
overflow-x: auto;
|
|
1018
|
-
margin: 12px 0;
|
|
1019
|
-
font-size: 12px;
|
|
1020
1135
|
}
|
|
1021
1136
|
|
|
1022
1137
|
.detail-desc code {
|
|
@@ -1026,11 +1141,6 @@
|
|
|
1026
1141
|
font-size: 0.9em;
|
|
1027
1142
|
}
|
|
1028
1143
|
|
|
1029
|
-
.detail-desc pre code {
|
|
1030
|
-
background: transparent;
|
|
1031
|
-
padding: 0;
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
1144
|
.detail-desc hr {
|
|
1035
1145
|
border: none;
|
|
1036
1146
|
border-top: 1px solid var(--border);
|
|
@@ -1366,6 +1476,19 @@
|
|
|
1366
1476
|
display: flex;
|
|
1367
1477
|
}
|
|
1368
1478
|
|
|
1479
|
+
.plan-modal-overlay {
|
|
1480
|
+
z-index: 10001;
|
|
1481
|
+
background: rgba(0, 0, 0, 0.6);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
.modal.plan-modal {
|
|
1485
|
+
width: 60vw;
|
|
1486
|
+
max-width: 60vw;
|
|
1487
|
+
max-height: 90vh;
|
|
1488
|
+
display: flex;
|
|
1489
|
+
flex-direction: column;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1369
1492
|
.modal {
|
|
1370
1493
|
background: var(--bg-surface);
|
|
1371
1494
|
border: 1px solid var(--border);
|
|
@@ -1533,6 +1656,11 @@
|
|
|
1533
1656
|
border-color: var(--text-muted);
|
|
1534
1657
|
}
|
|
1535
1658
|
|
|
1659
|
+
.btn:focus-visible {
|
|
1660
|
+
outline: 2px solid var(--accent);
|
|
1661
|
+
outline-offset: 2px;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1536
1664
|
/* Skip navigation */
|
|
1537
1665
|
.skip-link {
|
|
1538
1666
|
position: absolute;
|
|
@@ -1848,11 +1976,18 @@
|
|
|
1848
1976
|
<aside id="detail-panel" class="detail-panel">
|
|
1849
1977
|
<header class="detail-header">
|
|
1850
1978
|
<h3>Task Details</h3>
|
|
1851
|
-
<
|
|
1852
|
-
<
|
|
1853
|
-
<
|
|
1854
|
-
|
|
1855
|
-
|
|
1979
|
+
<div style="display: flex; gap: 8px; align-items: center;">
|
|
1980
|
+
<button id="delete-task-btn" class="icon-btn" title="Delete task (D)" aria-label="Delete task" style="color: #ef4444; border-color: #ef4444; display: none;">
|
|
1981
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1982
|
+
<path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
|
|
1983
|
+
</svg>
|
|
1984
|
+
</button>
|
|
1985
|
+
<button id="close-detail" class="detail-close" aria-label="Close detail panel">
|
|
1986
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1987
|
+
<path d="M6 18L18 6M6 6l12 12"/>
|
|
1988
|
+
</svg>
|
|
1989
|
+
</button>
|
|
1990
|
+
</div>
|
|
1856
1991
|
</header>
|
|
1857
1992
|
<div id="detail-content" class="detail-content"></div>
|
|
1858
1993
|
</aside>
|
|
@@ -1866,7 +2001,8 @@
|
|
|
1866
2001
|
let viewMode = 'session';
|
|
1867
2002
|
let sessionFilter = 'active';
|
|
1868
2003
|
let sessionLimit = '20';
|
|
1869
|
-
let filterProject =
|
|
2004
|
+
let filterProject = '__recent__'; // null = all, '__recent__' = last 24h, or project path
|
|
2005
|
+
let recentProjects = new Set();
|
|
1870
2006
|
let searchQuery = ''; // Search query for fuzzy search
|
|
1871
2007
|
let allTasksCache = []; // Cache all tasks for search
|
|
1872
2008
|
let bulkDeleteSessionId = null; // Track session for bulk delete
|
|
@@ -1895,7 +2031,7 @@
|
|
|
1895
2031
|
if (currentSessionId) params.set('session', currentSessionId);
|
|
1896
2032
|
if (sessionFilter !== 'active') params.set('filter', sessionFilter);
|
|
1897
2033
|
if (sessionLimit !== '20') params.set('limit', sessionLimit);
|
|
1898
|
-
if (filterProject) params.set('project', filterProject);
|
|
2034
|
+
if (filterProject && filterProject !== '__recent__') params.set('project', filterProject);
|
|
1899
2035
|
if (ownerFilter) params.set('owner', ownerFilter);
|
|
1900
2036
|
if (searchQuery) params.set('search', searchQuery);
|
|
1901
2037
|
const qs = params.toString();
|
|
@@ -1907,7 +2043,7 @@
|
|
|
1907
2043
|
history.replaceState(null, '', window.location.pathname);
|
|
1908
2044
|
sessionFilter = 'active';
|
|
1909
2045
|
sessionLimit = '20';
|
|
1910
|
-
filterProject =
|
|
2046
|
+
filterProject = '__recent__';
|
|
1911
2047
|
ownerFilter = '';
|
|
1912
2048
|
searchQuery = '';
|
|
1913
2049
|
viewMode = 'all';
|
|
@@ -2192,7 +2328,7 @@
|
|
|
2192
2328
|
const allTasks = await res.json();
|
|
2193
2329
|
let activeTasks = allTasks.filter(t => t.status === 'in_progress');
|
|
2194
2330
|
if (filterProject) {
|
|
2195
|
-
activeTasks = activeTasks.filter(t => t.project
|
|
2331
|
+
activeTasks = activeTasks.filter(t => matchesProjectFilter(t.project));
|
|
2196
2332
|
}
|
|
2197
2333
|
renderLiveUpdates(activeTasks);
|
|
2198
2334
|
} catch (error) {
|
|
@@ -2270,7 +2406,7 @@
|
|
|
2270
2406
|
const res = await fetch('/api/tasks/all');
|
|
2271
2407
|
let tasks = await res.json();
|
|
2272
2408
|
if (filterProject) {
|
|
2273
|
-
tasks = tasks.filter(t => t.project
|
|
2409
|
+
tasks = tasks.filter(t => matchesProjectFilter(t.project));
|
|
2274
2410
|
}
|
|
2275
2411
|
currentTasks = tasks;
|
|
2276
2412
|
updateUrl();
|
|
@@ -2290,9 +2426,10 @@
|
|
|
2290
2426
|
const completed = currentTasks.filter(t => t.status === 'completed').length;
|
|
2291
2427
|
const percent = totalTasks > 0 ? Math.round((completed / totalTasks) * 100) : 0;
|
|
2292
2428
|
|
|
2293
|
-
const
|
|
2294
|
-
|
|
2295
|
-
|
|
2429
|
+
const isFiltered = filterProject && filterProject !== '__recent__';
|
|
2430
|
+
const projectName = isFiltered ? filterProject.split(/[/\\]/).pop() : null;
|
|
2431
|
+
sessionTitle.textContent = isFiltered ? `Tasks: ${projectName}` : (filterProject === '__recent__' ? 'Recent Tasks' : 'All Tasks');
|
|
2432
|
+
sessionMeta.textContent = isFiltered
|
|
2296
2433
|
? `${totalTasks} tasks in this project`
|
|
2297
2434
|
: `${totalTasks} tasks across ${sessions.length} sessions`;
|
|
2298
2435
|
progressPercent.textContent = `${percent}%`;
|
|
@@ -2310,7 +2447,7 @@
|
|
|
2310
2447
|
filteredSessions = filteredSessions.filter(s => s.pending > 0 || s.inProgress > 0);
|
|
2311
2448
|
}
|
|
2312
2449
|
if (filterProject) {
|
|
2313
|
-
filteredSessions = filteredSessions.filter(s => s.project
|
|
2450
|
+
filteredSessions = filteredSessions.filter(s => matchesProjectFilter(s.project));
|
|
2314
2451
|
}
|
|
2315
2452
|
|
|
2316
2453
|
// Apply search filter
|
|
@@ -2389,14 +2526,14 @@
|
|
|
2389
2526
|
const memberCount = session.memberCount || 0;
|
|
2390
2527
|
|
|
2391
2528
|
return `
|
|
2392
|
-
<button onclick="fetchTasks('${session.id}')" class="session-item ${isActive ? 'active' : ''}" title="${tooltip}">
|
|
2529
|
+
<button onclick="fetchTasks('${session.id}')" data-session-id="${session.id}" class="session-item ${isActive ? 'active' : ''}" title="${tooltip}">
|
|
2393
2530
|
<div class="session-name">${escapeHtml(primaryName)}</div>
|
|
2394
2531
|
${secondaryName ? `<div class="session-secondary">${escapeHtml(secondaryName)}</div>` : ''}
|
|
2395
2532
|
${gitBranch ? `<div class="session-branch">${gitBranch}</div>` : ''}
|
|
2396
2533
|
<div class="session-progress">
|
|
2397
2534
|
<span class="session-indicators">
|
|
2398
2535
|
${isTeam ? `<span class="team-badge" title="${memberCount} team members"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>${memberCount}</span>` : ''}
|
|
2399
|
-
${isTeam ? `<span class="team-info-btn" onclick="event.stopPropagation();
|
|
2536
|
+
${(isTeam || session.project) ? `<span class="team-info-btn" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" title="View session info">ℹ</span>` : ''}
|
|
2400
2537
|
${hasInProgress ? '<span class="pulse"></span>' : ''}
|
|
2401
2538
|
</span>
|
|
2402
2539
|
<div class="progress-bar"><div class="progress-fill" style="width: ${percent}%"></div></div>
|
|
@@ -2530,7 +2667,7 @@
|
|
|
2530
2667
|
const card = document.querySelector(`.task-card[data-task-id="${selectedTaskId}"][data-session-id="${selectedSessionId}"]`)
|
|
2531
2668
|
|| document.querySelector(`.task-card[data-task-id="${selectedTaskId}"]`);
|
|
2532
2669
|
if (card) {
|
|
2533
|
-
card.classList.add('selected');
|
|
2670
|
+
if (focusZone === 'board') card.classList.add('selected');
|
|
2534
2671
|
} else {
|
|
2535
2672
|
selectedTaskId = null;
|
|
2536
2673
|
selectedSessionId = null;
|
|
@@ -2660,13 +2797,20 @@
|
|
|
2660
2797
|
selectSessionByIndex(selectedSessionIdx);
|
|
2661
2798
|
}
|
|
2662
2799
|
} else {
|
|
2800
|
+
// Session changed while in sidebar — reset stale selection
|
|
2801
|
+
if (selectedSessionId && selectedSessionId !== currentSessionId) {
|
|
2802
|
+
selectedTaskId = null;
|
|
2803
|
+
selectedSessionId = null;
|
|
2804
|
+
}
|
|
2663
2805
|
if (selectedTaskId) {
|
|
2664
|
-
const card = document.querySelector(`.task-card[data-task-id="${selectedTaskId}"][data-session-id="${selectedSessionId}"]`)
|
|
2665
|
-
|| document.querySelector(`.task-card[data-task-id="${selectedTaskId}"]`);
|
|
2806
|
+
const card = document.querySelector(`.task-card[data-task-id="${selectedTaskId}"][data-session-id="${selectedSessionId}"]`);
|
|
2666
2807
|
if (card) card.classList.add('selected');
|
|
2667
2808
|
} else {
|
|
2668
2809
|
navigateVertical(1);
|
|
2669
2810
|
}
|
|
2811
|
+
if (selectedTaskId && detailPanel.classList.contains('visible')) {
|
|
2812
|
+
showTaskDetail(selectedTaskId, selectedSessionId);
|
|
2813
|
+
}
|
|
2670
2814
|
}
|
|
2671
2815
|
}
|
|
2672
2816
|
|
|
@@ -2738,19 +2882,8 @@
|
|
|
2738
2882
|
|
|
2739
2883
|
detailContent.innerHTML = `
|
|
2740
2884
|
<div class="detail-section">
|
|
2741
|
-
<div
|
|
2742
|
-
|
|
2743
|
-
<div class="detail-label">Task #${task.id}</div>
|
|
2744
|
-
<h2 class="detail-title">${escapeHtml(task.subject)}</h2>
|
|
2745
|
-
</div>
|
|
2746
|
-
<div style="display: flex; gap: 8px;">
|
|
2747
|
-
<button id="delete-task-btn" class="icon-btn" title="Delete task (D)" aria-label="Delete task" style="color: #ef4444; border-color: #ef4444;">
|
|
2748
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
2749
|
-
<path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
|
|
2750
|
-
</svg>
|
|
2751
|
-
</button>
|
|
2752
|
-
</div>
|
|
2753
|
-
</div>
|
|
2885
|
+
<div class="detail-label">Task #${task.id}</div>
|
|
2886
|
+
<h2 class="detail-title">${escapeHtml(task.subject)}</h2>
|
|
2754
2887
|
</div>
|
|
2755
2888
|
|
|
2756
2889
|
<div class="detail-section" style="display: flex; gap: 12px; align-items: center;">
|
|
@@ -2774,7 +2907,7 @@
|
|
|
2774
2907
|
|
|
2775
2908
|
<div class="detail-section">
|
|
2776
2909
|
<div class="detail-label">Blocked By</div>
|
|
2777
|
-
<div class="detail-
|
|
2910
|
+
<div class="detail-deps">
|
|
2778
2911
|
${task.blockedBy && task.blockedBy.length > 0
|
|
2779
2912
|
? `<div class="detail-box blocked"><strong>Blocked by:</strong> ${task.blockedBy.map(id => '#' + id).join(', ')}</div>`
|
|
2780
2913
|
: '<em style="color: var(--text-muted); font-size: 13px;">No dependencies</em>'}
|
|
@@ -2783,7 +2916,7 @@
|
|
|
2783
2916
|
|
|
2784
2917
|
<div class="detail-section">
|
|
2785
2918
|
<div class="detail-label">Blocks</div>
|
|
2786
|
-
<div class="detail-
|
|
2919
|
+
<div class="detail-deps">
|
|
2787
2920
|
${task.blocks && task.blocks.length > 0
|
|
2788
2921
|
? `<div class="detail-box blocks"><strong>Blocks:</strong> ${task.blocks.map(id => '#' + id).join(', ')}</div>`
|
|
2789
2922
|
: '<em style="color: var(--text-muted); font-size: 13px;">No tasks blocked</em>'}
|
|
@@ -2800,7 +2933,111 @@
|
|
|
2800
2933
|
`;
|
|
2801
2934
|
|
|
2802
2935
|
// Setup button handlers
|
|
2803
|
-
document.getElementById('delete-task-btn')
|
|
2936
|
+
const deleteBtn = document.getElementById('delete-task-btn');
|
|
2937
|
+
deleteBtn.style.display = '';
|
|
2938
|
+
deleteBtn.onclick = () => deleteTask(task.id, actualSessionId);
|
|
2939
|
+
|
|
2940
|
+
// Setup inline editing
|
|
2941
|
+
const titleEl = detailContent.querySelector('.detail-title');
|
|
2942
|
+
if (titleEl) {
|
|
2943
|
+
titleEl.onclick = () => editTitle(titleEl, task, actualSessionId);
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
const descEl = detailContent.querySelector('.detail-desc');
|
|
2947
|
+
if (descEl) {
|
|
2948
|
+
descEl.onclick = () => editDescription(descEl, task, actualSessionId);
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
function editTitle(titleEl, task, sessionId) {
|
|
2953
|
+
if (titleEl.querySelector('input')) return;
|
|
2954
|
+
const input = document.createElement('input');
|
|
2955
|
+
input.type = 'text';
|
|
2956
|
+
input.className = 'detail-title-input';
|
|
2957
|
+
input.value = task.subject;
|
|
2958
|
+
|
|
2959
|
+
titleEl.replaceWith(input);
|
|
2960
|
+
input.focus();
|
|
2961
|
+
input.select();
|
|
2962
|
+
|
|
2963
|
+
const save = async () => {
|
|
2964
|
+
const val = input.value.trim();
|
|
2965
|
+
if (val && val !== task.subject) {
|
|
2966
|
+
await saveTaskField(task.id, sessionId, 'subject', val);
|
|
2967
|
+
} else {
|
|
2968
|
+
showTaskDetail(task.id, sessionId);
|
|
2969
|
+
}
|
|
2970
|
+
};
|
|
2971
|
+
|
|
2972
|
+
input.onkeydown = (e) => {
|
|
2973
|
+
if (e.key === 'Enter') { e.preventDefault(); save(); }
|
|
2974
|
+
if (e.key === 'Escape') showTaskDetail(task.id, sessionId);
|
|
2975
|
+
};
|
|
2976
|
+
input.onblur = () => save();
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
function editDescription(descEl, task, sessionId) {
|
|
2980
|
+
if (descEl.querySelector('textarea')) return;
|
|
2981
|
+
const wrapper = document.createElement('div');
|
|
2982
|
+
const textarea = document.createElement('textarea');
|
|
2983
|
+
textarea.className = 'detail-desc-textarea';
|
|
2984
|
+
textarea.value = task.description || '';
|
|
2985
|
+
textarea.rows = Math.max(5, (task.description || '').split('\n').length + 2);
|
|
2986
|
+
|
|
2987
|
+
const actions = document.createElement('div');
|
|
2988
|
+
actions.className = 'edit-actions';
|
|
2989
|
+
|
|
2990
|
+
const saveBtn = document.createElement('button');
|
|
2991
|
+
saveBtn.className = 'edit-save';
|
|
2992
|
+
saveBtn.textContent = 'Save';
|
|
2993
|
+
|
|
2994
|
+
const cancelBtn = document.createElement('button');
|
|
2995
|
+
cancelBtn.className = 'edit-cancel';
|
|
2996
|
+
cancelBtn.textContent = 'Cancel';
|
|
2997
|
+
|
|
2998
|
+
actions.append(cancelBtn, saveBtn);
|
|
2999
|
+
wrapper.append(textarea, actions);
|
|
3000
|
+
descEl.replaceWith(wrapper);
|
|
3001
|
+
textarea.focus();
|
|
3002
|
+
|
|
3003
|
+
const save = async () => {
|
|
3004
|
+
const val = textarea.value;
|
|
3005
|
+
if (val !== (task.description || '')) {
|
|
3006
|
+
await saveTaskField(task.id, sessionId, 'description', val);
|
|
3007
|
+
} else {
|
|
3008
|
+
showTaskDetail(task.id, sessionId);
|
|
3009
|
+
}
|
|
3010
|
+
};
|
|
3011
|
+
|
|
3012
|
+
saveBtn.onclick = save;
|
|
3013
|
+
cancelBtn.onclick = () => showTaskDetail(task.id, sessionId);
|
|
3014
|
+
textarea.onkeydown = (e) => {
|
|
3015
|
+
if (e.key === 'Escape') showTaskDetail(task.id, sessionId);
|
|
3016
|
+
};
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
async function saveTaskField(taskId, sessionId, field, value) {
|
|
3020
|
+
try {
|
|
3021
|
+
const res = await fetch(`/api/tasks/${sessionId}/${taskId}`, {
|
|
3022
|
+
method: 'PUT',
|
|
3023
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3024
|
+
body: JSON.stringify({ [field]: value })
|
|
3025
|
+
});
|
|
3026
|
+
|
|
3027
|
+
if (res.ok) {
|
|
3028
|
+
lastCurrentTasksHash = null;
|
|
3029
|
+
if (viewMode === 'all') {
|
|
3030
|
+
const tasksRes = await fetch('/api/tasks/all');
|
|
3031
|
+
currentTasks = await tasksRes.json();
|
|
3032
|
+
renderKanban();
|
|
3033
|
+
} else {
|
|
3034
|
+
await fetchTasks(sessionId);
|
|
3035
|
+
}
|
|
3036
|
+
showTaskDetail(taskId, sessionId);
|
|
3037
|
+
}
|
|
3038
|
+
} catch (error) {
|
|
3039
|
+
console.error('Failed to update task:', error);
|
|
3040
|
+
}
|
|
2804
3041
|
}
|
|
2805
3042
|
|
|
2806
3043
|
async function addNote(event, taskId, sessionId) {
|
|
@@ -2834,10 +3071,12 @@
|
|
|
2834
3071
|
|
|
2835
3072
|
function closeDetailPanel() {
|
|
2836
3073
|
detailPanel.classList.remove('visible');
|
|
3074
|
+
document.getElementById('delete-task-btn').style.display = 'none';
|
|
2837
3075
|
}
|
|
2838
3076
|
|
|
2839
3077
|
let deleteTaskId = null;
|
|
2840
3078
|
let deleteSessionId = null;
|
|
3079
|
+
let deleteModalKeyHandler = null;
|
|
2841
3080
|
|
|
2842
3081
|
function showBlockedTaskModal(task) {
|
|
2843
3082
|
const messageDiv = document.getElementById('blocked-task-message');
|
|
@@ -2890,15 +3129,31 @@
|
|
|
2890
3129
|
const modal = document.getElementById('delete-confirm-modal');
|
|
2891
3130
|
modal.classList.add('visible');
|
|
2892
3131
|
|
|
2893
|
-
|
|
2894
|
-
|
|
3132
|
+
const buttons = [
|
|
3133
|
+
document.getElementById('delete-cancel-btn'),
|
|
3134
|
+
document.getElementById('delete-confirm-btn')
|
|
3135
|
+
];
|
|
3136
|
+
let focusIdx = 1;
|
|
3137
|
+
buttons[focusIdx].focus();
|
|
3138
|
+
|
|
3139
|
+
deleteModalKeyHandler = (e) => {
|
|
2895
3140
|
if (e.key === 'Escape') {
|
|
2896
3141
|
e.preventDefault();
|
|
2897
3142
|
closeDeleteConfirmModal();
|
|
2898
|
-
|
|
3143
|
+
} else if (e.key === 'ArrowLeft' || e.key === 'h') {
|
|
3144
|
+
e.preventDefault();
|
|
3145
|
+
focusIdx = 0;
|
|
3146
|
+
buttons[focusIdx].focus();
|
|
3147
|
+
} else if (e.key === 'ArrowRight' || e.key === 'l') {
|
|
3148
|
+
e.preventDefault();
|
|
3149
|
+
focusIdx = 1;
|
|
3150
|
+
buttons[focusIdx].focus();
|
|
3151
|
+
} else if (e.key === 'Enter') {
|
|
3152
|
+
e.preventDefault();
|
|
3153
|
+
buttons[focusIdx].click();
|
|
2899
3154
|
}
|
|
2900
3155
|
};
|
|
2901
|
-
document.addEventListener('keydown',
|
|
3156
|
+
document.addEventListener('keydown', deleteModalKeyHandler);
|
|
2902
3157
|
}
|
|
2903
3158
|
|
|
2904
3159
|
function closeDeleteConfirmModal() {
|
|
@@ -2906,6 +3161,10 @@
|
|
|
2906
3161
|
modal.classList.remove('visible');
|
|
2907
3162
|
deleteTaskId = null;
|
|
2908
3163
|
deleteSessionId = null;
|
|
3164
|
+
if (deleteModalKeyHandler) {
|
|
3165
|
+
document.removeEventListener('keydown', deleteModalKeyHandler);
|
|
3166
|
+
deleteModalKeyHandler = null;
|
|
3167
|
+
}
|
|
2909
3168
|
}
|
|
2910
3169
|
|
|
2911
3170
|
async function confirmDelete() {
|
|
@@ -3008,6 +3267,20 @@
|
|
|
3008
3267
|
setFocusZone('board');
|
|
3009
3268
|
return;
|
|
3010
3269
|
}
|
|
3270
|
+
if (e.key === 'p' || e.key === 'P') {
|
|
3271
|
+
e.preventDefault();
|
|
3272
|
+
const highlighted = sessionsList.querySelector('.session-item.kb-selected');
|
|
3273
|
+
const sid = highlighted?.dataset.sessionId || currentSessionId;
|
|
3274
|
+
if (sid) openPlanForSession(sid);
|
|
3275
|
+
return;
|
|
3276
|
+
}
|
|
3277
|
+
if (e.key === 'i' || e.key === 'I') {
|
|
3278
|
+
e.preventDefault();
|
|
3279
|
+
const highlighted = sessionsList.querySelector('.session-item.kb-selected');
|
|
3280
|
+
const sid = highlighted?.dataset.sessionId || currentSessionId;
|
|
3281
|
+
if (sid) showSessionInfoModal(sid);
|
|
3282
|
+
return;
|
|
3283
|
+
}
|
|
3011
3284
|
if (e.key === '?' || (e.key === '/' && e.shiftKey)) {
|
|
3012
3285
|
e.preventDefault();
|
|
3013
3286
|
showHelpModal();
|
|
@@ -3050,18 +3323,23 @@
|
|
|
3050
3323
|
closeDetailPanel();
|
|
3051
3324
|
}
|
|
3052
3325
|
|
|
3053
|
-
if (
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3326
|
+
if (e.key === 'p' || e.key === 'P') {
|
|
3327
|
+
e.preventDefault();
|
|
3328
|
+
const sid = selectedSessionId || currentSessionId;
|
|
3329
|
+
if (sid) openPlanForSession(sid);
|
|
3330
|
+
return;
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
if (e.key === 'i' || e.key === 'I') {
|
|
3334
|
+
e.preventDefault();
|
|
3335
|
+
const sid = selectedSessionId || currentSessionId;
|
|
3336
|
+
if (sid) showSessionInfoModal(sid);
|
|
3337
|
+
return;
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
if ((e.key === 'd' || e.key === 'D') && selectedTaskId) {
|
|
3341
|
+
e.preventDefault();
|
|
3342
|
+
deleteTask(selectedTaskId, selectedSessionId || currentSessionId);
|
|
3065
3343
|
}
|
|
3066
3344
|
|
|
3067
3345
|
if (e.key === '?' || (e.key === '/' && e.shiftKey)) {
|
|
@@ -3178,6 +3456,12 @@
|
|
|
3178
3456
|
fetchSessions();
|
|
3179
3457
|
}
|
|
3180
3458
|
|
|
3459
|
+
function matchesProjectFilter(project) {
|
|
3460
|
+
if (!filterProject) return true;
|
|
3461
|
+
if (filterProject === '__recent__') return recentProjects.has(project);
|
|
3462
|
+
return project === filterProject;
|
|
3463
|
+
}
|
|
3464
|
+
|
|
3181
3465
|
function filterByProject(project) {
|
|
3182
3466
|
filterProject = project || null;
|
|
3183
3467
|
updateUrl();
|
|
@@ -3186,15 +3470,30 @@
|
|
|
3186
3470
|
showAllTasks();
|
|
3187
3471
|
}
|
|
3188
3472
|
|
|
3189
|
-
function updateProjectDropdown() {
|
|
3473
|
+
async function updateProjectDropdown() {
|
|
3190
3474
|
const dropdown = document.getElementById('project-filter');
|
|
3191
|
-
|
|
3475
|
+
let projects;
|
|
3476
|
+
try {
|
|
3477
|
+
const res = await fetch('/api/projects');
|
|
3478
|
+
projects = await res.json();
|
|
3479
|
+
} catch (e) {
|
|
3480
|
+
projects = [...new Set(sessions.map(s => s.project).filter(Boolean))].sort()
|
|
3481
|
+
.map(p => ({ path: p, modifiedAt: null }));
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3484
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
3485
|
+
recentProjects = new Set(
|
|
3486
|
+
projects.filter(p => p.modifiedAt && new Date(p.modifiedAt).getTime() > cutoff).map(p => p.path)
|
|
3487
|
+
);
|
|
3192
3488
|
|
|
3193
|
-
|
|
3489
|
+
const recentSelected = filterProject === '__recent__' ? ' selected' : '';
|
|
3490
|
+
dropdown.innerHTML =
|
|
3491
|
+
'<option value="">All Projects</option>' +
|
|
3492
|
+
`<option value="__recent__"${recentSelected}>Recent (24h)</option>` +
|
|
3194
3493
|
projects.map(p => {
|
|
3195
|
-
const name = p.split(
|
|
3196
|
-
const selected = p === filterProject ? ' selected' : '';
|
|
3197
|
-
return `<option value="${p}"${selected} title="${escapeHtml(p)}">${escapeHtml(name)}</option>`;
|
|
3494
|
+
const name = p.path.split(/[/\\]/).pop();
|
|
3495
|
+
const selected = p.path === filterProject ? ' selected' : '';
|
|
3496
|
+
return `<option value="${escapeHtml(p.path)}"${selected} title="${escapeHtml(p.path)}">${escapeHtml(name)}</option>`;
|
|
3198
3497
|
}).join('');
|
|
3199
3498
|
}
|
|
3200
3499
|
|
|
@@ -3210,6 +3509,15 @@
|
|
|
3210
3509
|
localStorage.setItem('theme', 'light');
|
|
3211
3510
|
}
|
|
3212
3511
|
updateThemeIcon();
|
|
3512
|
+
syncHljsTheme();
|
|
3513
|
+
}
|
|
3514
|
+
|
|
3515
|
+
function syncHljsTheme() {
|
|
3516
|
+
const isLight = document.body.classList.contains('light');
|
|
3517
|
+
const dark = document.getElementById('hljs-theme-dark');
|
|
3518
|
+
const light = document.getElementById('hljs-theme-light');
|
|
3519
|
+
if (dark) dark.disabled = isLight;
|
|
3520
|
+
if (light) light.disabled = !isLight;
|
|
3213
3521
|
}
|
|
3214
3522
|
|
|
3215
3523
|
function updateThemeIcon() {
|
|
@@ -3231,6 +3539,7 @@
|
|
|
3231
3539
|
}
|
|
3232
3540
|
// If no saved preference, system prefers-color-scheme CSS handles it
|
|
3233
3541
|
updateThemeIcon();
|
|
3542
|
+
syncHljsTheme();
|
|
3234
3543
|
}
|
|
3235
3544
|
|
|
3236
3545
|
function toggleSidebar() {
|
|
@@ -3239,6 +3548,7 @@
|
|
|
3239
3548
|
localStorage.setItem('sidebar-collapsed', collapsed);
|
|
3240
3549
|
if (collapsed) {
|
|
3241
3550
|
sidebar.style.width = '';
|
|
3551
|
+
if (focusZone === 'sidebar') setFocusZone('board');
|
|
3242
3552
|
} else {
|
|
3243
3553
|
const w = getComputedStyle(sidebar).getPropertyValue('--sidebar-width');
|
|
3244
3554
|
if (w) sidebar.style.width = w;
|
|
@@ -3294,68 +3604,127 @@
|
|
|
3294
3604
|
document.getElementById('session-limit').value = sessionLimit;
|
|
3295
3605
|
}
|
|
3296
3606
|
|
|
3297
|
-
async function
|
|
3607
|
+
async function showSessionInfoModal(sessionId) {
|
|
3298
3608
|
const session = sessions.find(s => s.id === sessionId);
|
|
3299
|
-
if (!session
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3609
|
+
if (!session) return;
|
|
3610
|
+
|
|
3611
|
+
const promises = [];
|
|
3612
|
+
|
|
3613
|
+
// Fetch team config
|
|
3614
|
+
let teamConfig = null;
|
|
3615
|
+
if (session.isTeam) {
|
|
3616
|
+
promises.push(
|
|
3617
|
+
fetch(`/api/teams/${sessionId}`).then(r => r.ok ? r.json() : null).catch(() => null)
|
|
3618
|
+
.then(data => { teamConfig = data; })
|
|
3619
|
+
);
|
|
3307
3620
|
}
|
|
3621
|
+
|
|
3622
|
+
// Fetch plan
|
|
3623
|
+
let planContent = null;
|
|
3624
|
+
promises.push(
|
|
3625
|
+
fetch(`/api/sessions/${sessionId}/plan`).then(r => r.ok ? r.json() : null).catch(() => null)
|
|
3626
|
+
.then(data => { planContent = data?.content || null; })
|
|
3627
|
+
);
|
|
3628
|
+
|
|
3629
|
+
await Promise.all(promises);
|
|
3630
|
+
|
|
3631
|
+
const tasks = currentSessionId === sessionId ? currentTasks : [];
|
|
3632
|
+
_planSessionId = sessionId;
|
|
3633
|
+
showInfoModal(session, teamConfig, tasks, planContent);
|
|
3308
3634
|
}
|
|
3309
3635
|
|
|
3310
|
-
|
|
3636
|
+
let _pendingPlanContent = null;
|
|
3637
|
+
|
|
3638
|
+
function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
3311
3639
|
const modal = document.getElementById('team-modal');
|
|
3312
3640
|
const titleEl = document.getElementById('team-modal-title');
|
|
3313
3641
|
const bodyEl = document.getElementById('team-modal-body');
|
|
3314
3642
|
|
|
3315
|
-
titleEl.textContent =
|
|
3643
|
+
titleEl.textContent = teamConfig
|
|
3644
|
+
? `Team: ${teamConfig.team_name || teamConfig.name || 'Unknown'}`
|
|
3645
|
+
: (session.name || session.slug || session.id);
|
|
3316
3646
|
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3647
|
+
let html = '';
|
|
3648
|
+
|
|
3649
|
+
// Session & project details as compact key-value rows
|
|
3650
|
+
const infoRows = [];
|
|
3651
|
+
infoRows.push(['Session', `<span style="font-family: 'IBM Plex Mono', monospace; font-size: 11px; user-select: all;">${escapeHtml(session.id)}</span>`]);
|
|
3652
|
+
if (session.modifiedAt) {
|
|
3653
|
+
const d = new Date(session.modifiedAt);
|
|
3654
|
+
infoRows.push(['Last Modified', `${formatDate(session.modifiedAt)} <span style="font-size: 10px; color: var(--text-tertiary);">(${d.toLocaleString()})</span>`]);
|
|
3655
|
+
}
|
|
3656
|
+
if (session.project) {
|
|
3657
|
+
const projectName = session.project.split(/[/\\]/).pop();
|
|
3658
|
+
infoRows.push(['Project', `${escapeHtml(projectName)}<br><span style="font-size: 10px; color: var(--text-tertiary);">${escapeHtml(session.project)}</span>`]);
|
|
3659
|
+
if (session.gitBranch) {
|
|
3660
|
+
infoRows.push(['Branch', escapeHtml(session.gitBranch)]);
|
|
3661
|
+
}
|
|
3662
|
+
if (session.description) {
|
|
3663
|
+
infoRows.push(['Description', escapeHtml(session.description)]);
|
|
3321
3664
|
}
|
|
3665
|
+
}
|
|
3666
|
+
html += `<div class="team-modal-meta" style="margin-bottom: 16px; display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; align-items: baseline;">`;
|
|
3667
|
+
infoRows.forEach(([label, value]) => {
|
|
3668
|
+
html += `<span style="font-weight: 500; color: var(--text-secondary); font-size: 12px; white-space: nowrap;">${label}</span><span>${value}</span>`;
|
|
3322
3669
|
});
|
|
3670
|
+
html += `</div>`;
|
|
3671
|
+
|
|
3672
|
+
if (planContent) {
|
|
3673
|
+
_pendingPlanContent = planContent;
|
|
3674
|
+
const titleMatch = planContent.match(/^#\s+(.+)$/m);
|
|
3675
|
+
const planTitle = titleMatch ? titleMatch[1].trim() : null;
|
|
3676
|
+
html += `<div onclick="openPlanModal()" style="margin-bottom: 16px; padding: 10px 14px; background: var(--bg-elevated); border: 1px solid var(--border); border-radius: 8px; cursor: pointer; display: flex; align-items: center; gap: 10px; transition: all 0.15s ease;" onmouseover="this.style.borderColor='var(--accent)';this.style.background='var(--bg-hover)'" onmouseout="this.style.borderColor='var(--border)';this.style.background='var(--bg-elevated)'">
|
|
3677
|
+
<span style="font-size: 14px;">📋</span>
|
|
3678
|
+
<div style="flex: 1; min-width: 0;">
|
|
3679
|
+
<div style="font-size: 11px; font-weight: 500; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px;">Plan</div>
|
|
3680
|
+
${planTitle ? `<div style="font-size: 13px; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${escapeHtml(planTitle)}</div>` : ''}
|
|
3681
|
+
</div>
|
|
3682
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="var(--text-muted)" stroke-width="2" style="width: 16px; height: 16px; flex-shrink: 0;"><path d="M9 18l6-6-6-6"/></svg>
|
|
3683
|
+
</div>`;
|
|
3684
|
+
}
|
|
3323
3685
|
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3686
|
+
// Team info section
|
|
3687
|
+
if (teamConfig) {
|
|
3688
|
+
const ownerCounts = {};
|
|
3689
|
+
tasks.forEach(t => {
|
|
3690
|
+
if (t.owner) ownerCounts[t.owner] = (ownerCounts[t.owner] || 0) + 1;
|
|
3691
|
+
});
|
|
3327
3692
|
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
}
|
|
3693
|
+
const members = teamConfig.members || [];
|
|
3694
|
+
const description = teamConfig.description || '';
|
|
3695
|
+
const lead = members.find(m => m.agentType === 'team-lead' || m.name === 'team-lead');
|
|
3332
3696
|
|
|
3333
|
-
|
|
3697
|
+
if (description) {
|
|
3698
|
+
html += `<div class="team-modal-desc">"${escapeHtml(description)}"</div>`;
|
|
3699
|
+
}
|
|
3334
3700
|
|
|
3335
|
-
|
|
3336
|
-
const taskCount = ownerCounts[member.name] || 0;
|
|
3337
|
-
html += `
|
|
3338
|
-
<div class="team-member-card">
|
|
3339
|
-
<div class="member-name">🟢 ${escapeHtml(member.name)}</div>
|
|
3340
|
-
<div class="member-detail">Role: ${escapeHtml(member.agentType || 'unknown')}</div>
|
|
3341
|
-
${member.model ? `<div class="member-detail">Model: ${escapeHtml(member.model)}</div>` : ''}
|
|
3342
|
-
<div class="member-tasks">Tasks: ${taskCount} assigned</div>
|
|
3343
|
-
</div>
|
|
3344
|
-
`;
|
|
3345
|
-
});
|
|
3701
|
+
html += `<div style="font-size: 12px; font-weight: 500; color: var(--text-secondary); margin-bottom: 10px;">Members (${members.length})</div>`;
|
|
3346
3702
|
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3703
|
+
members.forEach(member => {
|
|
3704
|
+
const taskCount = ownerCounts[member.name] || 0;
|
|
3705
|
+
html += `
|
|
3706
|
+
<div class="team-member-card">
|
|
3707
|
+
<div class="member-name">🟢 ${escapeHtml(member.name)}</div>
|
|
3708
|
+
<div class="member-detail">Role: ${escapeHtml(member.agentType || 'unknown')}</div>
|
|
3709
|
+
${member.model ? `<div class="member-detail">Model: ${escapeHtml(member.model)}</div>` : ''}
|
|
3710
|
+
<div class="member-tasks">Tasks: ${taskCount} assigned</div>
|
|
3711
|
+
</div>
|
|
3712
|
+
`;
|
|
3713
|
+
});
|
|
3714
|
+
|
|
3715
|
+
const metaParts = [];
|
|
3716
|
+
if (teamConfig.created_at) {
|
|
3717
|
+
metaParts.push(`Created: ${new Date(teamConfig.created_at).toLocaleString()}`);
|
|
3718
|
+
}
|
|
3719
|
+
if (lead) {
|
|
3720
|
+
metaParts.push(`Lead: ${lead.name}`);
|
|
3721
|
+
}
|
|
3722
|
+
if (teamConfig.working_dir) {
|
|
3723
|
+
metaParts.push(`Working dir: ${teamConfig.working_dir}`);
|
|
3724
|
+
}
|
|
3725
|
+
if (metaParts.length > 0) {
|
|
3726
|
+
html += `<div class="team-modal-meta">${metaParts.map(p => escapeHtml(p)).join('<br>')}</div>`;
|
|
3727
|
+
}
|
|
3359
3728
|
}
|
|
3360
3729
|
|
|
3361
3730
|
bodyEl.innerHTML = html;
|
|
@@ -3363,6 +3732,7 @@
|
|
|
3363
3732
|
|
|
3364
3733
|
const keyHandler = (e) => {
|
|
3365
3734
|
if (e.key === 'Escape') {
|
|
3735
|
+
if (document.getElementById('plan-modal').classList.contains('visible')) return;
|
|
3366
3736
|
e.preventDefault();
|
|
3367
3737
|
closeTeamModal();
|
|
3368
3738
|
document.removeEventListener('keydown', keyHandler);
|
|
@@ -3375,6 +3745,45 @@
|
|
|
3375
3745
|
document.getElementById('team-modal').classList.remove('visible');
|
|
3376
3746
|
}
|
|
3377
3747
|
|
|
3748
|
+
let _planSessionId = null;
|
|
3749
|
+
|
|
3750
|
+
function openPlanForSession(sid) {
|
|
3751
|
+
fetch(`/api/sessions/${sid}/plan`).then(r => r.ok ? r.json() : null).catch(() => null)
|
|
3752
|
+
.then(data => {
|
|
3753
|
+
if (data?.content) {
|
|
3754
|
+
_pendingPlanContent = data.content;
|
|
3755
|
+
_planSessionId = sid;
|
|
3756
|
+
openPlanModal();
|
|
3757
|
+
}
|
|
3758
|
+
});
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
function openPlanModal() {
|
|
3762
|
+
if (!_pendingPlanContent) return;
|
|
3763
|
+
const body = document.getElementById('plan-modal-body');
|
|
3764
|
+
body.innerHTML = DOMPurify.sanitize(marked.parse(_pendingPlanContent));
|
|
3765
|
+
document.getElementById('plan-modal').classList.add('visible');
|
|
3766
|
+
|
|
3767
|
+
const keyHandler = (e) => {
|
|
3768
|
+
if (e.key === 'Escape') {
|
|
3769
|
+
e.preventDefault();
|
|
3770
|
+
e.stopPropagation();
|
|
3771
|
+
closePlanModal();
|
|
3772
|
+
document.removeEventListener('keydown', keyHandler, true);
|
|
3773
|
+
}
|
|
3774
|
+
};
|
|
3775
|
+
document.addEventListener('keydown', keyHandler, true);
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3778
|
+
function closePlanModal() {
|
|
3779
|
+
document.getElementById('plan-modal').classList.remove('visible');
|
|
3780
|
+
}
|
|
3781
|
+
|
|
3782
|
+
function openPlanInEditor() {
|
|
3783
|
+
if (!_planSessionId) return;
|
|
3784
|
+
fetch(`/api/sessions/${_planSessionId}/plan/open`, { method: 'POST' }).catch(() => {});
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3378
3787
|
function updateOwnerFilter() {
|
|
3379
3788
|
const bar = document.getElementById('owner-filter-bar');
|
|
3380
3789
|
const select = document.getElementById('owner-filter');
|
|
@@ -3416,6 +3825,23 @@
|
|
|
3416
3825
|
|
|
3417
3826
|
// Init
|
|
3418
3827
|
loadTheme();
|
|
3828
|
+
|
|
3829
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
3830
|
+
if (typeof marked !== 'undefined' && typeof hljs !== 'undefined') {
|
|
3831
|
+
const renderer = new marked.Renderer();
|
|
3832
|
+
renderer.code = function({ text, lang }) {
|
|
3833
|
+
let highlighted;
|
|
3834
|
+
if (lang && hljs.getLanguage(lang)) {
|
|
3835
|
+
highlighted = hljs.highlight(text, { language: lang }).value;
|
|
3836
|
+
} else {
|
|
3837
|
+
highlighted = hljs.highlightAuto(text).value;
|
|
3838
|
+
}
|
|
3839
|
+
return `<pre><code class="hljs language-${escapeHtml(lang || '')}">${highlighted}</code></pre>`;
|
|
3840
|
+
};
|
|
3841
|
+
marked.use({ renderer });
|
|
3842
|
+
}
|
|
3843
|
+
});
|
|
3844
|
+
|
|
3419
3845
|
loadSidebarState();
|
|
3420
3846
|
initSidebarResize();
|
|
3421
3847
|
fetch('/api/version').then(r => r.json()).then(d => {
|
|
@@ -3425,7 +3851,7 @@
|
|
|
3425
3851
|
const urlState = getUrlState();
|
|
3426
3852
|
sessionFilter = urlState.filter || 'active';
|
|
3427
3853
|
sessionLimit = urlState.limit || '20';
|
|
3428
|
-
filterProject = urlState.project ||
|
|
3854
|
+
filterProject = urlState.project || '__recent__';
|
|
3429
3855
|
ownerFilter = urlState.owner || '';
|
|
3430
3856
|
searchQuery = urlState.search || '';
|
|
3431
3857
|
|
|
@@ -3449,7 +3875,7 @@
|
|
|
3449
3875
|
const s = getUrlState();
|
|
3450
3876
|
sessionFilter = s.filter || 'active';
|
|
3451
3877
|
sessionLimit = s.limit || '20';
|
|
3452
|
-
filterProject = s.project ||
|
|
3878
|
+
filterProject = s.project || '__recent__';
|
|
3453
3879
|
ownerFilter = s.owner || '';
|
|
3454
3880
|
searchQuery = s.search || '';
|
|
3455
3881
|
loadPreferences();
|
|
@@ -3520,6 +3946,14 @@
|
|
|
3520
3946
|
<div>
|
|
3521
3947
|
<h4 style="margin: 0 0 8px 0; color: var(--text-primary); font-size: 14px; font-weight: 600;">Task Actions</h4>
|
|
3522
3948
|
<table style="width: 100%; font-size: 13px;">
|
|
3949
|
+
<tr>
|
|
3950
|
+
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">P</kbd></td>
|
|
3951
|
+
<td style="padding: 4px 0; color: var(--text-primary);">Open session plan</td>
|
|
3952
|
+
</tr>
|
|
3953
|
+
<tr>
|
|
3954
|
+
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">I</kbd></td>
|
|
3955
|
+
<td style="padding: 4px 0; color: var(--text-primary);">Open session info</td>
|
|
3956
|
+
</tr>
|
|
3523
3957
|
<tr>
|
|
3524
3958
|
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">D</kbd></td>
|
|
3525
3959
|
<td style="padding: 4px 0; color: var(--text-primary);">Delete selected task</td>
|
|
@@ -3549,8 +3983,8 @@
|
|
|
3549
3983
|
<p id="delete-confirm-message" style="margin: 0; color: var(--text-primary);"></p>
|
|
3550
3984
|
</div>
|
|
3551
3985
|
<div class="modal-footer">
|
|
3552
|
-
<button class="btn btn-secondary" onclick="closeDeleteConfirmModal()">Cancel</button>
|
|
3553
|
-
<button class="btn btn-primary" onclick="confirmDelete()" style="background: #ef4444; border-color: #ef4444;">Delete</button>
|
|
3986
|
+
<button id="delete-cancel-btn" class="btn btn-secondary" onclick="closeDeleteConfirmModal()">Cancel</button>
|
|
3987
|
+
<button id="delete-confirm-btn" class="btn btn-primary" onclick="confirmDelete()" style="background: #ef4444; border-color: #ef4444;">Delete</button>
|
|
3554
3988
|
</div>
|
|
3555
3989
|
</div>
|
|
3556
3990
|
</div>
|
|
@@ -3616,6 +4050,25 @@
|
|
|
3616
4050
|
</div>
|
|
3617
4051
|
</div>
|
|
3618
4052
|
|
|
4053
|
+
<!-- Plan Modal (stacked on top of info modal) -->
|
|
4054
|
+
<div id="plan-modal" class="modal-overlay plan-modal-overlay" onclick="closePlanModal()">
|
|
4055
|
+
<div class="modal plan-modal" onclick="event.stopPropagation()">
|
|
4056
|
+
<div class="modal-header">
|
|
4057
|
+
<h3 id="plan-modal-title" class="modal-title">Plan</h3>
|
|
4058
|
+
<button class="btn btn-secondary" style="padding: 4px 10px; font-size: 11px; margin-left: auto; margin-right: 12px;" onclick="openPlanInEditor()">Open in Editor</button>
|
|
4059
|
+
<button class="modal-close" aria-label="Close dialog" onclick="closePlanModal()">
|
|
4060
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
4061
|
+
<path d="M18 6L6 18M6 6l12 12"/>
|
|
4062
|
+
</svg>
|
|
4063
|
+
</button>
|
|
4064
|
+
</div>
|
|
4065
|
+
<div id="plan-modal-body" class="modal-body detail-desc" style="overflow-y: auto; flex: 1;"></div>
|
|
4066
|
+
<div class="modal-footer">
|
|
4067
|
+
<button class="btn btn-primary" onclick="closePlanModal()">Close</button>
|
|
4068
|
+
</div>
|
|
4069
|
+
</div>
|
|
4070
|
+
</div>
|
|
4071
|
+
|
|
3619
4072
|
<!-- Blocked Task Warning Modal -->
|
|
3620
4073
|
<div id="blocked-task-modal" class="modal-overlay" onclick="closeBlockedTaskModal()">
|
|
3621
4074
|
<div class="modal" onclick="event.stopPropagation()" style="max-width: 450px;">
|
package/server.js
CHANGED
|
@@ -31,6 +31,7 @@ const CLAUDE_DIR = getClaudeDir();
|
|
|
31
31
|
const TASKS_DIR = path.join(CLAUDE_DIR, 'tasks');
|
|
32
32
|
const PROJECTS_DIR = path.join(CLAUDE_DIR, 'projects');
|
|
33
33
|
const TEAMS_DIR = path.join(CLAUDE_DIR, 'teams');
|
|
34
|
+
const PLANS_DIR = path.join(CLAUDE_DIR, 'plans');
|
|
34
35
|
|
|
35
36
|
function isTeamSession(sessionId) {
|
|
36
37
|
return existsSync(path.join(TEAMS_DIR, sessionId, 'config.json'));
|
|
@@ -143,20 +144,35 @@ function loadSessionMetadata() {
|
|
|
143
144
|
|
|
144
145
|
// Find all .jsonl files (session logs)
|
|
145
146
|
const files = readdirSync(projectPath).filter(f => f.endsWith('.jsonl'));
|
|
147
|
+
const sessionIds = [];
|
|
146
148
|
|
|
149
|
+
// First pass: read all JSONL files
|
|
150
|
+
let resolvedProjectPath = null;
|
|
147
151
|
for (const file of files) {
|
|
148
152
|
const sessionId = file.replace('.jsonl', '');
|
|
149
153
|
const jsonlPath = path.join(projectPath, file);
|
|
150
|
-
|
|
151
|
-
// Read customTitle, slug, and actual project path from JSONL
|
|
152
154
|
const sessionInfo = readSessionInfoFromJsonl(jsonlPath);
|
|
153
155
|
|
|
156
|
+
if (sessionInfo.projectPath && !resolvedProjectPath) {
|
|
157
|
+
resolvedProjectPath = sessionInfo.projectPath;
|
|
158
|
+
}
|
|
159
|
+
|
|
154
160
|
metadata[sessionId] = {
|
|
155
161
|
customTitle: sessionInfo.customTitle,
|
|
156
162
|
slug: sessionInfo.slug,
|
|
157
163
|
project: sessionInfo.projectPath || null,
|
|
158
164
|
jsonlPath: jsonlPath
|
|
159
165
|
};
|
|
166
|
+
sessionIds.push(sessionId);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Second pass: fill in missing project paths from siblings
|
|
170
|
+
if (resolvedProjectPath) {
|
|
171
|
+
for (const sid of sessionIds) {
|
|
172
|
+
if (!metadata[sid].project) {
|
|
173
|
+
metadata[sid].project = resolvedProjectPath;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
160
176
|
}
|
|
161
177
|
|
|
162
178
|
// Also check sessions-index.json for custom names (if /rename was used)
|
|
@@ -167,8 +183,15 @@ function loadSessionMetadata() {
|
|
|
167
183
|
const entries = indexData.entries || [];
|
|
168
184
|
|
|
169
185
|
for (const entry of entries) {
|
|
170
|
-
if (entry.sessionId
|
|
171
|
-
|
|
186
|
+
if (entry.sessionId) {
|
|
187
|
+
if (!metadata[entry.sessionId]) {
|
|
188
|
+
metadata[entry.sessionId] = {
|
|
189
|
+
customTitle: null,
|
|
190
|
+
slug: null,
|
|
191
|
+
project: entry.projectPath || null,
|
|
192
|
+
jsonlPath: null
|
|
193
|
+
};
|
|
194
|
+
}
|
|
172
195
|
metadata[entry.sessionId].description = entry.description || null;
|
|
173
196
|
metadata[entry.sessionId].gitBranch = entry.gitBranch || null;
|
|
174
197
|
metadata[entry.sessionId].created = entry.created || null;
|
|
@@ -183,6 +206,25 @@ function loadSessionMetadata() {
|
|
|
183
206
|
console.error('Error loading session metadata:', e);
|
|
184
207
|
}
|
|
185
208
|
|
|
209
|
+
// For team sessions with no JSONL match, try team config for project path
|
|
210
|
+
if (existsSync(TASKS_DIR)) {
|
|
211
|
+
const taskDirs = readdirSync(TASKS_DIR, { withFileTypes: true })
|
|
212
|
+
.filter(d => d.isDirectory());
|
|
213
|
+
for (const dir of taskDirs) {
|
|
214
|
+
if (!metadata[dir.name]) {
|
|
215
|
+
const teamConfig = loadTeamConfig(dir.name);
|
|
216
|
+
if (teamConfig) {
|
|
217
|
+
metadata[dir.name] = {
|
|
218
|
+
customTitle: null,
|
|
219
|
+
slug: null,
|
|
220
|
+
project: teamConfig.working_dir || null,
|
|
221
|
+
jsonlPath: null
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
186
228
|
sessionMetadataCache = metadata;
|
|
187
229
|
lastMetadataRefresh = now;
|
|
188
230
|
return metadata;
|
|
@@ -275,6 +317,32 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
275
317
|
}
|
|
276
318
|
}
|
|
277
319
|
|
|
320
|
+
// Add sessions from metadata that don't have task directories
|
|
321
|
+
for (const [sessionId, meta] of Object.entries(metadata)) {
|
|
322
|
+
if (!sessionsMap.has(sessionId)) {
|
|
323
|
+
let modifiedAt = meta.created || null;
|
|
324
|
+
if (!modifiedAt && meta.jsonlPath) {
|
|
325
|
+
try { modifiedAt = statSync(meta.jsonlPath).mtime.toISOString(); } catch (e) {}
|
|
326
|
+
}
|
|
327
|
+
sessionsMap.set(sessionId, {
|
|
328
|
+
id: sessionId,
|
|
329
|
+
name: getSessionDisplayName(sessionId, meta),
|
|
330
|
+
slug: meta.slug || null,
|
|
331
|
+
project: meta.project || null,
|
|
332
|
+
description: meta.description || null,
|
|
333
|
+
gitBranch: meta.gitBranch || null,
|
|
334
|
+
taskCount: 0,
|
|
335
|
+
completed: 0,
|
|
336
|
+
inProgress: 0,
|
|
337
|
+
pending: 0,
|
|
338
|
+
createdAt: meta.created || null,
|
|
339
|
+
modifiedAt: modifiedAt || new Date(0).toISOString(),
|
|
340
|
+
isTeam: false,
|
|
341
|
+
memberCount: 0
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
278
346
|
// Convert map to array and sort by most recently modified
|
|
279
347
|
let sessions = Array.from(sessionsMap.values());
|
|
280
348
|
sessions.sort((a, b) => new Date(b.modifiedAt) - new Date(a.modifiedAt));
|
|
@@ -291,6 +359,24 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
291
359
|
}
|
|
292
360
|
});
|
|
293
361
|
|
|
362
|
+
// API: Get distinct project paths with last-modified timestamps
|
|
363
|
+
app.get('/api/projects', (req, res) => {
|
|
364
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
365
|
+
const metadata = loadSessionMetadata();
|
|
366
|
+
const projectMap = {};
|
|
367
|
+
for (const meta of Object.values(metadata)) {
|
|
368
|
+
if (!meta.project) continue;
|
|
369
|
+
const mtime = meta.jsonlPath ? (() => { try { return statSync(meta.jsonlPath).mtime; } catch (e) { return null; } })() : null;
|
|
370
|
+
if (!projectMap[meta.project] || (mtime && mtime > projectMap[meta.project])) {
|
|
371
|
+
projectMap[meta.project] = mtime;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const projects = Object.entries(projectMap)
|
|
375
|
+
.map(([path, mtime]) => ({ path, modifiedAt: mtime ? mtime.toISOString() : null }))
|
|
376
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
377
|
+
res.json(projects);
|
|
378
|
+
});
|
|
379
|
+
|
|
294
380
|
// API: Get tasks for a session
|
|
295
381
|
app.get('/api/sessions/:sessionId', async (req, res) => {
|
|
296
382
|
try {
|
|
@@ -322,6 +408,45 @@ app.get('/api/sessions/:sessionId', async (req, res) => {
|
|
|
322
408
|
}
|
|
323
409
|
});
|
|
324
410
|
|
|
411
|
+
// API: Get session plan
|
|
412
|
+
app.get('/api/sessions/:sessionId/plan', async (req, res) => {
|
|
413
|
+
try {
|
|
414
|
+
const metadata = loadSessionMetadata();
|
|
415
|
+
const meta = metadata[req.params.sessionId];
|
|
416
|
+
const slug = meta?.slug;
|
|
417
|
+
if (!slug) return res.status(404).json({ error: 'No plan found' });
|
|
418
|
+
|
|
419
|
+
const planPath = path.join(PLANS_DIR, `${slug}.md`);
|
|
420
|
+
if (!existsSync(planPath)) return res.status(404).json({ error: 'No plan found' });
|
|
421
|
+
|
|
422
|
+
const content = await fs.readFile(planPath, 'utf8');
|
|
423
|
+
res.json({ content, slug });
|
|
424
|
+
} catch (error) {
|
|
425
|
+
console.error('Error reading plan:', error);
|
|
426
|
+
res.status(500).json({ error: 'Failed to read plan' });
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// API: Open session plan in VS Code
|
|
431
|
+
app.post('/api/sessions/:sessionId/plan/open', (req, res) => {
|
|
432
|
+
try {
|
|
433
|
+
const metadata = loadSessionMetadata();
|
|
434
|
+
const meta = metadata[req.params.sessionId];
|
|
435
|
+
const slug = meta?.slug;
|
|
436
|
+
if (!slug) return res.status(404).json({ error: 'No plan found' });
|
|
437
|
+
|
|
438
|
+
const planPath = path.join(PLANS_DIR, `${slug}.md`);
|
|
439
|
+
if (!existsSync(planPath)) return res.status(404).json({ error: 'No plan found' });
|
|
440
|
+
|
|
441
|
+
const editor = process.env.EDITOR || 'code';
|
|
442
|
+
require('child_process').exec(`${editor} "${planPath}"`);
|
|
443
|
+
res.json({ success: true });
|
|
444
|
+
} catch (error) {
|
|
445
|
+
console.error('Error opening plan in VS Code:', error);
|
|
446
|
+
res.status(500).json({ error: 'Failed to open plan' });
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
325
450
|
// API: Get team config
|
|
326
451
|
app.get('/api/teams/:name', (req, res) => {
|
|
327
452
|
const config = loadTeamConfig(req.params.name);
|
|
@@ -407,6 +532,32 @@ app.post('/api/tasks/:sessionId/:taskId/note', async (req, res) => {
|
|
|
407
532
|
}
|
|
408
533
|
});
|
|
409
534
|
|
|
535
|
+
// API: Update task fields (subject, description)
|
|
536
|
+
app.put('/api/tasks/:sessionId/:taskId', async (req, res) => {
|
|
537
|
+
try {
|
|
538
|
+
const { sessionId, taskId } = req.params;
|
|
539
|
+
const { subject, description } = req.body;
|
|
540
|
+
|
|
541
|
+
const taskPath = path.join(TASKS_DIR, sessionId, `${taskId}.json`);
|
|
542
|
+
|
|
543
|
+
if (!existsSync(taskPath)) {
|
|
544
|
+
return res.status(404).json({ error: 'Task not found' });
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const task = JSON.parse(await fs.readFile(taskPath, 'utf8'));
|
|
548
|
+
|
|
549
|
+
if (subject !== undefined) task.subject = subject;
|
|
550
|
+
if (description !== undefined) task.description = description;
|
|
551
|
+
|
|
552
|
+
await fs.writeFile(taskPath, JSON.stringify(task, null, 2));
|
|
553
|
+
|
|
554
|
+
res.json({ success: true, task });
|
|
555
|
+
} catch (error) {
|
|
556
|
+
console.error('Error updating task:', error);
|
|
557
|
+
res.status(500).json({ error: 'Failed to update task' });
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
|
|
410
561
|
// API: Delete a task
|
|
411
562
|
app.delete('/api/tasks/:sessionId/:taskId', async (req, res) => {
|
|
412
563
|
try {
|