mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 00:04:01 +00:00
#14397 split `shared/types.ts` into 46 per-domain modules but kept the path as a re-export barrel so the import sites did not have to change. This removes the barrel: every consumer now imports from the module that actually declares the type, and `src/shared/types.ts` is deleted. Barrels hide where a type lives, make every consumer look like it depends on the whole domain, and let an unrelated edit invalidate a module that ~2,000 files transitively import. 2,323 import declarations across 2,321 files. Rewritten mechanically: each specifier was resolved to an absolute path via the TypeScript AST and recomputed, rather than string-substituted, so alias forms (`@/../../shared/ types`) and per-specifier `type` modifiers survive. Four cases the mechanical pass had to handle, each found by a gate rather than by reading the diff: - Modules inside `src/shared` import the barrel as `./types`, not `shared/types`. A pre-filter on the latter string skipped 176 of them and left imports dangling at a deleted file, which surfaced as confusing `Property 'x' is optional in type 'Repo' but required in Pick<Repo, ...>` errors rather than "module not found". - The barrel RENAMED one type on the way through (`WorkspaceSource as WorkspaceCreateTelemetrySource`), so the original name in the owning module has to be re-aliased at each consumer. - Three test files put `;(globalThis as ...)` on the line after the import. TypeScript parses that `;` as the import statement's terminator, so replacing through `statement.getEnd()` deletes it and breaks ASI. The rewrite now stops at the module specifier. - A file that already imported directly from a module got a SECOND import from it, because the barrel re-exported those same names — which trips `import/no-duplicates` under `--deny-warnings`. A post-pass merges declarations sharing a specifier and type-only-ness; the `import type` plus `import` pair from one module is left alone, since that form is allowed. Splitting one barrel import into several genuinely adds lines, which pushed `terminal-layout-pty-ownership.ts` to 301 counted lines: its 107-character import must wrap, and neither local type collapses onto one line (101 and 116 characters). Rather than contort a type declaration to fit a line budget, `collectLeafIds` and `pruneLeaves` move to `terminal-pane-layout-tree.ts` — they are pure structural operations on the layout tree and independent of PTY ownership. `visible-worktrees.ts` similarly loses its own mini-barrel re-export of `isDefaultBranchWorkspace`, with the four real consumers repointed at the declaring module. No `max-lines` bypass added. Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted first — these projects are `composite: true` and reuse stale caches); the full `pnpm lint` green, not just bare oxlint — the narrower local check is what let the duplicate imports reach CI; max-lines ratchet OK at 344.
297 lines
9.5 KiB
TypeScript
297 lines
9.5 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
import type { FlatList } from 'react-native'
|
|
import type { DiffComment } from '../../../src/shared/diff-comment-types'
|
|
import type { ConnectionState } from '../transport/types'
|
|
import type { RpcClient } from '../transport/rpc-client'
|
|
import { getWorktreeLabel } from './worktree-label'
|
|
import { getUnsentMobileDiffComments } from './mobile-diff-comment-edit'
|
|
import {
|
|
buildMobileDiffReviewQueue,
|
|
filterMobileDiffReviewQueue,
|
|
mobileDiffReviewCommentMatchesItem,
|
|
type MobileDiffReviewQueueFilter,
|
|
type MobileDiffReviewQueueItem
|
|
} from './mobile-diff-review-queue'
|
|
import {
|
|
findMobileDiffReviewInitialIndex,
|
|
type MobileDiffReviewInitialTarget
|
|
} from './mobile-diff-review-positioning'
|
|
import { loadMobileDiffReviewSnapshot } from './mobile-diff-review-loaders'
|
|
import { useMobileDiffReviewDiffLoading } from './use-mobile-diff-review-diff-loading'
|
|
import { canOpenMobileBranchCompareDiff } from '../source-control/mobile-branch-compare'
|
|
import type {
|
|
ComposerState,
|
|
ReviewDiffLine,
|
|
ReviewScreenState,
|
|
SendSheetState
|
|
} from './mobile-diff-review-screen-model'
|
|
import { useMobileDiffReviewInteractions } from './use-mobile-diff-review-interactions'
|
|
import { useMobilePrSidebarController } from './use-mobile-pr-sidebar-controller'
|
|
|
|
type ControllerInput = {
|
|
client: RpcClient | null
|
|
connState: ConnectionState
|
|
hostId: string
|
|
worktreeId: string
|
|
name: string
|
|
initialFilter: MobileDiffReviewQueueFilter
|
|
initialTarget: MobileDiffReviewInitialTarget | null
|
|
onOpenSession: () => void
|
|
onReconnect: (hostId: string) => void | Promise<void>
|
|
}
|
|
|
|
export function useMobileDiffReviewController(input: ControllerInput) {
|
|
const {
|
|
client,
|
|
connState,
|
|
hostId,
|
|
worktreeId,
|
|
name,
|
|
initialFilter,
|
|
initialTarget,
|
|
onOpenSession,
|
|
onReconnect
|
|
} = input
|
|
const listRef = useRef<FlatList<ReviewDiffLine> | null>(null)
|
|
const loadGenerationRef = useRef(0)
|
|
const seededInitialTargetRef = useRef(false)
|
|
const initialTargetKey = initialTarget ? `${initialTarget.area}\0${initialTarget.filePath}` : ''
|
|
const [screenState, setScreenState] = useState<ReviewScreenState>({ kind: 'loading' })
|
|
const [filter, setFilter] = useState<MobileDiffReviewQueueFilter>(initialFilter)
|
|
const [currentIndex, setCurrentIndex] = useState(0)
|
|
const [activeHunkIndex, setActiveHunkIndex] = useState<number | null>(null)
|
|
const [composer, setComposer] = useState<ComposerState | null>(null)
|
|
const [composerBody, setComposerBody] = useState('')
|
|
const [actionError, setActionError] = useState<string | null>(null)
|
|
const [busyAction, setBusyAction] = useState<string | null>(null)
|
|
const [discardTarget, setDiscardTarget] = useState<MobileDiffReviewQueueItem | null>(null)
|
|
const [showOverflow, setShowOverflow] = useState(false)
|
|
const [sendSheet, setSendSheet] = useState<SendSheetState | null>(null)
|
|
const [showCompletion, setShowCompletion] = useState(false)
|
|
const worktreeLabel = getWorktreeLabel(name, worktreeId)
|
|
|
|
const loadReviewData = useCallback(async () => {
|
|
const generation = loadGenerationRef.current + 1
|
|
loadGenerationRef.current = generation
|
|
const isCurrent = () => generation === loadGenerationRef.current
|
|
if (!worktreeId) {
|
|
setScreenState({ kind: 'error', message: 'Missing worktree' })
|
|
return
|
|
}
|
|
// Why (F10): a loaded review outlives a blip — the waiting state is for a screen with nothing
|
|
// to show, and this branch (not the one below it) is the one a drop actually reaches.
|
|
const keepReady = (fallback: ReviewScreenState) => (prev: ReviewScreenState) =>
|
|
prev.kind === 'ready' ? prev : fallback
|
|
if (!client || connState !== 'connected') {
|
|
setScreenState(keepReady({ kind: 'error', message: 'Waiting for desktop...' }))
|
|
return
|
|
}
|
|
setScreenState(keepReady({ kind: 'loading' }))
|
|
try {
|
|
const nextState = await loadMobileDiffReviewSnapshot(client, worktreeId)
|
|
if (!isCurrent()) {
|
|
return
|
|
}
|
|
setScreenState(nextState)
|
|
setActionError(nextState.kind === 'ready' ? (nextState.branchError ?? null) : null)
|
|
} catch (err) {
|
|
if (isCurrent()) {
|
|
// Why (F10): a failed refresh after reconnect must not destroy the review
|
|
// already on screen; the error state is for a screen with nothing to show.
|
|
setScreenState(
|
|
keepReady({
|
|
kind: 'error',
|
|
message: err instanceof Error ? err.message : 'Unable to load review'
|
|
})
|
|
)
|
|
}
|
|
}
|
|
}, [client, connState, worktreeId])
|
|
|
|
useEffect(() => {
|
|
void loadReviewData()
|
|
}, [loadReviewData])
|
|
|
|
const queue = useMemo(() => {
|
|
if (screenState.kind !== 'ready') {
|
|
return []
|
|
}
|
|
const branchEntries =
|
|
screenState.branchCompare && canOpenMobileBranchCompareDiff(screenState.branchCompare.summary)
|
|
? screenState.branchCompare.entries
|
|
: []
|
|
return buildMobileDiffReviewQueue({
|
|
worktreeId,
|
|
statusEntries: screenState.status.entries,
|
|
branchEntries,
|
|
branchHeadOid: screenState.branchCompare?.summary.headOid,
|
|
branchMergeBase: screenState.branchCompare?.summary.mergeBase,
|
|
comments: screenState.comments,
|
|
reviewState: screenState.reviewState
|
|
})
|
|
}, [screenState, worktreeId])
|
|
|
|
const filteredQueue = useMemo(() => filterMobileDiffReviewQueue(queue, filter), [filter, queue])
|
|
const currentItem = filteredQueue[currentIndex] ?? null
|
|
const reviewedCount = queue.filter((item) => item.isReviewed).length
|
|
const unsentComments =
|
|
screenState.kind === 'ready' ? getUnsentMobileDiffComments(screenState.comments) : []
|
|
const reviewedUnstagedCount = queue.filter(
|
|
(item) => item.scope === 'unstaged' && item.isReviewed && item.canStage
|
|
).length
|
|
|
|
useEffect(() => {
|
|
seededInitialTargetRef.current = false
|
|
}, [initialTargetKey])
|
|
|
|
useEffect(() => {
|
|
if (seededInitialTargetRef.current || filteredQueue.length === 0) {
|
|
return
|
|
}
|
|
seededInitialTargetRef.current = true
|
|
// Why: review data loads asynchronously; seed the tapped file only after the
|
|
// first real queue exists so the clamp effect cannot reset it back to zero.
|
|
setCurrentIndex(findMobileDiffReviewInitialIndex(filteredQueue, initialTarget))
|
|
}, [filteredQueue, initialTarget])
|
|
|
|
useEffect(() => {
|
|
if (filteredQueue.length === 0) {
|
|
setCurrentIndex(0)
|
|
return
|
|
}
|
|
if (currentIndex >= filteredQueue.length) {
|
|
setCurrentIndex(filteredQueue.length - 1)
|
|
}
|
|
}, [currentIndex, filteredQueue.length])
|
|
|
|
const diffState = useMobileDiffReviewDiffLoading({
|
|
client,
|
|
connState,
|
|
worktreeId,
|
|
currentItem,
|
|
screenState,
|
|
setActiveHunkIndex
|
|
})
|
|
|
|
const commentsForCurrentItem = useMemo(() => {
|
|
if (!currentItem || screenState.kind !== 'ready') {
|
|
return []
|
|
}
|
|
return screenState.comments.filter((comment) =>
|
|
mobileDiffReviewCommentMatchesItem(comment, currentItem)
|
|
)
|
|
}, [currentItem, screenState])
|
|
|
|
const staleCommentIds = useMemo(
|
|
() =>
|
|
new Set(
|
|
commentsForCurrentItem
|
|
.filter(
|
|
(comment) =>
|
|
currentItem &&
|
|
comment.diffIdentity !== undefined &&
|
|
comment.diffIdentity !== currentItem.diffIdentity
|
|
)
|
|
.map((comment) => comment.id)
|
|
),
|
|
[commentsForCurrentItem, currentItem]
|
|
)
|
|
|
|
const commentsByLine = useMemo(() => {
|
|
const map = new Map<number, DiffComment[]>()
|
|
for (const comment of commentsForCurrentItem) {
|
|
const list = map.get(comment.lineNumber) ?? []
|
|
list.push(comment)
|
|
map.set(comment.lineNumber, list)
|
|
}
|
|
return map
|
|
}, [commentsForCurrentItem])
|
|
|
|
// Head branch + SHA for the PR sidebar come from git.status (the review snapshot),
|
|
// not the branchCompare base ref. headOid is the branch-compare fallback for the SHA.
|
|
const prSidebarBranch = screenState.kind === 'ready' ? (screenState.status.branch ?? null) : null
|
|
const prSidebarHeadSha =
|
|
screenState.kind === 'ready'
|
|
? (screenState.status.head ?? screenState.branchCompare?.summary.headOid ?? null)
|
|
: null
|
|
const prSidebar = useMobilePrSidebarController({
|
|
client,
|
|
connState,
|
|
worktreeId,
|
|
branch: prSidebarBranch,
|
|
headSha: prSidebarHeadSha
|
|
})
|
|
|
|
const interactions = useMobileDiffReviewInteractions({
|
|
client,
|
|
connState,
|
|
hostId,
|
|
worktreeId,
|
|
screenState,
|
|
diffState,
|
|
currentItem,
|
|
queue,
|
|
filteredQueue,
|
|
filter,
|
|
currentIndex,
|
|
activeHunkIndex,
|
|
composer,
|
|
composerBody,
|
|
listRef,
|
|
setScreenState,
|
|
setFilter,
|
|
setCurrentIndex,
|
|
setActiveHunkIndex,
|
|
setComposer,
|
|
setComposerBody,
|
|
setActionError,
|
|
setBusyAction,
|
|
setSendSheet,
|
|
setShowCompletion,
|
|
loadReviewData,
|
|
onOpenSession,
|
|
onReconnect
|
|
})
|
|
|
|
return {
|
|
...interactions,
|
|
...prSidebar,
|
|
// Exposed so the screen can thread the RPC client + worktree into the PR
|
|
// sidebar's lazy check-detail fetches (U5) and mutation actions (U6).
|
|
client,
|
|
connState,
|
|
worktreeId,
|
|
prSidebarBranch,
|
|
prSidebarHeadSha,
|
|
actionError,
|
|
activeHunkIndex,
|
|
busyAction,
|
|
commentsByLine,
|
|
composer,
|
|
composerBody,
|
|
currentIndex,
|
|
currentItem,
|
|
diffState,
|
|
discardTarget,
|
|
fileNotes: commentsByLine.get(0) ?? [],
|
|
filter,
|
|
filteredQueue,
|
|
listRef,
|
|
queue,
|
|
reviewedCount,
|
|
reviewedUnstagedCount,
|
|
screenState,
|
|
sendSheet,
|
|
setComposerBody,
|
|
setDiscardTarget,
|
|
setSendSheet,
|
|
setShowCompletion,
|
|
setShowOverflow,
|
|
showCompletion,
|
|
showOverflow,
|
|
staleCommentIds,
|
|
unsentComments,
|
|
worktreeLabel
|
|
}
|
|
}
|