mirror of
https://github.com/stablyai/orca.git
synced 2026-09-27 08:02:35 +00:00
273 lines
11 KiB
TypeScript
273 lines
11 KiB
TypeScript
import { useMemo, useState } from 'react'
|
|
import { Keyboard } from 'react-native'
|
|
import { getComposerRepoWorktreeBranches } from '../../../src/shared/composer-branch-selection'
|
|
import { getProjectIdentityKey } from '../../../src/shared/project-host-setup-projection'
|
|
import { shouldPreserveWorkspaceSourceOnRepoChange } from '../../../src/shared/new-workspace/workspace-source'
|
|
import type { SmartModeAvailabilityInput } from '../tasks/mobile-smart-source-modes'
|
|
import { deriveRepoSlug, type PasteRepoCandidate } from '../tasks/smart-source-paste-intent'
|
|
import { useMobileComposerSource } from '../tasks/use-mobile-composer-source'
|
|
import { useNewWorktreeRuntimeCapabilities } from '../tasks/worktree-create-capability'
|
|
import {
|
|
buildRetiredWorktreeNamesRefreshKey,
|
|
useRetiredWorktreeNames
|
|
} from '../worktree/use-retired-worktree-names'
|
|
import { BottomDrawerModalHost } from './bottom-drawer-modal-host'
|
|
import {
|
|
getMobileWorkspaceRepoBadgeColor,
|
|
type MobileWorkspaceRepo,
|
|
type NewWorktreeModalProps
|
|
} from './new-worktree-modal-types'
|
|
import {
|
|
buildNewWorkspaceProjectOptions,
|
|
buildNewWorkspaceRunTargetOptions,
|
|
getNewWorkspaceRunTarget
|
|
} from './new-workspace-project-targets'
|
|
import { NewWorktreeFormSheet } from './NewWorktreeFormSheet'
|
|
import { NewWorktreeModalDrawers } from './NewWorktreeModalDrawers'
|
|
import { useNewWorkspaceAgentSelection } from './use-new-workspace-agent-selection'
|
|
import { useNewWorkspaceCreateSubmit } from './use-new-workspace-create-submit'
|
|
import { useNewWorkspaceExecutionTarget } from './use-new-workspace-execution-target'
|
|
import { useNewWorkspaceRepositories } from './use-new-workspace-repositories'
|
|
import { useNewWorkspaceRuntimeContext } from './use-new-workspace-runtime-context'
|
|
import { useNewWorkspaceSetupScript } from './use-new-workspace-setup-script'
|
|
import { useNewWorktreeDrawerNavigation } from './use-new-worktree-drawer-navigation'
|
|
|
|
export function NewWorktreeModal(props: NewWorktreeModalProps) {
|
|
// Why: each drawer opening is a fresh form session; remounting resets local
|
|
// form state before paint instead of clearing it in a visible-prop Effect.
|
|
// State, not a ref: react-native-screens freezes a blurred screen by suspending
|
|
// this subtree, and a counter bumped during a render React then throws away
|
|
// would restart the session for an opening that never committed.
|
|
const [session, setSession] = useState({ openEpoch: 0, visible: props.visible })
|
|
if (session.visible !== props.visible) {
|
|
setSession({
|
|
openEpoch: props.visible ? session.openEpoch + 1 : session.openEpoch,
|
|
visible: props.visible
|
|
})
|
|
}
|
|
|
|
// Why: key the session on the HOST, never on the RpcClient object. A reconnect,
|
|
// forceReconnect, or foreground revival swaps that object for the same host
|
|
// (see useHostClient), and keying on it silently remounted this form mid-edit
|
|
// and threw away the picked source. Every client-scoped hook below already
|
|
// drops responses from a superseded client, so no remount is needed for that.
|
|
return <NewWorktreeModalContent key={`${session.openEpoch}:${props.hostId}`} {...props} />
|
|
}
|
|
|
|
function NewWorktreeModalContent(props: NewWorktreeModalProps) {
|
|
const {
|
|
visible,
|
|
client,
|
|
hostId,
|
|
existingWorktreePaths,
|
|
existingWorktrees,
|
|
openExternalUrl,
|
|
onCreated,
|
|
onClose
|
|
} = props
|
|
const { repos, selectedRepo, setSelectedRepo, loading } = useNewWorkspaceRepositories({
|
|
client,
|
|
hostId,
|
|
visible
|
|
})
|
|
const navigation = useNewWorktreeDrawerNavigation(visible)
|
|
const [note, setNote] = useState('')
|
|
const [error, setError] = useState('')
|
|
const runtime = useNewWorkspaceRuntimeContext(client, visible, hostId)
|
|
const { tasksSupported, hostPlatform, getWorktreeCreateCutoverSupport } =
|
|
useNewWorktreeRuntimeCapabilities(client, visible)
|
|
const selectedRepoConnectionId = selectedRepo?.connectionId ?? null
|
|
const executionTarget = useNewWorkspaceExecutionTarget({
|
|
client,
|
|
connectionId: selectedRepoConnectionId,
|
|
visible
|
|
})
|
|
const setupScript = useNewWorkspaceSetupScript({ client, selectedRepo })
|
|
const selectedRepoWorktreeBranches = useMemo(
|
|
() => getComposerRepoWorktreeBranches(existingWorktrees ?? [], selectedRepo?.id ?? null),
|
|
[existingWorktrees, selectedRepo]
|
|
)
|
|
const composer = useMobileComposerSource({
|
|
client,
|
|
selectedRepoId: selectedRepo?.id ?? null,
|
|
worktreeBranches: selectedRepoWorktreeBranches,
|
|
onError: setError
|
|
})
|
|
const agentSelection = useNewWorkspaceAgentSelection({
|
|
visible,
|
|
runtimeSettings: runtime.runtimeSettings,
|
|
detectedAgentIds: executionTarget.detectedAgentIds
|
|
})
|
|
const retiredNamesRefreshKey = useMemo(
|
|
() => buildRetiredWorktreeNamesRefreshKey(existingWorktreePaths),
|
|
[existingWorktreePaths]
|
|
)
|
|
const retiredWorktreeNames = useRetiredWorktreeNames(
|
|
client,
|
|
selectedRepo?.id,
|
|
retiredNamesRefreshKey
|
|
)
|
|
const createSubmit = useNewWorkspaceCreateSubmit({
|
|
client,
|
|
selectedRepo,
|
|
selectedAgent: agentSelection.selectedAgent,
|
|
setSelectedAgent: agentSelection.setSelectedAgent,
|
|
setAgentOverridden: agentSelection.setAgentOverridden,
|
|
runtimeSettings: runtime.runtimeSettings,
|
|
setRuntimeSettings: runtime.setRuntimeSettings,
|
|
detectedAgentIds: executionTarget.detectedAgentIds,
|
|
sshGate: executionTarget.sshGate,
|
|
composer,
|
|
note,
|
|
existingWorktreePaths,
|
|
retiredWorktreeNames,
|
|
setupCommand: setupScript.setupCommand,
|
|
setupTrust: setupScript.setupTrust,
|
|
setupRunPolicy: setupScript.setupRunPolicy,
|
|
setupDecisionChoice: setupScript.setupDecisionChoice,
|
|
runSetup: setupScript.runSetup,
|
|
trustedOrcaHooks: runtime.trustedOrcaHooks,
|
|
setTrustedOrcaHooks: runtime.setTrustedOrcaHooks,
|
|
getWorktreeCreateCutoverSupport,
|
|
transitionDrawer: navigation.transitionDrawer,
|
|
setError,
|
|
onCreated,
|
|
onClose
|
|
})
|
|
|
|
const selectedRepoIsGit = selectedRepo ? selectedRepo.kind !== 'folder' : true
|
|
const sourceAvailability: SmartModeAvailabilityInput = {
|
|
textOnly: selectedRepo != null && !selectedRepoIsGit,
|
|
tasksSupported,
|
|
hasRepo: selectedRepo != null,
|
|
githubAvailable: runtime.availableProviders.includes('github'),
|
|
gitlabAvailable: runtime.availableProviders.includes('gitlab'),
|
|
linearAvailable: runtime.availableProviders.includes('linear')
|
|
}
|
|
const pasteRepos = useMemo<PasteRepoCandidate[]>(
|
|
() =>
|
|
repos.map((repo) => ({
|
|
id: repo.id,
|
|
displayName: repo.displayName,
|
|
slug: deriveRepoSlug(repo)
|
|
})),
|
|
[repos]
|
|
)
|
|
const projectPickerItems = useMemo(() => buildNewWorkspaceProjectOptions(repos), [repos])
|
|
const selectedProjectId = selectedRepo ? getProjectIdentityKey(selectedRepo) : null
|
|
const selectedProject =
|
|
projectPickerItems.find((project) => project.id === selectedProjectId) ?? null
|
|
const runTargetPickerItems = useMemo(
|
|
() => buildNewWorkspaceRunTargetOptions(repos, selectedProjectId, hostPlatform),
|
|
[hostPlatform, repos, selectedProjectId]
|
|
)
|
|
const selectedRunTarget = selectedRepo
|
|
? getNewWorkspaceRunTarget(selectedRepo, hostPlatform)
|
|
: null
|
|
const needsSetupChoice = Boolean(setupScript.setupCommand) && setupScript.setupRunPolicy === 'ask'
|
|
const canCreate =
|
|
selectedRepo != null &&
|
|
!createSubmit.creating &&
|
|
!executionTarget.sshGate.requiresConnection &&
|
|
(!needsSetupChoice || setupScript.setupDecisionChoice != null)
|
|
|
|
function openPicker(view: 'project' | 'runTarget' | 'agent'): void {
|
|
Keyboard.dismiss()
|
|
navigation.transitionDrawer(view)
|
|
}
|
|
|
|
function selectRepo(repo: MobileWorkspaceRepo, clearRepoScopedSource: boolean): void {
|
|
const repoChanged = repo.id !== selectedRepo?.id
|
|
setSelectedRepo(repo)
|
|
if (
|
|
clearRepoScopedSource &&
|
|
repoChanged &&
|
|
!shouldPreserveWorkspaceSourceOnRepoChange(composer.linkedWorkItem)
|
|
) {
|
|
composer.handleClearSmartNameSelection()
|
|
}
|
|
}
|
|
|
|
function requestClose(): void {
|
|
if (navigation.drawerView === 'form') {
|
|
onClose()
|
|
} else if (navigation.drawerView === 'trust') {
|
|
createSubmit.closeSetupTrust()
|
|
} else {
|
|
navigation.transitionDrawer('form')
|
|
}
|
|
}
|
|
|
|
return (
|
|
<BottomDrawerModalHost visible={visible} onRequestClose={requestClose}>
|
|
<NewWorktreeFormSheet
|
|
visible={navigation.formSheetVisible}
|
|
interactive={navigation.formSheetInteractive}
|
|
loading={loading}
|
|
hasRepos={repos.length > 0}
|
|
project={selectedProject}
|
|
runTarget={selectedRunTarget}
|
|
projectBadgeColor={selectedRepo ? getMobileWorkspaceRepoBadgeColor(selectedRepo) : null}
|
|
selectedRepoIsGit={selectedRepoIsGit}
|
|
selectedRepoConnectionId={selectedRepoConnectionId}
|
|
selectedRepoName={selectedRepo?.displayName ?? 'Remote repository'}
|
|
sshGate={executionTarget.sshGate}
|
|
composer={composer}
|
|
selectedAgent={agentSelection.selectedAgent}
|
|
showAdvanced={setupScript.showAdvanced}
|
|
note={note}
|
|
setupCommand={setupScript.setupCommand}
|
|
setupSource={setupScript.setupSource}
|
|
setupRunPolicy={setupScript.setupRunPolicy}
|
|
setupDecisionChoice={setupScript.setupDecisionChoice}
|
|
runSetup={setupScript.runSetup}
|
|
error={error}
|
|
creating={createSubmit.creating}
|
|
canCreate={canCreate}
|
|
onClose={onClose}
|
|
onOpenExternalUrl={openExternalUrl}
|
|
onOpenProject={() => openPicker('project')}
|
|
onOpenRunTarget={() => openPicker('runTarget')}
|
|
onOpenSource={navigation.openSourceDrawer}
|
|
onClearError={() => setError('')}
|
|
onConnect={() => void executionTarget.connect()}
|
|
onOpenAgent={() => openPicker('agent')}
|
|
onShowAdvancedChange={setupScript.setShowAdvanced}
|
|
onNoteChange={setNote}
|
|
onSetupDecisionChange={setupScript.setSetupDecisionChoice}
|
|
onRunSetupChange={setupScript.setRunSetup}
|
|
onCreate={() => void createSubmit.create()}
|
|
/>
|
|
|
|
<NewWorktreeModalDrawers
|
|
visible={visible}
|
|
drawerView={navigation.drawerView}
|
|
client={client}
|
|
composer={composer}
|
|
sourceAvailability={sourceAvailability}
|
|
selectedRepo={selectedRepo}
|
|
repos={repos}
|
|
pasteRepos={pasteRepos}
|
|
sshReady={!executionTarget.sshGate.requiresConnection}
|
|
projectPickerItems={projectPickerItems}
|
|
selectedProjectId={selectedProjectId}
|
|
runTargetPickerItems={runTargetPickerItems}
|
|
pickerAgentOptions={agentSelection.pickerAgentOptions}
|
|
selectedAgent={agentSelection.selectedAgent}
|
|
setupTrustPrompt={createSubmit.setupTrustPrompt}
|
|
creating={createSubmit.creating}
|
|
onSourceRepoChange={(repo) => selectRepo(repo, false)}
|
|
onRepoChange={(repo) => selectRepo(repo, true)}
|
|
onAgentChange={(agent) => {
|
|
agentSelection.setAgentOverridden(true)
|
|
agentSelection.setSelectedAgent(agent)
|
|
}}
|
|
onTransitionToForm={() => navigation.transitionDrawer('form')}
|
|
onApproveSetupTrust={(alwaysTrust) => void createSubmit.approveSetupTrust(alwaysTrust)}
|
|
onSkipSetupTrust={createSubmit.skipSetupTrust}
|
|
onCloseSetupTrust={createSubmit.closeSetupTrust}
|
|
/>
|
|
</BottomDrawerModalHost>
|
|
)
|
|
}
|