refactor(desktop): code review fixes

- 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
This commit is contained in:
Zhengshou Lai
2026-05-27 11:23:14 +08:00
parent 0c227cae19
commit 2ac1c9750e
8 changed files with 56 additions and 75 deletions
+1 -1
View File
@@ -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):显示/隐藏宠物
**特性:**
+12 -25
View File
@@ -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')
}
})
})
+6 -2
View File
@@ -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: {
+12 -12
View File
@@ -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
+3 -5
View File
@@ -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"
+20 -23
View File
@@ -5,8 +5,6 @@ import '@xterm/xterm/css/xterm.css'
const Terminal: React.FC = () => {
const containerRef = useRef<HTMLDivElement>(null)
const xtermRef = useRef<XTerm | null>(null)
const fitAddonRef = useRef<FitAddon | null>(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()
}
}, [])
-5
View File
@@ -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); }
}
+2 -2
View File
@@ -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