Skip to content

Commit 984e123

Browse files
Invictoriusclaude
andcommitted
fix(v1.2.1): phantom-session cleanup, copy-to-clipboard reliability
- markCompleted now distinguishes three cases: normal completion, silent crash after an explicit resume (→ error), and pure phantom with no activity and no resume context (→ deleted from memory & config so ghost cards don't pile up). - Copy ID now uses Electron's native clipboard via IPC (renderer navigator.clipboard was failing silently) and copies the slug actually shown on the card — falls back to the full UUID when no slug exists. - Micro view hides completed sessions to keep the ambient signal clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d0c43be commit 984e123

5 files changed

Lines changed: 47 additions & 17 deletions

File tree

main.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { app, BrowserWindow, ipcMain, Notification, session, Tray, Menu, nativeImage, shell, screen } = require('electron');
1+
const { app, BrowserWindow, ipcMain, Notification, session, Tray, Menu, nativeImage, shell, screen, clipboard } = require('electron');
22
const path = require('path');
33
const { SessionWatcher, STATES } = require('./watcher');
44
const { SocketServer } = require('./socket');
@@ -328,6 +328,12 @@ function setupIPC() {
328328
};
329329
});
330330

331+
ipcMain.handle('copy-to-clipboard', (_, text) => {
332+
if (typeof text !== 'string') return false;
333+
clipboard.writeText(text);
334+
return true;
335+
});
336+
331337
ipcMain.handle('popover-hide', () => {
332338
if (popoverWindow && !popoverWindow.isDestroyed()) popoverWindow.hide();
333339
});

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.2.0",
3+
"version": "1.2.1",
44
"description": "Aby Claude Watcher — dashboard desktop pour monitorer en temps réel vos sessions Claude Code",
55
"main": "main.js",
66
"scripts": {

preload.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ contextBridge.exposeInMainWorld('api', {
3636
launchSession: (cwd) => ipcRenderer.invoke('launch-session', cwd),
3737
getLanguage: () => ipcRenderer.invoke('get-language'),
3838
setLanguage: (lang) => ipcRenderer.invoke('set-language', lang),
39+
copyToClipboard: (text) => ipcRenderer.invoke('copy-to-clipboard', text),
3940

4041
onSessionAdded: (callback) => ipcRenderer.on('session-added', (_, data) => callback(data)),
4142
onSessionUpdated: (callback) => ipcRenderer.on('session-updated', (_, data) => callback(data)),

ui/renderer.js

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -567,10 +567,17 @@ function showUpdateBanner(info) {
567567

568568
// ═══ Rendering ═══
569569

570+
function getRenderableSessions() {
571+
const arr = getSortedSessions();
572+
// Micro view hides completed sessions to keep the ambient signal clean
573+
if (viewMode === 'micro') return arr.filter(s => s.state.name !== 'completed');
574+
return arr;
575+
}
576+
570577
function render() {
571578
const count = sessions.size;
572579
const filtersActive = activeFilters.size > 0 || searchQuery;
573-
const visibleCount = getSortedSessions().length;
580+
const visibleCount = getRenderableSessions().length;
574581

575582
const showNoSessions = count === 0;
576583
const showNoResults = count > 0 && visibleCount === 0;
@@ -600,7 +607,7 @@ function viewItemHTML() {
600607
}
601608

602609
function fullRender() {
603-
const sorted = getSortedSessions();
610+
const sorted = getRenderableSessions();
604611
const htmlFn = viewItemHTML();
605612
viewContainer().innerHTML = sorted.map(s => htmlFn(s)).join('');
606613
}
@@ -626,6 +633,7 @@ function updateSession(s) {
626633
return;
627634
}
628635

636+
629637
// Patch in place — replace the element's HTML
630638
const htmlFn = viewItemHTML();
631639
const temp = document.createElement('div');
@@ -715,7 +723,7 @@ function cardHTML(s) {
715723
<div class="card-header">
716724
<div class="card-title">
717725
<div class="project-name">${esc(s.customName || s.projectName)}</div>
718-
<div class="session-slug" onclick="handleCopyId('${sid}', event)" title="${t('action_copy_id')}">
726+
<div class="session-slug" onclick="handleCopyId('${sid}', '${escAttr(s.slug || '')}', event)" title="${t('action_copy_id')}">
719727
${esc(s.slug || s.sessionId.slice(0, 8))}
720728
<span class="copy-icon">${ICONS.copy}</span>
721729
</div>
@@ -905,14 +913,16 @@ function closeRenameModal() {
905913

906914
// ═══ Copy session ID ═══
907915

908-
function handleCopyId(sessionId, event) {
916+
function handleCopyId(sessionId, slug, event) {
909917
event.stopPropagation();
910-
navigator.clipboard.writeText(sessionId).then(() => {
911-
const el = event.currentTarget;
912-
const original = el.innerHTML;
913-
el.innerHTML = `<span style="color: var(--state-running)">${t('action_copied')}</span>`;
914-
setTimeout(() => { el.innerHTML = original; }, 1200);
915-
}).catch(() => {});
918+
const el = event.currentTarget;
919+
// Copy the slug if we have one (that's what the user sees);
920+
// otherwise the full UUID is more useful than the 8-char prefix.
921+
window.api.copyToClipboard(slug || sessionId);
922+
if (!el) return;
923+
const original = el.innerHTML;
924+
el.innerHTML = `<span style="color: var(--state-running)">${t('action_copied')}</span>`;
925+
setTimeout(() => { el.innerHTML = original; }, 1200);
916926
}
917927

918928
// ═══ Remove session ═══

watcher.js

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class SessionWatcher extends EventEmitter {
5959
terminalId: data.terminalId || null,
6060
lastEventTime: Date.now(),
6161
hasActivity: savedState.name !== 'error',
62+
wasResumed: false,
6263
});
6364
this.emit('session-added', this.sessions.get(id));
6465
}
@@ -122,6 +123,7 @@ class SessionWatcher extends EventEmitter {
122123
terminalId: null,
123124
lastEventTime: Date.now(),
124125
hasActivity: false,
126+
wasResumed: false,
125127
});
126128
this.watchJsonl(sessionId);
127129
this.emit('session-added', this.sessions.get(sessionId));
@@ -140,6 +142,7 @@ class SessionWatcher extends EventEmitter {
140142
// Session was completed/errored but PID is alive — reactivate
141143
session.endedAt = null;
142144
session.hasActivity = false;
145+
session.wasResumed = true;
143146
this.setState(sessionId, STATES.WAITING, false);
144147
}
145148

@@ -601,11 +604,19 @@ class SessionWatcher extends EventEmitter {
601604
if (!sessionId) return;
602605
const session = this.sessions.get(sessionId);
603606
if (!session) return;
604-
// PID died + session file gone, but no user/assistant activity since last (re)start —
605-
// likely a crash rather than a clean finish.
606-
const target = session.hasActivity ? STATES.COMPLETED : STATES.ERROR;
607-
if (session.state !== target) {
608-
this.setState(sessionId, target, false);
607+
if (!session.hasActivity) {
608+
// User resumed the session but it died before writing anything — silent crash.
609+
if (session.wasResumed) {
610+
if (session.state !== STATES.ERROR) this.setState(sessionId, STATES.ERROR, false);
611+
return;
612+
}
613+
// Otherwise it's a phantom (wrapper spawn that died, stale scan entry, …) —
614+
// drop it entirely instead of surfacing a meaningless card.
615+
this.removeSession(sessionId);
616+
return;
617+
}
618+
if (session.state !== STATES.COMPLETED) {
619+
this.setState(sessionId, STATES.COMPLETED, false);
609620
}
610621
}
611622

@@ -663,6 +674,7 @@ class SessionWatcher extends EventEmitter {
663674
terminalId: null,
664675
lastEventTime: Date.now(),
665676
hasActivity: false,
677+
wasResumed: false,
666678
});
667679
this.startFileWatch(sessionId, sessionIdOrPath);
668680
this.emit('session-added', this.sessions.get(sessionId));
@@ -692,6 +704,7 @@ class SessionWatcher extends EventEmitter {
692704
terminalId: null,
693705
lastEventTime: Date.now(),
694706
hasActivity: false,
707+
wasResumed: false,
695708
});
696709
this.startFileWatch(sessionIdOrPath, jsonlPath);
697710
this.emit('session-added', this.sessions.get(sessionIdOrPath));

0 commit comments

Comments
 (0)