From 5412276776fbcfe3cfefa7ca44d7c10e0fbde1dd Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Fri, 4 Sep 2026 15:09:15 -0400 Subject: [PATCH] Fix status bar runtime target render loop (#18685) --- ...rtsStatusSegment.render-stability.test.tsx | 89 +++++++++++++++++++ .../runtime/use-worktree-runtime-target.ts | 6 +- 2 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 src/renderer/src/components/status-bar/PortsStatusSegment.render-stability.test.tsx diff --git a/src/renderer/src/components/status-bar/PortsStatusSegment.render-stability.test.tsx b/src/renderer/src/components/status-bar/PortsStatusSegment.render-stability.test.tsx new file mode 100644 index 00000000000..d6d4ad95184 --- /dev/null +++ b/src/renderer/src/components/status-bar/PortsStatusSegment.render-stability.test.tsx @@ -0,0 +1,89 @@ +// @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' + +vi.mock('@/components/ui/popover', () => ({ + Popover: ({ children }: { children: React.ReactNode }) => <>{children}, + PopoverContent: ({ children }: { children: React.ReactNode }) => <>{children}, + PopoverTrigger: ({ children }: { children: React.ReactNode }) => <>{children} +})) + +vi.mock('@/components/ui/tooltip', () => ({ + Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}, + TooltipContent: ({ children }: { children: React.ReactNode }) => <>{children}, + TooltipTrigger: ({ children }: { children: React.ReactNode }) => <>{children} +})) + +vi.mock('@/components/SelectedTextCopyMenu', () => ({ + SelectedTextCopyMenu: ({ children }: { children: React.ReactNode }) => <>{children} +})) + +vi.mock('./ports-status-popover-rows', () => ({ + PortRow: () =>
, + WorkspaceGroupRows: () =>
+})) + +vi.mock('@/lib/react-error-boundary-reporting', () => ({ + reportReactErrorBoundaryCrash: vi.fn() +})) + +vi.mock('@/i18n/i18n', () => ({ + translate: (_key: string, fallback: string, options?: Record) => + options + ? fallback.replace(/{{(\w+)}}/g, (_match, name: string) => String(options[name] ?? '')) + : fallback +})) + +import { RecoverableRenderErrorBoundary } from '../error-boundaries/RecoverableRenderErrorBoundary' +import { useAppStore } from '@/store' +import { PortsStatusSegment } from './PortsStatusSegment' + +describe('PortsStatusSegment render stability', () => { + let container: HTMLDivElement + let root: Root + let consoleError: ReturnType + + beforeEach(() => { + useAppStore.setState(useAppStore.getInitialState(), true) + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined) + }) + + afterEach(() => { + act(() => root.unmount()) + container.remove() + consoleError.mockRestore() + }) + + it('keeps the status controls mounted when the runtime owner is local', () => { + act(() => { + root.render( + + + + ) + }) + + const underlyingException = consoleError.mock.calls.find( + ([message, error]) => + message === '[oracle.status-bar] render crash contained by boundary' && + error instanceof Error && + error.message.includes('Maximum update depth exceeded') + ) + + expect.soft(container.textContent).not.toContain('The status bar hit an error.') + expect.soft(underlyingException).toBeUndefined() + expect(container.querySelector('button[aria-label^="Ports, 0 workspace"]')).not.toBeNull() + }) +}) diff --git a/src/renderer/src/runtime/use-worktree-runtime-target.ts b/src/renderer/src/runtime/use-worktree-runtime-target.ts index 25bd9099e90..22ef4ed5c4b 100644 --- a/src/renderer/src/runtime/use-worktree-runtime-target.ts +++ b/src/renderer/src/runtime/use-worktree-runtime-target.ts @@ -1,3 +1,4 @@ +import { useMemo } from 'react' import { useAppStore } from '@/store' import { getExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner' import { runtimeTargetForExecutionHostId, type RuntimeClientTarget } from './runtime-client-target' @@ -10,7 +11,6 @@ import { runtimeTargetForExecutionHostId, type RuntimeClientTarget } from './run export function useWorktreeRuntimeTarget( worktreeId: string | null | undefined ): RuntimeClientTarget | null { - return useAppStore((state) => - runtimeTargetForExecutionHostId(getExecutionHostIdForWorktree(state, worktreeId)) - ) + const executionHostId = useAppStore((state) => getExecutionHostIdForWorktree(state, worktreeId)) + return useMemo(() => runtimeTargetForExecutionHostId(executionHostId), [executionHostId]) }