specrails-desktop 2.42.1 → 2.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/ci-cd.md CHANGED
@@ -42,7 +42,7 @@ The macOS job retains `artifacts/chromium-signing/` as the `chromium-signing-mac
42
42
  ## Credentials and external requirements
43
43
 
44
44
  - `RELEASE_PAT` must be authorized for release-please's pull requests and releases. It also permits tag pushes to trigger publication workflows; events created with the repository's ordinary `GITHUB_TOKEN` generally do not trigger another workflow.
45
- - `NPM_TOKEN` must authenticate an account with publishing permission for `specrails-desktop`. The workflow checks authentication and gives an explicit error before publishing. Provenance requires `id-token: write`; it does **not** grant npm publishing permission. Trusted publishing requires separate registry configuration and is not silently enabled here. An npm `E404` during publication is not sufficient evidence to identify a particular token or permission failure. [npm publishing and provenance](https://docs.github.com/en/actions/tutorials/publish-packages/publish-nodejs-packages).
45
+ - Publication uses one of two credentials. When the `NPM_TOKEN` secret is set, it must authenticate an account with publishing permission for `specrails-desktop` (npm now requires such tokens to be granular with *bypass 2FA*, and they expire within 90 days); the workflow checks authentication and gives an explicit error before publishing. When the secret is empty, the job publishes through npm **trusted publishing**: it installs npm 11, removes the empty token line that `setup-node` writes, and lets npm exchange the job's OIDC identity (`id-token: write`) for a short-lived publish grant. That path only works once the package's *Trusted publisher* on npmjs.com names this repository (`fjpulidop/specrails-desktop`) and workflow file (`npm-publish.yml`, no environment); registry configuration is never changed from here. Provenance does **not** grant publishing permission in either mode. An npm `E404` during publication is npm's answer to an unauthorized publish and does not identify which token or permission failed; an `E403` mentioning two-factor authentication means the stored token lacks *bypass 2FA*. [npm publishing and provenance](https://docs.github.com/en/actions/tutorials/publish-packages/publish-nodejs-packages).
46
46
  - Existing Apple signing/notarization and Tauri updater signing secrets remain required. Installer build steps retain their platform-specific checks.
47
47
  - Hostinger credentials must support **explicit FTPS**, a valid TLS certificate, uploads and `RNFR`/`RNTO` in the release directory. There is no plaintext FTP fallback. A failed transfer or rename fails the workflow before old files are removed. [FTP action protocol and security options](https://github.com/SamKirkland/FTP-Deploy-Action/blob/v4.3.6/action.yml).
48
48
 
package/docs/codex.md CHANGED
@@ -46,9 +46,9 @@ binary isn't on `PATH`; it shows install commands if you click "More info".
46
46
  The first provider you select becomes the project default.
47
47
  4. Submit. The app writes `.specrails/install-config.yaml` (with
48
48
  `provider: codex` and `tier: quick` as YAML keys) and spawns
49
- `npx --yes --prefer-online specrails-core@^4.12.0 init --yes --from-config <file>`
49
+ `npx --yes --prefer-online specrails-core@^5.1.0 init --yes --from-config <file>`
50
50
  — the provider and tier live in the YAML, not as CLI flags. (The app
51
- pins `specrails-core@^4.12.0`; that floor is the version that ships the
51
+ pins `specrails-core@^5.1.0`; that floor is the version that ships the
52
52
  current provider targets, including Kimi and the Codex skill set.) The install
53
53
  produces:
54
54
  - `.codex/config.toml` — model, reasoning effort, sandbox mode, and
@@ -148,7 +148,7 @@ Most settings live in the UI. A few app-level switches are env-only because they
148
148
 
149
149
  | Variable | Effect |
150
150
  |----------|--------|
151
- | `SPECRAILS_CORE_BIN` | Override the `specrails-core` binary (default: `npx --yes --prefer-online specrails-core@^4.12.0` — the 4.12.0 floor is the release that ships the Kimi provider target) |
151
+ | `SPECRAILS_CORE_BIN` | Override the `specrails-core` binary (default: `npx --yes --prefer-online specrails-core@^5.1.0` — the 5.1.0 floor is the release that quotes the command path in its Windows shell runner, so OpenSpec init works from an install directory with spaces; it is also the version bundled with the app) |
152
152
  | `SPECRAILS_TECH_URL` | Override the specrails-tech proxy base URL |
153
153
  | `SPECRAILS_AGENTS_SECTION=false` | Hide the Agents section from every project |
154
154
  | `SPECRAILS_PLUGINS_SECTION=false` | Hide the Integrations section from every project |
package/docs/gemini.md CHANGED
@@ -24,11 +24,11 @@ Specrails supports **four AI providers**: Anthropic's
24
24
  |---|---|---|
25
25
  | `gemini` CLI ≥ 0.11.0 | Earlier versions lack `--output-format stream-json` + headless `--resume`, which the app relies on | `npm i -g @google/gemini-cli` · check with `gemini --version` |
26
26
  | A Gemini API key | The app spawns Gemini headlessly, so it needs non-interactive auth | Set `GEMINI_API_KEY` to a paid Gemini Developer API key from [Google AI Studio](https://aistudio.google.com/apikey) |
27
- | `specrails-core` ≥ 4.8.0 in the project | 4.8.0 ships the Gemini provider target (`.gemini/` commands + agents) that the rails pipeline needs | The current Add-Project install flow uses `specrails-core@^4.12.0` automatically |
27
+ | `specrails-core` ≥ 4.8.0 in the project | 4.8.0 ships the Gemini provider target (`.gemini/` commands + agents) that the rails pipeline needs | The current Add-Project install flow uses `specrails-core@^5.1.0` automatically |
28
28
  | `git`, `node`, `npm`, `npx` | Same as Claude — needed for `specrails-core init` | Use your usual installer |
29
29
 
30
30
  > **Two different minimums.** The `gemini` **binary** floor is **0.11.0**.
31
- > The current Desktop `specrails-core` **package** floor is **4.12.0** (a
31
+ > The current Desktop `specrails-core` **package** floor is **5.1.0** (a
32
32
  > single shared version for all providers, including Kimi). They are separate
33
33
  > things — the binary on your
34
34
  > machine vs. the artifacts installed into the project.
@@ -52,7 +52,7 @@ the Gemini provider checkbox with a "not found" hint when the binary isn't on
52
52
  project](#running-multiple-providers-in-one-project)). The first provider
53
53
  you select becomes the project default.
54
54
  4. Submit. The app writes `.specrails/install-config.yaml` and spawns
55
- `npx --yes --prefer-online specrails-core@^4.12.0 init --yes --from-config <file>`.
55
+ `npx --yes --prefer-online specrails-core@^5.1.0 init --yes --from-config <file>`.
56
56
  The install produces the `.gemini/` artifacts (commands + `sr-*` agents),
57
57
  plus a `GEMINI.md` instructions file.
58
58
 
@@ -61,7 +61,7 @@ specrails-desktop list # verify
61
61
  The setup wizard runs automatically. Three steps:
62
62
 
63
63
  1. **Configure** — choose which agents to install (the baseline trio `sr-architect`, `sr-developer`, `sr-reviewer` is always selected; optional agents like Test Writer or Security Reviewer are opt-in). Pick a model preset (Balanced / Budget / Max) and optionally override the model per agent.
64
- 2. **Install** — the app runs the installer (`npx --yes --prefer-online specrails-core@^4.12.0 init --yes --from-config <config>`) non-interactively and streams the output live. The version is pinned to the core release the app ships with — it never silently jumps to a new major.
64
+ 2. **Install** — the app runs the installer (`npx --yes --prefer-online specrails-core@^5.1.0 init --yes --from-config <config>`) non-interactively and streams the output live. The version is pinned to the core release the app ships with — it never silently jumps to a new major.
65
65
  3. **Done** — a summary tells you how many agents and commands landed. Click **Continue to project**.
66
66
 
67
67
  That's the whole onboarding. No tier picker, no second wizard. You can manage agents and their per-agent models later from the **Agents** page (Profiles tab).
@@ -26,7 +26,7 @@ This audit covers the `codex/multi-repo-projects` branch, including mission stee
26
26
 
27
27
  ## Automated gates
28
28
 
29
- `.github/workflows/windows-parity.yml` runs a `windows-parity` job on `windows-latest` (x64) for every push and pull request, and adds `windows-11-arm` on pushes to `main`. It installs dependencies, checks TypeScript, runs release/PTY helper regression tests, native filesystem/process tests (retried once, they drive real processes), the application build, native host build/tests, and four real native fixtures: WebView2 capture/selection, authentication popups, mission window handoff, and browser multiwindow transfer/parking. It is deliberately a separate workflow from `ci.yml`: `release.yml` waits for CI only, so a slow or flaky Windows run never delays the release PR, while Desktop Release still builds and installs on Windows before publication. The jsdom client suite is not repeated on Windows; it runs on Linux in CI. The bundled-core matrix in `ci.yml` exercises Windows junction/copy relocation for providers.
29
+ `.github/workflows/windows-parity.yml` runs a `windows-parity` job on `windows-latest` (x64) for every pull request and push to `main`, adding `windows-11-arm` on the `main` pushes. It installs dependencies, checks TypeScript, runs release/PTY helper regression tests, native filesystem/process tests (retried once, they drive real processes), the application build, native host build/tests, and four real native fixtures: WebView2 capture/selection, authentication popups, mission window handoff, and browser multiwindow transfer/parking. It is deliberately a separate workflow from `ci.yml`: `release.yml` waits for CI only, so a slow or flaky Windows run never delays the release PR, while Desktop Release still builds and installs on Windows before publication. The jsdom client suite is not repeated on Windows; it runs on Linux in CI. The bundled-core matrix in `ci.yml` exercises Windows junction/copy relocation for providers.
30
30
 
31
31
  `.github/workflows/desktop-release.yml` builds the real installers and runs `scripts/smoke-windows-installers.ps1` before publishing artifacts. For each NSIS and MSI package, the script installs into a temporary path containing spaces and drives `scripts/smoke-installed-windows.mjs` with the installed Node runtime. The driver uses an isolated user profile and tests:
32
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specrails-desktop",
3
- "version": "2.42.1",
3
+ "version": "2.43.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -139,7 +139,7 @@ async function gitIgnoredSet(projectPath, relPaths, maxDurationMs = Number.POSIT
139
139
  let stdout = '';
140
140
  let settled = false;
141
141
  let overflow = false;
142
- const child = (0, child_process_1.spawn)('git', ['check-ignore', '--stdin', '-z'], { cwd: projectPath, stdio: ['pipe', 'pipe', 'ignore'], env: file_provenance_1.GIT_EXEC_ENV });
142
+ const child = (0, child_process_1.spawn)('git', ['check-ignore', '--stdin', '-z'], { cwd: projectPath, stdio: ['pipe', 'pipe', 'ignore'], env: (0, file_provenance_1.gitExecEnv)() });
143
143
  const finish = (complete) => { if (!settled) {
144
144
  settled = true;
145
145
  resolve({ stdout, complete: complete && !overflow });
@@ -1,7 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CORE_PACKAGE_SPEC = void 0;
4
- /** Online fallback selects the supported Core 5 lifecycle. Bundled Core 4 remains
5
- * readable for installed apps; the runtime resolver selects the newest usable
6
- * compatible package and never downgrades a newer activated framework. */
7
- exports.CORE_PACKAGE_SPEC = 'specrails-core@^5.0.0';
4
+ /** Online fallback selects the supported Core 5 lifecycle; 5.1.0 is the floor
5
+ * because it quotes the command path in its Windows shell runner (OpenSpec init
6
+ * from an install directory with spaces). The desktop bundles the same version.
7
+ * Bundled Core 4 remains readable for installed apps; the runtime resolver
8
+ * selects the newest usable compatible package and never downgrades a newer
9
+ * activated framework. */
10
+ exports.CORE_PACKAGE_SPEC = 'specrails-core@^5.1.0';
@@ -3,7 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.STORY_EXCERPT_MAX_BYTES = exports.GIT_EXEC_ENV = void 0;
6
+ exports.STORY_EXCERPT_MAX_BYTES = void 0;
7
+ exports.gitExecEnv = gitExecEnv;
7
8
  exports.resolveHeadSha = resolveHeadSha;
8
9
  exports.listUntracked = listUntracked;
9
10
  exports.snapshotWorkingTree = snapshotWorkingTree;
@@ -34,9 +35,13 @@ const GIT_MAX_BUFFER = 16 * 1024 * 1024;
34
35
  // the on-demand diff patches beyond this cap are skipped (the UI shows "diff
35
36
  // unavailable" for them). Mirrors the existing large-job warn threshold (50).
36
37
  const MAX_PATCH_FILES = 50;
37
- // Exported: project-git.ts (the Agent-Mode git bar) runs the same cwd-scoped
38
- // git calls and needs the identical hostile-repo hardening.
39
- exports.GIT_EXEC_ENV = (() => {
38
+ // Exported: project-git.ts (the Agent-Mode git bar), the code explorer and the
39
+ // git diagnostics run the same cwd-scoped git calls and need the identical
40
+ // hostile-repo hardening. Computed PER CALL, never at module load: the startup
41
+ // path resolver prepends the bundled git directory to process.env.PATH after
42
+ // this module was imported, and a snapshot taken earlier could not find git in
43
+ // a packaged app without a system git — every guard then failed closed.
44
+ function gitExecEnv() {
40
45
  // Inherit the parent env but STRIP git-location vars. If the app process (or a
41
46
  // parent) ever exports GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE, every cwd-scoped
42
47
  // git call below would silently operate on that repo instead of the project —
@@ -61,7 +66,7 @@ exports.GIT_EXEC_ENV = (() => {
61
66
  env.GIT_CONFIG_NOSYSTEM = '1';
62
67
  env.GIT_CONFIG_PARAMETERS = "'core.fsmonitor=false' 'core.hooksPath=/dev/null' 'protocol.ext.allow=user'";
63
68
  return env;
64
- })();
69
+ }
65
70
  /** Resolve the current HEAD commit SHA. Best-effort: '' on any git failure
66
71
  * (no repo, unborn HEAD, no git, timeout). */
67
72
  function resolveHeadSha(cwd) {
@@ -72,7 +77,7 @@ function resolveHeadSha(cwd) {
72
77
  stdio: ['ignore', 'pipe', 'pipe'],
73
78
  timeout: GIT_TIMEOUT_MS,
74
79
  maxBuffer: GIT_MAX_BUFFER,
75
- env: exports.GIT_EXEC_ENV,
80
+ env: gitExecEnv(),
76
81
  });
77
82
  return out.trim();
78
83
  }
@@ -90,7 +95,7 @@ function listUntracked(cwd) {
90
95
  stdio: ['ignore', 'pipe', 'pipe'],
91
96
  timeout: GIT_TIMEOUT_MS,
92
97
  maxBuffer: GIT_MAX_BUFFER,
93
- env: exports.GIT_EXEC_ENV,
98
+ env: gitExecEnv(),
94
99
  });
95
100
  return out.split('\0').filter((p) => p.length > 0);
96
101
  }
@@ -107,7 +112,7 @@ function snapshotWorkingTree(cwd) {
107
112
  stdio: ['ignore', 'pipe', 'pipe'],
108
113
  timeout: GIT_TIMEOUT_MS,
109
114
  maxBuffer: GIT_MAX_BUFFER,
110
- env: exports.GIT_EXEC_ENV,
115
+ env: gitExecEnv(),
111
116
  });
112
117
  ref = out.trim();
113
118
  }
@@ -141,7 +146,7 @@ function diffAgainstSnapshot(cwd, snapshotRef, untrackedBefore, baseSha) {
141
146
  stdio: ['ignore', 'pipe', 'pipe'],
142
147
  timeout: GIT_TIMEOUT_MS,
143
148
  maxBuffer: GIT_MAX_BUFFER,
144
- env: exports.GIT_EXEC_ENV,
149
+ env: gitExecEnv(),
145
150
  });
146
151
  }
147
152
  catch {
@@ -309,7 +314,7 @@ function collectDiffPatches(cwd, snapshotRef, diff, baseSha) {
309
314
  stdio: ['ignore', 'pipe', 'pipe'],
310
315
  maxBuffer: MAX_PATCH_BYTES + 64 * 1024,
311
316
  timeout: GIT_TIMEOUT_MS,
312
- env: exports.GIT_EXEC_ENV,
317
+ env: gitExecEnv(),
313
318
  });
314
319
  }
315
320
  catch (err) {
@@ -17,7 +17,7 @@ const win_spawn_1 = require("./util/win-spawn");
17
17
  * SAFETY: the command list is a FIXED ALLOWLIST — the caller only picks an
18
18
  * action name, never arguments, so no flag/ref/path can be smuggled in. Every
19
19
  * command is read-only (no push/commit/checkout/pr-create). git runs with the
20
- * hardened `GIT_EXEC_ENV` (hostile-repo config stripped, prompts disabled); gh
20
+ * hardened `gitExecEnv()` (hostile-repo config stripped, prompts disabled); gh
21
21
  * runs with the normal env so it finds the user's `~/.config/gh` auth and the
22
22
  * bundled `gh` on PATH.
23
23
  */
@@ -43,7 +43,7 @@ const OUTPUT_CAP = 8_000;
43
43
  const defaultExec = (cmd, args, cwd) => new Promise((resolve) => {
44
44
  // git: hardened, cwd-pinned env. gh: user env (needs ~/.config/gh auth) with
45
45
  // the Windows shell-critical vars guaranteed present.
46
- const env = cmd === 'gh' ? (0, win_spawn_1.windowsSpawnEnv)() : file_provenance_1.GIT_EXEC_ENV;
46
+ const env = cmd === 'gh' ? (0, win_spawn_1.windowsSpawnEnv)() : (0, file_provenance_1.gitExecEnv)();
47
47
  (0, child_process_1.execFile)(cmd, args, { cwd, env, timeout: 15_000, maxBuffer: 8 * 1024 * 1024, windowsHide: true }, (err, stdout, stderr) => {
48
48
  const code = err && typeof err.code === 'number'
49
49
  ? err.code
@@ -35,7 +35,7 @@ function createBoundedExec(timeoutMs = exports.PR_COMMAND_TIMEOUT_MS) {
35
35
  // `windowsSpawnEnv()` backfills SystemRoot / ComSpec / USERPROFILE, which a
36
36
  // GUI-launched / pkg-stripped Windows sidecar can lack — without them the
37
37
  // `git`/`gh` child (and the `sh -c` the `!gh …` credential helper runs) can
38
- // fail to start. NOT `GIT_EXEC_ENV`: that hardened env disables credentials
38
+ // fail to start. NOT `gitExecEnv()`: that hardened env disables credentials
39
39
  // (GIT_ASKPASS=echo / GIT_TERMINAL_PROMPT=0), which the push must keep. No-op
40
40
  // on POSIX (returns process.env), so mac/Linux behaviour is byte-identical.
41
41
  const env = (0, win_spawn_1.windowsSpawnEnv)();
@@ -21,7 +21,7 @@ const GIT_MAX_BUFFER = 4 * 1024 * 1024;
21
21
  const COMMIT_SHA_RE = /^[0-9a-f]{40,64}$/i;
22
22
  function git(repoDir, args) {
23
23
  return new Promise((resolve, reject) => {
24
- (0, child_process_1.execFile)('git', args, { cwd: repoDir, env: file_provenance_1.GIT_EXEC_ENV, timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_MAX_BUFFER, windowsHide: true }, (err, stdout, stderr) => {
24
+ (0, child_process_1.execFile)('git', args, { cwd: repoDir, env: (0, file_provenance_1.gitExecEnv)(), timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_MAX_BUFFER, windowsHide: true }, (err, stdout, stderr) => {
25
25
  if (err) {
26
26
  const detail = String(stderr || err.message).trim();
27
27
  reject(new Error(detail));
@@ -772,7 +772,13 @@ class SetupManager {
772
772
  providers: list,
773
773
  continueOnError: true,
774
774
  onProviderStart: (p) => emit(p, 'running'),
775
- onProviderResult: (r) => emit(r.provider, r.ok ? 'done' : 'failed', r.error),
775
+ onProviderResult: (r) => {
776
+ // The WS event carries the reason to the app; the server log must too,
777
+ // or a packaged-app failure (installer smoke, support bundle) is blind.
778
+ if (!r.ok)
779
+ console.warn(`[SetupManager] silent assemble: ${r.provider} failed for ${projectId}: ${r.error ?? 'unknown error'}`);
780
+ emit(r.provider, r.ok ? 'done' : 'failed', r.error);
781
+ },
776
782
  })
777
783
  .then((results) => {
778
784
  const failed = results.filter((r) => !r.ok).map((r) => r.provider);
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.TerminalManager = exports.TERMINAL_EARLY_EXIT_MS = exports.TerminalSpawnError = exports.TerminalNameInvalidError = exports.TerminalNotFoundError = exports.TerminalLimitExceededError = exports.RingBuffer = exports.TERMINAL_DEFAULT_ROWS = exports.TERMINAL_DEFAULT_COLS = exports.TERMINAL_NAME_MAX = exports.TERMINAL_MAX_PER_PROJECT = exports.TERMINAL_KILL_GRACE_MS = exports.TERMINAL_SCROLLBACK_BYTES = void 0;
7
+ exports.killTerminalTree = killTerminalTree;
7
8
  exports.resolveShell = resolveShell;
8
9
  exports.resolveShellFor = resolveShellFor;
9
10
  exports.shellArgs = shellArgs;
@@ -20,6 +21,21 @@ const terminal_marks_store_1 = require("./terminal-marks-store");
20
21
  const terminal_shell_integration_1 = require("./terminal-shell-integration");
21
22
  exports.TERMINAL_SCROLLBACK_BYTES = 262_144;
22
23
  exports.TERMINAL_KILL_GRACE_MS = 2_000;
24
+ /**
25
+ * Windows: node-pty ends a ConPTY session by enumerating the pseudo console's
26
+ * processes through a helper it starts with `process.execPath`. In the
27
+ * packaged sidecar that path is the sidecar binary itself, so the helper never
28
+ * runs, only the shell is terminated and whatever the shell started (a dev
29
+ * server, a watcher) survives the closed terminal. Kill the shell's process
30
+ * tree ourselves (`taskkill /T /F` through tree-kill). No-op elsewhere: the
31
+ * POSIX pty closes its process group.
32
+ */
33
+ function killTerminalTree(pid, platform = process.platform, kill = win_spawn_1.treeKillSafe) {
34
+ if (platform !== 'win32' || !pid || !Number.isInteger(pid) || pid <= 0)
35
+ return false;
36
+ kill(pid, 'SIGKILL');
37
+ return true;
38
+ }
23
39
  exports.TERMINAL_MAX_PER_PROJECT = 10;
24
40
  exports.TERMINAL_NAME_MAX = 64;
25
41
  exports.TERMINAL_DEFAULT_COLS = 80;
@@ -455,6 +471,7 @@ class TerminalManager {
455
471
  async shutdown() {
456
472
  const all = Array.from(this.sessions.values());
457
473
  for (const s of all) {
474
+ killTerminalTree(s.pty.pid);
458
475
  try {
459
476
  s.pty.kill('SIGTERM');
460
477
  }
@@ -462,6 +479,7 @@ class TerminalManager {
462
479
  }
463
480
  await new Promise((r) => setTimeout(r, exports.TERMINAL_KILL_GRACE_MS));
464
481
  for (const s of Array.from(this.sessions.values())) {
482
+ killTerminalTree(s.pty.pid);
465
483
  try {
466
484
  s.pty.kill('SIGKILL');
467
485
  }
@@ -540,12 +558,14 @@ class TerminalManager {
540
558
  return;
541
559
  this.detachFromRegistry(s);
542
560
  if (!s.exited) {
561
+ killTerminalTree(s.pty.pid);
543
562
  try {
544
563
  s.pty.kill('SIGTERM');
545
564
  }
546
565
  catch { /* ignore */ }
547
566
  s.killTimer = setTimeout(() => {
548
567
  if (!s.exited) {
568
+ killTerminalTree(s.pty.pid);
549
569
  try {
550
570
  s.pty.kill('SIGKILL');
551
571
  }