claude-task-viewer 1.3.0 → 1.5.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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-task-viewer",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "A web-based Kanban board for viewing Claude Code tasks",
5
5
  "main": "server.js",
6
6
  "bin": {
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-toggle {
167
+ .filter-row {
202
168
  display: flex;
203
- align-items: center;
204
- gap: 6px;
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-toggle input[type="checkbox"] {
173
+ .filter-dropdown {
174
+ flex: 1;
211
175
  appearance: none;
212
- width: 10px;
213
- height: 10px;
214
- border: 1px solid var(--text-muted);
215
- border-radius: 2px;
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-toggle input[type="checkbox"]:checked {
220
- background: var(--accent);
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-project {
324
+ .session-secondary {
359
325
  font-size: 11px;
360
- color: var(--text-tertiary);
361
- margin-top: 4px;
326
+ color: var(--text-muted);
327
+ margin-top: 2px;
362
328
  white-space: nowrap;
363
329
  overflow: hidden;
364
330
  text-overflow: ellipsis;
@@ -503,6 +469,26 @@
503
469
  color: var(--accent);
504
470
  }
505
471
 
472
+ .search-input {
473
+ background: var(--bg-elevated);
474
+ border: 1px solid var(--border);
475
+ border-radius: 6px;
476
+ padding: 8px 12px;
477
+ font-family: var(--mono);
478
+ font-size: 12px;
479
+ color: var(--text-primary);
480
+ width: 200px;
481
+ }
482
+
483
+ .search-input:focus {
484
+ outline: none;
485
+ border-color: var(--accent);
486
+ }
487
+
488
+ .search-input::placeholder {
489
+ color: var(--text-muted);
490
+ }
491
+
506
492
  .icon-btn {
507
493
  width: 32px;
508
494
  height: 32px;
@@ -1013,23 +999,21 @@
1013
999
  </div>
1014
1000
  </div>
1015
1001
 
1016
- <!-- Sessions -->
1002
+ <!-- Tasks -->
1017
1003
  <div class="sidebar-section flex-1">
1018
1004
  <div class="section-header">
1019
- <span>Sessions</span>
1020
- <label class="filter-toggle">
1021
- <input type="checkbox" id="hide-inactive" onchange="toggleHideInactive()">
1022
- <span>Active only</span>
1023
- </label>
1005
+ <span>Tasks</span>
1024
1006
  </div>
1025
- <div id="sessions-list" class="sessions-list">
1026
- <button id="all-tasks-btn" class="nav-item" onclick="showAllTasks()">
1027
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1028
- <path d="M4 6h16M4 12h16M4 18h16"/>
1029
- </svg>
1030
- <span>All Tasks</span>
1031
- </button>
1007
+ <div class="filter-row">
1008
+ <select id="project-filter" class="filter-dropdown" onchange="filterByProject(this.value)">
1009
+ <option value="">All Projects</option>
1010
+ </select>
1011
+ <select id="session-filter" class="filter-dropdown" onchange="filterBySessions(this.value)">
1012
+ <option value="all">All Sessions</option>
1013
+ <option value="active">Active Only</option>
1014
+ </select>
1032
1015
  </div>
1016
+ <div id="sessions-list" class="sessions-list"></div>
1033
1017
  </div>
1034
1018
 
1035
1019
  <footer class="sidebar-footer">
@@ -1055,6 +1039,8 @@
1055
1039
  <p id="session-meta" class="view-meta"></p>
1056
1040
  </div>
1057
1041
  <div class="view-actions">
1042
+ <input type="text" id="task-search" class="search-input"
1043
+ placeholder="Search tasks..." oninput="searchTasks(this.value)">
1058
1044
  <div class="view-progress">
1059
1045
  <div class="progress-bar">
1060
1046
  <div id="progress-bar" class="progress-fill" style="width: 0%"></div>
@@ -1124,7 +1110,9 @@
1124
1110
  let currentSessionId = null;
1125
1111
  let currentTasks = [];
1126
1112
  let viewMode = 'session';
1127
- let hideInactive = localStorage.getItem('hideInactive') === 'true';
1113
+ let sessionFilter = localStorage.getItem('sessionFilter') || 'all'; // 'all' or 'active'
1114
+ let filterProject = null; // null = all projects, or project path to filter
1115
+ let searchQuery = '';
1128
1116
 
1129
1117
  // DOM
1130
1118
  const sessionsList = document.getElementById('sessions-list');
@@ -1159,7 +1147,10 @@
1159
1147
  try {
1160
1148
  const res = await fetch('/api/tasks/all');
1161
1149
  const allTasks = await res.json();
1162
- const activeTasks = allTasks.filter(t => t.status === 'in_progress');
1150
+ let activeTasks = allTasks.filter(t => t.status === 'in_progress');
1151
+ if (filterProject) {
1152
+ activeTasks = activeTasks.filter(t => t.project === filterProject);
1153
+ }
1163
1154
  renderLiveUpdates(activeTasks);
1164
1155
  } catch (error) {
1165
1156
  console.error('Failed to fetch live updates:', error);
@@ -1193,6 +1184,7 @@
1193
1184
  async function fetchTasks(sessionId) {
1194
1185
  try {
1195
1186
  viewMode = 'session';
1187
+ clearSearch();
1196
1188
  const res = await fetch(`/api/sessions/${sessionId}`);
1197
1189
  currentTasks = await res.json();
1198
1190
  currentSessionId = sessionId;
@@ -1206,8 +1198,13 @@
1206
1198
  try {
1207
1199
  viewMode = 'all';
1208
1200
  currentSessionId = null;
1201
+ clearSearch();
1209
1202
  const res = await fetch('/api/tasks/all');
1210
- currentTasks = await res.json();
1203
+ let tasks = await res.json();
1204
+ if (filterProject) {
1205
+ tasks = tasks.filter(t => t.project === filterProject);
1206
+ }
1207
+ currentTasks = tasks;
1211
1208
  renderAllTasks();
1212
1209
  renderSessions();
1213
1210
  } catch (error) {
@@ -1223,8 +1220,11 @@
1223
1220
  const completed = currentTasks.filter(t => t.status === 'completed').length;
1224
1221
  const percent = totalTasks > 0 ? Math.round((completed / totalTasks) * 100) : 0;
1225
1222
 
1226
- sessionTitle.textContent = 'All Tasks';
1227
- sessionMeta.textContent = `${totalTasks} tasks across ${sessions.length} sessions`;
1223
+ const projectName = filterProject ? filterProject.split('/').pop() : null;
1224
+ sessionTitle.textContent = filterProject ? `Tasks: ${projectName}` : 'All Tasks';
1225
+ sessionMeta.textContent = filterProject
1226
+ ? `${totalTasks} tasks in this project`
1227
+ : `${totalTasks} tasks across ${sessions.length} sessions`;
1228
1228
  progressPercent.textContent = `${percent}%`;
1229
1229
  progressBar.style.width = `${percent}%`;
1230
1230
 
@@ -1232,46 +1232,56 @@
1232
1232
  }
1233
1233
 
1234
1234
  function renderSessions() {
1235
- const allTasksBtn = document.getElementById('all-tasks-btn');
1236
- allTasksBtn.classList.toggle('active', viewMode === 'all');
1237
-
1238
- const filteredSessions = hideInactive
1239
- ? sessions.filter(s => s.pending > 0 || s.inProgress > 0)
1240
- : sessions;
1241
-
1242
- // Get container for session items (after All Tasks button)
1243
- let sessionsContainer = document.getElementById('sessions-items');
1244
- if (!sessionsContainer) {
1245
- sessionsContainer = document.createElement('div');
1246
- sessionsContainer.id = 'sessions-items';
1247
- sessionsList.appendChild(sessionsContainer);
1235
+ // Update project dropdown
1236
+ updateProjectDropdown();
1237
+
1238
+ let filteredSessions = sessions;
1239
+ if (sessionFilter === 'active') {
1240
+ filteredSessions = filteredSessions.filter(s => s.pending > 0 || s.inProgress > 0);
1241
+ }
1242
+ if (filterProject) {
1243
+ filteredSessions = filteredSessions.filter(s => s.project === filterProject);
1248
1244
  }
1249
1245
 
1250
1246
  if (filteredSessions.length === 0) {
1251
- sessionsContainer.innerHTML = `
1247
+ let emptyMsg = 'No sessions found';
1248
+ let emptyHint = 'Tasks appear when you use Claude Code';
1249
+ if (filterProject && sessionFilter === 'active') {
1250
+ emptyMsg = 'No active sessions for this project';
1251
+ emptyHint = 'Try "All Sessions" or "All Projects"';
1252
+ } else if (filterProject) {
1253
+ emptyMsg = 'No sessions for this project';
1254
+ emptyHint = 'Select "All Projects" to see all';
1255
+ } else if (sessionFilter === 'active') {
1256
+ emptyMsg = 'No active sessions';
1257
+ emptyHint = 'Select "All Sessions" to see all';
1258
+ }
1259
+ sessionsList.innerHTML = `
1252
1260
  <div style="padding: 24px 12px; text-align: center; color: var(--text-muted); font-size: 12px;">
1253
- <p>${hideInactive ? 'No active sessions' : 'No sessions found'}</p>
1254
- <p style="margin-top: 8px; font-size: 11px;">${hideInactive ? 'Uncheck filter to see all' : 'Tasks appear when you use Claude Code'}</p>
1261
+ <p>${emptyMsg}</p>
1262
+ <p style="margin-top: 8px; font-size: 11px;">${emptyHint}</p>
1255
1263
  </div>
1256
1264
  `;
1257
1265
  return;
1258
1266
  }
1259
1267
 
1260
- sessionsContainer.innerHTML = filteredSessions.map(session => {
1268
+ sessionsList.innerHTML = filteredSessions.map(session => {
1261
1269
  const total = session.taskCount;
1262
1270
  const percent = total > 0 ? Math.round((session.completed / total) * 100) : 0;
1263
1271
  const isActive = session.id === currentSessionId && viewMode === 'session';
1264
1272
  const hasInProgress = session.inProgress > 0;
1265
- const displayName = session.name || session.id.slice(0, 8) + '...';
1273
+ const sessionName = session.name || session.id.slice(0, 8) + '...';
1266
1274
  const projectName = session.project ? session.project.split('/').pop() : null;
1275
+ const primaryName = projectName || sessionName;
1276
+ const secondaryName = projectName ? sessionName : null;
1267
1277
 
1268
1278
  return `
1269
1279
  <button onclick="fetchTasks('${session.id}')" class="session-item ${isActive ? 'active' : ''}">
1270
1280
  <div class="session-name">
1271
- <span>${escapeHtml(displayName)}</span>
1281
+ <span>${escapeHtml(primaryName)}</span>
1272
1282
  ${hasInProgress ? '<span class="pulse"></span>' : ''}
1273
1283
  </div>
1274
- ${projectName ? `<div class="session-project">${escapeHtml(projectName)}</div>` : ''}
1284
+ ${secondaryName ? `<div class="session-secondary">${escapeHtml(secondaryName)}</div>` : ''}
1275
1285
  <div class="session-progress">
1276
1286
  <div class="progress-bar"><div class="progress-fill" style="width: ${percent}%"></div></div>
1277
1287
  <span class="progress-text">${session.completed}/${total}</span>
@@ -1326,9 +1336,17 @@
1326
1336
  }
1327
1337
 
1328
1338
  function renderKanban() {
1329
- const pending = currentTasks.filter(t => t.status === 'pending');
1330
- const inProgress = currentTasks.filter(t => t.status === 'in_progress');
1331
- const completed = currentTasks.filter(t => t.status === 'completed');
1339
+ let filteredTasks = currentTasks;
1340
+ if (searchQuery) {
1341
+ filteredTasks = currentTasks.filter(t =>
1342
+ t.subject.toLowerCase().includes(searchQuery) ||
1343
+ (t.description && t.description.toLowerCase().includes(searchQuery))
1344
+ );
1345
+ }
1346
+
1347
+ const pending = filteredTasks.filter(t => t.status === 'pending');
1348
+ const inProgress = filteredTasks.filter(t => t.status === 'in_progress');
1349
+ const completed = filteredTasks.filter(t => t.status === 'completed');
1332
1350
 
1333
1351
  pendingCount.textContent = pending.length;
1334
1352
  inProgressCount.textContent = inProgress.length;
@@ -1507,10 +1525,40 @@
1507
1525
  return div.innerHTML;
1508
1526
  }
1509
1527
 
1510
- function toggleHideInactive() {
1511
- hideInactive = document.getElementById('hide-inactive').checked;
1512
- localStorage.setItem('hideInactive', hideInactive);
1528
+ function filterBySessions(value) {
1529
+ sessionFilter = value;
1530
+ localStorage.setItem('sessionFilter', sessionFilter);
1531
+ renderSessions();
1532
+ }
1533
+
1534
+ function filterByProject(project) {
1535
+ filterProject = project || null;
1513
1536
  renderSessions();
1537
+ fetchLiveUpdates();
1538
+ showAllTasks();
1539
+ }
1540
+
1541
+ function searchTasks(query) {
1542
+ searchQuery = query.toLowerCase();
1543
+ renderKanban();
1544
+ }
1545
+
1546
+ function clearSearch() {
1547
+ searchQuery = '';
1548
+ const searchInput = document.getElementById('task-search');
1549
+ if (searchInput) searchInput.value = '';
1550
+ }
1551
+
1552
+ function updateProjectDropdown() {
1553
+ const dropdown = document.getElementById('project-filter');
1554
+ const projects = [...new Set(sessions.map(s => s.project).filter(Boolean))].sort();
1555
+
1556
+ dropdown.innerHTML = '<option value="">All Projects</option>' +
1557
+ projects.map(p => {
1558
+ const name = p.split('/').pop();
1559
+ const selected = p === filterProject ? ' selected' : '';
1560
+ return `<option value="${p}"${selected} title="${escapeHtml(p)}">${escapeHtml(name)}</option>`;
1561
+ }).join('');
1514
1562
  }
1515
1563
 
1516
1564
  function toggleTheme() {
@@ -1529,7 +1577,7 @@
1529
1577
  }
1530
1578
 
1531
1579
  function loadPreferences() {
1532
- document.getElementById('hide-inactive').checked = hideInactive;
1580
+ document.getElementById('session-filter').value = sessionFilter;
1533
1581
  }
1534
1582
 
1535
1583
  // Init