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
31 changes: 28 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,9 @@ jobs:
- name: Verify PPT generation leaves tracked sources clean
run: git diff --exit-code -- packages/ppt-runtime scripts/ppt
- run: npm run typecheck
- name: Boot Harness through Electron Node mode on Windows
run: node scripts/probe-electron-node-runtime.mjs
timeout-minutes: 5
- name: Check recovery UI on native Windows at three display scales
run: node scripts/check-recovery-ui.mjs
- uses: actions/upload-artifact@v4
Expand Down Expand Up @@ -349,11 +352,33 @@ jobs:
$desktopStdout = Join-Path $env:RUNNER_TEMP 'dsh-packaged-desktop.stdout.log'
$desktopStderr = Join-Path $env:RUNNER_TEMP 'dsh-packaged-desktop.stderr.log'
$packagedNode = Join-Path $isolatedApp 'resources\app.asar.unpacked\node_modules\node\bin\node.exe'
if (-not (Test-Path $packagedNode)) { throw 'Packaged Windows node.exe is missing.' }
if (Test-Path $packagedNode) { throw 'Standalone Windows node.exe must not be packaged.' }
# Exercise the actual native binding too; the best-effort fallback
# must not hide a missing or broken packaged koffi dependency.
& $packagedNode --input-type=module -e "import { createRequire } from 'node:module'; import { pathToFileURL } from 'node:url'; const require = createRequire(pathToFileURL(process.argv[1])); const resolved = require.resolve('koffi'); if (!resolved.startsWith(process.argv[2])) throw new Error('koffi resolved outside packaged app: ' + resolved); const koffi = require('koffi'); const getCurrentProcessId = koffi.load('kernel32.dll').func('GetCurrentProcessId', 'uint32', []); if (getCurrentProcessId() !== process.pid) throw new Error('Native koffi smoke failed'); console.log('Packaged koffi native binding passed');" (Join-Path $isolatedApp 'resources\app.asar.unpacked\node_modules\koffi\package.json') (Join-Path $isolatedApp 'resources\app.asar.unpacked\node_modules')
if ($LASTEXITCODE -ne 0) { throw 'Packaged koffi native binding failed.' }
# A Windows GUI-subsystem executable makes PowerShell's '&' return
# before the Node-mode child exits, leaving $LASTEXITCODE unset.
function Invoke-ElectronNode([string[]]$nodeArguments) {
$start = [System.Diagnostics.ProcessStartInfo]::new($executable)
$start.WorkingDirectory = $isolatedApp
$start.UseShellExecute = $false
$start.RedirectStandardOutput = $true
$start.RedirectStandardError = $true
$start.Environment['ELECTRON_RUN_AS_NODE'] = '1'
foreach ($argument in $nodeArguments) { [void]$start.ArgumentList.Add($argument) }
$child = [System.Diagnostics.Process]::Start($start)
$stdout = $child.StandardOutput.ReadToEnd()
$stderr = $child.StandardError.ReadToEnd()
$child.WaitForExit()
if ($stdout) { Write-Host $stdout }
if ($stderr) { Write-Host $stderr }
return $child.ExitCode
}
$koffiProbe = "import { createRequire } from 'node:module'; import { pathToFileURL } from 'node:url'; const require = createRequire(pathToFileURL(process.argv[1])); const resolved = require.resolve('koffi'); if (!resolved.startsWith(process.argv[2])) throw new Error('koffi resolved outside packaged app: ' + resolved); const koffi = require('koffi'); const getCurrentProcessId = koffi.load('kernel32.dll').func('GetCurrentProcessId', 'uint32', []); if (getCurrentProcessId() !== process.pid) throw new Error('Native koffi smoke failed'); console.log('Packaged koffi native binding passed'); process.exit(0);"
$koffiExitCode = Invoke-ElectronNode @('--input-type=module', '-e', $koffiProbe, (Join-Path $isolatedApp 'resources\app.asar.unpacked\node_modules\koffi\package.json'), (Join-Path $isolatedApp 'resources\app.asar.unpacked\node_modules'))
if ($koffiExitCode -ne 0) { throw "Packaged koffi native binding failed (exit code $koffiExitCode)." }
$pnpmEntry = Join-Path $isolatedApp 'resources\app.asar.unpacked\node_modules\pnpm\bin\pnpm.cjs'
$pnpmExitCode = Invoke-ElectronNode @($pnpmEntry, '--version')
if ($pnpmExitCode -ne 0) { throw "Packaged pnpm failed under Electron Node mode (exit code $pnpmExitCode)." }
if (Test-Path $userData) { Remove-Item -Recurse -Force $userData }
$desktop = Start-Process -FilePath $executable -WorkingDirectory $isolatedApp -PassThru `
-RedirectStandardOutput $desktopStdout -RedirectStandardError $desktopStderr
Expand Down
3 changes: 2 additions & 1 deletion build/harness-node-entry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import { enforceWindowsChildProcessHide } from './windows-child-process-hide.mjs
// `--expose-internals` shifts argv and the CLI answers "--profile <name> is
// required" instead of installing. Declaring it here, after this process has
// already parsed the Chromium switches it was launched with, marks only the
// children as Node processes. Windows and Linux use standalone Node runtimes.
// children as Node processes. Windows declares Node mode before launching this
// entry; Linux uses a standalone Node runtime.
if (process.versions.electron !== undefined) {
process.env.ELECTRON_RUN_AS_NODE = '1'
}
Expand Down
14 changes: 0 additions & 14 deletions build/windows-menu.html

This file was deleted.

2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ flowchart TD
MAIN --> UPDATE["Installed-build update manager"]
```

