fix: stabilize release terminal e2e (#5869)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo Hong 2026-06-19 19:43:42 -07:00 committed by GitHub
parent 230297b430
commit 3478efca08
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 89 additions and 22 deletions

View File

@ -1,6 +1,5 @@
import { useEffect, useId, useRef, useState } from 'react'
import { useEffect, useId, useMemo, useRef, useState } from 'react'
import { RefreshCw } from 'lucide-react'
import { useShallow } from 'zustand/react/shallow'
import { Button } from '@/components/ui/button'
import { useAppStore } from '../store'
import { translate } from '@/i18n/i18n'
@ -73,20 +72,21 @@ export default function CodexRestartChip({
isVisible?: boolean
worktreeId: string
}): React.JSX.Element | null {
const { staleWorktreePtyIds, restartNotice } = useAppStore(
useShallow((s) => {
const stalePtyIds = collectStalePtyIdsForTabs({
tabs: s.tabsByWorktree[worktreeId] ?? EMPTY_TABS,
ptyIdsByTabId: s.ptyIdsByTabId,
codexRestartNoticeByPtyId: s.codexRestartNoticeByPtyId
})
const firstStalePtyId = stalePtyIds[0]
return {
staleWorktreePtyIds: stalePtyIds,
restartNotice: firstStalePtyId ? s.codexRestartNoticeByPtyId[firstStalePtyId] : undefined
}
})
const tabs = useAppStore((s) => s.tabsByWorktree[worktreeId] ?? EMPTY_TABS)
const ptyIdsByTabId = useAppStore((s) => s.ptyIdsByTabId)
const codexRestartNoticeByPtyId = useAppStore((s) => s.codexRestartNoticeByPtyId)
const staleWorktreePtyIds = useMemo(
() =>
collectStalePtyIdsForTabs({
tabs,
ptyIdsByTabId,
codexRestartNoticeByPtyId
}),
[codexRestartNoticeByPtyId, ptyIdsByTabId, tabs]
)
const restartNotice = staleWorktreePtyIds[0]
? codexRestartNoticeByPtyId[staleWorktreePtyIds[0]]
: undefined
const queueCodexPaneRestarts = useAppStore((s) => s.queueCodexPaneRestarts)
const clearCodexRestartNotice = useAppStore((s) => s.clearCodexRestartNotice)
@ -96,10 +96,11 @@ export default function CodexRestartChip({
)
const currentCollapseState = getCodexRestartOverlayCollapseState(collapseState, noticeKey)
// Why: a new account switch must reopen loud mode even if the prior notice was collapsed.
if (currentCollapseState !== collapseState) {
setCollapseState(currentCollapseState)
}
useEffect(() => {
// Why: a new account switch must reopen loud mode without updating state
// during render, which can trip React's external-store snapshot guard.
setCollapseState((state) => getCodexRestartOverlayCollapseState(state, noticeKey))
}, [noticeKey])
if (staleWorktreePtyIds.length === 0 || !restartNotice) {
return null

View File

@ -1,10 +1,33 @@
import { describe, expect, it, vi } from 'vitest'
import {
// @vitest-environment happy-dom
import React, { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useAppStore } from '../store'
import CodexRestartChip, {
collectStalePtyIdsForTabs,
collectStaleWorktreePtyIds,
dismissStaleWorktreePtyIds
} from './CodexRestartChip'
let container: HTMLDivElement
let root: Root
beforeEach(() => {
useAppStore.setState(useAppStore.getInitialState(), true)
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
})
afterEach(() => {
act(() => {
root.unmount()
})
container.remove()
useAppStore.setState(useAppStore.getInitialState(), true)
})
describe('CodexRestartChip helpers', () => {
it('collects all stale PTY ids for tabs in a worktree', () => {
expect(
@ -68,4 +91,38 @@ describe('CodexRestartChip helpers', () => {
expect(clearCodexRestartNotice).toHaveBeenNthCalledWith(2, 'pty-3')
expect(clearCodexRestartNotice).toHaveBeenCalledTimes(2)
})
it('renders a stale Codex restart notice without an external-store update loop', async () => {
useAppStore.setState({
tabsByWorktree: {
'worktree-1': [
{
id: 'tab-1',
worktreeId: 'worktree-1',
title: 'Terminal',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1,
ptyId: null
}
]
},
ptyIdsByTabId: {
'tab-1': ['pty-1']
},
codexRestartNoticeByPtyId: {
'pty-1': {
previousAccountLabel: 'old@example.com',
nextAccountLabel: 'new@example.com'
}
}
})
await act(async () => {
root.render(React.createElement(CodexRestartChip, { worktreeId: 'worktree-1' }))
})
expect(container.textContent).toContain('Codex is still signed in as old@example.com')
})
})

View File

@ -25,7 +25,16 @@ export type ViewportSize = {
}
function isCursorProbePixel(red: number, green: number, blue: number, alpha: number): boolean {
return alpha > 240 && Math.abs(red - 35) <= 2 && green >= 253 && Math.abs(blue - 69) <= 2
if (alpha <= 240) {
return false
}
const isRawProbeColor = Math.abs(red - 35) <= 2 && green >= 253 && Math.abs(blue - 69) <= 2
// Why: WebGL can composite the forced cursor color over the terminal
// background before Playwright captures pixels, especially on macOS.
const isCompositedProbeColor =
Math.abs(red - 121) <= 3 && green >= 249 && Math.abs(blue - 100) <= 3
return isRawProbeColor || isCompositedProbeColor
}
export function analyzeRasterCursorCells(