mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
* fix(sidebar): land flip-a's switch-on fixes on main, still dark (no flip) The fixes found while the sidebar ran from main's feed, without turning that on: ORCA_SIDEBAR_INPUTS_IS_MAIN stays false, and none of flip-a's stop-gaps come along. - Main stops counting a closed tab's terminal once it was asked to stop and no pane shows it; a failed or refused stop takes the request back, and a host out of contact stays unverifiable. - Main fetches a paired server's rows with the transport's normal bound, so a server slower than 5 s still lists. - A deferred unread raise flips the folder row it was decided for, and a read while the owner check is out cancels it. - Phone: no worktree.ps flash before the feed; a host's last installed feed stands in while a fresh read is out; it highlights its own last-opened workspace and a fold never scrolls; projects file under their project group with the desktop's indent; the collapse spinner shows only after 500 ms; "Reopen to update" keeps the rows on screen. - e2e helpers and journeys for the switch-on suite. * fix(sidebar): keep an owed relay stop marked, clear a thrown stop, and prune pending unread raises - A stop-and-wait that throws takes its stop request back, like one that fails. - A final stop the legacy relay still owes stays marked, since the relay retries it. - Pending unread raises are tracked only while one waits, so the map no longer grows. - The paired-server fallback poll test gives its third fetch a reply and asserts live.
94 lines
3.4 KiB
TypeScript
94 lines
3.4 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
|
import type { SectionList, SectionListData } from 'react-native'
|
|
|
|
type WithId = { worktreeId: string; isActive?: boolean }
|
|
|
|
// Scrolls the highlighted (isActive) worktree into view when the highlight
|
|
// moves to a different workspace. A highlight a search or fold hides and then
|
|
// shows again is the same workspace, so it never yanks the list. Returns the
|
|
// ref to attach to the SectionList and the onScrollToIndexFailed handler it
|
|
// needs for rows that aren't measured yet (variable heights from the inline
|
|
// agent list).
|
|
export function useActiveWorktreeScroll<T extends WithId, S>(
|
|
sections: readonly (SectionListData<T, S> & { data: readonly T[] })[],
|
|
/** The folded sections: a highlight an unfold first reveals is not scrolled to either. */
|
|
collapsedGroups?: ReadonlySet<string>
|
|
): {
|
|
sectionListRef: React.RefObject<SectionList<T, S> | null>
|
|
onScrollToIndexFailed: (info: { averageItemLength: number }) => void
|
|
} {
|
|
const sectionListRef = useRef<SectionList<T, S>>(null)
|
|
const lastScrolledActiveIdRef = useRef<string | null>(null)
|
|
// The last workspace highlighted, scrolled to or not: a hidden row coming back is no change.
|
|
const seenActiveIdRef = useRef<string | null>(null)
|
|
const collapsedGroupsRef = useRef(collapsedGroups)
|
|
|
|
const activeWorktreeId = useMemo(() => {
|
|
for (const section of sections) {
|
|
const match = section.data.find((w) => w.isActive)
|
|
if (match) {
|
|
return match.worktreeId
|
|
}
|
|
}
|
|
return null
|
|
}, [sections])
|
|
|
|
// Live mirror of the current active id so the deferred retry can bail if the
|
|
// selection changed during its timeout (avoids a brief scroll to a stale row).
|
|
const activeWorktreeIdRef = useRef<string | null>(activeWorktreeId)
|
|
activeWorktreeIdRef.current = activeWorktreeId
|
|
|
|
const scrollToWorktree = useCallback(
|
|
(worktreeId: string): boolean => {
|
|
for (let sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
|
|
const itemIndex = sections[sectionIndex].data.findIndex((w) => w.worktreeId === worktreeId)
|
|
if (itemIndex !== -1) {
|
|
sectionListRef.current?.scrollToLocation({
|
|
sectionIndex,
|
|
itemIndex,
|
|
viewPosition: 0.5,
|
|
animated: true
|
|
})
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
},
|
|
[sections]
|
|
)
|
|
|
|
useEffect(() => {
|
|
const folded = collapsedGroupsRef.current !== collapsedGroups
|
|
collapsedGroupsRef.current = collapsedGroups
|
|
if (!activeWorktreeId || activeWorktreeId === seenActiveIdRef.current) {
|
|
return
|
|
}
|
|
seenActiveIdRef.current = activeWorktreeId
|
|
if (!folded && scrollToWorktree(activeWorktreeId)) {
|
|
lastScrolledActiveIdRef.current = activeWorktreeId
|
|
}
|
|
}, [collapsedGroups, activeWorktreeId, scrollToWorktree])
|
|
|
|
const onScrollToIndexFailed = useCallback(
|
|
(info: { averageItemLength: number }) => {
|
|
const target = lastScrolledActiveIdRef.current
|
|
if (!target) {
|
|
return
|
|
}
|
|
setTimeout(
|
|
() => {
|
|
// Bail if the active selection moved on while we waited — otherwise we'd
|
|
// scroll to a now-stale row before the effect corrects it.
|
|
if (activeWorktreeIdRef.current === target) {
|
|
scrollToWorktree(target)
|
|
}
|
|
},
|
|
info.averageItemLength > 0 ? 120 : 0
|
|
)
|
|
},
|
|
[scrollToWorktree]
|
|
)
|
|
|
|
return { sectionListRef, onScrollToIndexFailed }
|
|
}
|