\n {hero &&
}\n {sessionId === undefined ? null :
}',
+ reason: 'Let the idle mark and headline flex-center separately while the resident composer occupies the viewport floor.',
},
{
id: 'stats-header-slot', file: 'packages/client/ui-chat/src/client/apply.ts',
diff --git a/apps/deepviewer-adapter/desktop/bootstrap.ts b/apps/deepviewer-adapter/desktop/bootstrap.ts
index 990d2b3..bd5cbb3 100644
--- a/apps/deepviewer-adapter/desktop/bootstrap.ts
+++ b/apps/deepviewer-adapter/desktop/bootstrap.ts
@@ -1,11 +1,10 @@
import { configureRuntimeNetwork } from './network/runtime-network.ts'
-import { app, nativeImage, nativeTheme, session, dialog } from 'electron'
+import { app, session, dialog } from 'electron'
import { join } from 'node:path'
import { readFileSync } from 'node:fs'
import { configureDevelopmentProfile, resolveInstalledUserDataPath, developmentUserDataOverride } from './profile/development-profile.js'
import { migrateDeepViewerProfile, migrateDeepViewerFileNames } from './profile/profile-migration.js'
import { prepareUserData } from './profile/user-data-migration.js'
-import { followDockTheme } from './profile/dock-theme.js'
import { prepareDesktopProfile } from '../migrations/desktop-profile.ts'
import { prepareWebState } from '../migrations/web-state.ts'
import { prepareKernelSnapshot } from '../migrations/kernel-snapshot.ts'
@@ -62,10 +61,7 @@ export function initializeDeepViewer(): boolean {
return Object.fromEntries(Object.entries(spec.env).filter(([key]) => key.startsWith('DEEPVIEWER_') || ['HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy'].includes(key)))
})
;(globalThis as typeof globalThis & { __DEEPVIEWER_HOST_ENVIRONMENT__?: Promise
}).__DEEPVIEWER_HOST_ENVIRONMENT__ = hostEnvironment
- void app.whenReady().then(() => {
- if (!app.dock) return
- const dispose = followDockTheme({ theme: nativeTheme, resourcesPath: app.isPackaged ? process.resourcesPath : join(appRoot, '.desktop', 'native-icon'), loadImage: nativeImage.createFromPath, setIcon: image => app.dock?.setIcon(image), log: message => console.info('[DeepViewer icon]', message), onError: message => console.error('[DeepViewer icon]', message) })
- app.once('will-quit', dispose)
- })
+ // macOS selects the compiled bundle icon and its system appearance. A
+ // runtime PNG override would replace that catalog with full-bleed artwork.
return true
}
diff --git a/apps/deepviewer-adapter/desktop/profile/dock-theme.ts b/apps/deepviewer-adapter/desktop/profile/dock-theme.ts
deleted file mode 100644
index 4ccfef4..0000000
--- a/apps/deepviewer-adapter/desktop/profile/dock-theme.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { join } from 'node:path'
-
-interface ThemeSource {
- readonly shouldUseDarkColors: boolean
- on(event: 'updated', listener: () => void): unknown
- removeListener(event: 'updated', listener: () => void): unknown
-}
-
-// nativeTheme resolves light/dark/system from the existing renderer theme IPC.
-// These PNGs are extracted from Apple's compiled ICNS, not full-bleed artwork.
-export function followDockTheme(options: {
- theme: ThemeSource
- resourcesPath: string
- loadImage(path: string): Image
- setIcon(image: Image): void
- log(message: string): void
- onError(message: string): void
-}): () => void {
- const images = new Map()
- let applied: string | undefined
- const update = () => {
- const appearance = options.theme.shouldUseDarkColors ? 'dark' : 'light'
- if (applied === appearance) return
- try {
- let image = images.get(appearance)
- if (!image) {
- image = options.loadImage(join(options.resourcesPath, 'DeepViewerDockThemes', `${appearance}.png`))
- if (image.isEmpty()) throw new Error(`Missing native Dock rendition: ${appearance}`)
- images.set(appearance, image)
- }
- options.setIcon(image)
- applied = appearance
- options.log(`appearance=${appearance}`)
- } catch (error) {
- options.onError(error instanceof Error ? error.message : String(error))
- }
- }
- options.theme.on('updated', update)
- update()
- return () => options.theme.removeListener('updated', update)
-}
diff --git a/apps/deepviewer-adapter/package.json b/apps/deepviewer-adapter/package.json
index 771bb98..0ddb10d 100644
--- a/apps/deepviewer-adapter/package.json
+++ b/apps/deepviewer-adapter/package.json
@@ -1,6 +1,6 @@
{
"name": "@deepviewer/adapter",
- "version": "0.5.0",
+ "version": "0.5.1",
"private": true,
"type": "module",
"license": "MIT",
diff --git a/apps/deepviewer-adapter/presentation/smoke-renderer.js.txt b/apps/deepviewer-adapter/presentation/smoke-renderer.js.txt
index 12e1879..1b920c3 100644
--- a/apps/deepviewer-adapter/presentation/smoke-renderer.js.txt
+++ b/apps/deepviewer-adapter/presentation/smoke-renderer.js.txt
@@ -20,9 +20,14 @@
const hero = heroMark.closest('[data-deepviewer-hero]');
if (!hero || !/让我们做点什么|What shall we build\?/.test(hero.textContent) || hero.querySelector('canvas')) throw new Error('DeepViewer welcome branding is missing');
const markStyle = getComputedStyle(heroMark);
- if (!markStyle.maskImage.includes('/assets/deepviewer/deepviewer-loading-logo.svg') || heroMark.getBoundingClientRect().height < 100) throw new Error('Original DeepViewer mark is not rendered');
- const markResponse = await fetch('/assets/deepviewer/deepviewer-loading-logo.svg');
- if (!markResponse.ok || !(await markResponse.text()).includes('viewBox="0 0 150.374 160"')) throw new Error('Original DeepViewer mark asset is missing');
+ if (heroMark.localName !== 'svg' || heroMark.getAttribute('viewBox') !== '0 0 150.374 160' || heroMark.querySelectorAll('path').length !== 2 || Math.abs(heroMark.getBoundingClientRect().height - 48) > 0.1 || markStyle.opacity !== '0.5') throw new Error('Released DeepViewer idle mark geometry is not rendered');
+ const cursor = heroMark.querySelector('[data-deepviewer-brand-cursor]');
+ const cursorStyle = getComputedStyle(cursor);
+ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches ? cursorStyle.animationName !== 'none' : (!cursorStyle.animationName.includes('deepviewer-hero-cursor-blink') || cursorStyle.animationDuration !== '1s' || !(cursorStyle.animationTimingFunction === 'step-end' || cursorStyle.animationTimingFunction.startsWith('steps(1')))) throw new Error('Released DeepViewer idle cursor animation is missing');
+ const titleStyle = getComputedStyle(hero.querySelector('[data-deepviewer-hero-title]'));
+ if (titleStyle.fontSize !== '24px' || titleStyle.lineHeight !== '32px' || titleStyle.opacity !== '0.5' || getComputedStyle(hero.querySelector('[data-deepviewer-headline]')).gap !== '40px') throw new Error('Released DeepViewer idle text spacing is missing');
+ const scrollBody = hero.parentElement, composerSeat = scrollBody.querySelector(':scope > [data-deepviewer-composer-seat]');
+ if (!composerSeat || composerSeat.contains(hero) || scrollBody.getBoundingClientRect().bottom - composerSeat.getBoundingClientRect().bottom > 1) throw new Error('Idle hero is not separate from the bottom resident composer');
// Synthetic status checks live content-size variables without running a model.
const probe = document.createElement('div'); probe.setAttribute('data-chat-running', '');
const normal = document.createElement('span'); normal.style.color = 'var(--dsw-alias-label-secondary)';
diff --git a/apps/deepviewer-adapter/tests/idle-presentation.spec.tsx.txt b/apps/deepviewer-adapter/tests/idle-presentation.spec.tsx.txt
new file mode 100644
index 0000000..ace5976
--- /dev/null
+++ b/apps/deepviewer-adapter/tests/idle-presentation.spec.tsx.txt
@@ -0,0 +1,32 @@
+// Appended to the native skeleton suite so its genuine owner/store/input
+// fixtures exercise the presentation seam, without launching the desktop app.
+describe('DeepViewer released idle posture', () => {
+ it.each(['sessionless', 'blank'])('keeps the %s idle hero outside the resident composer', mode => {
+ const b = mount(sessionSnapshotOf({ blank: true }), undefined, undefined, {
+ sessionId: mode === 'sessionless' ? undefined : SID,
+ })
+ const host = b.view.container.querySelector('[data-conversation-scroll]')
+ const hero = b.view.container.querySelector('[data-deepviewer-hero]')
+ const seat = b.view.container.querySelector('[data-composer-seat]')
+ const input = b.view.getByRole('textbox')
+ expect(host).not.toBeNull()
+ expect(hero).not.toBeNull()
+ expect(seat).not.toBeNull()
+ expect(hero?.parentElement).toBe(host)
+ expect(seat?.parentElement).toBe(host)
+ expect(seat?.contains(hero)).toBe(false)
+ expect(seat?.contains(input)).toBe(true)
+ expect(host?.querySelectorAll('[data-deepviewer-hero]')).toHaveLength(1)
+ if (!hero || !seat) throw new Error('Released idle posture is missing')
+ expect(hero.compareDocumentPosition(seat) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
+ if (mode === 'blank') {
+ act(() => { b.wiring.setDraft('keep the resident draft') })
+ b.session.set(sessionSnapshotOf({ blank: false }))
+ b.rerender()
+ expect(b.view.container.querySelector('[data-deepviewer-hero]')).toBeNull()
+ expect(b.view.container.querySelector('[data-composer-seat]')).toBe(seat)
+ expect(b.view.getByRole('textbox')).toBe(input)
+ expect(b.wiring.snapshot.draft).toBe('keep the resident draft')
+ }
+ })
+})
diff --git a/apps/deepviewer-desktop/assets/DeepViewer.icon/Assets/Whale.svg b/apps/deepviewer-desktop/assets/DeepViewer.icon/Assets/Whale.svg
new file mode 100644
index 0000000..67c8e83
--- /dev/null
+++ b/apps/deepviewer-desktop/assets/DeepViewer.icon/Assets/Whale.svg
@@ -0,0 +1,6 @@
+
diff --git a/apps/deepviewer-desktop/assets/DeepViewer.icon/icon.json b/apps/deepviewer-desktop/assets/DeepViewer.icon/icon.json
new file mode 100644
index 0000000..7c1d706
--- /dev/null
+++ b/apps/deepviewer-desktop/assets/DeepViewer.icon/icon.json
@@ -0,0 +1,25 @@
+{
+ "fill-specializations": [
+ { "value": { "solid": "extended-gray:1.00000,1.00000" } }
+ ],
+ "groups": [
+ {
+ "layers": [
+ {
+ "image-name": "Whale.svg",
+ "name": "DeepViewer Whale",
+ "fill-specializations": [
+ { "value": { "solid": "extended-gray:0.14000,1.00000" } }
+ ],
+ "position": {
+ "scale": 4.375,
+ "translation-in-points": [47, 2]
+ }
+ }
+ ],
+ "shadow": { "kind": "neutral", "opacity": 0.5 },
+ "translucency": { "enabled": false, "value": 0 }
+ }
+ ],
+ "supported-platforms": { "squares": "shared" }
+}
diff --git a/apps/deepviewer-desktop/assets/NATIVE-ICON.md b/apps/deepviewer-desktop/assets/NATIVE-ICON.md
new file mode 100644
index 0000000..cc25394
--- /dev/null
+++ b/apps/deepviewer-desktop/assets/NATIVE-ICON.md
@@ -0,0 +1,25 @@
+# DeepViewer native macOS icon
+
+`DeepViewer.icon/Assets/Whale.svg` is the original DeepViewer whale vector,
+copied byte-for-byte from the shipped `deepviewer-loading-logo.svg` artwork
+(SHA-256 `16896da065025e41040115cc8f16f0268a94200bd5ea3c939e27e3078dff1816`).
+The Icon Composer document places that vector on a white background with a
+dark foreground fill. It has no pre-masked background bitmap. Layer placement
+preserves the existing brand composition in the 1024-point design canvas.
+
+Xcode 26 or newer is required for a native desktop build or package. `actool`
+compiles the document into `Assets.car`, the `DeepViewer` native icon and a
+complete `DeepViewer.icns` fallback using `--standalone-icon-behavior all`.
+The Apple compiler supplies the mask, appearance variants and legacy canvas
+margins. Both the development carrier and release bundle declare
+`CFBundleIconName=DeepViewer` and retain the matching ICNS fallback. The app
+does not replace this bundle icon at runtime with `app.dock.setIcon`.
+
+The old flattened PNGs remain in-app artwork only; they are not Dock or bundle
+icon build inputs. The native About panel uses `icon.png`, extracted byte-for-byte
+from Apple's largest compiled ICNS rendition. Core-only CI builds do not require
+the icon compiler.
+
+References: [Apple Icon Composer guidance](https://developer.apple.com/documentation/xcode/creating-your-app-icon-using-icon-composer),
+[Apple app icon design guidance](https://developer.apple.com/design/human-interface-guidelines/app-icons),
+and the Xcode `actool(1)` manual for compiler output and partial Info.plist keys.
diff --git a/apps/deepviewer-desktop/package.json b/apps/deepviewer-desktop/package.json
index f52e29a..92c3c57 100644
--- a/apps/deepviewer-desktop/package.json
+++ b/apps/deepviewer-desktop/package.json
@@ -1,9 +1,9 @@
{
"name": "@deepviewer/desktop",
"productName": "DeepViewer",
- "version": "0.5.0",
- "buildNumber": 90,
- "previewRelease": "0.5.0-preview.1",
+ "version": "0.5.1",
+ "buildNumber": 91,
+ "previewRelease": "0.5.1-preview.1",
"private": true,
"description": "DeepViewer agent workspace for DeepSeek Harness",
"license": "MIT",
@@ -22,7 +22,7 @@
"package:arm64": "node scripts/package-native.mjs --arch=arm64",
"test": "pnpm smoke",
"typecheck": "tsc --noEmit",
- "smoke": "vitest run test/native-workspace.test.ts test/native-package-closure.test.ts test/native-migration.test.ts test/native-modules.test.ts test/user-data-migration.test.ts test/profile-migration.test.ts test/dock-theme.test.ts test/network-policy.test.ts test/network-access.test.ts test/network-bridge.test.ts test/subscriptions-v4.test.ts test/subscriptions-ui-adapter.test.ts test/development-workflow.test.ts test/development-inputs.test.ts test/native-release-audit.test.ts test/native-build-metadata.test.ts test/native-package-metadata.test.ts",
+ "smoke": "vitest run test/native-workspace.test.ts test/native-package-closure.test.ts test/native-migration.test.ts test/native-modules.test.ts test/user-data-migration.test.ts test/profile-migration.test.ts test/dock-theme.test.ts test/native-idle-brand.test.ts test/network-policy.test.ts test/network-access.test.ts test/network-bridge.test.ts test/subscriptions-v4.test.ts test/subscriptions-ui-adapter.test.ts test/development-workflow.test.ts test/development-inputs.test.ts test/native-release-audit.test.ts test/native-build-metadata.test.ts test/native-package-metadata.test.ts",
"smoke:data": "node --expose-internals scripts/smoke-user-data.mjs",
"build:dev": "node scripts/bump-build-number.mjs && node scripts/build-native-desktop.mjs",
"smoke:native": "node scripts/smoke-native-host.mjs && node scripts/smoke-native-renderer.mjs && node scripts/smoke-native-web-state.mjs && node scripts/smoke-native-updates.mjs",
diff --git a/apps/deepviewer-desktop/scripts/development-shell.mjs b/apps/deepviewer-desktop/scripts/development-shell.mjs
index 696fd9a..0eb5614 100644
--- a/apps/deepviewer-desktop/scripts/development-shell.mjs
+++ b/apps/deepviewer-desktop/scripts/development-shell.mjs
@@ -41,18 +41,17 @@ export async function prepareDevelopmentShell(electronExecutable, appRoot) {
await cp(sourceBundle, bundle, { recursive: true, verbatimSymlinks: true, mode: constants.COPYFILE_FICLONE })
const resources = join(bundle, 'Contents', 'Resources')
await copyFile(icon.icns, join(resources, 'DeepViewer.icns'))
- await cp(icon.dockThemes, join(resources, 'DeepViewerDockThemes'), { recursive: true })
+ await copyFile(icon.catalog, join(resources, 'Assets.car'))
const plist = join(bundle, 'Contents', 'Info.plist')
for (const [key, value] of Object.entries({
CFBundleName: 'DeepViewer Dev', CFBundleDisplayName: 'DeepViewer Dev',
CFBundleIdentifier: 'com.deepviewer.desktop.dev',
- CFBundleIconFile: 'DeepViewer.icns', CFBundleShortVersionString: manifest.version,
+ CFBundleIconFile: 'DeepViewer.icns', CFBundleIconName: icon.name,
+ CFBundleShortVersionString: manifest.version,
CFBundleVersion: String(manifest.buildNumber),
})) {
await execute('/usr/bin/plutil', ['-replace', key, '-string', value, plist])
}
- // The inherited Electron bundle may declare a layered icon; use our ICNS.
- await execute('/usr/bin/plutil', ['-remove', 'CFBundleIconName', plist]).catch(() => {})
// Copied artwork and the Electron carrier can retain Finder/resource-fork
// metadata. Remove it only from this generated bundle before ad-hoc signing.
await execute('/usr/bin/xattr', ['-cr', bundle])
diff --git a/apps/deepviewer-desktop/scripts/native-app-icon.mjs b/apps/deepviewer-desktop/scripts/native-app-icon.mjs
index 7519a52..392130d 100644
--- a/apps/deepviewer-desktop/scripts/native-app-icon.mjs
+++ b/apps/deepviewer-desktop/scripts/native-app-icon.mjs
@@ -1,36 +1,77 @@
+import { execFile } from 'node:child_process'
import { createHash } from 'node:crypto'
-import { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
-import { join } from 'node:path'
+import { access, copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
+import { basename, join } from 'node:path'
+import { promisify } from 'node:util'
-// Preserve DeepViewer's original artwork byte-for-byte. Native packaging and
-// development use the shipped ICNS, without requiring Icon Composer or Xcode.
+const execute = promisify(execFile)
+
+// The foreground is DeepViewer's original vector artwork. Apple owns the
+// background mask, system appearances and legacy transparent margins; never
+// resize the old full-bleed PNG to approximate native macOS icon geometry.
export async function compileNativeAppIcon(appRoot) {
- const sourceIcns = await readFile(join(appRoot, 'assets', 'DeepViewer.icns'))
- const sourcePng = await readFile(join(appRoot, 'assets', 'deepviewer-icon-macos26-1024.png'))
- const fingerprint = createHash('sha256')
- .update(sourceIcns).update(sourcePng).update(await readFile(new URL(import.meta.url)))
- .digest('hex')
+ const source = join(appRoot, 'assets', 'DeepViewer.icon')
+ const document = JSON.parse(await readFile(join(source, 'icon.json'), 'utf8'))
+ const images = [...new Set(document.groups.flatMap(group => group.layers.map(layer => layer['image-name'])))].sort()
+ if (images.some(name => typeof name !== 'string' || basename(name) !== name || name === '..')) {
+ throw new Error('Native icon layers must be files within DeepViewer.icon/Assets')
+ }
+ const compiler = (await execute('/usr/bin/xcodebuild', ['-version'])).stdout
+ const major = Number(compiler.match(/Xcode (\d+)/u)?.[1])
+ if (!Number.isFinite(major) || major < 26) throw new Error('DeepViewer native icons require Xcode 26 or newer')
+ const hash = createHash('sha256').update(compiler).update(await readFile(new URL(import.meta.url)))
+ const paths = ['icon.json', ...images.map(name => `Assets/${name}`)]
+ for (const path of paths) hash.update(path).update(await readFile(join(source, path)))
+ const fingerprint = hash.digest('hex')
const output = join(appRoot, '.desktop', 'native-icon')
const stamp = join(output, 'source.sha256')
const result = {
+ catalog: join(output, 'Assets.car'),
icns: join(output, 'DeepViewer.icns'),
- dockThemes: join(output, 'DeepViewerDockThemes'),
+ info: join(output, 'icon-info.plist'),
+ about: join(output, 'icon.png'),
name: 'DeepViewer',
fingerprint,
}
- const light = join(result.dockThemes, 'light.png')
- const dark = join(result.dockThemes, 'dark.png')
- const complete = await Promise.all([result.icns, light, dark].map(path => access(path)))
+ const complete = await Promise.all([result.catalog, result.icns, result.info, result.about].map(path => access(path)))
.then(() => true, () => false)
if (complete && await readFile(stamp, 'utf8').catch(() => '') === fingerprint) return result
- // Clear stale layered catalogs so a previous Scoder build cannot leak into
- // the restored DeepViewer bundle.
+ // Clear all obsolete PNG Dock overrides and previous product catalogs.
await rm(output, { recursive: true, force: true })
- await mkdir(result.dockThemes, { recursive: true })
- await writeFile(result.icns, sourceIcns)
- // DeepViewer's original Dock icon is the same in both app appearances.
- await writeFile(light, sourcePng)
- await writeFile(dark, sourcePng)
+ const buildDocument = join(output, 'DeepViewer.icon')
+ await mkdir(join(buildDocument, 'Assets'), { recursive: true })
+ for (const path of paths) await copyFile(join(source, path), join(buildDocument, path))
+ await execute('/usr/bin/xcrun', [
+ 'actool', buildDocument, '--compile', output,
+ '--output-format', 'human-readable-text', '--notices', '--warnings',
+ '--output-partial-info-plist', result.info,
+ '--app-icon', result.name, '--include-all-app-icons',
+ '--standalone-icon-behavior', 'all', '--enable-on-demand-resources', 'NO',
+ '--development-region', 'en', '--target-device', 'mac',
+ '--minimum-deployment-target', '13.0', '--platform', 'macosx',
+ ])
+ const info = JSON.parse((await execute('/usr/bin/plutil', ['-convert', 'json', '-o', '-', result.info])).stdout)
+ if (info.CFBundleIconName !== result.name || info.CFBundleIconFile !== result.name) {
+ throw new Error('Apple icon compilation produced inconsistent native and legacy names')
+ }
+ const icns = await readFile(result.icns)
+ if (icns.subarray(0, 4).toString() !== 'icns' || (await readFile(result.catalog)).length === 0) {
+ throw new Error('Apple icon compilation did not produce the native catalog and legacy ICNS')
+ }
+ // About panels take a raster path. Extract Apple's largest legacy rendition;
+ // do not resize, mask or recolor an image, and never use it as a Dock override.
+ let largest
+ for (let offset = 8; offset < icns.length;) {
+ const length = icns.readUInt32BE(offset + 4)
+ if (length < 8 || offset + length > icns.length) throw new Error('Malformed compiled ICNS entry')
+ if (icns.toString('ascii', offset, offset + 4) === 'ic10') largest = icns.subarray(offset + 8, offset + length)
+ offset += length
+ }
+ if (!largest || !largest.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))
+ || largest.readUInt32BE(16) !== 1024 || largest.readUInt32BE(20) !== 1024) {
+ throw new Error('Apple ICNS is missing its largest 1024px PNG rendition')
+ }
+ await writeFile(result.about, largest)
await writeFile(stamp, fingerprint)
return result
}
diff --git a/apps/deepviewer-desktop/scripts/package-native.mjs b/apps/deepviewer-desktop/scripts/package-native.mjs
index 32ed041..8a6a564 100644
--- a/apps/deepviewer-desktop/scripts/package-native.mjs
+++ b/apps/deepviewer-desktop/scripts/package-native.mjs
@@ -77,7 +77,7 @@ export async function packagePreparedNative(stage,{output,signed,channel='latest
// The default builder signer converts the hash back to a potentially ambiguous
// display name. Retain the exact certificate through its standard sign helper.
sign:signed?options=>desktopRequire('app-builder-lib/out/codeSign/macCodeSign.js').sign({...options,identity:certificate.hash,identityValidation:true}):undefined,
- forceCodeSigning:signed,hardenedRuntime:signed,entitlements:join(upstream,'apps/desktop/scripts/macos-entitlements.plist'),entitlementsInherit:join(upstream,'apps/desktop/scripts/macos-entitlements.plist'),signIgnore:['/Contents/Resources/app\\.asar\\.unpacked/dsh(?:/|$)','/Contents/Resources/runtime/primary-runtime(?:/|$)','\\.pak$'],notarize:false,extendInfo:{CFBundleLocalizations:['en','zh_CN'],NSMicrophoneUsageDescription:'DeepViewer 使用麦克风进行语音输入。'}},
+ forceCodeSigning:signed,hardenedRuntime:signed,entitlements:join(upstream,'apps/desktop/scripts/macos-entitlements.plist'),entitlementsInherit:join(upstream,'apps/desktop/scripts/macos-entitlements.plist'),signIgnore:['/Contents/Resources/app\\.asar\\.unpacked/dsh(?:/|$)','/Contents/Resources/runtime/primary-runtime(?:/|$)','\\.pak$'],notarize:false,extendInfo:{CFBundleIconName:'DeepViewer',CFBundleLocalizations:['en','zh_CN'],NSMicrophoneUsageDescription:'DeepViewer 使用麦克风进行语音输入。'}},
publish:signed?[feed]:null,
afterPack:async context=>{const app=join(context.appOutDir,`${context.packager.appInfo.productFilename}.app`);const resources=join(app,'Contents/Resources');execFileSync('/usr/bin/xattr',['-cr',app]);await auditNativePackagedApp({appPath:app,projectRoot:root});if(signed)writeFileSync(join(resources,'app-update.yml'),dump(feed));const {verifyRuntimeArchive}=await import(join(upstream,'apps/desktop/scripts/verify-runtime-archive.ts'));await verifyRuntimeArchive(join(resources,'app.asar'),JSON.parse(readFileSync(join(stage,'app/dsh/desktop-runtime.json'),'utf8')))},
afterSign:signed?async context=>{const app=join(context.appOutDir,`${context.packager.appInfo.productFilename}.app`);await desktopRequire('@electron/notarize').notarize({tool:'notarytool',appPath:app,keychainProfile:process.env.DEEPVIEWER_NOTARY_PROFILE});const {verifyMacOSNotarizedApplication}=await import(join(upstream,'apps/desktop/scripts/verify-macos-signature.mjs'));verifyMacOSNotarizedApplication(app,identity)}:undefined,
diff --git a/apps/deepviewer-desktop/scripts/package.mjs b/apps/deepviewer-desktop/scripts/package.mjs
index 198400d..4d62a0d 100644
--- a/apps/deepviewer-desktop/scripts/package.mjs
+++ b/apps/deepviewer-desktop/scripts/package.mjs
@@ -182,7 +182,8 @@ for (const arch of architectures) {
appVersion,
buildVersion: appBuildVersion,
asar: true,
- extraResource: [runtimeRoot, nativeIcon.dockThemes],
+ extraResource: [runtimeRoot, nativeIcon.catalog],
+ extendInfo: { CFBundleIconName: nativeIcon.name },
afterCopyExtraResources: [async ({ buildPath }) => {
const temporaryAppPath = resolve(buildPath, `${packagedName}.app`)
const copiedRuntimeRoot = resolve(temporaryAppPath, 'Contents', 'Resources', 'harness')
diff --git a/apps/deepviewer-desktop/scripts/prepare-native-runtime.mjs b/apps/deepviewer-desktop/scripts/prepare-native-runtime.mjs
index 9e739ba..9d39da1 100644
--- a/apps/deepviewer-desktop/scripts/prepare-native-runtime.mjs
+++ b/apps/deepviewer-desktop/scripts/prepare-native-runtime.mjs
@@ -42,8 +42,8 @@ export async function prepareNativeRuntime(outputStage = join(appRoot, '.desktop
const pnpmSource = join(root,'node_modules/pnpm')
cpSync(pnpmSource,join(stage,'resources/runtime/pnpm'),{recursive:true,filter:path=>pnpmPayload(relative(pnpmSource,path).split('\\').join('/'))})
cpSync(join(upstream,'apps/desktop/scripts/node-bin'),join(stage,'resources/runtime/bin'),{recursive:true})
- cpSync(join(appRoot,'.desktop/native-icon/DeepViewerDockThemes'),join(stage,'resources/DeepViewerDockThemes'),{recursive:true})
- cpSync(join(root,'apps/deepviewer-adapter/assets/icon-light.png'),join(stage,'resources/icon.png'))
+ cpSync(join(appRoot,'.desktop/native-icon/Assets.car'),join(stage,'resources/Assets.car'))
+ cpSync(join(appRoot,'.desktop/native-icon/icon.png'),join(stage,'resources/icon.png'))
const buildMetadata=sanitizeNativeBuildMetadata(stage,{projectRoot:root})
// Seal the exact bytes electron-builder will retain in dependency manifests.
const packageMetadata=await normalizeNativePackageMetadata(shell)
diff --git a/apps/deepviewer-desktop/scripts/smoke-native-ui-regression.mjs b/apps/deepviewer-desktop/scripts/smoke-native-ui-regression.mjs
index d294a95..2f3d531 100644
--- a/apps/deepviewer-desktop/scripts/smoke-native-ui-regression.mjs
+++ b/apps/deepviewer-desktop/scripts/smoke-native-ui-regression.mjs
@@ -18,6 +18,7 @@ try {
// Approved DeepViewer hero copy; every chrome, composer and navigation assertion remains.
source = replace(source, 'Into the Unknown', 'What shall we build?', 1)
source = replace(source, '探索未至之境', '让我们做点什么', 3)
+ source += '\n' + readFileSync(resolve(root, 'apps/deepviewer-adapter/tests/idle-presentation.spec.tsx.txt'), 'utf8')
writeFileSync(skeleton, source); generated.push(skeleton)
const chatSource = resolve(upstream, 'packages/client/ui-chat/tests/chat-view.client.spec.tsx')
const chat = resolve(dirname(chatSource), 'deepviewer-native-regression-chat.client.spec.tsx')
diff --git a/apps/deepviewer-desktop/src/main/dock-theme.ts b/apps/deepviewer-desktop/src/main/dock-theme.ts
deleted file mode 100644
index f6a5c82..0000000
--- a/apps/deepviewer-desktop/src/main/dock-theme.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-// Compatibility export for existing callers; DeepViewer policy is owned by the adapter.
-export * from "../../../deepviewer-adapter/desktop/profile/dock-theme.js"
diff --git a/apps/deepviewer-desktop/test/app-identity.test.ts b/apps/deepviewer-desktop/test/app-identity.test.ts
index ffdfc4d..c9b4f33 100644
--- a/apps/deepviewer-desktop/test/app-identity.test.ts
+++ b/apps/deepviewer-desktop/test/app-identity.test.ts
@@ -17,21 +17,18 @@ describe('DeepViewer app identity', () => {
expect(setTitle).toHaveBeenCalledWith(DEEPVIEWER_APP_NAME)
})
- it('ships the original ICNS and stages product Dock resources for the native shell', () => {
+ it('declares the compiled native catalog and its legacy ICNS for every bundle', () => {
const packageScript = readFileSync(new URL('../scripts/package-native.mjs', import.meta.url), 'utf8')
const staging = readFileSync(new URL('../scripts/prepare-native-runtime.mjs', import.meta.url), 'utf8')
const bootstrap = readFileSync(new URL('../../deepviewer-adapter/desktop/bootstrap.ts', import.meta.url), 'utf8')
expect(packageScript).toContain("icon:join(appRoot,'.desktop/native-icon/DeepViewer.icns')")
- expect(packageScript).not.toContain('Assets.car')
- expect(packageScript).not.toContain('CFBundleIconName')
- expect(staging).toContain("join(stage,'resources/DeepViewerDockThemes')")
- expect(bootstrap).toContain('followDockTheme({')
- expect(bootstrap).toContain("join(appRoot, '.desktop', 'native-icon')")
- const icns = readFileSync(new URL('../assets/DeepViewer.icns', import.meta.url))
- expect(icns.subarray(0, 4).toString()).toBe('icns')
- expect(createHash('sha256').update(icns).digest('hex'))
- .toBe('e70e7aae72a23e71621d8c31bea14db18a21408ae1b811ebdd803e01a6fc8f5b')
+ expect(packageScript).toContain("CFBundleIconName:'DeepViewer'")
+ expect(staging).toContain("join(stage,'resources/Assets.car')")
+ expect(bootstrap).not.toMatch(/\.setIcon\s*\(/u)
+ const development = readFileSync(new URL('../scripts/development-shell.mjs', import.meta.url), 'utf8')
+ expect(development).toContain("join(resources, 'Assets.car')")
+ expect(development).toContain('CFBundleIconName: icon.name')
})
it('preserves the original PNG artwork for in-app surfaces', () => {
diff --git a/apps/deepviewer-desktop/test/development-workflow.test.ts b/apps/deepviewer-desktop/test/development-workflow.test.ts
index 8a8b14b..657f061 100644
--- a/apps/deepviewer-desktop/test/development-workflow.test.ts
+++ b/apps/deepviewer-desktop/test/development-workflow.test.ts
@@ -90,7 +90,7 @@ describe('DeepViewer development workflow (DV-0038)', () => {
})
it('pins DeepViewer identity and RC2 without reviving retired preview plugins', () => {
- expect(appManifest.version).toBe('0.5.0')
+ expect(appManifest.version).toBe('0.5.1')
expect(Number.isSafeInteger(appManifest.buildNumber)).toBe(true)
expect(appManifest.buildNumber).toBeGreaterThan(0)
expect(appManifest.productName).toBe('DeepViewer')
diff --git a/apps/deepviewer-desktop/test/dock-theme.test.ts b/apps/deepviewer-desktop/test/dock-theme.test.ts
index c1c86ba..26e23d0 100644
--- a/apps/deepviewer-desktop/test/dock-theme.test.ts
+++ b/apps/deepviewer-desktop/test/dock-theme.test.ts
@@ -1,96 +1,120 @@
-import { EventEmitter } from 'node:events'
-import { access, copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
+import { execFileSync } from 'node:child_process'
+import { createHash } from 'node:crypto'
+import { access, cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
-import { afterEach, describe, expect, it, vi } from 'vitest'
-import { followDockTheme } from '../src/main/dock-theme.js'
+import { inflateSync } from 'node:zlib'
+import { afterEach, describe, expect, it } from 'vitest'
// @ts-expect-error The native icon preparation helper is JavaScript.
const { compileNativeAppIcon } = await import('../scripts/native-app-icon.mjs')
const fixtures: string[] = []
+const source = resolve(import.meta.dirname, '../assets/DeepViewer.icon')
afterEach(async () => {
for (const path of fixtures.splice(0)) await rm(path, { recursive: true, force: true })
})
-describe('original DeepViewer icon staging', () => {
- it('preserves the original ICNS and PNG for both Dock themes and replaces stale layered assets', async () => {
- const appRoot = await mkdtemp(join(tmpdir(), 'deepviewer-icons-'))
- fixtures.push(appRoot)
- const assets = join(appRoot, 'assets')
- await mkdir(assets)
- const source = resolve(import.meta.dirname, '../assets')
- await copyFile(join(source, 'DeepViewer.icns'), join(assets, 'DeepViewer.icns'))
- await copyFile(join(source, 'deepviewer-icon-macos26-1024.png'), join(assets, 'deepviewer-icon-macos26-1024.png'))
- const output = join(appRoot, '.desktop', 'native-icon')
- await mkdir(join(output, 'DeepViewer.icon'), { recursive: true })
- await writeFile(join(output, 'Assets.car'), 'stale Scoder icon')
-
- const icon = await compileNativeAppIcon(appRoot)
- expect(await readFile(icon.icns)).toEqual(await readFile(join(assets, 'DeepViewer.icns')))
- for (const appearance of ['light', 'dark']) {
- expect(await readFile(join(icon.dockThemes, `${appearance}.png`)))
- .toEqual(await readFile(join(assets, 'deepviewer-icon-macos26-1024.png')))
+// Decode the actual compiler-provided PNG alpha; no rendering or image edits.
+function opaqueBounds(png: Buffer): [number, number, number, number] {
+ const width = png.readUInt32BE(16), height = png.readUInt32BE(20)
+ expect([png[24], png[25], png[28]]).toEqual([8, 6, 0]) // RGBA8, noninterlaced.
+ const chunks: Buffer[] = []
+ for (let offset = 8; offset < png.length;) {
+ const length = png.readUInt32BE(offset), type = png.toString('ascii', offset + 4, offset + 8)
+ if (type === 'IDAT') chunks.push(png.subarray(offset + 8, offset + 8 + length))
+ offset += length + 12
+ }
+ const data = inflateSync(Buffer.concat(chunks)), stride = width * 4
+ const pixels = Buffer.alloc(stride * height)
+ const paeth = (a: number, b: number, c: number) => {
+ const p = a + b - c, da = Math.abs(p - a), db = Math.abs(p - b), dc = Math.abs(p - c)
+ return da <= db && da <= dc ? a : db <= dc ? b : c
+ }
+ const bounds: [number, number, number, number] = [width, height, -1, -1]
+ for (let y = 0; y < height; y++) {
+ const filter = data.readUInt8(y * (stride + 1))
+ for (let x = 0; x < stride; x++) {
+ const offset = y * stride + x, left = x >= 4 ? pixels.readUInt8(offset - 4) : 0
+ const up = y ? pixels.readUInt8(offset - stride) : 0, upperLeft = y && x >= 4 ? pixels.readUInt8(offset - stride - 4) : 0
+ const correction = [0, left, up, Math.floor((left + up) / 2), paeth(left, up, upperLeft)][filter]
+ if (correction === undefined) throw new Error('Unsupported PNG row filter')
+ pixels[offset] = (data.readUInt8(y * (stride + 1) + x + 1) + correction) & 255
+ if (x % 4 === 3 && pixels.readUInt8(offset) > 127) {
+ const pixelX = Math.floor(x / 4)
+ bounds[0] = Math.min(bounds[0], pixelX); bounds[1] = Math.min(bounds[1], y)
+ bounds[2] = Math.max(bounds[2], pixelX); bounds[3] = Math.max(bounds[3], y)
+ }
}
- await expect(access(join(output, 'Assets.car'))).rejects.toThrow()
- await expect(access(join(output, 'DeepViewer.icon'))).rejects.toThrow()
- expect(await compileNativeAppIcon(appRoot)).toEqual(icon)
- await rm(join(icon.dockThemes, 'dark.png'))
- await compileNativeAppIcon(appRoot)
- expect(await readFile(join(icon.dockThemes, 'dark.png')))
- .toEqual(await readFile(join(assets, 'deepviewer-icon-macos26-1024.png')))
- })
-})
-
-class Theme extends EventEmitter {
- shouldUseDarkColors = false
- resolve(dark: boolean) {
- this.shouldUseDarkColors = dark
- this.emit('updated')
}
+ return bounds
}
-function setup() {
- const theme = new Theme()
- const options = {
- theme,
- resourcesPath: '/App/Contents/Resources',
- loadImage: vi.fn((path: string) => ({ path, isEmpty: (): boolean => false })),
- setIcon: vi.fn(), log: vi.fn(), onError: vi.fn(),
+function icnsEntries(buffer: Buffer): Map {
+ expect(buffer.subarray(0, 4).toString()).toBe('icns')
+ const entries = new Map()
+ for (let offset = 8; offset < buffer.length;) {
+ const type = buffer.toString('ascii', offset, offset + 4), length = buffer.readUInt32BE(offset + 4)
+ if (length < 8 || offset + length > buffer.length) throw new Error('Malformed ICNS entry')
+ entries.set(type, buffer.subarray(offset + 8, offset + length)); offset += length
}
- return { options, theme }
+ return entries
}
-describe('Dock theme synchronization', () => {
- it('applies the resolved app theme and follows later light/dark/system changes', () => {
- const { options, theme } = setup()
- const stop = followDockTheme(options)
- expect(options.setIcon.mock.lastCall?.[0].path).toBe('/App/Contents/Resources/DeepViewerDockThemes/light.png')
- theme.resolve(true)
- expect(options.setIcon.mock.lastCall?.[0].path).toContain('/dark.png')
- theme.resolve(true) // Changing source without changing resolved appearance.
- expect(options.setIcon).toHaveBeenCalledTimes(2)
- theme.resolve(false) // System resolves to light.
- expect(options.setIcon.mock.lastCall?.[0].path).toContain('/light.png')
- expect(options.loadImage).toHaveBeenCalledTimes(2)
- stop()
- theme.resolve(true)
- expect(options.setIcon).toHaveBeenCalledTimes(3)
- })
+let compilerAvailable = false
+if (process.platform === 'darwin') {
+ try {
+ compilerAvailable = Number(execFileSync('/usr/bin/xcodebuild', ['-version'], { encoding: 'utf8' }).match(/Xcode (\d+)/u)?.[1]) >= 26
+ } catch { /* Core-only CI may run without the native icon toolchain. */ }
+}
- it('starts with the current dark appearance', () => {
- const { options, theme } = setup()
- theme.shouldUseDarkColors = true
- followDockTheme(options)
- expect(options.setIcon.mock.lastCall?.[0].path).toContain('/dark.png')
+describe('native DeepViewer app icon', () => {
+ it('preserves the released whale vector byte-for-byte as an unmasked foreground', async () => {
+ const whale = await readFile(join(source, 'Assets/Whale.svg'))
+ expect(whale).toEqual(await readFile(new URL('../../deepviewer-adapter/assets/deepviewer-loading-logo.svg', import.meta.url)))
+ expect(createHash('sha256').update(whale).digest('hex'))
+ .toBe('16896da065025e41040115cc8f16f0268a94200bd5ea3c939e27e3078dff1816')
+ const document = JSON.parse(await readFile(join(source, 'icon.json'), 'utf8'))
+ expect(document.groups.flatMap((group: { layers: Array<{ 'image-name': string }> }) => group.layers.map(layer => layer['image-name'])))
+ .toEqual(['Whale.svg'])
})
- it('leaves the bundle icon intact when a rendition is unavailable, and can retry', () => {
- const { options, theme } = setup()
- options.loadImage.mockReturnValueOnce({ path: '', isEmpty: () => true })
- followDockTheme(options)
- expect(options.setIcon).not.toHaveBeenCalled()
- expect(options.onError).toHaveBeenCalledOnce()
- theme.resolve(false)
- expect(options.setIcon).toHaveBeenCalledOnce()
+ it('leaves the native bundle icon authoritative through appearance changes', async () => {
+ const bootstrap = await readFile(new URL('../../deepviewer-adapter/desktop/bootstrap.ts', import.meta.url), 'utf8')
+ expect(bootstrap).not.toMatch(/\.setIcon\s*\(/u)
+ expect(bootstrap).not.toContain('followDockTheme')
})
+
+ it.skipIf(!compilerAvailable)('compiles native appearances and a correctly padded, complete legacy ICNS, repairing partial output', async () => {
+ const appRoot = await mkdtemp(join(tmpdir(), 'deepviewer-native-icon-'))
+ fixtures.push(appRoot)
+ await mkdir(join(appRoot, 'assets'))
+ await cp(source, join(appRoot, 'assets/DeepViewer.icon'), { recursive: true })
+ const output = join(appRoot, '.desktop/native-icon')
+ await mkdir(join(output, 'DeepViewerDockThemes'), { recursive: true })
+ await writeFile(join(output, 'DeepViewerDockThemes/light.png'), 'obsolete full-bleed override')
+ const result = await compileNativeAppIcon(appRoot)
+ await expect(access(join(output, 'DeepViewerDockThemes'))).rejects.toThrow()
+ const entries = icnsEntries(await readFile(result.icns))
+ for (const size of ['ic04', 'ic05', 'ic07', 'ic08', 'ic09', 'ic10', 'ic11', 'ic12', 'ic13', 'ic14']) {
+ expect(entries.has(size), `missing complete native legacy rendition ${size}`).toBe(true)
+ }
+ const png = entries.get('ic10')!
+ expect(await readFile(result.about)).toEqual(png)
+ expect([png.readUInt32BE(16), png.readUInt32BE(20)]).toEqual([1024, 1024])
+ const [left, top, right, bottom] = opaqueBounds(png)
+ // The regression filled every edge. Apple's legacy mask owns these margins.
+ expect(left).toBeGreaterThanOrEqual(80); expect(top).toBeGreaterThanOrEqual(80)
+ expect(right).toBeLessThan(944); expect(bottom).toBeLessThan(944)
+ expect(right - left).toBeGreaterThan(780); expect(bottom - top).toBeGreaterThan(780)
+ const info = JSON.parse(execFileSync('/usr/bin/plutil', ['-convert', 'json', '-o', '-', result.info], { encoding: 'utf8' }))
+ expect(info).toEqual({ CFBundleIconFile: 'DeepViewer', CFBundleIconName: 'DeepViewer' })
+ const catalog = JSON.parse(execFileSync('/usr/bin/assetutil', ['--info', result.catalog], { encoding: 'utf8' }))
+ const appearances = catalog.filter((item: { AssetType: string; Name: string }) => item.AssetType === 'IconImageStack' && item.Name === 'DeepViewer')
+ .map((item: { Appearance: string }) => item.Appearance)
+ expect(appearances).toEqual(expect.arrayContaining(['NSAppearanceNameAqua', 'NSAppearanceNameDarkAqua', 'ISAppearanceTintable']))
+ expect(await compileNativeAppIcon(appRoot)).toEqual(result)
+ await rm(result.catalog)
+ expect(await compileNativeAppIcon(appRoot)).toEqual(result)
+ expect((await readFile(result.catalog)).length).toBeGreaterThan(0)
+ }, 30_000)
})
diff --git a/apps/deepviewer-desktop/test/fixtures/idle-v0.3.3.css b/apps/deepviewer-desktop/test/fixtures/idle-v0.3.3.css
new file mode 100644
index 0000000..d64ad51
--- /dev/null
+++ b/apps/deepviewer-desktop/test/fixtures/idle-v0.3.3.css
@@ -0,0 +1,69 @@
+/* The welcome occupies the space above the bottom composer. Its top padding
+ equals the workspace-row prelude inside the composer, so the visible module
+ centers between the window top and the input card top rather than the
+ composer seat's outer edge. */
+.root {
+ position: relative;
+ inset: auto;
+ flex: 1 1 auto;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ height: auto;
+ min-width: 0;
+ min-height: 0;
+ padding: var(--deepviewer-hero-input-prelude-height) 24px 0;
+ pointer-events: none;
+}
+
+.stack {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 40px;
+ width: 100%;
+}
+
+.logo {
+ display: block;
+ width: auto;
+ height: 48px;
+ flex: none;
+ overflow: visible;
+ color: var(--dsw-alias-label-primary);
+ opacity: 0.5;
+}
+
+.logoCursor {
+ animation: deepviewer-hero-cursor-blink 1s steps(1, jump-end) infinite;
+}
+
+.prompt {
+ margin: 0;
+ color: var(--dsw-alias-label-primary);
+ font-family: var(--dsw-font-family);
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 32px;
+ opacity: 0.5;
+ text-align: center;
+}
+
+@keyframes deepviewer-hero-cursor-blink {
+ 0%,
+ 49% {
+ opacity: 1;
+ }
+
+ 50%,
+ 100% {
+ opacity: 0;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .logoCursor {
+ animation: none;
+ }
+}
diff --git a/apps/deepviewer-desktop/test/native-idle-brand.test.ts b/apps/deepviewer-desktop/test/native-idle-brand.test.ts
new file mode 100644
index 0000000..90ba0d1
--- /dev/null
+++ b/apps/deepviewer-desktop/test/native-idle-brand.test.ts
@@ -0,0 +1,59 @@
+import { readFileSync } from 'node:fs'
+import { resolve } from 'node:path'
+import { describe, expect, it } from 'vitest'
+
+// v0.3.3 / Build 75, commit 58f5a8d43649ea1d9b6cc800fbfc39955c835eb1.
+// The CSS fixture is the verbatim released HideEmptyHeroBranding.module.css.
+// Its SVG source remains in the retained DSH 0.1.5 override; no tag lookup is
+// needed in CI's shallow checkout.
+const read = (path: string) => readFileSync(resolve(import.meta.dirname, path), 'utf8')
+const releasedCss = read('fixtures/idle-v0.3.3.css')
+const releasedMark = read('../upstream-overrides/dsh-015/EmptyHero.tsx')
+const currentCss = read('../../deepviewer-adapter/client/welcome.module.css')
+const currentMark = read('../../deepviewer-adapter/client/DeepViewerMascot.tsx')
+
+function declarations(source: string, selector: string) {
+ const body = source.slice(source.indexOf(selector) + selector.length).match(/^\s*\{([^}]+)\}/u)?.[1]
+ if (!body) throw new Error(`Missing released-style selector: ${selector}`)
+ return Object.fromEntries(body.split(';').filter(value => value.includes(':')).map(value => {
+ const at = value.indexOf(':')
+ return [value.slice(0, at).trim(), value.slice(at + 1).trim()]
+ }))
+}
+
+describe('released DeepViewer idle presentation', () => {
+ it('preserves both released SVG paths and its aspect ratio, with a separate cursor', () => {
+ const paths = (source: string) => [...source.matchAll(/\bd="([^"]+)"/gu)].map(match => match[1])
+ expect(paths(currentMark)).toEqual(paths(releasedMark))
+ expect(paths(currentMark)).toHaveLength(2)
+ expect(currentMark).toContain('viewBox="0 0 150.374 160"')
+ expect(currentMark).toContain('data-deepviewer-brand-cursor')
+ expect(currentMark).toContain('aria-hidden="true"')
+ expect(currentMark).toContain('focusable="false"')
+ })
+
+ it('matches the released size, ink, title typography and spacing', () => {
+ const logo = declarations(currentCss, '.brandLogo'), oldLogo = declarations(releasedCss, '.logo')
+ for (const property of ['display', 'width', 'height', 'flex', 'overflow', 'color', 'opacity']) expect(logo[property], property).toBe(oldLogo[property])
+ const title = declarations(currentCss, ':global([data-deepviewer-hero-title])'), oldTitle = declarations(releasedCss, '.prompt')
+ for (const property of ['color', 'font-family', 'font-size', 'font-weight', 'line-height', 'opacity', 'text-align']) expect(title[property], property).toBe(oldTitle[property])
+ expect(declarations(currentCss, ':global([data-deepviewer-headline])').gap).toBe(declarations(releasedCss, '.stack').gap)
+ expect(logo.mask).toBeUndefined()
+ })
+
+ it('retains the released cursor cadence and respects reduced motion', () => {
+ expect(declarations(currentCss, '.brandCursor').animation).toBe(declarations(releasedCss, '.logoCursor').animation)
+ const frames = (source: string) => source.slice(source.indexOf('@keyframes'), source.indexOf('@media')).replace(/\s/gu, '')
+ expect(frames(currentCss)).toBe(frames(releasedCss))
+ const reduced = currentCss.slice(currentCss.indexOf('@media (prefers-reduced-motion: reduce)'))
+ expect(declarations(reduced, '.brandCursor').animation).toBe(declarations(releasedCss.slice(releasedCss.indexOf('@media')), '.logoCursor').animation)
+ })
+
+ it('keeps the released flex geometry above the resident bottom composer', () => {
+ const hero = declarations(currentCss, ':global([data-deepviewer-hero])'), oldHero = declarations(releasedCss, '.root')
+ for (const property of ['position', 'inset', 'flex', 'display', 'align-items', 'justify-content', 'box-sizing', 'height', 'min-width', 'min-height', 'pointer-events']) expect(hero[property], property).toBe(oldHero[property])
+ expect(hero.padding).toBe('40px 24px 0')
+ expect(declarations(currentCss, ":global(body [data-conversation-content][data-content-phase='hero'] > [data-conversation-scroll])")['justify-content']).toBe('flex-start')
+ expect(declarations(currentCss, ":global([data-conversation-content][data-content-phase='hero'] > [data-conversation-scroll] > [data-deepviewer-composer-seat])")['margin-top']).toBe('auto')
+ })
+})
diff --git a/docs/sdd/releases/README.md b/docs/sdd/releases/README.md
index 5383c4b..80c25b3 100644
--- a/docs/sdd/releases/README.md
+++ b/docs/sdd/releases/README.md
@@ -8,6 +8,7 @@
| 版本 | 类型 | 发布日期 | 关联规格 | 记录 |
| --- | --- | --- | --- | --- |
+| `0.5.1` | Preparing / macOS ARM64 installer candidate | 未发布 | DV-0034(Implementing;Pending Manual) | [DeepViewer 0.5.1 候选](v0.5.1.md) |
| `v0.5.0` | Public signed installer / Latest / ARM64 | 2026-10-03 | DV-0033(Implementing;Pending Manual 保留) | [DeepViewer 0.5.0](v0.5.0.md) |
| `v0.3.3` | Public release / ARM64 | 2026-09-23 | DV-0031, DV-0032 | [DeepViewer 0.3.3](v0.3.3.md) |
| `v0.2.5-preview.1` | Release / Latest / ARM64 only | 2026-09-15 | DV-0017 | [DeepViewer 0.2.5](v0.2.5-preview.1.md) |
diff --git a/docs/sdd/releases/v0.5.1.md b/docs/sdd/releases/v0.5.1.md
new file mode 100644
index 0000000..2d15f08
--- /dev/null
+++ b/docs/sdd/releases/v0.5.1.md
@@ -0,0 +1,52 @@
+---
+version: 0.5.1
+build: null
+tag: null
+release_type: Preparing / macOS arm64 signed installer
+status: Preparing
+date: 2026-10-04
+published_at: null
+source_ref: null
+build_source_commit: null
+source_commit: null
+merge_commit: null
+release_id: null
+github_release: null
+specs: [DV-0034]
+---
+
+# DeepViewer 0.5.1 候选
+
+维护者于 2026-10-04 明确要求修复新会话待机动画与 macOS Dock 图标适配,随后构建 0.5.1 arm64 安装包、公开发布到 Duoasa/DeepViewer 并合并 main。此记录目前仅为发布准备,未生成或公开本版本安装资产;最终 build、源提交、tag、Release URL 和发布时间均待实际证据确认。现有 [v0.5.0](v0.5.0.md) 发布及其资产保持历史记录。
+
+## 授权范围与来源
+
+- 按 [DV-0034](../specs/DV-0034-brand-visual-051/spec.md) 恢复 v0.3.3 / Build 75 新会话待机动画的尺寸、样式、光标闪烁与布局,保留当前内核的 composer、会话状态与 reduced-motion 行为。
+- 从源资源、ICNS 原生表示、bundle 声明与运行时 Dock 选择链路修复图标过大,不简单缩小整张旧图。
+- 继续使用 DSH `0.2.0-rc.2` / `639ed015397290b3745d163aafe02ffee4aa3f84`,不升级内核或修改 agent loop;保留 DV-0033 的 HMR 修复、产品数据隔离与内置模块。
+- 产品清单版本为 0.5.1,previewRelease 为 0.5.1-preview.1;既有 build 90 保留为递增输入,最终 build 由全新正式构建确认。
+
+## 发布门禁与待办证据
+
+所有正式资产必须从本版本固定源码与锁文件重新生成 Runtime、staging、app、DMG 和 SHA-256 清单,不复用 v0.5.0 包体。签名、公证、严格 ASAR 库存、净化审计、安装包完整性及远端 digest 需绑定最终实际资产。授权与计划不构成通过证据。
+
+| 项目 | 当前状态 |
+| --- | --- |
+| 新会话动画恢复、图标根因与源资源检查 | Pending:见 DV-0034 实施与验证记录 |
+| 固定依赖、typecheck、相关 smoke 与全新 production build | Pending |
+| 最终 build 与固定构建源提交 | Pending |
+| 严格 ASAR 库存、最终包体与资源净化审计 | Pending |
+| runtime / app / DMG Developer ID 签名 | Pending |
+| app / DMG 公证 submission 与 Accepted、stapling / Gatekeeper | Pending |
+| 最终实际 payload 隔离 HOME / XDG / TMPDIR smoke | Pending |
+| DMG 完整性、只读挂载与内部 app 版本 / 架构检查 | Pending |
+| 新生成 SHA256SUMS.txt、本地 SHA-256 / 字节数 | Pending |
+| Git tag、GitHub Release、PR / main 合并与 CI | Pending |
+| 公开资产远端 digest / 大小对照、公开下载链接 | Pending |
+| 新会话与实际 Dock 视觉验收 | Pending Manual:由维护者完成 |
+
+## 资产边界与已知限制
+
+计划公开的资产仅为 `DeepViewer-0.5.1-arm64.dmg` 与本版本新生成的 `SHA256SUMS.txt`,目前均未发布。ZIP、blockmap 与自动更新 feed 不属于本次公开范围;实际签名安装、数据保留及重启更新 qualification 继续适用。
+
+本次不更改真实用户 HOME 或字体权限。v0.5.0 在隔离 HOME 的 Office 转换 smoke 通过,但真实用户环境下受保护 Office 字体目录扫描仍需人工验收;该边界不因升级版本或发布授权而自动消除。规格保持 Implementing,任何视觉或真实用户环境的 Pending Manual 不提升为 Pass。
diff --git a/docs/sdd/specs/DV-0034-brand-visual-051/design.md b/docs/sdd/specs/DV-0034-brand-visual-051/design.md
new file mode 100644
index 0000000..c25f4b9
--- /dev/null
+++ b/docs/sdd/specs/DV-0034-brand-visual-051/design.md
@@ -0,0 +1,17 @@
+# DV-0034 Design
+
+Status: Implementing
+
+## D-001:恢复旧发行版品牌表现 (R-001, NFR-001)
+
+以 Git v0.3.3 的 EmptyHero 与品牌 CSS 为事实来源,在新内核现有 conversation hero brand mark 槽恢复双 path SVG,使鱼标与 cursor 独立呈现。恢复尺寸、透明度、间距、字体与 reduced-motion。使用局部 ConversationContent 呈现 seam 将 hero 与 composerStack 分开,恢复旧版窗口顶部至底部输入区之间的居中待机位置;保留新内核的 resident composer、slot、overlay、inert、交互与会话状态。与应用启动 loading 页分开定位。
+
+## D-002:原生图标资源与选择链路 (R-002, NFR-001)
+
+检查源 PNG/ICNS、透明画布、各尺寸 renditions、bundle icon 声明及运行时 Dock override。使用原生图标表示与构建工具修复根因,保留原始鱼标设计;不把当前已含底板的整图简单缩小,不通过窗口/CSS 或运行时缩放补偿。具体根因、工具与实际资源证据在本次实现与发布同步边界补齐。
+
+已确认 0.5.0 的源 PNG 与最大 ICNS rendition 的底板占满 1024 画布;运行时在所有 macOS 渠道调用 dock.setIcon,覆盖了原生 bundle 图标。修复采用未变更几何的鱼标 SVG 作为 Icon Composer 前景,由 Xcode 26 的 Apple actool 编译分层 Assets.car 与 legacy ICNS;透明边界、底板及各尺寸由编译器生成,不缩放旧整图。稳定包与开发载体声明同名 CFBundleIconName 并携带 catalog,不再用运行时 PNG 覆盖。实际 release packaging 必须成功编译原生资源;CI 的 core-only 构建不要求调用 Xcode 图标编译器,编译器集成验证与其基础源码检查分别记录。
+
+## D-003:全新发布与验证边界 (R-003, NFR-001, NFR-002)
+
+复用 0.5.0 已验证的固定依赖、严格 ASAR 库存、精确 Developer ID 证书、公证与净化审计链路,全新构建 0.5.1 arm64。只公开 DMG 与新生成 SHA256SUMS.txt;ZIP 与自动更新 feed 无实际安装升级 qualification 时不公开。实际用户环境、界面与 Dock 验收保留 Pending Manual;版本与资产权威证据记录在 releases/v0.5.1.md。
diff --git a/docs/sdd/specs/DV-0034-brand-visual-051/spec.md b/docs/sdd/specs/DV-0034-brand-visual-051/spec.md
new file mode 100644
index 0000000..3c6e840
--- /dev/null
+++ b/docs/sdd/specs/DV-0034-brand-visual-051/spec.md
@@ -0,0 +1,30 @@
+# DV-0034 — 新会话品牌动画与 macOS 图标修复、0.5.1 ARM64 发布
+
+Status: Implementing
+Owner: Duoasa
+Date: 2026-10-04
+Approval: 维护者于 2026-10-04 直接要求恢复之前发行版的新会话待机动画(大小、动画、样式),从资源与原生图标链路解决 Dock 图标过大问题,明确拒绝简单缩放;完成后构建 0.5.1 arm64 安装包、推送 Duoasa/DeepViewer 并合并 main。该明确目标指令作为本规格范围认可。
+
+## 范围与基线
+
+当前公开基线为 v0.5.0 / Build 90;新会话视觉以 v0.3.3 / Build 75 的版本化源码为恢复依据。保持 DV-0033 的 DSH 0.2.0-rc.2 固定提交、原生 Conversation/composer、HMR 修复、数据目录及内置模块,不回放旧内核整文件覆盖,不操作真实用户数据或字体权限。
+
+## Requirements
+
+- R-001: 新会话恢复 v0.3.3 鱼标的尺寸、透明度、光标闪烁、间距与文本样式,以及待机区在窗口顶部与底部输入区之间的居中位置;遵循原有 reduced-motion 行为,通过现有品牌槽和局部呈现适配新内核。
+- R-002: 排查图标的源资源、画布与透明边界、ICNS 各尺寸、原生 bundle 与运行时 Dock 图标选择链路;修复不正确的图标表示或应用方式,保留原鱼标设计,不以简单缩小整张旧图、CSS scale 或重复套底板消除表象。
+- R-003: 版本号为 0.5.1,build 延续既有单调序列;从固定源码和依赖全新生成 arm64 Runtime、staging、app、DMG 与 SHA-256 清单,经签名、公证、最终净化审计后发布到 Duoasa/DeepViewer,推送并合并 main。
+- NFR-001: 不改变上游 agent loop、许可证、账户及持久化数据;代码与公开包不包含凭据、个人路径或用户内容。
+- NFR-002: 代理完成代码、源码/资源及打包基础验证。实际界面与 Dock 视觉验收继续由维护者完成;Pending Manual 不因授权发布自动成为 Pass。自动更新 feed 仍遵循既有实际安装/更新门禁。
+
+## Acceptance
+
+- AC-001: 恢复的 SVG 几何与旧源码一致;48px 高、50% opacity、1s steps cursor 闪烁、40px 图文间距和 24px/32px 文本规则有源码证据;待机区独立于底部 composer 居中,reduced-motion 禁用动画;既有新会话 composer 回归通过。
+- AC-002: 图标根因与修复链路被记录,构建输出中的原生 bundle/Dock 图标表示及选择符合相同原生画布约定;没有对旧整张图做简单缩放或运行时尺寸补丁。
+- AC-003: typecheck、相关 smoke、全新 production build、实际包资源检查通过。
+- AC-004: 维护者确认新会话视觉及实际 Dock 与邻近 macOS 图标协调(Pending Manual)。
+- AC-005: 0.5.1 arm64 的最终来源/build、签名公证、包体审计、DMG 完整性、SHA-256、公开资产 digest、源码推送及 main 合并均有真实证据。
+
+## 回滚
+
+代码与品牌资源可恢复 v0.5.0;版本化修复不写用户存储。已发布 v0.5.0 资产保留,不覆盖旧 Release。
diff --git a/docs/sdd/specs/DV-0034-brand-visual-051/tasks.md b/docs/sdd/specs/DV-0034-brand-visual-051/tasks.md
new file mode 100644
index 0000000..e443feb
--- /dev/null
+++ b/docs/sdd/specs/DV-0034-brand-visual-051/tasks.md
@@ -0,0 +1,9 @@
+# DV-0034 Tasks
+
+Status: Implementing
+
+- [x] T-001 (R-001, AC-001; D-001): 恢复 v0.3.3 的 SVG、动画与样式;4 项旧版源码基线检查、175 项 fresh-source 契约/两次幂等及 178 项原生 DOM 回归通过,包含 resident composer/draft 连续性。
+- [x] T-002 (R-002, AC-002; D-002): 保留鱼标 SVG,Apple actool 编译分层 catalog 与完整 ICNS、删除运行时 Dock override;本机图标/compiler 与 identity 6 项通过。最终包中的实际资源仍由 T-003 核验。
+- [ ] T-003 (R-003, AC-003, AC-005; D-003): 更新 0.5.1 版本,全新 arm64 构建,验证最终包、签名公证与 SHA-256。
+- [ ] T-004 (R-003, AC-005; D-003): 推送源码、CI、GitHub 公开 Release/远端 digest 核验与 main 合并,同步发布文档。
+- [ ] T-005 (AC-004, NFR-002): 维护者实际新会话与 Dock 视觉验收(Pending Manual)。
diff --git a/docs/sdd/specs/DV-0034-brand-visual-051/verification.md b/docs/sdd/specs/DV-0034-brand-visual-051/verification.md
new file mode 100644
index 0000000..f4bc623
--- /dev/null
+++ b/docs/sdd/specs/DV-0034-brand-visual-051/verification.md
@@ -0,0 +1,34 @@
+# DV-0034 Verification
+
+Status: Implementing
+Date: 2026-10-04
+
+| AC | 可复现证据 | 状态 |
+| --- | --- | --- |
+| AC-001 | 4 项旧版 SVG/CSS 基线检查通过;175 项 fresh pinned source 契约两次 apply 幂等,agent loop 未变;178 项 jsdom 原生 UI 回归通过,包含 sessionless/blank hero 与 composer 分离、首次消息后的输入 DOM/draft 保留 | Pass (源码与 DOM) |
+| AC-002 | 本机 Apple actool 实际编译通过;SVG SHA-256 保持原值,catalog 包含 Aqua/DarkAqua/Tintable 与原 vector;完整 10 个 ICNS renditions,1024 高 alpha 边界为 [100,100,923,923],旧图为 [0,0,1023,1023];已删除运行时 override。最终 bundle 资源待 T-003 | Pass (资源与选择链路;最终包待核验) |
+| AC-003 | frozen root/upstream install、typecheck 通过;完整 bounded smoke 18 文件/121 项通过;图标/compiler 与 identity 定向 6 项通过。全新构建及最终包检查进行中 | Pending (最终包) |
+| AC-004 | 新会话动画和实际 Dock 视觉需维护者确认 | Pending Manual |
+| AC-005 | 最终 0.5.1 来源、资产与发布证据待完成 | Pending |
+
+本规格未升级内核、依赖族或内置插件;DV-0033 的真实账户、迁移连续性与字体权限等 Pending Manual 边界保留。
+
+## 基础验证命令
+
+使用固定 Node 24 与仓库 pnpm 11.19.0;上游按其固定 packageManager 使用 pnpm 11.7.0。
+
+```sh
+pnpm install --frozen-lockfile
+pnpm --dir upstream/deepseek-harness install --frozen-lockfile
+pnpm typecheck
+pnpm desktop:smoke
+node apps/deepviewer-desktop/scripts/smoke-native-contract-source.mjs
+node apps/deepviewer-desktop/scripts/smoke-native-ui-regression.mjs
+pnpm --dir apps/deepviewer-desktop exec vitest run test/dock-theme.test.ts test/app-identity.test.ts
+```
+
+旧版 CSS fixture 原文来自 v0.3.3 / Build 75,SVG 对照使用已保留的 DSH 0.1.5 override;不依赖 CI 的 tag 可见性。cursor 为原 1s steps 动画,reduced-motion 禁用它。本轮未操作应用 GUI;renderer GUI smoke 仅更新了断言,不作为已通过证据。
+
+原鱼标 SVG SHA-256:`16896da065025e41040115cc8f16f0268a94200bd5ea3c939e27e3078dff1816`。`dock-theme` 编译器集成检查本机实际运行,没有 skip;测试通过真实 ICNS PNG alpha、catalog metadata 与完整 rendition 表验证编译器输出,同时测试缓存/部分资源修复。About PNG 直接提取 ic10 字节,不重编码或改变图片。
+
+121 项 bounded smoke 已包含 4 项 idle 基线与 3 项 native icon 检查,不将这些子集重复累加。实际图标与视觉验收保持 AC-004 Pending Manual。
diff --git a/docs/sdd/specs/README.md b/docs/sdd/specs/README.md
index 8379b14..1cf1fb0 100644
--- a/docs/sdd/specs/README.md
+++ b/docs/sdd/specs/README.md
@@ -45,9 +45,11 @@ DV-0022、DV-0025、DV-0028 和 DV-0030 是此前仅用于本地/内部固化的
| [DV-0033](DV-0033-scoder-native-rebase/spec.md) | Scoder 原生源码迁回 DeepViewer、白屏修复与 0.5.0 ARM64 发布 | Implementing | Duoasa | 2026-10-03 |
+| [DV-0034](DV-0034-brand-visual-051/spec.md) | 新会话品牌动画与 macOS 图标修复、0.5.1 ARM64 发布 | Implementing | Duoasa | 2026-10-04 |
+
## 下一个编号
-`DV-0034`
+`DV-0035`
## 目录规则
diff --git a/package.json b/package.json
index 99e3cc9..d10587b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@deepviewer/root",
- "version": "0.5.0",
+ "version": "0.5.1",
"private": true,
"type": "module",
"packageManager": "pnpm@11.19.0",