Skip to content

Commit 8843096

Browse files
Invictoriusclaude
andcommitted
fix(v1.5.1): /clear migration, remove unused filter chips, footer polish
- watcher: trust session.json's sid when tracked sid is stale and they diverge (post-/clear). Previously _findFreshUnclaimedJsonl excluded the new sid because it appears in liveSessionIds, leaving the card stuck on the old jsonl. - ui: remove unused filter chips (active/waiting/completed). Toolbar, i18n, CSS cleaned up. Search remains via Cmd+F. - ui: footer padding 96px → 16px so it fits narrow windows; usage bars bumped to 120×10 for readability. - factor closeSearch() helper out of Cmd+F + empty-state reset paths. - test: regression for the post-/clear migration case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d0803b7 commit 8843096

7 files changed

Lines changed: 47 additions & 208 deletions

File tree

i18n.js

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,6 @@
1212
state_completed: 'Terminé',
1313

1414
// Toolbar tooltips
15-
filter_all: 'Tout',
16-
filter_active: 'Actives',
17-
filter_waiting: 'En attente',
18-
filter_completed: 'Terminées',
19-
filter_clear: 'Tout afficher',
2015
search_placeholder: 'Rechercher...',
2116
view_grid: 'Vue grille',
2217
view_compact: 'Vue compacte',
@@ -31,7 +26,7 @@
3126
empty_title: 'Aucune session Claude Code',
3227
empty_hint: 'Lancez <code>claude</code> dans un terminal ou utilisez le bouton +',
3328
empty_filtered_title: 'Aucun résultat',
34-
empty_filtered_hint: 'Essayez de modifier vos filtres ou votre recherche',
29+
empty_filtered_hint: 'Essayez une autre recherche',
3530
reset: 'Réinitialiser',
3631

3732
// Card details
@@ -157,11 +152,6 @@
157152
state_completed: 'Completed',
158153

159154
// Toolbar tooltips
160-
filter_all: 'All',
161-
filter_active: 'Active',
162-
filter_waiting: 'Waiting',
163-
filter_completed: 'Completed',
164-
filter_clear: 'Show all',
165155
search_placeholder: 'Search...',
166156
view_grid: 'Grid view',
167157
view_compact: 'Compact view',
@@ -176,7 +166,7 @@
176166
empty_title: 'No Claude Code sessions',
177167
empty_hint: 'Run <code>claude</code> in a terminal or use the + button',
178168
empty_filtered_title: 'No results',
179-
empty_filtered_hint: 'Try adjusting your filters or search',
169+
empty_filtered_hint: 'Try a different search',
180170
reset: 'Reset',
181171

182172
// Card details

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "aby-claude-watcher",
3-
"version": "1.5.0",
3+
"version": "1.5.1",
44
"description": "Aby Claude Watcher — dashboard desktop pour monitorer en temps réel vos sessions Claude Code",
55
"main": "main.js",
66
"scripts": {

test/watcher.test.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,28 @@ test('scan: lagged session.json + fresh /clear jsonl → migrate tracked id once
337337
if (events.length !== 0) throw new Error(`expected sticky no-op, got ${JSON.stringify(events)}`);
338338
});
339339

