From 7bb2d65529eedfa90ab67dfc506526b39eb808a8 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 13 Aug 2026 02:02:16 -0700 Subject: [PATCH] fix(terminal): migrate hidden delivery gate default --- src/main/ipc/pty-hidden-delivery-gate.ts | 3 +- src/main/persistence.test.ts | 37 ++++++++++++++++++++++++ src/main/persistence.ts | 11 +++++++ src/shared/constants.test.ts | 5 ++++ src/shared/constants.ts | 12 ++------ src/shared/types.ts | 2 ++ 6 files changed, 58 insertions(+), 12 deletions(-) diff --git a/src/main/ipc/pty-hidden-delivery-gate.ts b/src/main/ipc/pty-hidden-delivery-gate.ts index 43fa8567b88..0fa58e6f909 100644 --- a/src/main/ipc/pty-hidden-delivery-gate.ts +++ b/src/main/ipc/pty-hidden-delivery-gate.ts @@ -31,8 +31,7 @@ const droppedSinceHiddenPtys = new Set() let droppedHiddenDeliveryChars = 0 let droppedHiddenDeliveryChunks = 0 -/** Gate kill switches, both read main-side: the gate only operates under main - * side-effect authority AND the gate-specific setting (both default on). */ +/** The gate operates only when main side-effect authority and the gate setting are enabled. */ export function isHiddenPtyDeliveryGateEnabled( settings: HiddenPtyDeliveryGateSettings | null | undefined ): boolean { diff --git a/src/main/persistence.test.ts b/src/main/persistence.test.ts index 17a73fa0e48..96f697c976d 100644 --- a/src/main/persistence.test.ts +++ b/src/main/persistence.test.ts @@ -7933,6 +7933,43 @@ describe('Store', () => { expect(store.getSettings().experimentalActivity).toBe(true) }) + it('disables the generated hidden PTY delivery gate for existing profiles', async () => { + writeDataFile({ + schemaVersion: 1, + repos: [], + worktreeMeta: {}, + settings: { terminalHiddenDeliveryGate: true }, + ui: {}, + githubCache: { pr: {}, issue: {} }, + workspaceSession: {} + }) + + const store = await createStore() + + expect(store.getSettings().terminalHiddenDeliveryGate).toBe(false) + expect(store.getSettings().terminalHiddenDeliveryGateDefaultedOffForAllUsers).toBe(true) + }) + + it('preserves hidden PTY delivery gate opt-ins after the default-off migration', async () => { + writeDataFile({ + schemaVersion: 1, + repos: [], + worktreeMeta: {}, + settings: { + terminalHiddenDeliveryGate: true, + terminalHiddenDeliveryGateDefaultedOffForAllUsers: true + }, + ui: {}, + githubCache: { pr: {}, issue: {} }, + workspaceSession: {} + }) + + const store = await createStore() + + expect(store.getSettings().terminalHiddenDeliveryGate).toBe(true) + expect(store.getSettings().terminalHiddenDeliveryGateDefaultedOffForAllUsers).toBe(true) + }) + // ── worktree-card property migration ─────────────────────────────── it('adds split-out default card properties for legacy detailed profiles', async () => { diff --git a/src/main/persistence.ts b/src/main/persistence.ts index f9fee511d39..975d558cec8 100644 --- a/src/main/persistence.ts +++ b/src/main/persistence.ts @@ -3290,6 +3290,15 @@ export class Store { const migratedExperimentalActivity = experimentalActivityDefaultedOffForAllUsers ? (parsed.settings?.experimentalActivity ?? false) : false + const terminalHiddenDeliveryGateDefaultedOffForAllUsers = + parsed.settings?.terminalHiddenDeliveryGateDefaultedOffForAllUsers === true + // Why: the old generated true is indistinguishable from an opt-in, so reset it once before preserving later overrides. + const migratedTerminalHiddenDeliveryGate = terminalHiddenDeliveryGateDefaultedOffForAllUsers + ? (parsed.settings?.terminalHiddenDeliveryGate ?? false) + : false + if (!terminalHiddenDeliveryGateDefaultedOffForAllUsers) { + this.loadNeedsSave = true + } const autoRenameBranchFromWorkDefaultedOn = parsed.settings?.autoRenameBranchFromWorkDefaultedOn === true // Why: default-on rollout activates old profiles once, but a later Settings opt-out survives reloads. @@ -3503,6 +3512,8 @@ export class Store { ...migratedTerminalTuiScrollSensitivity.settings, experimentalActivity: migratedExperimentalActivity, experimentalActivityDefaultedOffForAllUsers: true, + terminalHiddenDeliveryGate: migratedTerminalHiddenDeliveryGate, + terminalHiddenDeliveryGateDefaultedOffForAllUsers: true, // Why: compact worktree cards graduated from Experimental; preserve the old opt-in for rollout-era profiles. compactWorktreeCards: loadedCompactWorktreeCards, experimentalCompactWorktreeCards: undefined, diff --git a/src/shared/constants.test.ts b/src/shared/constants.test.ts index ea41c8a6af9..bd3e799793e 100644 --- a/src/shared/constants.test.ts +++ b/src/shared/constants.test.ts @@ -42,6 +42,11 @@ describe('getDefaultSettings', () => { expect(getDefaultSettings('/tmp').terminalCursorStyleDefaultedToBlock).toBe(true) }) + it('keeps hidden PTY bytes flowing to the renderer by default', () => { + expect(getDefaultSettings('/tmp').terminalHiddenDeliveryGate).toBe(false) + expect(getDefaultSettings('/tmp').terminalHiddenDeliveryGateDefaultedOffForAllUsers).toBe(true) + }) + it('allows OSC 52 clipboard writes by default for new settings', () => { expect(getDefaultSettings('/tmp').terminalAllowOsc52Clipboard).toBe(true) expect(getDefaultSettings('/tmp').terminalAllowOsc52ClipboardDefaultedOnForAllUsers).toBe(true) diff --git a/src/shared/constants.ts b/src/shared/constants.ts index ec0f8358005..95d85109557 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -311,17 +311,9 @@ export function getDefaultSettings(homedir: string): GlobalSettings { terminalHiddenWorktreeRetentionBudget: true, browserGuestWorktreeRetentionBudget: true, terminalMainSideEffectAuthority: true, - // Why default off: dropping renderer-bound bytes leaves the renderer's xterm - // a second emulator that has seen a different stream than the model, and a - // gap can strand any carried state — a swallowed `ESC[22m` leaves bold on - // every later cell, a swallowed `ESC(B` leaves line-drawing on (STA-4042). - // Every recovery path then has to reconstruct state it cannot actually know. - // Feeding the emulator unconditionally and suspending only the *rendering* - // (pane-manager suspendRendering + WebGL retention already do this) removes - // the gap class outright rather than repairing it after the fact. The gate - // stays available for anyone who needs to trade that correctness for the - // hidden-pane IPC/parse saving. + // Why: keep hidden-pane xterm state synchronized while rendering remains suspended. terminalHiddenDeliveryGate: false, + terminalHiddenDeliveryGateDefaultedOffForAllUsers: true, terminalModelQueryAuthority: true, defaultTuiAgent: null, disabledTuiAgents: [...DEFAULT_DISABLED_TUI_AGENTS], diff --git a/src/shared/types.ts b/src/shared/types.ts index 4d9a2e9b810..29ad921082f 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -3009,6 +3009,8 @@ export type GlobalSettings = { terminalMainSideEffectAuthority?: boolean /** Kill switch for main's hidden-delivery gate (Phase 4): drops PTY bytes to hidden views after model ingestion; requires terminalMainSideEffectAuthority. */ terminalHiddenDeliveryGate?: boolean + /** One-shot guard for defaulting the hidden-delivery gate off for existing profiles. */ + terminalHiddenDeliveryGateDefaultedOffForAllUsers?: boolean /** Kill switch for main's model query responder (Phase 5); active only when both Phase-4 gates are also on. */ terminalModelQueryAuthority?: boolean /** Which agent to pre-select in the new-workspace composer.