mirror of
https://github.com/stablyai/orca.git
synced 2026-09-25 08:02:31 +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.
295 lines
10 KiB
TypeScript
295 lines
10 KiB
TypeScript
import { useMemo } from 'react'
|
|
import { KeyboardAvoidingView, Platform, Pressable, Text, TextInput, View } from 'react-native'
|
|
import { Check, Copy, FileText, Plus, Send, Trash2, X } from 'lucide-react-native'
|
|
import type { DiffComment } from '../../../src/shared/diff-comment-types'
|
|
import { colors } from '../theme/mobile-theme'
|
|
import type { ActionSheetAction } from './ActionSheetModal'
|
|
import { ActionSheetModal } from './ActionSheetModal'
|
|
import { BottomDrawer } from './BottomDrawer'
|
|
import { ConfirmModal } from './ConfirmModal'
|
|
import { mobileReviewCountLabel } from '../session/mobile-diff-review-screen-model'
|
|
import type { useMobileDiffReviewController } from '../session/use-mobile-diff-review-controller'
|
|
import { mobileDiffReviewStyles as styles } from './mobile-diff-review-screen-styles'
|
|
|
|
type Props = {
|
|
controller: ReturnType<typeof useMobileDiffReviewController>
|
|
}
|
|
|
|
export function MobileDiffReviewDrawers({ controller }: Props) {
|
|
const sendActions = useSendActions(controller)
|
|
const overflowActions = useOverflowActions(controller)
|
|
return (
|
|
<>
|
|
<ActionSheetModal
|
|
visible={controller.showOverflow}
|
|
title="Review Actions"
|
|
message={
|
|
controller.reviewedUnstagedCount > 0
|
|
? `${controller.reviewedUnstagedCount} reviewed unstaged files can be staged`
|
|
: undefined
|
|
}
|
|
actions={overflowActions}
|
|
onClose={() => controller.setShowOverflow(false)}
|
|
/>
|
|
<ActionSheetModal
|
|
visible={controller.sendSheet !== null}
|
|
title="Send Notes"
|
|
message={sendSheetMessage(controller)}
|
|
actions={sendActions}
|
|
onClose={() => controller.setSendSheet(null)}
|
|
/>
|
|
<ConfirmModal
|
|
visible={controller.discardTarget !== null}
|
|
title="Discard File"
|
|
message={
|
|
controller.discardTarget
|
|
? `Discard changes to "${controller.discardTarget.filePath}"? This cannot be undone.`
|
|
: undefined
|
|
}
|
|
confirmLabel="Discard"
|
|
destructive
|
|
onConfirm={() => {
|
|
const target = controller.discardTarget
|
|
controller.setDiscardTarget(null)
|
|
if (target) {
|
|
void controller.runGitMutation('git.discard', target)
|
|
}
|
|
}}
|
|
onCancel={() => controller.setDiscardTarget(null)}
|
|
/>
|
|
<NoteComposerDrawer controller={controller} />
|
|
<CompletionDrawer controller={controller} />
|
|
</>
|
|
)
|
|
}
|
|
|
|
function useSendActions(controller: ReturnType<typeof useMobileDiffReviewController>) {
|
|
return useMemo<ActionSheetAction[]>(() => {
|
|
const comments = controller.unsentComments
|
|
const terminalActions =
|
|
controller.sendSheet?.kind === 'ready' || controller.sendSheet?.kind === 'error'
|
|
? controller.sendSheet.terminals.map((terminal) => ({
|
|
label: `${terminal.title || 'Terminal'} (${terminal.terminal.slice(0, 6)})`,
|
|
icon: Send,
|
|
disabled: comments.length === 0,
|
|
skipAutoClose: true,
|
|
onPress: () => void controller.sendPromptToTerminal(terminal.terminal, comments)
|
|
}))
|
|
: []
|
|
return [
|
|
...terminalActions,
|
|
{
|
|
label: 'New Agent Session',
|
|
icon: Plus,
|
|
disabled: comments.length === 0,
|
|
skipAutoClose: true,
|
|
onPress: () => void controller.createTerminalAndSend(comments)
|
|
},
|
|
{
|
|
label: 'Copy Notes',
|
|
icon: Copy,
|
|
disabled:
|
|
controller.screenState.kind !== 'ready' || controller.screenState.comments.length === 0,
|
|
onPress: () => void controller.copyNotes()
|
|
}
|
|
]
|
|
}, [controller])
|
|
}
|
|
|
|
function useOverflowActions(controller: ReturnType<typeof useMobileDiffReviewController>) {
|
|
return useMemo<ActionSheetAction[]>(
|
|
() => [
|
|
{
|
|
label: 'Copy Notes',
|
|
icon: Copy,
|
|
disabled:
|
|
controller.screenState.kind !== 'ready' || controller.screenState.comments.length === 0,
|
|
onPress: () => void controller.copyNotes()
|
|
},
|
|
{
|
|
label: 'Send Unsent Notes',
|
|
icon: Send,
|
|
disabled: controller.unsentComments.length === 0,
|
|
skipAutoClose: true,
|
|
onPress: () => void controller.openSendSheet()
|
|
},
|
|
{
|
|
label: 'Clear Sent Notes',
|
|
icon: Trash2,
|
|
disabled:
|
|
controller.screenState.kind !== 'ready' ||
|
|
controller.screenState.comments.every((comment) => comment.sentAt === undefined),
|
|
skipAutoClose: true,
|
|
onPress: () => void controller.clearSentNotes()
|
|
},
|
|
{
|
|
label: 'Stage Reviewed Files',
|
|
icon: Check,
|
|
disabled: controller.reviewedUnstagedCount === 0 || controller.busyAction !== null,
|
|
skipAutoClose: true,
|
|
onPress: () => void controller.stageReviewedFiles()
|
|
},
|
|
{
|
|
label: 'Mark Unreviewed',
|
|
icon: X,
|
|
disabled:
|
|
controller.screenState.kind !== 'ready' ||
|
|
!controller.currentItem ||
|
|
!controller.currentItem.isReviewed,
|
|
skipAutoClose: true,
|
|
onPress: () => void controller.markUnreviewed()
|
|
},
|
|
{
|
|
label: 'Open in Session',
|
|
icon: FileText,
|
|
disabled: !controller.currentItem || controller.currentItem.scope === 'branch',
|
|
onPress: () => void controller.openInSession()
|
|
}
|
|
],
|
|
[controller]
|
|
)
|
|
}
|
|
|
|
function sendSheetMessage(
|
|
controller: ReturnType<typeof useMobileDiffReviewController>
|
|
): string | undefined {
|
|
return controller.sendSheet?.kind === 'loading'
|
|
? 'Loading agent sessions...'
|
|
: controller.sendSheet?.kind === 'error'
|
|
? controller.sendSheet.message
|
|
: `${controller.unsentComments.length} unsent notes`
|
|
}
|
|
|
|
function NoteComposerDrawer({ controller }: Props) {
|
|
const composer = controller.composer
|
|
return (
|
|
<BottomDrawer visible={composer !== null} onClose={controller.closeComposer}>
|
|
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
|
|
<View style={styles.composerHeader}>
|
|
<View>
|
|
<Text style={styles.drawerTitle}>
|
|
{composer?.mode === 'edit' ? 'Edit Note' : 'Add Note'}
|
|
</Text>
|
|
<Text style={styles.drawerSubtitle}>
|
|
{composer?.mode === 'create' && composer.lineNumber > 0
|
|
? `Line ${composer.lineNumber}`
|
|
: 'File note'}
|
|
</Text>
|
|
</View>
|
|
<Pressable
|
|
style={({ pressed }) => [styles.iconButton, pressed && styles.iconButtonPressed]}
|
|
onPress={controller.closeComposer}
|
|
accessibilityRole="button"
|
|
accessibilityLabel="Cancel note"
|
|
>
|
|
<X size={18} color={colors.textPrimary} strokeWidth={2.2} />
|
|
</Pressable>
|
|
</View>
|
|
<TextInput
|
|
style={styles.composerInput}
|
|
value={controller.composerBody}
|
|
onChangeText={controller.setComposerBody}
|
|
multiline
|
|
autoFocus
|
|
placeholder="Review note"
|
|
placeholderTextColor={colors.textMuted}
|
|
accessibilityLabel={composerLabel(composer)}
|
|
/>
|
|
<View style={styles.drawerButtonRow}>
|
|
{composer?.mode === 'edit' ? (
|
|
<DeleteNoteButton onPress={controller.deleteComment} />
|
|
) : null}
|
|
<SaveNoteButton controller={controller} composer={composer} />
|
|
</View>
|
|
</KeyboardAvoidingView>
|
|
</BottomDrawer>
|
|
)
|
|
}
|
|
|
|
function composerLabel(
|
|
composer: { mode: 'create'; lineNumber: number } | { mode: 'edit'; comment: DiffComment } | null
|
|
): string {
|
|
return composer?.mode === 'create' && composer.lineNumber > 0
|
|
? `Save note on line ${composer.lineNumber}`
|
|
: 'Review note'
|
|
}
|
|
|
|
function DeleteNoteButton({ onPress }: { onPress: () => Promise<void> }) {
|
|
return (
|
|
<Pressable
|
|
style={({ pressed }) => [styles.secondaryButton, pressed && styles.buttonPressed]}
|
|
onPress={() => void onPress()}
|
|
accessibilityRole="button"
|
|
accessibilityLabel="Delete note"
|
|
>
|
|
<Trash2 size={14} color={colors.statusRed} strokeWidth={2.2} />
|
|
<Text style={styles.destructiveText}>Delete</Text>
|
|
</Pressable>
|
|
)
|
|
}
|
|
|
|
function SaveNoteButton({
|
|
controller,
|
|
composer
|
|
}: {
|
|
controller: ReturnType<typeof useMobileDiffReviewController>
|
|
composer: ReturnType<typeof useMobileDiffReviewController>['composer']
|
|
}) {
|
|
const disabled = controller.composerBody.trim().length === 0
|
|
return (
|
|
<Pressable
|
|
style={({ pressed }) => [
|
|
styles.primaryButton,
|
|
disabled && styles.buttonDisabled,
|
|
pressed && styles.buttonPressed
|
|
]}
|
|
disabled={disabled}
|
|
onPress={() => void controller.saveComposer()}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={composerLabel(composer)}
|
|
>
|
|
<Check size={14} color={colors.bgBase} strokeWidth={2.2} />
|
|
<Text style={styles.primaryButtonText}>Save</Text>
|
|
</Pressable>
|
|
)
|
|
}
|
|
|
|
function CompletionDrawer({ controller }: Props) {
|
|
const noteCount =
|
|
controller.screenState.kind === 'ready' ? controller.screenState.comments.length : 0
|
|
return (
|
|
<BottomDrawer
|
|
visible={controller.showCompletion}
|
|
onClose={() => controller.setShowCompletion(false)}
|
|
>
|
|
<Text style={styles.drawerTitle}>Review Complete</Text>
|
|
<Text style={styles.drawerSubtitle}>
|
|
{mobileReviewCountLabel(controller.queue.length, 'file', 'files')} reviewed,{' '}
|
|
{mobileReviewCountLabel(noteCount, 'note', 'notes')}
|
|
</Text>
|
|
<View style={styles.drawerButtonRow}>
|
|
<Pressable
|
|
style={({ pressed }) => [styles.secondaryButton, pressed && styles.buttonPressed]}
|
|
disabled={controller.reviewedUnstagedCount === 0}
|
|
onPress={() => void controller.stageReviewedFiles()}
|
|
accessibilityRole="button"
|
|
accessibilityLabel="Stage reviewed files"
|
|
>
|
|
<Check size={14} color={colors.textSecondary} strokeWidth={2.2} />
|
|
<Text style={styles.secondaryButtonText}>Stage Reviewed</Text>
|
|
</Pressable>
|
|
<Pressable
|
|
style={({ pressed }) => [styles.primaryButton, pressed && styles.buttonPressed]}
|
|
disabled={controller.unsentComments.length === 0}
|
|
onPress={() => void controller.openSendSheet()}
|
|
accessibilityRole="button"
|
|
accessibilityLabel="Send notes to agent"
|
|
>
|
|
<Send size={14} color={colors.bgBase} strokeWidth={2.2} />
|
|
<Text style={styles.primaryButtonText}>Send Notes</Text>
|
|
</Pressable>
|
|
</View>
|
|
</BottomDrawer>
|
|
)
|
|
}
|