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:
@@ -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):显示/隐藏宠物
|
||||
|
||||
**特性:**
|
||||
|
||||
|
||||
@@ -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
|
||||
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')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
if (dims) {
|
||||
api.pty.resize(dims.cols, dims.rows)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
handleResize()
|
||||
})
|
||||
resizeObserver.observe(containerRef.current)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
resizeObserver.disconnect()
|
||||
removeDataListener()
|
||||
removeExitListener()
|
||||
term.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
term.dispose()
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
|
||||
Vendored
+2
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user