addons/isl/src/smartActions/SmartActionsMenu.tsxblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import {Button} from 'isl-components/Button';
b69ab319import {Icon} from 'isl-components/Icon';
b69ab3110import {Tooltip} from 'isl-components/Tooltip';
b69ab3111import {useAtomValue} from 'jotai';
b69ab3112import {Suspense, useState} from 'react';
b69ab3113import {randomId} from 'shared/utils';
b69ab3114import {tracker} from '../analytics';
b69ab3115import serverAPI from '../ClientToServerAPI';
b69ab3116import {diffCommentData} from '../codeReview/codeReviewAtoms';
b69ab3117import {diffSummary} from '../codeReview/CodeReviewInfo';
b69ab3118import {DropdownFields} from '../DropdownFields';
b69ab3119import {useFeatureFlagAsync, useFeatureFlagSync} from '../featureFlags';
b69ab3120import {T} from '../i18n';
b69ab3121import {Internal} from '../Internal';
b69ab3122import {BaseSplitButton} from '../stackEdit/ui/BaseSplitButton';
b69ab3123import type {CommitInfo} from '../types';
b69ab3124
b69ab3125import platform from '../platform';
b69ab3126import {repositoryInfo} from '../serverAPIState';
b69ab3127import './SmartActionsMenu.css';
b69ab3128
b69ab3129export function SmartActionsMenu({commit}: {commit?: CommitInfo}) {
b69ab3130 const [dropdownVisible, setDropdownVisible] = useState(false);
b69ab3131
b69ab3132 const smartActionsMenuEnabled = useFeatureFlagSync(Internal.featureFlags?.SmartActionsMenu);
b69ab3133 if (!smartActionsMenuEnabled || !Internal.smartActions?.showSmartActions) {
b69ab3134 return null;
b69ab3135 }
b69ab3136
b69ab3137 return (
b69ab3138 <Tooltip
b69ab3139 component={dismiss => {
b69ab3140 return (
b69ab3141 <Suspense fallback={<Icon icon="loading" />}>
b69ab3142 <SmartActions commit={commit} dismiss={dismiss} />
b69ab3143 </Suspense>
b69ab3144 );
b69ab3145 }}
b69ab3146 trigger="click"
b69ab3147 title={<T>Smart Actions...</T>}
b69ab3148 onVisible={() => setDropdownVisible(true)}
b69ab3149 onDismiss={() => setDropdownVisible(false)}
b69ab3150 group="smart-actions">
b69ab3151 <Button
b69ab3152 icon
b69ab3153 data-testid="smart-actions-button"
b69ab3154 onClick={() => tracker.track('SmartActionsMenuOpened')}
b69ab3155 className={'smart-actions-button' + (dropdownVisible ? ' dropdown-visible' : '')}>
b69ab3156 <Icon icon="lightbulb-sparkle" />
b69ab3157 </Button>
b69ab3158 </Tooltip>
b69ab3159 );
b69ab3160}
b69ab3161
b69ab3162function SmartActions({commit, dismiss}: {commit?: CommitInfo; dismiss: () => void}) {
b69ab3163 const actions = [];
b69ab3164
b69ab3165 const aiCommitSplitEnabled = useFeatureFlagAsync(Internal.featureFlags?.AICommitSplit);
b69ab3166 if (commit && aiCommitSplitEnabled) {
b69ab3167 actions.push(<AutoSplitButton key="auto-split" commit={commit} dismiss={dismiss} />);
b69ab3168 }
b69ab3169
b69ab3170 const aiResolveCommentsEnabled = useFeatureFlagAsync(
b69ab3171 Internal.featureFlags?.InlineCommentAIResolve,
b69ab3172 );
b69ab3173 // For now, only support this in VS Code
b69ab3174 if (aiResolveCommentsEnabled && commit?.diffId && platform.platformName === 'vscode') {
b69ab3175 actions.push(
b69ab3176 <ResolveCommentsButton
b69ab3177 key="resolve-comments"
b69ab3178 diffId={commit.diffId}
b69ab3179 filePathsSample={commit.filePathsSample}
b69ab3180 dismiss={dismiss}
b69ab3181 disabled={!commit.isDot}
b69ab3182 disabledReason="This action is only available for the current commit."
b69ab3183 />,
b69ab3184 );
b69ab3185 }
b69ab3186
b69ab3187 const aiResolveFailedSignalsEnabled = useFeatureFlagAsync(
b69ab3188 Internal.featureFlags?.AIResolveFailedSignals,
b69ab3189 );
b69ab3190 // For now, only support this in VS Code
b69ab3191 if (aiResolveFailedSignalsEnabled && commit?.diffId && platform.platformName === 'vscode') {
b69ab3192 actions.push(
b69ab3193 <ResolveFailedSignalsButton
b69ab3194 key="resolve-failed-signals"
b69ab3195 hash={commit.hash}
b69ab3196 diffId={commit.diffId}
b69ab3197 dismiss={dismiss}
b69ab3198 disabled={!commit.isDot}
b69ab3199 disabledReason="This action is only available for the current commit."
b69ab31100 />,
b69ab31101 );
b69ab31102 }
b69ab31103
b69ab31104 const aiGenerateTestsForModifiedCodeEnabled = useFeatureFlagAsync(
b69ab31105 Internal.featureFlags?.AIGenerateTestsForModifiedCode,
b69ab31106 );
b69ab31107 // For now, only support this in VS Code
b69ab31108 if (aiGenerateTestsForModifiedCodeEnabled && platform.platformName === 'vscode') {
b69ab31109 const enabled = !commit || commit.isDot; // Enabled for `uncommitted changes` or the `current commit`.
b69ab31110 actions.push(
b69ab31111 <GenerateTestsForModifiedCodeButton
b69ab31112 key="generate-tests"
b69ab31113 dismiss={dismiss}
b69ab31114 disabled={!enabled}
b69ab31115 disabledReason="This action is only available for the current commit and uncommitted changes."
b69ab31116 />,
b69ab31117 );
b69ab31118 }
b69ab31119
b69ab31120 const aiGenerateCommitMessageEnabled = useFeatureFlagAsync(
b69ab31121 Internal.featureFlags?.AIGenerateCommitMessage,
b69ab31122 );
b69ab31123 // For now, only support this in VS Code
b69ab31124 if (!commit && aiGenerateCommitMessageEnabled && platform.platformName === 'vscode') {
b69ab31125 actions.push(<FillCommitInfoButton key="fill-commit-info" dismiss={dismiss} />);
b69ab31126 }
b69ab31127
b69ab31128 const aiValidateChangesEnabled = useFeatureFlagAsync(Internal.featureFlags?.AIValidateChanges);
b69ab31129 // For now, only support this in VS Code
b69ab31130 if (!commit && aiValidateChangesEnabled && platform.platformName === 'vscode') {
b69ab31131 actions.push(<ValidateChangesButton key="validate-changes" dismiss={dismiss} />);
b69ab31132 }
b69ab31133
b69ab31134 const aiCodeReviewUpsellEnabled = useFeatureFlagAsync(Internal.featureFlags?.AICodeReviewUpsell);
b69ab31135 // For now, only support this in VS Code
b69ab31136 if (aiCodeReviewUpsellEnabled && platform.platformName === 'vscode') {
b69ab31137 const enabled = !commit || commit.isDot; // Enabled for `uncommitted changes` or the `current commit`.
b69ab31138 actions.push(
b69ab31139 <ReviewCodeButton
b69ab31140 key="review-commit"
b69ab31141 commit={commit}
b69ab31142 dismiss={dismiss}
b69ab31143 disabled={!enabled}
b69ab31144 disabledReason="This action is only available for the current commit and uncommitted changes."
b69ab31145 />,
b69ab31146 );
b69ab31147 }
b69ab31148
b69ab31149 return (
b69ab31150 <DropdownFields
b69ab31151 title={<T>Smart Actions</T>}
b69ab31152 icon="lightbulb-sparkle"
b69ab31153 className="smart-actions-dropdown"
b69ab31154 data-testid="smart-actions-dropdown">
b69ab31155 {actions.length > 0 ? actions : <T>No smart actions available</T>}
b69ab31156 </DropdownFields>
b69ab31157 );
b69ab31158}
b69ab31159
b69ab31160/** Like SplitButton, but triggers AI split automatically. */
b69ab31161function AutoSplitButton({commit, dismiss}: {commit: CommitInfo; dismiss: () => void}) {
b69ab31162 return (
b69ab31163 <BaseSplitButton
b69ab31164 commit={commit}
b69ab31165 trackerEventName="SplitOpenFromSmartActions"
b69ab31166 autoSplit={true}
b69ab31167 onSplitInitiated={() => {
b69ab31168 tracker.track('SmartActionClicked', {extras: {action: 'AutoSplit'}});
b69ab31169 dismiss();
b69ab31170 }}>
b69ab31171 <Icon icon="sparkle" />
b69ab31172 <T>Auto-split</T>
b69ab31173 </BaseSplitButton>
b69ab31174 );
b69ab31175}
b69ab31176
b69ab31177/** Prompt AI to resolve all comments on a diff. */
b69ab31178function ResolveCommentsButton({
b69ab31179 diffId,
b69ab31180 filePathsSample,
b69ab31181 dismiss,
b69ab31182 disabled,
b69ab31183 disabledReason,
b69ab31184}: {
b69ab31185 diffId: string;
b69ab31186 filePathsSample: readonly string[];
b69ab31187 dismiss: () => void;
b69ab31188 disabled?: boolean;
b69ab31189 disabledReason?: string;
b69ab31190}) {
b69ab31191 const repo = useAtomValue(repositoryInfo);
b69ab31192 const repoPath = repo?.repoRoot;
b69ab31193 const diffComments = useAtomValue(diffCommentData(diffId));
b69ab31194 if (diffComments.state === 'loading') {
b69ab31195 return <Icon icon="loading" />;
b69ab31196 }
b69ab31197 if (diffComments.state === 'hasError' || diffComments.data.length === 0) {
b69ab31198 return;
b69ab31199 }
b69ab31200
b69ab31201 const button = (
b69ab31202 <Button
b69ab31203 data-testid="review-comments-button"
b69ab31204 onClick={e => {
b69ab31205 tracker.track('SmartActionClicked', {extras: {action: 'ResolveAllComments'}});
b69ab31206 serverAPI.postMessage({
b69ab31207 type: 'platform/resolveAllCommentsWithAI',
b69ab31208 diffId,
b69ab31209 comments: diffComments.data,
b69ab31210 filePaths: [...filePathsSample],
b69ab31211 repoPath,
b69ab31212 });
b69ab31213 dismiss();
b69ab31214 e.stopPropagation();
b69ab31215 }}
b69ab31216 disabled={disabled}>
b69ab31217 <Icon icon="sparkle" />
b69ab31218 <T>Resolve all comments</T>
b69ab31219 </Button>
b69ab31220 );
b69ab31221
b69ab31222 return disabled ? <Tooltip title={disabledReason}>{button}</Tooltip> : button;
b69ab31223}
b69ab31224
b69ab31225/** Prompt AI to fill commit info. */
b69ab31226function FillCommitInfoButton({dismiss}: {dismiss: () => void}) {
b69ab31227 return (
b69ab31228 <Button
b69ab31229 data-testid="fill-commit-info-button"
b69ab31230 onClick={e => {
b69ab31231 tracker.track('SmartActionClicked', {extras: {action: 'FillCommitMessage'}});
b69ab31232 serverAPI.postMessage({
b69ab31233 type: 'platform/fillCommitMessageWithAI',
b69ab31234 id: randomId(),
b69ab31235 source: 'smartAction',
b69ab31236 });
b69ab31237 dismiss();
b69ab31238 e.stopPropagation();
b69ab31239 }}>
b69ab31240 <Icon icon="sparkle" />
b69ab31241 <T>Fill commit info</T>
b69ab31242 </Button>
b69ab31243 );
b69ab31244}
b69ab31245
b69ab31246/** Prompt AI to resolve failed signals on a diff. */
b69ab31247function ResolveFailedSignalsButton({
b69ab31248 hash,
b69ab31249 diffId,
b69ab31250 dismiss,
b69ab31251 disabled,
b69ab31252 disabledReason,
b69ab31253}: {
b69ab31254 hash: string;
b69ab31255 diffId: string;
b69ab31256 dismiss: () => void;
b69ab31257 disabled?: boolean;
b69ab31258 disabledReason?: string;
b69ab31259}) {
b69ab31260 const repo = useAtomValue(repositoryInfo);
b69ab31261 const repoPath = repo?.repoRoot;
b69ab31262 const diffSummaryResult = useAtomValue(diffSummary(diffId));
b69ab31263
b69ab31264 // Only show the button if there are failed signals
b69ab31265 if (
b69ab31266 diffSummaryResult.error ||
b69ab31267 !diffSummaryResult.value?.signalSummary ||
b69ab31268 diffSummaryResult.value.signalSummary !== 'failed'
b69ab31269 ) {
b69ab31270 return null;
b69ab31271 }
b69ab31272
b69ab31273 const diffVersionNumber = Internal.getDiffVersionNumber?.(diffSummaryResult.value, hash);
b69ab31274
b69ab31275 const button = (
b69ab31276 <Button
b69ab31277 data-testid="resolve-failed-signals-button"
b69ab31278 onClick={e => {
b69ab31279 if (diffVersionNumber !== undefined) {
b69ab31280 tracker.track('SmartActionClicked', {extras: {action: 'ResolveFailedSignals'}});
b69ab31281 serverAPI.postMessage({
b69ab31282 type: 'platform/resolveFailedSignalsWithAI',
b69ab31283 diffId,
b69ab31284 diffVersionNumber,
b69ab31285 repoPath,
b69ab31286 });
b69ab31287 dismiss();
b69ab31288 }
b69ab31289 e.stopPropagation();
b69ab31290 }}
b69ab31291 disabled={disabled || diffVersionNumber === undefined}>
b69ab31292 <Icon icon="sparkle" />
b69ab31293 <T>Fix failed signals</T>
b69ab31294 </Button>
b69ab31295 );
b69ab31296
b69ab31297 return disabled || diffVersionNumber === undefined ? (
b69ab31298 <Tooltip
b69ab31299 title={
b69ab31300 diffVersionNumber === undefined
b69ab31301 ? 'Unable to determine Phabricator version number for this commit'
b69ab31302 : disabledReason
b69ab31303 }>
b69ab31304 {button}
b69ab31305 </Tooltip>
b69ab31306 ) : (
b69ab31307 button
b69ab31308 );
b69ab31309}
b69ab31310
b69ab31311function GenerateTestsForModifiedCodeButton({
b69ab31312 dismiss,
b69ab31313 disabled,
b69ab31314 disabledReason,
b69ab31315}: {
b69ab31316 dismiss: () => void;
b69ab31317 disabled?: boolean;
b69ab31318 disabledReason?: string;
b69ab31319}) {
b69ab31320 const button = (
b69ab31321 <Button
b69ab31322 data-testid="generate-tests-for-modified-code-button"
b69ab31323 onClick={e => {
b69ab31324 tracker.track('SmartActionClicked', {extras: {action: 'GenerateTests'}});
b69ab31325 serverAPI.postMessage({
b69ab31326 type: 'platform/createTestForModifiedCodeWithAI',
b69ab31327 });
b69ab31328 dismiss();
b69ab31329 e.stopPropagation();
b69ab31330 }}
b69ab31331 disabled={disabled}>
b69ab31332 <Icon icon="sparkle" />
b69ab31333 <T>Generate tests for changes</T>
b69ab31334 </Button>
b69ab31335 );
b69ab31336
b69ab31337 return disabled ? <Tooltip title={disabledReason}>{button}</Tooltip> : button;
b69ab31338}
b69ab31339
b69ab31340/** Prompt AI to validate code and fix errors in the working copy. */
b69ab31341function ValidateChangesButton({dismiss}: {dismiss: () => void}) {
b69ab31342 return (
b69ab31343 <Button
b69ab31344 data-testid="validate-changes-button"
b69ab31345 onClick={e => {
b69ab31346 tracker.track('SmartActionClicked', {extras: {action: 'ValidateChanges'}});
b69ab31347 serverAPI.postMessage({
b69ab31348 type: 'platform/validateChangesWithAI',
b69ab31349 });
b69ab31350 dismiss();
b69ab31351 e.stopPropagation();
b69ab31352 }}>
b69ab31353 <Icon icon="sparkle" />
b69ab31354 <T>Validate Changes</T>
b69ab31355 </Button>
b69ab31356 );
b69ab31357}
b69ab31358
b69ab31359/** Prompt AI to review the current commit and add comments */
b69ab31360function ReviewCodeButton({
b69ab31361 commit,
b69ab31362 dismiss,
b69ab31363 disabled,
b69ab31364 disabledReason,
b69ab31365}: {
b69ab31366 commit?: CommitInfo;
b69ab31367 dismiss: () => void;
b69ab31368 disabled?: boolean;
b69ab31369 disabledReason?: string;
b69ab31370}) {
b69ab31371 const button = (
b69ab31372 <Button
b69ab31373 data-testid="review-commit-button"
b69ab31374 onClick={e => {
b69ab31375 tracker.track('SmartActionClicked', {extras: {action: 'ReviewCommit'}});
b69ab31376 serverAPI.postMessage({
b69ab31377 type: 'platform/runAICodeReviewChat',
b69ab31378 source: 'smartAction',
b69ab31379 reviewScope: commit ? 'current commit' : 'uncommitted changes',
b69ab31380 });
b69ab31381 dismiss();
b69ab31382 e.stopPropagation();
b69ab31383 }}
b69ab31384 disabled={disabled}>
b69ab31385 <Icon icon="sparkle" />
b69ab31386 <T>{commit ? 'Review commit' : 'Review changes'}</T>
b69ab31387 </Button>
b69ab31388 );
b69ab31389
b69ab31390 return disabled ? <Tooltip title={disabledReason}>{button}</Tooltip> : button;
b69ab31391}