On macOS, Harness runs in an Electron UtilityProcess with Node capabilities. On Windows, it is launched with the bundled target-native Node.js executable. Cordis HMR's `--expose-internals` permission is granted to that isolated process and never to the web renderer.
On macOS, Harness runs in an Electron UtilityProcess with Node capabilities. On Windows, it is launched from the packaged Electron executable in Node mode (`ELECTRON_RUN_AS_NODE=1`); Windows packages carry no standalone `node.exe`. Both paths reach Node internals through Harness's `node-addon-require-builtin`, which accepts only the Electron builds it was compiled for, so `scripts/verify-target.mjs` runs the same loader calls under the packaging Electron before every package build. Cordis HMR's `--expose-internals` permission is granted to that isolated process and never to the web renderer. On Windows the window uses a hidden title bar: Harness draws the 40 DIP caption row, and Desktop adds the "应用" / "编辑" caption menubar beside the sidebar toggle, whose native popups carry the Desktop commands.

## Startup flow

Expand Down
38 changes: 22 additions & 16 deletions docs/baseline-runtime-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,28 @@ Harness 0.1.7 可以通过运行时 resolver 将宿主依赖解析到当前安

验证:内置 Fetch 的 gzip JSON、对象/数组响应头、重复 Set-Cookie、分块 SSE、HTTP/1.1 数组保持原样、trailers;在 Electron Node 24.17.0 中通过 Harness 代理向 Google OAuth token 端点发送无凭据诊断请求,修复前得到 gzip 字节且无响应头,修复后能解析预期的 HTTP 400 JSON。该检查不代表真实账号认证或模型推理验收。

### Windows header clearance after rc.2

`ConversationHeader` now owns the `<header>` outside `conversation.session.header`.
The old preload selector (`[data-slot="conversation.session.header"] > header`)
therefore no longer reserves space for native caption controls. The conversation
patch adds only `data-dsh-conversation-header` to the resident header; preload
uses that marker to reserve the native caption height and keeps all header slots
in normal flow below it. This covers sessionless and active views without cloning
contributions or relying on generated class names. This is window chrome, so the
layout stays in preload rather than a business plugin. Remove the marker patch
when upstream exposes an equivalent stable header/Windows clearance contract.

Regression: `test/windows-titlebar-header.test.mjs` executes the installed rc.2
header in both session states and verifies slot ownership/count. Local Chromium
geometry checks cover narrow/wide, light/dark and blank/active layouts; they do
not replace final Windows installer checks (DPI, zoom, maximize and clicks).
### Windows caption after rc.2

