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() const ptyManager = new PTYManager()
let mainWindow: BrowserWindow | null = null let mainWindow: BrowserWindow | null = null
let moveTimeout: NodeJS.Timeout | null = null
const COLLAPSED_SIZE = { width: 80, height: 80 } const COLLAPSED_SIZE = { width: 80, height: 80 }
const EXPANDED_SIZE = { width: 900, height: 600 } const EXPANDED_SIZE = { width: 900, height: 600 }
@@ -14,9 +15,8 @@ function createWindow() {
const display = screen.getPrimaryDisplay() const display = screen.getPrimaryDisplay()
const { width: screenWidth, height: screenHeight } = display.workAreaSize const { width: screenWidth, height: screenHeight } = display.workAreaSize
// Use saved position or default to bottom-right const x = config.windowPosition?.x ?? screenWidth - COLLAPSED_SIZE.width - 20
let x = config.windowPosition?.x ?? screenWidth - COLLAPSED_SIZE.width - 20 const y = config.windowPosition?.y ?? screenHeight - COLLAPSED_SIZE.height - 20
let y = config.windowPosition?.y ?? screenHeight - COLLAPSED_SIZE.height - 20
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
width: COLLAPSED_SIZE.width, width: COLLAPSED_SIZE.width,
@@ -37,7 +37,6 @@ function createWindow() {
}, },
}) })
// Dock hide on macOS
if (app.dock) { if (app.dock) {
app.dock.hide() app.dock.hide()
} }
@@ -52,12 +51,15 @@ function createWindow() {
mainWindow = null mainWindow = null
}) })
// Save position when moved
mainWindow.on('moved', () => { mainWindow.on('moved', () => {
if (!mainWindow) return if (!mainWindow) return
const [x, y] = mainWindow.getPosition() if (moveTimeout) clearTimeout(moveTimeout)
const config = loadConfig() moveTimeout = setTimeout(() => {
saveConfig({ ...config, windowPosition: { x, y } }) 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 x = currentPos[0]
let y = currentPos[1] let y = currentPos[1]
// Ensure window stays on screen
if (x + EXPANDED_SIZE.width > screenWidth) { if (x + EXPANDED_SIZE.width > screenWidth) {
x = screenWidth - EXPANDED_SIZE.width - 20 x = screenWidth - EXPANDED_SIZE.width - 20
} }
@@ -106,29 +107,15 @@ function collapseWindow() {
saveConfig({ ...config, collapsed: true, windowPosition: { x: currentPos[0], y: currentPos[1] } }) 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(() => { app.whenReady().then(() => {
createWindow() createWindow()
ptyManager.start('claude') ptyManager.start('claude')
// Collapse after load to ensure pet mode
mainWindow?.webContents.on('did-finish-load', () => { mainWindow?.webContents.on('did-finish-load', () => {
collapseWindow() collapseWindow()
}) })
// Register global shortcut const ret = globalShortcut.register('CommandOrControl+Shift+M', () => {
const ret = globalShortcut.register('CommandOrControl+Shift+C', () => {
if (!mainWindow) { if (!mainWindow) {
createWindow() createWindow()
return return
@@ -145,7 +132,6 @@ app.whenReady().then(() => {
console.log('Global shortcut registration failed') console.log('Global shortcut registration failed')
} }
// IPC handlers
ipcMain.on('pty:write', (_event, data: string) => { ipcMain.on('pty:write', (_event, data: string) => {
ptyManager.write(data) ptyManager.write(data)
}) })
@@ -189,6 +175,7 @@ app.whenReady().then(() => {
app.on('activate', () => { app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) { if (BrowserWindow.getAllWindows().length === 0) {
createWindow() createWindow()
ptyManager.start('claude')
} }
}) })
}) })
+6 -2
View File
@@ -5,10 +5,14 @@ const api = {
write: (data) => ipcRenderer.send('pty:write', data), write: (data) => ipcRenderer.send('pty:write', data),
resize: (cols, rows) => ipcRenderer.send('pty:resize', cols, rows), resize: (cols, rows) => ipcRenderer.send('pty:resize', cols, rows),
onData: (callback) => { 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) => { 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: { window: {
+12 -12
View File
@@ -1,15 +1,17 @@
import * as pty from 'node-pty' import * as pty from 'node-pty'
import { execSync } from 'child_process'
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
export class PTYManager extends EventEmitter { export class PTYManager extends EventEmitter {
private ptyProcess: pty.IPty | null = null private ptyProcess: pty.IPty | null = null
private resolvedShell: string
start(shell: string = 'claude', args: string[] = []) { constructor() {
const resolvedShell = this.resolveShell(shell) super()
const resolvedArgs = shell === 'claude' ? args : [] 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', name: 'xterm-color',
cols: 80, cols: 80,
rows: 24, rows: 24,
@@ -49,15 +51,13 @@ export class PTYManager extends EventEmitter {
private resolveShell(shell: string): string { private resolveShell(shell: string): string {
if (shell === 'claude') { if (shell === 'claude') {
const { execSync } = require('child_process')
try { try {
const path = execSync('which claude', { encoding: 'utf8' }).trim() return execSync('which claude', { encoding: 'utf8' }).trim()
return path
} catch { } catch {
try { throw new Error(
return execSync('which zsh', { encoding: 'utf8' }).trim() 'Claude CLI not found. Please install it: https://claude.ai/download'
} catch { )
return '/bin/zsh'
}
} }
} }
return shell 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\"", "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/", "build": "vite build && tsc -p tsconfig.electron.json && cp electron/preload.js dist-electron/",
"start": "npx electron dist-electron/main.js", "start": "npx electron dist-electron/main.js",
"rebuild": "electron-rebuild",
"pack": "npm run build && electron-builder --dir", "pack": "npm run build && electron-builder --dir",
"dist": "npm run build && electron-builder" "dist": "npm run build && electron-builder"
}, },
"build": { "build": {
"appId": "com.apaam.myclaude-pet", "appId": "com.apaam.myclaude",
"productName": "MyClaude", "productName": "MyClaude",
"directories": { "directories": {
"output": "release", "output": "release",
@@ -35,7 +34,8 @@
} }
], ],
"category": "public.app-category.productivity", "category": "public.app-category.productivity",
"hardenedRuntime": true, "identity": null,
"hardenedRuntime": false,
"gatekeeperAssess": false "gatekeeperAssess": false
} }
}, },
@@ -55,7 +55,6 @@
"react-dom": "^19.2.5" "react-dom": "^19.2.5"
}, },
"devDependencies": { "devDependencies": {
"@electron/rebuild": "^4.0.4",
"@types/node": "^25.6.0", "@types/node": "^25.6.0",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
@@ -63,7 +62,6 @@
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"electron": "^42.0.0", "electron": "^42.0.0",
"electron-builder": "^26.8.1", "electron-builder": "^26.8.1",
"tsx": "^4.21.0",
"typescript": "^6.0.3", "typescript": "^6.0.3",
"vite": "^8.0.10", "vite": "^8.0.10",
"wait-on": "^9.0.5" "wait-on": "^9.0.5"
+20 -23
View File
@@ -5,8 +5,6 @@ import '@xterm/xterm/css/xterm.css'
const Terminal: React.FC = () => { const Terminal: React.FC = () => {
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const xtermRef = useRef<XTerm | null>(null)
const fitAddonRef = useRef<FitAddon | null>(null)
useEffect(() => { useEffect(() => {
if (!containerRef.current) return if (!containerRef.current) return
@@ -38,7 +36,6 @@ const Terminal: React.FC = () => {
}, },
cursorBlink: true, cursorBlink: true,
scrollback: 10000, scrollback: 10000,
allowProposedApi: true,
}) })
const fitAddon = new FitAddon() const fitAddon = new FitAddon()
@@ -47,16 +44,13 @@ const Terminal: React.FC = () => {
term.open(containerRef.current) term.open(containerRef.current)
fitAddon.fit() fitAddon.fit()
xtermRef.current = term
fitAddonRef.current = fitAddon
const api = window.petAPI const api = window.petAPI
if (api) { if (api) {
api.pty.onData((data: string) => { const removeDataListener = api.pty.onData((data: string) => {
term.write(data) 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`) term.writeln(`\r\n\x1b[31mProcess exited with code ${code}\x1b[0m`)
}) })
@@ -68,26 +62,29 @@ const Terminal: React.FC = () => {
if (dims) { if (dims) {
api.pty.resize(dims.cols, dims.rows) api.pty.resize(dims.cols, dims.rows)
} }
}
const handleResize = () => { const handleResize = () => {
fitAddon.fit() fitAddon.fit()
const dims = fitAddon.proposeDimensions() const dims = fitAddon.proposeDimensions()
if (dims && api) { if (dims) {
api.pty.resize(dims.cols, dims.rows) 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 () => { return () => {
window.removeEventListener('resize', handleResize)
resizeObserver.disconnect()
term.dispose() term.dispose()
} }
}, []) }, [])
-5
View File
@@ -120,8 +120,3 @@ body {
25% { transform: translateY(-2px); } 25% { transform: translateY(-2px); }
75% { transform: translateY(1px); } 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: { pty: {
write: (data: string) => void write: (data: string) => void
resize: (cols: number, rows: number) => void resize: (cols: number, rows: number) => void
onData: (callback: (data: string) => void) => void onData: (callback: (data: string) => void) => () => void
onExit: (callback: (code: number) => void) => void onExit: (callback: (code: number) => void) => () => void
} }
window: { window: {
expand: () => void expand: () => void