Skip to content

Commit 06014bd

Browse files
authored
Merge pull request #117 from grimmerk/fix/project-path-display
fix: purple dot, path display, shortcut symbols, tab flash
2 parents 8687d33 + 81f9281 commit 06014bd

9 files changed

Lines changed: 199 additions & 128 deletions

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Changelog
22

3+
## 1.0.74
4+
5+
- Fix: session status dot stuck on purple for sessions with large responses (#116)
6+
- `tail -n 50` on large JSONL files exceeded `execFile` maxBuffer (1MB)
7+
- Reduced to 15 lines + raised maxBuffer to 5MB
8+
- Fix: eliminate tab flash on startup (default tab now passed via URL hash)
9+
- Style: project paths display `~/` instead of `/Users/<user>/`
10+
- Style: shortcut display uses macOS symbols (`⌘⌃R` instead of `Cmd+Ctrl+R`)
11+
- Style: needs-attention dot changed from orange `#FFA726` to warm red `#F06856`
12+
- Style: working pulse animation slowed from 2s to 2.5s
13+
- Style: normal mode banner only shown on first launch
14+
- Feat: clicking shortcut in title bar opens Settings → Shortcuts tab
15+
- Feat: project search supports `~/` prefix and full path matching (with highlight)
16+
317
## 1.0.73
418

519
- Feat: embedded terminal search (`Cmd+F`)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "CodeV",
33
"productName": "CodeV",
4-
"version": "1.0.73",
4+
"version": "1.0.74",
55
"description": "Quick switcher for VS Code, Cursor, and Claude Code sessions",
66
"repository": {
77
"type": "git",

src/claude-session-utility.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -891,7 +891,10 @@ export const detectActiveSessions = async (): Promise<ActiveSessionResult> => {
891891
if (!pid || !sessionId) continue;
892892

893893
// Verify process is still alive
894-
try { process.kill(pid, 0); } catch { continue; }
894+
try { process.kill(pid, 0); } catch {
895+
console.log(`[detect-active] PID ${pid} (${sessionId}) not alive, skipping`);
896+
continue;
897+
}
895898

896899
entrypoints.set(sessionId, entrypoint);
897900

@@ -924,17 +927,22 @@ export const detectActiveSessions = async (): Promise<ActiveSessionResult> => {
924927
activeMap.set(sessionId, pid);
925928
} else if (cwd) {
926929
// sessionId not in history — find session by cwd
930+
console.log(`[detect-active] PID ${pid} sessionId ${sessionId} not in history.jsonl, trying cwd match (${cwd})`);
927931
const cwdCandidates = allSessions.filter(s => s.project === cwd && !activeMap.has(s.sessionId));
928932
if (cwdCandidates.length === 1) {
929933
activeMap.set(cwdCandidates[0].sessionId, pid);
930934
} else if (cwdCandidates.length > 1) {
931935
// Multiple same-cwd candidates — queue for cross-reference
932936
needsCrossRef.push({ pid, cwd, candidates: cwdCandidates });
937+
} else {
938+
console.log(`[detect-active] PID ${pid} sessionId ${sessionId}: no cwd match found`);
933939
}
940+
} else {
941+
console.log(`[detect-active] PID ${pid} sessionId ${sessionId}: not in history and no cwd`);
934942
}
935943
}
936-
} catch {
937-
// skip malformed files
944+
} catch (err) {
945+
console.error(`[detect-active] Error processing session file ${file}:`, err);
938946
}
939947
}
940948

@@ -954,8 +962,8 @@ export const detectActiveSessions = async (): Promise<ActiveSessionResult> => {
954962
if (!fs.existsSync(sessionsDir)) {
955963
await detectActiveSessionsLegacy(activeMap);
956964
}
957-
} catch {
958-
// ignore
965+
} catch (err) {
966+
console.error('[detect-active] Error in detectActiveSessions:', err);
959967
}
960968

961969
cachedActiveMap = activeMap;

src/electron-api.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ type IpcCallback = (event: Electron.IpcRendererEvent, ...args: any[]) => void;
44