Harness 0.1.7 owns the Windows caption row once preload marks
`<html data-windows-titlebar>` and sets `--dsh-windows-titlebar-height` (40
DIP, matching the upstream Desktop): AppFrame reserves, paints and drags the
row, the sidebar collapses to zero width and its toggle sits in the caption.
Desktop no longer patches the conversation header or pads the expanded sidebar
for the caption; the former `data-dsh-conversation-header` marker and the
sidebar's non-macOS padding were removed from their patches.

The caption menubar ("应用" / "编辑") sits in the upstream seat beside the
sidebar toggle (`--dsh-windows-menu-start`) and opens native popup menus from
the main process. The application menu carries every Desktop command the former
dropdown offered (phone pairing, Harness restart, Safe Mode, log, session
export, view and zoom, updates, about, exit); edit commands are delivered as key
events so editor-owned history receives them. Desktop's own local pages, which
have no AppFrame, get the menubar at the caption's left edge and a drag strip.

Regression: `test/windows-titlebar.test.ts` (menu contents, commands, key
events, request validation) and `test/windows-menu-bar.test.ts` (seat,
labels, popup request). Visual checks on Windows (DPI, zoom, maximize,
collapsed sidebar, caption buttons) are still required.

## 插件 Harness 版本声明采用告警策略

Expand Down
2 changes: 1 addition & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ npm run package:win

Do not invoke `electron-builder --win` from macOS or Linux for a distributable Windows package. The target verification scripts intentionally reject host/target mismatches.

For local unsigned development packages, use the corresponding `package:dev:*` command. Windows packages retain a bundled Node runtime because the Harness native loader does not yet support Electron 43.4.0 in Node mode. Verify that `resources/app.asar.unpacked/node_modules/node/bin/node.exe` is present, the packaged native-module and Harness smokes pass, and the final signed installer passes the separate installed-app smoke before handoff.
For local unsigned development packages, use the corresponding `package:dev:*` command. Windows packages run Harness and package commands through the packaged Electron executable in Node mode; the standalone `resources/app.asar.unpacked/node_modules/node/bin/node.exe` must be absent. Verify the packaged native-module, pnpm and Harness smokes, then the final signed installer's separate installed-app smoke before handoff. The locked Electron 43.0.0 must remain compatible with the native loader: `scripts/verify-target.mjs` fails packaging when Electron in Node mode cannot load it, and changing Electron still requires a new Windows package qualification. Before packaging, `node scripts/probe-electron-node-runtime.mjs` boots Harness from the repository through Electron Node mode with a disposable `DSH_HOME` and checks an authenticated HTTP response; the Windows CI job runs it on every build.

Formal release artifacts are built, signed, and published by the tag workflow. A local build or pull-request check is not formal release evidence.

Expand Down
2 changes: 1 addition & 1 deletion docs/release-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Windows packaging and signing run as separate jobs. The GitHub-hosted Windows ru

The pinned Windows NSIS template stages the application in a sibling directory before closing the old app, then renames the old directory to a backup and promotes the staged directory. A failed extraction or rename restores the previous installation. The signed installer smoke also locks the old executable to verify that a failed upgrade leaves it runnable. A user-selected different directory is an independent installation: the installer does not automatically uninstall the previous directory, which remains available until the user removes it. Keep the user-selected installation directory when changing this template; the build adapter rejects unexpected upstream template changes.

