claude-code-kanban 2.1.0 → 2.2.0-rc.2
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/hooks/agent-spy.sh +18 -0
- package/install.js +1 -0
- package/lib/parsers.js +37 -1
- package/package.json +1 -1
- package/public/app.js +210 -26
- package/public/style.css +23 -1
- package/server.js +182 -24
package/hooks/agent-spy.sh
CHANGED
|
@@ -16,6 +16,24 @@ eval "$(echo "$INPUT" | jq -r '
|
|
|
16
16
|
|
|
17
17
|
[ -z "$SESSION_ID" ] && exit 0
|
|
18
18
|
|
|
19
|
+
# Map session to custom task list on session start
|
|
20
|
+
if [ "$EVENT" = "SessionStart" ]; then
|
|
21
|
+
TASK_LIST_ID="${CLAUDE_CODE_TASK_LIST_ID:-}"
|
|
22
|
+
if [ -n "$TASK_LIST_ID" ]; then
|
|
23
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd // ""')
|
|
24
|
+
MAPS_DIR="$HOME/.claude/agent-activity/_task-maps"
|
|
25
|
+
mkdir -p "$MAPS_DIR"
|
|
26
|
+
MAP_FILE="$MAPS_DIR/$TASK_LIST_ID.json"
|
|
27
|
+
TMP_FILE="$MAP_FILE.$$"
|
|
28
|
+
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
29
|
+
EXISTING="{}"
|
|
30
|
+
[ -f "$MAP_FILE" ] && EXISTING=$(cat "$MAP_FILE")
|
|
31
|
+
echo "$EXISTING" | jq -c --arg sid "$SESSION_ID" --arg cwd "$CWD" --arg ts "$TS" \
|
|
32
|
+
'.[$sid] = {project: $cwd, updatedAt: $ts}' > "$TMP_FILE" && mv "$TMP_FILE" "$MAP_FILE"
|
|
33
|
+
fi
|
|
34
|
+
exit 0
|
|
35
|
+
fi
|
|
36
|
+
|
|
19
37
|
# PostToolUse / non-waiting PreToolUse: clear waiting state
|
|
20
38
|
if [ "$EVENT" = "PostToolUse" ] || { [ "$EVENT" = "PreToolUse" ] && [ "$TOOL_NAME" != "AskUserQuestion" ]; }; then
|
|
21
39
|
WFILE="$HOME/.claude/agent-activity/$SESSION_ID/_waiting.json"
|
package/install.js
CHANGED
|
@@ -17,6 +17,7 @@ const AGENT_ACTIVITY_DIR = path.join(CLAUDE_DIR, 'agent-activity');
|
|
|
17
17
|
|
|
18
18
|
const HOOK_COMMAND = '~/.claude/hooks/agent-spy.sh';
|
|
19
19
|
const HOOK_EVENTS = [
|
|
20
|
+
{ event: 'SessionStart' },
|
|
20
21
|
{ event: 'SubagentStart' },
|
|
21
22
|
{ event: 'SubagentStop' },
|
|
22
23
|
{ event: 'TeammateIdle' },
|
package/lib/parsers.js
CHANGED
|
@@ -613,6 +613,41 @@ function findTerminatedTeammates(jsonlPath) {
|
|
|
613
613
|
return terminated;
|
|
614
614
|
}
|
|
615
615
|
|
|
616
|
+
function extractPromptFromTranscript(jsonlPath) {
|
|
617
|
+
const { openSync, readSync, closeSync } = fs;
|
|
618
|
+
const MAX_READ = 65536;
|
|
619
|
+
const CHUNK = 4096;
|
|
620
|
+
const fd = openSync(jsonlPath, 'r');
|
|
621
|
+
try {
|
|
622
|
+
let accumulated = '';
|
|
623
|
+
const buf = Buffer.alloc(CHUNK);
|
|
624
|
+
while (accumulated.length < MAX_READ) {
|
|
625
|
+
const bytesRead = readSync(fd, buf, 0, CHUNK, null);
|
|
626
|
+
if (bytesRead === 0) break;
|
|
627
|
+
accumulated += buf.toString('utf8', 0, bytesRead);
|
|
628
|
+
const nlIdx = accumulated.indexOf('\n');
|
|
629
|
+
if (nlIdx === -1) continue;
|
|
630
|
+
const firstLine = accumulated.slice(0, nlIdx);
|
|
631
|
+
try {
|
|
632
|
+
const obj = JSON.parse(firstLine);
|
|
633
|
+
if (obj.type === 'user') {
|
|
634
|
+
const content = obj.message?.content;
|
|
635
|
+
if (typeof content === 'string') return content.slice(0, 500);
|
|
636
|
+
if (Array.isArray(content)) {
|
|
637
|
+
for (const b of content) {
|
|
638
|
+
if (b.type === 'text' && b.text) return b.text.slice(0, 500);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
} catch (_) {}
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
} finally {
|
|
646
|
+
closeSync(fd);
|
|
647
|
+
}
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
|
|
616
651
|
module.exports = {
|
|
617
652
|
parseTask,
|
|
618
653
|
parseAgent,
|
|
@@ -624,5 +659,6 @@ module.exports = {
|
|
|
624
659
|
readRecentMessages,
|
|
625
660
|
buildAgentProgressMap,
|
|
626
661
|
readCompactSummaries,
|
|
627
|
-
findTerminatedTeammates
|
|
662
|
+
findTerminatedTeammates,
|
|
663
|
+
extractPromptFromTranscript
|
|
628
664
|
};
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -29,6 +29,8 @@ let selectedSessionKbId = null;
|
|
|
29
29
|
let sessionJustSelected = false;
|
|
30
30
|
let agentLogMode = null;
|
|
31
31
|
let agentLogSSE = null;
|
|
32
|
+
let currentProjectPath = null;
|
|
33
|
+
let currentProjectSessionIds = [];
|
|
32
34
|
|
|
33
35
|
function getUrlState() {
|
|
34
36
|
const params = new URLSearchParams(window.location.search);
|
|
@@ -41,12 +43,14 @@ function getUrlState() {
|
|
|
41
43
|
owner: params.get('owner'),
|
|
42
44
|
search: params.get('search'),
|
|
43
45
|
messages: params.get('messages') === '1',
|
|
46
|
+
projectView: params.get('projectView'),
|
|
44
47
|
};
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
function updateUrl() {
|
|
48
51
|
const params = new URLSearchParams();
|
|
49
52
|
if (viewMode === 'all') params.set('view', 'all');
|
|
53
|
+
if (viewMode === 'project' && currentProjectPath) params.set('projectView', btoa(currentProjectPath));
|
|
50
54
|
if (currentSessionId) params.set('session', currentSessionId);
|
|
51
55
|
if (sessionFilter !== 'active') params.set('filter', sessionFilter);
|
|
52
56
|
if (sessionLimit !== '20') params.set('limit', sessionLimit);
|
|
@@ -70,6 +74,8 @@ function resetState() {
|
|
|
70
74
|
viewMode = 'all';
|
|
71
75
|
if (agentLogMode) exitAgentLogMode();
|
|
72
76
|
currentSessionId = null;
|
|
77
|
+
currentProjectPath = null;
|
|
78
|
+
currentProjectSessionIds = [];
|
|
73
79
|
const searchInput = document.getElementById('search-input');
|
|
74
80
|
if (searchInput) searchInput.value = '';
|
|
75
81
|
document.getElementById('search-clear-btn')?.classList.remove('visible');
|
|
@@ -410,6 +416,7 @@ let lastCurrentTasksHash = '';
|
|
|
410
416
|
async function fetchTasks(sessionId) {
|
|
411
417
|
try {
|
|
412
418
|
viewMode = 'session';
|
|
419
|
+
document.getElementById('message-toggle')?.style.removeProperty('display');
|
|
413
420
|
const res = await fetch(`/api/sessions/${sessionId}`);
|
|
414
421
|
|
|
415
422
|
let newTasks;
|
|
@@ -483,6 +490,98 @@ async function fetchAgents(sessionId) {
|
|
|
483
490
|
}
|
|
484
491
|
}
|
|
485
492
|
|
|
493
|
+
async function fetchProjectView(projectPath) {
|
|
494
|
+
viewMode = 'project';
|
|
495
|
+
currentProjectPath = projectPath;
|
|
496
|
+
currentSessionId = null;
|
|
497
|
+
currentMessages = [];
|
|
498
|
+
lastMessagesHash = '';
|
|
499
|
+
if (messagePanelOpen) toggleMessagePanel();
|
|
500
|
+
document.getElementById('message-toggle')?.style.setProperty('display', 'none');
|
|
501
|
+
const msgContent = document.getElementById('message-panel-content');
|
|
502
|
+
if (msgContent) msgContent.innerHTML = '';
|
|
503
|
+
const msgPinned = document.getElementById('message-panel-pinned');
|
|
504
|
+
if (msgPinned) msgPinned.innerHTML = '';
|
|
505
|
+
const projectSessions = sessions.filter((s) => s.project === projectPath);
|
|
506
|
+
currentProjectSessionIds = projectSessions.map((s) => s.id);
|
|
507
|
+
const activeSessionIds = projectSessions.filter(isSessionActive).map((s) => s.id);
|
|
508
|
+
|
|
509
|
+
const encoded = btoa(projectPath);
|
|
510
|
+
const [tasksResult, agentResults] = await Promise.all([
|
|
511
|
+
fetch(`/api/projects/${encodeURIComponent(encoded)}/tasks`)
|
|
512
|
+
.then((r) => r.json())
|
|
513
|
+
.catch((e) => {
|
|
514
|
+
console.error('[fetchProjectView] tasks:', e);
|
|
515
|
+
return [];
|
|
516
|
+
}),
|
|
517
|
+
Promise.all(
|
|
518
|
+
activeSessionIds.map((id) =>
|
|
519
|
+
fetch(`/api/sessions/${id}/agents`)
|
|
520
|
+
.then((r) => r.json())
|
|
521
|
+
.catch(() => ({ agents: [] })),
|
|
522
|
+
),
|
|
523
|
+
),
|
|
524
|
+
]);
|
|
525
|
+
currentTasks = tasksResult;
|
|
526
|
+
const seen = new Set();
|
|
527
|
+
currentAgents = [];
|
|
528
|
+
const mergedColors = {};
|
|
529
|
+
let mergedWaiting = null;
|
|
530
|
+
for (let i = 0; i < agentResults.length; i++) {
|
|
531
|
+
const r = agentResults[i];
|
|
532
|
+
const sid = activeSessionIds[i];
|
|
533
|
+
const agents = r.agents || (Array.isArray(r) ? r : []);
|
|
534
|
+
for (const a of agents) {
|
|
535
|
+
if (a.agentId && !seen.has(a.agentId)) {
|
|
536
|
+
seen.add(a.agentId);
|
|
537
|
+
a._sourceSessionId = sid;
|
|
538
|
+
currentAgents.push(a);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (r.teamColors) Object.assign(mergedColors, r.teamColors);
|
|
542
|
+
if (r.waitingForUser && !mergedWaiting) mergedWaiting = r.waitingForUser;
|
|
543
|
+
}
|
|
544
|
+
currentWaiting = mergedWaiting;
|
|
545
|
+
Object.assign(teamColorMap, mergedColors);
|
|
546
|
+
|
|
547
|
+
renderProjectView();
|
|
548
|
+
renderAgentFooter();
|
|
549
|
+
renderKanban();
|
|
550
|
+
updateUrl();
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async function refreshProjectAgents() {
|
|
554
|
+
if (!currentProjectPath) return;
|
|
555
|
+
const projectSessions = sessions.filter((s) => s.project === currentProjectPath);
|
|
556
|
+
const activeSessionIds = projectSessions.filter(isSessionActive).map((s) => s.id);
|
|
557
|
+
const agentResults = await Promise.all(
|
|
558
|
+
activeSessionIds.map((id) =>
|
|
559
|
+
fetch(`/api/sessions/${id}/agents`)
|
|
560
|
+
.then((r) => r.json())
|
|
561
|
+
.catch(() => ({ agents: [] })),
|
|
562
|
+
),
|
|
563
|
+
);
|
|
564
|
+
const seen = new Set();
|
|
565
|
+
currentAgents = [];
|
|
566
|
+
let mergedWaiting = null;
|
|
567
|
+
for (let i = 0; i < agentResults.length; i++) {
|
|
568
|
+
const r = agentResults[i];
|
|
569
|
+
const sid = activeSessionIds[i];
|
|
570
|
+
const agents = r.agents || (Array.isArray(r) ? r : []);
|
|
571
|
+
for (const a of agents) {
|
|
572
|
+
if (a.agentId && !seen.has(a.agentId)) {
|
|
573
|
+
seen.add(a.agentId);
|
|
574
|
+
a._sourceSessionId = sid;
|
|
575
|
+
currentAgents.push(a);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (r.teamColors) Object.assign(teamColorMap, r.teamColors);
|
|
579
|
+
if (r.waitingForUser && !mergedWaiting) mergedWaiting = r.waitingForUser;
|
|
580
|
+
}
|
|
581
|
+
currentWaiting = mergedWaiting;
|
|
582
|
+
renderAgentFooter();
|
|
583
|
+
}
|
|
584
|
+
|
|
486
585
|
//#endregion
|
|
487
586
|
|
|
488
587
|
//#region MESSAGE_PANEL
|
|
@@ -507,8 +606,10 @@ async function viewAgentLog(agentId) {
|
|
|
507
606
|
}
|
|
508
607
|
if (!agent) return;
|
|
509
608
|
const shortId = agentId.length > 8 ? agentId.slice(0, 8) : agentId;
|
|
510
|
-
|
|
609
|
+
const agentSessionId = agent._sourceSessionId || currentSessionId;
|
|
610
|
+
agentLogMode = { agentId, sessionId: agentSessionId, agentType: agent.type || 'unknown' };
|
|
511
611
|
closeAgentModal();
|
|
612
|
+
document.getElementById('message-toggle')?.style.removeProperty('display');
|
|
512
613
|
if (!messagePanelOpen) toggleMessagePanel();
|
|
513
614
|
const header = document.querySelector('.message-panel-header h3');
|
|
514
615
|
if (header) {
|
|
@@ -537,6 +638,11 @@ function exitAgentLogMode() {
|
|
|
537
638
|
agentLogSSE.close();
|
|
538
639
|
agentLogSSE = null;
|
|
539
640
|
}
|
|
641
|
+
if (viewMode === 'project') {
|
|
642
|
+
if (messagePanelOpen) toggleMessagePanel();
|
|
643
|
+
document.getElementById('message-toggle')?.style.setProperty('display', 'none');
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
540
646
|
const header = document.querySelector('.message-panel-header h3');
|
|
541
647
|
if (header) header.textContent = 'Session Log';
|
|
542
648
|
lastMessagesHash = '';
|
|
@@ -1784,6 +1890,7 @@ function renderSessions() {
|
|
|
1784
1890
|
<div class="session-progress">
|
|
1785
1891
|
<span class="session-indicators">
|
|
1786
1892
|
${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>` : ''}
|
|
1893
|
+
${session.sharedTaskList ? `<span class="shared-tasklist-badge" title="Shared task list: ${escapeHtml(session.sharedTaskList)}"><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="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></span>` : ''}
|
|
1787
1894
|
${isTeam || session.project || showCtx ? `<span class="team-info-btn" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" title="View session info">ℹ</span>` : ''}
|
|
1788
1895
|
${session.hasPlan ? `<span class="plan-indicator" onclick="event.stopPropagation(); openPlanForSession('${session.id}')" title="View plan"><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="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg></span>` : ''}
|
|
1789
1896
|
${session.hasRunningAgents ? '<span class="agent-badge" title="Active agents">🤖</span>' : ''}
|
|
@@ -1850,8 +1957,8 @@ function renderSessions() {
|
|
|
1850
1957
|
<svg class="group-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
|
1851
1958
|
<span class="group-name">${escapeHtml(folderName)}</span>
|
|
1852
1959
|
<span class="group-count">${projectSessions.length}</span>
|
|
1853
|
-
<span class="
|
|
1854
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><
|
|
1960
|
+
<span class="project-view-btn" data-project-path="${escapedPath}" title="Open project view — combined tasks from all sessions">
|
|
1961
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
|
|
1855
1962
|
</span>
|
|
1856
1963
|
</div>
|
|
1857
1964
|
<div class="project-group-breadcrumb" data-full-path="${escapedPath}" title="Click to copy path">${breadcrumbHtml}</div>
|
|
@@ -1969,12 +2076,37 @@ function renderSession() {
|
|
|
1969
2076
|
renderSessions();
|
|
1970
2077
|
}
|
|
1971
2078
|
|
|
2079
|
+
function renderProjectView() {
|
|
2080
|
+
noSession.style.display = 'none';
|
|
2081
|
+
sessionView.classList.add('visible');
|
|
2082
|
+
|
|
2083
|
+
const folderName = currentProjectPath ? currentProjectPath.split(/[/\\]/).pop() : 'Project';
|
|
2084
|
+
sessionTitle.textContent = folderName;
|
|
2085
|
+
|
|
2086
|
+
const metaParts = [`${currentProjectSessionIds.length} sessions`, `${currentTasks.length} tasks`];
|
|
2087
|
+
if (currentProjectPath) metaParts.push(currentProjectPath);
|
|
2088
|
+
sessionMeta.textContent = metaParts.join(' · ');
|
|
2089
|
+
|
|
2090
|
+
const completed = currentTasks.filter((t) => t.status === 'completed').length;
|
|
2091
|
+
const percent = currentTasks.length > 0 ? Math.round((completed / currentTasks.length) * 100) : 0;
|
|
2092
|
+
|
|
2093
|
+
progressPercent.textContent = `${percent}%`;
|
|
2094
|
+
progressBar.style.width = `${percent}%`;
|
|
2095
|
+
const hasInProgress = currentTasks.some((t) => t.status === 'in_progress');
|
|
2096
|
+
progressBar.classList.toggle('shimmer', hasInProgress && percent < 100);
|
|
2097
|
+
|
|
2098
|
+
updateOwnerFilter();
|
|
2099
|
+
renderKanban();
|
|
2100
|
+
renderSessions();
|
|
2101
|
+
}
|
|
2102
|
+
|
|
1972
2103
|
function renderTaskCard(task) {
|
|
1973
2104
|
const isBlocked = task.blockedBy && task.blockedBy.length > 0;
|
|
1974
|
-
const
|
|
2105
|
+
const useSlug = viewMode === 'all' || viewMode === 'project';
|
|
2106
|
+
const taskId = useSlug ? `${(task._taskDir || task.sessionId || '')?.slice(0, 4)}-${task.id}` : task.id;
|
|
1975
2107
|
const sessionLabel = viewMode === 'all' && task.sessionName ? task.sessionName : null;
|
|
1976
2108
|
const statusClass = task.status.replace('_', '-');
|
|
1977
|
-
const actualSessionId = task.sessionId || currentSessionId;
|
|
2109
|
+
const actualSessionId = task._taskDir || task.sessionId || currentSessionId || '';
|
|
1978
2110
|
|
|
1979
2111
|
return `
|
|
1980
2112
|
<div
|
|
@@ -2112,7 +2244,9 @@ async function onColumnDrop(e) {
|
|
|
2112
2244
|
return;
|
|
2113
2245
|
}
|
|
2114
2246
|
const { taskId, sessionId } = data;
|
|
2115
|
-
const task = currentTasks.find(
|
|
2247
|
+
const task = currentTasks.find(
|
|
2248
|
+
(t) => t.id === taskId && (t._taskDir === sessionId || (t.sessionId || currentSessionId) === sessionId),
|
|
2249
|
+
);
|
|
2116
2250
|
if (!task || task.status === newStatus) return;
|
|
2117
2251
|
try {
|
|
2118
2252
|
const res = await fetch(`/api/tasks/${sessionId}/${taskId}`, {
|
|
@@ -2150,7 +2284,10 @@ function getSelectedCardInfo() {
|
|
|
2150
2284
|
for (let ci = 0; ci < COLUMNS.length; ci++) {
|
|
2151
2285
|
const cards = Array.from(COLUMNS[ci].el.querySelectorAll('.task-card'));
|
|
2152
2286
|
for (let i = 0; i < cards.length; i++) {
|
|
2153
|
-
if (
|
|
2287
|
+
if (
|
|
2288
|
+
cards[i].dataset.taskId === selectedTaskId &&
|
|
2289
|
+
(!selectedSessionId || cards[i].dataset.sessionId === selectedSessionId)
|
|
2290
|
+
) {
|
|
2154
2291
|
return { colIndex: ci, cardIndex: i, card: cards[i] };
|
|
2155
2292
|
}
|
|
2156
2293
|
}
|
|
@@ -2410,7 +2547,9 @@ function getAvailableTasksOptions(currentTaskId = null) {
|
|
|
2410
2547
|
|
|
2411
2548
|
//#region TASK_DETAIL
|
|
2412
2549
|
async function showTaskDetail(taskId, sessionId = null) {
|
|
2413
|
-
let task = currentTasks.find(
|
|
2550
|
+
let task = currentTasks.find(
|
|
2551
|
+
(t) => t.id === taskId && (!sessionId || t.sessionId === sessionId || t._taskDir === sessionId),
|
|
2552
|
+
);
|
|
2414
2553
|
|
|
2415
2554
|
// If task not found in currentTasks, fetch it from the session
|
|
2416
2555
|
if (!task && sessionId && sessionId !== 'undefined') {
|
|
@@ -2502,20 +2641,25 @@ async function showTaskDetail(taskId, sessionId = null) {
|
|
|
2502
2641
|
</div>
|
|
2503
2642
|
`;
|
|
2504
2643
|
|
|
2505
|
-
// Setup button handlers
|
|
2644
|
+
// Setup button handlers (read-only in project view)
|
|
2506
2645
|
const deleteBtn = document.getElementById('delete-task-btn');
|
|
2507
|
-
|
|
2508
|
-
deleteBtn.
|
|
2646
|
+
const isProjectView = viewMode === 'project';
|
|
2647
|
+
deleteBtn.style.display = isProjectView ? 'none' : '';
|
|
2648
|
+
if (!isProjectView) deleteBtn.onclick = () => deleteTask(task.id, actualSessionId);
|
|
2509
2649
|
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2650
|
+
const noteSection = detailContent.querySelector('.note-section');
|
|
2651
|
+
if (noteSection && isProjectView) noteSection.style.display = 'none';
|
|
2652
|
+
|
|
2653
|
+
if (!isProjectView) {
|
|
2654
|
+
const titleEl = detailContent.querySelector('.detail-title');
|
|
2655
|
+
if (titleEl) {
|
|
2656
|
+
titleEl.onclick = () => editTitle(titleEl, task, actualSessionId);
|
|
2657
|
+
}
|
|
2515
2658
|
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2659
|
+
const descEl = detailContent.querySelector('.detail-desc');
|
|
2660
|
+
if (descEl) {
|
|
2661
|
+
descEl.onclick = () => editDescription(descEl, task, actualSessionId);
|
|
2662
|
+
}
|
|
2519
2663
|
}
|
|
2520
2664
|
}
|
|
2521
2665
|
|
|
@@ -2815,6 +2959,12 @@ const _scratchpadModal = document.getElementById('scratchpad-modal');
|
|
|
2815
2959
|
const _scratchpadTextarea = document.getElementById('scratchpad-textarea');
|
|
2816
2960
|
const _scratchpadCharcount = document.getElementById('scratchpad-charcount');
|
|
2817
2961
|
|
|
2962
|
+
function _scratchpadKey() {
|
|
2963
|
+
if (currentSessionId) return `scratchpad-${currentSessionId}`;
|
|
2964
|
+
if (currentProjectPath) return `scratchpad-project:${currentProjectPath}`;
|
|
2965
|
+
return null;
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2818
2968
|
function toggleScratchpad() {
|
|
2819
2969
|
if (_scratchpadModal.classList.contains('visible')) {
|
|
2820
2970
|
closeScratchpad();
|
|
@@ -2824,8 +2974,9 @@ function toggleScratchpad() {
|
|
|
2824
2974
|
}
|
|
2825
2975
|
|
|
2826
2976
|
function showScratchpad() {
|
|
2827
|
-
|
|
2828
|
-
|
|
2977
|
+
const key = _scratchpadKey();
|
|
2978
|
+
if (!key) return;
|
|
2979
|
+
_scratchpadTextarea.value = localStorage.getItem(key) || '';
|
|
2829
2980
|
_scratchpadCharcount.textContent = `${_scratchpadTextarea.value.length} chars`;
|
|
2830
2981
|
_scratchpadModal.classList.add('visible');
|
|
2831
2982
|
_scratchpadTextarea.focus();
|
|
@@ -2841,8 +2992,9 @@ function closeScratchpad() {
|
|
|
2841
2992
|
}
|
|
2842
2993
|
|
|
2843
2994
|
function saveScratchpad() {
|
|
2844
|
-
|
|
2845
|
-
|
|
2995
|
+
const key = _scratchpadKey();
|
|
2996
|
+
if (!key) return;
|
|
2997
|
+
localStorage.setItem(key, _scratchpadTextarea.value);
|
|
2846
2998
|
}
|
|
2847
2999
|
|
|
2848
3000
|
_scratchpadTextarea.addEventListener('input', () => {
|
|
@@ -3122,6 +3274,9 @@ function setupEventSource() {
|
|
|
3122
3274
|
currentTasks = filterProject ? allTasksCache.filter((t) => matchesProjectFilter(t.project)) : allTasksCache;
|
|
3123
3275
|
renderAllTasks();
|
|
3124
3276
|
renderLiveUpdatesFromCache();
|
|
3277
|
+
} else if (viewMode === 'project' && currentProjectPath) {
|
|
3278
|
+
const hasUpdate = currentProjectSessionIds.some((id) => pendingTaskSessionIds.has(id));
|
|
3279
|
+
if (hasUpdate) fetchProjectView(currentProjectPath);
|
|
3125
3280
|
} else if (currentSessionId && pendingTaskSessionIds.has(currentSessionId)) {
|
|
3126
3281
|
fetchTasks(currentSessionId);
|
|
3127
3282
|
}
|
|
@@ -3144,7 +3299,9 @@ function setupEventSource() {
|
|
|
3144
3299
|
|
|
3145
3300
|
if (data.type === 'agent-update') {
|
|
3146
3301
|
fetchSessions().catch((err) => console.error('[SSE] fetchSessions failed:', err));
|
|
3147
|
-
if (
|
|
3302
|
+
if (viewMode === 'project' && currentProjectSessionIds.includes(data.sessionId)) {
|
|
3303
|
+
refreshProjectAgents();
|
|
3304
|
+
} else if (currentSessionId && data.sessionId === currentSessionId) {
|
|
3148
3305
|
fetchAgents(currentSessionId);
|
|
3149
3306
|
}
|
|
3150
3307
|
}
|
|
@@ -3312,6 +3469,10 @@ function renderContextDetail(raw) {
|
|
|
3312
3469
|
//#endregion
|
|
3313
3470
|
|
|
3314
3471
|
//#region UTILS
|
|
3472
|
+
function isSessionActive(s) {
|
|
3473
|
+
return s.hasRecentLog || s.inProgress > 0 || s.hasActiveAgents || s.hasWaitingForUser;
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3315
3476
|
function formatDate(dateStr) {
|
|
3316
3477
|
const date = new Date(dateStr);
|
|
3317
3478
|
const now = new Date();
|
|
@@ -3494,6 +3655,14 @@ document.addEventListener('click', (e) => {
|
|
|
3494
3655
|
return;
|
|
3495
3656
|
}
|
|
3496
3657
|
|
|
3658
|
+
const projectBtn = e.target.closest('.project-view-btn');
|
|
3659
|
+
if (projectBtn) {
|
|
3660
|
+
e.stopPropagation();
|
|
3661
|
+
const projectPath = projectBtn.dataset.projectPath;
|
|
3662
|
+
if (projectPath) fetchProjectView(projectPath);
|
|
3663
|
+
return;
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3497
3666
|
const header = e.target.closest('.project-group-header');
|
|
3498
3667
|
if (header) {
|
|
3499
3668
|
setGroupCollapsed(header, !collapsedProjectGroups.has(header.dataset.groupPath));
|
|
@@ -3804,6 +3973,9 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
|
3804
3973
|
if (session.tasksDir) {
|
|
3805
3974
|
infoRows.push(['Tasks Dir', session.tasksDir, { openPath: session.tasksDir }]);
|
|
3806
3975
|
}
|
|
3976
|
+
if (session.sharedTaskList) {
|
|
3977
|
+
infoRows.push(['Shared Tasks', session.sharedTaskList]);
|
|
3978
|
+
}
|
|
3807
3979
|
if (teamConfig?.configPath) {
|
|
3808
3980
|
const configDir = teamConfig.configPath.replace(/[/\\][^/\\]+$/, '');
|
|
3809
3981
|
infoRows.push(['Team Config', teamConfig.configPath, { openPath: configDir, openFile: teamConfig.configPath }]);
|
|
@@ -4109,7 +4281,13 @@ if (urlState.search) {
|
|
|
4109
4281
|
}
|
|
4110
4282
|
|
|
4111
4283
|
fetchSessions().then(async () => {
|
|
4112
|
-
if (urlState.
|
|
4284
|
+
if (urlState.projectView) {
|
|
4285
|
+
try {
|
|
4286
|
+
await fetchProjectView(atob(urlState.projectView));
|
|
4287
|
+
} catch (_) {
|
|
4288
|
+
showAllTasks();
|
|
4289
|
+
}
|
|
4290
|
+
} else if (urlState.session) {
|
|
4113
4291
|
await fetchTasks(urlState.session);
|
|
4114
4292
|
} else {
|
|
4115
4293
|
showAllTasks();
|
|
@@ -4127,7 +4305,13 @@ window.addEventListener('popstate', () => {
|
|
|
4127
4305
|
ownerFilter = s.owner || '';
|
|
4128
4306
|
searchQuery = s.search || '';
|
|
4129
4307
|
loadPreferences();
|
|
4130
|
-
if (s.
|
|
4308
|
+
if (s.projectView) {
|
|
4309
|
+
try {
|
|
4310
|
+
fetchProjectView(atob(s.projectView));
|
|
4311
|
+
} catch (_) {
|
|
4312
|
+
showAllTasks();
|
|
4313
|
+
}
|
|
4314
|
+
} else if (s.session) fetchTasks(s.session);
|
|
4131
4315
|
else showAllTasks();
|
|
4132
4316
|
if (s.messages !== messagePanelOpen) toggleMessagePanel();
|
|
4133
4317
|
});
|
package/public/style.css
CHANGED
|
@@ -488,13 +488,16 @@ body::before {
|
|
|
488
488
|
display: block;
|
|
489
489
|
width: 100%;
|
|
490
490
|
padding: 12px 14px;
|
|
491
|
-
margin-bottom:
|
|
491
|
+
margin-bottom: 4px;
|
|
492
492
|
background: transparent;
|
|
493
493
|
border: 1px solid transparent;
|
|
494
494
|
border-radius: 8px;
|
|
495
495
|
text-align: left;
|
|
496
496
|
cursor: pointer;
|
|
497
497
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
498
|
+
box-shadow:
|
|
499
|
+
0 1px 3px rgba(0, 0, 0, 0.12),
|
|
500
|
+
0 1px 2px rgba(0, 0, 0, 0.08);
|
|
498
501
|
}
|
|
499
502
|
|
|
500
503
|
.session-item:hover {
|
|
@@ -1578,6 +1581,13 @@ body::before {
|
|
|
1578
1581
|
font-weight: 600;
|
|
1579
1582
|
}
|
|
1580
1583
|
|
|
1584
|
+
.shared-tasklist-badge {
|
|
1585
|
+
display: inline-flex;
|
|
1586
|
+
align-items: center;
|
|
1587
|
+
color: var(--accent);
|
|
1588
|
+
flex-shrink: 0;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1581
1591
|
.team-info-btn {
|
|
1582
1592
|
width: 24px;
|
|
1583
1593
|
height: 24px;
|
|
@@ -3002,6 +3012,18 @@ select.form-input option:checked {
|
|
|
3002
3012
|
white-space: nowrap;
|
|
3003
3013
|
}
|
|
3004
3014
|
|
|
3015
|
+
.project-view-btn {
|
|
3016
|
+
color: var(--text-muted);
|
|
3017
|
+
cursor: pointer;
|
|
3018
|
+
display: flex;
|
|
3019
|
+
align-items: center;
|
|
3020
|
+
transition: color 0.15s;
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
.project-view-btn:hover {
|
|
3024
|
+
color: var(--accent);
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3005
3027
|
.project-group-header .group-count {
|
|
3006
3028
|
font-weight: 400;
|
|
3007
3029
|
color: var(--text-muted);
|
package/server.js
CHANGED
|
@@ -15,7 +15,8 @@ const {
|
|
|
15
15
|
readSessionInfoFromJsonl,
|
|
16
16
|
buildAgentProgressMap,
|
|
17
17
|
readCompactSummaries,
|
|
18
|
-
findTerminatedTeammates
|
|
18
|
+
findTerminatedTeammates,
|
|
19
|
+
extractPromptFromTranscript
|
|
19
20
|
} = require('./lib/parsers');
|
|
20
21
|
|
|
21
22
|
const isSetupCommand = process.argv.includes('--install') || process.argv.includes('--uninstall');
|
|
@@ -189,6 +190,9 @@ const terminatedCache = new Map();
|
|
|
189
190
|
const compactSummaryCache = new Map();
|
|
190
191
|
const taskCountsCache = new Map();
|
|
191
192
|
const contextStatusCache = new Map();
|
|
193
|
+
const TASK_MAPS_DIR = path.join(AGENT_ACTIVITY_DIR, '_task-maps');
|
|
194
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
195
|
+
function isUUID(s) { return UUID_RE.test(s); }
|
|
192
196
|
|
|
193
197
|
function evictStaleCache(cache) {
|
|
194
198
|
if (cache.size <= MAX_CACHE_ENTRIES) return;
|
|
@@ -196,6 +200,47 @@ function evictStaleCache(cache) {
|
|
|
196
200
|
if (oldest !== undefined) cache.delete(oldest);
|
|
197
201
|
}
|
|
198
202
|
|
|
203
|
+
let sessionToTaskListCache = null;
|
|
204
|
+
let lastTaskMapScan = 0;
|
|
205
|
+
const TASK_MAP_SCAN_TTL = 5000;
|
|
206
|
+
|
|
207
|
+
function loadAllTaskMaps() {
|
|
208
|
+
const now = Date.now();
|
|
209
|
+
if (sessionToTaskListCache && now - lastTaskMapScan < TASK_MAP_SCAN_TTL) return sessionToTaskListCache;
|
|
210
|
+
|
|
211
|
+
const sessionToList = {};
|
|
212
|
+
const listToSessions = {};
|
|
213
|
+
if (!existsSync(TASK_MAPS_DIR)) {
|
|
214
|
+
sessionToTaskListCache = { sessionToList, listToSessions };
|
|
215
|
+
lastTaskMapScan = now;
|
|
216
|
+
return sessionToTaskListCache;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
for (const file of readdirSync(TASK_MAPS_DIR).filter(f => f.endsWith('.json'))) {
|
|
220
|
+
const taskListName = file.replace(/\.json$/, '');
|
|
221
|
+
const mapPath = path.join(TASK_MAPS_DIR, file);
|
|
222
|
+
try {
|
|
223
|
+
const map = JSON.parse(readFileSync(mapPath, 'utf8'));
|
|
224
|
+
listToSessions[taskListName] = map;
|
|
225
|
+
for (const sessionId of Object.keys(map)) {
|
|
226
|
+
sessionToList[sessionId] = taskListName;
|
|
227
|
+
}
|
|
228
|
+
} catch (e) { /* skip invalid */ }
|
|
229
|
+
}
|
|
230
|
+
} catch (e) { /* ignore */ }
|
|
231
|
+
sessionToTaskListCache = { sessionToList, listToSessions };
|
|
232
|
+
lastTaskMapScan = now;
|
|
233
|
+
return sessionToTaskListCache;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function getCustomTaskDir(sessionId) {
|
|
237
|
+
const { sessionToList } = loadAllTaskMaps();
|
|
238
|
+
const taskListName = sessionToList[sessionId];
|
|
239
|
+
if (!taskListName) return null;
|
|
240
|
+
const dir = path.join(TASKS_DIR, taskListName);
|
|
241
|
+
return existsSync(dir) ? dir : null;
|
|
242
|
+
}
|
|
243
|
+
|
|
199
244
|
function getTaskCounts(sessionPath) {
|
|
200
245
|
const cached = taskCountsCache.get(sessionPath);
|
|
201
246
|
if (cached) return cached;
|
|
@@ -449,7 +494,7 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
449
494
|
const entries = readdirSync(TASKS_DIR, { withFileTypes: true });
|
|
450
495
|
|
|
451
496
|
for (const entry of entries) {
|
|
452
|
-
if (entry.isDirectory()) {
|
|
497
|
+
if (entry.isDirectory() && isUUID(entry.name)) {
|
|
453
498
|
const sessionPath = path.join(TASKS_DIR, entry.name);
|
|
454
499
|
const stat = statSync(sessionPath);
|
|
455
500
|
const { taskCount, completed, inProgress, pending, newestTaskMtime } = getTaskCounts(sessionPath);
|
|
@@ -497,6 +542,44 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
497
542
|
}));
|
|
498
543
|
}
|
|
499
544
|
}
|
|
545
|
+
|
|
546
|
+
// Process custom task lists (non-UUID directories mapped via _task-maps)
|
|
547
|
+
const { listToSessions } = loadAllTaskMaps();
|
|
548
|
+
for (const [taskListName, map] of Object.entries(listToSessions)) {
|
|
549
|
+
const customTaskDir = path.join(TASKS_DIR, taskListName);
|
|
550
|
+
if (!existsSync(customTaskDir)) continue;
|
|
551
|
+
const counts = getTaskCounts(customTaskDir);
|
|
552
|
+
|
|
553
|
+
for (const [sessionId, info] of Object.entries(map)) {
|
|
554
|
+
const existing = sessionsMap.get(sessionId);
|
|
555
|
+
if (existing) {
|
|
556
|
+
Object.assign(existing, {
|
|
557
|
+
taskCount: counts.taskCount,
|
|
558
|
+
completed: counts.completed,
|
|
559
|
+
inProgress: counts.inProgress,
|
|
560
|
+
pending: counts.pending,
|
|
561
|
+
tasksDir: customTaskDir,
|
|
562
|
+
sharedTaskList: taskListName,
|
|
563
|
+
});
|
|
564
|
+
if (counts.newestTaskMtime) {
|
|
565
|
+
const taskMtime = counts.newestTaskMtime.toISOString();
|
|
566
|
+
if (taskMtime > existing.modifiedAt) existing.modifiedAt = taskMtime;
|
|
567
|
+
}
|
|
568
|
+
} else {
|
|
569
|
+
const meta = { ...(metadata[sessionId] || {}) };
|
|
570
|
+
if (!meta.project && info.project) meta.project = info.project;
|
|
571
|
+
sessionsMap.set(sessionId, buildSessionObject(sessionId, meta, {
|
|
572
|
+
taskCount: counts.taskCount,
|
|
573
|
+
completed: counts.completed,
|
|
574
|
+
inProgress: counts.inProgress,
|
|
575
|
+
pending: counts.pending,
|
|
576
|
+
modifiedAt: counts.newestTaskMtime ? counts.newestTaskMtime.toISOString() : (info.updatedAt || new Date(0).toISOString()),
|
|
577
|
+
tasksDir: customTaskDir,
|
|
578
|
+
sharedTaskList: taskListName,
|
|
579
|
+
}));
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
500
583
|
}
|
|
501
584
|
|
|
502
585
|
// Add sessions from metadata that don't have task directories
|
|
@@ -645,7 +728,8 @@ app.get('/api/projects', (req, res) => {
|
|
|
645
728
|
// API: Get tasks for a session
|
|
646
729
|
app.get('/api/sessions/:sessionId', async (req, res) => {
|
|
647
730
|
try {
|
|
648
|
-
const
|
|
731
|
+
const customDir = getCustomTaskDir(req.params.sessionId);
|
|
732
|
+
const sessionPath = customDir || path.join(TASKS_DIR, req.params.sessionId);
|
|
649
733
|
|
|
650
734
|
if (!existsSync(sessionPath)) {
|
|
651
735
|
return res.status(404).json({ error: 'Session not found' });
|
|
@@ -673,6 +757,52 @@ app.get('/api/sessions/:sessionId', async (req, res) => {
|
|
|
673
757
|
}
|
|
674
758
|
});
|
|
675
759
|
|
|
760
|
+
// API: Get combined tasks for a project (all sessions + shared task lists)
|
|
761
|
+
app.get('/api/projects/:encodedPath/tasks', (req, res) => {
|
|
762
|
+
try {
|
|
763
|
+
const projectPath = Buffer.from(req.params.encodedPath, 'base64').toString('utf8');
|
|
764
|
+
const metadata = loadSessionMetadata();
|
|
765
|
+
const { sessionToList } = loadAllTaskMaps();
|
|
766
|
+
|
|
767
|
+
const projectSessionIds = Object.entries(metadata)
|
|
768
|
+
.filter(([, m]) => m.project === projectPath)
|
|
769
|
+
.map(([id]) => id);
|
|
770
|
+
|
|
771
|
+
const taskDirs = new Set();
|
|
772
|
+
for (const sid of projectSessionIds) {
|
|
773
|
+
const listName = sessionToList[sid];
|
|
774
|
+
if (listName) {
|
|
775
|
+
const dir = path.join(TASKS_DIR, listName);
|
|
776
|
+
if (existsSync(dir)) taskDirs.add(dir);
|
|
777
|
+
} else {
|
|
778
|
+
const dir = path.join(TASKS_DIR, sid);
|
|
779
|
+
if (existsSync(dir)) taskDirs.add(dir);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const tasks = [];
|
|
784
|
+
const seenKeys = new Set();
|
|
785
|
+
for (const dir of taskDirs) {
|
|
786
|
+
for (const file of readdirSync(dir).filter(f => f.endsWith('.json'))) {
|
|
787
|
+
try {
|
|
788
|
+
const task = JSON.parse(readFileSync(path.join(dir, file), 'utf8'));
|
|
789
|
+
const key = `${dir}:${task.id}`;
|
|
790
|
+
if (!seenKeys.has(key)) {
|
|
791
|
+
seenKeys.add(key);
|
|
792
|
+
task._taskDir = path.basename(dir);
|
|
793
|
+
tasks.push(task);
|
|
794
|
+
}
|
|
795
|
+
} catch (_) {}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
tasks.sort((a, b) => parseInt(a.id) - parseInt(b.id));
|
|
799
|
+
res.json(tasks);
|
|
800
|
+
} catch (error) {
|
|
801
|
+
console.error('Error getting project tasks:', error);
|
|
802
|
+
res.status(500).json({ error: 'Failed to get project tasks' });
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
|
|
676
806
|
// API: Get session plan
|
|
677
807
|
app.get('/api/sessions/:sessionId/plan', async (req, res) => {
|
|
678
808
|
try {
|
|
@@ -821,23 +951,26 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
821
951
|
} catch (_) {}
|
|
822
952
|
}
|
|
823
953
|
|
|
954
|
+
function persistPrompt(agent, prompt) {
|
|
955
|
+
agent.prompt = prompt;
|
|
956
|
+
const agentFile = path.join(agentDir, agent.agentId + '.json');
|
|
957
|
+
fs.writeFile(agentFile, JSON.stringify(agent), 'utf8').catch(() => {});
|
|
958
|
+
}
|
|
959
|
+
|
|
824
960
|
const agentsNeedingPrompt = agents.filter(a => !a.prompt);
|
|
825
961
|
if (agentsNeedingPrompt.length && meta.jsonlPath) {
|
|
962
|
+
let byAgentId = {};
|
|
826
963
|
try {
|
|
827
964
|
const progressMap = getProgressMap(meta.jsonlPath);
|
|
828
|
-
const byAgentId = {};
|
|
829
965
|
for (const entry of Object.values(progressMap)) {
|
|
830
966
|
if (entry.prompt && !byAgentId[entry.agentId]) byAgentId[entry.agentId] = entry.prompt;
|
|
831
967
|
}
|
|
832
|
-
for (const agent of agentsNeedingPrompt) {
|
|
833
|
-
const prompt = byAgentId[agent.agentId];
|
|
834
|
-
if (prompt) {
|
|
835
|
-
agent.prompt = prompt;
|
|
836
|
-
const agentFile = path.join(agentDir, agent.agentId + '.json');
|
|
837
|
-
fs.writeFile(agentFile, JSON.stringify(agent), 'utf8').catch(() => {});
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
968
|
} catch (_) {}
|
|
969
|
+
for (const agent of agentsNeedingPrompt) {
|
|
970
|
+
const prompt = byAgentId[agent.agentId]
|
|
971
|
+
|| (() => { try { return extractPromptFromTranscript(subagentJsonlPath(meta, agent.agentId)); } catch (_) { return null; } })();
|
|
972
|
+
if (prompt) persistPrompt(agent, prompt);
|
|
973
|
+
}
|
|
841
974
|
}
|
|
842
975
|
const teamColors = {};
|
|
843
976
|
if (teamConfig?.members) {
|
|
@@ -1066,7 +1199,8 @@ app.post('/api/tasks/:sessionId/:taskId/note', async (req, res) => {
|
|
|
1066
1199
|
return res.status(400).json({ error: 'Note cannot be empty' });
|
|
1067
1200
|
}
|
|
1068
1201
|
|
|
1069
|
-
const
|
|
1202
|
+
const sessionDir = getCustomTaskDir(sessionId) || path.join(TASKS_DIR, sessionId);
|
|
1203
|
+
const taskPath = path.join(sessionDir, `${taskId}.json`);
|
|
1070
1204
|
|
|
1071
1205
|
if (!existsSync(taskPath)) {
|
|
1072
1206
|
return res.status(404).json({ error: 'Task not found' });
|
|
@@ -1095,7 +1229,8 @@ app.put('/api/tasks/:sessionId/:taskId', async (req, res) => {
|
|
|
1095
1229
|
const { sessionId, taskId } = req.params;
|
|
1096
1230
|
const { subject, description } = req.body;
|
|
1097
1231
|
|
|
1098
|
-
const
|
|
1232
|
+
const sessionDir = getCustomTaskDir(sessionId) || path.join(TASKS_DIR, sessionId);
|
|
1233
|
+
const taskPath = path.join(sessionDir, `${taskId}.json`);
|
|
1099
1234
|
|
|
1100
1235
|
if (!existsSync(taskPath)) {
|
|
1101
1236
|
return res.status(404).json({ error: 'Task not found' });
|
|
@@ -1120,14 +1255,14 @@ app.put('/api/tasks/:sessionId/:taskId', async (req, res) => {
|
|
|
1120
1255
|
app.delete('/api/tasks/:sessionId/:taskId', async (req, res) => {
|
|
1121
1256
|
try {
|
|
1122
1257
|
const { sessionId, taskId } = req.params;
|
|
1123
|
-
const
|
|
1258
|
+
const sessionPath = getCustomTaskDir(sessionId) || path.join(TASKS_DIR, sessionId);
|
|
1259
|
+
const taskPath = path.join(sessionPath, `${taskId}.json`);
|
|
1124
1260
|
|
|
1125
1261
|
if (!existsSync(taskPath)) {
|
|
1126
1262
|
return res.status(404).json({ error: 'Task not found' });
|
|
1127
1263
|
}
|
|
1128
1264
|
|
|
1129
1265
|
// Check if this task blocks other tasks
|
|
1130
|
-
const sessionPath = path.join(TASKS_DIR, sessionId);
|
|
1131
1266
|
const taskFiles = readdirSync(sessionPath).filter(f => f.endsWith('.json'));
|
|
1132
1267
|
|
|
1133
1268
|
for (const file of taskFiles) {
|
|
@@ -1200,21 +1335,44 @@ const watcher = chokidar.watch(TASKS_DIR, {
|
|
|
1200
1335
|
watcher.on('all', (event, filePath) => {
|
|
1201
1336
|
if ((event === 'add' || event === 'change' || event === 'unlink') && filePath.endsWith('.json')) {
|
|
1202
1337
|
const relativePath = path.relative(TASKS_DIR, filePath);
|
|
1203
|
-
const
|
|
1338
|
+
const dirName = relativePath.split(path.sep)[0];
|
|
1204
1339
|
|
|
1205
|
-
taskCountsCache.delete(path.join(TASKS_DIR,
|
|
1340
|
+
taskCountsCache.delete(path.join(TASKS_DIR, dirName));
|
|
1206
1341
|
|
|
1207
|
-
|
|
1208
|
-
type: 'update',
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
});
|
|
1342
|
+
if (isUUID(dirName)) {
|
|
1343
|
+
broadcast({ type: 'update', event, sessionId: dirName, file: path.basename(filePath) });
|
|
1344
|
+
} else {
|
|
1345
|
+
broadcastToMappedSessions(dirName, event, filePath);
|
|
1346
|
+
}
|
|
1213
1347
|
}
|
|
1214
1348
|
});
|
|
1215
1349
|
|
|
1350
|
+
function broadcastToMappedSessions(taskListName, event, filePath) {
|
|
1351
|
+
const { listToSessions } = loadAllTaskMaps();
|
|
1352
|
+
const map = listToSessions[taskListName];
|
|
1353
|
+
if (!map) return;
|
|
1354
|
+
for (const sid of Object.keys(map)) {
|
|
1355
|
+
broadcast({ type: 'update', event, sessionId: sid, file: path.basename(filePath) });
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1216
1359
|
console.log(`Watching for changes in: ${TASKS_DIR}`);
|
|
1217
1360
|
|
|
1361
|
+
// Watch task maps directory for session→task-list mapping changes
|
|
1362
|
+
const taskMapsWatcher = chokidar.watch(TASK_MAPS_DIR, {
|
|
1363
|
+
persistent: true,
|
|
1364
|
+
ignoreInitial: true,
|
|
1365
|
+
depth: 1
|
|
1366
|
+
});
|
|
1367
|
+
taskMapsWatcher.on('all', (event, filePath) => {
|
|
1368
|
+
if ((event === 'add' || event === 'change' || event === 'unlink') && filePath.endsWith('.json')) {
|
|
1369
|
+
lastTaskMapScan = 0;
|
|
1370
|
+
const taskListName = path.basename(filePath, '.json');
|
|
1371
|
+
taskCountsCache.delete(path.join(TASKS_DIR, taskListName));
|
|
1372
|
+
broadcastToMappedSessions(taskListName, event, filePath);
|
|
1373
|
+
}
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1218
1376
|
// Watch teams directory for config changes
|
|
1219
1377
|
const teamsWatcher = chokidar.watch(TEAMS_DIR, {
|
|
1220
1378
|
persistent: true,
|