mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
refactor(renderer): make memo invalidators and layout flushes compiler-safe (#39414)
React Compiler drops bare `void x;` reads, narrows callback deps to the fields read, and oxc 0.152 emits `a[q.head++]` as a prefix update. Rewrite those sites so they behave the same with the compiler on, and identically to today with it off: - `void tick` memo invalidators -> `recomputeOn([tick], () => ...)` - `void el.offsetWidth` transition resets -> `forceLayoutReflow(el)` - pty termination queue reads, then advances, the head index - host agent preference subscription takes the whole subscription object Prunes the compiler-compat hazard baseline from 40 to 2 entries. Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -605,7 +605,6 @@
|
||||
1 src/renderer/src/components/task-page/github/use-task-page-github-quiet-refresh-effect.ts
|
||||
5 src/renderer/src/components/task-page/github/use-task-page-github-quiet-refresh.ts
|
||||
3 src/renderer/src/components/task-page/github/use-task-page-github-search-pagination.ts
|
||||
3 src/renderer/src/components/task-page/github/use-task-page-pr-check-hydration.ts
|
||||
4 src/renderer/src/components/task-page/github/use-task-page-work-items-cache-selection.ts
|
||||
1 src/renderer/src/components/task-page/gitlab/use-task-page-gitlab-loading.ts
|
||||
2 src/renderer/src/components/task-page/jira/task-page-jira-board-view.tsx
|
||||
|
||||
@@ -1,23 +1,4 @@
|
||||
# Grandfathered code React Compiler would run differently (<file>#<function>:<hazard>).
|
||||
# Generated; never hand-edit. Lower after a fix: pnpm check:react-compiler-compat-ratchet --prune
|
||||
2 src/renderer/src/components/LinearIssueMarkdownDescriptionEditor.tsx#LinearIssueMarkdownDescriptionEditor:void-read
|
||||
1 src/renderer/src/components/browser-pane/annotate/use-browser-page-annotation-send.ts#useBrowserPageAnnotationSend:void-read
|
||||
1 src/renderer/src/components/browser-pane/host-guest/browser-page-mount-admission.ts#useAnyBrowserPageMountAdmission:void-read
|
||||
1 src/renderer/src/components/editor/review-note-delivery/NotesSendMenu.tsx#NotesSendMenu:void-read
|
||||
1 src/renderer/src/components/feature-wall/ReviewNotesAnimatedVisual.tsx#ReviewNotesAnimatedVisual:void-read
|
||||
1 src/renderer/src/components/feature-wall/ReviewShipAnimatedVisual.tsx#ReviewShipAnimatedVisual:void-read
|
||||
1 src/renderer/src/components/feature-wall/agents-orchestration/OrchestrationPage.tsx#OrchestrationPage:void-read
|
||||
1 src/renderer/src/components/feature-wall/review-pr-view-animation.ts#useReviewPrViewAnimation:void-read
|
||||
1 src/renderer/src/components/feature-wall/use-editor-animated-visual-animation.ts#useEditorAnimatedVisualAnimation:void-read
|
||||
1 src/renderer/src/components/mobile/PhoneCarousel.tsx#PhoneCarousel:void-read
|
||||
1 src/renderer/src/components/native-chat/use-native-chat-can-send.ts#useNativeChatCanSend:void-read
|
||||
2 src/renderer/src/components/sidebar/workspace-card/activity/WorktreeCardAgents.tsx#WorktreeCardAgentsBody:void-read
|
||||
2 src/renderer/src/components/sidebar/workspace-creation/folder/folder-workspace-composer-path-status.ts#useFolderWorkspaceComposerPathStatus:void-read
|
||||
1 src/renderer/src/components/sidebar/worktree-list/listing/use-agent-send-target.ts#useAgentSendTargetWorktreeId:void-read
|
||||
2 src/renderer/src/components/sidebar/worktree-list/listing/use-folder-path-statuses.ts#useFolderWorkspacePathStatusRows:void-read
|
||||
1 src/renderer/src/components/sidebar/worktree-list/listing/use-sleeping-sweep-retention.ts#useSleepingSweepRetention:void-read
|
||||
1 src/renderer/src/components/status-bar/use-resource-pty-actions.ts#useResourcePtyActions:member-postfix-value
|
||||
6 src/renderer/src/components/task-page/github/use-task-page-pr-check-hydration.ts#useTaskPagePRCheckHydration:void-read
|
||||
11 src/renderer/src/components/worktree-jump/use-worktree-jump-palette-quick-actions.ts#useWorktreeJumpPaletteQuickActions:void-read
|
||||
1 src/renderer/src/hooks/use-palette-search-evaluation-context.ts#usePaletteSearchEvaluationContext:void-read
|
||||
1 src/renderer/src/hooks/useGitHubTaskItems.ts#useGitHubTaskProjectTable:void-read
|
||||
|
||||
@@ -12,8 +12,8 @@ const OPT_OUT_DIRECTIVES = new Set(['use no memo', 'use no forget'])
|
||||
export const HAZARD_FIXES = {
|
||||
'void-read': [
|
||||
'React Compiler drops a bare `void x;` read, so it neither invalidates a memo nor forces layout.',
|
||||
'Make the value a real input instead (pass it to the code that needs it, or wrap the read in a',
|
||||
'helper call), and flush layout with a function call that returns the measured value.'
|
||||
'Memo invalidator: `useMemo(() => recomputeOn([tick], () => compute()), [tick])` from',
|
||||
'@/lib/recompute-on. Layout flush: `forceLayoutReflow(el)` from @/lib/force-layout-reflow.'
|
||||
],
|
||||
'member-postfix-value': [
|
||||
'oxc React Compiler 0.152 compiles a member postfix update used as a value (`a[q.head++]`) as',
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
getRichMarkdownSpellcheckAttribute,
|
||||
useRichMarkdownSpellcheckAttribute
|
||||
} from '@/components/editor/rich-markdown/instance/rich-markdown-spellcheck'
|
||||
import { recomputeOn } from '@/lib/recompute-on'
|
||||
|
||||
type LinearIssueMarkdownDescriptionEditorProps = {
|
||||
value: string
|
||||
@@ -64,15 +65,15 @@ export function LinearIssueMarkdownDescriptionEditor({
|
||||
(s) => s.settings?.richMarkdownSpellcheckEnabled ?? true
|
||||
)
|
||||
// Recreate the private Marked registry with Tiptap on language changes to avoid accumulating tokenizers.
|
||||
const codec = useMemo(() => {
|
||||
void language
|
||||
return createRichMarkdownEditorCodec()
|
||||
}, [language])
|
||||
const linearIssueMarkdownExtensions = useMemo(() => {
|
||||
// Language keys editor recreation because Tiptap freezes translated extension options.
|
||||
void language
|
||||
return createLinearIssueMarkdownExtensions(codec)
|
||||
}, [codec, language])
|
||||
const codec = useMemo(
|
||||
() => recomputeOn([language], () => createRichMarkdownEditorCodec()),
|
||||
[language]
|
||||
)
|
||||
// Language keys editor recreation because Tiptap freezes translated extension options.
|
||||
const linearIssueMarkdownExtensions = useMemo(
|
||||
() => recomputeOn([language], () => createLinearIssueMarkdownExtensions(codec)),
|
||||
[codec, language]
|
||||
)
|
||||
|
||||
const editor = useEditor(
|
||||
{
|
||||
|
||||
+8
-4
@@ -12,6 +12,7 @@ import {
|
||||
isNoteInFlight,
|
||||
useNotesInFlightVersion
|
||||
} from '@/lib/notes-send-in-flight'
|
||||
import { recomputeOn } from '@/lib/recompute-on'
|
||||
|
||||
export function useBrowserPageAnnotationSend({
|
||||
browserTabId,
|
||||
@@ -59,10 +60,13 @@ export function useBrowserPageAnnotationSend({
|
||||
)
|
||||
// Annotations another send holds are left out of the next one.
|
||||
const inFlightVersion = useNotesInFlightVersion()
|
||||
const sendableAnnotations = useMemo(() => {
|
||||
void inFlightVersion
|
||||
return browserAnnotations.filter((annotation) => !isNoteInFlight(annotation))
|
||||
}, [browserAnnotations, inFlightVersion])
|
||||
const sendableAnnotations = useMemo(
|
||||
() =>
|
||||
recomputeOn([inFlightVersion], () =>
|
||||
browserAnnotations.filter((annotation) => !isNoteInFlight(annotation))
|
||||
),
|
||||
[browserAnnotations, inFlightVersion]
|
||||
)
|
||||
const browserAnnotationsPrompt = useMemo(
|
||||
() =>
|
||||
formatBrowserAnnotationsAsMarkdown(sendableAnnotations, {
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
useNotesInFlightVersion
|
||||
} from '@/lib/notes-send-in-flight'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { recomputeOn } from '@/lib/recompute-on'
|
||||
|
||||
function enabledSendTooltip(): string {
|
||||
return translate(
|
||||
@@ -99,15 +100,18 @@ export function NotesSendMenu<TNote extends DiffCommentDeliverySnapshot>({
|
||||
const targetModeId = useMemo(() => buildNotesSendTargetModeId(modeIdParts), [modeIdParts])
|
||||
const ownsTargetMode = useAppStateValue((s) => s.agentSendPopoverTargetMode?.id === targetModeId)
|
||||
const inFlightVersion = useNotesInFlightVersion()
|
||||
const sendableScopes = useMemo<SendableNotesScope<TNote>[]>(() => {
|
||||
void inFlightVersion
|
||||
return scopes.map(({ formatPrompt, ...scope }) => {
|
||||
const notes = owner
|
||||
? scope.notes.filter((note) => !isNoteInFlight(diffCommentSendKey(owner, note)))
|
||||
: []
|
||||
return { ...scope, notes, prompt: notes.length > 0 ? formatPrompt(notes) : '' }
|
||||
})
|
||||
}, [inFlightVersion, owner, scopes])
|
||||
const sendableScopes = useMemo<SendableNotesScope<TNote>[]>(
|
||||
() =>
|
||||
recomputeOn([inFlightVersion], () =>
|
||||
scopes.map(({ formatPrompt, ...scope }) => {
|
||||
const notes = owner
|
||||
? scope.notes.filter((note) => !isNoteInFlight(diffCommentSendKey(owner, note)))
|
||||
: []
|
||||
return { ...scope, notes, prompt: notes.length > 0 ? formatPrompt(notes) : '' }
|
||||
})
|
||||
),
|
||||
[inFlightVersion, owner, scopes]
|
||||
)
|
||||
const enabledScopes = useMemo(
|
||||
() => sendableScopes.filter((scope) => scope.notes.length > 0),
|
||||
[sendableScopes]
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ReviewNotesVisualStyles } from './review-animated-visual-notes-styles'
|
||||
import { ReviewDiffRows } from './review-notes-diff-rows'
|
||||
import { resetTerminal, runTerminalPhase } from './review-notes-terminal-phase'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { forceLayoutReflow } from '@/lib/force-layout-reflow'
|
||||
|
||||
// The animation owns timed DOM mutations to avoid React overwriting intermediate beats.
|
||||
export function ReviewNotesAnimatedVisual(props: { reducedMotion: boolean }): JSX.Element {
|
||||
@@ -165,7 +166,7 @@ export function ReviewNotesAnimatedVisual(props: { reducedMotion: boolean }): JS
|
||||
cursor.classList.remove('is-visible', 'is-clicking')
|
||||
cursor.style.transition = 'none'
|
||||
cursor.style.transform = 'translate(-30px, 220px)'
|
||||
void cursor.offsetWidth
|
||||
forceLayoutReflow(cursor)
|
||||
cursor.style.transition = ''
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from './review-animated-visual-shared'
|
||||
import { ReviewShipVisualStyles } from './review-animated-visual-ship-styles'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { forceLayoutReflow } from '@/lib/force-layout-reflow'
|
||||
|
||||
export function ReviewShipAnimatedVisual(props: { reducedMotion: boolean }): JSX.Element {
|
||||
const { reducedMotion } = props
|
||||
@@ -104,7 +105,7 @@ export function ReviewShipAnimatedVisual(props: { reducedMotion: boolean }): JSX
|
||||
cursor.classList.remove('is-visible', 'is-clicking')
|
||||
cursor.style.transition = 'none'
|
||||
cursor.style.transform = 'translate(-30px, 220px)'
|
||||
void cursor.offsetWidth
|
||||
forceLayoutReflow(cursor)
|
||||
cursor.style.transition = ''
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import { arrowPathFromCoordTo, bubblePathBetweenRows } from './orchestration-bubble-path'
|
||||
import { AgentRow, WorkspaceCard } from './orchestration-cards'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { forceLayoutReflow } from '@/lib/force-layout-reflow'
|
||||
|
||||
const INITIAL_CHILD_PENDING: RowPending = {
|
||||
'child-codex': true,
|
||||
@@ -181,7 +182,7 @@ export function OrchestrationPage(props: {
|
||||
'<rect x="3" y="5" width="18" height="14" rx="2"/>' +
|
||||
'<path d="M3 7l9 6 9-6"/></svg>'
|
||||
layer.appendChild(bubble)
|
||||
void bubble.offsetWidth
|
||||
forceLayoutReflow(bubble)
|
||||
nextFrame(() => bubble.classList.add('in-flight'))
|
||||
|
||||
later(() => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect } from 'react'
|
||||
import type { RefObject } from 'react'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { forceLayoutReflow } from '@/lib/force-layout-reflow'
|
||||
|
||||
function getReviewPrAnimatedStatusCopy(): {
|
||||
pendingLabel: string
|
||||
@@ -133,7 +134,7 @@ export function useReviewPrViewAnimation(
|
||||
cursorEl.classList.remove('is-visible', 'is-clicking')
|
||||
cursorEl.style.transition = 'none'
|
||||
cursorEl.style.transform = 'translate(-30px, 220px)'
|
||||
void cursorEl.offsetWidth
|
||||
forceLayoutReflow(cursorEl)
|
||||
cursorEl.style.transition = ''
|
||||
checksBlockEl.classList.remove('is-visible')
|
||||
commentsBlockEl.classList.remove('is-visible')
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
codeBlockHTML,
|
||||
type EditorAnimatedVisualRefs
|
||||
} from './editor-animated-visual-markup'
|
||||
import { forceLayoutReflow } from '@/lib/force-layout-reflow'
|
||||
|
||||
export function useEditorAnimatedVisualAnimation(
|
||||
reducedMotion: boolean,
|
||||
@@ -165,7 +166,7 @@ export function useEditorAnimatedVisualAnimation(
|
||||
cursor.style.opacity = '0'
|
||||
cursor.style.transform = 'translate(-30px, 80px)'
|
||||
// Force reflow so the next transition takes effect.
|
||||
void cursor.offsetWidth
|
||||
forceLayoutReflow(cursor)
|
||||
cursor.style.transition = ''
|
||||
await wait(EDITOR_ANIMATION_TIMING.preHoverMs)
|
||||
if (cancelled) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { HomeSlide } from './slides/HomeSlide'
|
||||
import { WorktreeListSlide } from './slides/WorktreeListSlide'
|
||||
import { TerminalSlide } from './slides/TerminalSlide'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { forceLayoutReflow } from '@/lib/force-layout-reflow'
|
||||
|
||||
const DWELL_MS = 4500
|
||||
const TAP_BEFORE_PUSH_MS = 240
|
||||
@@ -92,7 +93,7 @@ export function PhoneCarousel(): React.JSX.Element {
|
||||
return
|
||||
}
|
||||
const id = requestAnimationFrame(() => {
|
||||
void containerRef.current?.offsetHeight
|
||||
forceLayoutReflow(containerRef.current)
|
||||
})
|
||||
return () => cancelAnimationFrame(id)
|
||||
}, [phase])
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { getDriverForPty, onDriverChange } from '@/lib/pane-manager/mobile-driver-state'
|
||||
import { deriveNativeChatCanSend } from './native-chat-send-eligibility'
|
||||
import { recomputeOn } from '@/lib/recompute-on'
|
||||
|
||||
/** Subscribe to the external driver map so mobile-owned PTYs guard composer sends just like xterm input. */
|
||||
export function useNativeChatCanSend(ptyId: string | null): boolean {
|
||||
@@ -16,8 +17,11 @@ export function useNativeChatCanSend(ptyId: string | null): boolean {
|
||||
}),
|
||||
[ptyId]
|
||||
)
|
||||
return useMemo(() => {
|
||||
void driverTick
|
||||
return deriveNativeChatCanSend(ptyId ? getDriverForPty(ptyId) : null)
|
||||
}, [ptyId, driverTick])
|
||||
return useMemo(
|
||||
() =>
|
||||
recomputeOn([driverTick], () =>
|
||||
deriveNativeChatCanSend(ptyId ? getDriverForPty(ptyId) : null)
|
||||
),
|
||||
[ptyId, driverTick]
|
||||
)
|
||||
}
|
||||
|
||||
+42
-39
@@ -30,6 +30,7 @@ import {
|
||||
ChildAgentsWithIdleGroup,
|
||||
buildCollapsedChildAgentSignalStates
|
||||
} from './worktree-card-idle-child-agents'
|
||||
import { recomputeOn } from '@/lib/recompute-on'
|
||||
|
||||
export const SUPPRESS_WORKTREE_LIST_SCROLL_ADJUSTMENT_EVENT =
|
||||
'orca-suppress-worktree-list-scroll-adjustment'
|
||||
@@ -101,46 +102,48 @@ const WorktreeCardAgentsBody = React.memo(function WorktreeCardAgentsBody({
|
||||
)
|
||||
|
||||
const isAgentSendTargetModeActive = agentSendPopoverTargetMode !== null
|
||||
const sendTargetsByPaneKey = useMemo(() => {
|
||||
void agentStatusEpoch
|
||||
void languageRevision
|
||||
if (!isAgentSendTargetModeActive) {
|
||||
return new Map<
|
||||
string,
|
||||
{ status: 'eligible' | 'disabled' | 'sending'; disabledReason?: string }
|
||||
>()
|
||||
}
|
||||
const sendTargetsByPaneKey = useMemo(
|
||||
() =>
|
||||
recomputeOn([agentStatusEpoch, languageRevision], () => {
|
||||
if (!isAgentSendTargetModeActive) {
|
||||
return new Map<
|
||||
string,
|
||||
{ status: 'eligible' | 'disabled' | 'sending'; disabledReason?: string }
|
||||
>()
|
||||
}
|
||||
|
||||
return new Map(
|
||||
deriveRunningAgentSendTargets(sendTargetInputs, worktreeId).map((target) => [
|
||||
target.paneKey,
|
||||
agentSendPopoverTargetMode?.status === 'sending' &&
|
||||
agentSendPopoverTargetMode.sendingPaneKey === target.paneKey
|
||||
? {
|
||||
status: 'sending' as const,
|
||||
disabledReason: translate(
|
||||
'auto.components.sidebar.WorktreeCardAgents.sendingTarget',
|
||||
'Sending...'
|
||||
)
|
||||
}
|
||||
: target.disabledReason
|
||||
? {
|
||||
status: target.status,
|
||||
disabledReason: agentSendDisabledReasonLabel(target.disabledReason)
|
||||
}
|
||||
: { status: target.status }
|
||||
])
|
||||
)
|
||||
}, [
|
||||
// Freshness timers advance this epoch without replacing the status map.
|
||||
agentStatusEpoch,
|
||||
languageRevision,
|
||||
agentSendPopoverTargetMode?.sendingPaneKey,
|
||||
agentSendPopoverTargetMode?.status,
|
||||
isAgentSendTargetModeActive,
|
||||
sendTargetInputs,
|
||||
worktreeId
|
||||
])
|
||||
return new Map(
|
||||
deriveRunningAgentSendTargets(sendTargetInputs, worktreeId).map((target) => [
|
||||
target.paneKey,
|
||||
agentSendPopoverTargetMode?.status === 'sending' &&
|
||||
agentSendPopoverTargetMode.sendingPaneKey === target.paneKey
|
||||
? {
|
||||
status: 'sending' as const,
|
||||
disabledReason: translate(
|
||||
'auto.components.sidebar.WorktreeCardAgents.sendingTarget',
|
||||
'Sending...'
|
||||
)
|
||||
}
|
||||
: target.disabledReason
|
||||
? {
|
||||
status: target.status,
|
||||
disabledReason: agentSendDisabledReasonLabel(target.disabledReason)
|
||||
}
|
||||
: { status: target.status }
|
||||
])
|
||||
)
|
||||
}),
|
||||
[
|
||||
// Freshness timers advance this epoch without replacing the status map.
|
||||
agentStatusEpoch,
|
||||
languageRevision,
|
||||
agentSendPopoverTargetMode?.sendingPaneKey,
|
||||
agentSendPopoverTargetMode?.status,
|
||||
isAgentSendTargetModeActive,
|
||||
sendTargetInputs,
|
||||
worktreeId
|
||||
]
|
||||
)
|
||||
|
||||
const handleSendTargetClick = useCallback(
|
||||
(paneKey: string) => {
|
||||
|
||||
+4
-3
@@ -8,6 +8,7 @@ import {
|
||||
} from '@/lib/folder-workspace-path-status'
|
||||
import { isConfirmedStaleFolderPathStatus } from '../../../../../../shared/folder-workspaces/folder-workspace-path-status'
|
||||
import type { ProjectGroup } from '../../../../../../shared/project-group-types'
|
||||
import { recomputeOn } from '@/lib/recompute-on'
|
||||
|
||||
export function useFolderWorkspaceComposerPathStatus(
|
||||
projectGroup: ProjectGroup | null,
|
||||
@@ -63,9 +64,9 @@ export function useFolderWorkspaceComposerPathStatus(
|
||||
return null
|
||||
}
|
||||
// Subscribe to writes, but let the TTL-aware accessor decide whether negative results remain authoritative.
|
||||
void cachedPathStatusEntry
|
||||
void cacheExpiryTick
|
||||
return getFreshFolderWorkspacePathStatus(pathStatusRequest, pathStatusRouteOptions)
|
||||
return recomputeOn([cachedPathStatusEntry, cacheExpiryTick], () =>
|
||||
getFreshFolderWorkspacePathStatus(pathStatusRequest, pathStatusRouteOptions)
|
||||
)
|
||||
}, [
|
||||
cachedPathStatusEntry,
|
||||
cacheExpiryTick,
|
||||
|
||||
+34
-30
@@ -2,6 +2,7 @@ import { useMemo } from 'react'
|
||||
import { useAppStore } from '@/store'
|
||||
import type { AppState } from '@/store/types'
|
||||
import { deriveRunningAgentSendTargets } from '@/lib/running-agent-targets'
|
||||
import { recomputeOn } from '@/lib/recompute-on'
|
||||
|
||||
// Why: selectors that opt out of a slice must return one stable reference, or every store tick looks like a change.
|
||||
const EMPTY_AGENT_STATUS_BY_PANE_KEY: AppState['agentStatusByPaneKey'] = {}
|
||||
@@ -36,34 +37,37 @@ export function useAgentSendTargetWorktreeId(): string | null {
|
||||
const agentTargetRuntimePaneTitlesByTabId = useAppStore((s) =>
|
||||
agentSendPopoverTargetMode ? s.runtimePaneTitlesByTabId : EMPTY_RUNTIME_PANE_TITLES_BY_TAB_ID
|
||||
)
|
||||
return useMemo(() => {
|
||||
void agentTargetStatusEpoch
|
||||
if (!agentSendPopoverTargetMode) {
|
||||
return null
|
||||
}
|
||||
const targets = deriveRunningAgentSendTargets(
|
||||
{
|
||||
agentStatusByPaneKey: agentTargetStatusByPaneKey,
|
||||
tabsByWorktree: agentTargetTabsByWorktree,
|
||||
unifiedTabsByWorktree: agentTargetUnifiedTabsByWorktree,
|
||||
terminalLayoutsByTabId: agentTargetTerminalLayoutsByTabId,
|
||||
ptyIdsByTabId: agentTargetPtyIdsByTabId,
|
||||
runtimePaneTitlesByTabId: agentTargetRuntimePaneTitlesByTabId
|
||||
},
|
||||
agentSendPopoverTargetMode.worktreeId
|
||||
)
|
||||
return targets.some((target) => target.status === 'eligible')
|
||||
? agentSendPopoverTargetMode.worktreeId
|
||||
: null
|
||||
}, [
|
||||
// Why: eligibility can flip when the stale-boundary scheduler bumps this epoch without replacing the status map.
|
||||
agentTargetStatusEpoch,
|
||||
agentSendPopoverTargetMode,
|
||||
agentTargetStatusByPaneKey,
|
||||
agentTargetTabsByWorktree,
|
||||
agentTargetUnifiedTabsByWorktree,
|
||||
agentTargetTerminalLayoutsByTabId,
|
||||
agentTargetPtyIdsByTabId,
|
||||
agentTargetRuntimePaneTitlesByTabId
|
||||
])
|
||||
return useMemo(
|
||||
() =>
|
||||
recomputeOn([agentTargetStatusEpoch], () => {
|
||||
if (!agentSendPopoverTargetMode) {
|
||||
return null
|
||||
}
|
||||
const targets = deriveRunningAgentSendTargets(
|
||||
{
|
||||
agentStatusByPaneKey: agentTargetStatusByPaneKey,
|
||||
tabsByWorktree: agentTargetTabsByWorktree,
|
||||
unifiedTabsByWorktree: agentTargetUnifiedTabsByWorktree,
|
||||
terminalLayoutsByTabId: agentTargetTerminalLayoutsByTabId,
|
||||
ptyIdsByTabId: agentTargetPtyIdsByTabId,
|
||||
runtimePaneTitlesByTabId: agentTargetRuntimePaneTitlesByTabId
|
||||
},
|
||||
agentSendPopoverTargetMode.worktreeId
|
||||
)
|
||||
return targets.some((target) => target.status === 'eligible')
|
||||
? agentSendPopoverTargetMode.worktreeId
|
||||
: null
|
||||
}),
|
||||
[
|
||||
// Why: eligibility can flip when the stale-boundary scheduler bumps this epoch without replacing the status map.
|
||||
agentTargetStatusEpoch,
|
||||
agentSendPopoverTargetMode,
|
||||
agentTargetStatusByPaneKey,
|
||||
agentTargetTabsByWorktree,
|
||||
agentTargetUnifiedTabsByWorktree,
|
||||
agentTargetTerminalLayoutsByTabId,
|
||||
agentTargetPtyIdsByTabId,
|
||||
agentTargetRuntimePaneTitlesByTabId
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
+15
-13
@@ -9,6 +9,7 @@ import type { ProjectGroup } from '../../../../../../shared/project-group-types'
|
||||
import type { Repo } from '../../../../../../shared/repo-types'
|
||||
import type { AppState } from '@/store/types'
|
||||
import { getFolderPathStatusRouteOptionsForRows } from './folder-path-status-routing'
|
||||
import { recomputeOn } from '@/lib/recompute-on'
|
||||
|
||||
type FolderPathStatusRequest = Parameters<AppState['fetchFolderWorkspacePathStatus']>[0]
|
||||
|
||||
@@ -166,23 +167,24 @@ export function useFolderWorkspacePathStatusRows(args: {
|
||||
getFolderWorkspacePathStatusCacheKey,
|
||||
projectGroups
|
||||
])
|
||||
const getCachedFolderWorkspacePathStatus = useCallback(
|
||||
(request: FolderPathStatusRequest) => {
|
||||
const options = getFolderPathStatusRouteOptions(request)
|
||||
if (!options) {
|
||||
return null
|
||||
}
|
||||
const cacheKey = getFolderWorkspacePathStatusCacheKey(request, options)
|
||||
// Why: don't let an expired negative status keep folder workspaces disabled while a refresh is in flight.
|
||||
void folderWorkspacePathStatuses[cacheKey]
|
||||
void folderPathStatusCacheExpiryTick
|
||||
return getFreshFolderWorkspacePathStatus(request, options)
|
||||
},
|
||||
// Why: status writes and expiry ticks renew the getter so an expired negative status can't keep
|
||||
// folder workspaces disabled while a refresh is in flight.
|
||||
const getCachedFolderWorkspacePathStatus = useMemo(
|
||||
() =>
|
||||
recomputeOn(
|
||||
[folderWorkspacePathStatuses, folderPathStatusCacheExpiryTick],
|
||||
() => (request: FolderPathStatusRequest) => {
|
||||
const options = getFolderPathStatusRouteOptions(request)
|
||||
if (!options) {
|
||||
return null
|
||||
}
|
||||
return getFreshFolderWorkspacePathStatus(request, options)
|
||||
}
|
||||
),
|
||||
[
|
||||
folderWorkspacePathStatuses,
|
||||
folderPathStatusCacheExpiryTick,
|
||||
getFolderPathStatusRouteOptions,
|
||||
getFolderWorkspacePathStatusCacheKey,
|
||||
getFreshFolderWorkspacePathStatus
|
||||
]
|
||||
)
|
||||
|
||||
@@ -129,7 +129,9 @@ export function useResourcePtyActions(inventory: ReturnType<typeof useResourceSe
|
||||
function drain(): void {
|
||||
const pending = queue.current
|
||||
while (pending.active < MAX_CONCURRENT_TERMINATIONS && pending.head < pending.items.length) {
|
||||
const target = pending.items[pending.head++]!
|
||||
// Why: oxc React Compiler 0.152 emits a member postfix update used as a value as a prefix one.
|
||||
const target = pending.items[pending.head]!
|
||||
pending.head += 1
|
||||
pending.active += 1
|
||||
void terminate(target).finally(() => {
|
||||
pending.active -= 1
|
||||
|
||||
+136
-130
@@ -27,6 +27,7 @@ import { deriveTaskPagePRCheckSummary } from '../task-page-pr-check-summary'
|
||||
import { PR_CHECKS_EAGER_PREFETCH_LIMIT } from '../task-page-source-context'
|
||||
import { getTaskPageRepoSourceContext } from '../../../lib/task-repo-source-context'
|
||||
import { sameOptionalGitHubOwnerRepo } from './task-page-github-review-model'
|
||||
import { recomputeOn } from '@/lib/recompute-on'
|
||||
export function useTaskPagePRCheckHydration(
|
||||
model: TaskPageJiraCreationMetadataModel,
|
||||
filteredWorkItems: GitHubWorkItem[],
|
||||
@@ -57,19 +58,25 @@ export function useTaskPagePRCheckHydration(
|
||||
})
|
||||
.join('|')
|
||||
)
|
||||
const checkReads = useMemo(() => {
|
||||
// A new list query owns its own bounded demand reads.
|
||||
void model.selectedReposKey
|
||||
void model.taskRefreshNonce
|
||||
void model.workItemsInvalidationNonce
|
||||
void model.appliedTaskSearch
|
||||
return new Map<string, PRCheckContract>()
|
||||
}, [
|
||||
model.selectedReposKey,
|
||||
model.taskRefreshNonce,
|
||||
model.workItemsInvalidationNonce,
|
||||
model.appliedTaskSearch
|
||||
])
|
||||
// A new list query owns its own bounded demand reads.
|
||||
const checkReads = useMemo(
|
||||
() =>
|
||||
recomputeOn(
|
||||
[
|
||||
model.selectedReposKey,
|
||||
model.taskRefreshNonce,
|
||||
model.workItemsInvalidationNonce,
|
||||
model.appliedTaskSearch
|
||||
],
|
||||
() => new Map<string, PRCheckContract>()
|
||||
),
|
||||
[
|
||||
model.selectedReposKey,
|
||||
model.taskRefreshNonce,
|
||||
model.workItemsInvalidationNonce,
|
||||
model.appliedTaskSearch
|
||||
]
|
||||
)
|
||||
const activeCheckReads = useRef<typeof checkReads | null>(null)
|
||||
useLayoutEffect(() => {
|
||||
activeCheckReads.current = checkReads
|
||||
@@ -79,125 +86,124 @@ export function useTaskPagePRCheckHydration(
|
||||
}
|
||||
}
|
||||
}, [checkReads])
|
||||
const ensurePRChecksLoaded = useCallback(
|
||||
(item: GitHubWorkItem): void => {
|
||||
if (item.type !== 'pr') {
|
||||
return
|
||||
}
|
||||
// Capability observations wake the existing demand trigger without adding per-row subscriptions.
|
||||
void localCapabilities
|
||||
void runtimeStatuses
|
||||
const repo = getWorkItemRepo(repos, item)
|
||||
if (!repo) {
|
||||
return
|
||||
}
|
||||
const sourceContext = getTaskPageRepoSourceContext(repo, 'github')
|
||||
const runtimeContract = capturePRCheckContract(
|
||||
useAppStore.getState,
|
||||
getGitHubSourceRuntimeTarget(sourceContext),
|
||||
sourceContext
|
||||
)
|
||||
const generation = model.paginationGenerationRef.current
|
||||
const contract = {
|
||||
...runtimeContract,
|
||||
current: () =>
|
||||
runtimeContract.current() &&
|
||||
activeCheckReads.current === checkReads &&
|
||||
model.paginationGenerationRef.current === generation
|
||||
}
|
||||
if (
|
||||
item.checksSummary &&
|
||||
(!contract.complete || completePRCheckOwnerScope(item) === contract.scope)
|
||||
) {
|
||||
return
|
||||
}
|
||||
const readKey = JSON.stringify([
|
||||
item.repoId,
|
||||
item.id,
|
||||
item.headSha,
|
||||
sourceContext,
|
||||
contract.complete
|
||||
])
|
||||
if (checkReads.get(readKey)?.current()) {
|
||||
return
|
||||
}
|
||||
checkReads.set(readKey, contract)
|
||||
// The query's loaded/demanded rows bound attempts; eviction must not re-arm failed eager reads.
|
||||
const requestedHeadSha = item.headSha
|
||||
const requestedPRRepo = item.prRepo ?? null
|
||||
const owner = resolvePullRequestRepo(item)
|
||||
const read =
|
||||
contract.complete && owner
|
||||
? lookupGitHubWorkItemByOwnerRepoForSource({
|
||||
repoPath: repo.path,
|
||||
repoId: repo.id,
|
||||
sourceContext,
|
||||
...owner,
|
||||
number: item.number,
|
||||
type: 'pr',
|
||||
accepts: contract.current,
|
||||
checkContract: runtimeContract
|
||||
})
|
||||
: fetchPRChecks(
|
||||
repo.path,
|
||||
item.number,
|
||||
item.branchName,
|
||||
item.headSha,
|
||||
item.prRepo ?? null,
|
||||
{
|
||||
// Capability observations wake the existing demand trigger without adding per-row subscriptions.
|
||||
const ensurePRChecksLoaded = useMemo(
|
||||
() =>
|
||||
recomputeOn([localCapabilities, runtimeStatuses], () => (item: GitHubWorkItem): void => {
|
||||
if (item.type !== 'pr') {
|
||||
return
|
||||
}
|
||||
const repo = getWorkItemRepo(repos, item)
|
||||
if (!repo) {
|
||||
return
|
||||
}
|
||||
const sourceContext = getTaskPageRepoSourceContext(repo, 'github')
|
||||
const runtimeContract = capturePRCheckContract(
|
||||
useAppStore.getState,
|
||||
getGitHubSourceRuntimeTarget(sourceContext),
|
||||
sourceContext
|
||||
)
|
||||
const generation = model.paginationGenerationRef.current
|
||||
const contract = {
|
||||
...runtimeContract,
|
||||
current: () =>
|
||||
runtimeContract.current() &&
|
||||
activeCheckReads.current === checkReads &&
|
||||
model.paginationGenerationRef.current === generation
|
||||
}
|
||||
if (
|
||||
item.checksSummary &&
|
||||
(!contract.complete || completePRCheckOwnerScope(item) === contract.scope)
|
||||
) {
|
||||
return
|
||||
}
|
||||
const readKey = JSON.stringify([
|
||||
item.repoId,
|
||||
item.id,
|
||||
item.headSha,
|
||||
sourceContext,
|
||||
contract.complete
|
||||
])
|
||||
if (checkReads.get(readKey)?.current()) {
|
||||
return
|
||||
}
|
||||
checkReads.set(readKey, contract)
|
||||
// The query's loaded/demanded rows bound attempts; eviction must not re-arm failed eager reads.
|
||||
const requestedHeadSha = item.headSha
|
||||
const requestedPRRepo = item.prRepo ?? null
|
||||
const owner = resolvePullRequestRepo(item)
|
||||
const read =
|
||||
contract.complete && owner
|
||||
? lookupGitHubWorkItemByOwnerRepoForSource({
|
||||
repoPath: repo.path,
|
||||
repoId: repo.id,
|
||||
sourceContext
|
||||
}
|
||||
sourceContext,
|
||||
...owner,
|
||||
number: item.number,
|
||||
type: 'pr',
|
||||
accepts: contract.current,
|
||||
checkContract: runtimeContract
|
||||
})
|
||||
: fetchPRChecks(
|
||||
repo.path,
|
||||
item.number,
|
||||
item.branchName,
|
||||
item.headSha,
|
||||
item.prRepo ?? null,
|
||||
{
|
||||
repoId: repo.id,
|
||||
sourceContext
|
||||
}
|
||||
)
|
||||
void read
|
||||
.then((result) => {
|
||||
if (!contract.current()) {
|
||||
return
|
||||
}
|
||||
const observed = Array.isArray(result) ? null : result
|
||||
if (
|
||||
contract.complete &&
|
||||
(!observed ||
|
||||
observed.headSha !== requestedHeadSha ||
|
||||
!sameOptionalGitHubOwnerRepo(observed.prRepo ?? null, requestedPRRepo))
|
||||
) {
|
||||
return
|
||||
}
|
||||
const checksSummary = Array.isArray(result)
|
||||
? deriveTaskPagePRCheckSummary(result)
|
||||
: observed?.checksSummary
|
||||
if (!checksSummary && !contract.complete) {
|
||||
return
|
||||
}
|
||||
const accepted =
|
||||
observed &&
|
||||
acceptPRCheckObservation(observed, item, { ...contract, current: contract.current() })
|
||||
patchTaskPageWorkItemRows(
|
||||
{
|
||||
id: item.id,
|
||||
repoId: item.repoId
|
||||
},
|
||||
contract.complete && accepted
|
||||
? projectPRCheckOwner(accepted, {
|
||||
checksSummary: accepted.checksSummary,
|
||||
number: item.number,
|
||||
url: item.url,
|
||||
prRepo: item.prRepo,
|
||||
headSha: item.headSha
|
||||
})
|
||||
: { checksSummary },
|
||||
(currentItem) =>
|
||||
currentItem.type === 'pr' &&
|
||||
(!contract.complete || currentItem.checksSummary === item.checksSummary) &&
|
||||
currentItem.headSha === requestedHeadSha &&
|
||||
sameOptionalGitHubOwnerRepo(currentItem.prRepo, requestedPRRepo) &&
|
||||
(currentItem.checksSummary?.cancelled === undefined ||
|
||||
currentItem.checksSummary.state !== checksSummary?.state ||
|
||||
contract.complete)
|
||||
)
|
||||
void read
|
||||
.then((result) => {
|
||||
if (!contract.current()) {
|
||||
return
|
||||
}
|
||||
const observed = Array.isArray(result) ? null : result
|
||||
if (
|
||||
contract.complete &&
|
||||
(!observed ||
|
||||
observed.headSha !== requestedHeadSha ||
|
||||
!sameOptionalGitHubOwnerRepo(observed.prRepo ?? null, requestedPRRepo))
|
||||
) {
|
||||
return
|
||||
}
|
||||
const checksSummary = Array.isArray(result)
|
||||
? deriveTaskPagePRCheckSummary(result)
|
||||
: observed?.checksSummary
|
||||
if (!checksSummary && !contract.complete) {
|
||||
return
|
||||
}
|
||||
const accepted =
|
||||
observed &&
|
||||
acceptPRCheckObservation(observed, item, { ...contract, current: contract.current() })
|
||||
patchTaskPageWorkItemRows(
|
||||
{
|
||||
id: item.id,
|
||||
repoId: item.repoId
|
||||
},
|
||||
contract.complete && accepted
|
||||
? projectPRCheckOwner(accepted, {
|
||||
checksSummary: accepted.checksSummary,
|
||||
number: item.number,
|
||||
url: item.url,
|
||||
prRepo: item.prRepo,
|
||||
headSha: item.headSha
|
||||
})
|
||||
: { checksSummary },
|
||||
(currentItem) =>
|
||||
currentItem.type === 'pr' &&
|
||||
(!contract.complete || currentItem.checksSummary === item.checksSummary) &&
|
||||
currentItem.headSha === requestedHeadSha &&
|
||||
sameOptionalGitHubOwnerRepo(currentItem.prRepo, requestedPRRepo) &&
|
||||
(currentItem.checksSummary?.cancelled === undefined ||
|
||||
currentItem.checksSummary.state !== checksSummary?.state ||
|
||||
contract.complete)
|
||||
)
|
||||
})
|
||||
.catch(() => {})
|
||||
},
|
||||
})
|
||||
.catch(() => {})
|
||||
}),
|
||||
[
|
||||
localCapabilities,
|
||||
runtimeStatuses,
|
||||
|
||||
+62
-53
@@ -23,6 +23,7 @@ import type { WorktreeJumpPaletteOpenTabs } from './use-worktree-jump-palette-op
|
||||
import type { WorktreeJumpPaletteProjectTargets } from './use-worktree-jump-palette-project-targets'
|
||||
import type { WorktreeJumpPaletteStoreState } from './use-worktree-jump-palette-store-state'
|
||||
import type { WorktreeJumpPaletteWorktrees } from './use-worktree-jump-palette-worktrees'
|
||||
import { recomputeOn } from '@/lib/recompute-on'
|
||||
|
||||
function getComposerPrefetchRepoId(
|
||||
state: ReturnType<typeof useAppStore.getState>,
|
||||
@@ -163,59 +164,67 @@ export function useWorktreeJumpPaletteQuickActions({
|
||||
]
|
||||
)
|
||||
// Availability reads store state imperatively; these values supply its real memo dependencies, including managed-browser runtime status.
|
||||
const availableActions = useMemo(() => {
|
||||
void activeView
|
||||
void activeWorktreeId
|
||||
void worktreesByRepo
|
||||
void repos
|
||||
void sshConnectionStates
|
||||
void activeGroupIdByWorktree
|
||||
void groupsByWorktree
|
||||
void unifiedTabsByWorktree
|
||||
void isLoading
|
||||
void settings?.activeRuntimeEnvironmentId
|
||||
void runtimeStatusByEnvironmentId
|
||||
const context = buildQuickActionContext()
|
||||
return {
|
||||
context,
|
||||
actionResults: actionResults
|
||||
.filter(
|
||||
(action) =>
|
||||
(action.id === 'git-graph' &&
|
||||
context.activeWorktree &&
|
||||
repos.find((repo) => repo.id === context.activeWorktree?.repoId)?.kind !==
|
||||
'folder') ||
|
||||
action.isAvailable(context).available
|
||||
)
|
||||
.map((action) =>
|
||||
action.id === 'git-graph'
|
||||
? {
|
||||
...action,
|
||||
disabled: graphSupport !== true,
|
||||
description:
|
||||
graphSupport === false && activeWorktreeId
|
||||
? gitGraphUpdateMessage(activeWorktreeId)
|
||||
: ''
|
||||
}
|
||||
: action
|
||||
)
|
||||
}
|
||||
}, [
|
||||
graphSupport,
|
||||
actionResults,
|
||||
buildQuickActionContext,
|
||||
activeView,
|
||||
activeWorktreeId,
|
||||
worktreesByRepo,
|
||||
repos,
|
||||
sshConnectionStates,
|
||||
activeGroupIdByWorktree,
|
||||
groupsByWorktree,
|
||||
unifiedTabsByWorktree,
|
||||
isLoading,
|
||||
settings?.activeRuntimeEnvironmentId,
|
||||
runtimeStatusByEnvironmentId
|
||||
])
|
||||
const availableActions = useMemo(
|
||||
() =>
|
||||
recomputeOn(
|
||||
[
|
||||
activeView,
|
||||
activeWorktreeId,
|
||||
worktreesByRepo,
|
||||
repos,
|
||||
sshConnectionStates,
|
||||
activeGroupIdByWorktree,
|
||||
groupsByWorktree,
|
||||
unifiedTabsByWorktree,
|
||||
isLoading,
|
||||
settings?.activeRuntimeEnvironmentId,
|
||||
runtimeStatusByEnvironmentId
|
||||
],
|
||||
() => {
|
||||
const context = buildQuickActionContext()
|
||||
return {
|
||||
context,
|
||||
actionResults: actionResults
|
||||
.filter(
|
||||
(action) =>
|
||||
(action.id === 'git-graph' &&
|
||||
context.activeWorktree &&
|
||||
repos.find((repo) => repo.id === context.activeWorktree?.repoId)?.kind !==
|
||||
'folder') ||
|
||||
action.isAvailable(context).available
|
||||
)
|
||||
.map((action) =>
|
||||
action.id === 'git-graph'
|
||||
? {
|
||||
...action,
|
||||
disabled: graphSupport !== true,
|
||||
description:
|
||||
graphSupport === false && activeWorktreeId
|
||||
? gitGraphUpdateMessage(activeWorktreeId)
|
||||
: ''
|
||||
}
|
||||
: action
|
||||
)
|
||||
}
|
||||
}
|
||||
),
|
||||
[
|
||||
graphSupport,
|
||||
actionResults,
|
||||
buildQuickActionContext,
|
||||
activeView,
|
||||
activeWorktreeId,
|
||||
worktreesByRepo,
|
||||
repos,
|
||||
sshConnectionStates,
|
||||
activeGroupIdByWorktree,
|
||||
groupsByWorktree,
|
||||
unifiedTabsByWorktree,
|
||||
isLoading,
|
||||
settings?.activeRuntimeEnvironmentId,
|
||||
runtimeStatusByEnvironmentId
|
||||
]
|
||||
)
|
||||
const middleItems = useMemo<(SettingsPaletteItem | QuickActionPaletteItem)[]>(() => {
|
||||
const results = rankCmdJMiddleResults({
|
||||
query: deferredQuery,
|
||||
|
||||
@@ -3,12 +3,14 @@ import {
|
||||
createPaletteSearchContext,
|
||||
type PaletteSearchContext
|
||||
} from '../../../shared/palette-match/palette-ranking'
|
||||
import { recomputeOn } from '@/lib/recompute-on'
|
||||
|
||||
/** One clock for every source participating in the current search snapshot. */
|
||||
export function usePaletteSearchEvaluationContext(snapshot: unknown): PaletteSearchContext {
|
||||
return useMemo(() => {
|
||||
void snapshot
|
||||
// oxlint-disable-next-line react/purity -- Each changed snapshot starts one synchronous evaluation clock.
|
||||
return createPaletteSearchContext(Date.now())
|
||||
}, [snapshot])
|
||||
return useMemo(
|
||||
() =>
|
||||
// oxlint-disable-next-line react/purity -- Each changed snapshot starts one synchronous evaluation clock.
|
||||
recomputeOn([snapshot], () => createPaletteSearchContext(Date.now())),
|
||||
[snapshot]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { projectViewSourceScope } from '@/store/github/cache/cache-identity'
|
||||
import type { GitHubWorkItem } from '../../../shared/github/work-item-types'
|
||||
import type { GitHubProjectTable } from '../../../shared/github/project-types'
|
||||
import type { RuntimeClientTarget } from '../../../shared/runtime-client-target'
|
||||
import { recomputeOn } from '@/lib/recompute-on'
|
||||
|
||||
const noopSubscribe = () => () => {}
|
||||
const noopSnapshot = () => 0
|
||||
@@ -34,8 +35,9 @@ export function useGitHubTaskProjectTable(
|
||||
cache?.snapshot ?? noopSnapshot
|
||||
)
|
||||
const scope = projectViewSourceScope(target)
|
||||
return useMemo(() => {
|
||||
void revision
|
||||
return table && cache ? cache.projectTable(table, scope) : table
|
||||
}, [cache, table, scope, revision])
|
||||
return useMemo(
|
||||
() =>
|
||||
recomputeOn([revision], () => (table && cache ? cache.projectTable(table, scope) : table)),
|
||||
[cache, table, scope, revision]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/** Flushes pending style changes so the next one starts a CSS transition. A call, unlike a bare
|
||||
* `void el.offsetWidth`, survives React Compiler's dead-read removal. */
|
||||
export function forceLayoutReflow(element: HTMLElement | null | undefined): number | undefined {
|
||||
return element?.offsetWidth
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/** Returns `compute()`. `_inputs` are values a memo must recompute for although `compute` never
|
||||
* reads them: React Compiler keys memos on what a call receives, and drops a bare `void x`. */
|
||||
export function recomputeOn<T>(_inputs: readonly unknown[], compute: () => T): T {
|
||||
return compute()
|
||||
}
|
||||
+25
-14
@@ -109,6 +109,30 @@ export async function ensureAgentLaunchPreferences(
|
||||
}
|
||||
}
|
||||
|
||||
// Takes the whole subscription so contact/open stay inputs; React Compiler keys a callback on the fields it reads.
|
||||
function subscribeHostPreferences(
|
||||
subscription: {
|
||||
environmentId: string | undefined
|
||||
pairingRevision: number | undefined
|
||||
runtimeId: string | null | undefined
|
||||
active: boolean
|
||||
},
|
||||
listener: () => void
|
||||
): () => void {
|
||||
if (!subscription.environmentId || !subscription.active) {
|
||||
return () => {}
|
||||
}
|
||||
return acquire(
|
||||
resource({
|
||||
environmentId: subscription.environmentId,
|
||||
pairingRevision: subscription.pairingRevision,
|
||||
runtimeId: subscription.runtimeId ?? undefined
|
||||
}),
|
||||
listener,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
export function useHostAgentMenuSettings(
|
||||
owner: {
|
||||
executionHostId: string
|
||||
@@ -160,20 +184,7 @@ export function useHostAgentMenuSettings(
|
||||
pending
|
||||
] = useAppViewExternalStore(
|
||||
useCallback(
|
||||
(listener: () => void) => {
|
||||
if (!subscription.environmentId || !subscription.active) {
|
||||
return () => {}
|
||||
}
|
||||
return acquire(
|
||||
resource({
|
||||
environmentId: subscription.environmentId,
|
||||
pairingRevision: subscription.pairingRevision,
|
||||
runtimeId: subscription.runtimeId ?? undefined
|
||||
}),
|
||||
listener,
|
||||
true
|
||||
)
|
||||
},
|
||||
(listener: () => void) => subscribeHostPreferences(subscription, listener),
|
||||
[subscription]
|
||||
),
|
||||
() => effectiveSnapshot(undefined),
|
||||
|
||||
Reference in New Issue
Block a user