Skip to content

Commit a68b6a9

Browse files
grimmerkclaude
andcommitted
fix: address review — try/catch init, hoist readClaudeSessions, atomic write, CWD escape, execFile
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3eb9a54 commit a68b6a9

3 files changed

Lines changed: 28 additions & 19 deletions

File tree

docs/session-status-hooks-design.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Show active session status (working / idle / needs-attention) via colored dots i
88

99
| Status | Dot Color | Meaning | Trigger |
1010
|---|---|---|---|
11-
| **Working** | Purple `#CE93D8` | Claude is processing | `UserPromptSubmit` hook |
11+
| **Working** | Orange `#E8956A` (pulse animation) | Claude is processing | `UserPromptSubmit` hook |
1212
| **Idle** | Green `#66BB6A` | Waiting for user input | `Stop` hook |
1313
| **Needs attention** | Orange `#FFA726` | Permission prompt or question | `PermissionRequest` hook, or pending `AskUserQuestion` |
1414
| **Active (unknown)** | Purple `#CE93D8` | Running but no hook data yet | Detected via `sessions/*.json` but no status file |

src/main.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1793,17 +1793,21 @@ ipcMain.on('set-login-item-settings', (_event, openAtLogin: boolean) => {
17931793
let statusWatcherCleanup: (() => void) | null = null;
17941794

17951795
const initSessionStatusHooks = async () => {
1796-
const enabled = (await settings.get('session-status-hooks')) !== false; // default: true
1797-
if (enabled) {
1798-
installHooks();
1799-
// Start watching for status changes
1800-
if (!statusWatcherCleanup) {
1801-
statusWatcherCleanup = watchStatusDir((statuses) => {
1802-
const obj: Record<string, SessionStatus> = {};
1803-
statuses.forEach((v, k) => { obj[k] = v; });
1804-
switcherWindow?.webContents.send('session-statuses-updated', obj);
1805-
});
1796+
try {
1797+
const enabled = (await settings.get('session-status-hooks')) !== false; // default: true
1798+
if (enabled) {
1799+
installHooks();
1800+
// Start watching for status changes
1801+
if (!statusWatcherCleanup) {
1802+
statusWatcherCleanup = watchStatusDir((statuses) => {
1803+
const obj: Record<string, SessionStatus> = {};
1804+
statuses.forEach((v, k) => { obj[k] = v; });
1805+
switcherWindow?.webContents.send('session-statuses-updated', obj);
1806+
});
1807+
}
18061808
}
1809+
} catch (e) {
1810+
if (isDebug) console.error('Failed to init session status hooks:', e);
18071811
}
18081812
};
18091813

@@ -1839,11 +1843,11 @@ ipcMain.handle('get-session-statuses', async () => {
18391843
// Scan active sessions that don't have status files yet
18401844
try {
18411845
const activeMap = await detectActiveSessions();
1846+
const allSessions = readClaudeSessions(500); // hoisted out of loop
18421847
const sessionsWithoutStatus = Array.from(activeMap.entries())
18431848
.filter(([sessionId]) => !obj[sessionId])
18441849
.map(([sessionId]) => {
1845-
const sessions = readClaudeSessions(500);
1846-
const session = sessions.find((s: any) => s.sessionId === sessionId);
1850+
const session = allSessions.find((s: any) => s.sessionId === sessionId);
18471851
return session ? { sessionId, project: session.project } : null;
18481852
})
18491853
.filter(Boolean) as { sessionId: string; project: string }[];

src/session-status-hooks.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@ case "$EVENT" in
4444
*) STATUS="unknown" ;;
4545
esac
4646
47-
echo "{\\"status\\":\\"$STATUS\\",\\"timestamp\\":$(date +%s),\\"cwd\\":\\"$CWD\\"}" > "$STATUS_DIR/$SESSION_ID.json"
47+
# Escape CWD for JSON (handle backslashes and double quotes)
48+
SAFE_CWD=$(echo "$CWD" | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g')
49+
# Atomic write via temp file + mv
50+
TMPFILE="$STATUS_DIR/.$SESSION_ID.tmp"
51+
echo "{\\"status\\":\\"$STATUS\\",\\"timestamp\\":$(date +%s),\\"cwd\\":\\"$SAFE_CWD\\"}" > "$TMPFILE"
52+
mv -f "$TMPFILE" "$STATUS_DIR/$SESSION_ID.json"
4853
`;
4954

5055
/**
@@ -160,7 +165,7 @@ export const isHooksInstalled = (): boolean => {
160165
}
161166
};
162167

163-
export type SessionStatus = 'working' | 'idle' | 'needs-attention' | 'active' | null;
168+
export type SessionStatus = 'working' | 'idle' | 'needs-attention' | 'unknown' | null;
164169

165170
/**
166171
* Read all status files from codev-status directory
@@ -210,10 +215,10 @@ export const watchStatusDir = (
210215
export const scanInitialStatuses = async (
211216
activeSessions: { sessionId: string; project: string }[],
212217
): Promise<Map<string, SessionStatus>> => {
213-
const { exec } = require('child_process');
214-
const execPromise = (cmd: string): Promise<string> =>
218+
const { execFile } = require('child_process');
219+
const tailFile = (filePath: string): Promise<string> =>
215220
new Promise((resolve) => {
216-
exec(cmd, { encoding: 'utf-8', timeout: 3000 }, (err: any, stdout: string) => {
221+
execFile('tail', ['-n', '50', filePath], { encoding: 'utf-8', timeout: 3000 }, (err: any, stdout: string) => {
217222
resolve(err ? '' : stdout);
218223
});
219224
});
@@ -234,7 +239,7 @@ export const scanInitialStatuses = async (
234239
if (!fs.existsSync(jsonlPath)) return;
235240

236241
// Read last 50 lines
237-
const tail = await execPromise(`tail -n 50 "${jsonlPath}"`);
242+
const tail = await tailFile(jsonlPath);
238243
if (!tail.trim()) return;
239244

240245
const lines = tail.trim().split('\n');

0 commit comments

Comments
 (0)