From 2ac1c9750eadaa66b39d0b5f2ee7646cc46a689a Mon Sep 17 00:00:00 2001 From: Zhengshou Lai Date: Wed, 6 May 2026 19:26:08 +0800 Subject: [PATCH] refactor(desktop): code review fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Shortcut: Cmd+Shift+C → Cmd+Shift+M (avoid Chrome DevTools conflict) - Preload: onData/onExit now return cleanup functions (fix memory leak) - PTY: resolve shell path at construction time, throw on missing claude CLI - Terminal: remove allowProposedApi, dedupe resize listeners, add cleanup - Main: debounce window move config writes (500ms), restart PTY on activate - CSS: remove unused orbGlow keyframes - Package: fix appId to com.apaam.myclaude, skip code signing, remove unused deps --- README.md | 2 +- desktop/electron/main.ts | 37 ++++++++----------------- desktop/electron/preload.js | 8 ++++-- desktop/electron/pty.ts | 24 ++++++++-------- desktop/package.json | 8 ++---- desktop/src/components/Terminal.tsx | 43 ++++++++++++++--------------- desktop/src/index.css | 5 ---- desktop/src/types/global.d.ts | 4 +-- 8 files changed, 56 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index 6c2d731..bbb236a 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,7 @@ npm run dev # 开发模式 **快捷键:** -- `Cmd+Shift+C`(macOS)/ `Ctrl+Shift+C`(Win/Linux):显示/隐藏宠物 +- `Cmd+Shift+M`(macOS)/ `Ctrl+Shift+M`(Win/Linux):显示/隐藏宠物 **特性:** diff --git a/desktop/electron/main.ts b/desktop/electron/main.ts index b67d33d..19c03fd 100644 --- a/desktop/electron/main.ts +++ b/desktop/electron/main.ts @@ -5,6 +5,7 @@ import { loadConfig, saveConfig } from './config' const ptyManager = new PTYManager() let mainWindow: BrowserWindow | null = null +let moveTimeout: NodeJS.Timeout | null = null const COLLAPSED_SIZE = { width: 80, height: 80 } const EXPANDED_SIZE = { width: 900, height: 600 } @@ -14,9 +15,8 @@ function createWindow() { const display = screen.getPrimaryDisplay() const { width: screenWidth, height: screenHeight } = display.workAreaSize - // Use saved position or default to bottom-right - let x = config.windowPosition?.x ?? screenWidth - COLLAPSED_SIZE.width - 20 - let y = config.windowPosition?.y ?? screenHeight - COLLAPSED_SIZE.height - 20 + const x = config.windowPosition?.x ?? screenWidth - COLLAPSED_SIZE.width - 20 + const y = config.windowPosition?.y ?? screenHeight - COLLAPSED_SIZE.height - 20 mainWindow = new BrowserWindow({ width: COLLAPSED_SIZE.width, @@ -37,7 +37,6 @@ function createWindow() { }, }) - // Dock hide on macOS if (app.dock) { app.dock.hide() } @@ -52,12 +51,15 @@ function createWindow() { mainWindow = null }) - // Save position when moved mainWindow.on('moved', () => { if (!mainWindow) return - const [x, y] = mainWindow.getPosition() - const config = loadConfig() - saveConfig({ ...config, windowPosition: { x, y } }) + if (moveTimeout) clearTimeout(moveTimeout) + moveTimeout = setTimeout(() => { + if (!mainWindow) return + const [x, y] = mainWindow.getPosition() + const config = loadConfig() + saveConfig({ ...config, windowPosition: { x, y } }) + }, 500) }) } @@ -70,7 +72,6 @@ function expandWindow() { let x = currentPos[0] let y = currentPos[1] - // Ensure window stays on screen if (x + EXPANDED_SIZE.width > screenWidth) { x = screenWidth - EXPANDED_SIZE.width - 20 } @@ -106,29 +107,15 @@ function collapseWindow() { saveConfig({ ...config, collapsed: true, windowPosition: { x: currentPos[0], y: currentPos[1] } }) } -function toggleWindow() { - if (!mainWindow) { - createWindow() - return - } - if (mainWindow.isVisible()) { - mainWindow.hide() - } else { - mainWindow.show() - } -} - app.whenReady().then(() => { createWindow() ptyManager.start('claude') - // Collapse after load to ensure pet mode mainWindow?.webContents.on('did-finish-load', () => { collapseWindow() }) - // Register global shortcut - const ret = globalShortcut.register('CommandOrControl+Shift+C', () => { + const ret = globalShortcut.register('CommandOrControl+Shift+M', () => { if (!mainWindow) { createWindow() return @@ -145,7 +132,6 @@ app.whenReady().then(() => { console.log('Global shortcut registration failed') } - // IPC handlers ipcMain.on('pty:write', (_event, data: string) => { ptyManager.write(data) }) @@ -189,6 +175,7 @@ app.whenReady().then(() => { app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow() + ptyManager.start('claude') } }) }) diff --git a/desktop/electron/preload.js b/desktop/electron/preload.js index aad92e8..17f24de 100644 --- a/desktop/electron/preload.js +++ b/desktop/electron/preload.js @@ -5,10 +5,14 @@ const api = { write: (data) => ipcRenderer.send('pty:write', data), resize: (cols, rows) => ipcRenderer.send('pty:resize', cols, rows), onData: (callback) => { - ipcRenderer.on('pty:data', (_event, data) => callback(data)) + const handler = (_event, data) => callback(data) + ipcRenderer.on('pty:data', handler) + return () => ipcRenderer.removeListener('pty:data', handler) }, onExit: (callback) => { - ipcRenderer.on('pty:exit', (_event, code) => callback(code)) + const handler = (_event, code) => callback(code) + ipcRenderer.on('pty:exit', handler) + return () => ipcRenderer.removeListener('pty:exit', handler) }, }, window: { diff --git a/desktop/electron/pty.ts b/desktop/electron/pty.ts index 88c40f5..5791105 100644 --- a/desktop/electron/pty.ts +++ b/desktop/electron/pty.ts @@ -1,15 +1,17 @@ import * as pty from 'node-pty' -import { execSync } from 'child_process' import { EventEmitter } from 'events' export class PTYManager extends EventEmitter { private ptyProcess: pty.IPty | null = null + private resolvedShell: string - start(shell: string = 'claude', args: string[] = []) { - const resolvedShell = this.resolveShell(shell) - const resolvedArgs = shell === 'claude' ? args : [] + constructor() { + super() + this.resolvedShell = this.resolveShell('claude') + } - this.ptyProcess = pty.spawn(resolvedShell, resolvedArgs, { + start(_shell: string = 'claude', args: string[] = []) { + this.ptyProcess = pty.spawn(this.resolvedShell, args, { name: 'xterm-color', cols: 80, rows: 24, @@ -49,15 +51,13 @@ export class PTYManager extends EventEmitter { private resolveShell(shell: string): string { if (shell === 'claude') { + const { execSync } = require('child_process') try { - const path = execSync('which claude', { encoding: 'utf8' }).trim() - return path + return execSync('which claude', { encoding: 'utf8' }).trim() } catch { - try { - return execSync('which zsh', { encoding: 'utf8' }).trim() - } catch { - return '/bin/zsh' - } + throw new Error( + 'Claude CLI not found. Please install it: https://claude.ai/download' + ) } } return shell diff --git a/desktop/package.json b/desktop/package.json index 13e2188..3628ecd 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -7,12 +7,11 @@ "dev": "npx tsc -p tsconfig.electron.json && concurrently \"vite\" \"tsc -p tsconfig.electron.json --watch --preserveWatchOutput\" \"wait-on http://localhost:5173 && VITE_DEV_SERVER_URL=http://localhost:5173 npx electron dist-electron/main.js\"", "build": "vite build && tsc -p tsconfig.electron.json && cp electron/preload.js dist-electron/", "start": "npx electron dist-electron/main.js", - "rebuild": "electron-rebuild", "pack": "npm run build && electron-builder --dir", "dist": "npm run build && electron-builder" }, "build": { - "appId": "com.apaam.myclaude-pet", + "appId": "com.apaam.myclaude", "productName": "MyClaude", "directories": { "output": "release", @@ -35,7 +34,8 @@ } ], "category": "public.app-category.productivity", - "hardenedRuntime": true, + "identity": null, + "hardenedRuntime": false, "gatekeeperAssess": false } }, @@ -55,7 +55,6 @@ "react-dom": "^19.2.5" }, "devDependencies": { - "@electron/rebuild": "^4.0.4", "@types/node": "^25.6.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", @@ -63,7 +62,6 @@ "concurrently": "^9.2.1", "electron": "^42.0.0", "electron-builder": "^26.8.1", - "tsx": "^4.21.0", "typescript": "^6.0.3", "vite": "^8.0.10", "wait-on": "^9.0.5" diff --git a/desktop/src/components/Terminal.tsx b/desktop/src/components/Terminal.tsx index a154269..daf0f2b 100644 --- a/desktop/src/components/Terminal.tsx +++ b/desktop/src/components/Terminal.tsx @@ -5,8 +5,6 @@ import '@xterm/xterm/css/xterm.css' const Terminal: React.FC = () => { const containerRef = useRef(null) - const xtermRef = useRef(null) - const fitAddonRef = useRef(null) useEffect(() => { if (!containerRef.current) return @@ -38,7 +36,6 @@ const Terminal: React.FC = () => { }, cursorBlink: true, scrollback: 10000, - allowProposedApi: true, }) const fitAddon = new FitAddon() @@ -47,16 +44,13 @@ const Terminal: React.FC = () => { term.open(containerRef.current) fitAddon.fit() - xtermRef.current = term - fitAddonRef.current = fitAddon - const api = window.petAPI if (api) { - api.pty.onData((data: string) => { + const removeDataListener = api.pty.onData((data: string) => { term.write(data) }) - api.pty.onExit((code: number) => { + const removeExitListener = api.pty.onExit((code: number) => { term.writeln(`\r\n\x1b[31mProcess exited with code ${code}\x1b[0m`) }) @@ -68,26 +62,29 @@ const Terminal: React.FC = () => { if (dims) { api.pty.resize(dims.cols, dims.rows) } - } - const handleResize = () => { - fitAddon.fit() - const dims = fitAddon.proposeDimensions() - if (dims && api) { - api.pty.resize(dims.cols, dims.rows) + const handleResize = () => { + fitAddon.fit() + const dims = fitAddon.proposeDimensions() + if (dims) { + api.pty.resize(dims.cols, dims.rows) + } + } + + const resizeObserver = new ResizeObserver(() => { + handleResize() + }) + resizeObserver.observe(containerRef.current) + + return () => { + resizeObserver.disconnect() + removeDataListener() + removeExitListener() + term.dispose() } } - window.addEventListener('resize', handleResize) - - const resizeObserver = new ResizeObserver(() => { - handleResize() - }) - resizeObserver.observe(containerRef.current) - return () => { - window.removeEventListener('resize', handleResize) - resizeObserver.disconnect() term.dispose() } }, []) diff --git a/desktop/src/index.css b/desktop/src/index.css index 454d7e2..78e346d 100644 --- a/desktop/src/index.css +++ b/desktop/src/index.css @@ -120,8 +120,3 @@ body { 25% { transform: translateY(-2px); } 75% { transform: translateY(1px); } } - -@keyframes orbGlow { - 0%, 100% { box-shadow: 0 4px 16px rgba(22, 163, 74, 0.4), inset 0 2px 4px rgba(255,255,255,0.2); } - 50% { box-shadow: 0 4px 24px rgba(74, 222, 128, 0.6), inset 0 2px 4px rgba(255,255,255,0.3); } -} diff --git a/desktop/src/types/global.d.ts b/desktop/src/types/global.d.ts index c4ef1de..00486a5 100644 --- a/desktop/src/types/global.d.ts +++ b/desktop/src/types/global.d.ts @@ -6,8 +6,8 @@ declare global { pty: { write: (data: string) => void resize: (cols: number, rows: number) => void - onData: (callback: (data: string) => void) => void - onExit: (callback: (code: number) => void) => void + onData: (callback: (data: string) => void) => () => void + onExit: (callback: (code: number) => void) => () => void } window: { expand: () => void