mirror of
https://github.com/stablyai/orca.git
synced 2026-09-25 08:02:31 +00:00
* fix(pr-comments): let users mark comment authors as bots for the Humans/Bots filter Some review bots post from regular user accounts that defeat both provider bot metadata and login heuristics, so their comments were misclassified as human. Adds a persisted prBotAuthorOverrides setting with a "Mark author as bot" comment action, applied consistently across desktop and mobile. Fixes #7597 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(pr-comments): address review feedback on bot-author overrides - Cap sanitized prBotAuthorOverrides at 500 entries so malformed payloads can't bloat GlobalSettings or slow comment classification - Reuse the shared normalizePRCommentAuthorLogin in isBotPRComment on desktop and mobile instead of duplicating the normalization inline - Pass botAuthorOverrides from CommentRow to CommentMoreMenu instead of re-subscribing per menu instance - Re-fetch mobile bot-author overrides alongside each PR refetch so they don't stay a stale one-shot snapshot for the whole session Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(pr-comments): harden bot author override sync * fix(pr-comments): bound and recover override updates * fix(pr-comments): merge overrides from canonical settings * fix(pr-comments): make bot override updates atomic * fix(pr-comments): surface rejected bot overrides * fix(i18n): translate bot override warning * fix(i18n): translate bot author actions --------- Co-authored-by: Dzmitry Bachko <dbachko@users.noreply.github.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
336 lines
10 KiB
TypeScript
336 lines
10 KiB
TypeScript
import { ActivityIndicator, Pressable, ScrollView, Text, View } from 'react-native'
|
|
import { RotateCw } from 'lucide-react-native'
|
|
import { colors } from '../theme/mobile-theme'
|
|
import type { PrSidebarState } from '../session/mobile-pr-sidebar-state'
|
|
import type { ConnectionState } from '../transport/types'
|
|
import type { RpcClient } from '../transport/rpc-client'
|
|
import { useMobilePrActions, type MobilePrActions } from '../session/use-mobile-pr-actions'
|
|
import {
|
|
useMobilePrCommentActions,
|
|
type MobilePrCommentActions
|
|
} from '../session/use-mobile-pr-comment-actions'
|
|
import {
|
|
useMobilePrTitleAction,
|
|
type MobilePrTitleAction
|
|
} from '../session/use-mobile-pr-title-action'
|
|
import { useMobilePrAiTriage, type MobilePrAiTriage } from '../session/use-mobile-pr-ai-triage'
|
|
import { usePRBotAuthorOverrides } from '../session/use-pr-bot-author-overrides'
|
|
import { buildFixChecksPrompt, buildResolveConflictsPrompt } from '../session/pr-ai-triage-prompt'
|
|
import { prSidebarRenderBranch } from './mobile-pr-sidebar-presentation'
|
|
import { mobilePrSidebarStyles as styles } from './pr-sidebar/mobile-pr-sidebar-styles'
|
|
import type { MobileGitStatusResult } from '../source-control/mobile-git-status'
|
|
import { PRSidebarHeader } from './pr-sidebar/PRSidebarHeader'
|
|
import { PRConflictingFilesSection } from './pr-sidebar/PRConflictingFilesSection'
|
|
import { PRActionsSection } from './pr-sidebar/PRActionsSection'
|
|
import { PRReviewersSection } from './pr-sidebar/PRReviewersSection'
|
|
import { PRChecksSection } from './pr-sidebar/PRChecksSection'
|
|
import { PRCommentsSection } from './pr-sidebar/PRCommentsSection'
|
|
import { PrSidebarCreateEmptyState } from './pr-sidebar/PrSidebarCreateEmptyState'
|
|
|
|
type Props = {
|
|
state: PrSidebarState
|
|
onRetry: () => void
|
|
// Re-fetches authoritative PR data after a successful mutation (U3/U6) or create.
|
|
refetch: () => void
|
|
// Threaded to sections for github.* fetches + mutations.
|
|
client: RpcClient | null
|
|
connState: ConnectionState
|
|
worktreeId: string
|
|
gitBranch: string | null
|
|
gitStatus: MobileGitStatusResult | null
|
|
headSha: string | null
|
|
bottomInset?: number
|
|
// Hub chrome already shows open-on-web; hide the in-body icon there.
|
|
showOpenOnWeb?: boolean
|
|
}
|
|
|
|
// Mutation hooks run unconditionally here and gate internally until a PR is ready.
|
|
export function MobilePRSidebar({
|
|
state,
|
|
onRetry,
|
|
refetch,
|
|
client,
|
|
connState,
|
|
worktreeId,
|
|
gitBranch,
|
|
gitStatus,
|
|
headSha,
|
|
bottomInset = 0,
|
|
showOpenOnWeb = true
|
|
}: Props) {
|
|
const branch = prSidebarRenderBranch(state)
|
|
// prNumber is 0 until ready; the hook gates on `ready` so it never fires early.
|
|
const prNumber = state.kind === 'ready' ? state.data.pr.number : 0
|
|
// Prefer the stable PRInfo.prRepo reference — cloning owner/repo each render
|
|
// reallocates and thrash-updates the mutation/comment/title hooks.
|
|
const prRepo = state.kind === 'ready' ? (state.data.pr.prRepo ?? null) : null
|
|
const actions = useMobilePrActions({
|
|
client,
|
|
connState,
|
|
worktreeId,
|
|
prNumber,
|
|
headSha,
|
|
prRepo,
|
|
refetch
|
|
})
|
|
const commentActions = useMobilePrCommentActions({
|
|
client,
|
|
connState,
|
|
worktreeId,
|
|
prNumber,
|
|
prRepo,
|
|
refetch
|
|
})
|
|
const titleAction = useMobilePrTitleAction({
|
|
client,
|
|
connState,
|
|
worktreeId,
|
|
prNumber,
|
|
prRepo,
|
|
refetch
|
|
})
|
|
const triage = useMobilePrAiTriage({ client, connState, worktreeId })
|
|
// Keyed on the PR payload identity so overrides re-fetch with each PR refetch
|
|
// instead of staying a stale one-shot snapshot for the whole session.
|
|
const botAuthorOverrides = usePRBotAuthorOverrides(
|
|
client,
|
|
connState,
|
|
state.kind === 'ready' ? state.data.details : null
|
|
)
|
|
|
|
return (
|
|
<ScrollView
|
|
style={{ flex: 1 }}
|
|
contentContainerStyle={[styles.scrollContent, { paddingBottom: bottomInset }]}
|
|
keyboardShouldPersistTaps="handled"
|
|
// Why: root-comment / reply composers sit at the bottom of this scroll
|
|
// area; without keyboard insets the focused field stays under the keyboard.
|
|
automaticallyAdjustKeyboardInsets
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
<PrSidebarContent
|
|
branch={branch}
|
|
state={state}
|
|
onRetry={onRetry}
|
|
refetch={refetch}
|
|
client={client}
|
|
connState={connState}
|
|
worktreeId={worktreeId}
|
|
gitBranch={gitBranch}
|
|
gitStatus={gitStatus}
|
|
actions={actions}
|
|
commentActions={commentActions}
|
|
titleAction={titleAction}
|
|
triage={triage}
|
|
botAuthorOverrides={botAuthorOverrides}
|
|
showOpenOnWeb={showOpenOnWeb}
|
|
/>
|
|
</ScrollView>
|
|
)
|
|
}
|
|
|
|
function PrSidebarContent({
|
|
branch,
|
|
state,
|
|
onRetry,
|
|
refetch,
|
|
client,
|
|
connState,
|
|
worktreeId,
|
|
gitBranch,
|
|
gitStatus,
|
|
actions,
|
|
commentActions,
|
|
titleAction,
|
|
triage,
|
|
showOpenOnWeb,
|
|
botAuthorOverrides
|
|
}: {
|
|
branch: ReturnType<typeof prSidebarRenderBranch>
|
|
state: PrSidebarState
|
|
onRetry: () => void
|
|
refetch: () => void
|
|
client: RpcClient | null
|
|
connState: ConnectionState
|
|
worktreeId: string
|
|
gitBranch: string | null
|
|
gitStatus: MobileGitStatusResult | null
|
|
actions: MobilePrActions
|
|
commentActions: MobilePrCommentActions
|
|
titleAction: MobilePrTitleAction
|
|
triage: MobilePrAiTriage
|
|
showOpenOnWeb: boolean
|
|
botAuthorOverrides: ReadonlySet<string>
|
|
}) {
|
|
if (branch === 'loading') {
|
|
return (
|
|
<View style={styles.stateArea}>
|
|
<ActivityIndicator color={colors.textSecondary} />
|
|
<Text style={styles.stateText}>Loading pull request…</Text>
|
|
</View>
|
|
)
|
|
}
|
|
if (branch === 'error') {
|
|
const message = state.kind === 'error' ? state.message : 'Something went wrong.'
|
|
return (
|
|
<View style={styles.stateArea}>
|
|
<Text style={styles.stateText}>{message}</Text>
|
|
<Pressable
|
|
style={styles.retryButton}
|
|
onPress={onRetry}
|
|
accessibilityRole="button"
|
|
accessibilityLabel="Retry loading pull request"
|
|
>
|
|
<RotateCw size={14} color={colors.textPrimary} strokeWidth={2.2} />
|
|
<Text style={styles.retryText}>Retry</Text>
|
|
</Pressable>
|
|
</View>
|
|
)
|
|
}
|
|
if (branch === 'blocked' || actions.blocked) {
|
|
// Permanent failure (R9): explanatory, no retry-encouragement styling. A
|
|
// mutation-time block (actions.blocked) routes here even from a ready state.
|
|
const message =
|
|
actions.blocked ??
|
|
(state.kind === 'blocked'
|
|
? state.message
|
|
: 'Not permitted — your GitHub account is not connected.')
|
|
return (
|
|
<View style={styles.stateArea}>
|
|
<Text style={styles.blockedText}>{message}</Text>
|
|
</View>
|
|
)
|
|
}
|
|
if (branch === 'none') {
|
|
// GitHub repo, but the current branch has no open PR — offer to create one
|
|
// (desktop parity) rather than showing a dead-end message.
|
|
return (
|
|
<PrSidebarCreateEmptyState
|
|
client={client}
|
|
worktreeId={worktreeId}
|
|
gitBranch={gitBranch}
|
|
gitStatus={gitStatus}
|
|
connState={connState}
|
|
onCreated={refetch}
|
|
/>
|
|
)
|
|
}
|
|
if (branch === 'ready' && state.kind === 'ready') {
|
|
return (
|
|
<PrSidebarSections
|
|
data={state.data}
|
|
client={client}
|
|
worktreeId={worktreeId}
|
|
actions={actions}
|
|
commentActions={commentActions}
|
|
titleAction={titleAction}
|
|
triage={triage}
|
|
refetch={refetch}
|
|
botAuthorOverrides={botAuthorOverrides}
|
|
showOpenOnWeb={showOpenOnWeb}
|
|
/>
|
|
)
|
|
}
|
|
return null
|
|
}
|
|
|
|
function PrSidebarSections({
|
|
data,
|
|
client,
|
|
worktreeId,
|
|
actions,
|
|
commentActions,
|
|
titleAction,
|
|
triage,
|
|
refetch,
|
|
showOpenOnWeb,
|
|
botAuthorOverrides
|
|
}: {
|
|
data: Extract<PrSidebarState, { kind: 'ready' }>['data']
|
|
client: RpcClient | null
|
|
worktreeId: string
|
|
actions: MobilePrActions
|
|
commentActions: MobilePrCommentActions
|
|
titleAction: MobilePrTitleAction
|
|
triage: MobilePrAiTriage
|
|
refetch: () => void
|
|
showOpenOnWeb: boolean
|
|
botAuthorOverrides: ReadonlySet<string>
|
|
}) {
|
|
const pr = data.pr
|
|
// Bind the triage launchers to this PR's data; the prompt builders are pure so
|
|
// building lazily inside launch() keeps a stale capture from leaking in.
|
|
const checksTriage = {
|
|
fixChecks: () =>
|
|
void triage.launch('fix-checks', () =>
|
|
buildFixChecksPrompt({
|
|
prNumber: pr.number,
|
|
prTitle: pr.title,
|
|
prUrl: pr.url,
|
|
checks: data.checks
|
|
})
|
|
),
|
|
isBusy: triage.isBusy('fix-checks'),
|
|
error: triage.error
|
|
}
|
|
const conflictsTriage = {
|
|
resolveConflicts: () =>
|
|
void triage.launch('resolve-conflicts', () =>
|
|
buildResolveConflictsPrompt({
|
|
prNumber: pr.number,
|
|
baseRef: pr.conflictSummary?.baseRef ?? pr.baseRefName ?? null,
|
|
files: pr.conflictSummary?.files ?? []
|
|
})
|
|
),
|
|
isBusy: triage.isBusy('resolve-conflicts'),
|
|
error: triage.error
|
|
}
|
|
// One card for identity + actions so the ready PR isn't a stack of thin
|
|
// duplicate blocks (badge row, title, branches, then another action band).
|
|
return (
|
|
<>
|
|
<View style={styles.section}>
|
|
<View style={styles.sectionBody}>
|
|
<PRSidebarHeader
|
|
pr={data.pr}
|
|
details={data.details}
|
|
titleAction={titleAction}
|
|
showOpenOnWeb={showOpenOnWeb}
|
|
bare
|
|
/>
|
|
<PRActionsSection
|
|
pr={data.pr}
|
|
actions={actions}
|
|
client={client}
|
|
worktreeId={worktreeId}
|
|
onUnlinked={refetch}
|
|
/>
|
|
</View>
|
|
</View>
|
|
{/* Own titled section when present; null otherwise (no empty chrome). */}
|
|
<PRConflictingFilesSection pr={data.pr} triage={conflictsTriage} />
|
|
<PRReviewersSection
|
|
details={data.details}
|
|
actions={actions}
|
|
client={client}
|
|
worktreeId={worktreeId}
|
|
/>
|
|
<PRChecksSection
|
|
checks={data.checks}
|
|
client={client}
|
|
worktreeId={worktreeId}
|
|
prRepo={data.pr.prRepo ?? null}
|
|
actions={actions}
|
|
triage={checksTriage}
|
|
/>
|
|
<PRCommentsSection
|
|
details={data.details}
|
|
prState={data.pr.state}
|
|
prRepo={data.pr.prRepo ?? null}
|
|
actions={commentActions}
|
|
botAuthorOverrides={botAuthorOverrides}
|
|
/>
|
|
</>
|
|
)
|
|
}
|