claude-code-popup 0.1.0
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/LICENSE +21 -0
- package/README.md +240 -0
- package/bin/cli.js +188 -0
- package/bin/postinstall.js +18 -0
- package/bin/preuninstall.js +14 -0
- package/electron/main.js +452 -0
- package/electron/notify.css +221 -0
- package/electron/notify.html +22 -0
- package/electron/notify.js +176 -0
- package/electron/preload.js +24 -0
- package/electron/setup.css +185 -0
- package/electron/setup.html +76 -0
- package/electron/setup.js +137 -0
- package/package.json +38 -0
- package/src/config.js +58 -0
- package/src/hooks.js +77 -0
- package/src/i18n.js +88 -0
- package/src/ipc.js +85 -0
- package/src/runtime.js +80 -0
package/electron/main.js
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const { app, BrowserWindow, ipcMain, screen, nativeTheme, shell } = require('electron');
|
|
7
|
+
|
|
8
|
+
const ipc = require('../src/ipc');
|
|
9
|
+
const runtime = require('../src/runtime');
|
|
10
|
+
const cfg = require('../src/config');
|
|
11
|
+
const i18n = require('../src/i18n');
|
|
12
|
+
const { installHooks, uninstallHooks } = require('../src/hooks');
|
|
13
|
+
|
|
14
|
+
const MODE = process.env.CLAUDE_NOTIFY_MODE || 'notify';
|
|
15
|
+
|
|
16
|
+
const NOTIFY_WIDTH = 400;
|
|
17
|
+
const NOTIFY_MARGIN = 16;
|
|
18
|
+
|
|
19
|
+
let notifyWindow = null;
|
|
20
|
+
let setupWindow = null;
|
|
21
|
+
let ipcServer = null;
|
|
22
|
+
const sessions = new Set(); // tracks active Claude Code session dirs
|
|
23
|
+
const sessionPpid = new Map(); // dir -> parent pid of the CLI invocation (fallback)
|
|
24
|
+
const sessionHwnd = new Map(); // dir -> HWND captured at SessionStart (primary)
|
|
25
|
+
const cards = new Map(); // id -> { id, dir, addedAt, ppid }
|
|
26
|
+
let cardSeq = 0;
|
|
27
|
+
|
|
28
|
+
app.commandLine.appendSwitch('disable-features', 'DialMediaRouteProvider');
|
|
29
|
+
|
|
30
|
+
if (process.platform === 'darwin') {
|
|
31
|
+
app.dock?.hide();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
app.whenReady().then(async () => {
|
|
35
|
+
if (MODE === 'setup') {
|
|
36
|
+
await openSetupWindow();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
await startNotifyMode();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
app.on('window-all-closed', (e) => {
|
|
43
|
+
// Keep alive even with no windows open in notify mode (server still listens)
|
|
44
|
+
if (MODE === 'notify') {
|
|
45
|
+
e.preventDefault?.();
|
|
46
|
+
} else {
|
|
47
|
+
app.quit();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
app.on('before-quit', () => {
|
|
52
|
+
try {
|
|
53
|
+
ipcServer?.close();
|
|
54
|
+
} catch {
|
|
55
|
+
/* ignore */
|
|
56
|
+
}
|
|
57
|
+
runtime.clearRuntime();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/* -------------------------------------------------------------------------- */
|
|
61
|
+
/* Notify mode */
|
|
62
|
+
/* -------------------------------------------------------------------------- */
|
|
63
|
+
|
|
64
|
+
async function startNotifyMode() {
|
|
65
|
+
ipcServer = ipc.createServer(handleIpcMessage);
|
|
66
|
+
const port = await ipc.listen(ipcServer);
|
|
67
|
+
runtime.writeRuntime({ pid: process.pid, port, startedAt: Date.now() });
|
|
68
|
+
// pre-create the window hidden
|
|
69
|
+
ensureNotifyWindow();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function activeDisplay() {
|
|
73
|
+
// Multi-monitor: prefer the display the cursor is on (likely where the user is working).
|
|
74
|
+
try {
|
|
75
|
+
const point = screen.getCursorScreenPoint();
|
|
76
|
+
return screen.getDisplayNearestPoint(point);
|
|
77
|
+
} catch {
|
|
78
|
+
return screen.getPrimaryDisplay();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function ensureNotifyWindow() {
|
|
83
|
+
if (notifyWindow && !notifyWindow.isDestroyed()) return notifyWindow;
|
|
84
|
+
const config = cfg.readConfig();
|
|
85
|
+
const { workArea } = activeDisplay();
|
|
86
|
+
const winHeight = 200;
|
|
87
|
+
const { x, y } = positionFor(config.position, workArea, NOTIFY_WIDTH, winHeight);
|
|
88
|
+
|
|
89
|
+
notifyWindow = new BrowserWindow({
|
|
90
|
+
width: NOTIFY_WIDTH,
|
|
91
|
+
height: winHeight,
|
|
92
|
+
x,
|
|
93
|
+
y,
|
|
94
|
+
frame: false,
|
|
95
|
+
transparent: true,
|
|
96
|
+
resizable: false,
|
|
97
|
+
movable: false,
|
|
98
|
+
minimizable: false,
|
|
99
|
+
maximizable: false,
|
|
100
|
+
skipTaskbar: true,
|
|
101
|
+
alwaysOnTop: true,
|
|
102
|
+
show: false,
|
|
103
|
+
focusable: false,
|
|
104
|
+
hasShadow: false,
|
|
105
|
+
webPreferences: {
|
|
106
|
+
preload: path.join(__dirname, 'preload.js'),
|
|
107
|
+
contextIsolation: true,
|
|
108
|
+
sandbox: true,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
notifyWindow.setAlwaysOnTop(true, 'screen-saver');
|
|
112
|
+
notifyWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
|
113
|
+
notifyWindow.loadFile(path.join(__dirname, 'notify.html'));
|
|
114
|
+
// Re-broadcast once the renderer has finished loading — covers the race where
|
|
115
|
+
// the very first `send` arrives before notify.js has registered onState.
|
|
116
|
+
notifyWindow.webContents.once('did-finish-load', () => {
|
|
117
|
+
broadcastState();
|
|
118
|
+
});
|
|
119
|
+
notifyWindow.on('closed', () => {
|
|
120
|
+
notifyWindow = null;
|
|
121
|
+
});
|
|
122
|
+
return notifyWindow;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function positionFor(position, workArea, w, h) {
|
|
126
|
+
const top = workArea.y + NOTIFY_MARGIN;
|
|
127
|
+
const left = workArea.x + NOTIFY_MARGIN;
|
|
128
|
+
const right = workArea.x + workArea.width - w - NOTIFY_MARGIN;
|
|
129
|
+
const bottom = workArea.y + workArea.height - h - NOTIFY_MARGIN;
|
|
130
|
+
switch (position) {
|
|
131
|
+
case 'top-left':
|
|
132
|
+
return { x: left, y: top };
|
|
133
|
+
case 'top-right':
|
|
134
|
+
return { x: right, y: top };
|
|
135
|
+
case 'bottom-left':
|
|
136
|
+
return { x: left, y: bottom };
|
|
137
|
+
case 'bottom-right':
|
|
138
|
+
default:
|
|
139
|
+
return { x: right, y: bottom };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function applyWindowGeometry(measuredHeight) {
|
|
144
|
+
if (!notifyWindow || notifyWindow.isDestroyed()) return;
|
|
145
|
+
const config = cfg.readConfig();
|
|
146
|
+
const { workArea } = activeDisplay();
|
|
147
|
+
const h = Math.max(80, Math.min(measuredHeight, Math.floor(workArea.height * 0.8)));
|
|
148
|
+
const { x, y } = positionFor(config.position, workArea, NOTIFY_WIDTH, h);
|
|
149
|
+
notifyWindow.setBounds({ x, y, width: NOTIFY_WIDTH, height: h });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function broadcastState() {
|
|
153
|
+
ensureNotifyWindow();
|
|
154
|
+
const config = cfg.readConfig();
|
|
155
|
+
const cardList = Array.from(cards.values()).sort((a, b) => a.addedAt - b.addedAt);
|
|
156
|
+
notifyWindow.webContents.send('state', { config, cards: cardList });
|
|
157
|
+
if (cardList.length > 0) {
|
|
158
|
+
if (!notifyWindow.isVisible()) notifyWindow.showInactive();
|
|
159
|
+
} else {
|
|
160
|
+
if (notifyWindow.isVisible()) notifyWindow.hide();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function quitSoon() {
|
|
165
|
+
setTimeout(() => app.quit(), 200);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function handleIpcMessage(msg) {
|
|
169
|
+
switch (msg?.type) {
|
|
170
|
+
case 'start': {
|
|
171
|
+
const dir = msg.dir || '';
|
|
172
|
+
if (dir) {
|
|
173
|
+
sessions.add(dir);
|
|
174
|
+
if (msg.ppid) sessionPpid.set(dir, Number(msg.ppid));
|
|
175
|
+
// SessionStart fires while the user is still focused on the terminal,
|
|
176
|
+
// so the captured hwnd reliably points at the right window.
|
|
177
|
+
if (msg.hwnd) sessionHwnd.set(dir, Number(msg.hwnd));
|
|
178
|
+
}
|
|
179
|
+
ensureNotifyWindow();
|
|
180
|
+
return { running: true };
|
|
181
|
+
}
|
|
182
|
+
case 'send': {
|
|
183
|
+
const dir = msg.dir || '';
|
|
184
|
+
sessions.add(dir);
|
|
185
|
+
const ppid = msg.ppid ? Number(msg.ppid) : sessionPpid.get(dir) || null;
|
|
186
|
+
if (ppid) sessionPpid.set(dir, ppid);
|
|
187
|
+
// Only adopt the foreground from `send` if `start` never seeded one —
|
|
188
|
+
// by the time a Notification hook fires the user is typically away.
|
|
189
|
+
if (msg.hwnd && !sessionHwnd.has(dir)) sessionHwnd.set(dir, Number(msg.hwnd));
|
|
190
|
+
const id = nextCardId(dir);
|
|
191
|
+
cards.set(id, { id, dir, addedAt: Date.now(), ppid });
|
|
192
|
+
broadcastState();
|
|
193
|
+
maybeNotifySound();
|
|
194
|
+
return { id };
|
|
195
|
+
}
|
|
196
|
+
case 'end': {
|
|
197
|
+
const dir = msg.dir || '';
|
|
198
|
+
sessions.delete(dir);
|
|
199
|
+
sessionPpid.delete(dir);
|
|
200
|
+
sessionHwnd.delete(dir);
|
|
201
|
+
for (const [id, card] of cards) {
|
|
202
|
+
if (card.dir === dir) cards.delete(id);
|
|
203
|
+
}
|
|
204
|
+
broadcastState();
|
|
205
|
+
// Claude Code session ended — if it was the last one, terminate Electron
|
|
206
|
+
if (sessions.size === 0) {
|
|
207
|
+
cards.clear();
|
|
208
|
+
broadcastState();
|
|
209
|
+
quitSoon();
|
|
210
|
+
}
|
|
211
|
+
return { removed: true };
|
|
212
|
+
}
|
|
213
|
+
case 'dismiss': {
|
|
214
|
+
if (msg.id && cards.has(msg.id)) cards.delete(msg.id);
|
|
215
|
+
broadcastState();
|
|
216
|
+
return { removed: true };
|
|
217
|
+
}
|
|
218
|
+
case 'clearAll': {
|
|
219
|
+
cards.clear();
|
|
220
|
+
broadcastState();
|
|
221
|
+
return { cleared: true };
|
|
222
|
+
}
|
|
223
|
+
case 'getConfig':
|
|
224
|
+
return cfg.readConfig();
|
|
225
|
+
case 'saveConfig': {
|
|
226
|
+
const merged = cfg.writeConfig(msg.config || {});
|
|
227
|
+
broadcastState();
|
|
228
|
+
return merged;
|
|
229
|
+
}
|
|
230
|
+
case 'installHooks':
|
|
231
|
+
return { path: installHooks() };
|
|
232
|
+
case 'uninstallHooks':
|
|
233
|
+
return { path: uninstallHooks() };
|
|
234
|
+
case 'openSetup':
|
|
235
|
+
await openSetupWindow();
|
|
236
|
+
return { opened: true };
|
|
237
|
+
default:
|
|
238
|
+
throw new Error(`unknown_message_type: ${msg?.type}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function nextCardId(dir) {
|
|
243
|
+
cardSeq += 1;
|
|
244
|
+
return `${Buffer.from(dir).toString('base64url')}-${Date.now()}-${cardSeq}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function maybeNotifySound() {
|
|
248
|
+
const config = cfg.readConfig();
|
|
249
|
+
if (!config.sound) return;
|
|
250
|
+
if (process.platform === 'win32') {
|
|
251
|
+
spawnDetached('powershell.exe', [
|
|
252
|
+
'-NoProfile',
|
|
253
|
+
'-NonInteractive',
|
|
254
|
+
'-Command',
|
|
255
|
+
'[System.Media.SystemSounds]::Notification.Play()',
|
|
256
|
+
]);
|
|
257
|
+
} else if (process.platform === 'darwin') {
|
|
258
|
+
spawnDetached('afplay', ['/System/Library/Sounds/Glass.aiff']);
|
|
259
|
+
} else {
|
|
260
|
+
spawnDetached('paplay', ['/usr/share/sounds/freedesktop/stereo/message.oga']);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* -------------------------------------------------------------------------- */
|
|
265
|
+
/* Setup mode */
|
|
266
|
+
/* -------------------------------------------------------------------------- */
|
|
267
|
+
|
|
268
|
+
async function openSetupWindow() {
|
|
269
|
+
if (setupWindow && !setupWindow.isDestroyed()) {
|
|
270
|
+
setupWindow.show();
|
|
271
|
+
setupWindow.focus();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const width = 720;
|
|
275
|
+
const height = 720;
|
|
276
|
+
const { workArea } = activeDisplay();
|
|
277
|
+
const x = Math.round(workArea.x + (workArea.width - width) / 2);
|
|
278
|
+
const y = Math.round(workArea.y + (workArea.height - height) / 2);
|
|
279
|
+
|
|
280
|
+
setupWindow = new BrowserWindow({
|
|
281
|
+
width,
|
|
282
|
+
height,
|
|
283
|
+
x,
|
|
284
|
+
y,
|
|
285
|
+
title: 'claude-code-popup Settings',
|
|
286
|
+
resizable: true,
|
|
287
|
+
minimizable: true,
|
|
288
|
+
maximizable: false,
|
|
289
|
+
autoHideMenuBar: true,
|
|
290
|
+
show: false,
|
|
291
|
+
webPreferences: {
|
|
292
|
+
preload: path.join(__dirname, 'preload.js'),
|
|
293
|
+
contextIsolation: true,
|
|
294
|
+
sandbox: true,
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
setupWindow.once('ready-to-show', () => {
|
|
298
|
+
setupWindow.show();
|
|
299
|
+
setupWindow.focus();
|
|
300
|
+
setupWindow.moveTop();
|
|
301
|
+
});
|
|
302
|
+
setupWindow.on('closed', () => {
|
|
303
|
+
setupWindow = null;
|
|
304
|
+
if (MODE === 'setup' && sessions.size === 0 && cards.size === 0) {
|
|
305
|
+
app.quit();
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
await setupWindow.loadFile(path.join(__dirname, 'setup.html'));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/* -------------------------------------------------------------------------- */
|
|
312
|
+
/* Renderer-facing IPC (preload) */
|
|
313
|
+
/* -------------------------------------------------------------------------- */
|
|
314
|
+
|
|
315
|
+
ipcMain.handle('cn:getConfig', () => cfg.readConfig());
|
|
316
|
+
ipcMain.handle('cn:saveConfig', (_e, next) => {
|
|
317
|
+
const merged = cfg.writeConfig(next || {});
|
|
318
|
+
broadcastState();
|
|
319
|
+
return merged;
|
|
320
|
+
});
|
|
321
|
+
ipcMain.handle('cn:installHooks', () => ({ path: installHooks() }));
|
|
322
|
+
ipcMain.handle('cn:uninstallHooks', () => ({ path: uninstallHooks() }));
|
|
323
|
+
ipcMain.handle('cn:platform', () => ({
|
|
324
|
+
platform: process.platform,
|
|
325
|
+
themeShouldBeDark: nativeTheme.shouldUseDarkColors,
|
|
326
|
+
}));
|
|
327
|
+
ipcMain.handle('cn:i18n', () => i18n.getDict(app.getLocale()));
|
|
328
|
+
ipcMain.handle('cn:openExternal', (_e, url) => shell.openExternal(url));
|
|
329
|
+
ipcMain.handle('cn:dismiss', (_e, id) => handleIpcMessage({ type: 'dismiss', id }));
|
|
330
|
+
ipcMain.handle('cn:clearAll', () => handleIpcMessage({ type: 'clearAll' }));
|
|
331
|
+
ipcMain.handle('cn:openSetup', () => openSetupWindow());
|
|
332
|
+
ipcMain.handle('cn:resize', (_e, height) => applyWindowGeometry(Number(height) || 0));
|
|
333
|
+
ipcMain.handle('cn:openTerminal', (_e, dir) => openTerminalAt(dir));
|
|
334
|
+
ipcMain.handle('cn:focusOrigin', (_e, dir) => {
|
|
335
|
+
const hwnd = sessionHwnd.get(dir);
|
|
336
|
+
const ppid = sessionPpid.get(dir);
|
|
337
|
+
if (hwnd) focusHwnd(hwnd);
|
|
338
|
+
else if (ppid) focusProcessTree(ppid);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
function openTerminalAt(dir) {
|
|
342
|
+
if (!dir) return;
|
|
343
|
+
if (process.platform === 'win32') {
|
|
344
|
+
spawnDetached('cmd', ['/c', 'start', '', 'cmd', '/k', `cd /d "${dir}"`]);
|
|
345
|
+
} else if (process.platform === 'darwin') {
|
|
346
|
+
spawnDetached('open', ['-a', 'Terminal', dir]);
|
|
347
|
+
} else {
|
|
348
|
+
spawnDetached('x-terminal-emulator', [], { cwd: dir });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function spawnDetached(cmd, args, opts = {}) {
|
|
353
|
+
const { spawn } = require('node:child_process');
|
|
354
|
+
try {
|
|
355
|
+
const child = spawn(cmd, args, { detached: true, stdio: 'ignore', ...opts });
|
|
356
|
+
child.unref();
|
|
357
|
+
} catch {
|
|
358
|
+
/* ignore */
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function focusHwnd(hwnd) {
|
|
363
|
+
if (!hwnd || process.platform !== 'win32') return;
|
|
364
|
+
const script = `
|
|
365
|
+
$ErrorActionPreference = 'SilentlyContinue'
|
|
366
|
+
Add-Type -MemberDefinition '
|
|
367
|
+
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
|
|
368
|
+
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
|
369
|
+
[DllImport("user32.dll")] public static extern bool IsIconic(IntPtr hWnd);
|
|
370
|
+
[DllImport("user32.dll")] public static extern bool IsWindow(IntPtr hWnd);
|
|
371
|
+
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
|
372
|
+
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint procId);
|
|
373
|
+
[DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();
|
|
374
|
+
[DllImport("user32.dll")] public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
|
|
375
|
+
' -Name CN -Namespace Win
|
|
376
|
+
$hwnd = [IntPtr]${Number(hwnd)}
|
|
377
|
+
if (-not [Win.CN]::IsWindow($hwnd)) { exit 0 }
|
|
378
|
+
if ([Win.CN]::IsIconic($hwnd)) { [Win.CN]::ShowWindow($hwnd, 9) | Out-Null }
|
|
379
|
+
$foreHwnd = [Win.CN]::GetForegroundWindow()
|
|
380
|
+
$foreThread = 0
|
|
381
|
+
$null = [Win.CN]::GetWindowThreadProcessId($foreHwnd, [ref]$foreThread)
|
|
382
|
+
$myThread = [Win.CN]::GetCurrentThreadId()
|
|
383
|
+
[Win.CN]::AttachThreadInput($myThread, $foreThread, $true) | Out-Null
|
|
384
|
+
[Win.CN]::SetForegroundWindow($hwnd) | Out-Null
|
|
385
|
+
[Win.CN]::AttachThreadInput($myThread, $foreThread, $false) | Out-Null
|
|
386
|
+
`;
|
|
387
|
+
runPowerShell(script);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function focusProcessTree(startPid) {
|
|
391
|
+
if (!startPid) return;
|
|
392
|
+
if (process.platform === 'win32') {
|
|
393
|
+
const script = `
|
|
394
|
+
$ErrorActionPreference = 'SilentlyContinue'
|
|
395
|
+
Add-Type -MemberDefinition '
|
|
396
|
+
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
|
|
397
|
+
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
|
398
|
+
[DllImport("user32.dll")] public static extern bool IsIconic(IntPtr hWnd);
|
|
399
|
+
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
|
400
|
+
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint procId);
|
|
401
|
+
[DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();
|
|
402
|
+
[DllImport("user32.dll")] public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
|
|
403
|
+
' -Name CN -Namespace Win
|
|
404
|
+
$targetPid = ${Number(startPid)}
|
|
405
|
+
$hwnd = [IntPtr]::Zero
|
|
406
|
+
for ($i = 0; $i -lt 12; $i++) {
|
|
407
|
+
$proc = Get-CimInstance Win32_Process -Filter ("ProcessId = " + $targetPid)
|
|
408
|
+
if (-not $proc) { break }
|
|
409
|
+
$p = Get-Process -Id $targetPid -ErrorAction SilentlyContinue
|
|
410
|
+
if ($p -and $p.MainWindowHandle -ne [IntPtr]::Zero) { $hwnd = $p.MainWindowHandle; break }
|
|
411
|
+
$targetPid = [int]$proc.ParentProcessId
|
|
412
|
+
if ($targetPid -le 0) { break }
|
|
413
|
+
}
|
|
414
|
+
if ($hwnd -ne [IntPtr]::Zero) {
|
|
415
|
+
if ([Win.CN]::IsIconic($hwnd)) { [Win.CN]::ShowWindow($hwnd, 9) | Out-Null }
|
|
416
|
+
$foreHwnd = [Win.CN]::GetForegroundWindow()
|
|
417
|
+
$foreThread = 0
|
|
418
|
+
$null = [Win.CN]::GetWindowThreadProcessId($foreHwnd, [ref]$foreThread)
|
|
419
|
+
$myThread = [Win.CN]::GetCurrentThreadId()
|
|
420
|
+
[Win.CN]::AttachThreadInput($myThread, $foreThread, $true) | Out-Null
|
|
421
|
+
[Win.CN]::SetForegroundWindow($hwnd) | Out-Null
|
|
422
|
+
[Win.CN]::AttachThreadInput($myThread, $foreThread, $false) | Out-Null
|
|
423
|
+
}
|
|
424
|
+
`;
|
|
425
|
+
runPowerShell(script);
|
|
426
|
+
} else if (process.platform === 'darwin') {
|
|
427
|
+
// best effort: ask the system to activate the frontmost terminal app
|
|
428
|
+
spawnDetached('osascript', ['-e', 'tell application "Terminal" to activate']);
|
|
429
|
+
} else {
|
|
430
|
+
spawnDetached('wmctrl', ['-a', `:ACTIVE:`]);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function runPowerShell(script) {
|
|
435
|
+
const { spawn } = require('node:child_process');
|
|
436
|
+
try {
|
|
437
|
+
const scriptPath = path.join(os.tmpdir(), `claude-code-popup-focus-${process.pid}-${Date.now()}.ps1`);
|
|
438
|
+
fs.writeFileSync(scriptPath, '' + script, 'utf8');
|
|
439
|
+
const child = spawn(
|
|
440
|
+
'powershell.exe',
|
|
441
|
+
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', scriptPath],
|
|
442
|
+
{ windowsHide: true },
|
|
443
|
+
);
|
|
444
|
+
child.stdout?.on('data', () => {});
|
|
445
|
+
child.stderr?.on('data', () => {});
|
|
446
|
+
child.on('exit', () => {
|
|
447
|
+
setTimeout(() => { try { fs.unlinkSync(scriptPath); } catch { /* ignore */ } }, 200);
|
|
448
|
+
});
|
|
449
|
+
} catch {
|
|
450
|
+
/* ignore */
|
|
451
|
+
}
|
|
452
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--cn-radius: 12px;
|
|
3
|
+
--cn-card-radius: 10px;
|
|
4
|
+
--cn-shadow: 0 12px 32px rgba(15, 17, 21, 0.18), 0 2px 6px rgba(15, 17, 21, 0.08);
|
|
5
|
+
--cn-anim: 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
* { box-sizing: border-box; }
|
|
9
|
+
html, body {
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
background: transparent;
|
|
13
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Hiragino Kaku Gothic ProN",
|
|
14
|
+
"Noto Sans JP", Roboto, Helvetica, Arial, sans-serif;
|
|
15
|
+
color: #1a1d21;
|
|
16
|
+
-webkit-user-select: none;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.cn-shell {
|
|
20
|
+
border-radius: var(--cn-radius);
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
background: #ffffff;
|
|
23
|
+
box-shadow: var(--cn-shadow);
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: column;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.cn-header {
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: 8px;
|
|
32
|
+
padding: 10px 14px;
|
|
33
|
+
font-size: 13px;
|
|
34
|
+
font-weight: 600;
|
|
35
|
+
background: #f5f6f8;
|
|
36
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
37
|
+
}
|
|
38
|
+
.cn-header__icon { font-size: 14px; }
|
|
39
|
+
.cn-header__title { flex: 1; }
|
|
40
|
+
.cn-header__count {
|
|
41
|
+
font-size: 11px;
|
|
42
|
+
font-weight: 600;
|
|
43
|
+
color: #606770;
|
|
44
|
+
background: rgba(0, 0, 0, 0.06);
|
|
45
|
+
padding: 1px 8px;
|
|
46
|
+
border-radius: 999px;
|
|
47
|
+
}
|
|
48
|
+
.cn-header__btn {
|
|
49
|
+
width: 22px;
|
|
50
|
+
height: 22px;
|
|
51
|
+
border-radius: 6px;
|
|
52
|
+
border: none;
|
|
53
|
+
background: transparent;
|
|
54
|
+
color: inherit;
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
font-size: 14px;
|
|
57
|
+
line-height: 1;
|
|
58
|
+
display: inline-flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
justify-content: center;
|
|
61
|
+
opacity: 0.65;
|
|
62
|
+
}
|
|
63
|
+
.cn-header__btn:hover { background: rgba(0,0,0,0.08); opacity: 1; }
|
|
64
|
+
body[data-design="2"] .cn-header__btn:hover,
|
|
65
|
+
body[data-design="5"] .cn-header__btn:hover { background: rgba(255,255,255,0.10); }
|
|
66
|
+
.cn-card {
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.cn-cards {
|
|
71
|
+
list-style: none;
|
|
72
|
+
margin: 0;
|
|
73
|
+
padding: 8px;
|
|
74
|
+
display: flex;
|
|
75
|
+
flex-direction: column;
|
|
76
|
+
gap: 8px;
|
|
77
|
+
overflow-y: auto;
|
|
78
|
+
scrollbar-width: thin;
|
|
79
|
+
}
|
|
80
|
+
.cn-cards::-webkit-scrollbar { width: 6px; }
|
|
81
|
+
.cn-cards::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.18); border-radius: 4px; }
|
|
82
|
+
|
|
83
|
+
.cn-card {
|
|
84
|
+
position: relative;
|
|
85
|
+
background: #ffffff;
|
|
86
|
+
border-radius: var(--cn-card-radius);
|
|
87
|
+
padding: 14px 40px 14px 14px;
|
|
88
|
+
border: 1px solid rgba(0, 0, 0, 0.06);
|
|
89
|
+
box-shadow: 0 1px 2px rgba(15, 17, 21, 0.04);
|
|
90
|
+
animation: cn-fade-in var(--cn-anim);
|
|
91
|
+
display: flex;
|
|
92
|
+
flex-direction: column;
|
|
93
|
+
gap: 6px;
|
|
94
|
+
}
|
|
95
|
+
.cn-card--leaving {
|
|
96
|
+
animation: cn-fade-out 180ms forwards;
|
|
97
|
+
}
|
|
98
|
+
.cn-card__dir {
|
|
99
|
+
font-size: 11px;
|
|
100
|
+
font-weight: 500;
|
|
101
|
+
color: #606770;
|
|
102
|
+
display: flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
gap: 6px;
|
|
105
|
+
white-space: nowrap;
|
|
106
|
+
overflow: hidden;
|
|
107
|
+
text-overflow: ellipsis;
|
|
108
|
+
}
|
|
109
|
+
.cn-card__title {
|
|
110
|
+
font-size: 13px;
|
|
111
|
+
font-weight: 600;
|
|
112
|
+
color: #1a1d21;
|
|
113
|
+
}
|
|
114
|
+
.cn-card__body {
|
|
115
|
+
font-size: 12px;
|
|
116
|
+
color: #2c2f33;
|
|
117
|
+
}
|
|
118
|
+
.cn-card__close {
|
|
119
|
+
position: absolute;
|
|
120
|
+
top: 6px;
|
|
121
|
+
right: 6px;
|
|
122
|
+
width: 22px;
|
|
123
|
+
height: 22px;
|
|
124
|
+
border-radius: 50%;
|
|
125
|
+
border: none;
|
|
126
|
+
background: rgba(0, 0, 0, 0.06);
|
|
127
|
+
color: #1a1d21;
|
|
128
|
+
cursor: pointer;
|
|
129
|
+
font-size: 12px;
|
|
130
|
+
line-height: 1;
|
|
131
|
+
display: inline-flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
justify-content: center;
|
|
134
|
+
}
|
|
135
|
+
.cn-card__close:hover { background: rgba(0, 0, 0, 0.12); }
|
|
136
|
+
|
|
137
|
+
.cn-card__progress {
|
|
138
|
+
display: block;
|
|
139
|
+
height: 3px;
|
|
140
|
+
border-radius: 2px;
|
|
141
|
+
background: rgba(0, 0, 0, 0.08);
|
|
142
|
+
margin-top: 8px;
|
|
143
|
+
overflow: hidden;
|
|
144
|
+
}
|
|
145
|
+
.cn-card__progress-bar {
|
|
146
|
+
display: block;
|
|
147
|
+
height: 100%;
|
|
148
|
+
background: #d97757;
|
|
149
|
+
transform-origin: left center;
|
|
150
|
+
animation: cn-shrink linear forwards;
|
|
151
|
+
}
|
|
152
|
+
/* darker designs need a brighter base for the bar */
|
|
153
|
+
body[data-design="2"] .cn-card__progress,
|
|
154
|
+
body[data-design="5"] .cn-card__progress { background: rgba(255,255,255,0.10); }
|
|
155
|
+
|
|
156
|
+
@keyframes cn-fade-in {
|
|
157
|
+
from { opacity: 0; transform: translateY(-6px); }
|
|
158
|
+
to { opacity: 1; transform: translateY(0); }
|
|
159
|
+
}
|
|
160
|
+
@keyframes cn-fade-out {
|
|
161
|
+
from { opacity: 1; transform: translateX(0); max-height: 200px; }
|
|
162
|
+
to { opacity: 0; transform: translateX(20px); max-height: 0; padding: 0; margin: 0; }
|
|
163
|
+
}
|
|
164
|
+
@keyframes cn-shrink {
|
|
165
|
+
from { transform: scaleX(1); }
|
|
166
|
+
to { transform: scaleX(0); }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* ---------- Design 1: minimal (default — nothing extra) ---------- */
|
|
170
|
+
body[data-design="1"] .cn-card__actions { display: none; }
|
|
171
|
+
|
|
172
|
+
/* ---------- Design 2: accent bar dark ---------- */
|
|
173
|
+
body[data-design="2"] { color: #e6e8eb; }
|
|
174
|
+
body[data-design="2"] .cn-shell { background: #1d2026; box-shadow: var(--cn-shadow); }
|
|
175
|
+
body[data-design="2"] .cn-header { background: #15171b; color: #f4f5f7; }
|
|
176
|
+
body[data-design="2"] .cn-header__count { background: rgba(255,255,255,0.08); color: #c2c6cc; }
|
|
177
|
+
body[data-design="2"] .cn-card {
|
|
178
|
+
background: #23272d;
|
|
179
|
+
color: #e6e8eb;
|
|
180
|
+
border: none;
|
|
181
|
+
border-left: 3px solid #5fa8ff;
|
|
182
|
+
border-radius: 6px;
|
|
183
|
+
}
|
|
184
|
+
body[data-design="2"] .cn-card__dir { color: #9aa0a8; }
|
|
185
|
+
body[data-design="2"] .cn-card__title { color: #f4f5f7; }
|
|
186
|
+
body[data-design="2"] .cn-card__body { color: #d1d4d9; }
|
|
187
|
+
body[data-design="2"] .cn-card__close { background: rgba(255,255,255,0.08); color: #e6e8eb; }
|
|
188
|
+
|
|
189
|
+
/* ---------- Design 3: Claude colour ---------- */
|
|
190
|
+
body[data-design="3"] .cn-shell { background: #fffaf5; border: 1px solid #f3dccb; }
|
|
191
|
+
body[data-design="3"] .cn-header {
|
|
192
|
+
background: linear-gradient(90deg, #d97757 0%, #c95f3f 100%);
|
|
193
|
+
color: #fff;
|
|
194
|
+
}
|
|
195
|
+
body[data-design="3"] .cn-header__count { background: rgba(255,255,255,0.22); color: #fff; }
|
|
196
|
+
body[data-design="3"] .cn-card { background: #fff; border-color: #f3dccb; }
|
|
197
|
+
|
|
198
|
+
/* ---------- Design 4: light + accent bar ---------- */
|
|
199
|
+
body[data-design="4"] .cn-shell { background: #ffffff; }
|
|
200
|
+
body[data-design="4"] .cn-header { background: #fff8f3; color: #2a1d14; }
|
|
201
|
+
body[data-design="4"] .cn-header__count { background: rgba(217,119,87,0.16); color: #c95f3f; }
|
|
202
|
+
body[data-design="4"] .cn-card {
|
|
203
|
+
background: #ffffff;
|
|
204
|
+
border: none;
|
|
205
|
+
border-left: 3px solid #d97757;
|
|
206
|
+
border-radius: 6px;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* ---------- Design 5: dark + timer bar ---------- */
|
|
210
|
+
body[data-design="5"] .cn-shell { background: #1d2026; }
|
|
211
|
+
body[data-design="5"] .cn-header { background: #15171b; color: #f4f5f7; }
|
|
212
|
+
body[data-design="5"] .cn-header__count { background: rgba(255,255,255,0.08); color: #c2c6cc; }
|
|
213
|
+
body[data-design="5"] .cn-card {
|
|
214
|
+
background: #23272d;
|
|
215
|
+
color: #e6e8eb;
|
|
216
|
+
border-color: rgba(255,255,255,0.04);
|
|
217
|
+
}
|
|
218
|
+
body[data-design="5"] .cn-card__dir { color: #9aa0a8; }
|
|
219
|
+
body[data-design="5"] .cn-card__title { color: #f4f5f7; }
|
|
220
|
+
body[data-design="5"] .cn-card__body { color: #d1d4d9; }
|
|
221
|
+
body[data-design="5"] .cn-card__close { background: rgba(255,255,255,0.08); color: #e6e8eb; }
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self';" />
|
|
6
|
+
<title>claude-code-popup</title>
|
|
7
|
+
<link rel="stylesheet" href="notify.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body data-design="1">
|
|
10
|
+
<div id="root" class="cn-shell">
|
|
11
|
+
<header class="cn-header">
|
|
12
|
+
<span class="cn-header__icon" aria-hidden="true">🤖</span>
|
|
13
|
+
<span class="cn-header__title">Claude Code</span>
|
|
14
|
+
<span class="cn-header__count" id="cn-count">0</span>
|
|
15
|
+
<button id="cn-settings" class="cn-header__btn" type="button">⚙</button>
|
|
16
|
+
<button id="cn-clear" class="cn-header__btn" type="button">×</button>
|
|
17
|
+
</header>
|
|
18
|
+
<ul id="cn-cards" class="cn-cards"></ul>
|
|
19
|
+
</div>
|
|
20
|
+
<script src="notify.js"></script>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|