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 .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
node_modules/
out/
dist/
dist-dev/
.DS_Store
*.log
coverage/
Expand Down
17 changes: 17 additions & 0 deletions electron-builder.dev.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const packageJson = require('./package.json')

module.exports = {
...packageJson.build,
appId: 'io.dsh.desktop.dev',
productName: 'DSH Desktop Dev',
directories: {
...packageJson.build.directories,
output: 'dist-dev'
},
extraMetadata: {
name: 'dsh-desktop-dev',
productName: 'DSH Desktop Dev',
dshDesktopChannel: 'development'
},
publish: null
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"test": "vitest run",
"test:watch": "vitest",
"package:dir": "npm run build && electron-builder --dir",
"package:dev:dir": "npm run build && electron-builder --dir --config electron-builder.dev.cjs",
"package:mac": "npm run build && electron-builder --mac --publish never",
"package:mac:arm64": "node scripts/verify-target.mjs darwin arm64 && npm run build && electron-builder --mac --arm64 --publish never",
"package:mac:x64": "node scripts/verify-target.mjs darwin x64 && npm run build && electron-builder --mac --x64 --publish never",
Expand Down
241 changes: 222 additions & 19 deletions patches/@deepseek-ai+dsh-client-ui-agent-preset+0.1.0-rc.6.patch

Large diffs are not rendered by default.

82 changes: 70 additions & 12 deletions src/main/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { join } from 'node:path'
import { readFileSync } from 'node:fs'
import {
app,
BrowserWindow,
Expand All @@ -11,7 +12,7 @@ import {
import { HarnessRuntime } from './runtime/harness-runtime'
import { secureWindow } from './security'
import { ensureLaunchRoot } from './state/launch-root'
import { shouldLoadHarnessUrl } from './window-navigation'
import { isAbortedNavigationError, shouldLoadHarnessUrl } from './window-navigation'
import {
checkForUpdates,
registerUpdateHandlers,
Expand All @@ -26,14 +27,64 @@ let launchDirectory: string
let quitting = false
let failureDialogVisible = false

function isDevelopmentBuild(): boolean {
if (!app.isPackaged) return true

try {
const metadata = JSON.parse(
readFileSync(join(app.getAppPath(), 'package.json'), 'utf8')
) as { dshDesktopChannel?: unknown }
return metadata.dshDesktopChannel === 'development'
} catch {
return false
}
}

const developmentBuild = isDevelopmentBuild()

function configureAppIdentity(): void {
if (developmentBuild) {
app.setName('DSH Desktop Dev')
app.setPath('userData', join(app.getPath('appData'), 'dsh-desktop-dev'))
return
}

app.setName('DSH Desktop')
}

async function syncNativeTheme(window: BrowserWindow): Promise<void> {
if (window.isDestroyed()) return

// The sidebar already reserves enough room for macOS traffic lights. Read
// Harness's resolved theme before showing the window so the native surface
// matches the first rendered frame without injecting a second titlebar.
// matches the first rendered frame. The transparent drag strip restores the
// native window gesture without adding a visual titlebar or covering the
// traffic lights and right-side header actions.
const isDark = await window.webContents.executeJavaScript(
"document.body.hasAttribute('data-ds-dark-theme')"
`(() => {
if (${process.platform === 'darwin'}) {
let dragRegion = document.getElementById('dsh-desktop-drag-region')
if (!dragRegion) {
dragRegion = document.createElement('div')
dragRegion.id = 'dsh-desktop-drag-region'
dragRegion.setAttribute('aria-hidden', 'true')
Object.assign(dragRegion.style, {
position: 'fixed',
zIndex: '18',
top: '0',
left: '80px',
right: '220px',
height: '24px',
background: 'transparent',
pointerEvents: 'auto',
userSelect: 'none'
})
dragRegion.style.setProperty('-webkit-app-region', 'drag')
document.body.appendChild(dragRegion)
}
}
return document.body.hasAttribute('data-ds-dark-theme')
})()`
)
window.setBackgroundColor(isDark ? '#141416' : '#ffffff')
}
Expand Down Expand Up @@ -97,7 +148,12 @@ function createWindow(): BrowserWindow {
async function openHarness(url: string): Promise<void> {
const window = mainWindow && !mainWindow.isDestroyed() ? mainWindow : createWindow()
if (shouldLoadHarnessUrl(window.webContents.getURL(), url)) {
await window.loadURL(url)
try {
await window.loadURL(url)
} catch (error) {
if (isAbortedNavigationError(error)) return
throw error
}
}
if (runtime.snapshot().url !== url || window.isDestroyed()) return
await syncNativeTheme(window)
Expand Down Expand Up @@ -260,20 +316,22 @@ async function bootstrap(): Promise<void> {
})
installMenu()
await launchHarness()
startUpdateManager({
prepareToInstall: async () => {
await runtime.stop()
quitting = true
stopUpdateManager()
}
})
if (!developmentBuild) {
startUpdateManager({
prepareToInstall: async () => {
await runtime.stop()
quitting = true
stopUpdateManager()
}
})
}
}

configureAppIdentity()
const singleInstance = app.requestSingleInstanceLock()
if (!singleInstance) {
app.quit()
} else {
app.setName('DSH Desktop')
app.on('second-instance', () => {
const snapshot = runtime?.snapshot()
if (snapshot?.phase === 'ready' && snapshot.url) {
Expand Down
12 changes: 12 additions & 0 deletions src/main/window-navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,15 @@ export function shouldLoadHarnessUrl(currentUrl: string, targetUrl: string): boo
return true
}
}

export function isAbortedNavigationError(error: unknown): boolean {
if (typeof error !== 'object' || error === null) return false

const navigationError = error as { code?: unknown; errno?: unknown; message?: unknown }
if (navigationError.code === 'ERR_ABORTED' || navigationError.errno === -3) return true

return (
typeof navigationError.message === 'string' &&
/(?:^|\s)ERR_ABORTED\s*\(-3\)(?:\s|$)/.test(navigationError.message)
)
}
5 changes: 5 additions & 0 deletions test/branding-patch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ describe('DSH Desktop sidebar branding', () => {
expect(main).not.toContain('dsh-desktop-titlebar-style')
expect(main).not.toContain('--dsh-desktop-titlebar-height')
expect(main).not.toContain('body { box-sizing: border-box; padding-top:')
expect(main).toContain("dragRegion.id = 'dsh-desktop-drag-region'")
expect(main).toContain("dragRegion.style.setProperty('-webkit-app-region', 'drag')")
expect(main).toContain("left: '80px'")
expect(main).toContain("right: '220px'")
expect(main).toContain("height: '24px'")
})

it('pairs the DSH logo with the original Harness wordmark in the expanded sidebar', async () => {
Expand Down
33 changes: 33 additions & 0 deletions test/preset-transfer-patch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,39 @@ describe('agent preset package transfer', () => {
expect(patch).toContain('自定义预设可以使用与 Agent 相同权限的工具和命令')
expect(patch).toContain('draft.conflict ? "idTaken"')
expect(patch).toContain('.dshpreset')
expect(patch).toContain('importPreset: "Import"')
expect(patch).toContain('awesomePreset: "Awesome preset"')
expect(patch).toContain('https://www.dshdesktop.com/preset/')
expect(patch).toContain('"_blank", "noopener,noreferrer"')
expect(patch).toContain('AgentPresetSection_module_css_default.sectionActions')
})

it('keeps a large mode roster searchable, grouped, compact, and connected to Awesome Presets', async () => {
const patch = await readFile(
path.join(
projectRoot,
'patches',
'@deepseek-ai+dsh-client-ui-agent-preset+0.1.0-rc.6.patch'
),
'utf8'
)

expect(patch).toContain('searchPresets: "Search modes…"')
expect(patch).toContain('recentPresets: "Recent"')
expect(patch).toContain('RECENT_PRESETS_KEY')
expect(patch).toContain('option.trust === "system"')
expect(patch).toContain('option.trust === "user"')
expect(patch).toContain('text-overflow:ellipsis')
expect(patch).toContain('IconSearchOutline16')
expect(patch).toContain('IconSparkle16')
expect(patch).toContain('selectedItem')
expect(patch).toContain(':focus-within')
expect(patch).toContain('[role=menu]:has(')
expect(patch).toContain('max-height:min(360px')
expect(patch).toContain('side: "bottom"')
expect(patch).toContain('footer: [{')
expect(patch).toContain('id: AWESOME_PRESETS_ID')
expect(patch).toContain('browseAwesomePresets: "浏览 Awesome Presets…"')
})

it('keeps the loopback API discoverable by an explicitly requested online Skill', async () => {
Expand Down
20 changes: 20 additions & 0 deletions test/release.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,26 @@ describe('GitHub release contract', () => {
}
})

it('packages an isolated development channel from the current workspace', async () => {
const packageJson = JSON.parse(
await readFile(path.join(projectRoot, 'package.json'), 'utf8')
) as { scripts: Record<string, string> }
const developmentConfig = await readFile(
path.join(projectRoot, 'electron-builder.dev.cjs'),
'utf8'
)
const main = await readFile(path.join(projectRoot, 'src', 'main', 'index.ts'), 'utf8')

expect(packageJson.scripts['package:dev:dir']).toContain('npm run build')
expect(packageJson.scripts['package:dev:dir']).toContain('electron-builder.dev.cjs')
expect(developmentConfig).toContain("appId: 'io.dsh.desktop.dev'")
expect(developmentConfig).toContain("productName: 'DSH Desktop Dev'")
expect(developmentConfig).toContain("output: 'dist-dev'")
expect(developmentConfig).toContain("dshDesktopChannel: 'development'")
expect(main).toContain("app.setPath('userData', join(app.getPath('appData'), 'dsh-desktop-dev'))")
expect(main).toContain('if (!developmentBuild)')
})

it('builds and publishes every supported platform', async () => {
const workflow = await readFile(
path.join(projectRoot, '.github', 'workflows', 'release.yml'),
Expand Down
17 changes: 16 additions & 1 deletion test/runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { describe, expect, it } from 'vitest'
import { buildHarnessArguments, buildNodeArguments } from '../src/main/runtime/harness-runtime'
import { canGrantWindowPermission, isTrustedAppUrl } from '../src/main/security-policy'
import { shouldLoadHarnessUrl } from '../src/main/window-navigation'
import {
isAbortedNavigationError,
shouldLoadHarnessUrl
} from '../src/main/window-navigation'

describe('Harness launch contract', () => {
it('binds the web server to a random loopback port', () => {
Expand Down Expand Up @@ -91,4 +94,16 @@ describe('Harness window activation', () => {
shouldLoadHarnessUrl('http://127.0.0.1:43127/settings', 'http://127.0.0.1:43128')
).toBe(true)
})

it('recognizes Electron navigation cancellation without hiding other load failures', () => {
expect(isAbortedNavigationError({ code: 'ERR_ABORTED', errno: -3 })).toBe(true)
expect(
isAbortedNavigationError(
new Error("ERR_ABORTED (-3) loading 'http://127.0.0.1:43127/'")
)
).toBe(true)
expect(isAbortedNavigationError({ code: 'ERR_CONNECTION_REFUSED', errno: -102 })).toBe(
false
)
})
})
Loading