mirror of
https://github.com/stablyai/orca.git
synced 2026-10-09 08:02:35 +00:00
refactor(composer): delete the full-create path no caller reaches (#26052)
* refactor(composer): delete the full-create path no caller reaches NewWorkspaceComposerModal is the only renderer of the composer card, and it overrides the card props' onCreate (the full-create submit) with its quick create and never reads useComposerState's submit. The full path (source and submit preparation, creation execution and its finalization, issue-command, startup and structured-launch helpers, the orchestration) was reachable only from its own tests. Delete it with them, and drop onCreate and submit from the composer contracts. * refactor(composer): delete code the full-create path left orphaned Removing the full-create path left code whose only readers were gone: - applyWorktreeMeta and the updateWorktreeMeta plumbing that fed it - createWorktree and setSidebarOpen on the composer target store - currentIssueCommand on the composer model - buildAgentPromptWithContext and getLinkedWorkItemPromptContext (only their own tests still called them) and those test cases - the "Selected agent is disabled" locale string in every catalog - the export on confirmRuntimeIssueCommandRead The 'full' create-gate mode goes too. Its only caller passed 'quick', so the default named a mode with no create action. That removes the option, the full gate, the issue-automation wait flag and the renderer issue command preload that ran only in 'full' mode. Quick create is unchanged. The quick path's name-retirement comment pointed at the deleted full submit path; it now carries that reasoning itself, including the mobile counterpart that must change with it. An e2e comment no longer names applyWorktreeMeta.
This commit is contained in:
@@ -149,8 +149,7 @@ function QuickTabBody({
|
||||
onCreated: onClose,
|
||||
isSubmissionCancelled,
|
||||
...(modalData.telemetrySource ? { telemetrySource: modalData.telemetrySource } : {}),
|
||||
enableIssueAutomation: modalData.enableIssueAutomation === true,
|
||||
createGateMode: 'quick'
|
||||
enableIssueAutomation: modalData.enableIssueAutomation === true
|
||||
})
|
||||
// Why: the composer's built-in `onOpenAgentSettings` handler navigates to
|
||||
// the settings page and closes the modal. For the quick-create flow we want
|
||||
|
||||
@@ -34,10 +34,6 @@ export function assembleComposerModel(
|
||||
...source.issueSourceActions,
|
||||
...source.composerNavigationActions,
|
||||
...submit.folderSubmitOrchestration,
|
||||
...submit.fullSubmitSourcePreparation,
|
||||
...submit.fullSubmitPreparation,
|
||||
...submit.fullCreationExecution,
|
||||
...submit.fullSubmitOrchestration,
|
||||
...submit.multipleCreateReset,
|
||||
...submit.quickSubmitSourcePreparation,
|
||||
...submit.quickSubmitPreparation,
|
||||
|
||||
@@ -223,7 +223,6 @@ export function useComposerAsyncState(input: ComposerAsyncStateInput) {
|
||||
setCheckedHooksContextKey,
|
||||
loadedIssueCommand,
|
||||
setLoadedIssueCommand,
|
||||
currentIssueCommand,
|
||||
issueCommandTemplate,
|
||||
hasLoadedIssueCommand,
|
||||
setupDecision,
|
||||
|
||||
@@ -17,7 +17,6 @@ export type ComposerAsyncModel = {
|
||||
setLoadedIssueCommand: React.Dispatch<
|
||||
React.SetStateAction<{ contextKey: string; result: IssueCommandReadResult } | null>
|
||||
>
|
||||
currentIssueCommand: IssueCommandReadResult | null
|
||||
issueCommandTemplate: string
|
||||
hasLoadedIssueCommand: boolean
|
||||
setupDecision: 'skip' | 'run' | null
|
||||
|
||||
@@ -96,7 +96,6 @@ export type ComposerCardActionProps = {
|
||||
onOpenAgentSettings: ComposerModel['handleOpenAgentSettings']
|
||||
onToggleAdvanced: () => void
|
||||
createDisabled: boolean
|
||||
onCreate: () => void
|
||||
onNoteChange: ComposerModel['setNote']
|
||||
onBaseBranchChange: ComposerModel['handleBaseBranchChange']
|
||||
onBaseBranchPrSelect: ComposerModel['handleBaseBranchPrSelect']
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { getAttachmentLabel } from '@/lib/new-workspace'
|
||||
import {
|
||||
getFullComposerCreateDisabled,
|
||||
getQuickComposerCreateDisabled
|
||||
} from '@/lib/new-workspace-create-gates'
|
||||
import { getQuickComposerCreateDisabled } from '@/lib/new-workspace-create-gates'
|
||||
import type { ComposerModel } from './composer-model'
|
||||
import type { ComposerCardActionProps, ComposerCardSourceProps } from './composer-card-contract'
|
||||
|
||||
@@ -14,7 +11,6 @@ export function buildComposerCardProps(state: ComposerModel) {
|
||||
baseBranch,
|
||||
branchNameOverride,
|
||||
parentWorktreeId,
|
||||
createGateMode,
|
||||
createError,
|
||||
createMultiple,
|
||||
creating,
|
||||
@@ -89,7 +85,6 @@ export function buildComposerCardProps(state: ComposerModel) {
|
||||
selectedRepoIsGit,
|
||||
selectedRepoProjectId,
|
||||
selectedRepoRequiresConnection,
|
||||
shouldWaitForIssueAutomationCheck,
|
||||
sourceIntentBlocksCreate,
|
||||
sparseError,
|
||||
selectedRepoSshStatus,
|
||||
@@ -116,26 +111,19 @@ export function buildComposerCardProps(state: ComposerModel) {
|
||||
sparsePresets,
|
||||
sparseSelectedPresetId,
|
||||
startFromResetHint,
|
||||
submit,
|
||||
tuiAgent
|
||||
} = state
|
||||
|
||||
const createGateInput = {
|
||||
const repoCreateDisabled = getQuickComposerCreateDisabled({
|
||||
repoId,
|
||||
workspaceSeedName,
|
||||
creating,
|
||||
shouldWaitForSetupCheck,
|
||||
shouldWaitForIssueAutomationCheck,
|
||||
sourceIntentBlocksCreate,
|
||||
requiresExplicitSetupChoice,
|
||||
hasSetupDecision: Boolean(setupDecision),
|
||||
selectedRepoRequiresConnection,
|
||||
sparseError
|
||||
}
|
||||
const repoCreateDisabled =
|
||||
createGateMode === 'quick'
|
||||
? getQuickComposerCreateDisabled(createGateInput)
|
||||
: getFullComposerCreateDisabled(createGateInput)
|
||||
})
|
||||
const createDisabled = isProjectGroupTarget ? folderCreateDisabled : repoCreateDisabled
|
||||
const cardProps: ComposerCardSourceProps & ComposerCardActionProps = {
|
||||
eligibleRepos: isProjectGroupTarget ? folderSourceRepos : eligibleRepos,
|
||||
@@ -216,7 +204,6 @@ export function buildComposerCardProps(state: ComposerModel) {
|
||||
createDisabled,
|
||||
projectError: isProjectGroupTarget ? pathStatusProjectError : projectError,
|
||||
creating,
|
||||
onCreate: () => void submit(),
|
||||
baseBranch: isProjectGroupTarget ? undefined : baseBranch,
|
||||
onBaseBranchChange: isProjectGroupTarget ? () => {} : handleBaseBranchChange,
|
||||
onBaseBranchPrSelect: isProjectGroupTarget ? () => {} : handleBaseBranchPrSelect,
|
||||
|
||||
@@ -9,9 +9,7 @@ export function useComposerExternalSync(target: ComposerTargetState): ComposerEx
|
||||
const hostRuntimeEffects = useHostRuntimeEffects({
|
||||
commitHookCheckIfCurrent: target.providerRuntimeSync.commitHookCheckIfCurrent,
|
||||
connectionId: target.workspaceIdentityState.connectionId,
|
||||
createGateMode: target.composerTargetStore.createGateMode,
|
||||
disabledTuiAgents: target.workspaceIdentityState.disabledTuiAgents,
|
||||
enableIssueAutomation: target.composerTargetStore.enableIssueAutomation,
|
||||
ensureDetectedAgents: target.workspaceIdentityState.ensureDetectedAgents,
|
||||
ensureRemoteDetectedAgents: target.workspaceIdentityState.ensureRemoteDetectedAgents,
|
||||
ensureRuntimeDetectedAgents: target.workspaceIdentityState.ensureRuntimeDetectedAgents,
|
||||
@@ -24,12 +22,9 @@ export function useComposerExternalSync(target: ComposerTargetState): ComposerEx
|
||||
repoIdRef: target.runtimeTargetSelection.repoIdRef,
|
||||
runtimeEnvironmentId: target.workspaceIdentityState.runtimeEnvironmentId,
|
||||
selectedRepoConnectionIdRef: target.asyncComposerState.selectedRepoConnectionIdRef,
|
||||
selectedRepoExecutionHostId: target.runtimeTargetSelection.selectedRepoExecutionHostId,
|
||||
selectedRepoHookContextKey: target.runtimeTargetSelection.selectedRepoHookContextKey,
|
||||
selectedRepoIsGit: target.runtimeTargetSelection.selectedRepoIsGit,
|
||||
selectedRepoSettingsRef: target.asyncComposerState.selectedRepoSettingsRef,
|
||||
selectedRepoSshStatus: target.runtimeTargetSelection.selectedRepoSshStatus,
|
||||
setLoadedIssueCommand: target.asyncComposerState.setLoadedIssueCommand,
|
||||
setTuiAgent: target.workspaceIdentityState.setTuiAgent,
|
||||
settings: target.composerTargetStore.settings,
|
||||
tuiAgent: target.workspaceIdentityState.tuiAgent
|
||||
|
||||
@@ -12,12 +12,10 @@ type ComposerNavigationActionsInput = Pick<
|
||||
| 'setActiveRuntimeEnvironmentPreference'
|
||||
| 'smartNameJiraSourceContext'
|
||||
| 'sourceIntentBlocksCreate'
|
||||
| 'updateWorktreeMeta'
|
||||
>
|
||||
|
||||
import { useCallback } from 'react'
|
||||
import { getTaskSourceRuntimeSettings } from '../../../../shared/task-source-context'
|
||||
import type { WorktreeMeta } from '../../../../shared/worktree/meta-types'
|
||||
|
||||
export function useComposerNavigationActions(input: ComposerNavigationActionsInput) {
|
||||
const {
|
||||
@@ -30,8 +28,7 @@ export function useComposerNavigationActions(input: ComposerNavigationActionsInp
|
||||
selectedProjectGroup,
|
||||
setActiveRuntimeEnvironmentPreference,
|
||||
smartNameJiraSourceContext,
|
||||
sourceIntentBlocksCreate,
|
||||
updateWorktreeMeta
|
||||
sourceIntentBlocksCreate
|
||||
} = input
|
||||
|
||||
const handleOpenAgentSettings = useCallback((): void => {
|
||||
@@ -61,20 +58,6 @@ export function useComposerNavigationActions(input: ComposerNavigationActionsInp
|
||||
smartNameJiraSourceContext
|
||||
])
|
||||
|
||||
const applyWorktreeMeta = useCallback(
|
||||
async (worktreeId: string, meta: Partial<WorktreeMeta>): Promise<void> => {
|
||||
if (Object.keys(meta).length === 0) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await updateWorktreeMeta(worktreeId, meta)
|
||||
} catch {
|
||||
console.error('Failed to update worktree meta after creation')
|
||||
}
|
||||
},
|
||||
[updateWorktreeMeta]
|
||||
)
|
||||
|
||||
const folderCreateDisabled =
|
||||
creating ||
|
||||
sourceIntentBlocksCreate ||
|
||||
@@ -85,7 +68,6 @@ export function useComposerNavigationActions(input: ComposerNavigationActionsInp
|
||||
return {
|
||||
handleOpenAgentSettings,
|
||||
handleOpenJiraSettings,
|
||||
applyWorktreeMeta,
|
||||
folderCreateDisabled
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { GitLabWorkItem } from '../../../../shared/gitlab-types'
|
||||
import type { GitHubWorkItem } from '../../../../shared/github/work-item-types'
|
||||
import type { WorktreeMeta } from '../../../../shared/worktree/meta-types'
|
||||
import type { GitPushTarget } from '../../../../shared/worktree/types'
|
||||
import type { JiraIssue } from '../../../../shared/jira-types'
|
||||
import type { TaskSourceContext } from '../../../../shared/task-source-context'
|
||||
@@ -17,7 +16,6 @@ export type ComposerSourceModel = {
|
||||
options?: { preserveBranchNameOverride?: boolean | undefined }
|
||||
) => void
|
||||
applyLocalComposerDrop: (paths: string[], canApply?: () => boolean) => Promise<void>
|
||||
applyWorktreeMeta: (worktreeId: string, meta: Partial<WorktreeMeta>) => Promise<void>
|
||||
canPrefetchSelectedRepoWorkItems: boolean
|
||||
folderCreateDisabled: boolean
|
||||
handleAddAttachment: () => Promise<void>
|
||||
|
||||
@@ -46,9 +46,6 @@ export type ComposerSourceState = {
|
||||
>
|
||||
composerNavigationActions: Pick<
|
||||
ComposerModel,
|
||||
| 'handleOpenAgentSettings'
|
||||
| 'handleOpenJiraSettings'
|
||||
| 'applyWorktreeMeta'
|
||||
| 'folderCreateDisabled'
|
||||
'handleOpenAgentSettings' | 'handleOpenJiraSettings' | 'folderCreateDisabled'
|
||||
>
|
||||
}
|
||||
|
||||
@@ -281,8 +281,7 @@ export function useComposerSourceState(
|
||||
setActiveRuntimeEnvironmentPreference:
|
||||
target.composerTargetStore.setActiveRuntimeEnvironmentPreference,
|
||||
smartNameJiraSourceContext: target.sourceContextState.smartNameJiraSourceContext,
|
||||
sourceIntentBlocksCreate: target.workspaceIdentityState.sourceIntentBlocksCreate,
|
||||
updateWorktreeMeta: target.composerTargetStore.updateWorktreeMeta
|
||||
sourceIntentBlocksCreate: target.workspaceIdentityState.sourceIntentBlocksCreate
|
||||
})
|
||||
return {
|
||||
sourceIdentityActions,
|
||||
|
||||
@@ -1,78 +1,17 @@
|
||||
import type { UISlice } from '../../store/slices/ui'
|
||||
import type {
|
||||
CreateSparseCheckoutRequest,
|
||||
CreateWorktreeArgs,
|
||||
CreateWorktreeResult,
|
||||
SetupDecision
|
||||
} from '../../../../shared/worktree/create-types'
|
||||
import type { WorktreeStartupLaunch } from '../../../../shared/worktree/launch-types'
|
||||
import type {
|
||||
WorkspaceLinkedItem,
|
||||
GitPushTarget,
|
||||
WorkspaceStatus
|
||||
} from '../../../../shared/worktree/types'
|
||||
import type { WorktreeMeta } from '../../../../shared/worktree/meta-types'
|
||||
import type { RepoUpdate } from '../../store/repos/repo-state'
|
||||
import type { WorktreeMetaUpdateOptions } from '../../store/slices/worktree-helpers'
|
||||
import type { FolderWorkspace } from '../../../../shared/folder-workspace-types'
|
||||
import type { ExecutionHostId } from '../../../../shared/execution-host'
|
||||
import type { TaskSourceContext } from '../../../../shared/task-source-context'
|
||||
import type { TuiAgent } from '../../../../shared/tui-agent'
|
||||
import type { WorkspaceSource as WorkspaceCreateTelemetrySource } from '../../../../shared/workspace-source'
|
||||
|
||||
export type ComposerStoreActions = {
|
||||
setNewWorkspaceDraft: (draft: NonNullable<UISlice['newWorkspaceDraft']>) => void
|
||||
clearNewWorkspaceDraft: () => void
|
||||
createWorktree: (
|
||||
repoId: string,
|
||||
name: string,
|
||||
baseBranch?: string,
|
||||
setupDecision?: SetupDecision,
|
||||
sparseCheckout?: CreateSparseCheckoutRequest,
|
||||
telemetrySource?: WorkspaceCreateTelemetrySource,
|
||||
displayName?: string,
|
||||
linkedIssue?: number,
|
||||
linkedPR?: number,
|
||||
pushTarget?: GitPushTarget,
|
||||
createdWithAgent?: TuiAgent,
|
||||
linkedLinearIssue?: string,
|
||||
branchNameOverride?: string,
|
||||
workspaceStatus?: WorkspaceStatus,
|
||||
linkedGitLabMR?: number,
|
||||
linkedGitLabIssue?: number,
|
||||
startup?: WorktreeStartupLaunch,
|
||||
pendingFirstAgentMessageRename?: boolean,
|
||||
creationId?: string,
|
||||
linkedLinearIssueWorkspaceId?: string | null,
|
||||
linkedLinearIssueOrganizationUrlKey?: string | null,
|
||||
linkedBitbucketPR?: number | null,
|
||||
linkedAzureDevOpsPR?: number | null,
|
||||
linkedGiteaPR?: number | null,
|
||||
compareBaseRef?: string,
|
||||
options?: {
|
||||
automationProvenanceRequest?: CreateWorktreeArgs['automationProvenanceRequest']
|
||||
linkedWorkItem?: WorkspaceLinkedItem | null
|
||||
linkedTaskSourceContext?: TaskSourceContext | null
|
||||
startupDraft?: string
|
||||
nameWasGenerated?: boolean
|
||||
parentWorktreeId?: string
|
||||
provisionedRoot?: {
|
||||
runtimeId: string
|
||||
executionHostId: ExecutionHostId
|
||||
expectedPath: string
|
||||
}
|
||||
}
|
||||
) => Promise<CreateWorktreeResult>
|
||||
updateRepo: (
|
||||
projectId: string,
|
||||
updates: RepoUpdate,
|
||||
options?: { hostId?: ExecutionHostId }
|
||||
) => Promise<boolean>
|
||||
updateWorktreeMeta: (
|
||||
worktreeId: string,
|
||||
updates: Partial<WorktreeMeta>,
|
||||
options?: WorktreeMetaUpdateOptions
|
||||
) => Promise<{ ok: true } | { ok: false; error: string }>
|
||||
createFolderWorkspace: (
|
||||
args: {
|
||||
projectGroupId: string
|
||||
@@ -86,7 +25,6 @@ export type ComposerStoreActions = {
|
||||
},
|
||||
options?: { runtimeEnvironmentId?: string | null }
|
||||
) => Promise<FolderWorkspace | null>
|
||||
setSidebarOpen: (open: boolean) => void
|
||||
closeModal: () => void
|
||||
openSettingsPage: () => void
|
||||
openSettingsTarget: (target: NonNullable<UISlice['settingsNavigationTarget']>) => void
|
||||
|
||||
@@ -3,8 +3,6 @@ import type { GitPushTarget } from '../../../../shared/worktree/types'
|
||||
import type { SetupDecision } from '../../../../shared/worktree/create-types'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import type { WorkspaceIntentName } from '../../../../shared/workspace-name'
|
||||
import type { AgentStartupPlan } from '../../../../shared/tui-agent-startup'
|
||||
import type { AgentStartedTelemetry } from '@/lib/worktree-startup-payload'
|
||||
import type { LinkedWorkItemSummary } from '@/lib/new-workspace'
|
||||
import type { WorktreeCreationRequest } from '@/lib/pending-worktree-creation'
|
||||
import type { PendingSmartGitHubSubmitResolution } from './source-selection-decisions'
|
||||
@@ -14,40 +12,6 @@ type SmartCreateNames = {
|
||||
displayName: string | undefined
|
||||
}
|
||||
|
||||
export type FullSubmitSource = {
|
||||
submitLinkedWorkItem: LinkedWorkItemSummary | null
|
||||
submitLinkedIssueNumber: number | null
|
||||
submitLinkedPR: number | null
|
||||
submitTitleName: WorkspaceIntentName | null
|
||||
nameIsAutoManaged: boolean
|
||||
smartGitHubCreateNames: SmartCreateNames
|
||||
workspaceName: string
|
||||
nameWasGenerated: boolean
|
||||
submitBaseBranch: string | undefined
|
||||
submitCompareBaseRef: string | undefined
|
||||
submitPushTarget: GitPushTarget | undefined
|
||||
submitBranchNameOverride: string | undefined
|
||||
submitLinkedWorkItemProvider: LinkedWorkItemSummary['provider'] | null
|
||||
submitStartupPrompt: string
|
||||
submitShouldRunIssueAutomation: boolean
|
||||
}
|
||||
|
||||
export type PreparedFullSubmit = FullSubmitSource & {
|
||||
effectiveSetupDecision: SetupDecision
|
||||
issueCommandTrustDecision: 'run' | 'skip'
|
||||
confirmedIssueCommandTemplate: string
|
||||
linkedLinearIssue: string | undefined
|
||||
linkedLinearIssueWorkspaceId: string | undefined
|
||||
linkedLinearIssueOrganizationUrlKey: string | undefined
|
||||
effectiveBranchNameOverride: string | undefined
|
||||
createDisplayName: string | undefined
|
||||
pendingFirstAgentMessageRename: boolean
|
||||
startupPlan: AgentStartupPlan | null
|
||||
shouldSeedInitialAgentStatus: boolean
|
||||
composerTelemetry: AgentStartedTelemetry
|
||||
backendStartup: WorktreeCreationRequest['startup']
|
||||
}
|
||||
|
||||
export type QuickSubmitSource = {
|
||||
submitLinkedWorkItem: LinkedWorkItemSummary | null
|
||||
agent: TuiAgent | null
|
||||
@@ -78,10 +42,6 @@ export type PreparedQuickSubmit = QuickSubmitSource & {
|
||||
}
|
||||
|
||||
export type ComposerSubmitModel = {
|
||||
executeFullCreation: (
|
||||
resolution: PendingSmartGitHubSubmitResolution,
|
||||
repoId: string
|
||||
) => Promise<void>
|
||||
executeQuickCreation: (
|
||||
resolution: PendingSmartGitHubSubmitResolution,
|
||||
requestedAgent: TuiAgent | null,
|
||||
@@ -90,12 +50,6 @@ export type ComposerSubmitModel = {
|
||||
repoId: string,
|
||||
selectedRepo: Repo
|
||||
) => Promise<void>
|
||||
prepareFullSubmit: (
|
||||
resolution: PendingSmartGitHubSubmitResolution
|
||||
) => Promise<PreparedFullSubmit | null>
|
||||
prepareFullSubmitSource: (
|
||||
resolution: PendingSmartGitHubSubmitResolution
|
||||
) => FullSubmitSource | null
|
||||
prepareQuickSubmit: (
|
||||
resolution: PendingSmartGitHubSubmitResolution,
|
||||
requestedAgent: TuiAgent | null,
|
||||
@@ -107,7 +61,6 @@ export type ComposerSubmitModel = {
|
||||
workspaceNameSeed: string
|
||||
) => QuickSubmitSource | null
|
||||
resetForNextCreate: () => void
|
||||
submit: () => Promise<void>
|
||||
submitQuick: (agent: TuiAgent | null) => Promise<void>
|
||||
submitFolderTarget: (requestedAgent: TuiAgent | null) => Promise<void>
|
||||
}
|
||||
|
||||
@@ -3,10 +3,6 @@ import type { ComposerExternalSyncState } from './composer-external-sync-contrac
|
||||
import type { ComposerSourceState } from './composer-source-state-contract'
|
||||
import type { ComposerSubmitState } from './composer-submit-state-contract'
|
||||
import { useFolderSubmitOrchestration } from './folder-submit-orchestration'
|
||||
import { useFullSubmitSourcePreparation } from './full-submit-source-preparation'
|
||||
import { useFullSubmitPreparation } from './full-submit-preparation'
|
||||
import { useFullCreationExecution } from './full-creation-execution'
|
||||
import { useFullSubmitOrchestration } from './full-submit-orchestration'
|
||||
import { useMultipleCreateReset } from './multiple-create-reset'
|
||||
import { useQuickSubmitSourcePreparation } from './quick-submit-source-preparation'
|
||||
import { useQuickSubmitPreparation } from './quick-submit-preparation'
|
||||
@@ -45,98 +41,6 @@ export function useComposerSubmitOrchestration(
|
||||
taskSourceContext: target.sourceContextState.taskSourceContext,
|
||||
telemetrySource: target.composerTargetStore.telemetrySource
|
||||
})
|
||||
const fullSubmitSourcePreparation = useFullSubmitSourcePreparation({
|
||||
agentPrompt: target.sourceContextState.agentPrompt,
|
||||
attachmentPaths: target.sourceContextState.attachmentPaths,
|
||||
baseBranch: target.workspaceIdentityState.baseBranch,
|
||||
branchNameOverride: target.workspaceIdentityState.branchNameOverride,
|
||||
compareBaseRef: target.workspaceIdentityState.compareBaseRef,
|
||||
decisions: target.composerTargetStore.decisions,
|
||||
effectiveLinkedPR: target.derivedComposerState.effectiveLinkedPR,
|
||||
enableIssueAutomation: target.composerTargetStore.enableIssueAutomation,
|
||||
fallbackCreatureName: target.derivedComposerState.fallbackCreatureName,
|
||||
hasLoadedIssueCommand: target.asyncComposerState.hasLoadedIssueCommand,
|
||||
issueCommandTemplate: target.asyncComposerState.issueCommandTemplate,
|
||||
lastAutoNameRef: target.asyncComposerState.lastAutoNameRef,
|
||||
linkedGitLabMR: target.workspaceIdentityState.linkedGitLabMR,
|
||||
linkedWorkItem: target.sourceContextState.linkedWorkItem,
|
||||
name: target.sourceContextState.name,
|
||||
parsedLinkedIssueNumber: target.derivedComposerState.parsedLinkedIssueNumber,
|
||||
pushTarget: target.workspaceIdentityState.pushTarget,
|
||||
workspaceSeedName: target.derivedComposerState.workspaceSeedName
|
||||
})
|
||||
const fullSubmitPreparation = useFullSubmitPreparation({
|
||||
branchAutoNameRef: target.asyncComposerState.branchAutoNameRef,
|
||||
branchNameOverridePreservesNameEdits:
|
||||
target.workspaceIdentityState.branchNameOverridePreservesNameEdits,
|
||||
currentIssueCommand: target.asyncComposerState.currentIssueCommand,
|
||||
isSubmissionCancelled: target.composerTargetStore.isSubmissionCancelled,
|
||||
issueCommandTemplate: target.asyncComposerState.issueCommandTemplate,
|
||||
name: target.sourceContextState.name,
|
||||
prepareFullSubmitSource: fullSubmitSourcePreparation.prepareFullSubmitSource,
|
||||
repoId: target.initialTargetState.repoId,
|
||||
resolvedSetupDecision: target.derivedComposerState.resolvedSetupDecision,
|
||||
selectedRepo: target.runtimeTargetSelection.selectedRepo,
|
||||
selectedRepoAgentLaunchPlatform: target.runtimeTargetSelection.selectedRepoAgentLaunchPlatform,
|
||||
selectedRepoExecutionHostId: target.runtimeTargetSelection.selectedRepoExecutionHostId,
|
||||
selectedRepoIsGit: target.runtimeTargetSelection.selectedRepoIsGit,
|
||||
selectedRepoIsRemote: target.runtimeTargetSelection.selectedRepoIsRemote,
|
||||
selectedRepoStartupShell: target.runtimeTargetSelection.selectedRepoStartupShell,
|
||||
settings: target.composerTargetStore.settings,
|
||||
smartNameMode: target.workspaceIdentityState.smartNameMode,
|
||||
telemetrySource: target.composerTargetStore.telemetrySource,
|
||||
tuiAgent: target.workspaceIdentityState.tuiAgent
|
||||
})
|
||||
const fullCreationExecution = useFullCreationExecution({
|
||||
applyWorktreeMeta: source.composerNavigationActions.applyWorktreeMeta,
|
||||
clearNewWorkspaceDraft: target.composerTargetStore.clearNewWorkspaceDraft,
|
||||
createWorktree: target.composerTargetStore.createWorktree,
|
||||
effectivePresetId: target.derivedComposerState.effectivePresetId,
|
||||
isSubmissionCancelled: target.composerTargetStore.isSubmissionCancelled,
|
||||
linkedGitLabIssue: target.workspaceIdentityState.linkedGitLabIssue,
|
||||
linkedGitLabMR: target.workspaceIdentityState.linkedGitLabMR,
|
||||
normalizedSparseDirectories: target.derivedComposerState.normalizedSparseDirectories,
|
||||
note: target.sourceContextState.note,
|
||||
onCreated: target.composerTargetStore.onCreated,
|
||||
parentWorktreeId: target.workspaceIdentityState.parentWorktreeId,
|
||||
persistDraft: target.composerTargetStore.persistDraft,
|
||||
persistSetupAgentStartupPolicy: target.providerRuntimeSync.persistSetupAgentStartupPolicy,
|
||||
prepareFullSubmit: fullSubmitPreparation.prepareFullSubmit,
|
||||
resolvedInitialWorkspaceStatus: target.initialTargetState.resolvedInitialWorkspaceStatus,
|
||||
selectedRepoExecutionHostId: target.runtimeTargetSelection.selectedRepoExecutionHostId,
|
||||
selectedRepoIsGit: target.runtimeTargetSelection.selectedRepoIsGit,
|
||||
setSidebarOpen: target.composerTargetStore.setSidebarOpen,
|
||||
sparseEnabled: target.asyncComposerState.sparseEnabled,
|
||||
taskSourceContext: target.sourceContextState.taskSourceContext,
|
||||
telemetrySource: target.composerTargetStore.telemetrySource,
|
||||
tuiAgent: target.workspaceIdentityState.tuiAgent
|
||||
})
|
||||
const fullSubmitOrchestration = useFullSubmitOrchestration({
|
||||
disabledTuiAgents: target.workspaceIdentityState.disabledTuiAgents,
|
||||
executeFullCreation: fullCreationExecution.executeFullCreation,
|
||||
fallbackDefaultAgent: target.workspaceIdentityState.fallbackDefaultAgent,
|
||||
isProjectGroupTarget: target.runtimeTargetSelection.isProjectGroupTarget,
|
||||
isSubmissionCancelled: target.composerTargetStore.isSubmissionCancelled,
|
||||
repoId: target.initialTargetState.repoId,
|
||||
requiresExplicitSetupChoice: target.derivedComposerState.requiresExplicitSetupChoice,
|
||||
resolvePendingSmartGitHubSubmit:
|
||||
external.githubSubmitResolution.resolvePendingSmartGitHubSubmit,
|
||||
selectedRepo: target.runtimeTargetSelection.selectedRepo,
|
||||
selectedRepoRequiresConnection: target.runtimeTargetSelection.selectedRepoRequiresConnection,
|
||||
setCreateError: target.asyncComposerState.setCreateError,
|
||||
setCreating: target.asyncComposerState.setCreating,
|
||||
setTuiAgent: target.workspaceIdentityState.setTuiAgent,
|
||||
setupDecision: target.asyncComposerState.setupDecision,
|
||||
shouldWaitForIssueAutomationCheck:
|
||||
target.derivedComposerState.shouldWaitForIssueAutomationCheck,
|
||||
shouldWaitForSetupCheck: target.derivedComposerState.shouldWaitForSetupCheck,
|
||||
showProjectRequiredError: source.branchStartPointActions.showProjectRequiredError,
|
||||
sourceIntentBlocksCreate: target.workspaceIdentityState.sourceIntentBlocksCreate,
|
||||
sparseError: target.derivedComposerState.sparseError,
|
||||
submitFolderTarget: folderSubmitOrchestration.submitFolderTarget,
|
||||
tuiAgent: target.workspaceIdentityState.tuiAgent,
|
||||
workspaceSeedName: target.derivedComposerState.workspaceSeedName
|
||||
})
|
||||
const multipleCreateReset = useMultipleCreateReset({
|
||||
handleClearSmartNameSelection: source.issueSourceActions.handleClearSmartNameSelection,
|
||||
lastAutoNameRef: target.asyncComposerState.lastAutoNameRef,
|
||||
@@ -244,10 +148,6 @@ export function useComposerSubmitOrchestration(
|
||||
})
|
||||
return {
|
||||
folderSubmitOrchestration,
|
||||
fullSubmitSourcePreparation,
|
||||
fullSubmitPreparation,
|
||||
fullCreationExecution,
|
||||
fullSubmitOrchestration,
|
||||
multipleCreateReset,
|
||||
quickSubmitSourcePreparation,
|
||||
quickSubmitPreparation,
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import type { ComposerModel } from './composer-model'
|
||||
export type ComposerSubmitState = {
|
||||
folderSubmitOrchestration: Pick<ComposerModel, 'submitFolderTarget'>
|
||||
fullSubmitSourcePreparation: Pick<ComposerModel, 'prepareFullSubmitSource'>
|
||||
fullSubmitPreparation: Pick<ComposerModel, 'prepareFullSubmit'>
|
||||
fullCreationExecution: Pick<ComposerModel, 'executeFullCreation'>
|
||||
fullSubmitOrchestration: Pick<ComposerModel, 'submit'>
|
||||
multipleCreateReset: Pick<ComposerModel, 'resetForNextCreate'>
|
||||
quickSubmitSourcePreparation: Pick<ComposerModel, 'prepareQuickSubmitSource'>
|
||||
quickSubmitPreparation: Pick<ComposerModel, 'prepareQuickSubmit'>
|
||||
|
||||
@@ -32,7 +32,6 @@ export type ComposerStateInput = {
|
||||
onRepoIdOverrideChange?: (value: string) => void
|
||||
telemetrySource?: WorkspaceCreateTelemetrySource
|
||||
enableIssueAutomation?: boolean
|
||||
createGateMode?: 'full' | 'quick'
|
||||
}
|
||||
|
||||
const NEVER_CANCEL_COMPOSER_SUBMIT = (): boolean => false
|
||||
@@ -55,7 +54,6 @@ export function useComposerTargetStore(options: ComposerStateInput, decisions: C
|
||||
onRepoIdOverrideChange,
|
||||
telemetrySource,
|
||||
enableIssueAutomation = true,
|
||||
createGateMode = 'full',
|
||||
initialProjectGroupId
|
||||
} = options
|
||||
|
||||
@@ -63,11 +61,8 @@ export function useComposerTargetStore(options: ComposerStateInput, decisions: C
|
||||
useShallow((s) => ({
|
||||
setNewWorkspaceDraft: s.setNewWorkspaceDraft,
|
||||
clearNewWorkspaceDraft: s.clearNewWorkspaceDraft,
|
||||
createWorktree: s.createWorktree,
|
||||
updateRepo: s.updateRepo,
|
||||
updateWorktreeMeta: s.updateWorktreeMeta,
|
||||
createFolderWorkspace: s.createFolderWorkspace,
|
||||
setSidebarOpen: s.setSidebarOpen,
|
||||
closeModal: s.closeModal,
|
||||
openSettingsPage: s.openSettingsPage,
|
||||
openSettingsTarget: s.openSettingsTarget,
|
||||
@@ -81,11 +76,8 @@ export function useComposerTargetStore(options: ComposerStateInput, decisions: C
|
||||
const {
|
||||
setNewWorkspaceDraft,
|
||||
clearNewWorkspaceDraft,
|
||||
createWorktree,
|
||||
updateRepo,
|
||||
updateWorktreeMeta,
|
||||
createFolderWorkspace,
|
||||
setSidebarOpen,
|
||||
closeModal,
|
||||
openSettingsPage,
|
||||
openSettingsTarget,
|
||||
@@ -178,17 +170,13 @@ export function useComposerTargetStore(options: ComposerStateInput, decisions: C
|
||||
onRepoIdOverrideChange,
|
||||
telemetrySource,
|
||||
enableIssueAutomation,
|
||||
createGateMode,
|
||||
initialProjectGroupId,
|
||||
decisions,
|
||||
actions,
|
||||
setNewWorkspaceDraft,
|
||||
clearNewWorkspaceDraft,
|
||||
createWorktree,
|
||||
updateRepo,
|
||||
updateWorktreeMeta,
|
||||
createFolderWorkspace,
|
||||
setSidebarOpen,
|
||||
closeModal,
|
||||
openSettingsPage,
|
||||
openSettingsTarget,
|
||||
|
||||
@@ -128,18 +128,6 @@ export function useDerivedComposerState(input: DerivedComposerStateInput) {
|
||||
|
||||
const linkedWorkItemProvider = linkedWorkItem ? getLinkedWorkItemProvider(linkedWorkItem) : null
|
||||
|
||||
// Why: sentinel-based Jira/Linear items must bypass repository issue templates.
|
||||
const willApplyIssueCommandAsPrompt =
|
||||
enableIssueAutomation &&
|
||||
!agentPrompt.trim() &&
|
||||
Boolean(linkedWorkItem) &&
|
||||
canUseIssueCommandForLinkedItemProvider(linkedWorkItemProvider)
|
||||
|
||||
const shouldWaitForIssueAutomationCheck =
|
||||
enableIssueAutomation &&
|
||||
(parsedLinkedIssueNumber !== null || willApplyIssueCommandAsPrompt) &&
|
||||
!hasLoadedIssueCommand
|
||||
|
||||
const requiresExplicitSetupChoice = Boolean(setupConfig) && setupPolicy === 'ask'
|
||||
|
||||
const resolvedSetupDecision =
|
||||
@@ -258,8 +246,6 @@ export function useDerivedComposerState(input: DerivedComposerStateInput) {
|
||||
setupConfig,
|
||||
setupPolicy,
|
||||
linkedWorkItemProvider,
|
||||
willApplyIssueCommandAsPrompt,
|
||||
shouldWaitForIssueAutomationCheck,
|
||||
requiresExplicitSetupChoice,
|
||||
resolvedSetupDecision,
|
||||
isSetupCheckPending,
|
||||
|
||||
@@ -17,8 +17,6 @@ export type ComposerDerivedModel = {
|
||||
setupConfig: SetupConfig | null
|
||||
setupPolicy: SetupRunPolicy
|
||||
linkedWorkItemProvider: 'github' | 'gitlab' | 'linear' | 'jira' | null
|
||||
willApplyIssueCommandAsPrompt: boolean
|
||||
shouldWaitForIssueAutomationCheck: boolean
|
||||
requiresExplicitSetupChoice: boolean
|
||||
resolvedSetupDecision: 'skip' | 'run' | null
|
||||
isSetupCheckPending: boolean
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
useFullCreationExecution,
|
||||
type FullCreationExecutionInput
|
||||
} from './full-creation-execution'
|
||||
import type { PreparedFullSubmit } from './composer-submit-model'
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void
|
||||
const promise = new Promise<T>((next) => {
|
||||
resolve = next
|
||||
})
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
describe('useFullCreationExecution cancellation', () => {
|
||||
it('does not create after dismissal while the late startup-policy preflight is pending', async () => {
|
||||
const startupPolicy = deferred<boolean>()
|
||||
let cancelled = false
|
||||
const createWorktree = vi.fn<FullCreationExecutionInput['createWorktree']>()
|
||||
const prepared = {
|
||||
submitLinkedWorkItem: null,
|
||||
submitLinkedIssueNumber: null,
|
||||
submitLinkedPR: null,
|
||||
submitTitleName: null,
|
||||
nameIsAutoManaged: false,
|
||||
smartGitHubCreateNames: {
|
||||
workspaceName: 'workspace',
|
||||
displayName: undefined
|
||||
},
|
||||
workspaceName: 'workspace',
|
||||
nameWasGenerated: false,
|
||||
submitBaseBranch: 'main',
|
||||
submitCompareBaseRef: undefined,
|
||||
submitPushTarget: undefined,
|
||||
submitBranchNameOverride: undefined,
|
||||
submitLinkedWorkItemProvider: null,
|
||||
submitStartupPrompt: '',
|
||||
submitShouldRunIssueAutomation: false,
|
||||
effectiveSetupDecision: 'skip',
|
||||
issueCommandTrustDecision: 'skip',
|
||||
confirmedIssueCommandTemplate: '',
|
||||
linkedLinearIssue: undefined,
|
||||
linkedLinearIssueWorkspaceId: undefined,
|
||||
linkedLinearIssueOrganizationUrlKey: undefined,
|
||||
effectiveBranchNameOverride: undefined,
|
||||
createDisplayName: undefined,
|
||||
pendingFirstAgentMessageRename: false,
|
||||
startupPlan: null,
|
||||
shouldSeedInitialAgentStatus: false,
|
||||
composerTelemetry: {
|
||||
agent_kind: 'claude-code',
|
||||
launch_source: 'new_workspace_composer',
|
||||
request_kind: 'new'
|
||||
},
|
||||
backendStartup: undefined
|
||||
} satisfies PreparedFullSubmit
|
||||
const persistSetupAgentStartupPolicy = vi.fn(() => startupPolicy.promise)
|
||||
const state = {
|
||||
applyWorktreeMeta: vi
|
||||
.fn<FullCreationExecutionInput['applyWorktreeMeta']>()
|
||||
.mockResolvedValue(),
|
||||
clearNewWorkspaceDraft: vi.fn<FullCreationExecutionInput['clearNewWorkspaceDraft']>(),
|
||||
createWorktree,
|
||||
effectivePresetId: null,
|
||||
isSubmissionCancelled: () => cancelled,
|
||||
linkedGitLabIssue: null,
|
||||
linkedGitLabMR: null,
|
||||
normalizedSparseDirectories: [],
|
||||
note: '',
|
||||
onCreated: vi.fn<NonNullable<FullCreationExecutionInput['onCreated']>>(),
|
||||
parentWorktreeId: null,
|
||||
persistDraft: false,
|
||||
persistSetupAgentStartupPolicy,
|
||||
prepareFullSubmit: vi
|
||||
.fn<FullCreationExecutionInput['prepareFullSubmit']>()
|
||||
.mockResolvedValue(prepared),
|
||||
resolvedInitialWorkspaceStatus: undefined,
|
||||
selectedRepoExecutionHostId: 'local',
|
||||
selectedRepoIsGit: true,
|
||||
setSidebarOpen: vi.fn<FullCreationExecutionInput['setSidebarOpen']>(),
|
||||
sparseEnabled: false,
|
||||
taskSourceContext: null,
|
||||
telemetrySource: undefined,
|
||||
tuiAgent: 'claude'
|
||||
} satisfies FullCreationExecutionInput
|
||||
const hook = renderHook(() => useFullCreationExecution(state))
|
||||
|
||||
let creation!: Promise<void>
|
||||
act(() => {
|
||||
creation = hook.result.current.executeFullCreation({ kind: 'none' }, 'repo-1')
|
||||
})
|
||||
await act(() => Promise.resolve())
|
||||
expect(persistSetupAgentStartupPolicy).toHaveBeenCalledTimes(1)
|
||||
|
||||
cancelled = true
|
||||
startupPolicy.resolve(true)
|
||||
await act(async () => creation)
|
||||
|
||||
expect(createWorktree).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,309 +0,0 @@
|
||||
import type { ComposerModel } from './composer-model'
|
||||
|
||||
export type FullCreationExecutionInput = Pick<
|
||||
ComposerModel,
|
||||
| 'applyWorktreeMeta'
|
||||
| 'clearNewWorkspaceDraft'
|
||||
| 'createWorktree'
|
||||
| 'effectivePresetId'
|
||||
| 'isSubmissionCancelled'
|
||||
| 'linkedGitLabIssue'
|
||||
| 'linkedGitLabMR'
|
||||
| 'normalizedSparseDirectories'
|
||||
| 'note'
|
||||
| 'onCreated'
|
||||
| 'parentWorktreeId'
|
||||
| 'persistDraft'
|
||||
| 'persistSetupAgentStartupPolicy'
|
||||
| 'prepareFullSubmit'
|
||||
| 'resolvedInitialWorkspaceStatus'
|
||||
| 'selectedRepoExecutionHostId'
|
||||
| 'selectedRepoIsGit'
|
||||
| 'setSidebarOpen'
|
||||
| 'sparseEnabled'
|
||||
| 'taskSourceContext'
|
||||
| 'telemetrySource'
|
||||
| 'tuiAgent'
|
||||
>
|
||||
|
||||
import { useCallback } from 'react'
|
||||
import type { PendingSmartGitHubSubmitResolution } from './source-selection-decisions'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { settleComposerSubmit } from '@/lib/composer-submit-cancellation'
|
||||
import { toFolderWorkspaceLinkedTask } from '@/components/sidebar/folder-workspace-composer-helpers'
|
||||
import { ensureAgentStartupInTerminal } from '@/lib/new-workspace'
|
||||
import { createBrowserUuid } from '@/lib/browser-uuid'
|
||||
import { activateAndRevealWorktree } from '@/lib/worktree-activation'
|
||||
import { seedNativeChatAppliedSessionOptions } from '@/components/native-chat/native-chat-session-option-cache'
|
||||
import { queueWorkspaceActivationTerminalFocus } from '@/lib/workspace-activation-terminal-focus'
|
||||
import { useAppStore } from '@/store'
|
||||
import { planAgentSessionLaunch } from '@/lib/agent-session-launch-plan'
|
||||
import { newAgentLaunchRequestId } from '@/lib/agent-launch-request-id'
|
||||
import { beginFullCreationStructuredLaunch } from './full-creation-structured-launch'
|
||||
import { finalizeFullCreation } from './full-creation-finalization'
|
||||
import { buildFullCreationIssueCommand } from './full-creation-issue-command'
|
||||
import { buildFullCreationStartup } from './full-creation-startup'
|
||||
|
||||
export function useFullCreationExecution(input: FullCreationExecutionInput) {
|
||||
const {
|
||||
applyWorktreeMeta,
|
||||
clearNewWorkspaceDraft,
|
||||
createWorktree,
|
||||
effectivePresetId,
|
||||
isSubmissionCancelled,
|
||||
linkedGitLabIssue,
|
||||
linkedGitLabMR,
|
||||
normalizedSparseDirectories,
|
||||
note,
|
||||
onCreated,
|
||||
parentWorktreeId,
|
||||
persistDraft,
|
||||
persistSetupAgentStartupPolicy,
|
||||
prepareFullSubmit,
|
||||
resolvedInitialWorkspaceStatus,
|
||||
selectedRepoExecutionHostId,
|
||||
selectedRepoIsGit,
|
||||
setSidebarOpen,
|
||||
sparseEnabled,
|
||||
taskSourceContext,
|
||||
telemetrySource,
|
||||
tuiAgent
|
||||
} = input
|
||||
|
||||
const executeFullCreation = useCallback(
|
||||
async (
|
||||
smartGitHubResolution: PendingSmartGitHubSubmitResolution,
|
||||
repoId: string
|
||||
): Promise<void> => {
|
||||
const prepared = await prepareFullSubmit(smartGitHubResolution)
|
||||
|
||||
if (!prepared) {
|
||||
return
|
||||
}
|
||||
|
||||
const {
|
||||
submitLinkedWorkItem,
|
||||
submitLinkedIssueNumber,
|
||||
submitLinkedPR,
|
||||
workspaceName,
|
||||
nameWasGenerated,
|
||||
nameIsAutoManaged,
|
||||
submitBaseBranch,
|
||||
submitCompareBaseRef,
|
||||
submitPushTarget,
|
||||
submitStartupPrompt,
|
||||
submitShouldRunIssueAutomation,
|
||||
effectiveSetupDecision,
|
||||
issueCommandTrustDecision,
|
||||
confirmedIssueCommandTemplate,
|
||||
linkedLinearIssue,
|
||||
linkedLinearIssueWorkspaceId,
|
||||
linkedLinearIssueOrganizationUrlKey,
|
||||
effectiveBranchNameOverride,
|
||||
createDisplayName,
|
||||
pendingFirstAgentMessageRename,
|
||||
startupPlan,
|
||||
shouldSeedInitialAgentStatus,
|
||||
composerTelemetry,
|
||||
backendStartup
|
||||
} = prepared
|
||||
|
||||
const startupPolicySettlement = await settleComposerSubmit(
|
||||
persistSetupAgentStartupPolicy(),
|
||||
isSubmissionCancelled
|
||||
)
|
||||
|
||||
if (startupPolicySettlement.status === 'cancelled') {
|
||||
return
|
||||
}
|
||||
|
||||
if (!startupPolicySettlement.value) {
|
||||
throw new Error(
|
||||
translate(
|
||||
'auto.hooks.useComposerState.setupAgentStartupPolicySaveFailed',
|
||||
'Failed to save setup startup behavior.'
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (isSubmissionCancelled()) {
|
||||
return
|
||||
}
|
||||
|
||||
const launchPlan = planAgentSessionLaunch(useAppStore.getState(), {
|
||||
requestId: newAgentLaunchRequestId(),
|
||||
agent: tuiAgent,
|
||||
workspace: {
|
||||
kind: selectedRepoIsGit ? 'git-worktree' : 'folder',
|
||||
repoId,
|
||||
executionHostId: selectedRepoExecutionHostId ?? undefined
|
||||
},
|
||||
prompt: startupPlan?.draftPrompt ?? submitStartupPrompt,
|
||||
promptDelivery: startupPlan?.draftPrompt ? 'draft' : 'auto-submit',
|
||||
initialSessionOptions: startupPlan?.sessionOptions
|
||||
})
|
||||
const structuredLaunch = launchPlan.route === 'structured-native-chat'
|
||||
const effectiveBackendStartup = structuredLaunch ? undefined : backendStartup
|
||||
|
||||
const result = await createWorktree(
|
||||
repoId,
|
||||
workspaceName,
|
||||
selectedRepoIsGit ? submitBaseBranch : undefined,
|
||||
effectiveSetupDecision,
|
||||
selectedRepoIsGit && sparseEnabled
|
||||
? {
|
||||
directories: normalizedSparseDirectories,
|
||||
...(effectivePresetId ? { presetId: effectivePresetId } : {})
|
||||
}
|
||||
: undefined,
|
||||
telemetrySource,
|
||||
createDisplayName,
|
||||
submitLinkedIssueNumber ?? undefined,
|
||||
submitLinkedPR ?? undefined,
|
||||
submitPushTarget,
|
||||
tuiAgent,
|
||||
linkedLinearIssue,
|
||||
effectiveBranchNameOverride,
|
||||
resolvedInitialWorkspaceStatus,
|
||||
smartGitHubResolution.kind === 'none' ? (linkedGitLabMR ?? undefined) : undefined,
|
||||
smartGitHubResolution.kind === 'none' ? (linkedGitLabIssue ?? undefined) : undefined,
|
||||
effectiveBackendStartup,
|
||||
pendingFirstAgentMessageRename,
|
||||
undefined,
|
||||
linkedLinearIssueWorkspaceId,
|
||||
linkedLinearIssueOrganizationUrlKey,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
submitCompareBaseRef,
|
||||
{
|
||||
linkedWorkItem: toFolderWorkspaceLinkedTask(submitLinkedWorkItem),
|
||||
linkedTaskSourceContext: taskSourceContext,
|
||||
nameWasGenerated,
|
||||
...(createDisplayName
|
||||
? { displayNameKind: nameIsAutoManaged ? ('generated' as const) : ('user' as const) }
|
||||
: {}),
|
||||
...(!structuredLaunch && !effectiveBackendStartup && startupPlan?.draftPrompt
|
||||
? { startupDraft: startupPlan.draftPrompt }
|
||||
: {}),
|
||||
...(parentWorktreeId ? { parentWorktreeId } : {})
|
||||
}
|
||||
)
|
||||
|
||||
const worktree = result.worktree
|
||||
const issueCommand = buildFullCreationIssueCommand({
|
||||
shouldRun: submitShouldRunIssueAutomation && issueCommandTrustDecision === 'run',
|
||||
template: confirmedIssueCommandTemplate,
|
||||
issueNumber: submitLinkedIssueNumber,
|
||||
artifactUrl: submitLinkedWorkItem?.url
|
||||
})
|
||||
|
||||
const backendSpawnedStartup = result.startupTerminal?.spawned === true
|
||||
|
||||
if (startupPlan && !backendSpawnedStartup && !startupPlan.launchToken) {
|
||||
// Why: delayed delivery must target the exact pane from this queued startup, so both halves share one renderer-session token.
|
||||
startupPlan.launchToken = createBrowserUuid()
|
||||
}
|
||||
|
||||
const startup = buildFullCreationStartup({
|
||||
startupPlan,
|
||||
backendSpawnedStartup,
|
||||
agent: tuiAgent,
|
||||
shouldSeedInitialAgentStatus,
|
||||
prompt: submitStartupPrompt,
|
||||
telemetry: composerTelemetry
|
||||
})
|
||||
|
||||
const activationHolder: { value: ReturnType<typeof activateAndRevealWorktree> } = {
|
||||
value: false
|
||||
}
|
||||
const revealWorkspace = (): boolean => {
|
||||
activationHolder.value = activateAndRevealWorktree(worktree.id, {
|
||||
sidebarRevealBehavior: 'auto',
|
||||
agent: tuiAgent,
|
||||
setup: result.setup,
|
||||
defaultTabs: result.defaultTabs,
|
||||
issueCommand,
|
||||
...(backendSpawnedStartup ? { backendStartupTerminalSpawned: true } : {}),
|
||||
...(!structuredLaunch && startup ? { startup } : {}),
|
||||
...(structuredLaunch ? { providesInitialSurface: true } : {})
|
||||
})
|
||||
return activationHolder.value !== false
|
||||
}
|
||||
if (structuredLaunch) {
|
||||
try {
|
||||
beginFullCreationStructuredLaunch({
|
||||
plan: launchPlan,
|
||||
worktreeId: worktree.id,
|
||||
beforeOpen: revealWorkspace
|
||||
})
|
||||
} catch (error) {
|
||||
// Why: a failed reveal must not turn a structured route into a legacy terminal; the
|
||||
// completed workspace remains usable and the launch surface can be retried there.
|
||||
console.error('full creation: structured chat surface failed', worktree.id, error)
|
||||
}
|
||||
}
|
||||
if (!structuredLaunch) {
|
||||
revealWorkspace()
|
||||
}
|
||||
const structuredLaunchAccepted = structuredLaunch
|
||||
const activation = activationHolder.value
|
||||
|
||||
const trimmedNote = note.trim()
|
||||
await applyWorktreeMeta(worktree.id, trimmedNote ? { comment: trimmedNote } : {})
|
||||
|
||||
if (!structuredLaunchAccepted && startupPlan) {
|
||||
const optionScopeKey =
|
||||
(activation !== false ? activation.primaryTabId : null) ?? result.startupTerminal?.tabId
|
||||
if (optionScopeKey) {
|
||||
seedNativeChatAppliedSessionOptions(optionScopeKey, tuiAgent, startupPlan.sessionOptions)
|
||||
}
|
||||
}
|
||||
|
||||
if (!structuredLaunchAccepted && startupPlan && !backendSpawnedStartup) {
|
||||
void ensureAgentStartupInTerminal({
|
||||
worktreeId: worktree.id,
|
||||
primaryTabId: activation === false ? null : activation.primaryTabId,
|
||||
startup: startupPlan
|
||||
})
|
||||
}
|
||||
|
||||
finalizeFullCreation({
|
||||
setSidebarOpen,
|
||||
persistDraft,
|
||||
clearNewWorkspaceDraft,
|
||||
onCreated,
|
||||
structuredLaunchAccepted,
|
||||
worktreeId: worktree.id,
|
||||
activation,
|
||||
queueWorkspaceActivationTerminalFocus
|
||||
})
|
||||
},
|
||||
[
|
||||
applyWorktreeMeta,
|
||||
clearNewWorkspaceDraft,
|
||||
createWorktree,
|
||||
effectivePresetId,
|
||||
isSubmissionCancelled,
|
||||
linkedGitLabIssue,
|
||||
linkedGitLabMR,
|
||||
normalizedSparseDirectories,
|
||||
note,
|
||||
onCreated,
|
||||
parentWorktreeId,
|
||||
persistDraft,
|
||||
persistSetupAgentStartupPolicy,
|
||||
prepareFullSubmit,
|
||||
resolvedInitialWorkspaceStatus,
|
||||
selectedRepoExecutionHostId,
|
||||
selectedRepoIsGit,
|
||||
setSidebarOpen,
|
||||
sparseEnabled,
|
||||
taskSourceContext,
|
||||
telemetrySource,
|
||||
tuiAgent
|
||||
]
|
||||
)
|
||||
|
||||
return { executeFullCreation }
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { ActivateAndRevealResult } from '@/lib/worktree-activation'
|
||||
|
||||
export function finalizeFullCreation(args: {
|
||||
setSidebarOpen: (open: boolean) => void
|
||||
persistDraft: boolean
|
||||
clearNewWorkspaceDraft: () => void
|
||||
onCreated?: () => void
|
||||
structuredLaunchAccepted: boolean
|
||||
worktreeId: string
|
||||
activation: ActivateAndRevealResult | false
|
||||
queueWorkspaceActivationTerminalFocus: (
|
||||
worktreeId: string,
|
||||
activation: ActivateAndRevealResult | false
|
||||
) => void
|
||||
}): void {
|
||||
args.setSidebarOpen(true)
|
||||
if (args.persistDraft) {
|
||||
args.clearNewWorkspaceDraft()
|
||||
}
|
||||
args.onCreated?.()
|
||||
if (!args.structuredLaunchAccepted) {
|
||||
args.queueWorkspaceActivationTerminalFocus(args.worktreeId, args.activation)
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { renderIssueCommandTemplate } from '@/lib/new-workspace'
|
||||
|
||||
export function buildFullCreationIssueCommand(args: {
|
||||
shouldRun: boolean
|
||||
template: string
|
||||
issueNumber: number | null | undefined
|
||||
artifactUrl: string | null | undefined
|
||||
}): { command: string } | undefined {
|
||||
if (!args.shouldRun) {
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
command: renderIssueCommandTemplate(args.template, {
|
||||
issueNumber: args.issueNumber ?? null,
|
||||
artifactUrl: args.artifactUrl ?? null
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import type { TuiAgent } from '../../../../shared/tui-agent'
|
||||
import type { AgentStartupPlan } from '@/lib/tui-agent-startup'
|
||||
import type { WorktreeStartupPayload } from '@/lib/worktree-startup-payload'
|
||||
|
||||
export function buildFullCreationStartup(args: {
|
||||
startupPlan: AgentStartupPlan | null
|
||||
backendSpawnedStartup: boolean
|
||||
agent: TuiAgent
|
||||
shouldSeedInitialAgentStatus: boolean
|
||||
prompt: string
|
||||
telemetry: WorktreeStartupPayload['telemetry']
|
||||
}): WorktreeStartupPayload | undefined {
|
||||
if (!args.startupPlan || args.backendSpawnedStartup) {
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
command: args.startupPlan.launchCommand,
|
||||
...(args.startupPlan.env ? { env: args.startupPlan.env } : {}),
|
||||
launchConfig: args.startupPlan.launchConfig,
|
||||
...(args.startupPlan.launchToken ? { launchToken: args.startupPlan.launchToken } : {}),
|
||||
launchAgent: args.agent,
|
||||
...(args.startupPlan.draftPrompt ? { draftPrompt: args.startupPlan.draftPrompt } : {}),
|
||||
...(args.startupPlan.startupCommandDelivery
|
||||
? { startupCommandDelivery: args.startupPlan.startupCommandDelivery }
|
||||
: {}),
|
||||
...(args.shouldSeedInitialAgentStatus
|
||||
? { initialAgentStatus: { agent: args.agent, prompt: args.prompt.trim() } }
|
||||
: {}),
|
||||
telemetry: args.telemetry
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { AgentSessionLaunchPlan } from '@/lib/agent-session-launch-plan'
|
||||
import {
|
||||
beginStructuredAgentSessionProvisionalLaunch,
|
||||
type StructuredAgentSessionProvisionalLaunch
|
||||
} from '@/lib/structured-agent-session-provisional-tab'
|
||||
|
||||
/** Full-create dialog: the structured launch plus what this flow did before structured chat
|
||||
* existed. Returns null when no visible surface can be owned. */
|
||||
export function beginFullCreationStructuredLaunch(args: {
|
||||
/** Planned before the worktree existed; `worktreeId` names the one that was created. */
|
||||
plan: AgentSessionLaunchPlan
|
||||
worktreeId: string
|
||||
beforeOpen: (sessionId?: string) => boolean | void
|
||||
}): StructuredAgentSessionProvisionalLaunch | null {
|
||||
return beginStructuredAgentSessionProvisionalLaunch({
|
||||
plan: args.plan,
|
||||
hooks: {},
|
||||
target: { worktreeId: args.worktreeId },
|
||||
beforeOpen: args.beforeOpen
|
||||
})
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
useFullSubmitOrchestration,
|
||||
type FullSubmitOrchestrationInput
|
||||
} from './full-submit-orchestration'
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void
|
||||
const promise = new Promise<T>((next) => {
|
||||
resolve = next
|
||||
})
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
describe('useFullSubmitOrchestration cancellation', () => {
|
||||
it('settles creating but never crosses the irreversible creation boundary after dismissal', async () => {
|
||||
const resolution = deferred<{ kind: 'none' }>()
|
||||
let cancelled = false
|
||||
const executeFullCreation = vi.fn<FullSubmitOrchestrationInput['executeFullCreation']>()
|
||||
const setCreating = vi.fn<FullSubmitOrchestrationInput['setCreating']>()
|
||||
const state = {
|
||||
disabledTuiAgents: [],
|
||||
executeFullCreation,
|
||||
fallbackDefaultAgent: 'claude',
|
||||
isProjectGroupTarget: false,
|
||||
isSubmissionCancelled: () => cancelled,
|
||||
repoId: 'repo-1',
|
||||
requiresExplicitSetupChoice: false,
|
||||
resolvePendingSmartGitHubSubmit: () => resolution.promise,
|
||||
selectedRepo: {
|
||||
id: 'repo-1',
|
||||
path: '/repos/repo-1',
|
||||
displayName: 'Repo 1',
|
||||
badgeColor: '#000000',
|
||||
addedAt: 0
|
||||
},
|
||||
selectedRepoRequiresConnection: false,
|
||||
setCreateError: vi.fn<FullSubmitOrchestrationInput['setCreateError']>(),
|
||||
setCreating,
|
||||
setTuiAgent: vi.fn<FullSubmitOrchestrationInput['setTuiAgent']>(),
|
||||
setupDecision: null,
|
||||
shouldWaitForIssueAutomationCheck: false,
|
||||
shouldWaitForSetupCheck: false,
|
||||
showProjectRequiredError: vi.fn<FullSubmitOrchestrationInput['showProjectRequiredError']>(),
|
||||
sourceIntentBlocksCreate: false,
|
||||
sparseError: null,
|
||||
submitFolderTarget: vi.fn<FullSubmitOrchestrationInput['submitFolderTarget']>(),
|
||||
tuiAgent: 'claude',
|
||||
workspaceSeedName: 'workspace'
|
||||
} satisfies FullSubmitOrchestrationInput
|
||||
const hook = renderHook(() => useFullSubmitOrchestration(state))
|
||||
|
||||
let submission!: Promise<void>
|
||||
act(() => {
|
||||
submission = hook.result.current.submit()
|
||||
})
|
||||
expect(setCreating).toHaveBeenCalledWith(true)
|
||||
|
||||
cancelled = true
|
||||
resolution.resolve({ kind: 'none' })
|
||||
await act(async () => submission)
|
||||
|
||||
expect(executeFullCreation).not.toHaveBeenCalled()
|
||||
expect(setCreating).toHaveBeenLastCalledWith(false)
|
||||
})
|
||||
})
|
||||
@@ -1,149 +0,0 @@
|
||||
import type { ComposerModel } from './composer-model'
|
||||
|
||||
export type FullSubmitOrchestrationInput = Pick<
|
||||
ComposerModel,
|
||||
| 'disabledTuiAgents'
|
||||
| 'executeFullCreation'
|
||||
| 'fallbackDefaultAgent'
|
||||
| 'isProjectGroupTarget'
|
||||
| 'isSubmissionCancelled'
|
||||
| 'repoId'
|
||||
| 'requiresExplicitSetupChoice'
|
||||
| 'resolvePendingSmartGitHubSubmit'
|
||||
| 'selectedRepo'
|
||||
| 'selectedRepoRequiresConnection'
|
||||
| 'setCreateError'
|
||||
| 'setCreating'
|
||||
| 'setTuiAgent'
|
||||
| 'setupDecision'
|
||||
| 'shouldWaitForIssueAutomationCheck'
|
||||
| 'shouldWaitForSetupCheck'
|
||||
| 'showProjectRequiredError'
|
||||
| 'sourceIntentBlocksCreate'
|
||||
| 'sparseError'
|
||||
| 'submitFolderTarget'
|
||||
| 'tuiAgent'
|
||||
| 'workspaceSeedName'
|
||||
>
|
||||
|
||||
import { useCallback } from 'react'
|
||||
import { isTuiAgentEnabled } from '../../../../shared/tui-agent-selection'
|
||||
import { toast } from 'sonner'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { settleComposerSubmit } from '@/lib/composer-submit-cancellation'
|
||||
import {
|
||||
formatWorkspaceCreateError,
|
||||
getWorkspaceCreateErrorToastMessage
|
||||
} from '@/lib/workspace-create-error-format'
|
||||
|
||||
export function useFullSubmitOrchestration(input: FullSubmitOrchestrationInput) {
|
||||
const {
|
||||
disabledTuiAgents,
|
||||
executeFullCreation,
|
||||
fallbackDefaultAgent,
|
||||
isProjectGroupTarget,
|
||||
isSubmissionCancelled,
|
||||
repoId,
|
||||
requiresExplicitSetupChoice,
|
||||
resolvePendingSmartGitHubSubmit,
|
||||
selectedRepo,
|
||||
selectedRepoRequiresConnection,
|
||||
setCreateError,
|
||||
setCreating,
|
||||
setTuiAgent,
|
||||
setupDecision,
|
||||
shouldWaitForIssueAutomationCheck,
|
||||
shouldWaitForSetupCheck,
|
||||
showProjectRequiredError,
|
||||
sourceIntentBlocksCreate,
|
||||
sparseError,
|
||||
submitFolderTarget,
|
||||
tuiAgent,
|
||||
workspaceSeedName
|
||||
} = input
|
||||
|
||||
const submit = useCallback(async (): Promise<void> => {
|
||||
if (isProjectGroupTarget) {
|
||||
await submitFolderTarget(tuiAgent)
|
||||
return
|
||||
}
|
||||
|
||||
if (!repoId || !selectedRepo) {
|
||||
showProjectRequiredError()
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
!workspaceSeedName ||
|
||||
selectedRepoRequiresConnection ||
|
||||
shouldWaitForSetupCheck ||
|
||||
shouldWaitForIssueAutomationCheck ||
|
||||
sourceIntentBlocksCreate ||
|
||||
(requiresExplicitSetupChoice && !setupDecision) ||
|
||||
sparseError !== null
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!isTuiAgentEnabled(tuiAgent, disabledTuiAgents)) {
|
||||
setTuiAgent(fallbackDefaultAgent)
|
||||
toast.error(
|
||||
translate(
|
||||
'auto.hooks.useComposerState.7eb3f44ff7',
|
||||
'Selected agent is disabled. Choose an enabled agent before creating.'
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
setCreateError(null)
|
||||
|
||||
setCreating(true)
|
||||
try {
|
||||
const smartGitHubSettlement = await settleComposerSubmit(
|
||||
resolvePendingSmartGitHubSubmit(),
|
||||
isSubmissionCancelled
|
||||
)
|
||||
if (smartGitHubSettlement.status === 'cancelled') {
|
||||
return
|
||||
}
|
||||
await executeFullCreation(smartGitHubSettlement.value, repoId)
|
||||
} catch (error) {
|
||||
if (isSubmissionCancelled()) {
|
||||
return
|
||||
}
|
||||
const formattedError = formatWorkspaceCreateError(error)
|
||||
setCreateError(formattedError)
|
||||
toast.error(getWorkspaceCreateErrorToastMessage(formattedError))
|
||||
} finally {
|
||||
setCreating(false)
|
||||
}
|
||||
}, [
|
||||
disabledTuiAgents,
|
||||
executeFullCreation,
|
||||
fallbackDefaultAgent,
|
||||
isProjectGroupTarget,
|
||||
isSubmissionCancelled,
|
||||
repoId,
|
||||
requiresExplicitSetupChoice,
|
||||
resolvePendingSmartGitHubSubmit,
|
||||
selectedRepo,
|
||||
selectedRepoRequiresConnection,
|
||||
setCreateError,
|
||||
setCreating,
|
||||
setTuiAgent,
|
||||
setupDecision,
|
||||
shouldWaitForIssueAutomationCheck,
|
||||
shouldWaitForSetupCheck,
|
||||
showProjectRequiredError,
|
||||
sourceIntentBlocksCreate,
|
||||
sparseError,
|
||||
submitFolderTarget,
|
||||
tuiAgent,
|
||||
workspaceSeedName
|
||||
])
|
||||
|
||||
return {
|
||||
submit
|
||||
}
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
import type { ComposerModel } from './composer-model'
|
||||
|
||||
type FullSubmitPreparationInput = Pick<
|
||||
ComposerModel,
|
||||
| 'branchAutoNameRef'
|
||||
| 'branchNameOverridePreservesNameEdits'
|
||||
| 'currentIssueCommand'
|
||||
| 'isSubmissionCancelled'
|
||||
| 'issueCommandTemplate'
|
||||
| 'name'
|
||||
| 'prepareFullSubmitSource'
|
||||
| 'repoId'
|
||||
| 'resolvedSetupDecision'
|
||||
| 'selectedRepo'
|
||||
| 'selectedRepoAgentLaunchPlatform'
|
||||
| 'selectedRepoExecutionHostId'
|
||||
| 'selectedRepoIsGit'
|
||||
| 'selectedRepoIsRemote'
|
||||
| 'selectedRepoStartupShell'
|
||||
| 'settings'
|
||||
| 'smartNameMode'
|
||||
| 'telemetrySource'
|
||||
| 'tuiAgent'
|
||||
>
|
||||
|
||||
import { useCallback } from 'react'
|
||||
import { settleComposerSubmit } from '@/lib/composer-submit-cancellation'
|
||||
import { ensureHooksConfirmed, confirmRuntimeIssueCommandRead } from '@/lib/ensure-hooks-confirmed'
|
||||
import { useAppStore } from '@/store'
|
||||
import type { SetupDecision } from '../../../../shared/worktree/create-types'
|
||||
import { resolveComposerBranchNameOverrideForCreate } from '../composer-branch-selection'
|
||||
import { buildAgentStartupPlan } from '@/lib/tui-agent-startup'
|
||||
import {
|
||||
resolveTuiAgentLaunchArgs,
|
||||
resolveTuiAgentLaunchEnv
|
||||
} from '../../../../shared/tui-agent-launch-defaults'
|
||||
import { resolveInitialNativeChatSessionOptions } from '@/components/native-chat/native-chat-launch-session-options'
|
||||
import { isNativeChatTranscriptLocalReadable } from '@/lib/native-chat-transcript-readability'
|
||||
import type { AgentStartedTelemetry } from '@/lib/worktree-startup-payload'
|
||||
import { tuiAgentToAgentKind } from '@/lib/telemetry'
|
||||
import type { PendingSmartGitHubSubmitResolution } from './source-selection-decisions'
|
||||
|
||||
export function useFullSubmitPreparation(input: FullSubmitPreparationInput) {
|
||||
const {
|
||||
branchAutoNameRef,
|
||||
branchNameOverridePreservesNameEdits,
|
||||
currentIssueCommand,
|
||||
isSubmissionCancelled,
|
||||
issueCommandTemplate,
|
||||
name,
|
||||
prepareFullSubmitSource,
|
||||
repoId,
|
||||
resolvedSetupDecision,
|
||||
selectedRepo,
|
||||
selectedRepoAgentLaunchPlatform,
|
||||
selectedRepoExecutionHostId,
|
||||
selectedRepoIsGit,
|
||||
selectedRepoIsRemote,
|
||||
selectedRepoStartupShell,
|
||||
settings,
|
||||
smartNameMode,
|
||||
telemetrySource,
|
||||
tuiAgent
|
||||
} = input
|
||||
|
||||
const prepareFullSubmit = useCallback(
|
||||
async (smartGitHubResolution: PendingSmartGitHubSubmitResolution) => {
|
||||
const source = prepareFullSubmitSource(smartGitHubResolution)
|
||||
if (!source) {
|
||||
return null
|
||||
}
|
||||
const {
|
||||
submitLinkedWorkItem,
|
||||
submitTitleName,
|
||||
nameIsAutoManaged,
|
||||
smartGitHubCreateNames,
|
||||
workspaceName,
|
||||
submitBranchNameOverride,
|
||||
submitLinkedWorkItemProvider,
|
||||
submitStartupPrompt,
|
||||
submitShouldRunIssueAutomation
|
||||
} = source
|
||||
|
||||
const setupTrustSettlement = await settleComposerSubmit(
|
||||
selectedRepoIsGit
|
||||
? ensureHooksConfirmed(
|
||||
useAppStore.getState(),
|
||||
repoId,
|
||||
'setup',
|
||||
selectedRepoExecutionHostId ?? undefined,
|
||||
undefined,
|
||||
isSubmissionCancelled
|
||||
)
|
||||
: Promise.resolve<'skip'>('skip'),
|
||||
isSubmissionCancelled
|
||||
)
|
||||
|
||||
if (setupTrustSettlement.status === 'cancelled') {
|
||||
return null
|
||||
}
|
||||
|
||||
const setupTrustDecision = setupTrustSettlement.value
|
||||
|
||||
const effectiveSetupDecision: SetupDecision =
|
||||
setupTrustDecision === 'skip'
|
||||
? 'skip'
|
||||
: ((resolvedSetupDecision ?? 'inherit') as SetupDecision)
|
||||
|
||||
let issueCommandTrustDecision: 'run' | 'skip' = 'run'
|
||||
|
||||
let confirmedIssueCommandTemplate = issueCommandTemplate
|
||||
|
||||
if (
|
||||
selectedRepoIsGit &&
|
||||
submitShouldRunIssueAutomation &&
|
||||
currentIssueCommand &&
|
||||
selectedRepoExecutionHostId
|
||||
) {
|
||||
if (setupTrustDecision === 'skip') {
|
||||
issueCommandTrustDecision = 'skip'
|
||||
} else {
|
||||
const issueCommandSettlement = await settleComposerSubmit(
|
||||
confirmRuntimeIssueCommandRead(
|
||||
useAppStore.getState(),
|
||||
repoId,
|
||||
selectedRepoExecutionHostId,
|
||||
currentIssueCommand,
|
||||
isSubmissionCancelled
|
||||
),
|
||||
isSubmissionCancelled
|
||||
)
|
||||
if (issueCommandSettlement.status === 'cancelled') {
|
||||
return null
|
||||
}
|
||||
const confirmed = issueCommandSettlement.value
|
||||
issueCommandTrustDecision = confirmed.trustDecision
|
||||
confirmedIssueCommandTemplate = confirmed.template
|
||||
}
|
||||
}
|
||||
|
||||
const linkedLinearIssue =
|
||||
submitLinkedWorkItem && submitLinkedWorkItemProvider === 'linear'
|
||||
? submitLinkedWorkItem.linearIdentifier
|
||||
: undefined
|
||||
|
||||
const linkedLinearIssueWorkspaceId =
|
||||
submitLinkedWorkItem && submitLinkedWorkItemProvider === 'linear'
|
||||
? submitLinkedWorkItem.linearWorkspaceId
|
||||
: undefined
|
||||
|
||||
const linkedLinearIssueOrganizationUrlKey =
|
||||
submitLinkedWorkItem && submitLinkedWorkItemProvider === 'linear'
|
||||
? submitLinkedWorkItem.linearOrganizationUrlKey
|
||||
: undefined
|
||||
|
||||
const effectiveBranchNameOverride = resolveComposerBranchNameOverrideForCreate({
|
||||
branchNameOverride: submitBranchNameOverride,
|
||||
branchAutoName: branchAutoNameRef.current,
|
||||
workspaceName,
|
||||
preserveWorkspaceNameEdits:
|
||||
smartGitHubResolution.kind === 'pr-start-point' || branchNameOverridePreservesNameEdits,
|
||||
createBranchFromWorkspaceName:
|
||||
smartGitHubResolution.kind === 'none' && smartNameMode === 'branches'
|
||||
})
|
||||
|
||||
const createDisplayName = !nameIsAutoManaged
|
||||
? workspaceName
|
||||
: smartGitHubResolution.kind === 'none'
|
||||
? submitTitleName?.displayName
|
||||
: smartGitHubCreateNames.displayName
|
||||
|
||||
// Why: the first-work hook only renames blank, auto-generated git workspaces that launch an agent; persist that pending state for the card.
|
||||
const pendingFirstAgentMessageRename =
|
||||
selectedRepoIsGit &&
|
||||
settings?.autoRenameBranchFromWork === true &&
|
||||
!name.trim() &&
|
||||
Boolean(tuiAgent) &&
|
||||
!effectiveBranchNameOverride &&
|
||||
!createDisplayName
|
||||
|
||||
const startupPlan = buildAgentStartupPlan({
|
||||
agent: tuiAgent,
|
||||
prompt: submitStartupPrompt,
|
||||
cmdOverrides: settings?.agentCmdOverrides ?? {},
|
||||
agentArgs: resolveTuiAgentLaunchArgs(tuiAgent, settings?.agentDefaultArgs),
|
||||
agentEnv: resolveTuiAgentLaunchEnv(tuiAgent, settings?.agentDefaultEnv),
|
||||
sessionOptions: resolveInitialNativeChatSessionOptions(
|
||||
{
|
||||
experimentalNativeChat: settings?.experimentalNativeChat,
|
||||
openAgentTabsInChatByDefault: settings?.openAgentTabsInChatByDefault,
|
||||
nativeChatSessionOptions: settings?.nativeChatSessionOptions
|
||||
},
|
||||
{
|
||||
agent: tuiAgent,
|
||||
nativeChatTranscriptIsLocalReadable: isNativeChatTranscriptLocalReadable(
|
||||
selectedRepo?.connectionId
|
||||
)
|
||||
}
|
||||
),
|
||||
platform: selectedRepoAgentLaunchPlatform,
|
||||
shell: selectedRepoStartupShell,
|
||||
isRemote: selectedRepoIsRemote
|
||||
})
|
||||
|
||||
const shouldSeedInitialAgentStatus =
|
||||
tuiAgent === 'command-code' && submitStartupPrompt.trim().length > 0
|
||||
|
||||
// Why: backend startup is safe only for self-contained launch commands; agents needing post-ready paste stay on the renderer path.
|
||||
const composerTelemetry: AgentStartedTelemetry = {
|
||||
agent_kind: tuiAgentToAgentKind(tuiAgent),
|
||||
launch_source: telemetrySource === 'onboarding' ? 'onboarding' : 'new_workspace_composer',
|
||||
request_kind: 'new'
|
||||
}
|
||||
|
||||
const backendStartup =
|
||||
startupPlan && !startupPlan.draftPrompt && !startupPlan.followupPrompt
|
||||
? {
|
||||
command: startupPlan.launchCommand,
|
||||
...(startupPlan.env ? { env: startupPlan.env } : {}),
|
||||
launchConfig: startupPlan.launchConfig,
|
||||
launchAgent: tuiAgent,
|
||||
...(startupPlan.startupCommandDelivery
|
||||
? { startupCommandDelivery: startupPlan.startupCommandDelivery }
|
||||
: {}),
|
||||
telemetry: composerTelemetry
|
||||
}
|
||||
: undefined
|
||||
|
||||
return Object.assign(source, {
|
||||
effectiveSetupDecision,
|
||||
issueCommandTrustDecision,
|
||||
confirmedIssueCommandTemplate,
|
||||
linkedLinearIssue,
|
||||
linkedLinearIssueWorkspaceId,
|
||||
linkedLinearIssueOrganizationUrlKey,
|
||||
effectiveBranchNameOverride,
|
||||
createDisplayName,
|
||||
pendingFirstAgentMessageRename,
|
||||
startupPlan,
|
||||
shouldSeedInitialAgentStatus,
|
||||
composerTelemetry,
|
||||
backendStartup
|
||||
})
|
||||
},
|
||||
[
|
||||
branchNameOverridePreservesNameEdits,
|
||||
currentIssueCommand,
|
||||
isSubmissionCancelled,
|
||||
issueCommandTemplate,
|
||||
name,
|
||||
prepareFullSubmitSource,
|
||||
repoId,
|
||||
resolvedSetupDecision,
|
||||
selectedRepo,
|
||||
selectedRepoAgentLaunchPlatform,
|
||||
selectedRepoExecutionHostId,
|
||||
selectedRepoIsGit,
|
||||
selectedRepoIsRemote,
|
||||
selectedRepoStartupShell,
|
||||
settings,
|
||||
smartNameMode,
|
||||
telemetrySource,
|
||||
tuiAgent,
|
||||
branchAutoNameRef
|
||||
]
|
||||
)
|
||||
|
||||
return {
|
||||
prepareFullSubmit
|
||||
}
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
import type { ComposerModel } from './composer-model'
|
||||
|
||||
type FullSubmitSourcePreparationInput = Pick<
|
||||
ComposerModel,
|
||||
| 'agentPrompt'
|
||||
| 'attachmentPaths'
|
||||
| 'baseBranch'
|
||||
| 'branchNameOverride'
|
||||
| 'compareBaseRef'
|
||||
| 'effectiveLinkedPR'
|
||||
| 'decisions'
|
||||
| 'enableIssueAutomation'
|
||||
| 'fallbackCreatureName'
|
||||
| 'hasLoadedIssueCommand'
|
||||
| 'issueCommandTemplate'
|
||||
| 'lastAutoNameRef'
|
||||
| 'linkedGitLabMR'
|
||||
| 'linkedWorkItem'
|
||||
| 'name'
|
||||
| 'parsedLinkedIssueNumber'
|
||||
| 'pushTarget'
|
||||
| 'workspaceSeedName'
|
||||
>
|
||||
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
getLinkedWorkItemWorkspaceName,
|
||||
getLinkedWorkItemProvider,
|
||||
canUseIssueCommandForLinkedItemProvider,
|
||||
renderIssueCommandTemplate,
|
||||
DEFAULT_ISSUE_COMMAND_TEMPLATE,
|
||||
buildAgentPromptWithContext
|
||||
} from '@/lib/new-workspace'
|
||||
import { getLinkedWorkItemPromptContext } from '@/lib/linked-work-item-context'
|
||||
import type { PendingSmartGitHubSubmitResolution } from './source-selection-decisions'
|
||||
|
||||
export function useFullSubmitSourcePreparation(input: FullSubmitSourcePreparationInput) {
|
||||
const {
|
||||
agentPrompt,
|
||||
attachmentPaths,
|
||||
baseBranch,
|
||||
branchNameOverride,
|
||||
compareBaseRef,
|
||||
decisions,
|
||||
effectiveLinkedPR,
|
||||
enableIssueAutomation,
|
||||
fallbackCreatureName,
|
||||
hasLoadedIssueCommand,
|
||||
issueCommandTemplate,
|
||||
lastAutoNameRef,
|
||||
linkedGitLabMR,
|
||||
linkedWorkItem,
|
||||
name,
|
||||
parsedLinkedIssueNumber,
|
||||
pushTarget,
|
||||
workspaceSeedName
|
||||
} = input
|
||||
const { isExplicitWorkspaceNameInput, resolveSmartGitHubCreateNames } = decisions
|
||||
|
||||
const prepareFullSubmitSource = useCallback(
|
||||
(smartGitHubResolution: PendingSmartGitHubSubmitResolution) => {
|
||||
const submitLinkedWorkItem =
|
||||
smartGitHubResolution.kind === 'none'
|
||||
? linkedWorkItem
|
||||
: smartGitHubResolution.linkedWorkItem
|
||||
|
||||
const submitLinkedIssueNumber =
|
||||
smartGitHubResolution.kind === 'none'
|
||||
? parsedLinkedIssueNumber
|
||||
: smartGitHubResolution.linkedIssueNumber
|
||||
|
||||
const submitLinkedPR =
|
||||
smartGitHubResolution.kind === 'none' ? effectiveLinkedPR : smartGitHubResolution.linkedPR
|
||||
|
||||
const submitTitleName = submitLinkedWorkItem
|
||||
? getLinkedWorkItemWorkspaceName(submitLinkedWorkItem)
|
||||
: null
|
||||
|
||||
const nameIsAutoManaged = !isExplicitWorkspaceNameInput({
|
||||
name,
|
||||
lastAutoName: lastAutoNameRef.current
|
||||
})
|
||||
|
||||
const smartGitHubCreateNames =
|
||||
smartGitHubResolution.kind === 'none'
|
||||
? { workspaceName: workspaceSeedName, displayName: undefined }
|
||||
: resolveSmartGitHubCreateNames({
|
||||
resolutionKind: smartGitHubResolution.kind,
|
||||
smartWorkspaceName: smartGitHubResolution.workspaceName,
|
||||
smartDisplayName: smartGitHubResolution.displayName,
|
||||
fallbackWorkspaceName: workspaceSeedName,
|
||||
nameIsAutoManaged
|
||||
})
|
||||
|
||||
const workspaceName =
|
||||
smartGitHubResolution.kind === 'none'
|
||||
? nameIsAutoManaged && submitTitleName
|
||||
? submitTitleName.seedName
|
||||
: workspaceSeedName
|
||||
: smartGitHubCreateNames.workspaceName
|
||||
|
||||
if (!workspaceName) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Why: only a name Orca generated may be retired — the creature pool contains ordinary words
|
||||
// ("orca", "runner", "molly") a user can type deliberately and expect to reuse.
|
||||
// The identity check is what a linked PR/issue seed makes necessary here; mobile's blank-create
|
||||
// path (NewWorktreeModal, `nameWasGenerated: !trimmedName`) has no other seed, so it can't
|
||||
// share this expression. Same rule, two submit paths — change both together.
|
||||
const nameWasGenerated = !name.trim() && workspaceName === fallbackCreatureName
|
||||
|
||||
const submitBaseBranch =
|
||||
smartGitHubResolution.kind === 'pr-start-point'
|
||||
? smartGitHubResolution.baseBranch
|
||||
: smartGitHubResolution.kind === 'metadata-only' &&
|
||||
(effectiveLinkedPR !== null || linkedGitLabMR !== null)
|
||||
? undefined
|
||||
: baseBranch
|
||||
|
||||
const submitCompareBaseRef =
|
||||
smartGitHubResolution.kind === 'pr-start-point'
|
||||
? smartGitHubResolution.compareBaseRef
|
||||
: smartGitHubResolution.kind === 'none'
|
||||
? compareBaseRef
|
||||
: undefined
|
||||
|
||||
const submitPushTarget =
|
||||
smartGitHubResolution.kind === 'pr-start-point'
|
||||
? smartGitHubResolution.pushTarget
|
||||
: smartGitHubResolution.kind === 'none'
|
||||
? pushTarget
|
||||
: undefined
|
||||
|
||||
const submitBranchNameOverride =
|
||||
smartGitHubResolution.kind === 'pr-start-point'
|
||||
? smartGitHubResolution.branchNameOverride
|
||||
: smartGitHubResolution.kind === 'none'
|
||||
? branchNameOverride
|
||||
: undefined
|
||||
|
||||
const submitLinkedWorkItemProvider = submitLinkedWorkItem
|
||||
? getLinkedWorkItemProvider(submitLinkedWorkItem)
|
||||
: null
|
||||
|
||||
const submitShouldApplyLinkedOnlyTemplate =
|
||||
enableIssueAutomation &&
|
||||
!agentPrompt.trim() &&
|
||||
Boolean(submitLinkedWorkItem) &&
|
||||
hasLoadedIssueCommand &&
|
||||
canUseIssueCommandForLinkedItemProvider(submitLinkedWorkItemProvider)
|
||||
|
||||
const submitLinkedOnlyTemplatePrompt =
|
||||
submitShouldApplyLinkedOnlyTemplate && submitLinkedWorkItem
|
||||
? renderIssueCommandTemplate(
|
||||
issueCommandTemplate.trim() || DEFAULT_ISSUE_COMMAND_TEMPLATE,
|
||||
{
|
||||
issueNumber:
|
||||
submitLinkedWorkItem.type === 'issue' ? submitLinkedWorkItem.number : null,
|
||||
artifactUrl: submitLinkedWorkItem.url
|
||||
}
|
||||
)
|
||||
: ''
|
||||
|
||||
const linkedPromptContext = getLinkedWorkItemPromptContext(submitLinkedWorkItem)
|
||||
|
||||
const submitStartupPrompt = submitShouldApplyLinkedOnlyTemplate
|
||||
? buildAgentPromptWithContext(
|
||||
submitLinkedOnlyTemplatePrompt,
|
||||
attachmentPaths,
|
||||
[],
|
||||
linkedPromptContext.linkedContextBlocks
|
||||
)
|
||||
: buildAgentPromptWithContext(
|
||||
agentPrompt,
|
||||
attachmentPaths,
|
||||
linkedPromptContext.linkedUrls,
|
||||
linkedPromptContext.linkedContextBlocks
|
||||
)
|
||||
|
||||
const submitShouldRunIssueAutomation =
|
||||
enableIssueAutomation &&
|
||||
canUseIssueCommandForLinkedItemProvider(submitLinkedWorkItemProvider) &&
|
||||
submitLinkedIssueNumber !== null &&
|
||||
issueCommandTemplate.length > 0 &&
|
||||
!submitShouldApplyLinkedOnlyTemplate
|
||||
|
||||
return {
|
||||
submitLinkedWorkItem,
|
||||
submitLinkedIssueNumber,
|
||||
submitLinkedPR,
|
||||
submitTitleName,
|
||||
nameIsAutoManaged,
|
||||
smartGitHubCreateNames,
|
||||
workspaceName,
|
||||
nameWasGenerated,
|
||||
submitBaseBranch,
|
||||
submitCompareBaseRef,
|
||||
submitPushTarget,
|
||||
submitBranchNameOverride,
|
||||
submitLinkedWorkItemProvider,
|
||||
submitStartupPrompt,
|
||||
submitShouldRunIssueAutomation
|
||||
}
|
||||
},
|
||||
[
|
||||
agentPrompt,
|
||||
attachmentPaths,
|
||||
baseBranch,
|
||||
branchNameOverride,
|
||||
effectiveLinkedPR,
|
||||
enableIssueAutomation,
|
||||
fallbackCreatureName,
|
||||
hasLoadedIssueCommand,
|
||||
issueCommandTemplate,
|
||||
linkedGitLabMR,
|
||||
linkedWorkItem,
|
||||
isExplicitWorkspaceNameInput,
|
||||
name,
|
||||
parsedLinkedIssueNumber,
|
||||
pushTarget,
|
||||
resolveSmartGitHubCreateNames,
|
||||
workspaceSeedName,
|
||||
compareBaseRef,
|
||||
lastAutoNameRef
|
||||
]
|
||||
)
|
||||
|
||||
return {
|
||||
prepareFullSubmitSource
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,7 @@ type HostRuntimeEffectsInput = Pick<
|
||||
ComposerModel,
|
||||
| 'commitHookCheckIfCurrent'
|
||||
| 'connectionId'
|
||||
| 'createGateMode'
|
||||
| 'disabledTuiAgents'
|
||||
| 'enableIssueAutomation'
|
||||
| 'ensureDetectedAgents'
|
||||
| 'ensureRemoteDetectedAgents'
|
||||
| 'ensureRuntimeDetectedAgents'
|
||||
@@ -19,12 +17,9 @@ type HostRuntimeEffectsInput = Pick<
|
||||
| 'repoIdRef'
|
||||
| 'runtimeEnvironmentId'
|
||||
| 'selectedRepoConnectionIdRef'
|
||||
| 'selectedRepoExecutionHostId'
|
||||
| 'selectedRepoHookContextKey'
|
||||
| 'selectedRepoIsGit'
|
||||
| 'selectedRepoSettingsRef'
|
||||
| 'selectedRepoSshStatus'
|
||||
| 'setLoadedIssueCommand'
|
||||
| 'setTuiAgent'
|
||||
| 'settings'
|
||||
| 'tuiAgent'
|
||||
@@ -33,7 +28,6 @@ type HostRuntimeEffectsInput = Pick<
|
||||
import { useEffect, useCallback } from 'react'
|
||||
import { filterEnabledTuiAgents, isTuiAgentEnabled } from '../../../../shared/tui-agent-selection'
|
||||
import { getAgentCatalog } from '@/lib/agent-catalog'
|
||||
import { readRuntimeIssueCommand } from '@/runtime/runtime-hooks-client'
|
||||
import { useAppStore } from '@/store'
|
||||
import { isSshConnectInProgress } from '@/lib/new-workspace-ssh-gate'
|
||||
import { toast } from 'sonner'
|
||||
@@ -43,9 +37,7 @@ export function useHostRuntimeEffects(input: HostRuntimeEffectsInput) {
|
||||
const {
|
||||
commitHookCheckIfCurrent,
|
||||
connectionId,
|
||||
createGateMode,
|
||||
disabledTuiAgents,
|
||||
enableIssueAutomation,
|
||||
ensureDetectedAgents,
|
||||
ensureRemoteDetectedAgents,
|
||||
ensureRuntimeDetectedAgents,
|
||||
@@ -58,12 +50,9 @@ export function useHostRuntimeEffects(input: HostRuntimeEffectsInput) {
|
||||
repoIdRef,
|
||||
runtimeEnvironmentId,
|
||||
selectedRepoConnectionIdRef,
|
||||
selectedRepoExecutionHostId,
|
||||
selectedRepoHookContextKey,
|
||||
selectedRepoIsGit,
|
||||
selectedRepoSettingsRef,
|
||||
selectedRepoSshStatus,
|
||||
setLoadedIssueCommand,
|
||||
setTuiAgent,
|
||||
settings,
|
||||
tuiAgent
|
||||
@@ -109,7 +98,7 @@ export function useHostRuntimeEffects(input: HostRuntimeEffectsInput) {
|
||||
setTuiAgent
|
||||
])
|
||||
|
||||
// Per-repo: load yaml hooks + issue command template.
|
||||
// Per-repo: load yaml hooks.
|
||||
useEffect(() => {
|
||||
if (!repoId || !selectedRepoIsGit || !selectedRepoHookContextKey) {
|
||||
return
|
||||
@@ -129,59 +118,16 @@ export function useHostRuntimeEffects(input: HostRuntimeEffectsInput) {
|
||||
}
|
||||
})
|
||||
|
||||
if (!enableIssueAutomation) {
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}
|
||||
|
||||
if (createGateMode === 'quick') {
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}
|
||||
|
||||
void readRuntimeIssueCommand(
|
||||
selectedRepoSettingsRef.current,
|
||||
repoId,
|
||||
selectedRepoExecutionHostId ?? undefined
|
||||
)
|
||||
.then((result) => {
|
||||
if (!cancelled) {
|
||||
setLoadedIssueCommand({ contextKey: selectedRepoHookContextKey, result })
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
setLoadedIssueCommand({
|
||||
contextKey: selectedRepoHookContextKey,
|
||||
result: {
|
||||
status: 'error',
|
||||
localContent: null,
|
||||
sharedContent: null,
|
||||
effectiveContent: null,
|
||||
localFilePath: '',
|
||||
source: 'none'
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [
|
||||
commitHookCheckIfCurrent,
|
||||
createGateMode,
|
||||
enableIssueAutomation,
|
||||
loadHookCheckForRepo,
|
||||
repoId,
|
||||
selectedRepoExecutionHostId,
|
||||
selectedRepoHookContextKey,
|
||||
selectedRepoIsGit,
|
||||
runtimeEnvironmentId,
|
||||
selectedRepoSettingsRef,
|
||||
setLoadedIssueCommand
|
||||
runtimeEnvironmentId
|
||||
])
|
||||
|
||||
const onConnectSelectedRepo = useCallback(async (): Promise<void> => {
|
||||
|
||||
@@ -96,7 +96,11 @@ export function useQuickSubmitSourcePreparation(input: QuickSubmitSourcePreparat
|
||||
return null
|
||||
}
|
||||
|
||||
// Why: only a name Orca generated may be retired — see the full-composer submit path.
|
||||
// Why: only a name Orca generated may be retired — the creature pool contains ordinary words
|
||||
// ("orca", "runner", "molly") a user can type deliberately and expect to reuse.
|
||||
// The identity check is what a linked PR/issue seed makes necessary here; mobile's blank-create
|
||||
// path (use-new-workspace-create-submit.ts, `nameWasGenerated: !trimmedName`) has no other seed,
|
||||
// so it can't share this expression. Same rule, two submit paths — change both together.
|
||||
const nameWasGenerated = !name.trim() && workspaceName === fallbackCreatureName
|
||||
|
||||
const smartSubmitBaseBranch =
|
||||
|
||||
@@ -39,17 +39,13 @@ export type ComposerTargetStoreModel = {
|
||||
onRepoIdOverrideChange: ((value: string) => void) | undefined
|
||||
telemetrySource: WorkspaceCreateTelemetrySource | undefined
|
||||
enableIssueAutomation: boolean
|
||||
createGateMode: 'full' | 'quick'
|
||||
initialProjectGroupId: string | undefined
|
||||
decisions: ComposerDecisions
|
||||
actions: ComposerStoreActions
|
||||
setNewWorkspaceDraft: ComposerStoreActions['setNewWorkspaceDraft']
|
||||
clearNewWorkspaceDraft: ComposerStoreActions['clearNewWorkspaceDraft']
|
||||
createWorktree: ComposerStoreActions['createWorktree']
|
||||
updateRepo: ComposerStoreActions['updateRepo']
|
||||
updateWorktreeMeta: ComposerStoreActions['updateWorktreeMeta']
|
||||
createFolderWorkspace: ComposerStoreActions['createFolderWorkspace']
|
||||
setSidebarOpen: ComposerStoreActions['setSidebarOpen']
|
||||
closeModal: ComposerStoreActions['closeModal']
|
||||
openSettingsPage: ComposerStoreActions['openSettingsPage']
|
||||
openSettingsTarget: ComposerStoreActions['openSettingsTarget']
|
||||
|
||||
@@ -48,11 +48,9 @@ describe('useComposerState integrated lifecycle', () => {
|
||||
return unsubscribe
|
||||
})
|
||||
|
||||
const first = renderHook(() =>
|
||||
useComposerState({ initialName: 'first', persistDraft: false, createGateMode: 'quick' })
|
||||
)
|
||||
const first = renderHook(() => useComposerState({ initialName: 'first', persistDraft: false }))
|
||||
const second = renderHook(() =>
|
||||
useComposerState({ initialName: 'second', persistDraft: false, createGateMode: 'quick' })
|
||||
useComposerState({ initialName: 'second', persistDraft: false })
|
||||
)
|
||||
|
||||
expect(first.result.current.cardProps.name).toBe('first')
|
||||
|
||||
@@ -47,7 +47,6 @@ export type UseComposerStateOptions = {
|
||||
onRepoIdOverrideChange?: (value: string) => void
|
||||
telemetrySource?: WorkspaceCreateTelemetrySource
|
||||
enableIssueAutomation?: boolean
|
||||
createGateMode?: 'full' | 'quick'
|
||||
}
|
||||
|
||||
export type ComposerCardProps = ComposerCardSourceProps & ComposerCardActionProps
|
||||
@@ -58,7 +57,6 @@ export type UseComposerStateResult = {
|
||||
onComposerNodeChange: (node: HTMLDivElement | null) => void
|
||||
promptTextareaRef: RefObject<HTMLTextAreaElement | null>
|
||||
nameInputRef: RefObject<HTMLInputElement | null>
|
||||
submit: () => Promise<void>
|
||||
submitQuick: (agent: TuiAgent | null) => Promise<void>
|
||||
createDisabled: boolean
|
||||
selectAddedProjectRepo: (repoId: string) => void
|
||||
@@ -221,7 +219,6 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
|
||||
onComposerNodeChange: model.handleComposerNodeChange,
|
||||
promptTextareaRef: model.promptTextareaRef,
|
||||
nameInputRef: model.nameInputRef,
|
||||
submit: model.submit,
|
||||
submitQuick: model.submitQuick,
|
||||
createDisabled,
|
||||
selectAddedProjectRepo: model.selectAddedProjectRepo
|
||||
|
||||
@@ -1166,7 +1166,6 @@
|
||||
"workspaceHostUnresolved": "The target workspace spans more than one host, so this run has no single host to use."
|
||||
},
|
||||
"useComposerState": {
|
||||
"7eb3f44ff7": "Selected agent is disabled. Choose an enabled agent before creating.",
|
||||
"b2ead86962": "Failed to resolve PR base.",
|
||||
"3db83fc58a": "No project path is available on this host for attachments.",
|
||||
"ba6cb77082": "Failed to connect to project.",
|
||||
|
||||
@@ -993,7 +993,6 @@
|
||||
"workspaceHostUnresolved": "El espacio de trabajo de destino abarca más de un host, así que esta ejecución no tiene un único host que usar."
|
||||
},
|
||||
"useComposerState": {
|
||||
"7eb3f44ff7": "El agente seleccionado está deshabilitado. Elige un agente habilitado antes de crear.",
|
||||
"b2ead86962": "No se pudo resolver la base del PR.",
|
||||
"3db83fc58a": "No hay ninguna ruta de proyecto remoto disponible para los archivos adjuntos.",
|
||||
"ba6cb77082": "No se pudo conectar al proyecto.",
|
||||
|
||||
@@ -1094,7 +1094,6 @@
|
||||
"workspaceHostUnresolved": "L'espace de travail cible s'étend sur plusieurs hôtes, cette exécution n'a donc aucun hôte unique à utiliser."
|
||||
},
|
||||
"useComposerState": {
|
||||
"7eb3f44ff7": "L'agent sélectionné est désactivé. Choisissez un agent activé avant de créer.",
|
||||
"b2ead86962": "Échec de la résolution de la base de la PR.",
|
||||
"3db83fc58a": "Aucun chemin de projet n'est disponible sur cet hôte pour les pièces jointes.",
|
||||
"ba6cb77082": "Échec de la connexion au projet.",
|
||||
|
||||
@@ -993,7 +993,6 @@
|
||||
"workspaceHostUnresolved": "ターゲットワークスペースは複数のホストにまたがっているため、この実行では使用する単一のホストがありません。"
|
||||
},
|
||||
"useComposerState": {
|
||||
"7eb3f44ff7": "選択した Agent は無効です。作成する前に、有効な Agent を選択してください。",
|
||||
"b2ead86962": "PR ベースを解決できませんでした。",
|
||||
"3db83fc58a": "このホスト上に、添付に使用できるプロジェクトパスがありません。",
|
||||
"ba6cb77082": "プロジェクトへの接続に失敗しました。",
|
||||
|
||||
@@ -993,7 +993,6 @@
|
||||
"workspaceHostUnresolved": "대상 워크스페이스는 두 개 이상의 호스트에 걸쳐 있으므로 이 실행에는 사용할 단일 호스트가 없습니다."
|
||||
},
|
||||
"useComposerState": {
|
||||
"7eb3f44ff7": "선택한 agent가 비활성화되었습니다. 생성하기 전에 활성화된 agent를 선택하세요.",
|
||||
"b2ead86962": "PR 기반을 해결하지 못했습니다.",
|
||||
"3db83fc58a": "이 호스트에 첨부 파일에 사용할 수 있는 프로젝트 경로가 없습니다.",
|
||||
"ba6cb77082": "프로젝트에 연결하지 못했습니다.",
|
||||
|
||||
@@ -993,7 +993,6 @@
|
||||
"workspaceHostUnresolved": "目标工作区跨越多个主机,因此此运行没有可供使用的单个主机。"
|
||||
},
|
||||
"useComposerState": {
|
||||
"7eb3f44ff7": "所选智能体已禁用。创建之前选择启用的智能体。",
|
||||
"b2ead86962": "无法解析 PR 基础引用。",
|
||||
"3db83fc58a": "没有可用于附件的远程项目路径。",
|
||||
"ba6cb77082": "无法连接到项目。",
|
||||
|
||||
@@ -193,7 +193,7 @@ export type ConfirmedRuntimeIssueCommand = {
|
||||
trustDecision: 'run' | 'skip'
|
||||
}
|
||||
|
||||
export function confirmRuntimeIssueCommandRead(
|
||||
function confirmRuntimeIssueCommandRead(
|
||||
state: AppState,
|
||||
repoId: string,
|
||||
hostId: ExecutionHostId,
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildAgentPromptWithContext } from './new-workspace'
|
||||
import {
|
||||
buildContainedLinkedContextBlock,
|
||||
buildLinearLaunchContextBlock,
|
||||
getLaunchableWorkItemDraftContent,
|
||||
getLinkedWorkItemPromptContext,
|
||||
LINKED_CONTEXT_BLOCK_MAX_CHARS,
|
||||
resolveQuickCreateLinkedWorkItemPrompt
|
||||
} from './linked-work-item-context'
|
||||
@@ -152,34 +150,6 @@ describe('buildLinearLaunchContextBlock', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('getLinkedWorkItemPromptContext', () => {
|
||||
it('returns a link-only Linear reference for Linear items', () => {
|
||||
const result = getLinkedWorkItemPromptContext(LINEAR_ITEM)
|
||||
|
||||
expect(result.linkedUrls).toEqual([])
|
||||
expect(result.linkedContextBlocks).toEqual([
|
||||
'Linked Linear issue: ENG-123\nhttps://linear.app/acme/issue/ENG-123/test'
|
||||
])
|
||||
expectNoLinearTicketContent(result.linkedContextBlocks[0])
|
||||
expectNoProductWorkflowDirection(result.linkedContextBlocks[0])
|
||||
})
|
||||
|
||||
it('falls back to the URL for non-Linear items', () => {
|
||||
expect(
|
||||
getLinkedWorkItemPromptContext({
|
||||
url: 'https://gitlab.example.com/group/project/-/issues/1'
|
||||
})
|
||||
).toEqual({
|
||||
linkedUrls: ['https://gitlab.example.com/group/project/-/issues/1'],
|
||||
linkedContextBlocks: []
|
||||
})
|
||||
expect(getLinkedWorkItemPromptContext(null)).toEqual({
|
||||
linkedUrls: [],
|
||||
linkedContextBlocks: []
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveQuickCreateLinkedWorkItemPrompt', () => {
|
||||
it('drafts the note above the link-only Linear reference', () => {
|
||||
const result = resolveQuickCreateLinkedWorkItemPrompt(
|
||||
@@ -277,34 +247,3 @@ describe('getLaunchableWorkItemDraftContent', () => {
|
||||
).toBe('Linked Linear issue\nhttps://linear.app/acme/issue/ENG-123/test\n')
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildAgentPromptWithContext', () => {
|
||||
it('appends link-only Linear references alongside prompt attachments', () => {
|
||||
const linearBlock = buildLinearLaunchContextBlock({
|
||||
provider: 'linear',
|
||||
identifier: 'ENG-123',
|
||||
url: LINEAR_ITEM.url
|
||||
})
|
||||
|
||||
const prompt = buildAgentPromptWithContext(
|
||||
'Fix this',
|
||||
['/tmp/report.txt'],
|
||||
[],
|
||||
linearBlock ? [linearBlock] : []
|
||||
)
|
||||
|
||||
expect(prompt).toContain(
|
||||
[
|
||||
'Fix this',
|
||||
'',
|
||||
'Attachments:',
|
||||
'- /tmp/report.txt',
|
||||
'',
|
||||
'Linked Linear issue: ENG-123',
|
||||
'https://linear.app/acme/issue/ENG-123/test'
|
||||
].join('\n')
|
||||
)
|
||||
expectNoLinearTicketContent(prompt)
|
||||
expectNoProductWorkflowDirection(prompt)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -150,32 +150,6 @@ function capLinkedContextSourceLines(args: { sourceLines: string; fixedChars: nu
|
||||
return [capped, truncationLine].filter(Boolean).join('\n')
|
||||
}
|
||||
|
||||
export function getLinkedWorkItemPromptContext(
|
||||
linkedWorkItem:
|
||||
| (Pick<
|
||||
{ provider?: TaskProvider; url: string; title?: string; linearIdentifier?: string },
|
||||
'provider' | 'url' | 'title' | 'linearIdentifier'
|
||||
> & { linkedContext?: LinkedWorkItemContext })
|
||||
| null
|
||||
| undefined
|
||||
): { linkedUrls: string[]; linkedContextBlocks: string[] } {
|
||||
if (isLinearWorkItemReference(linkedWorkItem)) {
|
||||
const linearBlock = buildLinearLaunchContextBlock({
|
||||
provider: linkedWorkItem?.provider,
|
||||
identifier: linkedWorkItem?.linearIdentifier,
|
||||
title: linkedWorkItem?.title,
|
||||
url: linkedWorkItem?.url
|
||||
})
|
||||
return linearBlock
|
||||
? { linkedUrls: [], linkedContextBlocks: [linearBlock] }
|
||||
: { linkedUrls: [], linkedContextBlocks: [] }
|
||||
}
|
||||
const linkedUrl = linkedWorkItem?.url?.trim()
|
||||
return linkedUrl
|
||||
? { linkedUrls: [linkedUrl], linkedContextBlocks: [] }
|
||||
: { linkedUrls: [], linkedContextBlocks: [] }
|
||||
}
|
||||
|
||||
export function getLaunchableWorkItemDraftContent(args: {
|
||||
provider?: TaskProvider
|
||||
pasteContent?: string
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
getFullComposerCreateDisabled,
|
||||
getQuickComposerCreateDisabled,
|
||||
type ComposerCreateGateInput
|
||||
} from './new-workspace-create-gates'
|
||||
@@ -9,8 +8,6 @@ const readyInput: ComposerCreateGateInput = {
|
||||
repoId: 'repo-1',
|
||||
workspaceSeedName: 'feature',
|
||||
creating: false,
|
||||
shouldWaitForSetupCheck: false,
|
||||
shouldWaitForIssueAutomationCheck: false,
|
||||
requiresExplicitSetupChoice: false,
|
||||
hasSetupDecision: false,
|
||||
selectedRepoRequiresConnection: false,
|
||||
@@ -18,32 +15,6 @@ const readyInput: ComposerCreateGateInput = {
|
||||
}
|
||||
|
||||
describe('new workspace create gates', () => {
|
||||
it('keeps the full composer disabled while setup and issue automation probes are pending', () => {
|
||||
expect(
|
||||
getFullComposerCreateDisabled({
|
||||
...readyInput,
|
||||
shouldWaitForSetupCheck: true
|
||||
})
|
||||
).toBe(true)
|
||||
|
||||
expect(
|
||||
getFullComposerCreateDisabled({
|
||||
...readyInput,
|
||||
shouldWaitForIssueAutomationCheck: true
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('lets quick create submit while background setup and issue probes are pending', () => {
|
||||
expect(
|
||||
getQuickComposerCreateDisabled({
|
||||
...readyInput,
|
||||
shouldWaitForSetupCheck: true,
|
||||
shouldWaitForIssueAutomationCheck: true
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps quick create clickable when no repo is selected so submit can validate inline', () => {
|
||||
expect(getQuickComposerCreateDisabled({ ...readyInput, repoId: '' })).toBe(false)
|
||||
})
|
||||
@@ -66,10 +37,9 @@ describe('new workspace create gates', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('blocks full and quick create synchronously for unresolved source intent', () => {
|
||||
it('blocks quick create synchronously for unresolved source intent', () => {
|
||||
const blocked = { ...readyInput, sourceIntentBlocksCreate: true }
|
||||
|
||||
expect(getFullComposerCreateDisabled(blocked)).toBe(true)
|
||||
expect(getQuickComposerCreateDisabled(blocked)).toBe(true)
|
||||
expect(getQuickComposerCreateDisabled({ ...blocked, sourceIntentBlocksCreate: false })).toBe(
|
||||
false
|
||||
|
||||
@@ -2,8 +2,6 @@ export type ComposerCreateGateInput = {
|
||||
repoId: string
|
||||
workspaceSeedName: string
|
||||
creating: boolean
|
||||
shouldWaitForSetupCheck: boolean
|
||||
shouldWaitForIssueAutomationCheck: boolean
|
||||
sourceIntentBlocksCreate?: boolean
|
||||
requiresExplicitSetupChoice: boolean
|
||||
hasSetupDecision: boolean
|
||||
@@ -22,14 +20,6 @@ function hasBlockingCreateState(input: ComposerCreateGateInput): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
export function getFullComposerCreateDisabled(input: ComposerCreateGateInput): boolean {
|
||||
return (
|
||||
hasBlockingCreateState(input) ||
|
||||
input.shouldWaitForSetupCheck ||
|
||||
input.shouldWaitForIssueAutomationCheck
|
||||
)
|
||||
}
|
||||
|
||||
export function getQuickComposerCreateDisabled(input: ComposerCreateGateInput): boolean {
|
||||
// Why: quick create resolves setup hooks and optional issue automation inside submit. Keeping those
|
||||
// background probes out of the disabled gate makes the primary action usable
|
||||
|
||||
@@ -105,38 +105,6 @@ export function renderIssueCommandTemplate(
|
||||
return rendered
|
||||
}
|
||||
|
||||
export function buildAgentPromptWithContext(
|
||||
prompt: string,
|
||||
attachments: string[],
|
||||
linkedUrls: string[],
|
||||
linkedContextBlocks: string[] = []
|
||||
): string {
|
||||
const trimmedPrompt = prompt.trim()
|
||||
if (attachments.length === 0 && linkedUrls.length === 0 && linkedContextBlocks.length === 0) {
|
||||
return trimmedPrompt
|
||||
}
|
||||
|
||||
const sections: string[] = []
|
||||
if (attachments.length > 0) {
|
||||
const attachmentBlock = attachments.map((pathValue) => `- ${pathValue}`).join('\n')
|
||||
sections.push(`Attachments:\n${attachmentBlock}`)
|
||||
}
|
||||
if (linkedUrls.length > 0) {
|
||||
const linkBlock = linkedUrls.map((url) => `- ${url}`).join('\n')
|
||||
sections.push(`Linked work items:\n${linkBlock}`)
|
||||
}
|
||||
if (linkedContextBlocks.length > 0) {
|
||||
sections.push(linkedContextBlocks.join('\n\n'))
|
||||
}
|
||||
// Why: the new-workspace flow launches each agent with a single plain-text
|
||||
// startup prompt. Appending attachments and bounded linked context keeps
|
||||
// extra data visible to Claude/Codex/OpenCode without cluttering the textarea.
|
||||
if (!trimmedPrompt) {
|
||||
return sections.join('\n\n')
|
||||
}
|
||||
return `${trimmedPrompt}\n\n${sections.join('\n\n')}`
|
||||
}
|
||||
|
||||
export function getAttachmentLabel(pathValue: string): string {
|
||||
const segments = pathValue.split(/[/\\]/)
|
||||
return segments.at(-1) || pathValue
|
||||
|
||||
@@ -94,9 +94,9 @@ test.describe('Create Workspace', () => {
|
||||
await expect(nameInput).toBeVisible()
|
||||
await nameInput.fill(workspaceName)
|
||||
|
||||
// 4. Click Create. This fires the full submitQuick path:
|
||||
// createWorktree IPC, applyWorktreeMeta, activateAndRevealWorktree,
|
||||
// and closeModal via onCreated.
|
||||
// 4. Click Create. submitQuick starts the background creation
|
||||
// (createWorktree IPC, then activateAndRevealWorktree) and closes the
|
||||
// modal via onCreated.
|
||||
const createButton = dialog.getByRole('button', { name: /Create (Workspace|Worktree)/i })
|
||||
await expect(createButton).toBeEnabled()
|
||||
await createButton.click()
|
||||
|
||||
Reference in New Issue
Block a user