Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/electron/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,12 +503,14 @@ async function bootstrap(): Promise<void> {
// 如果用户有保存的图标偏好则使用,否则用默认图标
if (process.platform === 'darwin' && app.dock) {
await app.dock.show()
const { resolveAppIconPath } = require('./ipc')
const { resolveAppIconPath, setAppBundleIcon } = require('./ipc')
const settings = getSettings()
const variantId = settings.appIconVariant
const dockIconPath = resolveAppIconPath(variantId ?? 'default')
if (dockIconPath && existsSync(dockIconPath)) {
app.dock.setIcon(dockIconPath)
const isDefault = !variantId || variantId === 'default'
setAppBundleIcon(isDefault ? null : dockIconPath)
}
}

Expand Down
34 changes: 33 additions & 1 deletion apps/electron/src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,36 @@ export function resolveAppIconPath(variantId: string): string | null {
return join(resourcesDir, 'proma-logos', `proma-${variantId}.png`)
}

/**
* macOS: 将 PNG 图标写入 .app bundle 的自定义图标属性,
* 使 Finder / Launchpad / Dock(未运行时)也显示用户选择的图标。
* 传 null 清除自定义图标,恢复 bundle 内置 .icns。
*/
export async function setAppBundleIcon(pngPath: string | null): Promise<void> {
if (process.platform !== 'darwin' || !app.isPackaged) return
const appPath = app.getPath('exe').replace(/\/Contents\/MacOS\/.*$/, '')
if (!appPath.endsWith('.app')) return
const { execFile } = await import('node:child_process')
const script = pngPath
? `
ObjC.import('AppKit');
const ws = $.NSWorkspace.sharedWorkspace;
const img = $.NSImage.alloc.initWithContentsOfFile('${pngPath.replace(/'/g, "\\'")}');
ws.setIconForFileOptions(img, '${appPath.replace(/'/g, "\\'")}', 0);
`
: `
ObjC.import('AppKit');
const ws = $.NSWorkspace.sharedWorkspace;
ws.setIconForFileOptions($.nil, '${appPath.replace(/'/g, "\\'")}', 0);
`
return new Promise<void>((resolve) => {
execFile('/usr/bin/osascript', ['-l', 'JavaScript', '-e', script], (err) => {
if (err) console.warn('[图标] 设置 bundle 图标失败:', err.message)
resolve()
})
})
}

export function registerIpcHandlers(): void {
console.log('[IPC] 正在注册 IPC 处理器...')

Expand Down Expand Up @@ -1637,9 +1667,11 @@ export function registerIpcHandlers(): void {
return false
}

// macOS: 设置 Dock 图标
// macOS: 设置 Dock 图标(运行时)+ bundle 图标(Finder/Launchpad/Dock 未运行时)
if (process.platform === 'darwin' && app.dock) {
app.dock.setIcon(iconPath)
const isDefault = !variantId || variantId === 'default'
await setAppBundleIcon(isDefault ? null : iconPath)
}

// 持久化到设置
Expand Down
10 changes: 6 additions & 4 deletions apps/electron/src/renderer/components/app-shell/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { automationFormAtom } from '@/atoms/automation-atoms'
import { activeViewAtom } from '@/atoms/active-view'
import { interfaceVariantAtom } from '@/atoms/theme'
import { WindowControls } from '@/components/WindowControls'
import { detectIsWindows, WINDOW_CONTROLS_INSET_RIGHT } from '@/lib/platform'
import { detectIsWindows, detectIsMac, WINDOW_CONTROLS_INSET_RIGHT } from '@/lib/platform'
import { cn } from '@/lib/utils'

const MIN_RIGHT_PANEL_WIDTH = 300
Expand Down Expand Up @@ -53,6 +53,7 @@ export function AppShell({ contextValue }: AppShellProps): React.ReactElement {
const activeView = useAtomValue(activeViewAtom)
const showRightPanel = appMode === 'agent' && !!currentSessionId && !automationForm.open && activeView !== 'automations' && activeView !== 'agent-skills'
const isWindows = React.useMemo(() => detectIsWindows(), [])
const isMac = React.useMemo(() => detectIsMac(), [])

// 左侧边栏可拖拽宽度
const [leftSidebarWidth, setLeftSidebarWidth] = useAtom(leftSidebarWidthAtom)
Expand Down Expand Up @@ -180,6 +181,10 @@ export function AppShell({ contextValue }: AppShellProps): React.ReactElement {
{/* 左侧边栏:可折叠,可拖拽调整宽度 */}
<div className={cn(isClassic ? 'p-2 pr-0' : '', 'relative z-[60] crt-sidebar')}>
<LeftSidebar width={clampedLeftSidebarWidth} noTransition={isDraggingLeftSidebar} />
{/* 侧边栏右侧分隔线:absolute 定位避免 flex gap 暴露父级渐变背景 */}
{!isClassic && (
<div aria-hidden="true" className={cn('absolute right-0 w-px z-[61] bg-border/80 dark:bg-border/70', isMac && sidebarCollapsed ? 'top-[34px] bottom-0' : 'top-0 bottom-0')} />
)}
{/* 侧边栏展开时显示拖拽手柄,折叠态隐藏 */}
{!sidebarCollapsed && (
<div
Expand All @@ -190,9 +195,6 @@ export function AppShell({ contextValue }: AppShellProps): React.ReactElement {
/>
)}
</div>
{!isClassic && (
<div aria-hidden="true" className="relative z-[61] w-px flex-shrink-0 bg-border/80 dark:bg-border/70" />
)}

{/* 中间容器:relative z-[60] 使其在 z-50 拖动区域之上 */}
<div className={cn('flex-1 min-w-0 relative z-[60]', isClassic && 'p-2')}>
Expand Down
7 changes: 6 additions & 1 deletion apps/electron/src/renderer/components/tabs/TabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
tabsAtom,
activeTabIdAtom,
tabIndicatorMapAtom,
sidebarCollapsedAtom,
} from '@/atoms/tab-atoms'
import type { TabItem } from '@/atoms/tab-atoms'
import type { SessionIndicatorStatus } from '@/atoms/agent-atoms'
Expand All @@ -35,7 +36,7 @@ import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { TabBarItem } from './TabBarItem'
import { useCloseTab } from '@/hooks/useCloseTab'
import { detectIsWindows, WINDOW_CONTROLS_INSET_RIGHT, WINDOW_CONTROLS_PADDING_RIGHT } from '@/lib/platform'
import { detectIsWindows, detectIsMac, WINDOW_CONTROLS_INSET_RIGHT, WINDOW_CONTROLS_PADDING_RIGHT } from '@/lib/platform'
import { registerShortcut } from '@/lib/shortcut-registry'
import { cn } from '@/lib/utils'

Expand Down Expand Up @@ -225,6 +226,8 @@ function TabBarInner({
const leaveTimerRef = React.useRef<ReturnType<typeof setTimeout>>()
const fadeTimerRef = React.useRef<ReturnType<typeof setTimeout>>()
const isWindows = React.useMemo(() => detectIsWindows(), [])
const isMac = React.useMemo(() => detectIsMac(), [])
const sidebarCollapsed = useAtomValue(sidebarCollapsedAtom)

// 文件面板切换(全局共享):活动 Tab 是 Agent 且面板关闭时,在 TabBar 右上角展示"打开"按钮。
// 该按钮的 absolute 定位与 DiffPanelTabBar.PanelRightClose 的 mr-1 mb-[3px] 坐标耦合,
Expand Down Expand Up @@ -387,6 +390,8 @@ function TabBarInner({
// Windows 始终避开 WindowControls(~126px);非 Windows 打开按钮时给 scroll 预留空间
isWindows && WINDOW_CONTROLS_PADDING_RIGHT,
!isWindows && showOpenPanelButton && "pr-10",
// macOS 侧边栏收起时,避让 traffic lights 右侧
isMac && sidebarCollapsed && "pl-4",
)}
>
{tabs.map((tab) => (
Expand Down