claude-task-viewer 1.3.0 → 1.4.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/README.md +4 -1
- package/package.json +1 -1
- package/public/index.html +107 -103
package/README.md
CHANGED
|
@@ -26,8 +26,11 @@ Tasks can block other tasks. The viewer shows these relationships clearly — bl
|
|
|
26
26
|
### Notes
|
|
27
27
|
Add context to any task. Your notes are appended to the task description, so Claude sees them when it reads the task. Use this to clarify requirements, add constraints, or redirect work — all without interrupting Claude's flow.
|
|
28
28
|
|
|
29
|
+
### Project Filtering
|
|
30
|
+
Filter tasks by project using the dropdown. Working on multiple codebases? See only what's relevant. Combine with the session filter to show just active sessions for a specific project.
|
|
31
|
+
|
|
29
32
|
### Session Management
|
|
30
|
-
Browse all your Claude Code sessions with progress indicators. Filter to active sessions only. Each session shows completion percentage
|
|
33
|
+
Browse all your Claude Code sessions with progress indicators. Filter to active sessions only, or show everything. Each session shows completion percentage.
|
|
31
34
|
|
|
32
35
|
## Installation
|
|
33
36
|
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -139,40 +139,6 @@
|
|
|
139
139
|
background: #ef4444;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
/* Nav item */
|
|
143
|
-
.nav-item {
|
|
144
|
-
display: flex;
|
|
145
|
-
align-items: center;
|
|
146
|
-
gap: 10px;
|
|
147
|
-
width: 100%;
|
|
148
|
-
padding: 10px 12px;
|
|
149
|
-
background: transparent;
|
|
150
|
-
border: none;
|
|
151
|
-
border-radius: 6px;
|
|
152
|
-
color: var(--text-secondary);
|
|
153
|
-
font-family: var(--mono);
|
|
154
|
-
font-size: 12px;
|
|
155
|
-
cursor: pointer;
|
|
156
|
-
transition: all 0.15s ease;
|
|
157
|
-
text-align: left;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
.nav-item:hover {
|
|
161
|
-
background: var(--bg-hover);
|
|
162
|
-
color: var(--text-primary);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
.nav-item.active {
|
|
166
|
-
background: var(--bg-elevated);
|
|
167
|
-
color: var(--text-primary);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
.nav-item svg {
|
|
171
|
-
width: 16px;
|
|
172
|
-
height: 16px;
|
|
173
|
-
opacity: 0.6;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
142
|
/* Sidebar sections */
|
|
177
143
|
.sidebar-section {
|
|
178
144
|
display: flex;
|
|
@@ -198,26 +164,37 @@
|
|
|
198
164
|
color: var(--text-muted);
|
|
199
165
|
}
|
|
200
166
|
|
|
201
|
-
.filter-
|
|
167
|
+
.filter-row {
|
|
202
168
|
display: flex;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
font-weight: 400;
|
|
206
|
-
color: var(--text-tertiary);
|
|
207
|
-
cursor: pointer;
|
|
169
|
+
gap: 8px;
|
|
170
|
+
padding: 0 12px 12px;
|
|
208
171
|
}
|
|
209
172
|
|
|
210
|
-
.filter-
|
|
173
|
+
.filter-dropdown {
|
|
174
|
+
flex: 1;
|
|
211
175
|
appearance: none;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
border:
|
|
215
|
-
|
|
176
|
+
background: var(--bg-elevated);
|
|
177
|
+
border: 1px solid var(--border);
|
|
178
|
+
border-radius: 4px;
|
|
179
|
+
padding: 6px 24px 6px 10px;
|
|
180
|
+
font-family: var(--mono);
|
|
181
|
+
font-size: 11px;
|
|
182
|
+
color: var(--text-secondary);
|
|
216
183
|
cursor: pointer;
|
|
184
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%235a5c60' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
|
|
185
|
+
background-repeat: no-repeat;
|
|
186
|
+
background-position: right 8px center;
|
|
187
|
+
text-overflow: ellipsis;
|
|
188
|
+
min-width: 0;
|
|
217
189
|
}
|
|
218
190
|
|
|
219
|
-
.filter-
|
|
220
|
-
background: var(--
|
|
191
|
+
.filter-dropdown option {
|
|
192
|
+
background: var(--bg-surface);
|
|
193
|
+
color: var(--text-primary);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.filter-dropdown:focus {
|
|
197
|
+
outline: none;
|
|
221
198
|
border-color: var(--accent);
|
|
222
199
|
}
|
|
223
200
|
|
|
@@ -292,17 +269,6 @@
|
|
|
292
269
|
padding: 0 12px 12px;
|
|
293
270
|
}
|
|
294
271
|
|
|
295
|
-
.sessions-list .nav-item {
|
|
296
|
-
margin-bottom: 8px;
|
|
297
|
-
border-bottom: 1px solid var(--border);
|
|
298
|
-
padding-bottom: 12px;
|
|
299
|
-
border-radius: 0;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
.sessions-list .nav-item:hover {
|
|
303
|
-
background: transparent;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
272
|
.session-item {
|
|
307
273
|
display: block;
|
|
308
274
|
width: 100%;
|
|
@@ -355,10 +321,10 @@
|
|
|
355
321
|
50% { opacity: 0.6; transform: scale(0.9); }
|
|
356
322
|
}
|
|
357
323
|
|
|
358
|
-
.session-
|
|
324
|
+
.session-secondary {
|
|
359
325
|
font-size: 11px;
|
|
360
|
-
color: var(--text-
|
|
361
|
-
margin-top:
|
|
326
|
+
color: var(--text-muted);
|
|
327
|
+
margin-top: 2px;
|
|
362
328
|
white-space: nowrap;
|
|
363
329
|
overflow: hidden;
|
|
364
330
|
text-overflow: ellipsis;
|
|
@@ -1013,23 +979,21 @@
|
|
|
1013
979
|
</div>
|
|
1014
980
|
</div>
|
|
1015
981
|
|
|
1016
|
-
<!--
|
|
982
|
+
<!-- Tasks -->
|
|
1017
983
|
<div class="sidebar-section flex-1">
|
|
1018
984
|
<div class="section-header">
|
|
1019
|
-
<span>
|
|
1020
|
-
<label class="filter-toggle">
|
|
1021
|
-
<input type="checkbox" id="hide-inactive" onchange="toggleHideInactive()">
|
|
1022
|
-
<span>Active only</span>
|
|
1023
|
-
</label>
|
|
985
|
+
<span>Tasks</span>
|
|
1024
986
|
</div>
|
|
1025
|
-
<div
|
|
1026
|
-
<
|
|
1027
|
-
<
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
<
|
|
1031
|
-
|
|
987
|
+
<div class="filter-row">
|
|
988
|
+
<select id="project-filter" class="filter-dropdown" onchange="filterByProject(this.value)">
|
|
989
|
+
<option value="">All Projects</option>
|
|
990
|
+
</select>
|
|
991
|
+
<select id="session-filter" class="filter-dropdown" onchange="filterBySessions(this.value)">
|
|
992
|
+
<option value="all">All Sessions</option>
|
|
993
|
+
<option value="active">Active Only</option>
|
|
994
|
+
</select>
|
|
1032
995
|
</div>
|
|
996
|
+
<div id="sessions-list" class="sessions-list"></div>
|
|
1033
997
|
</div>
|
|
1034
998
|
|
|
1035
999
|
<footer class="sidebar-footer">
|
|
@@ -1124,7 +1088,8 @@
|
|
|
1124
1088
|
let currentSessionId = null;
|
|
1125
1089
|
let currentTasks = [];
|
|
1126
1090
|
let viewMode = 'session';
|
|
1127
|
-
let
|
|
1091
|
+
let sessionFilter = localStorage.getItem('sessionFilter') || 'all'; // 'all' or 'active'
|
|
1092
|
+
let filterProject = null; // null = all projects, or project path to filter
|
|
1128
1093
|
|
|
1129
1094
|
// DOM
|
|
1130
1095
|
const sessionsList = document.getElementById('sessions-list');
|
|
@@ -1159,7 +1124,10 @@
|
|
|
1159
1124
|
try {
|
|
1160
1125
|
const res = await fetch('/api/tasks/all');
|
|
1161
1126
|
const allTasks = await res.json();
|
|
1162
|
-
|
|
1127
|
+
let activeTasks = allTasks.filter(t => t.status === 'in_progress');
|
|
1128
|
+
if (filterProject) {
|
|
1129
|
+
activeTasks = activeTasks.filter(t => t.project === filterProject);
|
|
1130
|
+
}
|
|
1163
1131
|
renderLiveUpdates(activeTasks);
|
|
1164
1132
|
} catch (error) {
|
|
1165
1133
|
console.error('Failed to fetch live updates:', error);
|
|
@@ -1207,7 +1175,11 @@
|
|
|
1207
1175
|
viewMode = 'all';
|
|
1208
1176
|
currentSessionId = null;
|
|
1209
1177
|
const res = await fetch('/api/tasks/all');
|
|
1210
|
-
|
|
1178
|
+
let tasks = await res.json();
|
|
1179
|
+
if (filterProject) {
|
|
1180
|
+
tasks = tasks.filter(t => t.project === filterProject);
|
|
1181
|
+
}
|
|
1182
|
+
currentTasks = tasks;
|
|
1211
1183
|
renderAllTasks();
|
|
1212
1184
|
renderSessions();
|
|
1213
1185
|
} catch (error) {
|
|
@@ -1223,8 +1195,11 @@
|
|
|
1223
1195
|
const completed = currentTasks.filter(t => t.status === 'completed').length;
|
|
1224
1196
|
const percent = totalTasks > 0 ? Math.round((completed / totalTasks) * 100) : 0;
|
|
1225
1197
|
|
|
1226
|
-
|
|
1227
|
-
|
|
1198
|
+
const projectName = filterProject ? filterProject.split('/').pop() : null;
|
|
1199
|
+
sessionTitle.textContent = filterProject ? `Tasks: ${projectName}` : 'All Tasks';
|
|
1200
|
+
sessionMeta.textContent = filterProject
|
|
1201
|
+
? `${totalTasks} tasks in this project`
|
|
1202
|
+
: `${totalTasks} tasks across ${sessions.length} sessions`;
|
|
1228
1203
|
progressPercent.textContent = `${percent}%`;
|
|
1229
1204
|
progressBar.style.width = `${percent}%`;
|
|
1230
1205
|
|
|
@@ -1232,46 +1207,56 @@
|
|
|
1232
1207
|
}
|
|
1233
1208
|
|
|
1234
1209
|
function renderSessions() {
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
if (!sessionsContainer) {
|
|
1245
|
-
sessionsContainer = document.createElement('div');
|
|
1246
|
-
sessionsContainer.id = 'sessions-items';
|
|
1247
|
-
sessionsList.appendChild(sessionsContainer);
|
|
1210
|
+
// Update project dropdown
|
|
1211
|
+
updateProjectDropdown();
|
|
1212
|
+
|
|
1213
|
+
let filteredSessions = sessions;
|
|
1214
|
+
if (sessionFilter === 'active') {
|
|
1215
|
+
filteredSessions = filteredSessions.filter(s => s.pending > 0 || s.inProgress > 0);
|
|
1216
|
+
}
|
|
1217
|
+
if (filterProject) {
|
|
1218
|
+
filteredSessions = filteredSessions.filter(s => s.project === filterProject);
|
|
1248
1219
|
}
|
|
1249
1220
|
|
|
1250
1221
|
if (filteredSessions.length === 0) {
|
|
1251
|
-
|
|
1222
|
+
let emptyMsg = 'No sessions found';
|
|
1223
|
+
let emptyHint = 'Tasks appear when you use Claude Code';
|
|
1224
|
+
if (filterProject && sessionFilter === 'active') {
|
|
1225
|
+
emptyMsg = 'No active sessions for this project';
|
|
1226
|
+
emptyHint = 'Try "All Sessions" or "All Projects"';
|
|
1227
|
+
} else if (filterProject) {
|
|
1228
|
+
emptyMsg = 'No sessions for this project';
|
|
1229
|
+
emptyHint = 'Select "All Projects" to see all';
|
|
1230
|
+
} else if (sessionFilter === 'active') {
|
|
1231
|
+
emptyMsg = 'No active sessions';
|
|
1232
|
+
emptyHint = 'Select "All Sessions" to see all';
|
|
1233
|
+
}
|
|
1234
|
+
sessionsList.innerHTML = `
|
|
1252
1235
|
<div style="padding: 24px 12px; text-align: center; color: var(--text-muted); font-size: 12px;">
|
|
1253
|
-
<p>${
|
|
1254
|
-
<p style="margin-top: 8px; font-size: 11px;">${
|
|
1236
|
+
<p>${emptyMsg}</p>
|
|
1237
|
+
<p style="margin-top: 8px; font-size: 11px;">${emptyHint}</p>
|
|
1255
1238
|
</div>
|
|
1256
1239
|
`;
|
|
1257
1240
|
return;
|
|
1258
1241
|
}
|
|
1259
1242
|
|
|
1260
|
-
|
|
1243
|
+
sessionsList.innerHTML = filteredSessions.map(session => {
|
|
1261
1244
|
const total = session.taskCount;
|
|
1262
1245
|
const percent = total > 0 ? Math.round((session.completed / total) * 100) : 0;
|
|
1263
1246
|
const isActive = session.id === currentSessionId && viewMode === 'session';
|
|
1264
1247
|
const hasInProgress = session.inProgress > 0;
|
|
1265
|
-
const
|
|
1248
|
+
const sessionName = session.name || session.id.slice(0, 8) + '...';
|
|
1266
1249
|
const projectName = session.project ? session.project.split('/').pop() : null;
|
|
1250
|
+
const primaryName = projectName || sessionName;
|
|
1251
|
+
const secondaryName = projectName ? sessionName : null;
|
|
1267
1252
|
|
|
1268
1253
|
return `
|
|
1269
1254
|
<button onclick="fetchTasks('${session.id}')" class="session-item ${isActive ? 'active' : ''}">
|
|
1270
1255
|
<div class="session-name">
|
|
1271
|
-
<span>${escapeHtml(
|
|
1256
|
+
<span>${escapeHtml(primaryName)}</span>
|
|
1272
1257
|
${hasInProgress ? '<span class="pulse"></span>' : ''}
|
|
1273
1258
|
</div>
|
|
1274
|
-
${
|
|
1259
|
+
${secondaryName ? `<div class="session-secondary">${escapeHtml(secondaryName)}</div>` : ''}
|
|
1275
1260
|
<div class="session-progress">
|
|
1276
1261
|
<div class="progress-bar"><div class="progress-fill" style="width: ${percent}%"></div></div>
|
|
1277
1262
|
<span class="progress-text">${session.completed}/${total}</span>
|
|
@@ -1507,12 +1492,31 @@
|
|
|
1507
1492
|
return div.innerHTML;
|
|
1508
1493
|
}
|
|
1509
1494
|
|
|
1510
|
-
function
|
|
1511
|
-
|
|
1512
|
-
localStorage.setItem('
|
|
1495
|
+
function filterBySessions(value) {
|
|
1496
|
+
sessionFilter = value;
|
|
1497
|
+
localStorage.setItem('sessionFilter', sessionFilter);
|
|
1513
1498
|
renderSessions();
|
|
1514
1499
|
}
|
|
1515
1500
|
|
|
1501
|
+
function filterByProject(project) {
|
|
1502
|
+
filterProject = project || null;
|
|
1503
|
+
renderSessions();
|
|
1504
|
+
fetchLiveUpdates();
|
|
1505
|
+
showAllTasks();
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
function updateProjectDropdown() {
|
|
1509
|
+
const dropdown = document.getElementById('project-filter');
|
|
1510
|
+
const projects = [...new Set(sessions.map(s => s.project).filter(Boolean))].sort();
|
|
1511
|
+
|
|
1512
|
+
dropdown.innerHTML = '<option value="">All Projects</option>' +
|
|
1513
|
+
projects.map(p => {
|
|
1514
|
+
const name = p.split('/').pop();
|
|
1515
|
+
const selected = p === filterProject ? ' selected' : '';
|
|
1516
|
+
return `<option value="${p}"${selected} title="${escapeHtml(p)}">${escapeHtml(name)}</option>`;
|
|
1517
|
+
}).join('');
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1516
1520
|
function toggleTheme() {
|
|
1517
1521
|
const isLight = document.body.classList.toggle('light');
|
|
1518
1522
|
localStorage.setItem('theme', isLight ? 'light' : 'dark');
|
|
@@ -1529,7 +1533,7 @@
|
|
|
1529
1533
|
}
|
|
1530
1534
|
|
|
1531
1535
|
function loadPreferences() {
|
|
1532
|
-
document.getElementById('
|
|
1536
|
+
document.getElementById('session-filter').value = sessionFilter;
|
|
1533
1537
|
}
|
|
1534
1538
|
|
|
1535
1539
|
// Init
|