claude-task-viewer 1.5.0 → 1.7.1

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,6 +26,9 @@ 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
+ ### Search
30
+ Find tasks instantly. Type in the search box to filter across all columns by subject or description. Case-insensitive, real-time results.
31
+
29
32
  ### Project Filtering
30
33
  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
34
 
@@ -90,6 +93,9 @@ PORT=8080 npx claude-task-viewer
90
93
 
91
94
  # Open browser automatically
92
95
  npx claude-task-viewer --open
96
+
97
+ # Use a different Claude config directory (for multiple accounts)
98
+ npx claude-task-viewer --dir=~/.claude-work
93
99
  ```
94
100
 
95
101
  ## API
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-task-viewer",
3
- "version": "1.5.0",
3
+ "version": "1.7.1",
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
@@ -327,6 +327,19 @@
327
327
  margin-top: 2px;
328
328
  white-space: nowrap;
329
329
  overflow: hidden;
330
+ }
331
+
332
+ .session-branch {
333
+ font-size: 10px;
334
+ color: var(--accent);
335
+ margin-top: 3px;
336
+ padding: 2px 6px;
337
+ background: var(--border);
338
+ border-radius: 3px;
339
+ display: inline-block;
340
+ font-family: var(--mono);
341
+ white-space: nowrap;
342
+ overflow: hidden;
330
343
  text-overflow: ellipsis;
331
344
  }
332
345
 
@@ -469,26 +482,6 @@
469
482
  color: var(--accent);
470
483
  }
471
484
 
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
-
492
485
  .icon-btn {
493
486
  width: 32px;
494
487
  height: 32px;
@@ -1039,8 +1032,6 @@
1039
1032
  <p id="session-meta" class="view-meta"></p>
1040
1033
  </div>
1041
1034
  <div class="view-actions">
1042
- <input type="text" id="task-search" class="search-input"
1043
- placeholder="Search tasks..." oninput="searchTasks(this.value)">
1044
1035
  <div class="view-progress">
1045
1036
  <div class="progress-bar">
1046
1037
  <div id="progress-bar" class="progress-fill" style="width: 0%"></div>
@@ -1112,7 +1103,6 @@
1112
1103
  let viewMode = 'session';
1113
1104
  let sessionFilter = localStorage.getItem('sessionFilter') || 'all'; // 'all' or 'active'
1114
1105
  let filterProject = null; // null = all projects, or project path to filter
1115
- let searchQuery = '';
1116
1106
 
1117
1107
  // DOM
1118
1108
  const sessionsList = document.getElementById('sessions-list');
@@ -1184,7 +1174,6 @@
1184
1174
  async function fetchTasks(sessionId) {
1185
1175
  try {
1186
1176
  viewMode = 'session';
1187
- clearSearch();
1188
1177
  const res = await fetch(`/api/sessions/${sessionId}`);
1189
1178
  currentTasks = await res.json();
1190
1179
  currentSessionId = sessionId;
@@ -1198,7 +1187,6 @@
1198
1187
  try {
1199
1188
  viewMode = 'all';
1200
1189
  currentSessionId = null;
1201
- clearSearch();
1202
1190
  const res = await fetch('/api/tasks/all');
1203
1191
  let tasks = await res.json();
1204
1192
  if (filterProject) {
@@ -1282,6 +1270,7 @@
1282
1270
  ${hasInProgress ? '<span class="pulse"></span>' : ''}
1283
1271
  </div>
1284
1272
  ${secondaryName ? `<div class="session-secondary">${escapeHtml(secondaryName)}</div>` : ''}
1273
+ ${session.gitBranch ? `<div class="session-branch">${escapeHtml(session.gitBranch)}</div>` : ''}
1285
1274
  <div class="session-progress">
1286
1275
  <div class="progress-bar"><div class="progress-fill" style="width: ${percent}%"></div></div>
1287
1276
  <span class="progress-text">${session.completed}/${total}</span>
@@ -1302,7 +1291,8 @@
1302
1291
  const displayName = session.name || currentSessionId;
1303
1292
  sessionTitle.textContent = displayName;
1304
1293
  const projectName = session.project ? session.project.split('/').pop() : null;
1305
- sessionMeta.textContent = `${currentTasks.length} tasks${projectName ? ' · ' + projectName : ''} · ${formatDate(session.modifiedAt)}`;
1294
+ const branchInfo = session.gitBranch ? ` · ${session.gitBranch}` : '';
1295
+ sessionMeta.textContent = `${currentTasks.length} tasks${projectName ? ' · ' + projectName : ''}${branchInfo} · ${formatDate(session.modifiedAt)}`;
1306
1296
 
1307
1297
  const completed = currentTasks.filter(t => t.status === 'completed').length;
1308
1298
  const percent = currentTasks.length > 0 ? Math.round((completed / currentTasks.length) * 100) : 0;
@@ -1336,17 +1326,9 @@
1336
1326
  }
1337
1327
 
1338
1328
  function renderKanban() {
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');
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');
1350
1332
 
1351
1333
  pendingCount.textContent = pending.length;
1352
1334
  inProgressCount.textContent = inProgress.length;
@@ -1538,17 +1520,6 @@
1538
1520
  showAllTasks();
1539
1521
  }
1540
1522
 
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
1523
  function updateProjectDropdown() {
1553
1524
  const dropdown = document.getElementById('project-filter');
1554
1525
  const projects = [...new Set(sessions.map(s => s.project).filter(Boolean))].sort();
package/server.js CHANGED
@@ -10,7 +10,24 @@ const os = require('os');
10
10
 
11
11
  const app = express();
12
12
  const PORT = process.env.PORT || 3456;
13
- const CLAUDE_DIR = process.env.CLAUDE_DIR || path.join(os.homedir(), '.claude');
13
+
14
+ // Parse --dir flag for custom Claude directory
15
+ function getClaudeDir() {
16
+ const dirIndex = process.argv.findIndex(arg => arg.startsWith('--dir'));
17
+ if (dirIndex !== -1) {
18
+ const arg = process.argv[dirIndex];
19
+ if (arg.includes('=')) {
20
+ const dir = arg.split('=')[1];
21
+ return dir.startsWith('~') ? dir.replace('~', os.homedir()) : dir;
22
+ } else if (process.argv[dirIndex + 1]) {
23
+ const dir = process.argv[dirIndex + 1];
24
+ return dir.startsWith('~') ? dir.replace('~', os.homedir()) : dir;
25
+ }
26
+ }
27
+ return process.env.CLAUDE_DIR || path.join(os.homedir(), '.claude');
28
+ }
29
+
30
+ const CLAUDE_DIR = getClaudeDir();
14
31
  const TASKS_DIR = path.join(CLAUDE_DIR, 'tasks');
15
32
  const PROJECTS_DIR = path.join(CLAUDE_DIR, 'projects');
16
33
 
@@ -352,45 +369,43 @@ function broadcast(data) {
352
369
  }
353
370
  }
354
371
 
355
- // Watch for file changes
356
- if (existsSync(TASKS_DIR)) {
357
- const watcher = chokidar.watch(TASKS_DIR, {
358
- persistent: true,
359
- ignoreInitial: true,
360
- depth: 2
361
- });
372
+ // Watch for file changes (chokidar handles non-existent paths)
373
+ const watcher = chokidar.watch(TASKS_DIR, {
374
+ persistent: true,
375
+ ignoreInitial: true,
376
+ depth: 2
377
+ });
362
378
 
363
- watcher.on('all', (event, filePath) => {
364
- if (filePath.endsWith('.json')) {
365
- const relativePath = path.relative(TASKS_DIR, filePath);
366
- const sessionId = relativePath.split(path.sep)[0];
367
-
368
- broadcast({
369
- type: 'update',
370
- event,
371
- sessionId,
372
- file: path.basename(filePath)
373
- });
374
- }
375
- });
379
+ watcher.on('all', (event, filePath) => {
380
+ if (filePath.endsWith('.json')) {
381
+ const relativePath = path.relative(TASKS_DIR, filePath);
382
+ const sessionId = relativePath.split(path.sep)[0];
383
+
384
+ broadcast({
385
+ type: 'update',
386
+ event,
387
+ sessionId,
388
+ file: path.basename(filePath)
389
+ });
390
+ }
391
+ });
376
392
 
377
- console.log(`Watching for changes in: ${TASKS_DIR}`);
378
- }
393
+ console.log(`Watching for changes in: ${TASKS_DIR}`);
379
394
 
380
395
  // Also watch projects dir for metadata changes
381
- if (existsSync(PROJECTS_DIR)) {
382
- const projectsWatcher = chokidar.watch(path.join(PROJECTS_DIR, '*/*.jsonl'), {
383
- persistent: true,
384
- ignoreInitial: true,
385
- depth: 1
386
- });
396
+ const projectsWatcher = chokidar.watch(PROJECTS_DIR, {
397
+ persistent: true,
398
+ ignoreInitial: true,
399
+ depth: 2
400
+ });
387
401
 
388
- projectsWatcher.on('all', (event) => {
402
+ projectsWatcher.on('all', (event, filePath) => {
403
+ if (filePath.endsWith('.jsonl')) {
389
404
  // Invalidate cache on any change
390
405
  lastMetadataRefresh = 0;
391
406
  broadcast({ type: 'metadata-update' });
392
- });
393
- }
407
+ }
408
+ });
394
409
 
395
410
  // Start server
396
411
  app.listen(PORT, () => {