From 81db6135a75877083584d5027e83dd3458926c20 Mon Sep 17 00:00:00 2001 From: blindmikey Date: Thu, 20 Aug 2026 22:51:58 -0700 Subject: [PATCH] fix (windows): use node-pty's bundled ConPTY so sixel reaches the terminal Bump node-pty 1.0.0 -> 1.1.0 and pass `useConptyDll: true`, loading the ConPTY that ships with node-pty (1.23.251008001) instead of the one built into Windows. Older inbox ConPTY versions answer DA1 themselves without the Sixel attribute and filter DCS out of the output stream, so `imageSupport` never receives any image data on those builds. Also run node-pty's post-install.js after rebuild-node-pty: node-gyp clears build/, and pnpm 10 does not run node-pty's own lifecycle scripts (onlyBuiltDependencies), so conpty.dll goes missing next to conpty.node and loading it fails at runtime. Adds a `useConptyDll` config option (default true) to opt back out. before: ESC[?61;6;7;21;22;23;24;28;32;42c (conhost answering, no 4) after: ESC[?62;4;9;22c (xterm-addon-image answering) --- app/config/schema.json | 4 ++++ app/index.ts | 31 +++++++++++++++++++++++-------- app/package.json | 2 +- app/session.ts | 7 +++++++ bin/mk-snapshot.js | 17 +++++++++++++++-- electron-builder.json | 3 +++ package.json | 2 +- typings/config.d.ts | 8 ++++++++ 8 files changed, 62 insertions(+), 12 deletions(-) diff --git a/app/config/schema.json b/app/config/schema.json index 6bcf850036eb..90e02aa5178a 100644 --- a/app/config/schema.json +++ b/app/config/schema.json @@ -342,6 +342,10 @@ }, "useConpty": { "type": "boolean" + }, + "useConptyDll": { + "description": "Windows only. If `true` (default), use the ConPTY that ships with `node-pty`\ninstead of the one built into Windows. The bundled ConPTY (1.23+) passes DCS\nsequences such as Sixel through to the terminal verbatim and reports Sixel\nsupport in its DA1 response; older inbox ConPTY versions filter them out,\nwhich breaks `imageSupport`.", + "type": "boolean" } }, "required": [ diff --git a/app/index.ts b/app/index.ts index 84a804a4da14..e770bc7a2a7c 100644 --- a/app/index.ts +++ b/app/index.ts @@ -78,14 +78,29 @@ async function installDevExtensions(isDev_: boolean) { if (!isDev_) { return []; } - const {default: installer, REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS} = await import('electron-devtools-installer'); - - const extensions = [REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS]; - const forceDownload = Boolean(process.env.UPGRADE_EXTENSIONS); - - return Promise.all( - extensions.map((extension) => installer(extension, {forceDownload, loadExtensionOptions: {allowFileAccess: true}})) - ); + // Installing devtools extensions is a dev-only convenience that depends on a + // remote download, and it can fail (e.g. the Chrome Web Store returning an + // error page instead of a .crx). This must never reject: the `ready` handler + // below creates the app windows inside the `.then()` of this promise, so a + // rejection here starts Hyper with no window at all. + try { + const {default: installer, REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS} = await import('electron-devtools-installer'); + + const extensions = [REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS]; + const forceDownload = Boolean(process.env.UPGRADE_EXTENSIONS); + + return await Promise.all( + extensions.map((extension) => + installer(extension, {forceDownload, loadExtensionOptions: {allowFileAccess: true}}).catch((err: unknown) => { + console.warn('Skipping devtools extension:', err); + return undefined; + }) + ) + ); + } catch (err) { + console.warn('Skipping devtools extensions:', err); + return []; + } } // eslint-disable-next-line @typescript-eslint/no-misused-promises diff --git a/app/package.json b/app/package.json index 149ce08aff29..2bf244324d73 100644 --- a/app/package.json +++ b/app/package.json @@ -30,7 +30,7 @@ "lodash": "4.17.21", "ms": "2.1.3", "native-process-working-directory": "^1.0.2", - "node-pty": "1.0.0", + "node-pty": "1.1.0", "os-locale": "5.0.0", "parse-url": "8.1.0", "queue": "6.0.2", diff --git a/app/session.ts b/app/session.ts index 4b706b3d61da..90e1889bdf4f 100644 --- a/app/session.ts +++ b/app/session.ts @@ -27,6 +27,11 @@ try { } const useConpty = config.getConfig().useConpty; +// Defaults to true: the ConPTY bundled with node-pty (1.23+) passes DCS sequences +// such as Sixel through verbatim and advertises Sixel in its DA1 response, which +// the inbox ConPTY on many Windows builds does not. Set `useConptyDll: false` in +// the config to fall back to the Windows one. +const useConptyDll = config.getConfig().useConptyDll ?? true; // Max duration to batch session data before sending it to the renderer process. const BATCH_DURATION_MS = 16; @@ -156,6 +161,8 @@ export default class Session extends EventEmitter { options.useConpty = useConpty; } + options.useConptyDll = useConptyDll; + try { this.pty = spawn(shell, shellArgs, options); } catch (_err) { diff --git a/bin/mk-snapshot.js b/bin/mk-snapshot.js index a65c54622017..c3c093764f6e 100644 --- a/bin/mk-snapshot.js +++ b/bin/mk-snapshot.js @@ -42,9 +42,22 @@ async function main() { } console.log(`Generating startup blob in "${outputBlobPath}"`); + const isWindows = process.platform === 'win32'; + const mksnapshotPath = path.resolve( + __dirname, + '..', + 'node_modules', + '.bin', + 'mksnapshot' + (isWindows ? '.cmd' : '') + ); + // Node >= 18.20.2 / 20.12.2 / 21.7.0 refuses to spawn .cmd and .bat files + // without `shell` (CVE-2024-27980), which makes execFileSync fail with + // EINVAL. On Windows, go through the shell and quote the arguments here. + const quote = (arg) => (isWindows ? `"${arg}"` : arg); childProcess.execFileSync( - path.resolve(__dirname, '..', 'node_modules', '.bin', 'mksnapshot' + (process.platform === 'win32' ? '.cmd' : '')), - [snapshotScriptPath, '--output_dir', outputBlobPath] + quote(mksnapshotPath), + [snapshotScriptPath, '--output_dir', outputBlobPath].map(quote), + isWindows ? {shell: true} : {} ); } diff --git a/electron-builder.json b/electron-builder.json index e5f0d69198e0..95f54090874b 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -18,6 +18,9 @@ } ], "artifactName": "${productName}-${version}-${arch}.${ext}", + "asarUnpack": [ + "**/node_modules/node-pty/**" + ], "linux": { "category": "TerminalEmulator", "target": [ diff --git a/package.json b/package.json index f0d433ff23ba..78ed077e4be1 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "test:unit:watch": "pnpm run test:unit -- --watch", "test:e2e": "ava --config ava-e2e.config.js", "postinstall": "pnpm run v8-snapshot && webpack --config-name hyper-app && electron-builder install-app-deps && pnpm run rebuild-node-pty && cpy --cwd=target node_modules \"../../app/\" && husky install && pnpm run generate-schema", - "rebuild-node-pty": "electron-rebuild -f -o node-pty -m target", + "rebuild-node-pty": "electron-rebuild -f -o node-pty -m target && node target/node_modules/node-pty/scripts/post-install.js", "dist": "pnpm run build && electron-builder", "clean": "node ./bin/rimraf-standalone.js node_modules && node ./bin/rimraf-standalone.js ./app/node_modules && node ./bin/rimraf-standalone.js ./app/renderer", "v8-snapshot": "cross-env npm_config_arch=x64 pnpm run v8-snapshot:arch && cross-env npm_config_arch=arm64 pnpm run v8-snapshot:arch", diff --git a/typings/config.d.ts b/typings/config.d.ts index 7a6053e93fe3..878298fe73cc 100644 --- a/typings/config.d.ts +++ b/typings/config.d.ts @@ -32,6 +32,14 @@ type rootConfigOptions = { /** choose either `'stable'` for receiving highly polished, or `'canary'` for less polished but more frequent updates */ updateChannel: 'stable' | 'canary'; useConpty?: boolean; + /** + * Windows only. If `true` (default), use the ConPTY that ships with `node-pty` + * instead of the one built into Windows. The bundled ConPTY (1.23+) passes DCS + * sequences such as Sixel through to the terminal verbatim and reports Sixel + * support in its DA1 response; older inbox ConPTY versions filter them out, + * which breaks `imageSupport`. + */ + useConptyDll?: boolean; }; type profileConfigOptions = {