55
interface IElectronAPI {
66
// App actions
7+
getHomeDir: () => Promise<string>;
8+
getBannerSeen: () => Promise<boolean>;
9+
setBannerSeen: () => void;
710
invokeVSCode: (path: string, option: string) => void;
811
hideApp: () => void;
912
openFolderSelector: () => void;

src/main.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ const createSettingsWindow = (
410410
return settingsWindow;
411411
};
412412

413-
const createSwitcherWindow = (): BrowserWindow => {
413+
const createSwitcherWindow = (initialMode?: string): BrowserWindow => {
414414
// Create the browser window.
415415
const window = new BrowserWindow({
416416
// maximizable: false,
@@ -431,7 +431,9 @@ const createSwitcherWindow = (): BrowserWindow => {
431431
});
432432

433433
// and load the index.html of the app.
434-
window.loadURL(SWITCHER_WINDOW_WEBPACK_ENTRY);
434+
// Pass initial mode via hash so renderer can use it synchronously (no async IPC)
435+
const hash = initialMode ? `#mode=${initialMode}` : '';
436+
window.loadURL(SWITCHER_WINDOW_WEBPACK_ENTRY + hash);
435437

436438
// Open external links in default browser
437439
const { shell } = require('electron');
@@ -619,6 +621,10 @@ ipcMain.on('search-working-folder', (event, path: string) => {
619621
}
620622
});
621623

624+
ipcMain.handle('get-home-dir', () => {
625+
return require('os').homedir();
626+
});
627+
622628
ipcMain.on('hide-app', (event) => {
623629
hideSwitcherWindow();
624630
});
@@ -1094,7 +1100,8 @@ const trayToggleEvtHandler = async () => {
10941100
}
10951101
}
10961102

1097-
switcherWindow = createSwitcherWindow();
1103+
const defaultSwitcherMode = ((await settings.get('default-switcher-mode')) as string) || 'projects';
1104+
switcherWindow = createSwitcherWindow(defaultSwitcherMode);
10981105
if (isDebug) {
10991106
console.log('when ready');
11001107
}
@@ -1980,6 +1987,10 @@ ipcMain.handle('get-session-statuses', async () => {
19801987
// Scan active sessions that don't have status files yet + cleanup stale ones
19811988
try {
19821989
const { activeMap, vscodeSessions } = await detectActiveSessions();
1990+
if (isDebug) {
1991+
console.log('[session-status] activeMap keys:', Array.from(activeMap.keys()));
1992+
console.log('[session-status] status files:', Object.keys(obj));
1993+
}
19831994
cleanupStaleStatuses(new Set(activeMap.keys()));
19841995
const allSessions = readClaudeSessions(500); // hoisted out of loop
19851996
// Merge VS Code sessions for status scanning
@@ -1988,19 +1999,30 @@ ipcMain.handle('get-session-statuses', async () => {
19881999
.filter(([sessionId]) => !obj[sessionId])
19892000
.map(([sessionId]) => {
19902001
const session = allKnown.find((s: any) => s.sessionId === sessionId);
2002+
if (!session && isDebug) {
2003+
console.log(`[session-status] active session ${sessionId} not found in allKnown (${allKnown.length} sessions)`);
2004+
}
19912005
return session ? { sessionId, project: session.project } : null;
19922006
})
19932007
.filter(Boolean) as { sessionId: string; project: string }[];
19942008

19952009
if (sessionsWithoutStatus.length > 0) {
2010+
if (isDebug) {
2011+
console.log('[session-status] scanning', sessionsWithoutStatus.length, 'sessions without status:', sessionsWithoutStatus.map(s => s.sessionId));
2012+
}
19962013
const scanned = await scanInitialStatuses(sessionsWithoutStatus);
2014+
if (isDebug) {
2015+
console.log('[session-status] scan results:', Array.from(scanned.entries()));
2016+
}
19972017
scanned.forEach((v, k) => {
19982018
obj[k] = { status: v, timestamp: Math.floor(Date.now() / 1000) };
19992019
// Persist scanned status to file so fs.watch treats all statuses uniformly
20002020
writeStatusFile(k, v as string);
20012021
});
20022022
}
2003-
} catch {}
2023+
} catch (err) {
2024+
console.error('[session-status] Error during status scan:', err);
2025+
}
20042026

20052027
return obj;
20062028
});
@@ -2009,6 +2031,14 @@ ipcMain.handle('get-app-mode', async () => {
20092031
return appMode;
20102032
});
20112033

2034+
ipcMain.handle('get-banner-seen', async () => {
2035+
return await settings.get('normal-mode-banner-seen');
2036+
});
2037+
2038+
ipcMain.on('set-banner-seen', async () => {
2039+
await settings.set('normal-mode-banner-seen', true);
2040+
});
2041+
20122042
ipcMain.on('set-app-mode', async (_event, mode: string) => {
20132043
const newMode = mode === 'menubar' ? 'menubar' : 'normal';
20142044
await settings.set('app-mode', newMode);

src/popup.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,15 @@ const PopupDefaultExample = ({
4646
saveCallback,
4747
openCallback,
4848
switcherMode,
49+
openToTab,
50+
onOpenToTabConsumed,
4951
}: {
5052
workingFolderPath?: string;
5153
saveCallback?: (key: string, value: string) => void;
5254
openCallback?: any;
5355
switcherMode?: string;
56+
openToTab?: 'general' | 'sessions' | 'shortcuts' | null;
57+
onOpenToTabConsumed?: () => void;
5458
}) => {
5559
const [isOpen, setIsOpen] = useState(false);
5660
const [launchAtLogin, setLaunchAtLogin] = useState(false);
@@ -142,6 +146,15 @@ const PopupDefaultExample = ({
142146
}
143147
}, [isOpen]);
144148

149+
// Allow parent to open Settings on a specific tab
150+
useEffect(() => {
151+
if (openToTab) {
152+
setSettingsTab(openToTab);
153+
setIsOpen(true);
154+
onOpenToTabConsumed?.();
155+
}
156+
}, [openToTab]);
157+
145158
const handleLaunchAtLoginChange = (checked: boolean) => {
146159
setLaunchAtLogin(checked);
147160
window.electronAPI.setLoginItemSettings(checked);

src/preload.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
const { contextBridge, ipcRenderer } = require('electron');
55

66
contextBridge.exposeInMainWorld('electronAPI', {
7+
getHomeDir: () => ipcRenderer.invoke('get-home-dir'),
8+
getBannerSeen: () => ipcRenderer.invoke('get-banner-seen'),
9+
setBannerSeen: () => ipcRenderer.send('set-banner-seen'),
710
invokeVSCode: (path: string, option: string) =>
811
ipcRenderer.send('invoke-vscode', path, option),
912

src/session-status-hooks.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ export const watchStatusDir = (
259259
/**
260260
* Scan active sessions' JSONL files to determine initial status
261261
* (for sessions started before CodeV or before hooks were installed).
262-
* Reads last ~50 lines of each session's JSONL to check:
262+
* Reads last ~15 lines of each session's JSONL to check:
263263
* - Pending AskUserQuestion tool use → needs-attention
264264
* - Last assistant message with stop_reason "end_turn" → idle
265265
* - Otherwise → working
@@ -270,7 +270,11 @@ export const scanInitialStatuses = async (
270270
const { execFile } = require('child_process');
271271
const tailFile = (filePath: string): Promise<string> =>
272272
new Promise((resolve) => {
273-
execFile('tail', ['-n', '50', filePath], { encoding: 'utf-8', timeout: 3000 }, (err: any, stdout: string) => {
273+
// Use 15 lines (not 50) — large assistant messages can make 50 lines > 1MB,
274+
// exceeding execFile's maxBuffer and silently failing.
275+
// The last ~5-10 lines are usually system entries; assistant entry is typically within 15.
276+
execFile('tail', ['-n', '15', filePath], { encoding: 'utf-8', timeout: 3000, maxBuffer: 5 * 1024 * 1024 }, (err: any, stdout: string) => {
277+
if (err) console.error(`[session-status] tailFile error for ${path.basename(filePath)}:`, err.message || err);
274278
resolve(err ? '' : stdout);
275279
});
276280
});
@@ -290,7 +294,7 @@ export const scanInitialStatuses = async (
290294
const jsonlPath = path.join(claudeDir, encodedProject, `${session.sessionId}.jsonl`);
291295
if (!fs.existsSync(jsonlPath)) return;
292296

293-
// Read last 50 lines
297+
// Read last 15 lines
294298
const tail = await tailFile(jsonlPath);
295299
if (!tail.trim()) return;
296300

@@ -359,10 +363,15 @@ export const cleanupStaleStatuses = (activeSessionIds: Set<string>): void => {
359363
if (!file.endsWith('.json')) continue;
360364
const sessionId = file.replace('.json', '');
361365
if (!activeSessionIds.has(sessionId)) {
362-
try { fs.unlinkSync(path.join(STATUS_DIR, file)); } catch {}
366+
console.log(`[session-status] cleanup: removing stale status file ${file}`);
367+
try { fs.unlinkSync(path.join(STATUS_DIR, file)); } catch (err) {
368+
console.error(`[session-status] cleanup: failed to delete ${file}:`, err);
369+
}
363370
}
364371
}
365-
} catch {}
372+
} catch (err) {
373+
console.error('[session-status] cleanup error:', err);
374+
}
366375
};
367376

368377
/**
@@ -375,7 +384,9 @@ export const writeStatusFile = (sessionId: string, status: string): void => {
375384
const targetFile = path.join(STATUS_DIR, `${sessionId}.json`);
376385
fs.writeFileSync(tmpFile, JSON.stringify({ status, timestamp: Math.floor(Date.now() / 1000), cwd: '' }));
377386
fs.renameSync(tmpFile, targetFile);
378-
} catch {}
387+
} catch (err) {
388+
console.error(`[session-status] writeStatusFile failed for ${sessionId}:`, err);
389+
}
379390
};
380391

381392
export { STATUS_DIR, HOOK_SCRIPT_PATH };

0 commit comments

Comments
 (0)