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()
|
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 (moveTimeout) clearTimeout(moveTimeout)
|
||||||
|
moveTimeout = setTimeout(() => {
|
||||||
if (!mainWindow) return
|
if (!mainWindow) return
|
||||||
const [x, y] = mainWindow.getPosition()
|
const [x, y] = mainWindow.getPosition()
|
||||||
const config = loadConfig()
|
const config = loadConfig()
|
||||||
saveConfig({ ...config, windowPosition: { x, y } })
|
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')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize)
|
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
handleResize()
|
handleResize()
|
||||||
})
|
})
|
||||||
resizeObserver.observe(containerRef.current)
|
resizeObserver.observe(containerRef.current)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', handleResize)
|
|
||||||
resizeObserver.disconnect()
|
resizeObserver.disconnect()
|
||||||
|
removeDataListener()
|
||||||
|
removeExitListener()
|
||||||
|
term.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
term.dispose()
|
term.dispose()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|||||||
@@ -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); }
|
|
||||||
}
|
|
||||||
|
|||||||
Vendored
+2
-2
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user