diff --git a/mobile/src/tasks/composer-linked-work-item.ts b/mobile/src/tasks/composer-linked-work-item.ts index 13d5c845b37..8a3621be8f8 100644 --- a/mobile/src/tasks/composer-linked-work-item.ts +++ b/mobile/src/tasks/composer-linked-work-item.ts @@ -50,7 +50,11 @@ export function buildLinearLinkedWorkItem(issue: { // Faithful port of desktop applyLinkedWorkItem's name gate: the derived name // replaces the current field only when it's empty, still the last auto-name, or // a lookup query — never a name the user deliberately typed. -export function shouldApplyAutoName(args: { currentName: string; lastAutoName: string }): boolean { +export function shouldApplyAutoName(args: { + currentName: string + lastAutoName: string + lookupTextIsQuery?: boolean +}): boolean { return shouldApplyWorkspaceSourceAutoName(args) } diff --git a/mobile/src/tasks/use-mobile-composer-name-ownership.test.ts b/mobile/src/tasks/use-mobile-composer-name-ownership.test.ts new file mode 100644 index 00000000000..270a0439eba --- /dev/null +++ b/mobile/src/tasks/use-mobile-composer-name-ownership.test.ts @@ -0,0 +1,123 @@ +import { createElement } from 'react' +import { act, create, type ReactTestRenderer } from 'react-test-renderer' +import { afterEach, describe, expect, it } from 'vitest' +import type { GitHubWorkItem } from '../../../src/shared/github/work-item-types' +import type { LinearIssue } from '../../../src/shared/linear/issue-types' +import { useMobileComposerSource } from './use-mobile-composer-source' +import { buildTaskWorkspaceCreateParams } from './workspace-create-params' + +const mounted: ReactTestRenderer[] = [] +afterEach(() => { + act(() => { + for (const renderer of mounted.splice(0)) { + renderer.unmount() + } + }) +}) + +function renderMobileComposerSource() { + const holder: { value?: ReturnType } = {} + function Probe() { + holder.value = useMobileComposerSource({ client: null, selectedRepoId: 'fixture-repo' }) + return null + } + act(() => { + mounted.push(create(createElement(Probe))) + }) + return { + get current() { + if (!holder.value) { + throw new Error('Composer source did not render') + } + return holder.value + } + } +} + +function issue(number: number): GitHubWorkItem { + return { + id: `issue-${number}`, + type: 'issue', + number, + title: 'Fix export', + state: 'open', + url: `https://github.com/fixture/repo/issues/${number}`, + repoId: 'fixture-repo', + labels: [], + updatedAt: '', + author: null + } +} + +describe('mobile workspace name ownership', () => { + const linearIssue: LinearIssue = { + id: 'linear-9', + identifier: 'ENG-9', + title: 'Fix export', + url: 'https://linear.app/fixture/issue/ENG-9', + state: { name: 'Open', type: 'unstarted', color: '' }, + team: { id: 'engineering', name: 'Engineering', key: 'ENG' }, + labels: [], + labelIds: [], + priority: 0, + updatedAt: '' + } + + it('keeps a manual Linear identifier name when its source refreshes', () => { + const result = renderMobileComposerSource() + act(() => result.current.handleSmartLinearIssueSelect(linearIssue)) + act(() => result.current.setName('ENG-9')) + act(() => + result.current.handleSmartLinearIssueSelect({ ...linearIssue, title: 'Fix refreshed export' }) + ) + + expect(result.current.name).toBe('ENG-9') + expect(result.current.isNameAutoManaged).toBe(false) + }) + + it('still replaces an unselected Linear identifier query', () => { + const result = renderMobileComposerSource() + act(() => result.current.setName('ENG-9')) + act(() => result.current.handleSmartLinearIssueSelect(linearIssue)) + + expect(result.current.name).toBe('eng-9-fix-export') + expect(result.current.isNameAutoManaged).toBe(true) + }) + + it.each([2, 347])('keeps an edited name when issue #%s replaces the source', (number) => { + const result = renderMobileComposerSource() + act(() => result.current.handleSmartGitHubItemSelect(issue(2))) + act(() => result.current.setName('002')) + act(() => result.current.handleSmartGitHubItemSelect(issue(number))) + + expect(result.current.name).toBe('002') + expect(result.current.isNameAutoManaged).toBe(false) + expect( + buildTaskWorkspaceCreateParams({ + item: { + provider: 'github', + source: { ...issue(number), type: 'issue', repoId: 'fixture-repo' } + }, + targetRepoId: 'fixture-repo', + setupDecision: 'skip', + agent: 'blank', + workspaceName: result.current.name, + nameIsAutoManaged: result.current.isNameAutoManaged + }) + ).toMatchObject({ + name: '002', + displayName: '002', + displayNameKind: 'user', + linkedIssue: number + }) + }) + + it.each(['002', '347', '#347'])('still generates a name when picking query %s', (query) => { + const result = renderMobileComposerSource() + act(() => result.current.setName(query)) + act(() => result.current.handleSmartGitHubItemSelect(issue(query === '002' ? 2 : 347))) + + expect(result.current.name).toBe('fix-export') + expect(result.current.isNameAutoManaged).toBe(true) + }) +}) diff --git a/mobile/src/tasks/use-mobile-composer-name.ts b/mobile/src/tasks/use-mobile-composer-name.ts new file mode 100644 index 00000000000..26dfe4872e4 --- /dev/null +++ b/mobile/src/tasks/use-mobile-composer-name.ts @@ -0,0 +1,27 @@ +import { useCallback, useRef, useState } from 'react' +import { shouldApplyAutoName } from './composer-linked-work-item' +import type { MobileLinkedWorkItem } from './mobile-composer-source-types' + +export function useMobileComposerName(linkedWorkItem: MobileLinkedWorkItem | null) { + const [name, setNameState] = useState('') + const lastAutoNameRef = useRef('') + const setName = useCallback((value: string) => setNameState(value), []) + const applyAutoName = useCallback( + (suggested: string, currentName: string) => { + if ( + suggested && + shouldApplyAutoName({ + currentName, + lastAutoName: lastAutoNameRef.current, + lookupTextIsQuery: !linkedWorkItem + }) + ) { + setNameState(suggested) + lastAutoNameRef.current = suggested + } + }, + [linkedWorkItem] + ) + const isNameAutoManaged = !name.trim() || name === lastAutoNameRef.current + return { name, setName, setNameState, lastAutoNameRef, applyAutoName, isNameAutoManaged } +} diff --git a/mobile/src/tasks/use-mobile-composer-source.ts b/mobile/src/tasks/use-mobile-composer-source.ts index 5d6c373d454..e3bb9c02518 100644 --- a/mobile/src/tasks/use-mobile-composer-source.ts +++ b/mobile/src/tasks/use-mobile-composer-source.ts @@ -28,6 +28,7 @@ import type { MobileLinkedWorkItem, SmartNameSelection } from './mobile-composer-source-types' +import { useMobileComposerName } from './use-mobile-composer-name' const EMPTY_BASE: ComposerBaseState = {} export type UseMobileComposerSourceArgs = { @@ -39,7 +40,6 @@ export type UseMobileComposerSourceArgs = { export function useMobileComposerSource(args: UseMobileComposerSourceArgs) { const { client, selectedRepoId, worktreeBranches = [], onError } = args - const [name, setNameState] = useState('') const [linkedWorkItem, setLinkedWorkItem] = useState(null) const [base, setBase] = useState(EMPTY_BASE) const [reuseEligibleBranch, setReuseEligibleBranch] = useState(null) @@ -50,19 +50,12 @@ export function useMobileComposerSource(args: UseMobileComposerSourceArgs) { // may contain slashes) is kept verbatim as the git branch (folder is sanitized). const [branchCreateIntent, setBranchCreateIntent] = useState(false) - const lastAutoNameRef = useRef('') const branchSelectionRef = useRef<{ refName: string; localBranchName: string } | null>(null) // Guards async base resolution: only the latest selection applies its result. const resolveTokenRef = useRef(0) - const setName = useCallback((value: string) => setNameState(value), []) - - const applyAutoName = useCallback((suggested: string, currentName: string) => { - if (suggested && shouldApplyAutoName({ currentName, lastAutoName: lastAutoNameRef.current })) { - setNameState(suggested) - lastAutoNameRef.current = suggested - } - }, []) + const { name, setName, setNameState, lastAutoNameRef, applyAutoName, isNameAutoManaged } = + useMobileComposerName(linkedWorkItem) const clearBaseAndBranch = useCallback(() => { branchSelectionRef.current = null @@ -201,18 +194,23 @@ export function useMobileComposerSource(args: UseMobileComposerSourceArgs) { resolveTokenRef.current += 1 setLinkedWorkItem(buildLinearLinkedWorkItem(issue)) const suggested = resolveLinearAutoName(issue) - const identifierTyped = name.trim().toLowerCase() === issue.identifier.toLowerCase() + const identifierTyped = + !linkedWorkItem && name.trim().toLowerCase() === issue.identifier.toLowerCase() if ( suggested && (identifierTyped || - shouldApplyAutoName({ currentName: name, lastAutoName: lastAutoNameRef.current })) + shouldApplyAutoName({ + currentName: name, + lastAutoName: lastAutoNameRef.current, + lookupTextIsQuery: !linkedWorkItem + })) ) { setNameState(suggested) lastAutoNameRef.current = suggested } clearBaseAndBranch() }, - [clearBaseAndBranch, name] + [clearBaseAndBranch, linkedWorkItem, name] ) const handleSmartBranchSelect = useCallback( @@ -302,10 +300,6 @@ export function useMobileComposerSource(args: UseMobileComposerSourceArgs) { [base, branchCreateIntent, linkedWorkItem, name, reuseEligibleBranch, reuseSelectedBranch] ) - // Auto-managed until the user edits the name away from the last derived value; - // desktop suppresses the workspace displayName once the name is user-edited. - const isNameAutoManaged = !name.trim() || name === lastAutoNameRef.current - return { name, setName, diff --git a/src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts b/src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts index ae4e8df209a..85d5f2020be 100644 --- a/src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts +++ b/src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts @@ -65,6 +65,36 @@ function makeFolderWorkspace(overrides: Partial = {}): FolderWo } describe('submitFolderWorkspaceCreate', () => { + it.each(['347', '002'])('keeps manual folder name %s with its linked source', async (name) => { + const linkedWorkItem = { + provider: 'github' as const, + type: 'issue' as const, + number: Number(name), + title: 'Fix export', + url: `https://github.com/fixture/repo/issues/${Number(name)}` + } + const createFolderWorkspace = vi.fn(async () => null) + await submitFolderWorkspaceCreate({ + projectGroup: makeProjectGroup(), + name, + lastAutoName: 'Fix export', + linkedWorkItem, + note: '', + quickAgent: null, + autoRenameBranchFromWork: false, + agentCmdOverrides: {}, + createFolderWorkspace, + onOpenChange: vi.fn() + }) + + expect(createFolderWorkspace).toHaveBeenCalledWith({ + projectGroupId: 'group-1', + name, + connectionId: null, + linkedTask: linkedWorkItem + }) + }) + beforeEach(() => { mocks.activateAndRevealFolderWorkspace.mockReturnValue({ primaryTabId: 'tab-1' }) }) diff --git a/src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts b/src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts index dc3afb246ff..0dde03495e3 100644 --- a/src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts +++ b/src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts @@ -4,7 +4,7 @@ import { createBrowserUuid } from '@/lib/browser-uuid' import { buildAgentStartupPlan } from '@/lib/tui-agent-startup' import { tuiAgentToAgentKind } from '@/lib/telemetry' import { activateAndRevealFolderWorkspace } from '@/lib/worktree-activation' -import { isWorkItemLookupText } from '@/lib/work-item-lookup-text' +import { shouldApplyWorkspaceSourceAutoName } from '../../../../shared/new-workspace/workspace-source' import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' import type { ProjectGroup } from '../../../../shared/project-group-types' import type { TuiAgent } from '../../../../shared/tui-agent' @@ -82,7 +82,11 @@ export async function submitFolderWorkspaceCreate({ onOpenChange }: SubmitFolderWorkspaceCreateParams): Promise { const linkedName = linkedWorkItem ? getLinkedItemDisplayName(linkedWorkItem) : null - const nameIsAutoManaged = !name.trim() || name === lastAutoName || isWorkItemLookupText(name) + const nameIsAutoManaged = shouldApplyWorkspaceSourceAutoName({ + currentName: name, + lastAutoName, + lookupTextIsQuery: !linkedWorkItem + }) const workspaceName = nameIsAutoManaged && linkedName ? linkedName diff --git a/src/renderer/src/hooks/composer-state/branch-start-point-actions.ts b/src/renderer/src/hooks/composer-state/branch-start-point-actions.ts index 6a5a7b0af8c..caa7f5e4706 100644 --- a/src/renderer/src/hooks/composer-state/branch-start-point-actions.ts +++ b/src/renderer/src/hooks/composer-state/branch-start-point-actions.ts @@ -135,7 +135,8 @@ export function useBranchStartPointActions(input: BranchStartPointActionsInput) item: GitHubWorkItem, nextPushTarget?: GitPushTarget, nextBranchNameOverride?: string, - nextCompareBaseRef?: string + nextCompareBaseRef?: string, + options: { sourceAlreadyLinked?: boolean } = {} ): void => { setBaseBranch(nextBaseBranch) setBaseBranchNamesWorkspace(true) @@ -146,7 +147,10 @@ export function useBranchStartPointActions(input: BranchStartPointActionsInput) branchAutoNameRef.current = '' setStartFromResetHint(null) // Why: a Start-from PR pick is also a linkedWorkItem assignment; reuse applyLinkedWorkItem so auto-name and linkedPR stay one code path. - applyLinkedWorkItem(item, { preserveBranchNameOverride: Boolean(nextBranchNameOverride) }) + // Provider picks already linked the source; a delayed base result must not replay its name. + if (!options.sourceAlreadyLinked) { + applyLinkedWorkItem(item, { preserveBranchNameOverride: Boolean(nextBranchNameOverride) }) + } // Why: prefill the note from the PR (only when empty or still an auto-fill) so the sidebar surfaces it without clobbering user text. const identity = resolveGitHubWorkItemIdentity(item) if (identity.type === 'pr') { @@ -180,7 +184,8 @@ export function useBranchStartPointActions(input: BranchStartPointActionsInput) nextBaseBranch: string, item: GitLabWorkItem, nextPushTarget?: GitPushTarget, - nextCompareBaseRef?: string + nextCompareBaseRef?: string, + options: { sourceAlreadyLinked?: boolean } = {} ): void => { setBaseBranch(nextBaseBranch) setBaseBranchNamesWorkspace(true) @@ -189,7 +194,9 @@ export function useBranchStartPointActions(input: BranchStartPointActionsInput) setBranchNameOverride(undefined) branchAutoNameRef.current = '' setStartFromResetHint(null) - applyLinkedGitLabWorkItem(item) + if (!options.sourceAlreadyLinked) { + applyLinkedGitLabWorkItem(item) + } if (item.type === 'mr') { const suggestedNote = `MR !${item.number} — ${item.title}` const currentNote = noteRef.current diff --git a/src/renderer/src/hooks/composer-state/composer-decisions.ts b/src/renderer/src/hooks/composer-state/composer-decisions.ts index ce852acc7c5..58651fe83b4 100644 --- a/src/renderer/src/hooks/composer-state/composer-decisions.ts +++ b/src/renderer/src/hooks/composer-state/composer-decisions.ts @@ -3,7 +3,7 @@ import { getLinkedWorkItemWorkspaceName } from '../../../../shared/workspace-name' import { normalizeExecutionHostId } from '../../../../shared/execution-host' -import { isWorkItemLookupText } from '@/lib/work-item-lookup-text' +import { shouldApplyWorkspaceSourceAutoName } from '../../../../shared/new-workspace/workspace-source' import { resolveGitHubWorkItemIdentity } from '@/lib/github-work-item-identity' import { isWorkspaceLinkedItemSourceContextMatch } from '../../../../shared/workspace-linked-item-source-context' import { getGitHubLinkedWorkItemIdentity } from './source-selection-decisions' @@ -30,7 +30,11 @@ export type ComposerDecisions = { item: LinkedWorkItemSummary | null | undefined, context: TaskSourceContext | null | undefined ) => TaskSourceContext | null - isExplicitWorkspaceNameInput: (input: { name: string; lastAutoName: string }) => boolean + isExplicitWorkspaceNameInput: (input: { + name: string + lastAutoName: string + lookupTextIsQuery?: boolean + }) => boolean resolveInitialWorkspaceRunSeed: (input: { draftProjectId?: string | null draftHostId?: string | null @@ -67,13 +71,15 @@ export function canResolveFolderSmartGitHubSubmit({ export function isExplicitWorkspaceNameInput({ name, - lastAutoName + lastAutoName, + lookupTextIsQuery }: { name: string lastAutoName: string + lookupTextIsQuery?: boolean }): boolean { // Why: a user-authored name must win over linked-item and first-message AI naming. - return Boolean(name.trim()) && name !== lastAutoName && !isWorkItemLookupText(name) + return !shouldApplyWorkspaceSourceAutoName({ currentName: name, lastAutoName, lookupTextIsQuery }) } export function resolveSmartGitHubCreateNames({ diff --git a/src/renderer/src/hooks/composer-state/composer-deferred-source-name.test.ts b/src/renderer/src/hooks/composer-state/composer-deferred-source-name.test.ts new file mode 100644 index 00000000000..7dce9f21b52 --- /dev/null +++ b/src/renderer/src/hooks/composer-state/composer-deferred-source-name.test.ts @@ -0,0 +1,181 @@ +// @vitest-environment happy-dom + +import { act, renderHook } from '@testing-library/react' +import { useRef, useState } from 'react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { LinkedWorkItemSummary } from '@/lib/new-workspace' +import type { GitHubWorkItem } from '../../../../shared/github/work-item-types' +import type { GitLabWorkItem } from '../../../../shared/gitlab-types' +import type { GitHubPrStartPoint } from '../../../../shared/worktree/types' +import type { SmartGitHubPrStartPointSelection } from './source-selection-decisions' +import { useGitHubSourceApplication } from './github-source-application' +import { useSourceIdentityActions } from './source-identity-actions' +import { useBranchStartPointActions } from './branch-start-point-actions' +import { useGitHubProviderSelection } from './github-provider-selection' +import { useGitLabProviderSelection } from './gitlab-provider-selection' + +const repo = { + id: 'fixture-repo', + path: '/repos/fixture', + displayName: 'Fixture', + badgeColor: '', + addedAt: 0 +} +const pr: GitHubWorkItem = { + id: 'pr-2', + type: 'pr', + number: 2, + title: 'Fix export', + state: 'open', + url: 'https://github.com/fixture/repo/pull/2', + labels: [], + updatedAt: '', + author: null, + repoId: repo.id +} +const mr: GitLabWorkItem = { + id: 'mr-2', + number: 2, + title: 'Fix export', + labels: [], + updatedAt: '', + author: null, + repoId: repo.id, + type: 'mr', + state: 'opened', + url: 'https://gitlab.com/fixture/repo/-/merge_requests/2' +} +let originalApi: PropertyDescriptor | undefined +const resolvePrBase = vi.fn() +const resolveMrBase = vi.fn() + +beforeEach(() => { + originalApi = Object.getOwnPropertyDescriptor(window, 'api') + Object.defineProperty(window, 'api', { + configurable: true, + value: { worktrees: { resolvePrBase, resolveMrBase } } + }) +}) +afterEach(() => { + vi.resetAllMocks() + if (originalApi) { + Object.defineProperty(window, 'api', originalApi) + } else { + Reflect.deleteProperty(window, 'api') + } +}) + +function deferred() { + let resolve!: (value: GitHubPrStartPoint) => void + const promise = new Promise((next) => { + resolve = next + }) + return { promise, resolve } +} + +function useDeferredSource() { + const [name, setName] = useState('') + const [linkedWorkItem, setLinkedWorkItem] = useState(null) + const [baseBranch, setBaseBranch] = useState() + const common = { + name, + setName, + linkedWorkItem, + setLinkedWorkItem, + baseBranch, + setBaseBranch, + lastAutoNameRef: useRef(''), + branchAutoNameRef: useRef(''), + smartGitHubPrStartPointSelectionRef: useRef(null), + lastAutoNoteRef: useRef(''), + noteRef: useRef(''), + initialProjectGroupAppliedRef: useRef(false), + selectedRepoGitHubSourceContext: null, + branchNameOverride: undefined, + branchNameOverridePreservesNameEdits: false, + forkPushWarning: null, + pushTarget: undefined, + baseBranchNamesWorkspace: false, + isProjectGroupTarget: false, + settings: null, + eligibleRepos: [repo], + selectedRepo: repo, + setBranchNameOverride: vi.fn(), + setBranchNameOverridePreservesNameEdits: vi.fn(), + setCreateError: vi.fn(), + setForkPushWarning: vi.fn(), + setLinkDebouncedQuery: vi.fn(), + setLinkDirectItem: vi.fn(), + setLinkPopoverOpen: vi.fn(), + setLinkQuery: vi.fn(), + setLinkedGitLabIssue: vi.fn(), + setLinkedGitLabMR: vi.fn(), + setLinkedIssue: vi.fn(), + setLinkedPR: vi.fn(), + setLinkedTaskSourceContext: vi.fn(), + setPushTarget: vi.fn(), + setReuseEligibleBranch: vi.fn(), + setReuseSelectedBranch: vi.fn(), + setCompareBaseRef: vi.fn(), + setStartFromResetHint: vi.fn(), + setBaseBranchNamesWorkspace: vi.fn(), + setNote: vi.fn(), + setProjectError: vi.fn(), + setSelectedProjectGroupId: vi.fn(), + setSparseDirectories: vi.fn(), + setSparseEnabled: vi.fn(), + setSparseSelectedPresetId: vi.fn(), + handleRepoChange: vi.fn() + } + const application = useGitHubSourceApplication(common) + const identity = useSourceIdentityActions({ ...common, ...application }) + const branch = useBranchStartPointActions({ ...common, ...application, ...identity }) + const github = useGitHubProviderSelection({ ...common, ...application, ...branch }) + const gitlab = useGitLabProviderSelection({ ...common, ...identity, ...branch }) + return { ...github, ...gitlab, ...identity, ...branch, name, linkedWorkItem, baseBranch } +} + +describe('name ownership while resolving review bases', () => { + it.each(['github', 'gitlab'])( + 'keeps an Advanced name while %s resolves its base', + async (provider) => { + const pending = deferred() + resolvePrBase.mockReturnValue(pending.promise) + resolveMrBase.mockReturnValue(pending.promise) + const { result } = renderHook(useDeferredSource) + act(() => { + if (provider === 'github') { + result.current.handleSmartGitHubItemSelect(pr) + } else { + result.current.handleSmartGitLabItemSelect(mr) + } + }) + act(() => result.current.handleNameValueChange('002')) + await act(async () => { + pending.resolve({ baseBranch: 'feature/export' }) + await pending.promise + }) + + expect(result.current.name).toBe('002') + expect(result.current.linkedWorkItem).toMatchObject({ provider, number: 2 }) + expect(result.current.baseBranch).toBe('feature/export') + } + ) + + it.each(['github', 'gitlab'])( + 'still links and auto-names a direct %s Start-from selection', + (provider) => { + const { result } = renderHook(useDeferredSource) + act(() => { + if (provider === 'github') { + result.current.handleBaseBranchPrSelect('feature/export', pr) + } else { + result.current.handleBaseBranchMrSelect('feature/export', mr) + } + }) + + expect(result.current.name).toBe('fix-export') + expect(result.current.linkedWorkItem).toMatchObject({ provider, number: 2 }) + } + ) +}) diff --git a/src/renderer/src/hooks/composer-state/composer-external-sync.ts b/src/renderer/src/hooks/composer-state/composer-external-sync.ts index f7efc502f88..66801c987ec 100644 --- a/src/renderer/src/hooks/composer-state/composer-external-sync.ts +++ b/src/renderer/src/hooks/composer-state/composer-external-sync.ts @@ -56,6 +56,7 @@ export function useComposerExternalSync(target: ComposerTargetState): ComposerEx const githubSourceApplication = useGitHubSourceApplication({ branchAutoNameRef: target.asyncComposerState.branchAutoNameRef, lastAutoNameRef: target.asyncComposerState.lastAutoNameRef, + linkedWorkItem: target.sourceContextState.linkedWorkItem, name: target.sourceContextState.name, selectedRepoGitHubSourceContext: target.sourceContextState.selectedRepoGitHubSourceContext, setBranchNameOverride: target.workspaceIdentityState.setBranchNameOverride, diff --git a/src/renderer/src/hooks/composer-state/composer-name-ownership.test.ts b/src/renderer/src/hooks/composer-state/composer-name-ownership.test.ts new file mode 100644 index 00000000000..db3277864fa --- /dev/null +++ b/src/renderer/src/hooks/composer-state/composer-name-ownership.test.ts @@ -0,0 +1,212 @@ +// @vitest-environment happy-dom + +import { act, renderHook } from '@testing-library/react' +import { useRef, useState } from 'react' +import { describe, expect, it, vi } from 'vitest' +import type { GitHubWorkItem } from '../../../../shared/github/work-item-types' +import type { LinkedWorkItemSummary } from '@/lib/new-workspace' +import { getSmartGitHubSubmitResolution } from '@/lib/smart-github-submit' +import { useGitHubSourceApplication } from './github-source-application' +import { useSourceIdentityActions } from './source-identity-actions' +import { useQuickSubmitSourcePreparation } from './quick-submit-source-preparation' +import * as decisions from './composer-decisions' +import type { + PendingSmartGitHubSubmitResolution, + SmartGitHubPrStartPointSelection +} from './source-selection-decisions' + +function issue(number: number): GitHubWorkItem { + return { + id: `issue-${number}`, + type: 'issue', + number, + title: 'Fix export', + state: 'open', + url: `https://github.com/fixture/repo/issues/${number}`, + labels: [], + updatedAt: '', + author: null, + repoId: 'fixture-repo' + } +} + +function useNameOwnership(draft?: { name: string; linkedWorkItem: LinkedWorkItemSummary }) { + const [name, setName] = useState(draft?.name ?? '') + const [linkedWorkItem, setLinkedWorkItem] = useState( + draft?.linkedWorkItem ?? null + ) + const lastAutoNameRef = useRef( + decisions.getInitialAutoManagedWorkspaceName({ + initialName: '', + draftName: draft?.name, + draftLinkedWorkItem: draft?.linkedWorkItem + }) + ) + const common = { + name, + setName, + linkedWorkItem, + setLinkedWorkItem, + lastAutoNameRef, + branchAutoNameRef: useRef(''), + smartGitHubPrStartPointSelectionRef: useRef(null), + selectedRepoGitHubSourceContext: null, + branchNameOverride: undefined, + branchNameOverridePreservesNameEdits: false, + forkPushWarning: null, + pushTarget: undefined, + setBranchNameOverride: vi.fn(), + setBranchNameOverridePreservesNameEdits: vi.fn(), + setCreateError: vi.fn(), + setForkPushWarning: vi.fn(), + setLinkDebouncedQuery: vi.fn(), + setLinkDirectItem: vi.fn(), + setLinkPopoverOpen: vi.fn(), + setLinkQuery: vi.fn(), + setLinkedGitLabIssue: vi.fn(), + setLinkedGitLabMR: vi.fn(), + setLinkedIssue: vi.fn(), + setLinkedPR: vi.fn(), + setLinkedTaskSourceContext: vi.fn(), + setPushTarget: vi.fn(), + setReuseEligibleBranch: vi.fn(), + setReuseSelectedBranch: vi.fn() + } + const application = useGitHubSourceApplication(common) + const identity = useSourceIdentityActions({ ...common, ...application }) + const source = useQuickSubmitSourcePreparation({ + ...common, + baseBranch: 'main', + compareBaseRef: undefined, + disabledTuiAgents: [], + effectiveLinkedPR: null, + decisions, + fallbackCreatureName: 'otter', + linkedGitLabMR: null, + parsedLinkedIssueNumber: linkedWorkItem?.number ?? null + }) + return { + ...application, + ...identity, + name, + linkedWorkItem, + prepare: (resolution: PendingSmartGitHubSubmitResolution = { kind: 'none' }) => + source.prepareQuickSubmitSource(resolution, null, name || 'otter') + } +} + +describe('workspace name ownership after linking a source', () => { + it.each(['347', '002'])('submits Advanced name %s as user input', (name) => { + const { result } = renderHook(() => useNameOwnership()) + act(() => result.current.applyLinkedWorkItem(issue(Number(name)))) + act(() => result.current.handleNameValueChange(name)) + + expect(result.current.prepare()).toMatchObject({ + workspaceName: name, + nameIsAutoManaged: false, + submitLinkedWorkItem: { number: Number(name), url: issue(Number(name)).url } + }) + }) + + it.each([2, 347])('preserves an edited name when source #%s replaces the source', (number) => { + const { result } = renderHook(() => useNameOwnership()) + act(() => result.current.applyLinkedWorkItem(issue(2))) + act(() => result.current.handleNameValueChange('002')) + act(() => result.current.applyLinkedWorkItem(issue(number))) + + expect(result.current.name).toBe('002') + expect(result.current.prepare()).toMatchObject({ + workspaceName: '002', + nameIsAutoManaged: false, + submitLinkedWorkItem: { number } + }) + }) + + it('restores manual ownership from an existing draft without a new persisted flag', () => { + const { result } = renderHook(() => useNameOwnership({ name: '002', linkedWorkItem: issue(2) })) + + expect(result.current.prepare()).toMatchObject({ + workspaceName: '002', + nameIsAutoManaged: false + }) + }) + + it('keeps manual ownership when the linked source is removed', () => { + const { result } = renderHook(() => useNameOwnership()) + act(() => result.current.applyLinkedWorkItem(issue(2))) + act(() => result.current.handleNameValueChange('002')) + act(() => result.current.handleRemoveLinkedWorkItem()) + + expect(result.current.prepare()).toMatchObject({ + workspaceName: '002', + nameIsAutoManaged: false, + submitLinkedWorkItem: null + }) + }) + + it.each(['347', '002', '#347'])('still auto-names a source picked from query %s', (query) => { + const { result } = renderHook(() => useNameOwnership()) + act(() => result.current.handleNameValueChange(query)) + act(() => result.current.applyLinkedWorkItem(issue(query === '002' ? 2 : 347))) + + expect(result.current.prepare()).toMatchObject({ + workspaceName: 'fix-export', + nameIsAutoManaged: true + }) + }) + + it('restores automatic naming when the Advanced name is cleared', () => { + const { result } = renderHook(() => useNameOwnership()) + act(() => result.current.applyLinkedWorkItem(issue(2))) + act(() => result.current.handleNameValueChange('002')) + act(() => result.current.handleNameValueChange('')) + + expect(result.current.prepare()).toMatchObject({ + workspaceName: 'fix-export', + nameIsAutoManaged: true + }) + }) + + it('keeps automatic ownership when a generated name is restored or refreshed', () => { + const { result } = renderHook(() => + useNameOwnership({ name: 'fix-export', linkedWorkItem: issue(2) }) + ) + act(() => result.current.applyLinkedWorkItem({ ...issue(2), title: 'Fix refreshed export' })) + + expect(result.current.prepare()).toMatchObject({ + workspaceName: 'fix-refreshed-export', + nameIsAutoManaged: true + }) + }) + + it('still resolves a pasted lookup at submit time as an automatic name', () => { + const { result } = renderHook(() => useNameOwnership()) + act(() => result.current.handleNameValueChange(issue(2).url)) + + expect( + result.current.prepare({ + ...getSmartGitHubSubmitResolution(issue(2)), + kind: 'metadata-only' + }) + ).toMatchObject({ workspaceName: 'fix-export', nameIsAutoManaged: true }) + }) + + it('keeps an Advanced name when the selected pull request resolves its base at submit time', () => { + const pr: GitHubWorkItem = { + ...issue(2), + type: 'pr', + url: 'https://github.com/fixture/repo/pull/2' + } + const { result } = renderHook(() => useNameOwnership()) + act(() => result.current.applyLinkedWorkItem(pr)) + act(() => result.current.handleNameValueChange('002')) + + expect( + result.current.prepare({ + ...getSmartGitHubSubmitResolution(pr), + kind: 'pr-start-point', + baseBranch: 'feature/export' + }) + ).toMatchObject({ workspaceName: '002', nameIsAutoManaged: false, submitLinkedPR: 2 }) + }) +}) diff --git a/src/renderer/src/hooks/composer-state/composer-name-source-selection.test.ts b/src/renderer/src/hooks/composer-state/composer-name-source-selection.test.ts index 3de3c57099d..5aa4dec0efc 100644 --- a/src/renderer/src/hooks/composer-state/composer-name-source-selection.test.ts +++ b/src/renderer/src/hooks/composer-state/composer-name-source-selection.test.ts @@ -5,9 +5,23 @@ import { describe, expect, it, vi } from 'vitest' import { useIssueSourceActions } from './issue-source-actions' import { resolveDraftBaseBranchNamesWorkspace } from './workspace-identity-state' import { useGitHubProviderSelection } from './github-provider-selection' +import type { LinearIssue } from '../../../../shared/linear/issue-types' type Input = Parameters[0] +const linearIssue: LinearIssue = { + id: 'linear-9', + identifier: 'ENG-9', + title: 'Fix refreshed export', + url: 'https://linear.app/fixture/issue/ENG-9', + state: { name: 'Open', type: 'unstarted', color: '' }, + team: { id: 'engineering', name: 'Engineering', key: 'ENG' }, + labels: [], + labelIds: [], + priority: 0, + updatedAt: '' +} + function createInput(overrides: Partial = {}): Input { return { baseBranch: undefined, @@ -42,6 +56,43 @@ function createInput(overrides: Partial = {}): Input { } describe('composer name source selection', () => { + it.each([false, true])( + 'keeps an Advanced Linear identifier name on source refresh (folder %s)', + (isProjectGroupTarget) => { + const input = createInput({ + isProjectGroupTarget, + name: 'ENG-9', + lastAutoNameRef: { current: 'eng-9-fix-export' }, + linkedWorkItem: { + provider: 'linear', + type: 'issue', + number: 0, + title: 'Fix export', + url: linearIssue.url, + linearIdentifier: 'ENG-9' + } + }) + const { result } = renderHook(() => useIssueSourceActions(input)) + act(() => result.current.handleSmartLinearIssueSelect(linearIssue)) + + expect(input.setName).not.toHaveBeenCalled() + expect(input.setLinkedWorkItem).toHaveBeenCalledWith( + expect.objectContaining({ linearIdentifier: 'ENG-9' }) + ) + } + ) + + it.each([false, true])( + 'still replaces an unselected Linear identifier query (folder %s)', + (isProjectGroupTarget) => { + const input = createInput({ isProjectGroupTarget, name: 'ENG-9' }) + const { result } = renderHook(() => useIssueSourceActions(input)) + act(() => result.current.handleSmartLinearIssueSelect(linearIssue)) + + expect(input.setName).toHaveBeenCalledTimes(1) + } + ) + it.each([false, true])('clears only source-owned bases when selecting Jira (%s)', (owned) => { const input = createInput({ baseBranch: 'release/1.2', baseBranchNamesWorkspace: owned }) const { result } = renderHook(() => useIssueSourceActions(input)) diff --git a/src/renderer/src/hooks/composer-state/composer-source-model.ts b/src/renderer/src/hooks/composer-state/composer-source-model.ts index f2af568f123..a73377928c8 100644 --- a/src/renderer/src/hooks/composer-state/composer-source-model.ts +++ b/src/renderer/src/hooks/composer-state/composer-source-model.ts @@ -25,14 +25,16 @@ export type ComposerSourceModel = { nextBaseBranch: string, item: GitLabWorkItem, nextPushTarget?: GitPushTarget | undefined, - nextCompareBaseRef?: string | undefined + nextCompareBaseRef?: string | undefined, + options?: { sourceAlreadyLinked?: boolean } ) => void handleBaseBranchPrSelect: ( nextBaseBranch: string, item: GitHubWorkItem, nextPushTarget?: GitPushTarget | undefined, nextBranchNameOverride?: string | undefined, - nextCompareBaseRef?: string | undefined + nextCompareBaseRef?: string | undefined, + options?: { sourceAlreadyLinked?: boolean } ) => void handleBranchNameOverrideChange: (value: string | undefined) => void handleClearSmartNameSelection: () => void diff --git a/src/renderer/src/hooks/composer-state/composer-source-state.ts b/src/renderer/src/hooks/composer-state/composer-source-state.ts index cb5052336d6..94516fd22e2 100644 --- a/src/renderer/src/hooks/composer-state/composer-source-state.ts +++ b/src/renderer/src/hooks/composer-state/composer-source-state.ts @@ -159,6 +159,7 @@ export function useComposerSourceState( handleBaseBranchPrSelect: branchStartPointActions.handleBaseBranchPrSelect, isProjectGroupTarget: target.runtimeTargetSelection.isProjectGroupTarget, lastAutoNameRef: target.asyncComposerState.lastAutoNameRef, + linkedWorkItem: target.sourceContextState.linkedWorkItem, name: target.sourceContextState.name, selectedRepo: target.runtimeTargetSelection.selectedRepo, selectedRepoGitHubSourceContext: target.sourceContextState.selectedRepoGitHubSourceContext, @@ -188,6 +189,7 @@ export function useComposerSourceState( handleBaseBranchMrSelect: branchStartPointActions.handleBaseBranchMrSelect, isProjectGroupTarget: target.runtimeTargetSelection.isProjectGroupTarget, lastAutoNameRef: target.asyncComposerState.lastAutoNameRef, + linkedWorkItem: target.sourceContextState.linkedWorkItem, name: target.sourceContextState.name, selectedRepo: target.runtimeTargetSelection.selectedRepo, setBaseBranch: target.workspaceIdentityState.setBaseBranch, diff --git a/src/renderer/src/hooks/composer-state/github-provider-selection.ts b/src/renderer/src/hooks/composer-state/github-provider-selection.ts index 0c72a672586..5a40353f436 100644 --- a/src/renderer/src/hooks/composer-state/github-provider-selection.ts +++ b/src/renderer/src/hooks/composer-state/github-provider-selection.ts @@ -9,6 +9,7 @@ type GitHubProviderSelectionInput = Pick< | 'handleBaseBranchPrSelect' | 'isProjectGroupTarget' | 'lastAutoNameRef' + | 'linkedWorkItem' | 'name' | 'selectedRepo' | 'selectedRepoGitHubSourceContext' @@ -55,6 +56,7 @@ export function useGitHubProviderSelection(input: GitHubProviderSelectionInput) handleBaseBranchPrSelect, isProjectGroupTarget, lastAutoNameRef, + linkedWorkItem, name, selectedRepo, selectedRepoGitHubSourceContext, @@ -96,7 +98,8 @@ export function useGitHubProviderSelection(input: GitHubProviderSelectionInput) nextName && shouldApplyWorkspaceSourceAutoName({ currentName: name, - lastAutoName: lastAutoNameRef.current + lastAutoName: lastAutoNameRef.current, + lookupTextIsQuery: !linkedWorkItem }) ) { setName(nextName) @@ -150,7 +153,8 @@ export function useGitHubProviderSelection(input: GitHubProviderSelectionInput) normalizedItem, result.pushTarget, result.branchNameOverride, - result.compareBaseRef + result.compareBaseRef, + { sourceAlreadyLinked: true } ) // Why: a fork PR push lands on the contributor's fork; without maintainer-edits allowed GitHub rejects it, so warn up front. setForkPushWarning(getForkPushWarning(result)) @@ -176,6 +180,7 @@ export function useGitHubProviderSelection(input: GitHubProviderSelectionInput) handleBaseBranchPrSelect, isProjectGroupTarget, name, + linkedWorkItem, selectedRepo, selectedRepoGitHubSourceContext, branchAutoNameRef, diff --git a/src/renderer/src/hooks/composer-state/github-source-application.ts b/src/renderer/src/hooks/composer-state/github-source-application.ts index 55476a90ea1..bda7d766881 100644 --- a/src/renderer/src/hooks/composer-state/github-source-application.ts +++ b/src/renderer/src/hooks/composer-state/github-source-application.ts @@ -4,6 +4,7 @@ type GitHubSourceApplicationInput = Pick< ComposerModel, | 'branchAutoNameRef' | 'lastAutoNameRef' + | 'linkedWorkItem' | 'name' | 'selectedRepoGitHubSourceContext' | 'setBranchNameOverride' @@ -27,6 +28,7 @@ export function useGitHubSourceApplication(input: GitHubSourceApplicationInput) const { branchAutoNameRef, lastAutoNameRef, + linkedWorkItem, name, selectedRepoGitHubSourceContext, setBranchNameOverride, @@ -73,7 +75,8 @@ export function useGitHubSourceApplication(input: GitHubSourceApplicationInput) suggestedName && shouldApplyWorkspaceSourceAutoName({ currentName: name, - lastAutoName: lastAutoNameRef.current + lastAutoName: lastAutoNameRef.current, + lookupTextIsQuery: !linkedWorkItem }) ) { setName(suggestedName) @@ -87,6 +90,7 @@ export function useGitHubSourceApplication(input: GitHubSourceApplicationInput) }, [ name, + linkedWorkItem, selectedRepoGitHubSourceContext, branchAutoNameRef, lastAutoNameRef, diff --git a/src/renderer/src/hooks/composer-state/gitlab-provider-selection.ts b/src/renderer/src/hooks/composer-state/gitlab-provider-selection.ts index e322d84156c..1dfc9ac7ee6 100644 --- a/src/renderer/src/hooks/composer-state/gitlab-provider-selection.ts +++ b/src/renderer/src/hooks/composer-state/gitlab-provider-selection.ts @@ -8,6 +8,7 @@ type GitLabProviderSelectionInput = Pick< | 'handleBaseBranchMrSelect' | 'isProjectGroupTarget' | 'lastAutoNameRef' + | 'linkedWorkItem' | 'name' | 'selectedRepo' | 'setBaseBranch' @@ -47,6 +48,7 @@ export function useGitLabProviderSelection(input: GitLabProviderSelectionInput) handleBaseBranchMrSelect, isProjectGroupTarget, lastAutoNameRef, + linkedWorkItem, name, selectedRepo, setBaseBranch, @@ -81,7 +83,8 @@ export function useGitLabProviderSelection(input: GitLabProviderSelectionInput) nextName && shouldApplyWorkspaceSourceAutoName({ currentName: name, - lastAutoName: lastAutoNameRef.current + lastAutoName: lastAutoNameRef.current, + lookupTextIsQuery: !linkedWorkItem }) ) { setName(nextName) @@ -149,7 +152,8 @@ export function useGitLabProviderSelection(input: GitLabProviderSelectionInput) result.baseBranch, item, result.pushTarget, - result.compareBaseRef + result.compareBaseRef, + { sourceAlreadyLinked: true } ) }) .catch((error: unknown) => { @@ -169,6 +173,7 @@ export function useGitLabProviderSelection(input: GitLabProviderSelectionInput) handleBaseBranchMrSelect, isProjectGroupTarget, name, + linkedWorkItem, selectedRepo, branchAutoNameRef, lastAutoNameRef, diff --git a/src/renderer/src/hooks/composer-state/issue-source-actions.ts b/src/renderer/src/hooks/composer-state/issue-source-actions.ts index 58df16c9cc0..e65aadd35e8 100644 --- a/src/renderer/src/hooks/composer-state/issue-source-actions.ts +++ b/src/renderer/src/hooks/composer-state/issue-source-actions.ts @@ -103,9 +103,10 @@ export function useIssueSourceActions(input: IssueSourceActionsInput) { if ( shouldApplyWorkspaceSourceAutoName({ currentName: name, - lastAutoName: lastAutoNameRef.current + lastAutoName: lastAutoNameRef.current, + lookupTextIsQuery: !linkedWorkItem }) || - name.trim().toLowerCase() === issue.identifier.toLowerCase() + (!linkedWorkItem && name.trim().toLowerCase() === issue.identifier.toLowerCase()) ) { setName(suggestedName) lastAutoNameRef.current = suggestedName @@ -124,9 +125,10 @@ export function useIssueSourceActions(input: IssueSourceActionsInput) { if ( shouldApplyWorkspaceSourceAutoName({ currentName: name, - lastAutoName: lastAutoNameRef.current + lastAutoName: lastAutoNameRef.current, + lookupTextIsQuery: !linkedWorkItem }) || - name.trim().toLowerCase() === issue.identifier.toLowerCase() + (!linkedWorkItem && name.trim().toLowerCase() === issue.identifier.toLowerCase()) ) { setName(suggestedName) lastAutoNameRef.current = suggestedName @@ -141,6 +143,7 @@ export function useIssueSourceActions(input: IssueSourceActionsInput) { [ isProjectGroupTarget, name, + linkedWorkItem, branchAutoNameRef, lastAutoNameRef, setBranchNameOverride, @@ -182,7 +185,8 @@ export function useIssueSourceActions(input: IssueSourceActionsInput) { suggestedName && shouldApplyWorkspaceSourceAutoName({ currentName: name, - lastAutoName: lastAutoNameRef.current + lastAutoName: lastAutoNameRef.current, + lookupTextIsQuery: !linkedWorkItem }) ) { setName(suggestedName) @@ -191,6 +195,7 @@ export function useIssueSourceActions(input: IssueSourceActionsInput) { }, [ name, + linkedWorkItem, baseBranchNamesWorkspace, branchAutoNameRef, lastAutoNameRef, diff --git a/src/renderer/src/hooks/composer-state/quick-submit-source-preparation.ts b/src/renderer/src/hooks/composer-state/quick-submit-source-preparation.ts index 4cf534953c3..407bfb9e658 100644 --- a/src/renderer/src/hooks/composer-state/quick-submit-source-preparation.ts +++ b/src/renderer/src/hooks/composer-state/quick-submit-source-preparation.ts @@ -71,7 +71,8 @@ export function useQuickSubmitSourcePreparation(input: QuickSubmitSourcePreparat const nameIsAutoManaged = !isExplicitWorkspaceNameInput({ name, - lastAutoName: lastAutoNameRef.current + lastAutoName: lastAutoNameRef.current, + lookupTextIsQuery: !linkedWorkItem && smartGitHubResolution.kind !== 'none' }) const smartGitHubCreateNames = diff --git a/src/renderer/src/hooks/composer-state/source-identity-actions.ts b/src/renderer/src/hooks/composer-state/source-identity-actions.ts index 1120737e48a..f41fe696e2f 100644 --- a/src/renderer/src/hooks/composer-state/source-identity-actions.ts +++ b/src/renderer/src/hooks/composer-state/source-identity-actions.ts @@ -111,7 +111,8 @@ export function useSourceIdentityActions(input: SourceIdentityActionsInput) { nextName && shouldApplyWorkspaceSourceAutoName({ currentName: name, - lastAutoName: lastAutoNameRef.current + lastAutoName: lastAutoNameRef.current, + lookupTextIsQuery: !linkedWorkItem }) ) { setName(nextName) @@ -123,6 +124,7 @@ export function useSourceIdentityActions(input: SourceIdentityActionsInput) { }, [ name, + linkedWorkItem, branchAutoNameRef, lastAutoNameRef, setBranchNameOverride, diff --git a/src/shared/new-workspace/workspace-source.test.ts b/src/shared/new-workspace/workspace-source.test.ts index 35c7ffd76a3..2fc35e1c34c 100644 --- a/src/shared/new-workspace/workspace-source.test.ts +++ b/src/shared/new-workspace/workspace-source.test.ts @@ -10,6 +10,24 @@ import { } from './workspace-source' describe('workspace source policy', () => { + it.each(['347', '002', '#347', 'https://github.com/fixture/repo/issues/347'])( + 'keeps manual name %s after the query field becomes a selected source', + (currentName) => { + expect( + shouldApplyWorkspaceSourceAutoName({ + currentName, + lastAutoName: 'fix-export', + lookupTextIsQuery: false + }) + ).toBe(false) + expect( + shouldApplyWorkspaceSourceAutoName({ + currentName, + lastAutoName: 'fix-export' + }) + ).toBe(true) + } + ) const linear = buildLinearWorkspaceSource({ identifier: 'ENG-42', title: 'Ship mobile parity', diff --git a/src/shared/new-workspace/workspace-source.ts b/src/shared/new-workspace/workspace-source.ts index 42af0804915..d7764ad0e78 100644 --- a/src/shared/new-workspace/workspace-source.ts +++ b/src/shared/new-workspace/workspace-source.ts @@ -160,11 +160,13 @@ export function buildJiraWorkspaceSource( export function shouldApplyWorkspaceSourceAutoName(args: { currentName: string lastAutoName: string + lookupTextIsQuery?: boolean }): boolean { return ( !args.currentName.trim() || args.currentName === args.lastAutoName || - isWorkItemLookupText(args.currentName) + // A selected source exposes a name field; its manual edits are no longer lookup queries. + (args.lookupTextIsQuery !== false && isWorkItemLookupText(args.currentName)) ) }