diff --git a/src/main/ipc/pty-management.test.ts b/src/main/ipc/pty-management.test.ts index cb4783c1ec9..d1c2689000b 100644 --- a/src/main/ipc/pty-management.test.ts +++ b/src/main/ipc/pty-management.test.ts @@ -239,35 +239,6 @@ describe('pty:management IPC handlers', () => { }) }) - describe('sessionPersistence', () => { - async function readAvailability(): Promise { - const { registerDaemonManagementHandlers } = await importFresh() - registerDaemonManagementHandlers() - const result = (await buildHandlerMap()['pty:management:sessionPersistence']({})) as { - available: boolean - } - return result.available - } - - it('reports preservation while the daemon owns terminals', async () => { - getDaemonProviderMock.mockReturnValue(await makeRouter(makeAdapter(5, []))) - - await expect(readAvailability()).resolves.toBe(true) - }) - - it('reports no preservation when terminals fell back to local PTYs', async () => { - getDaemonProviderMock.mockReturnValue(null) - - await expect(readAvailability()).resolves.toBe(false) - }) - - it('reports no preservation while fresh spawns bypass the daemon', async () => { - getDaemonProviderMock.mockReturnValue(await makeDegradedProvider(makeAdapter(5, []))) - - await expect(readAvailability()).resolves.toBe(false) - }) - }) - describe('killAll', () => { // Why: the handler sleeps POLL_INTERVAL_MS between listSessions polls. // Fake timers let the tests drive that loop deterministically — without diff --git a/src/main/ipc/pty-management.ts b/src/main/ipc/pty-management.ts index 2e46224ac07..6260b8454af 100644 --- a/src/main/ipc/pty-management.ts +++ b/src/main/ipc/pty-management.ts @@ -38,11 +38,6 @@ function isDaemonDegraded(): boolean { ) } -// Why: no daemon provider means terminals run on fallback local PTYs that die with the app, so nothing survives a quit-and-install. -function isSessionPersistenceAvailable(): boolean { - return getDaemonProvider() !== null && !isDaemonDegraded() -} - async function collectSessions(adapters: DaemonPtyAdapter[]): Promise { const results = await Promise.allSettled( adapters.map(async (adapter) => { @@ -62,13 +57,6 @@ export function registerDaemonManagementHandlers(): void { ipcMain.removeHandler('pty:management:killOne') ipcMain.removeHandler('pty:management:restart') ipcMain.removeHandler('pty:management:macTccAttribution') - ipcMain.removeHandler('pty:management:sessionPersistence') - - // Why: lets the update card stop promising sessions survive an install it has no mechanism to keep them through. - ipcMain.handle( - 'pty:management:sessionPersistence', - async (): Promise<{ available: boolean }> => ({ available: isSessionPersistenceAvailable() }) - ) // Why: lets Settings warn that macOS privacy grants no longer reach daemon terminals (STA-3491). ipcMain.handle( diff --git a/src/preload/api/pty-management-api.ts b/src/preload/api/pty-management-api.ts index e704a8eefa5..10fef2d2303 100644 --- a/src/preload/api/pty-management-api.ts +++ b/src/preload/api/pty-management-api.ts @@ -27,6 +27,4 @@ export type PtyManagementApi = { killOne: (args: { sessionId: string }) => Promise<{ success: boolean }> restart: () => Promise<{ success: boolean }> macTccAttribution: () => Promise<{ health: PtyManagementMacTccAttributionHealth }> - // `false`: terminals run on local PTYs that die with the app, so no session survives quit/update. `null`: unverifiable here. - sessionPersistence: () => Promise<{ available: boolean | null }> } diff --git a/src/preload/index.ts b/src/preload/index.ts index d1dc3bcba6a..b8a00c6d647 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1347,8 +1347,7 @@ const api = { killAll: () => ipcRenderer.invoke('pty:management:killAll'), killOne: (args: { sessionId: string }) => ipcRenderer.invoke('pty:management:killOne', args), restart: () => ipcRenderer.invoke('pty:management:restart'), - macTccAttribution: () => ipcRenderer.invoke('pty:management:macTccAttribution'), - sessionPersistence: () => ipcRenderer.invoke('pty:management:sessionPersistence') + macTccAttribution: () => ipcRenderer.invoke('pty:management:macTccAttribution') } }, diff --git a/src/renderer/src/components/UpdateCard.error-card.test.tsx b/src/renderer/src/components/UpdateCard.error-card.test.tsx index 4cb297b7ca4..935c8a5e81b 100644 --- a/src/renderer/src/components/UpdateCard.error-card.test.tsx +++ b/src/renderer/src/components/UpdateCard.error-card.test.tsx @@ -23,7 +23,7 @@ const PACKAGE_RECOVERY: LinuxPackageInstallRecovery = { version: '1.4.200' } -function renderAfterAvailableStatus(): RenderResult { +function renderAfterAvailableStatus(updateReassuranceSeen = true): RenderResult { useAppStore.setState({ updateStatus: { state: 'available', @@ -33,7 +33,7 @@ function renderAfterAvailableStatus(): RenderResult { updateChangelog: null, dismissedUpdateVersion: null, updateCardCollapsed: false, - updateReassuranceSeen: true + updateReassuranceSeen }) return render() } @@ -90,6 +90,16 @@ afterEach(() => { useAppStore.setState(useAppStore.getInitialState(), true) }) +describe('UpdateCard available copy', () => { + it('makes no promise about terminal sessions', () => { + renderAfterAvailableStatus(false) + + expect(screen.getByText('Update Available')).toBeTruthy() + expect(screen.queryByText(/terminal sessions/i)).toBeNull() + expect(screen.queryByText(/sessions won't be interrupted/i)).toBeNull() + }) +}) + describe('UpdateCard Windows signature failures', () => { it('does not offer the rejected version as a manual publisher-check bypass', () => { const message = diff --git a/src/renderer/src/components/UpdateCard.session-persistence.test.tsx b/src/renderer/src/components/UpdateCard.session-persistence.test.tsx deleted file mode 100644 index 3df0924c7f7..00000000000 --- a/src/renderer/src/components/UpdateCard.session-persistence.test.tsx +++ /dev/null @@ -1,101 +0,0 @@ -// @vitest-environment happy-dom -import { cleanup, render, screen, waitFor } from '@testing-library/react' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { useAppStore } from '../store' -import { UpdateCard } from './UpdateCard' - -const sessionPersistence = vi.fn() - -function mockApi(): void { - Object.defineProperty(window, 'api', { - configurable: true, - value: { - app: { relaunch: vi.fn() }, - settings: { set: vi.fn().mockResolvedValue(undefined) }, - shell: { openUrl: vi.fn() }, - ui: { set: vi.fn().mockResolvedValue(undefined), writeClipboardText: vi.fn() }, - pty: { management: { sessionPersistence } }, - updater: { - check: vi.fn(), - dismissNudge: vi.fn(), - dismissAvailableUpdate: vi.fn().mockResolvedValue(undefined), - download: vi.fn(), - getLinuxPackageInstallInstructions: vi.fn().mockResolvedValue({ ok: false }), - showLinuxPackage: vi.fn(), - quitAndInstall: vi.fn().mockResolvedValue(undefined) - } - } - }) -} - -function renderAvailableUpdate(): void { - useAppStore.setState({ - updateStatus: { state: 'available', version: '1.4.200', changelog: null }, - updateChangelog: null, - dismissedUpdateVersion: null, - updateCardCollapsed: false, - updateReassuranceSeen: false - }) - render() -} - -beforeEach(() => { - useAppStore.setState(useAppStore.getInitialState(), true) - sessionPersistence.mockReset() - mockApi() - Object.defineProperty(window, 'matchMedia', { - configurable: true, - value: vi.fn().mockReturnValue({ - matches: false, - addEventListener: vi.fn(), - removeEventListener: vi.fn() - }) - }) -}) - -afterEach(() => { - cleanup() - useAppStore.setState(useAppStore.getInitialState(), true) -}) - -describe('UpdateCard session-preservation promise', () => { - it('drops the no-interruption promise when terminals run on fallback local PTYs', async () => { - sessionPersistence.mockResolvedValue({ available: false }) - renderAvailableUpdate() - - await waitFor(() => expect(screen.getAllByText(/may not be preserved/).length).toBe(2)) - expect(screen.queryByText(/won't be interrupted/)).toBeNull() - expect( - screen.getByText('Terminal sessions may not be preserved during this update.') - ).toBeTruthy() - expect(screen.getByText('Sessions may not be preserved.')).toBeTruthy() - }) - - it('keeps the promise when the daemon owns the sessions', async () => { - sessionPersistence.mockResolvedValue({ available: true }) - renderAvailableUpdate() - - await waitFor(() => expect(screen.getByText("Sessions won't be interrupted.")).toBeTruthy()) - expect( - screen.getByText("Your terminal sessions won't be interrupted during the update.") - ).toBeTruthy() - }) - - it('claims nothing while persistence is still unknown', async () => { - sessionPersistence.mockReturnValue(new Promise(() => {})) - renderAvailableUpdate() - - await waitFor(() => expect(screen.getByText('Update Available')).toBeTruthy()) - expect(screen.queryByText(/interrupted/)).toBeNull() - expect(screen.queryByText(/preserved/)).toBeNull() - }) - - it('claims nothing when the persistence probe fails', async () => { - sessionPersistence.mockRejectedValue(new Error('ipc unavailable')) - renderAvailableUpdate() - - await waitFor(() => expect(screen.getByText('Update Available')).toBeTruthy()) - expect(screen.queryByText(/interrupted/)).toBeNull() - expect(screen.queryByText(/preserved/)).toBeNull() - }) -}) diff --git a/src/renderer/src/components/UpdateCard.tsx b/src/renderer/src/components/UpdateCard.tsx index 6ab94e149d9..d48deb9a9ad 100644 --- a/src/renderer/src/components/UpdateCard.tsx +++ b/src/renderer/src/components/UpdateCard.tsx @@ -1,7 +1,6 @@ /* eslint-disable max-lines -- Why: keeps the updater state machine and its presentation variants in one file. */ import { useCallback, useEffect, useRef, useState } from 'react' import { usePrefersReducedMotion } from '@/hooks/usePrefersReducedMotion' -import { useTerminalSessionPersistence } from '@/hooks/useTerminalSessionPersistence' import { useAppStore } from '../store' import { Card } from './ui/card' import { Button } from './ui/button' @@ -88,8 +87,6 @@ export function UpdateCard() { const dismissUpdate = useAppStore((s) => s.dismissUpdate) const collapsed = useAppStore((s) => s.updateCardCollapsed) const setCollapsed = useAppStore((s) => s.setUpdateCardCollapsed) - const reassuranceSeen = useAppStore((s) => s.updateReassuranceSeen) - const markReassuranceSeen = useAppStore((s) => s.markUpdateReassuranceSeen) const hasStartedDownload = useRef(false) const dismissAnimationTimerRef = useRef(null) const collapseAnimationTimerRef = useRef(null) @@ -170,11 +167,6 @@ export function UpdateCard() { // ── Prefers-reduced-motion ────────────────────────────────────────── const prefersReducedMotion = usePrefersReducedMotion() - // Why: the card must not promise sessions survive the install unless the daemon that preserves them is serving this app. - const sessionsPreserved = useTerminalSessionPersistence( - status.state === 'available' || status.state === 'downloading' - ) - const clearAnimationTimers = useCallback(() => { if (dismissAnimationTimerRef.current !== null) { window.clearTimeout(dismissAnimationTimerRef.current) @@ -250,10 +242,6 @@ export function UpdateCard() { const handleUpdate = () => { hasStartedDownload.current = true - // Why: clicking Update implies the user isn't worried about interruption, so retire the reassurance tip. - if (!reassuranceSeen) { - markReassuranceSeen() - } void window.api.updater.download() } @@ -603,53 +591,18 @@ export function UpdateCard() { ) })() - // One-time tip about what the update does to running terminals; persisted once seen, and silent while persistence is unknown. - const showReassurance = - !reassuranceSeen && - sessionsPreserved !== null && - (status.state === 'available' || status.state === 'downloading') - return (
- {showReassurance && ( - -
-
-

- {sessionsPreserved - ? translate( - 'auto.components.UpdateCard.b1d867f4fb', - "Your terminal sessions won't be interrupted during the update." - ) - : translate( - 'auto.components.UpdateCard.6bcb39fd76', - 'Terminal sessions may not be preserved during this update.' - )} -

-
- -
-
- )} void onClose: () => void }) { @@ -799,14 +749,6 @@ function SimpleCardContent({ })}

- {sessionsPreserved !== null && ( -

- {sessionsPreserved - ? translate('auto.components.UpdateCard.fdd4a364fa', "Sessions won't be interrupted.") - : translate('auto.components.UpdateCard.6930f48207', 'Sessions may not be preserved.')} -

- )} - {releaseUrl && (