diff --git a/core/README.md b/core/README.md
index a4a7387..a881270 100644
--- a/core/README.md
+++ b/core/README.md
@@ -277,7 +277,10 @@ resolve: { dedupe: ['react', 'react-dom'] }, optimizeDeps: { exclude: ['prism-te
`react`, `node-pty` and `@xterm/*` must stay at compatible versions in both apps
so npm keeps ONE copy of each; the core is verified against Prism Terminal's
-lockfile but ships against Prism's.
+lockfile but ships against Prism's. Both apps pin `node-pty` to EXACTLY
+`1.2.0-beta.15` (1.1.0's quit crash, #127/#159). The core's peer names that
+build (`^1.1.0 || 1.2.0-beta.15`): a bare `^1.1.0` matches no prerelease, so npm
+refused the pin in Prism.
## Releasing the core
diff --git a/core/package.json b/core/package.json
index 1e67f97..092d71d 100644
--- a/core/package.json
+++ b/core/package.json
@@ -1,6 +1,6 @@
{
"name": "prism-term-core",
- "version": "0.28.1",
+ "version": "0.28.2",
"description": "What Prism Terminal and Prism share: the terminal (pty, shells, agent detection and indicator, themes, links, the panel, dictation) and the update chip with its window. TypeScript source, compiled by the host.",
"license": "MIT",
"private": true,
@@ -40,7 +40,7 @@
"@xterm/addon-unicode11": "^0.9.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
- "node-pty": "^1.1.0",
+ "node-pty": "^1.1.0 || 1.2.0-beta.15",
"react": "^19.2.7",
"react-dom": "^19.2.7"
}
diff --git a/docs/regression-rules.md b/docs/regression-rules.md
index ca44b6b..78836ae 100644
--- a/docs/regression-rules.md
+++ b/docs/regression-rules.md
@@ -49,6 +49,8 @@ one-liner in `CLAUDE.md` only if it is among the most important.
`ThreadSafeFunction::CallJS` -> abort). So every kill goes through `killPty` and `will-quit` holds
the quit on `shellsGone` (3 s cap). It waits for the agent's `exitCode`, set by the native callback,
not the exit EVENT, which lags 1-2.7 s for a pwsh killed mid-start (a warm shell). `quitManyShells`.
+ The pin was LOST once (#159, 2026-10-10): an icon PR's merge put `^1.1.0` back and the dialog
+ returned for the owner. `src/main/nodePtyPin.test.ts` now fails on any build without the pin.
- **`titleBarStyle: 'hidden'`, never `frame: false`**: DWM will not composite acrylic behind a
frameless window.
- **Material before colour** (`material.ts`, measured on Electron 43): `setBackgroundMaterial('none')`
diff --git a/package-lock.json b/package-lock.json
index ec04151..f2ecb57 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "prism-terminal",
- "version": "0.35.1",
+ "version": "0.35.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "prism-terminal",
- "version": "0.35.1",
+ "version": "0.35.2",
"license": "MIT",
"dependencies": {
"@xterm/addon-fit": "^0.11.0",
@@ -14,7 +14,7 @@
"@xterm/addon-unicode11": "^0.9.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
- "node-pty": "^1.1.0",
+ "node-pty": "1.2.0-beta.15",
"react": "^19.2.7",
"react-dom": "^19.2.7"
},
@@ -2204,6 +2204,72 @@
"node": ">=14.0.0"
}
},
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
+ "version": "1.11.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.2",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
+ "version": "1.11.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.2",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.4",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
+ "version": "0.10.2",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
+ "version": "2.8.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "0BSD",
+ "optional": true
+ },
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz",
@@ -6107,9 +6173,9 @@
"license": "MIT"
},
"node_modules/node-pty": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0.tgz",
- "integrity": "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==",
+ "version": "1.2.0-beta.15",
+ "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.2.0-beta.15.tgz",
+ "integrity": "sha512-vORSzHXi4Ofl7HemVWpuudLqCPdaQb4LfpRCUpE5HPxhp4JYscl8zZwxh11p26v2wvW24WMwnMfLjhRLixrfxA==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 1dcd21a..f93f5f0 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "prism-terminal",
"productName": "Prism Terminal",
- "version": "0.35.1",
+ "version": "0.35.2",
"description": "A tabbed Windows terminal for AI CLIs.",
"main": "./out/main/index.js",
"author": "Max",
@@ -29,7 +29,7 @@
"@xterm/addon-unicode11": "^0.9.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
- "node-pty": "^1.1.0",
+ "node-pty": "1.2.0-beta.15",
"react": "^19.2.7",
"react-dom": "^19.2.7"
},
@@ -57,6 +57,6 @@
"vitest": "^3.2.7"
},
"allowScripts": {
- "node-pty@1.1.0": true
+ "node-pty@1.2.0-beta.15": true
}
}
diff --git a/src/main/nodePtyPin.test.ts b/src/main/nodePtyPin.test.ts
new file mode 100644
index 0000000..167b63d
--- /dev/null
+++ b/src/main/nodePtyPin.test.ts
@@ -0,0 +1,35 @@
+import { readFileSync } from 'node:fs'
+import { join } from 'node:path'
+import { describe, expect, it } from 'vitest'
+
+// #127/#128 pinned node-pty to 1.2.0-beta.15: 1.1.0's exit threads race on an
+// unlocked vector and its prebuild asserts ("Assertion failed! remove_pty_baton"),
+// a modal dialog that holds the app open. #130 (an icon PR) put ^1.1.0 back in a
+// merge and nothing noticed until the owner saw the dialog again (#159). The
+// quit e2e only catches the race sometimes; this catches the pin every run.
+const PIN = '1.2.0-beta.15'
+const root = join(__dirname, '..', '..')
+interface Pkg {
+ dependencies: Record
+ peerDependencies: Record
+ packages: Record
+}
+const json = (f: string): Pkg => JSON.parse(readFileSync(join(root, f), 'utf8')) as Pkg
+
+describe('node-pty pin (#159)', () => {
+ it('package.json asks for exactly the pinned build', () => {
+ const pkg = json('package.json')
+ expect(pkg.dependencies['node-pty']).toBe(PIN)
+ expect(JSON.stringify(pkg)).not.toContain('node-pty@1.1.0')
+ })
+
+ // A `^1.1.0` peer matches no prerelease, so Prism's npm refused the pin. It
+ // still allows 1.1.0 so Prism's core bump installs before Prism's own pin lands.
+ it('the core lets its hosts take the pinned build', () => {
+ expect(json('core/package.json').peerDependencies['node-pty'].split(' || ')).toContain(PIN)
+ })
+
+ it('the lockfile resolves it', () => {
+ expect(json('package-lock.json').packages['node_modules/node-pty'].version).toBe(PIN)
+ })
+})