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:
Brennan Benson
2026-10-07 01:26:38 -07:00
committed by GitHub
parent e6c298e8a9
commit 535c83a687
46 changed files with 21 additions and 1765 deletions
@@ -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
-1
View File
@@ -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.",
-1
View File
@@ -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.",
-1
View File
@@ -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.",
-1
View File
@@ -993,7 +993,6 @@
"workspaceHostUnresolved": "ターゲットワークスペースは複数のホストにまたがっているため、この実行では使用する単一のホストがありません。"
},
"useComposerState": {
"7eb3f44ff7": "選択した Agent は無効です。作成する前に、有効な Agent を選択してください。",
"b2ead86962": "PR ベースを解決できませんでした。",
"3db83fc58a": "このホスト上に、添付に使用できるプロジェクトパスがありません。",
"ba6cb77082": "プロジェクトへの接続に失敗しました。",
-1
View File
@@ -993,7 +993,6 @@
"workspaceHostUnresolved": "대상 워크스페이스는 두 개 이상의 호스트에 걸쳐 있으므로 이 실행에는 사용할 단일 호스트가 없습니다."
},
"useComposerState": {
"7eb3f44ff7": "선택한 agent가 비활성화되었습니다. 생성하기 전에 활성화된 agent를 선택하세요.",
"b2ead86962": "PR 기반을 해결하지 못했습니다.",
"3db83fc58a": "이 호스트에 첨부 파일에 사용할 수 있는 프로젝트 경로가 없습니다.",
"ba6cb77082": "프로젝트에 연결하지 못했습니다.",
-1
View File
@@ -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
-32
View File
@@ -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
+3 -3
View File
@@ -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()