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 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.4.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;
@@ -1013,23 +979,21 @@
1013
979
  </div>
1014
980
  </div>
1015
981
 
1016
- <!-- Sessions -->
982
+ <!-- Tasks -->
1017
983
  <div class="sidebar-section flex-1">
1018
984
  <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>
985
+ <span>Tasks</span>
1024
986
  </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>
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 hideInactive = localStorage.getItem('hideInactive') === 'true';
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
- const activeTasks = allTasks.filter(t => t.status === 'in_progress');
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
- currentTasks = await res.json();
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
- sessionTitle.textContent = 'All Tasks';
1227
- sessionMeta.textContent = `${totalTasks} tasks across ${sessions.length} sessions`;
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
- 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);
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
- sessionsContainer.innerHTML = `
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>${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>
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
- sessionsContainer.innerHTML = filteredSessions.map(session => {
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 displayName = session.name || session.id.slice(0, 8) + '...';
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(displayName)}</span>
1256
+ <span>${escapeHtml(primaryName)}</span>
1272
1257
  ${hasInProgress ? '<span class="pulse"></span>' : ''}
1273
1258
  </div>
1274
- ${projectName ? `<div class="session-project">${escapeHtml(projectName)}</div>` : ''}
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 toggleHideInactive() {
1511
- hideInactive = document.getElementById('hide-inactive').checked;
1512
- localStorage.setItem('hideInactive', hideInactive);
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('hide-inactive').checked = hideInactive;
1536
+ document.getElementById('session-filter').value = sessionFilter;
1533
1537
  }
1534
1538
 
1535
1539
  // Init