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
2 changes: 1 addition & 1 deletion patches/@deepseek-ai+dsh-client-ui-layout+0.1.2-rc.1.patch
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ index cdaea1a..131fc45 100644
* LG breakpoint); a manual toggle below it re-expands over the squeezed center
* (stores.ts narrowExpanded). */
const SIDEBAR_AUTO_COLLAPSE = 1024;
+ const COLLAPSED_SIDEBAR_WIDTH = navigator.userAgent.includes("Macintosh") ? 80 : 56;
+ const COLLAPSED_SIDEBAR_WIDTH = navigator.userAgent.includes("Macintosh") ? 96 : 56;
/**
* Clamp a panel width into its contract range.
* @param px - requested width.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ index 696591c..90c1520 100644
tag.dataset.plugin = "@deepseek-ai/dsh-client-ui-sidebar";
tag.dataset.pluginCss = tagId;
- tag.textContent = css;
+ tag.textContent = css + "[data-dsh-sidebar-root][data-dsh-sidebar-wide=\"true\"]{padding-top:32px}[data-dsh-sidebar-brand-identity]{gap:4px}" + (navigator.userAgent.includes("Macintosh") ? "[data-dsh-sidebar-root][data-dsh-sidebar-wide=\"true\"]{padding-top:28px}[data-dsh-sidebar-root][data-dsh-sidebar-wide=\"false\"]{padding:32px 22px 6px}" : "");
+ tag.textContent = css + "[data-dsh-sidebar-root][data-dsh-sidebar-wide=\"true\"]{padding-top:32px}[data-dsh-sidebar-brand-identity]{gap:4px}" + (navigator.userAgent.includes("Macintosh") ? "[data-dsh-sidebar-root][data-dsh-sidebar-wide=\"true\"]{padding-top:28px}[data-dsh-sidebar-root][data-dsh-sidebar-wide=\"false\"]{padding:28px 30px 6px}[data-dsh-sidebar-root][data-dsh-sidebar-wide=\"false\"] [class*=\"_logoRow\"]{height:60px;margin-bottom:8px}[data-dsh-sidebar-root][data-dsh-sidebar-wide=\"false\"] [class*=\"_newSession\"]{height:38px}" : "");
document.head.appendChild(tag);
}
var SidebarRoot_module_css_default = {
Expand Down
4 changes: 1 addition & 3 deletions src/main/context-menu-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,7 @@ export function isExternalWebUrl(rawUrl: string): boolean {
try {
const url = new URL(rawUrl)
return (
(url.protocol === 'https:' || url.protocol === 'http:') &&
url.hostname !== '127.0.0.1' &&
url.hostname !== 'localhost'
url.protocol === 'https:' || url.protocol === 'http:'
)
} catch {
return false
Expand Down
20 changes: 6 additions & 14 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
serializeGpuFallbackState
} from './gpu-fallback'
import { secureWindow } from './security'
import { MAC_WINDOW_BUTTON_POSITION } from './window-button-position'
import { SafeModeOverlay } from './safe-mode-overlay'
import { ensureLaunchRoot } from './state/launch-root'
import {
Expand Down Expand Up @@ -932,18 +933,8 @@ function createWindow(): BrowserWindow {
})
if (process.platform === 'darwin') {
window.setWindowButtonVisibility(true)
// Match the sidebar inset at the current zoom, with a 2px optical correction
// for the round native buttons relative to the logo's visible left edge.
const alignWindowButtons = (): void => {
if (window.isDestroyed()) return
window.setWindowButtonPosition({
x: Math.round(16 * window.webContents.getZoomFactor()) - 2,
y: 9
})
}
alignWindowButtons()
window.webContents.on('did-finish-load', alignWindowButtons)
window.webContents.on('zoom-changed', () => setImmediate(alignWindowButtons))
// Native window chrome stays fixed regardless of sidebar state or page zoom.
window.setWindowButtonPosition(MAC_WINDOW_BUTTON_POSITION)
} else if (isWindows) {
window.setMenuBarVisibility(false)
}
Expand All @@ -964,7 +955,8 @@ function createWindow(): BrowserWindow {
appendRendererPluginFailureLog(details.message)
})
installPluginRecoveryNavigation(window)
secureWindow(window)
// Resolve dynamically because Harness may restart on a different port.
secureWindow(window, () => runtime?.snapshot().url)
installContextMenu(window, harnessLocale)
installMainWindowRendererRecovery(window)
window.on('closed', () => {
Expand Down Expand Up @@ -2567,7 +2559,7 @@ async function showMobilePairing(): Promise<void> {
webSecurity: true
}
})
secureWindow(mobileWindow)
secureWindow(mobileWindow, () => snapshot.desktopUrl)
mobileWindow.on('closed', () => {
mobileWindow = undefined
})
Expand Down
2 changes: 1 addition & 1 deletion src/main/safe-mode-overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class SafeModeOverlay {
this.webContents = this.view.webContents
this.view.setBackgroundColor('#00000000')
this.view.setVisible(false)
secureWindow(this.view)
secureWindow(this.view, () => undefined)
parent.contentView.addChildView(this.view)
parent.on('resize', this.syncBounds)
parent.on('enter-full-screen', this.syncBounds)
Expand Down
9 changes: 7 additions & 2 deletions src/main/security-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@ function isHarnessUrl(rawUrl: string): boolean {
}
}

export function isTrustedAppUrl(rawUrl: string): boolean {
export function isTrustedAppUrl(rawUrl: string, appUrl?: string): boolean {
try {
const parsed = new URL(rawUrl)
if (parsed.protocol === 'file:' || parsed.protocol === 'dsh-recovery:') return true
} catch {
return false
}
return isHarnessUrl(rawUrl)
if (!appUrl || !isHarnessUrl(appUrl)) return false
try {
return new URL(rawUrl).origin === new URL(appUrl).origin
} catch {
return false
}
}

export function canGrantWindowPermission(
Expand Down
9 changes: 6 additions & 3 deletions src/main/security.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { shell, type BrowserWindow } from 'electron'
import { canGrantWindowPermission, isTrustedAppUrl } from './security-policy'

export function secureWindow(window: Pick<BrowserWindow, 'webContents'>): void {
export function secureWindow(
window: Pick<BrowserWindow, 'webContents'>,
appUrl: () => string | undefined
): void {
window.webContents.setWindowOpenHandler(({ url }) => {
if (isTrustedAppUrl(url)) return { action: 'allow' }
if (isTrustedAppUrl(url, appUrl())) return { action: 'allow' }
if (url.startsWith('https://') || url.startsWith('http://')) void shell.openExternal(url)
return { action: 'deny' }
})

window.webContents.on('will-navigate', (event, url) => {
if (isTrustedAppUrl(url)) return
if (isTrustedAppUrl(url, appUrl())) return
event.preventDefault()
if (url.startsWith('https://') || url.startsWith('http://')) void shell.openExternal(url)
})
Expand Down
9 changes: 9 additions & 0 deletions src/main/window-button-position.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Native macOS controls span 60 DIP and do not scale with web content.
const TRAFFIC_LIGHT_WIDTH = 60
const COLLAPSED_SIDEBAR_WIDTH = 96

// Center in the collapsed rail at Actual Size; never follow renderer geometry.
export const MAC_WINDOW_BUTTON_POSITION = Object.freeze({
x: (COLLAPSED_SIDEBAR_WIDTH - TRAFFIC_LIGHT_WIDTH) / 2,
y: 9
})
12 changes: 8 additions & 4 deletions test/branding-patch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ describe('DSH Desktop sidebar branding', () => {
expect(main).toContain("document.body.hasAttribute('data-ds-dark-theme')")
expect(main).toContain("window.setBackgroundColor(isDark ? '#141416' : '#ffffff')")
expect(main).toContain('window.setWindowButtonVisibility(true)')
expect(main).toContain('x: Math.round(16 * window.webContents.getZoomFactor()) - 2')
expect(main).toContain('window.setWindowButtonPosition(MAC_WINDOW_BUTTON_POSITION)')
expect(main).not.toContain('desktop:window-button-anchor')
expect(main).not.toContain('alignWindowButtons')
expect(main).toContain("titleBarStyle: 'hidden' as const")
expect(main).not.toContain('dsh-desktop-titlebar-style')
expect(main).not.toContain('--dsh-desktop-titlebar-height')
Expand Down Expand Up @@ -64,20 +66,22 @@ describe('DSH Desktop sidebar branding', () => {
expect(patch).toContain('[data-dsh-sidebar-brand-identity]{gap:4px}')
expect(patch).toContain('navigator.userAgent.includes("Macintosh")')
expect(patch).toContain('padding-top:28px')
expect(patch).toContain('padding:32px 22px 6px')
expect(patch).toContain('padding:28px 30px 6px')
expect(patch).toContain('{height:60px;margin-bottom:8px}')
expect(patch).toContain('{height:38px}')
expect(installedSidebar).toContain('renderSlot("sidebar.brand.mark"')
expect(installedSidebar).toContain('renderSlot("sidebar.brand.name"')
expect(installedSidebar).not.toContain('DshDesktopBrand')
expect(installedSidebar).not.toContain('brandWordmark')
})

it('uses an 80px macOS rail that clears the traffic lights', async () => {
it('uses a 96px macOS rail that clears the traffic lights', async () => {
const patch = await readFile(
patchPath('@deepseek-ai/dsh-client-ui-layout'),
'utf8'
)

expect(patch).toContain('navigator.userAgent.includes("Macintosh") ? 80 : 56')
expect(patch).toContain('navigator.userAgent.includes("Macintosh") ? 96 : 56')
expect(patch).toContain('sidebar === 0 ? COLLAPSED_SIDEBAR_WIDTH')
})

Expand Down
4 changes: 2 additions & 2 deletions test/context-menu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ describe('conversation context menu', () => {
it('recognizes only HTTP and HTTPS as external web URLs', () => {
expect(isExternalWebUrl('https://example.com')).toBe(true)
expect(isExternalWebUrl('http://example.com')).toBe(true)
expect(isExternalWebUrl('http://127.0.0.1:43127/session')).toBe(false)
expect(isExternalWebUrl('http://localhost:43127/settings')).toBe(false)
expect(isExternalWebUrl('http://127.0.0.1:43127/session')).toBe(true)
expect(isExternalWebUrl('http://localhost:43127/settings')).toBe(true)
expect(isExternalWebUrl('file:///tmp/report.html')).toBe(false)
expect(isExternalWebUrl('not a URL')).toBe(false)
})
Expand Down
10 changes: 7 additions & 3 deletions test/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,10 +516,14 @@ describe('offending plugin extraction', () => {
})

describe('navigation trust boundary', () => {
it('only trusts the launcher and loopback HTTP pages', () => {
it('only trusts the launcher and the exact configured app origin', () => {
expect(isTrustedAppUrl('file:///app/index.html')).toBe(true)
expect(isTrustedAppUrl('http://127.0.0.1:43127')).toBe(true)
expect(isTrustedAppUrl('http://localhost:43127')).toBe(true)
const appUrl = 'http://127.0.0.1:43127'
expect(isTrustedAppUrl(`${appUrl}/session/1`, appUrl)).toBe(true)
expect(isTrustedAppUrl('http://localhost:43127', appUrl)).toBe(false)
expect(isTrustedAppUrl('http://127.0.0.1:8080', appUrl)).toBe(false)
expect(isTrustedAppUrl(appUrl)).toBe(false)
expect(isTrustedAppUrl('http://localhost:43127', 'http://localhost:43127')).toBe(true)
expect(isTrustedAppUrl('https://127.0.0.1:43127')).toBe(false)
expect(isTrustedAppUrl('http://example.com')).toBe(false)
expect(isTrustedAppUrl('javascript:alert(1)')).toBe(false)
Expand Down
39 changes: 39 additions & 0 deletions test/security.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, expect, it, vi } from 'vitest'

vi.mock('electron', () => ({ shell: { openExternal: vi.fn().mockResolvedValue(undefined) } }))
import { shell } from 'electron'
import { secureWindow } from '../src/main/security'

describe('desktop link routing', () => {
it('routes other origins externally and follows the current runtime port', () => {
const listeners = new Map<string, (...args: any[]) => void>()
let open!: (details: { url: string }) => { action: string }
let appUrl = 'http://127.0.0.1:43127'
const webContents = {
setWindowOpenHandler: vi.fn((handler) => { open = handler }),
on: vi.fn((event, handler) => { listeners.set(event, handler) }),
session: {
setPermissionCheckHandler: vi.fn(),
setPermissionRequestHandler: vi.fn()
}
}
secureWindow({ webContents } as unknown as Parameters<typeof secureWindow>[0], () => appUrl)
expect(open({ url: `${appUrl}/session` })).toEqual({ action: 'allow' })
for (const url of ['http://127.0.0.1:8080/', 'http://localhost:8080/', 'https://example.com/']) {
expect(open({ url })).toEqual({ action: 'deny' })
expect(shell.openExternal).toHaveBeenLastCalledWith(url)
const preventDefault = vi.fn()
listeners.get('will-navigate')!({ preventDefault }, url)
expect(preventDefault).toHaveBeenCalledOnce()
}
const preventDefault = vi.fn()
listeners.get('will-navigate')!({ preventDefault }, `${appUrl}/settings`)
expect(preventDefault).not.toHaveBeenCalled()
appUrl = 'http://127.0.0.1:43128'
expect(open({ url: 'http://127.0.0.1:43127/session' })).toEqual({ action: 'deny' })
expect(open({ url: `${appUrl}/session` })).toEqual({ action: 'allow' })
vi.mocked(shell.openExternal).mockClear()
expect(open({ url: 'javascript:alert(1)' })).toEqual({ action: 'deny' })
expect(shell.openExternal).not.toHaveBeenCalled()
})
})
10 changes: 10 additions & 0 deletions test/window-button-position.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { describe, expect, it } from 'vitest'
import { MAC_WINDOW_BUTTON_POSITION } from '../src/main/window-button-position'

describe('macOS traffic light alignment', () => {
it('uses an immutable native position centered in the Actual Size rail', () => {
expect(MAC_WINDOW_BUTTON_POSITION).toEqual({ x: 18, y: 9 })
expect(MAC_WINDOW_BUTTON_POSITION.x + 60 / 2).toBe(96 / 2)
expect(Object.isFrozen(MAC_WINDOW_BUTTON_POSITION)).toBe(true)
})
})
Loading