Skip to content
Merged
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
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ dist
out
playwright-report
test-results
PLAN[0-9]*.md
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
- 修复终态任务仍可再次触发 `Approve and run` 的问题,主进程会将其视为 no-op,渲染层也会在任务进入终态后禁用执行按钮。
- 修复 `Claude Code CLI` 适配器在同一 session 重复执行时复用事件 ID 的问题,避免时间线渲染出现重复 key。
- 为桌面主窗口补充受信导航策略,仅允许本地 `file://` 或开发服务器地址留在应用内,其它链接统一改走系统浏览器。
- 修复桌面端构建把 `@mango/*` workspace 包 external 到运行时的问题,避免 clean checkout 下 Electron 启动时因缺少 `dist` 产物而报 `ERR_MODULE_NOT_FOUND`。
- 强化桌面端 smoke 检查,校验主进程与 preload 产物中不再残留 `@mango/*` 裸导入,并兼容根级 `out/renderer` 输出路径。
- 修复桌面端开发启动仍依赖旧版 `MAIN_WINDOW_VITE_*` 全局变量的问题,改为读取 `electron-vite` 当前注入的 `ELECTRON_RENDERER_URL` 并固定加载 `out/renderer/index.html`。

## [0.1.0] - 2026-03-25

Expand Down
19 changes: 17 additions & 2 deletions apps/desktop/electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import { resolve } from 'node:path'
import react from '@vitejs/plugin-react'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'

const internalWorkspacePackages = [
'@mango/core',
'@mango/adapters',
'@mango/contracts',
'@mango/ui'
]