340+
test('scan: updated session.json + fresh /clear jsonl → migrate even when new sid in liveSessionIds', () => {
341+
// Single Claude, post-/clear. session.json was updated to NEW-id. On disk:
342+
// OLD.jsonl is stale, NEW.jsonl is fresh. Tracked: OLD-id (config-restored or
343+
// pre-/clear state). Regression: previously, _findFreshUnclaimedJsonl excluded
344+
// NEW-id because it appears in liveSessionIds, so migration silently failed
345+
// and the card stayed stuck on OLD-id. Expected: migrate OLD → NEW.
346+
const tree = makeFakeClaudeTree();
347+
const cwd = '/tmp/proj-clear';
348+
const now = Date.now();
349+
writeSessionJson(tree.sessions, 9101, 'NEW-id', cwd);
350+
writeJsonl(tree.projects, cwd, 'OLD-id', now - 60000); // stale (tracked)
351+
writeJsonl(tree.projects, cwd, 'NEW-id', now - 1000); // fresh (Claude is here now)
352+
353+
const w = freshScanWatcher(tree.root);
354+
w.sessions.set('OLD-id', makeSession('OLD-id', { pid: 9101, cwd, state: STATES.WAITING }));
355+
356+
w.scan();
357+
358+
if (w.sessions.has('OLD-id')) throw new Error('OLD-id must be migrated away');
359+
if (!w.sessions.has('NEW-id')) throw new Error('NEW-id must be tracked');
360+
});
361+
340362
test('scan: oscillation regression — two Claudes alternating writes, no flap after first attribution', () => {
341363
// Two Claudes in same cwd. Both /clear'd (both session.json sids stale).
342364
// Both wrote fresh post-/clear JSONLs. First scan attributes each pid to its

ui/index.html

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -26,28 +26,6 @@
2626

2727
<!-- Toolbar -->
2828
<div class="toolbar">
29-
<div class="filter-chips status-filter">
30-
<button class="filter-chip" data-filter="active" data-i18n-title="filter_active" title="Active">
31-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
32-
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
33-
</svg>
34-
</button>
35-
<button class="filter-chip" data-filter="waiting" data-i18n-title="filter_waiting" title="Waiting">
36-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
37-
<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>
38-
</svg>
39-
</button>
40-
<button class="filter-chip" data-filter="completed" data-i18n-title="filter_completed" title="Completed">
41-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
42-
<path d="M20 6 9 17l-5-5"/>
43-
</svg>
44-
</button>
45-
<button class="filter-clear" id="btnClearFilters" data-i18n-title="filter_clear" title="Show all" style="display:none;">
46-
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
47-
<path d="M18 6 6 18"/><path d="m6 6 12 12"/>
48-
</svg>
49-
</button>
50-
</div>
5129
<input type="text" class="search-input" id="searchInput" data-i18n-placeholder="search_placeholder" placeholder="Search..." style="display:none;">
5230

5331
<div class="toolbar-spacer"></div>
@@ -113,7 +91,7 @@
11391
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
11492
</svg>
11593
<p data-i18n="empty_filtered_title">No results</p>
116-
<p class="hint" data-i18n="empty_filtered_hint">Try adjusting your filters or search</p>
94+
<p class="hint" data-i18n="empty_filtered_hint">Try a different search</p>
11795
<button class="empty-action" id="btnClearAllFilters" data-i18n="reset">Reset</button>
11896
</div>
11997
<div class="grid-view" id="gridView" style="display:none;"></div>

ui/renderer.js

Lines changed: 12 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ let autoLaunch = false;
3131
let searchQuery = '';
3232
let sessionOrder = []; // User-defined order of session IDs
3333
let draggedId = null;
34-
let activeFilters = new Set(); // empty = all visible, or: 'active', 'waiting', 'completed'
3534

3635
// ═══ DOM refs ═══
3736

@@ -138,37 +137,8 @@ async function init() {
138137
$btnBack.addEventListener('click', () => setView(previousViewMode || 'grid'));
139138
$btnPinMicro.addEventListener('click', togglePin);
140139

141-
// Status filter chips (multi-select)
142-
document.querySelectorAll('.status-filter .filter-chip').forEach(chip => {
143-
chip.addEventListener('click', () => {
144-
const filter = chip.dataset.filter;
145-
if (activeFilters.has(filter)) {
146-
activeFilters.delete(filter);
147-
} else {
148-
activeFilters.add(filter);
149-
}
150-
updateFilterPills();
151-
render();
152-
});
153-
});
154-
155-
// Clear filters button
156-
document.getElementById('btnClearFilters').addEventListener('click', () => {
157-
activeFilters.clear();
158-
updateFilterPills();
159-
render();
160-
});
161-
162-
// Clear all (filters + search) from empty state
163-
document.getElementById('btnClearAllFilters').addEventListener('click', () => {
164-
activeFilters.clear();
165-
searchQuery = '';
166-
const search = document.getElementById('searchInput');
167-
search.value = '';
168-
search.style.display = 'none';
169-
updateFilterPills();
170-
render();
171-
});
140+
// Clear search from empty state
141+
document.getElementById('btnClearAllFilters').addEventListener('click', closeSearch);
172142
$btnPin.addEventListener('click', togglePin);
173143
$btnAdd.addEventListener('click', () => $addModal.style.display = 'flex');
174144
$addCancel.addEventListener('click', closeAddModal);
@@ -320,10 +290,7 @@ async function init() {
320290
search.style.display = 'block';
321291
search.focus();
322292
} else {
323-
search.style.display = 'none';
324-
search.value = '';
325-
searchQuery = '';
326-
render();
293+
closeSearch();
327294
}
328295
}
329296

@@ -393,15 +360,15 @@ function updatePinButton() {
393360
$btnPin.classList.toggle('active', alwaysOnTop);
394361
}
395362

396-
397-
function updateFilterPills() {
398-
document.querySelectorAll('.status-filter .filter-chip').forEach(p => {
399-
p.classList.toggle('active', activeFilters.has(p.dataset.filter));
400-
});
401-
const clearBtn = document.getElementById('btnClearFilters');
402-
clearBtn.style.display = activeFilters.size > 0 ? 'inline-flex' : 'none';
363+
function closeSearch() {
364+
const search = document.getElementById('searchInput');
365+
search.style.display = 'none';
366+
search.value = '';
367+
searchQuery = '';
368+
render();
403369
}
404370

371+
405372
function updateNotifPosition() {
406373
$notificationOverlay.setAttribute('data-position', notifPosition);
407374
document.querySelectorAll('.position-btn').forEach(btn => {
@@ -576,7 +543,6 @@ function getRenderableSessions() {
576543

577544
function render() {
578545
const count = sessions.size;
579-
const filtersActive = activeFilters.size > 0 || searchQuery;
580546
const visibleCount = getRenderableSessions().length;
581547

582548
const showNoSessions = count === 0;
@@ -672,18 +638,6 @@ function getSortedSessions() {
672638
);
673639
}
674640

675-
// Filter by active filter pills (multi-select, OR logic)
676-
if (activeFilters.size > 0) {
677-
arr = arr.filter(s => {
678-
const name = s.state.name;
679-
if (activeFilters.has('active') && (name === 'running' || name === 'thinking')) return true;
680-
if (activeFilters.has('waiting') && (name === 'waiting' || name === 'pending')) return true;
681-
if (activeFilters.has('completed') && name === 'completed') return true;
682-
return false;
683-
});
684-
}
685-
686-
687641
// Separate completed from active
688642
const active = arr.filter(s => s.state.name !== 'completed');
689643
const completed = arr.filter(s => s.state.name === 'completed');
@@ -1305,11 +1259,8 @@ function updateStatusBar() {
13051259
const totalInput = all.reduce((sum, s) => sum + (s.tokens?.input || 0), 0);
13061260
const totalOutput = all.reduce((sum, s) => sum + (s.tokens?.output || 0), 0);
13071261

1308-
const filtersActive = activeFilters.size > 0 || searchQuery;
1309-
const visibleCount = filtersActive ? getSortedSessions().length : all.length;
1310-
1311-
const activeLabel = filtersActive
1312-
? t('status_filtered', { visible: visibleCount, total: all.length })
1262+
const activeLabel = searchQuery
1263+
? t('status_filtered', { visible: getSortedSessions().length, total: all.length })
13131264
: t('status_active', { n: active.length });
13141265

13151266
document.getElementById('statActive').textContent = activeLabel;

ui/styles.css

Lines changed: 4 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -367,113 +367,6 @@ body.micro-mode .usage-reset-icon {
367367
-webkit-app-region: no-drag;
368368
}
369369

370-
.filter-chips {
371-
display: inline-flex;
372-
align-items: center;
373-
gap: 3px;
374-
-webkit-app-region: no-drag;
375-
position: relative;
376-
}
377-
378-
.filter-chip {
379-
display: inline-flex;
380-
align-items: center;
381-
justify-content: center;
382-
width: 24px;
383-
height: 22px;
384-
background: transparent;
385-
border: none;
386-
border-radius: 3px;
387-
color: var(--text-muted);
388-
cursor: pointer;
389-
transition: all var(--transition);
390-
position: relative;
391-
}
392-
393-
.filter-chip:hover {
394-
color: var(--text-secondary);
395-
}
396-
397-
.filter-chip.active {
398-
background: var(--bg-tertiary);
399-
}
400-
401-
.filter-chip[data-filter="active"].active { color: var(--state-running); }
402-
.filter-chip[data-filter="waiting"].active { color: var(--state-waiting); }
403-
.filter-chip[data-filter="completed"].active { color: var(--text-secondary); }
404-
405-
.filter-chip::after {
406-
content: attr(title);
407-
position: absolute;
408-
top: calc(100% + 6px);
409-
left: 50%;
410-
transform: translateX(-50%);
411-
background: var(--bg-secondary);
412-
color: var(--text-primary);
413-
padding: 3px 8px;
414-
border-radius: var(--radius-sm);
415-
border: 1px solid var(--border);
416-
font-size: 11px;
417-
font-family: var(--font-sans);
418-
white-space: nowrap;
419-
opacity: 0;
420-
pointer-events: none;
421-
transition: opacity 0.15s ease-out;
422-
z-index: 100;
423-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
424-
}
425-
426-
.filter-chip:hover::after {
427-
opacity: 1;
428-
transition-delay: 0.3s;
429-
}
430-
431-
.filter-clear {
432-
display: inline-flex;
433-
align-items: center;
434-
justify-content: center;
435-
width: 20px;
436-
height: 20px;
437-
margin-left: 2px;
438-
padding: 0;
439-
background: transparent;
440-
border: none;
441-
border-radius: 50%;
442-
color: var(--text-muted);
443-
cursor: pointer;
444-
transition: all var(--transition);
445-
position: relative;
446-
}
447-
448-
.filter-clear:hover {
449-
background: var(--bg-tertiary);
450-
color: var(--text-primary);
451-
}
452-
453-
.filter-clear::after {
454-
content: attr(title);
455-
position: absolute;
456-
top: calc(100% + 6px);
457-
left: 50%;
458-
transform: translateX(-50%);
459-
background: var(--bg-secondary);
460-
color: var(--text-primary);
461-
padding: 3px 8px;
462-
border-radius: var(--radius-sm);
463-
border: 1px solid var(--border);
464-
font-size: 11px;
465-
white-space: nowrap;
466-
opacity: 0;
467-
pointer-events: none;
468-
transition: opacity 0.15s ease-out;
469-
z-index: 100;
470-
}
471-
472-
.filter-clear:hover::after {
473-
opacity: 1;
474-
transition-delay: 0.3s;
475-
}
476-
477370
.filter-seg {
478371
display: inline-flex;
479372
align-items: center;
@@ -1831,7 +1724,7 @@ body.micro-mode .usage-reset-icon {
18311724
align-items: center;
18321725
justify-content: center;
18331726
gap: 8px;
1834-
padding: 6px 96px;
1727+
padding: 6px 16px;
18351728
background: var(--bg-secondary);
18361729
border-top: 1px solid var(--border);
18371730
font-size: 11px;
@@ -1863,11 +1756,11 @@ body.micro-mode .usage-reset-icon {
18631756
}
18641757

18651758
.usage-bar {
1866-
width: 90px;
1867-
height: 6px;
1759+
width: 120px;
1760+
height: 10px;
18681761
background: var(--bg-primary);
18691762
border: 1px solid var(--border);
1870-
border-radius: 3px;
1763+
border-radius: 5px;
18711764
overflow: hidden;
18721765
}
18731766

watcher.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,12 @@ class SessionWatcher extends EventEmitter {
164164
if (trackedId) {
165165
if (!this._isSidStale(trackedId, cwd)) {
166166
effectiveId = trackedId;
167+
} else if (sessionId !== trackedId) {
168+
// trackedId is stale and session.json reports a different sid —
169+
// trust session.json (e.g., after /clear created a new JSONL).
170+
effectiveId = sessionId;
167171
} else {
172+
// session.json is lagged on the same stale sid: scan unclaimed JSONLs.
168173
const fresh = this._findFreshUnclaimedJsonl(cwd, liveSessionIds, claimedJsonls);
169174
effectiveId = fresh || trackedId;
170175
}

0 commit comments

Comments
 (0)