claude-code-kanban 1.12.0 → 1.13.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 +366 -116
- package/server.js +129 -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 {
|
|
@@ -1011,12 +1014,23 @@
|
|
|
1011
1014
|
}
|
|
1012
1015
|
|
|
1013
1016
|
.detail-desc pre {
|
|
1017
|
+
border-radius: 6px;
|
|
1018
|
+
overflow: hidden;
|
|
1019
|
+
margin: 12px 0;
|
|
1020
|
+
font-size: 12px;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
.detail-desc pre code.hljs {
|
|
1024
|
+
padding: 12px;
|
|
1025
|
+
border-radius: 6px;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
.detail-desc pre code {
|
|
1014
1029
|
background: var(--bg-elevated);
|
|
1015
1030
|
padding: 12px;
|
|
1016
1031
|
border-radius: 6px;
|
|
1032
|
+
display: block;
|
|
1017
1033
|
overflow-x: auto;
|
|
1018
|
-
margin: 12px 0;
|
|
1019
|
-
font-size: 12px;
|
|
1020
1034
|
}
|
|
1021
1035
|
|
|
1022
1036
|
.detail-desc code {
|
|
@@ -1026,11 +1040,6 @@
|
|
|
1026
1040
|
font-size: 0.9em;
|
|
1027
1041
|
}
|
|
1028
1042
|
|
|
1029
|
-
.detail-desc pre code {
|
|
1030
|
-
background: transparent;
|
|
1031
|
-
padding: 0;
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
1043
|
.detail-desc hr {
|
|
1035
1044
|
border: none;
|
|
1036
1045
|
border-top: 1px solid var(--border);
|
|
@@ -1366,6 +1375,19 @@
|
|
|
1366
1375
|
display: flex;
|
|
1367
1376
|
}
|
|
1368
1377
|
|
|
1378
|
+
.plan-modal-overlay {
|
|
1379
|
+
z-index: 10001;
|
|
1380
|
+
background: rgba(0, 0, 0, 0.6);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
.modal.plan-modal {
|
|
1384
|
+
width: 60vw;
|
|
1385
|
+
max-width: 60vw;
|
|
1386
|
+
max-height: 90vh;
|
|
1387
|
+
display: flex;
|
|
1388
|
+
flex-direction: column;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1369
1391
|
.modal {
|
|
1370
1392
|
background: var(--bg-surface);
|
|
1371
1393
|
border: 1px solid var(--border);
|
|
@@ -1533,6 +1555,11 @@
|
|
|
1533
1555
|
border-color: var(--text-muted);
|
|
1534
1556
|
}
|
|
1535
1557
|
|
|
1558
|
+
.btn:focus-visible {
|
|
1559
|
+
outline: 2px solid var(--accent);
|
|
1560
|
+
outline-offset: 2px;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1536
1563
|
/* Skip navigation */
|
|
1537
1564
|
.skip-link {
|
|
1538
1565
|
position: absolute;
|
|
@@ -1848,11 +1875,18 @@
|
|
|
1848
1875
|
<aside id="detail-panel" class="detail-panel">
|
|
1849
1876
|
<header class="detail-header">
|
|
1850
1877
|
<h3>Task Details</h3>
|
|
1851
|
-
<
|
|
1852
|
-
<
|
|
1853
|
-
<
|
|
1854
|
-
|
|
1855
|
-
|
|
1878
|
+
<div style="display: flex; gap: 8px; align-items: center;">
|
|
1879
|
+
<button id="delete-task-btn" class="icon-btn" title="Delete task (D)" aria-label="Delete task" style="color: #ef4444; border-color: #ef4444; display: none;">
|
|
1880
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1881
|
+
<path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
|
|
1882
|
+
</svg>
|
|
1883
|
+
</button>
|
|
1884
|
+
<button id="close-detail" class="detail-close" aria-label="Close detail panel">
|
|
1885
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1886
|
+
<path d="M6 18L18 6M6 6l12 12"/>
|
|
1887
|
+
</svg>
|
|
1888
|
+
</button>
|
|
1889
|
+
</div>
|
|
1856
1890
|
</header>
|
|
1857
1891
|
<div id="detail-content" class="detail-content"></div>
|
|
1858
1892
|
</aside>
|
|
@@ -1866,7 +1900,8 @@
|
|
|
1866
1900
|
let viewMode = 'session';
|
|
1867
1901
|
let sessionFilter = 'active';
|
|
1868
1902
|
let sessionLimit = '20';
|
|
1869
|
-
let filterProject =
|
|
1903
|
+
let filterProject = '__recent__'; // null = all, '__recent__' = last 24h, or project path
|
|
1904
|
+
let recentProjects = new Set();
|
|
1870
1905
|
let searchQuery = ''; // Search query for fuzzy search
|
|
1871
1906
|
let allTasksCache = []; // Cache all tasks for search
|
|
1872
1907
|
let bulkDeleteSessionId = null; // Track session for bulk delete
|
|
@@ -1895,7 +1930,7 @@
|
|
|
1895
1930
|
if (currentSessionId) params.set('session', currentSessionId);
|
|
1896
1931
|
if (sessionFilter !== 'active') params.set('filter', sessionFilter);
|
|
1897
1932
|
if (sessionLimit !== '20') params.set('limit', sessionLimit);
|
|
1898
|
-
if (filterProject) params.set('project', filterProject);
|
|
1933
|
+
if (filterProject && filterProject !== '__recent__') params.set('project', filterProject);
|
|
1899
1934
|
if (ownerFilter) params.set('owner', ownerFilter);
|
|
1900
1935
|
if (searchQuery) params.set('search', searchQuery);
|
|
1901
1936
|
const qs = params.toString();
|
|
@@ -1907,7 +1942,7 @@
|
|
|
1907
1942
|
history.replaceState(null, '', window.location.pathname);
|
|
1908
1943
|
sessionFilter = 'active';
|
|
1909
1944
|
sessionLimit = '20';
|
|
1910
|
-
filterProject =
|
|
1945
|
+
filterProject = '__recent__';
|
|
1911
1946
|
ownerFilter = '';
|
|
1912
1947
|
searchQuery = '';
|
|
1913
1948
|
viewMode = 'all';
|
|
@@ -2192,7 +2227,7 @@
|
|
|
2192
2227
|
const allTasks = await res.json();
|
|
2193
2228
|
let activeTasks = allTasks.filter(t => t.status === 'in_progress');
|
|
2194
2229
|
if (filterProject) {
|
|
2195
|
-
activeTasks = activeTasks.filter(t => t.project
|
|
2230
|
+
activeTasks = activeTasks.filter(t => matchesProjectFilter(t.project));
|
|
2196
2231
|
}
|
|
2197
2232
|
renderLiveUpdates(activeTasks);
|
|
2198
2233
|
} catch (error) {
|
|
@@ -2270,7 +2305,7 @@
|
|
|
2270
2305
|
const res = await fetch('/api/tasks/all');
|
|
2271
2306
|
let tasks = await res.json();
|
|
2272
2307
|
if (filterProject) {
|
|
2273
|
-
tasks = tasks.filter(t => t.project
|
|
2308
|
+
tasks = tasks.filter(t => matchesProjectFilter(t.project));
|
|
2274
2309
|
}
|
|
2275
2310
|
currentTasks = tasks;
|
|
2276
2311
|
updateUrl();
|
|
@@ -2290,9 +2325,10 @@
|
|
|
2290
2325
|
const completed = currentTasks.filter(t => t.status === 'completed').length;
|
|
2291
2326
|
const percent = totalTasks > 0 ? Math.round((completed / totalTasks) * 100) : 0;
|
|
2292
2327
|
|
|
2293
|
-
const
|
|
2294
|
-
|
|
2295
|
-
|
|
2328
|
+
const isFiltered = filterProject && filterProject !== '__recent__';
|
|
2329
|
+
const projectName = isFiltered ? filterProject.split(/[/\\]/).pop() : null;
|
|
2330
|
+
sessionTitle.textContent = isFiltered ? `Tasks: ${projectName}` : (filterProject === '__recent__' ? 'Recent Tasks' : 'All Tasks');
|
|
2331
|
+
sessionMeta.textContent = isFiltered
|
|
2296
2332
|
? `${totalTasks} tasks in this project`
|
|
2297
2333
|
: `${totalTasks} tasks across ${sessions.length} sessions`;
|
|
2298
2334
|
progressPercent.textContent = `${percent}%`;
|
|
@@ -2310,7 +2346,7 @@
|
|
|
2310
2346
|
filteredSessions = filteredSessions.filter(s => s.pending > 0 || s.inProgress > 0);
|
|
2311
2347
|
}
|
|
2312
2348
|
if (filterProject) {
|
|
2313
|
-
filteredSessions = filteredSessions.filter(s => s.project
|
|
2349
|
+
filteredSessions = filteredSessions.filter(s => matchesProjectFilter(s.project));
|
|
2314
2350
|
}
|
|
2315
2351
|
|
|
2316
2352
|
// Apply search filter
|
|
@@ -2389,14 +2425,14 @@
|
|
|
2389
2425
|
const memberCount = session.memberCount || 0;
|
|
2390
2426
|
|
|
2391
2427
|
return `
|
|
2392
|
-
<button onclick="fetchTasks('${session.id}')" class="session-item ${isActive ? 'active' : ''}" title="${tooltip}">
|
|
2428
|
+
<button onclick="fetchTasks('${session.id}')" data-session-id="${session.id}" class="session-item ${isActive ? 'active' : ''}" title="${tooltip}">
|
|
2393
2429
|
<div class="session-name">${escapeHtml(primaryName)}</div>
|
|
2394
2430
|
${secondaryName ? `<div class="session-secondary">${escapeHtml(secondaryName)}</div>` : ''}
|
|
2395
2431
|
${gitBranch ? `<div class="session-branch">${gitBranch}</div>` : ''}
|
|
2396
2432
|
<div class="session-progress">
|
|
2397
2433
|
<span class="session-indicators">
|
|
2398
2434
|
${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();
|
|
2435
|
+
${(isTeam || session.project) ? `<span class="team-info-btn" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" title="View session info">ℹ</span>` : ''}
|
|
2400
2436
|
${hasInProgress ? '<span class="pulse"></span>' : ''}
|
|
2401
2437
|
</span>
|
|
2402
2438
|
<div class="progress-bar"><div class="progress-fill" style="width: ${percent}%"></div></div>
|
|
@@ -2530,7 +2566,7 @@
|
|
|
2530
2566
|
const card = document.querySelector(`.task-card[data-task-id="${selectedTaskId}"][data-session-id="${selectedSessionId}"]`)
|
|
2531
2567
|
|| document.querySelector(`.task-card[data-task-id="${selectedTaskId}"]`);
|
|
2532
2568
|
if (card) {
|
|
2533
|
-
card.classList.add('selected');
|
|
2569
|
+
if (focusZone === 'board') card.classList.add('selected');
|
|
2534
2570
|
} else {
|
|
2535
2571
|
selectedTaskId = null;
|
|
2536
2572
|
selectedSessionId = null;
|
|
@@ -2660,13 +2696,20 @@
|
|
|
2660
2696
|
selectSessionByIndex(selectedSessionIdx);
|
|
2661
2697
|
}
|
|
2662
2698
|
} else {
|
|
2699
|
+
// Session changed while in sidebar — reset stale selection
|
|
2700
|
+
if (selectedSessionId && selectedSessionId !== currentSessionId) {
|
|
2701
|
+
selectedTaskId = null;
|
|
2702
|
+
selectedSessionId = null;
|
|
2703
|
+
}
|
|
2663
2704
|
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}"]`);
|
|
2705
|
+
const card = document.querySelector(`.task-card[data-task-id="${selectedTaskId}"][data-session-id="${selectedSessionId}"]`);
|
|
2666
2706
|
if (card) card.classList.add('selected');
|
|
2667
2707
|
} else {
|
|
2668
2708
|
navigateVertical(1);
|
|
2669
2709
|
}
|
|
2710
|
+
if (selectedTaskId && detailPanel.classList.contains('visible')) {
|
|
2711
|
+
showTaskDetail(selectedTaskId, selectedSessionId);
|
|
2712
|
+
}
|
|
2670
2713
|
}
|
|
2671
2714
|
}
|
|
2672
2715
|
|
|
@@ -2738,19 +2781,8 @@
|
|
|
2738
2781
|
|
|
2739
2782
|
detailContent.innerHTML = `
|
|
2740
2783
|
<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>
|
|
2784
|
+
<div class="detail-label">Task #${task.id}</div>
|
|
2785
|
+
<h2 class="detail-title">${escapeHtml(task.subject)}</h2>
|
|
2754
2786
|
</div>
|
|
2755
2787
|
|
|
2756
2788
|
<div class="detail-section" style="display: flex; gap: 12px; align-items: center;">
|
|
@@ -2800,7 +2832,9 @@
|
|
|
2800
2832
|
`;
|
|
2801
2833
|
|
|
2802
2834
|
// Setup button handlers
|
|
2803
|
-
document.getElementById('delete-task-btn')
|
|
2835
|
+
const deleteBtn = document.getElementById('delete-task-btn');
|
|
2836
|
+
deleteBtn.style.display = '';
|
|
2837
|
+
deleteBtn.onclick = () => deleteTask(task.id, actualSessionId);
|
|
2804
2838
|
}
|
|
2805
2839
|
|
|
2806
2840
|
async function addNote(event, taskId, sessionId) {
|
|
@@ -2834,10 +2868,12 @@
|
|
|
2834
2868
|
|
|
2835
2869
|
function closeDetailPanel() {
|
|
2836
2870
|
detailPanel.classList.remove('visible');
|
|
2871
|
+
document.getElementById('delete-task-btn').style.display = 'none';
|
|
2837
2872
|
}
|
|
2838
2873
|
|
|
2839
2874
|
let deleteTaskId = null;
|
|
2840
2875
|
let deleteSessionId = null;
|
|
2876
|
+
let deleteModalKeyHandler = null;
|
|
2841
2877
|
|
|
2842
2878
|
function showBlockedTaskModal(task) {
|
|
2843
2879
|
const messageDiv = document.getElementById('blocked-task-message');
|
|
@@ -2890,15 +2926,31 @@
|
|
|
2890
2926
|
const modal = document.getElementById('delete-confirm-modal');
|
|
2891
2927
|
modal.classList.add('visible');
|
|
2892
2928
|
|
|
2893
|
-
|
|
2894
|
-
|
|
2929
|
+
const buttons = [
|
|
2930
|
+
document.getElementById('delete-cancel-btn'),
|
|
2931
|
+
document.getElementById('delete-confirm-btn')
|
|
2932
|
+
];
|
|
2933
|
+
let focusIdx = 1;
|
|
2934
|
+
buttons[focusIdx].focus();
|
|
2935
|
+
|
|
2936
|
+
deleteModalKeyHandler = (e) => {
|
|
2895
2937
|
if (e.key === 'Escape') {
|
|
2896
2938
|
e.preventDefault();
|
|
2897
2939
|
closeDeleteConfirmModal();
|
|
2898
|
-
|
|
2940
|
+
} else if (e.key === 'ArrowLeft' || e.key === 'h') {
|
|
2941
|
+
e.preventDefault();
|
|
2942
|
+
focusIdx = 0;
|
|
2943
|
+
buttons[focusIdx].focus();
|
|
2944
|
+
} else if (e.key === 'ArrowRight' || e.key === 'l') {
|
|
2945
|
+
e.preventDefault();
|
|
2946
|
+
focusIdx = 1;
|
|
2947
|
+
buttons[focusIdx].focus();
|
|
2948
|
+
} else if (e.key === 'Enter') {
|
|
2949
|
+
e.preventDefault();
|
|
2950
|
+
buttons[focusIdx].click();
|
|
2899
2951
|
}
|
|
2900
2952
|
};
|
|
2901
|
-
document.addEventListener('keydown',
|
|
2953
|
+
document.addEventListener('keydown', deleteModalKeyHandler);
|
|
2902
2954
|
}
|
|
2903
2955
|
|
|
2904
2956
|
function closeDeleteConfirmModal() {
|
|
@@ -2906,6 +2958,10 @@
|
|
|
2906
2958
|
modal.classList.remove('visible');
|
|
2907
2959
|
deleteTaskId = null;
|
|
2908
2960
|
deleteSessionId = null;
|
|
2961
|
+
if (deleteModalKeyHandler) {
|
|
2962
|
+
document.removeEventListener('keydown', deleteModalKeyHandler);
|
|
2963
|
+
deleteModalKeyHandler = null;
|
|
2964
|
+
}
|
|
2909
2965
|
}
|
|
2910
2966
|
|
|
2911
2967
|
async function confirmDelete() {
|
|
@@ -3008,6 +3064,20 @@
|
|
|
3008
3064
|
setFocusZone('board');
|
|
3009
3065
|
return;
|
|
3010
3066
|
}
|
|
3067
|
+
if (e.key === 'p' || e.key === 'P') {
|
|
3068
|
+
e.preventDefault();
|
|
3069
|
+
const highlighted = sessionsList.querySelector('.session-item.kb-selected');
|
|
3070
|
+
const sid = highlighted?.dataset.sessionId || currentSessionId;
|
|
3071
|
+
if (sid) openPlanForSession(sid);
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
3074
|
+
if (e.key === 'i' || e.key === 'I') {
|
|
3075
|
+
e.preventDefault();
|
|
3076
|
+
const highlighted = sessionsList.querySelector('.session-item.kb-selected');
|
|
3077
|
+
const sid = highlighted?.dataset.sessionId || currentSessionId;
|
|
3078
|
+
if (sid) showSessionInfoModal(sid);
|
|
3079
|
+
return;
|
|
3080
|
+
}
|
|
3011
3081
|
if (e.key === '?' || (e.key === '/' && e.shiftKey)) {
|
|
3012
3082
|
e.preventDefault();
|
|
3013
3083
|
showHelpModal();
|
|
@@ -3050,18 +3120,23 @@
|
|
|
3050
3120
|
closeDetailPanel();
|
|
3051
3121
|
}
|
|
3052
3122
|
|
|
3053
|
-
if (
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3123
|
+
if (e.key === 'p' || e.key === 'P') {
|
|
3124
|
+
e.preventDefault();
|
|
3125
|
+
const sid = selectedSessionId || currentSessionId;
|
|
3126
|
+
if (sid) openPlanForSession(sid);
|
|
3127
|
+
return;
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
if (e.key === 'i' || e.key === 'I') {
|
|
3131
|
+
e.preventDefault();
|
|
3132
|
+
const sid = selectedSessionId || currentSessionId;
|
|
3133
|
+
if (sid) showSessionInfoModal(sid);
|
|
3134
|
+
return;
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
if ((e.key === 'd' || e.key === 'D') && selectedTaskId) {
|
|
3138
|
+
e.preventDefault();
|
|
3139
|
+
deleteTask(selectedTaskId, selectedSessionId || currentSessionId);
|
|
3065
3140
|
}
|
|
3066
3141
|
|
|
3067
3142
|
if (e.key === '?' || (e.key === '/' && e.shiftKey)) {
|
|
@@ -3178,6 +3253,12 @@
|
|
|
3178
3253
|
fetchSessions();
|
|
3179
3254
|
}
|
|
3180
3255
|
|
|
3256
|
+
function matchesProjectFilter(project) {
|
|
3257
|
+
if (!filterProject) return true;
|
|
3258
|
+
if (filterProject === '__recent__') return recentProjects.has(project);
|
|
3259
|
+
return project === filterProject;
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3181
3262
|
function filterByProject(project) {
|
|
3182
3263
|
filterProject = project || null;
|
|
3183
3264
|
updateUrl();
|
|
@@ -3186,15 +3267,30 @@
|
|
|
3186
3267
|
showAllTasks();
|
|
3187
3268
|
}
|
|
3188
3269
|
|
|
3189
|
-
function updateProjectDropdown() {
|
|
3270
|
+
async function updateProjectDropdown() {
|
|
3190
3271
|
const dropdown = document.getElementById('project-filter');
|
|
3191
|
-
|
|
3272
|
+
let projects;
|
|
3273
|
+
try {
|
|
3274
|
+
const res = await fetch('/api/projects');
|
|
3275
|
+
projects = await res.json();
|
|
3276
|
+
} catch (e) {
|
|
3277
|
+
projects = [...new Set(sessions.map(s => s.project).filter(Boolean))].sort()
|
|
3278
|
+
.map(p => ({ path: p, modifiedAt: null }));
|
|
3279
|
+
}
|
|
3192
3280
|
|
|
3193
|
-
|
|
3281
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
3282
|
+
recentProjects = new Set(
|
|
3283
|
+
projects.filter(p => p.modifiedAt && new Date(p.modifiedAt).getTime() > cutoff).map(p => p.path)
|
|
3284
|
+
);
|
|
3285
|
+
|
|
3286
|
+
const recentSelected = filterProject === '__recent__' ? ' selected' : '';
|
|
3287
|
+
dropdown.innerHTML =
|
|
3288
|
+
'<option value="">All Projects</option>' +
|
|
3289
|
+
`<option value="__recent__"${recentSelected}>Recent (24h)</option>` +
|
|
3194
3290
|
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>`;
|
|
3291
|
+
const name = p.path.split(/[/\\]/).pop();
|
|
3292
|
+
const selected = p.path === filterProject ? ' selected' : '';
|
|
3293
|
+
return `<option value="${escapeHtml(p.path)}"${selected} title="${escapeHtml(p.path)}">${escapeHtml(name)}</option>`;
|
|
3198
3294
|
}).join('');
|
|
3199
3295
|
}
|
|
3200
3296
|
|
|
@@ -3210,6 +3306,15 @@
|
|
|
3210
3306
|
localStorage.setItem('theme', 'light');
|
|
3211
3307
|
}
|
|
3212
3308
|
updateThemeIcon();
|
|
3309
|
+
syncHljsTheme();
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
function syncHljsTheme() {
|
|
3313
|
+
const isLight = document.body.classList.contains('light');
|
|
3314
|
+
const dark = document.getElementById('hljs-theme-dark');
|
|
3315
|
+
const light = document.getElementById('hljs-theme-light');
|
|
3316
|
+
if (dark) dark.disabled = isLight;
|
|
3317
|
+
if (light) light.disabled = !isLight;
|
|
3213
3318
|
}
|
|
3214
3319
|
|
|
3215
3320
|
function updateThemeIcon() {
|
|
@@ -3231,6 +3336,7 @@
|
|
|
3231
3336
|
}
|
|
3232
3337
|
// If no saved preference, system prefers-color-scheme CSS handles it
|
|
3233
3338
|
updateThemeIcon();
|
|
3339
|
+
syncHljsTheme();
|
|
3234
3340
|
}
|
|
3235
3341
|
|
|
3236
3342
|
function toggleSidebar() {
|
|
@@ -3239,6 +3345,7 @@
|
|
|
3239
3345
|
localStorage.setItem('sidebar-collapsed', collapsed);
|
|
3240
3346
|
if (collapsed) {
|
|
3241
3347
|
sidebar.style.width = '';
|
|
3348
|
+
if (focusZone === 'sidebar') setFocusZone('board');
|
|
3242
3349
|
} else {
|
|
3243
3350
|
const w = getComputedStyle(sidebar).getPropertyValue('--sidebar-width');
|
|
3244
3351
|
if (w) sidebar.style.width = w;
|
|
@@ -3294,68 +3401,127 @@
|
|
|
3294
3401
|
document.getElementById('session-limit').value = sessionLimit;
|
|
3295
3402
|
}
|
|
3296
3403
|
|
|
3297
|
-
async function
|
|
3404
|
+
async function showSessionInfoModal(sessionId) {
|
|
3298
3405
|
const session = sessions.find(s => s.id === sessionId);
|
|
3299
|
-
if (!session
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3406
|
+
if (!session) return;
|
|
3407
|
+
|
|
3408
|
+
const promises = [];
|
|
3409
|
+
|
|
3410
|
+
// Fetch team config
|
|
3411
|
+
let teamConfig = null;
|
|
3412
|
+
if (session.isTeam) {
|
|
3413
|
+
promises.push(
|
|
3414
|
+
fetch(`/api/teams/${sessionId}`).then(r => r.ok ? r.json() : null).catch(() => null)
|
|
3415
|
+
.then(data => { teamConfig = data; })
|
|
3416
|
+
);
|
|
3307
3417
|
}
|
|
3418
|
+
|
|
3419
|
+
// Fetch plan
|
|
3420
|
+
let planContent = null;
|
|
3421
|
+
promises.push(
|
|
3422
|
+
fetch(`/api/sessions/${sessionId}/plan`).then(r => r.ok ? r.json() : null).catch(() => null)
|
|
3423
|
+
.then(data => { planContent = data?.content || null; })
|
|
3424
|
+
);
|
|
3425
|
+
|
|
3426
|
+
await Promise.all(promises);
|
|
3427
|
+
|
|
3428
|
+
const tasks = currentSessionId === sessionId ? currentTasks : [];
|
|
3429
|
+
_planSessionId = sessionId;
|
|
3430
|
+
showInfoModal(session, teamConfig, tasks, planContent);
|
|
3308
3431
|
}
|
|
3309
3432
|
|
|
3310
|
-
|
|
3433
|
+
let _pendingPlanContent = null;
|
|
3434
|
+
|
|
3435
|
+
function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
3311
3436
|
const modal = document.getElementById('team-modal');
|
|
3312
3437
|
const titleEl = document.getElementById('team-modal-title');
|
|
3313
3438
|
const bodyEl = document.getElementById('team-modal-body');
|
|
3314
3439
|
|
|
3315
|
-
titleEl.textContent =
|
|
3440
|
+
titleEl.textContent = teamConfig
|
|
3441
|
+
? `Team: ${teamConfig.team_name || teamConfig.name || 'Unknown'}`
|
|
3442
|
+
: (session.name || session.slug || session.id);
|
|
3316
3443
|
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3444
|
+
let html = '';
|
|
3445
|
+
|
|
3446
|
+
// Session & project details as compact key-value rows
|
|
3447
|
+
const infoRows = [];
|
|
3448
|
+
infoRows.push(['Session', `<span style="font-family: 'IBM Plex Mono', monospace; font-size: 11px; user-select: all;">${escapeHtml(session.id)}</span>`]);
|
|
3449
|
+
if (session.modifiedAt) {
|
|
3450
|
+
const d = new Date(session.modifiedAt);
|
|
3451
|
+
infoRows.push(['Last Modified', `${formatDate(session.modifiedAt)} <span style="font-size: 10px; color: var(--text-tertiary);">(${d.toLocaleString()})</span>`]);
|
|
3452
|
+
}
|
|
3453
|
+
if (session.project) {
|
|
3454
|
+
const projectName = session.project.split(/[/\\]/).pop();
|
|
3455
|
+
infoRows.push(['Project', `${escapeHtml(projectName)}<br><span style="font-size: 10px; color: var(--text-tertiary);">${escapeHtml(session.project)}</span>`]);
|
|
3456
|
+
if (session.gitBranch) {
|
|
3457
|
+
infoRows.push(['Branch', escapeHtml(session.gitBranch)]);
|
|
3458
|
+
}
|
|
3459
|
+
if (session.description) {
|
|
3460
|
+
infoRows.push(['Description', escapeHtml(session.description)]);
|
|
3321
3461
|
}
|
|
3462
|
+
}
|
|
3463
|
+
html += `<div class="team-modal-meta" style="margin-bottom: 16px; display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; align-items: baseline;">`;
|
|
3464
|
+
infoRows.forEach(([label, value]) => {
|
|
3465
|
+
html += `<span style="font-weight: 500; color: var(--text-secondary); font-size: 12px; white-space: nowrap;">${label}</span><span>${value}</span>`;
|
|
3322
3466
|
});
|
|
3467
|
+
html += `</div>`;
|
|
3468
|
+
|
|
3469
|
+
if (planContent) {
|
|
3470
|
+
_pendingPlanContent = planContent;
|
|
3471
|
+
const titleMatch = planContent.match(/^#\s+(.+)$/m);
|
|
3472
|
+
const planTitle = titleMatch ? titleMatch[1].trim() : null;
|
|
3473
|
+
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)'">
|
|
3474
|
+
<span style="font-size: 14px;">📋</span>
|
|
3475
|
+
<div style="flex: 1; min-width: 0;">
|
|
3476
|
+
<div style="font-size: 11px; font-weight: 500; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px;">Plan</div>
|
|
3477
|
+
${planTitle ? `<div style="font-size: 13px; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${escapeHtml(planTitle)}</div>` : ''}
|
|
3478
|
+
</div>
|
|
3479
|
+
<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>
|
|
3480
|
+
</div>`;
|
|
3481
|
+
}
|
|
3323
3482
|
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3483
|
+
// Team info section
|
|
3484
|
+
if (teamConfig) {
|
|
3485
|
+
const ownerCounts = {};
|
|
3486
|
+
tasks.forEach(t => {
|
|
3487
|
+
if (t.owner) ownerCounts[t.owner] = (ownerCounts[t.owner] || 0) + 1;
|
|
3488
|
+
});
|
|
3327
3489
|
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
}
|
|
3490
|
+
const members = teamConfig.members || [];
|
|
3491
|
+
const description = teamConfig.description || '';
|
|
3492
|
+
const lead = members.find(m => m.agentType === 'team-lead' || m.name === 'team-lead');
|
|
3332
3493
|
|
|
3333
|
-
|
|
3494
|
+
if (description) {
|
|
3495
|
+
html += `<div class="team-modal-desc">"${escapeHtml(description)}"</div>`;
|
|
3496
|
+
}
|
|
3334
3497
|
|
|
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
|
-
});
|
|
3498
|
+
html += `<div style="font-size: 12px; font-weight: 500; color: var(--text-secondary); margin-bottom: 10px;">Members (${members.length})</div>`;
|
|
3346
3499
|
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3500
|
+
members.forEach(member => {
|
|
3501
|
+
const taskCount = ownerCounts[member.name] || 0;
|
|
3502
|
+
html += `
|
|
3503
|
+
<div class="team-member-card">
|
|
3504
|
+
<div class="member-name">🟢 ${escapeHtml(member.name)}</div>
|
|
3505
|
+
<div class="member-detail">Role: ${escapeHtml(member.agentType || 'unknown')}</div>
|
|
3506
|
+
${member.model ? `<div class="member-detail">Model: ${escapeHtml(member.model)}</div>` : ''}
|
|
3507
|
+
<div class="member-tasks">Tasks: ${taskCount} assigned</div>
|
|
3508
|
+
</div>
|
|
3509
|
+
`;
|
|
3510
|
+
});
|
|
3511
|
+
|
|
3512
|
+
const metaParts = [];
|
|
3513
|
+
if (teamConfig.created_at) {
|
|
3514
|
+
metaParts.push(`Created: ${new Date(teamConfig.created_at).toLocaleString()}`);
|
|
3515
|
+
}
|
|
3516
|
+
if (lead) {
|
|
3517
|
+
metaParts.push(`Lead: ${lead.name}`);
|
|
3518
|
+
}
|
|
3519
|
+
if (teamConfig.working_dir) {
|
|
3520
|
+
metaParts.push(`Working dir: ${teamConfig.working_dir}`);
|
|
3521
|
+
}
|
|
3522
|
+
if (metaParts.length > 0) {
|
|
3523
|
+
html += `<div class="team-modal-meta">${metaParts.map(p => escapeHtml(p)).join('<br>')}</div>`;
|
|
3524
|
+
}
|
|
3359
3525
|
}
|
|
3360
3526
|
|
|
3361
3527
|
bodyEl.innerHTML = html;
|
|
@@ -3363,6 +3529,7 @@
|
|
|
3363
3529
|
|
|
3364
3530
|
const keyHandler = (e) => {
|
|
3365
3531
|
if (e.key === 'Escape') {
|
|
3532
|
+
if (document.getElementById('plan-modal').classList.contains('visible')) return;
|
|
3366
3533
|
e.preventDefault();
|
|
3367
3534
|
closeTeamModal();
|
|
3368
3535
|
document.removeEventListener('keydown', keyHandler);
|
|
@@ -3375,6 +3542,45 @@
|
|
|
3375
3542
|
document.getElementById('team-modal').classList.remove('visible');
|
|
3376
3543
|
}
|
|
3377
3544
|
|
|
3545
|
+
let _planSessionId = null;
|
|
3546
|
+
|
|
3547
|
+
function openPlanForSession(sid) {
|
|
3548
|
+
fetch(`/api/sessions/${sid}/plan`).then(r => r.ok ? r.json() : null).catch(() => null)
|
|
3549
|
+
.then(data => {
|
|
3550
|
+
if (data?.content) {
|
|
3551
|
+
_pendingPlanContent = data.content;
|
|
3552
|
+
_planSessionId = sid;
|
|
3553
|
+
openPlanModal();
|
|
3554
|
+
}
|
|
3555
|
+
});
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
function openPlanModal() {
|
|
3559
|
+
if (!_pendingPlanContent) return;
|
|
3560
|
+
const body = document.getElementById('plan-modal-body');
|
|
3561
|
+
body.innerHTML = DOMPurify.sanitize(marked.parse(_pendingPlanContent));
|
|
3562
|
+
document.getElementById('plan-modal').classList.add('visible');
|
|
3563
|
+
|
|
3564
|
+
const keyHandler = (e) => {
|
|
3565
|
+
if (e.key === 'Escape') {
|
|
3566
|
+
e.preventDefault();
|
|
3567
|
+
e.stopPropagation();
|
|
3568
|
+
closePlanModal();
|
|
3569
|
+
document.removeEventListener('keydown', keyHandler, true);
|
|
3570
|
+
}
|
|
3571
|
+
};
|
|
3572
|
+
document.addEventListener('keydown', keyHandler, true);
|
|
3573
|
+
}
|
|
3574
|
+
|
|
3575
|
+
function closePlanModal() {
|
|
3576
|
+
document.getElementById('plan-modal').classList.remove('visible');
|
|
3577
|
+
}
|
|
3578
|
+
|
|
3579
|
+
function openPlanInEditor() {
|
|
3580
|
+
if (!_planSessionId) return;
|
|
3581
|
+
fetch(`/api/sessions/${_planSessionId}/plan/open`, { method: 'POST' }).catch(() => {});
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3378
3584
|
function updateOwnerFilter() {
|
|
3379
3585
|
const bar = document.getElementById('owner-filter-bar');
|
|
3380
3586
|
const select = document.getElementById('owner-filter');
|
|
@@ -3416,6 +3622,23 @@
|
|
|
3416
3622
|
|
|
3417
3623
|
// Init
|
|
3418
3624
|
loadTheme();
|
|
3625
|
+
|
|
3626
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
3627
|
+
if (typeof marked !== 'undefined' && typeof hljs !== 'undefined') {
|
|
3628
|
+
const renderer = new marked.Renderer();
|
|
3629
|
+
renderer.code = function({ text, lang }) {
|
|
3630
|
+
let highlighted;
|
|
3631
|
+
if (lang && hljs.getLanguage(lang)) {
|
|
3632
|
+
highlighted = hljs.highlight(text, { language: lang }).value;
|
|
3633
|
+
} else {
|
|
3634
|
+
highlighted = hljs.highlightAuto(text).value;
|
|
3635
|
+
}
|
|
3636
|
+
return `<pre><code class="hljs language-${escapeHtml(lang || '')}">${highlighted}</code></pre>`;
|
|
3637
|
+
};
|
|
3638
|
+
marked.use({ renderer });
|
|
3639
|
+
}
|
|
3640
|
+
});
|
|
3641
|
+
|
|
3419
3642
|
loadSidebarState();
|
|
3420
3643
|
initSidebarResize();
|
|
3421
3644
|
fetch('/api/version').then(r => r.json()).then(d => {
|
|
@@ -3425,7 +3648,7 @@
|
|
|
3425
3648
|
const urlState = getUrlState();
|
|
3426
3649
|
sessionFilter = urlState.filter || 'active';
|
|
3427
3650
|
sessionLimit = urlState.limit || '20';
|
|
3428
|
-
filterProject = urlState.project ||
|
|
3651
|
+
filterProject = urlState.project || '__recent__';
|
|
3429
3652
|
ownerFilter = urlState.owner || '';
|
|
3430
3653
|
searchQuery = urlState.search || '';
|
|
3431
3654
|
|
|
@@ -3449,7 +3672,7 @@
|
|
|
3449
3672
|
const s = getUrlState();
|
|
3450
3673
|
sessionFilter = s.filter || 'active';
|
|
3451
3674
|
sessionLimit = s.limit || '20';
|
|
3452
|
-
filterProject = s.project ||
|
|
3675
|
+
filterProject = s.project || '__recent__';
|
|
3453
3676
|
ownerFilter = s.owner || '';
|
|
3454
3677
|
searchQuery = s.search || '';
|
|
3455
3678
|
loadPreferences();
|
|
@@ -3520,6 +3743,14 @@
|
|
|
3520
3743
|
<div>
|
|
3521
3744
|
<h4 style="margin: 0 0 8px 0; color: var(--text-primary); font-size: 14px; font-weight: 600;">Task Actions</h4>
|
|
3522
3745
|
<table style="width: 100%; font-size: 13px;">
|
|
3746
|
+
<tr>
|
|
3747
|
+
<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>
|
|
3748
|
+
<td style="padding: 4px 0; color: var(--text-primary);">Open session plan</td>
|
|
3749
|
+
</tr>
|
|
3750
|
+
<tr>
|
|
3751
|
+
<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>
|
|
3752
|
+
<td style="padding: 4px 0; color: var(--text-primary);">Open session info</td>
|
|
3753
|
+
</tr>
|
|
3523
3754
|
<tr>
|
|
3524
3755
|
<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
3756
|
<td style="padding: 4px 0; color: var(--text-primary);">Delete selected task</td>
|
|
@@ -3549,8 +3780,8 @@
|
|
|
3549
3780
|
<p id="delete-confirm-message" style="margin: 0; color: var(--text-primary);"></p>
|
|
3550
3781
|
</div>
|
|
3551
3782
|
<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>
|
|
3783
|
+
<button id="delete-cancel-btn" class="btn btn-secondary" onclick="closeDeleteConfirmModal()">Cancel</button>
|
|
3784
|
+
<button id="delete-confirm-btn" class="btn btn-primary" onclick="confirmDelete()" style="background: #ef4444; border-color: #ef4444;">Delete</button>
|
|
3554
3785
|
</div>
|
|
3555
3786
|
</div>
|
|
3556
3787
|
</div>
|
|
@@ -3616,6 +3847,25 @@
|
|
|
3616
3847
|
</div>
|
|
3617
3848
|
</div>
|
|
3618
3849
|
|
|
3850
|
+
<!-- Plan Modal (stacked on top of info modal) -->
|
|
3851
|
+
<div id="plan-modal" class="modal-overlay plan-modal-overlay" onclick="closePlanModal()">
|
|
3852
|
+
<div class="modal plan-modal" onclick="event.stopPropagation()">
|
|
3853
|
+
<div class="modal-header">
|
|
3854
|
+
<h3 id="plan-modal-title" class="modal-title">Plan</h3>
|
|
3855
|
+
<button class="btn btn-secondary" style="padding: 4px 10px; font-size: 11px; margin-left: auto; margin-right: 12px;" onclick="openPlanInEditor()">Open in Editor</button>
|
|
3856
|
+
<button class="modal-close" aria-label="Close dialog" onclick="closePlanModal()">
|
|
3857
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
3858
|
+
<path d="M18 6L6 18M6 6l12 12"/>
|
|
3859
|
+
</svg>
|
|
3860
|
+
</button>
|
|
3861
|
+
</div>
|
|
3862
|
+
<div id="plan-modal-body" class="modal-body detail-desc" style="overflow-y: auto; flex: 1;"></div>
|
|
3863
|
+
<div class="modal-footer">
|
|
3864
|
+
<button class="btn btn-primary" onclick="closePlanModal()">Close</button>
|
|
3865
|
+
</div>
|
|
3866
|
+
</div>
|
|
3867
|
+
</div>
|
|
3868
|
+
|
|
3619
3869
|
<!-- Blocked Task Warning Modal -->
|
|
3620
3870
|
<div id="blocked-task-modal" class="modal-overlay" onclick="closeBlockedTaskModal()">
|
|
3621
3871
|
<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);
|