Runtime dependencies are unpacked beside `app.asar` because Harness, pnpm, native addons, and the standalone Windows Node process need physical paths. The macOS ARM64 test package contains a 6.2 MB `app.asar` and about 588 MB of unpacked dependencies; enabling asar alone did not reduce its 256 MB DMG. Do not remove `node.exe` from Windows yet: the pinned `node-addon-require-builtin@0.1.6` rejects Electron 43.4.0 in RunAsNode mode during Harness boot, even though the Koffi native probe passes ([Windows CI evidence](https://github.com/dataelement/dsh-desktop/actions/runs/35972303467)). Repeat the packaged Windows Harness and final signed-installer gates after a loader version explicitly supports the target Electron fingerprint before revisiting that removal.
Runtime dependencies remain unpacked beside `app.asar` because Harness, pnpm, native addons, and plugin generation installation need physical paths. The macOS ARM64 test package contains a 6.2 MB `app.asar` and about 588 MB of unpacked dependencies; enabling asar alone did not reduce its 256 MB DMG. Windows uses the packaged Electron executable in Node mode instead of an independent `node.exe`. The locked Electron 43.0.0 is a native-loader supported fingerprint; an earlier Electron 43.4.0 attempt failed during Harness boot even though the Koffi probe passed ([Windows CI evidence](https://github.com/dataelement/dsh-desktop/actions/runs/35972303467)). Qualify any Electron or native-loader change with packaged Windows Harness and final signed-installer gates.

Prepare the local runner once:

Expand Down
3 changes: 1 addition & 2 deletions electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ export default defineConfig({
build: {
rollupOptions: {
input: {
index: resolve('src/preload/index.ts'),
'windows-menu': resolve('src/preload/windows-menu.ts')
index: resolve('src/preload/index.ts')
},
output: {
format: 'cjs',
Expand Down
5 changes: 1 addition & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -434,10 +434,6 @@
{
"from": "build/community-wechat-qr.png",
"to": "community-wechat-qr.png"
},
{
"from": "build/windows-menu.html",
"to": "windows-menu.html"
}
],
"publish": [
Expand All @@ -463,6 +459,7 @@
},
"win": {
"icon": "build/icon.ico",
"files": ["!node_modules/node/**/*"],
"verifyUpdateCodeSignature": true,
"signtoolOptions": {
"publisherName": "Beijing Shuju Xiangsu Intelligence Technology Co., Ltd.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ async function defaultRunInstall(options, stagingDir) {
cwd: stagingDir,
env: {
...(options.environment ?? process.env),
...(process.platform === 'win32' && process.versions.electron &&
options.nodeExecutablePath === process.execPath
? { ELECTRON_RUN_AS_NODE: '1' }
: {}),
CI: 'true',
NO_COLOR: '1',
npm_config_side_effects_cache: 'false'
Expand Down
32 changes: 12 additions & 20 deletions patches/@deepseek-ai+dsh-client-ui-conversation+0.1.7-rc.2.patch
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
diff --git a/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/client.js b/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/client.js
index 2107bbb..6b4defc 100644
index 2107bbb..8f4eeb5 100644
--- a/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/client.js
+++ b/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/client.js
@@ -15719,12 +15719,13 @@ window.__ModuleLoader__.load({
Expand Down Expand Up @@ -54,15 +54,7 @@ index 2107bbb..6b4defc 100644
]
});
const composer = renderSlotChain("conversation.composer", {
@@ -16290,6 +16298,7 @@ window.__ModuleLoader__.load({
const blank = session === void 0 || conversation === void 0 || session.blank && conversationPhase(session, conversation) === "blank";
return (0, react_jsx_runtime.jsxs)("header", {
className: clsx(ConversationRoot_module_css_default.header, blank && ConversationRoot_module_css_default.headerBlank, sessionId === void 0 && ConversationRoot_module_css_default.headerSessionless),
+ "data-dsh-conversation-header": "",
"data-window-drag": true,
children: [(0, react_jsx_runtime.jsx)("div", {
className: ConversationRoot_module_css_default.headerLeading,
@@ -17092,13 +17101,14 @@ window.__ModuleLoader__.load({
@@ -17092,13 +17100,14 @@ window.__ModuleLoader__.load({
}
//#endregion
//#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css.mjs
Expand All @@ -78,15 +70,15 @@ index 2107bbb..6b4defc 100644
document.head.appendChild(tag);
}
var InputBar_module_css_default = {
@@ -17120,6 +17130,7 @@ window.__ModuleLoader__.load({
@@ -17120,6 +17129,7 @@ window.__ModuleLoader__.load({
"pending": "uV2eYG_pending",
"placeholder": "uV2eYG_placeholder",
"primary": "uV2eYG_primary",
+ "promptRow": "uV2eYG_promptRow",
"retry": "uV2eYG_retry",
"root": "uV2eYG_root",
"row": "uV2eYG_row",
@@ -17146,7 +17157,7 @@ window.__ModuleLoader__.load({
@@ -17146,7 +17156,7 @@ window.__ModuleLoader__.load({
* The no-session state renders the SAME div inert as the Workspace-picker
* trigger instead of a parallel tree.
*/
Expand All @@ -95,7 +87,7 @@ index 2107bbb..6b4defc 100644
const input = useInput((s) => s);
const notice = useNotices((s) => s);
const busyEnter = useBusyEnter((s) => s);
@@ -17374,10 +17385,6 @@ window.__ModuleLoader__.load({
@@ -17374,10 +17384,6 @@ window.__ModuleLoader__.load({
className: InputBar_module_css_default.overlayAnchor,
children: renderSlot("conversation.input.overlay", {})
}),
Expand All @@ -106,7 +98,7 @@ index 2107bbb..6b4defc 100644
renderSlot("conversation.input.attachments", {
attachments,
canAcceptDrop,
@@ -17389,25 +17396,35 @@ window.__ModuleLoader__.load({
@@ -17389,25 +17395,35 @@ window.__ModuleLoader__.load({
onRetryFile: (id) => {
retryFileUpload?.(id);
},
Expand Down Expand Up @@ -156,7 +148,7 @@ index 2107bbb..6b4defc 100644
}),
(0, react_jsx_runtime.jsxs)("div", {
ref: rowRef,
@@ -17535,7 +17552,7 @@ window.__ModuleLoader__.load({
@@ -17535,7 +17551,7 @@ window.__ModuleLoader__.load({
}),
(0, react_jsx_runtime.jsxs)("div", {
className: InputBar_module_css_default.dock,
Expand All @@ -165,7 +157,7 @@ index 2107bbb..6b4defc 100644
useProjection,
t
})]
@@ -18102,6 +18119,14 @@ window.__ModuleLoader__.load({
@@ -18102,6 +18118,14 @@ window.__ModuleLoader__.load({
"conversation.hero.agentPreset": {
kind: "single",
scope: "session-maybe"
Expand All @@ -180,7 +172,7 @@ index 2107bbb..6b4defc 100644
}
},
slots: {
@@ -18225,6 +18250,10 @@ window.__ModuleLoader__.load({
@@ -18225,6 +18249,10 @@ window.__ModuleLoader__.load({
kind: "list",
scope: "session"
},
Expand All @@ -191,15 +183,15 @@ index 2107bbb..6b4defc 100644
"conversation.input.permission": {
kind: "single",
scope: "session"
@@ -18261,6 +18290,7 @@ window.__ModuleLoader__.load({
@@ -18261,6 +18289,7 @@ window.__ModuleLoader__.load({
removeAttachment: void 0,
resolveDraftAttachments: void 0,
retryFileUpload: void 0,
+ openUploadedFile: void 0,
toggleCommandMenu: void 0,
stop: void 0,
hooks: {
@@ -18327,6 +18357,11 @@ window.__ModuleLoader__.load({
@@ -18327,6 +18356,11 @@ window.__ModuleLoader__.load({
retryFileUpload: (id) => {
if (sessions.binding(sessionId) !== void 0) conversation.retryFileUpload(sessionId, id);
},
Expand All @@ -211,7 +203,7 @@ index 2107bbb..6b4defc 100644
toggleCommandMenu: inputTriggers === void 0 ? void 0 : (selection) => {
shell.dismissPopup();
const snapshot = shell.snapshot;
@@ -18359,10 +18394,7 @@ window.__ModuleLoader__.load({
@@ -18359,10 +18393,7 @@ window.__ModuleLoader__.load({
yield slots.register({
name: "main",
key: "conversation",
Expand Down
Loading
Loading