export default defineConfig({
main: {
resolve: {
Expand All @@ -13,7 +20,11 @@ export default defineConfig({
'@mango/ui': resolve(__dirname, '../../packages/ui/src/index.ts')
}
},
plugins: [externalizeDepsPlugin()]
plugins: [
externalizeDepsPlugin({
exclude: internalWorkspacePackages
})
]
},
preload: {
resolve: {
Expand All @@ -24,7 +35,11 @@ export default defineConfig({
'@mango/ui': resolve(__dirname, '../../packages/ui/src/index.ts')
}
},
plugins: [externalizeDepsPlugin()]
plugins: [
externalizeDepsPlugin({
exclude: internalWorkspacePackages
})
]
},
renderer: {
resolve: {
Expand Down
18 changes: 9 additions & 9 deletions apps/desktop/src/main/app/bootstrapDesktopApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import { FileDesktopStore } from '../persistence/fileDesktopStore'
import { SQLiteDesktopStore } from '../persistence/sqliteDesktopStore'
import { DesktopController } from '../services/desktopController'
import { isTrustedDesktopUrl } from './navigationPolicy'

declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string | undefined
declare const MAIN_WINDOW_VITE_NAME: string
import { getRendererDevServerUrl, getRendererIndexHtmlPath } from './rendererRuntime'

let mainWindow: BrowserWindow | null = null

const createMainWindow = () => {
const rendererDevServerUrl = getRendererDevServerUrl(process.env)

mainWindow = new BrowserWindow({
width: 1540,
height: 980,
Expand All @@ -31,7 +31,7 @@ const createMainWindow = () => {
})

const handleExternalNavigation = (url: string) => {
if (isTrustedDesktopUrl(url, MAIN_WINDOW_VITE_DEV_SERVER_URL)) {
if (isTrustedDesktopUrl(url, rendererDevServerUrl)) {
return
}

Expand All @@ -42,24 +42,24 @@ const createMainWindow = () => {
handleExternalNavigation(url)

return {
action: isTrustedDesktopUrl(url, MAIN_WINDOW_VITE_DEV_SERVER_URL) ? 'allow' : 'deny'
action: isTrustedDesktopUrl(url, rendererDevServerUrl) ? 'allow' : 'deny'
}
})

mainWindow.webContents.on('will-navigate', (event, url) => {
if (isTrustedDesktopUrl(url, MAIN_WINDOW_VITE_DEV_SERVER_URL)) {
if (isTrustedDesktopUrl(url, rendererDevServerUrl)) {
return
}

event.preventDefault()
handleExternalNavigation(url)
})

if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
void mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL)
if (rendererDevServerUrl) {
void mainWindow.loadURL(rendererDevServerUrl)
mainWindow.webContents.openDevTools({ mode: 'detach' })
} else {
void mainWindow.loadFile(join(__dirname, `../../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`))
void mainWindow.loadFile(getRendererIndexHtmlPath(__dirname))
}
}

Expand Down
19 changes: 19 additions & 0 deletions apps/desktop/src/main/app/rendererRuntime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { join } from 'node:path'

import { getRendererDevServerUrl, getRendererIndexHtmlPath } from './rendererRuntime'

describe('rendererRuntime', () => {
it('reads the renderer dev server url from electron-vite env', () => {
expect(
getRendererDevServerUrl({
ELECTRON_RENDERER_URL: 'http://localhost:5173'
} as NodeJS.ProcessEnv)
).toBe('http://localhost:5173')
})

it('resolves the packaged renderer entry next to the main bundle', () => {
expect(getRendererIndexHtmlPath('D:/workspace/apps/desktop/out/main')).toBe(
join('D:/workspace/apps/desktop/out/main', '../renderer/index.html')
)
})
})
7 changes: 7 additions & 0 deletions apps/desktop/src/main/app/rendererRuntime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { join } from 'node:path'

export const getRendererDevServerUrl = (env: NodeJS.ProcessEnv): string | undefined =>
env.ELECTRON_RENDERER_URL

export const getRendererIndexHtmlPath = (currentDir: string): string =>
join(currentDir, '../renderer/index.html')
1 change: 1 addition & 0 deletions packages/config-eslint/base.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const baseConfig = tseslint.config(
'**/out/**',
'**/coverage/**',
'**/.turbo/**',
'**/.worktrees/**',
'**/*.d.ts',
'**/*.d.mts',
'**/playwright-report/**',
Expand Down
52 changes: 46 additions & 6 deletions tooling/scripts/desktop-smoke.mjs
Original file line number Diff line number Diff line change
@@ -1,16 +1,56 @@
import { access } from 'node:fs/promises'
import { access, readFile } from 'node:fs/promises'
import { resolve } from 'node:path'

const repoRoot = resolve(import.meta.dirname, '..', '..')

const requiredOutputs = [
'apps/desktop/out/main/index.js',
'apps/desktop/out/preload/index.mjs',
'apps/desktop/out/renderer/index.html'
]
const requiredOutputs = ['apps/desktop/out/main/index.js', 'apps/desktop/out/preload/index.mjs']

for (const relativePath of requiredOutputs) {
await access(resolve(repoRoot, relativePath))
}

const rendererCandidates = ['out/renderer/index.html', 'apps/desktop/out/renderer/index.html']

let resolvedRendererOutput = null

for (const relativePath of rendererCandidates) {
try {
await access(resolve(repoRoot, relativePath))
resolvedRendererOutput = relativePath
break
} catch {
// Keep searching known renderer output locations.
}
}

if (!resolvedRendererOutput) {
throw new Error(
`Desktop renderer output is missing. Checked: ${rendererCandidates
.map((relativePath) => resolve(repoRoot, relativePath))
.join(', ')}`
)
}

const workspaceImportPattern = /['"]@mango\//
const legacyElectronViteGlobals = [/MAIN_WINDOW_VITE_DEV_SERVER_URL/, /MAIN_WINDOW_VITE_NAME/]

for (const relativePath of [...requiredOutputs, resolvedRendererOutput]) {
const absolutePath = resolve(repoRoot, relativePath)
const contents = await readFile(absolutePath, 'utf8')

if (workspaceImportPattern.test(contents)) {
throw new Error(
`Desktop bundle still contains workspace package imports at runtime: ${absolutePath}`
)
}

for (const pattern of legacyElectronViteGlobals) {
if (pattern.test(contents)) {
throw new Error(
`Desktop bundle still contains deprecated electron-vite globals at runtime: ${absolutePath}`
)
}
}
}

console.log('Desktop smoke check passed.')
Loading