Use work item titles for create-from workspace names
This commit is contained in:
parent
5337e93cb4
commit
e5fe0f14d1
|
|
@ -130,7 +130,7 @@ import { JiraIcon } from '@/components/icons/JiraIcon'
|
|||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
getLinkedWorkItemSuggestedName,
|
||||
getWorkspaceIntentName,
|
||||
getLinkedWorkItemWorkspaceName,
|
||||
getTaskPresetQuery,
|
||||
PER_REPO_FETCH_LIMIT,
|
||||
CROSS_REPO_DISPLAY_LIMIT
|
||||
|
|
@ -335,34 +335,28 @@ const GITHUB_TASK_ROW_HOVER_SURFACE_CLASS =
|
|||
'group-hover/github-task-row:[background:color-mix(in_srgb,var(--muted)_70%,var(--background))]'
|
||||
|
||||
function getGitHubWorkItemWorkspaceSeed(item: GitHubWorkItem): string {
|
||||
return (
|
||||
getWorkspaceIntentName({ workItem: item })?.seedName ?? getLinkedWorkItemSuggestedName(item)
|
||||
)
|
||||
return getLinkedWorkItemWorkspaceName(item)?.seedName ?? getLinkedWorkItemSuggestedName(item)
|
||||
}
|
||||
|
||||
function getGitLabWorkItemWorkspaceSeed(item: GitLabWorkItem): string {
|
||||
return (
|
||||
getWorkspaceIntentName({
|
||||
workItem: {
|
||||
type: item.type,
|
||||
provider: 'gitlab',
|
||||
number: item.number,
|
||||
title: item.title
|
||||
}
|
||||
getLinkedWorkItemWorkspaceName({
|
||||
type: item.type,
|
||||
provider: 'gitlab',
|
||||
number: item.number,
|
||||
title: item.title
|
||||
})?.seedName ?? getLinkedWorkItemSuggestedName(item)
|
||||
)
|
||||
}
|
||||
|
||||
function getJiraIssueWorkspaceSeed(issue: JiraIssue): string {
|
||||
return (
|
||||
getWorkspaceIntentName({
|
||||
workItem: {
|
||||
type: 'issue',
|
||||
provider: 'jira',
|
||||
number: 0,
|
||||
title: `${issue.key} ${issue.title}`,
|
||||
jiraIdentifier: issue.key
|
||||
}
|
||||
getLinkedWorkItemWorkspaceName({
|
||||
type: 'issue',
|
||||
provider: 'jira',
|
||||
number: 0,
|
||||
title: `${issue.key} ${issue.title}`,
|
||||
jiraIdentifier: issue.key
|
||||
})?.seedName ?? getLinkedWorkItemSuggestedName(issue)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
} from '@/components/ui/command'
|
||||
import { branchName } from '@/lib/git-utils'
|
||||
import { parseGitHubIssueOrPRNumber, parseGitHubIssueOrPRLink } from '@/lib/github-links'
|
||||
import { getLinkedWorkItemSuggestedName, getWorkspaceIntentName } from '@/lib/new-workspace'
|
||||
import { getLinkedWorkItemSuggestedName, getLinkedWorkItemWorkspaceName } from '@/lib/new-workspace'
|
||||
import type { LinkedWorkItemSummary } from '@/lib/new-workspace'
|
||||
import { sortWorktreesSmart } from '@/components/sidebar/smart-sort'
|
||||
import { isDefaultBranchWorkspace } from '@/components/sidebar/visible-worktrees'
|
||||
|
|
@ -1037,7 +1037,7 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
|
|||
}
|
||||
data.linkedWorkItem = linkedWorkItem
|
||||
data.prefilledName =
|
||||
getWorkspaceIntentName({ sourceText: trimmed, workItem: linkedWorkItem })?.seedName ??
|
||||
getLinkedWorkItemWorkspaceName(linkedWorkItem)?.seedName ??
|
||||
getLinkedWorkItemSuggestedName({ title: item.title })
|
||||
} else {
|
||||
// Fallback: we couldn't resolve the URL, just seed the name.
|
||||
|
|
@ -1102,7 +1102,7 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
|
|||
}
|
||||
data.linkedWorkItem = linkedWorkItem
|
||||
data.prefilledName =
|
||||
getWorkspaceIntentName({ sourceText: trimmed, workItem: linkedWorkItem })?.seedName ??
|
||||
getLinkedWorkItemWorkspaceName(linkedWorkItem)?.seedName ??
|
||||
getLinkedWorkItemSuggestedName({ title: item.title })
|
||||
} else {
|
||||
data.prefilledName = trimmed
|
||||
|
|
|
|||
|
|
@ -45,8 +45,8 @@ import {
|
|||
getAttachmentLabel,
|
||||
getLinkedWorkItemProvider,
|
||||
getLinkedWorkItemSuggestedName,
|
||||
getLinkedWorkItemWorkspaceName,
|
||||
getSetupConfig,
|
||||
getWorkspaceIntentName,
|
||||
getWorkspaceSeedName,
|
||||
isGitLabIssueUrl,
|
||||
PER_REPO_FETCH_LIMIT,
|
||||
|
|
@ -1154,10 +1154,7 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
|
|||
url: item.url
|
||||
})
|
||||
const suggestedName =
|
||||
getWorkspaceIntentName({
|
||||
sourceText: name,
|
||||
workItem: item
|
||||
})?.seedName ?? getLinkedWorkItemSuggestedName(item)
|
||||
getLinkedWorkItemWorkspaceName(item)?.seedName ?? getLinkedWorkItemSuggestedName(item)
|
||||
if (suggestedName && (!name.trim() || name === lastAutoNameRef.current)) {
|
||||
setName(suggestedName)
|
||||
lastAutoNameRef.current = suggestedName
|
||||
|
|
@ -1192,9 +1189,7 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
|
|||
throw new Error('Could not resolve the GitHub item before creating the workspace.')
|
||||
}
|
||||
|
||||
const resolution = getSmartGitHubSubmitResolution(item, {
|
||||
sourceText: [name, agentPrompt, noteRef.current].filter(Boolean).join('\n')
|
||||
})
|
||||
const resolution = getSmartGitHubSubmitResolution(item)
|
||||
// Why: Create can be clicked before the debounced smart field commits
|
||||
// its selected source. Commit the resolved item here so failures leave
|
||||
// the form showing the title instead of the raw URL.
|
||||
|
|
@ -1211,7 +1206,7 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
|
|||
branchAutoNameRef.current = ''
|
||||
setStartFromResetHint(null)
|
||||
return resolution
|
||||
}, [agentPrompt, linkedWorkItem, name, selectedRepo, selectedRepoIsGit])
|
||||
}, [linkedWorkItem, name, selectedRepo, selectedRepoIsGit])
|
||||
|
||||
// Why: parallel of applyLinkedWorkItem for GitLab. Touches the GitLab
|
||||
// state slots only — the GitHub linkedIssue/linkedPR remain unchanged
|
||||
|
|
@ -1241,16 +1236,13 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
|
|||
title: item.title,
|
||||
branchName: item.branchName
|
||||
} as unknown as GitHubWorkItem)
|
||||
const intentName = getWorkspaceIntentName({
|
||||
sourceText: name,
|
||||
workItem: {
|
||||
type: item.type,
|
||||
provider: 'gitlab',
|
||||
number: item.number,
|
||||
title: item.title
|
||||
}
|
||||
const titleName = getLinkedWorkItemWorkspaceName({
|
||||
type: item.type,
|
||||
provider: 'gitlab',
|
||||
number: item.number,
|
||||
title: item.title
|
||||
})
|
||||
const nextName = intentName?.seedName ?? suggestedName
|
||||
const nextName = titleName?.seedName ?? suggestedName
|
||||
if (nextName && (!name.trim() || name === lastAutoNameRef.current)) {
|
||||
setName(nextName)
|
||||
lastAutoNameRef.current = nextName
|
||||
|
|
@ -1894,16 +1886,13 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
|
|||
const submitLinkedIssueNumber =
|
||||
smartGitHubResolution?.linkedIssueNumber ?? parsedLinkedIssueNumber
|
||||
const submitLinkedPR = smartGitHubResolution?.linkedPR ?? effectiveLinkedPR
|
||||
const submitIntentName = submitLinkedWorkItem
|
||||
? getWorkspaceIntentName({
|
||||
sourceText: [name, agentPrompt, note].filter(Boolean).join('\n'),
|
||||
workItem: submitLinkedWorkItem
|
||||
})
|
||||
const submitTitleName = submitLinkedWorkItem
|
||||
? getLinkedWorkItemWorkspaceName(submitLinkedWorkItem)
|
||||
: null
|
||||
const nameIsAutoManaged = !name.trim() || name === lastAutoNameRef.current
|
||||
const workspaceName =
|
||||
smartGitHubResolution?.workspaceName ??
|
||||
(nameIsAutoManaged && submitIntentName ? submitIntentName.seedName : workspaceSeedName)
|
||||
(nameIsAutoManaged && submitTitleName ? submitTitleName.seedName : workspaceSeedName)
|
||||
if (!workspaceName) {
|
||||
return
|
||||
}
|
||||
|
|
@ -1971,7 +1960,7 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
|
|||
})
|
||||
const createDisplayName =
|
||||
smartGitHubResolution?.displayName ??
|
||||
(nameIsAutoManaged ? submitIntentName?.displayName : undefined)
|
||||
(nameIsAutoManaged ? submitTitleName?.displayName : undefined)
|
||||
// Why: the first-work hook only renames blank, auto-generated git workspaces
|
||||
// that actually launch an agent. Persist that known-pending state for the card.
|
||||
const pendingFirstAgentMessageRename =
|
||||
|
|
@ -2168,16 +2157,13 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
|
|||
const submitLinkedIssueNumber =
|
||||
smartGitHubResolution?.linkedIssueNumber ?? parsedLinkedIssueNumber
|
||||
const submitLinkedPR = smartGitHubResolution?.linkedPR ?? effectiveLinkedPR
|
||||
const submitIntentName = submitLinkedWorkItem
|
||||
? getWorkspaceIntentName({
|
||||
sourceText: [name, agentPrompt, note].filter(Boolean).join('\n'),
|
||||
workItem: submitLinkedWorkItem
|
||||
})
|
||||
const submitTitleName = submitLinkedWorkItem
|
||||
? getLinkedWorkItemWorkspaceName(submitLinkedWorkItem)
|
||||
: null
|
||||
const nameIsAutoManaged = !name.trim() || name === lastAutoNameRef.current
|
||||
const workspaceName =
|
||||
smartGitHubResolution?.workspaceName ??
|
||||
(nameIsAutoManaged && submitIntentName ? submitIntentName.seedName : workspaceNameSeed)
|
||||
(nameIsAutoManaged && submitTitleName ? submitTitleName.seedName : workspaceNameSeed)
|
||||
if (!workspaceName) {
|
||||
return
|
||||
}
|
||||
|
|
@ -2228,7 +2214,7 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
|
|||
})
|
||||
const createDisplayName =
|
||||
smartGitHubResolution?.displayName ??
|
||||
(nameIsAutoManaged ? submitIntentName?.displayName : undefined)
|
||||
(nameIsAutoManaged ? submitTitleName?.displayName : undefined)
|
||||
// Why: quick create uses the same blank-name creature branch flow; the card
|
||||
// needs an explicit marker rather than guessing from the generated title.
|
||||
const pendingFirstAgentMessageRename =
|
||||
|
|
@ -2346,7 +2332,6 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
|
|||
}
|
||||
},
|
||||
[
|
||||
agentPrompt,
|
||||
baseBranch,
|
||||
branchNameOverride,
|
||||
branchNameOverridePreservesNameEdits,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { resolveHookCommandSourcePolicy } from '../../../shared/hook-command-sou
|
|||
import { isExpectedAgentProcess } from '../../../shared/agent-process-recognition'
|
||||
import { slugifyForWorkspaceName } from '../../../shared/workspace-name'
|
||||
export { getLinkedWorkItemSuggestedName } from '../../../shared/workspace-name'
|
||||
export { getLinkedWorkItemWorkspaceName } from '../../../shared/workspace-name'
|
||||
export { getWorkspaceIntentName } from '../../../shared/workspace-name'
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -264,20 +264,17 @@ describe('lookupSmartGitHubSubmitItem', () => {
|
|||
})
|
||||
|
||||
describe('getSmartGitHubSubmitResolution', () => {
|
||||
it('uses short intent identity for workspace name, display name, and linked PR metadata', () => {
|
||||
it('uses the resolved PR title for workspace name, display name, and linked PR metadata', () => {
|
||||
expect(
|
||||
getSmartGitHubSubmitResolution(
|
||||
{
|
||||
type: 'pr',
|
||||
number: 2049,
|
||||
title: 'Fix smart resolution delay',
|
||||
url: 'https://github.com/stablyai/orca/pull/2049'
|
||||
},
|
||||
{ sourceText: 'review https://github.com/stablyai/orca/pull/2049' }
|
||||
)
|
||||
getSmartGitHubSubmitResolution({
|
||||
type: 'pr',
|
||||
number: 2049,
|
||||
title: 'Fix smart resolution delay',
|
||||
url: 'https://github.com/stablyai/orca/pull/2049'
|
||||
})
|
||||
).toEqual({
|
||||
workspaceName: 'review-pr-2049',
|
||||
displayName: 'Review PR 2049',
|
||||
workspaceName: 'fix-smart-resolution-delay',
|
||||
displayName: 'Fix smart resolution delay',
|
||||
linkedWorkItem: {
|
||||
type: 'pr',
|
||||
number: 2049,
|
||||
|
|
@ -289,19 +286,16 @@ describe('getSmartGitHubSubmitResolution', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('uses user intent instead of the raw title for linked issue metadata', () => {
|
||||
const resolution = getSmartGitHubSubmitResolution(
|
||||
{
|
||||
type: 'issue',
|
||||
number: 2050,
|
||||
title: 'Issue #2050: Make create feel instant',
|
||||
url: 'https://github.com/stablyai/orca/issues/2050'
|
||||
},
|
||||
{ sourceText: 'https://github.com/stablyai/orca/issues/2050 and fix it' }
|
||||
)
|
||||
it('strips duplicated issue prefixes while preserving linked issue metadata', () => {
|
||||
const resolution = getSmartGitHubSubmitResolution({
|
||||
type: 'issue',
|
||||
number: 2050,
|
||||
title: 'Issue #2050: Make create feel instant',
|
||||
url: 'https://github.com/stablyai/orca/issues/2050'
|
||||
})
|
||||
|
||||
expect(resolution.workspaceName).toBe('fix-issue-2050')
|
||||
expect(resolution.displayName).toBe('Fix Issue 2050')
|
||||
expect(resolution.workspaceName).toBe('make-create-feel-instant')
|
||||
expect(resolution.displayName).toBe('Make create feel instant')
|
||||
expect(resolution.linkedIssueNumber).toBe(2050)
|
||||
expect(resolution.linkedPR).toBeNull()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { GitHubWorkItem } from '../../../shared/types'
|
||||
import { getWorkspaceIntentName } from '../../../shared/workspace-name'
|
||||
import { getLinkedWorkItemWorkspaceName } from '../../../shared/workspace-name'
|
||||
import type { LinkedWorkItemSummary } from './new-workspace'
|
||||
import { parseGitHubIssueOrPRLink } from './github-links'
|
||||
|
||||
|
|
@ -179,15 +179,11 @@ export function getSmartGitHubSubmitLookupCacheSizeForTests(): number {
|
|||
}
|
||||
|
||||
export function getSmartGitHubSubmitResolution(
|
||||
item: Pick<GitHubWorkItem, 'number' | 'title' | 'type' | 'url'>,
|
||||
options: { sourceText?: string } = {}
|
||||
item: Pick<GitHubWorkItem, 'number' | 'title' | 'type' | 'url'>
|
||||
): SmartGitHubSubmitResolution {
|
||||
const fallbackName = `${item.type}-${item.number}`
|
||||
const intentName = getWorkspaceIntentName({
|
||||
sourceText: options.sourceText,
|
||||
workItem: item
|
||||
})
|
||||
const workspaceName = intentName?.seedName || fallbackName
|
||||
const titleName = getLinkedWorkItemWorkspaceName(item)
|
||||
const workspaceName = titleName?.seedName || fallbackName
|
||||
const linkedWorkItem: LinkedWorkItemSummary = {
|
||||
type: item.type,
|
||||
number: item.number,
|
||||
|
|
@ -197,7 +193,7 @@ export function getSmartGitHubSubmitResolution(
|
|||
|
||||
return {
|
||||
workspaceName,
|
||||
displayName: intentName?.displayName ?? fallbackName,
|
||||
displayName: titleName?.displayName ?? fallbackName,
|
||||
linkedWorkItem,
|
||||
linkedIssueNumber: item.type === 'issue' ? item.number : null,
|
||||
linkedPR: item.type === 'pr' ? item.number : null
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
|
|||
import {
|
||||
getLinearIssueWorkspaceName,
|
||||
getLinkedWorkItemSuggestedName,
|
||||
getLinkedWorkItemWorkspaceName,
|
||||
getWorkspaceIntentName,
|
||||
resolveWorkspaceCreateName,
|
||||
slugifyForWorkspaceName
|
||||
|
|
@ -35,6 +36,36 @@ describe('getLinkedWorkItemSuggestedName', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('getLinkedWorkItemWorkspaceName', () => {
|
||||
it('uses the resolved GitHub title instead of the source URL or provider number', () => {
|
||||
expect(
|
||||
getLinkedWorkItemWorkspaceName({
|
||||
type: 'pr',
|
||||
number: 2049,
|
||||
title: 'Fix pasted URL workspace names'
|
||||
})
|
||||
).toEqual({
|
||||
displayName: 'Fix pasted URL workspace names',
|
||||
seedName: 'fix-pasted-url-workspace-names'
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps external provider identifiers without duplicating title prefixes', () => {
|
||||
expect(
|
||||
getLinkedWorkItemWorkspaceName({
|
||||
type: 'issue',
|
||||
provider: 'jira',
|
||||
number: 0,
|
||||
title: 'PROJ-7 Fix flaky import',
|
||||
jiraIdentifier: 'PROJ-7'
|
||||
})
|
||||
).toEqual({
|
||||
displayName: 'PROJ-7 Fix flaky import',
|
||||
seedName: 'proj-7-fix-flaky-import'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getWorkspaceIntentName', () => {
|
||||
it('uses explicit user intent for linked issues without copying long titles', () => {
|
||||
expect(
|
||||
|
|
|
|||
|
|
@ -33,14 +33,7 @@ export function slugifyForWorkspaceName(input: string): string {
|
|||
}
|
||||
|
||||
export function getLinkedWorkItemSuggestedName(item: { title: string }): string {
|
||||
const withoutLeadingNumber = item.title
|
||||
.trim()
|
||||
.replace(/^(?:issue|pr|pull request)\s*#?\d+\s*[:-]\s*/i, '')
|
||||
.replace(/^#\d+\s*[:-]\s*/, '')
|
||||
.replace(/\(#\d+\)/gi, '')
|
||||
.replace(/\b#\d+\b/g, '')
|
||||
.trim()
|
||||
const seed = withoutLeadingNumber || item.title.trim()
|
||||
const seed = getLinkedWorkItemTitleSubject(item) || item.title.trim()
|
||||
return slugifyForWorkspaceName(seed)
|
||||
}
|
||||
|
||||
|
|
@ -58,6 +51,16 @@ export type WorkspaceIntentName = {
|
|||
seedName: string
|
||||
}
|
||||
|
||||
function getLinkedWorkItemTitleSubject(item: { title: string }): string {
|
||||
return item.title
|
||||
.trim()
|
||||
.replace(/^(?:issue|pr|pull request|mr|merge request)\s*[#!]?\d+\s*[:-]\s*/i, '')
|
||||
.replace(/^#\d+\s*[:-]\s*/, '')
|
||||
.replace(/\([#!]?\d+\)/g, '')
|
||||
.replace(/\b#\d+\b/g, '')
|
||||
.trim()
|
||||
}
|
||||
|
||||
// Why: generated workspace seeds are hyphenated; `issue-123-fix-title`
|
||||
// must not be reinterpreted as the user explicitly asking to fix a new issue.
|
||||
const ACTION_LABELS: [RegExp, string][] = [
|
||||
|
|
@ -169,6 +172,24 @@ function workItemIdentity(item: WorkspaceIntentWorkItem): string {
|
|||
return `Issue ${item.number}`
|
||||
}
|
||||
|
||||
export function getLinkedWorkItemWorkspaceName(
|
||||
item: WorkspaceIntentWorkItem
|
||||
): WorkspaceIntentName | null {
|
||||
const identifier = item.linearIdentifier ?? item.jiraIdentifier
|
||||
let subject = getLinkedWorkItemTitleSubject(item) || item.title.trim()
|
||||
if (identifier) {
|
||||
subject = subject
|
||||
.replace(new RegExp(`^${escapeRegExp(identifier)}\\s*[:-]?\\s*`, 'i'), '')
|
||||
.trim()
|
||||
}
|
||||
const displayName = [identifier, subject].filter(Boolean).join(' ') || workItemIdentity(item)
|
||||
const seedName = slugifyForWorkspaceName(displayName)
|
||||
if (!seedName) {
|
||||
return null
|
||||
}
|
||||
return { displayName, seedName }
|
||||
}
|
||||
|
||||
function defaultActionForWorkItem(item: WorkspaceIntentWorkItem): string | null {
|
||||
return item.type === 'pr' || item.type === 'mr' ? 'Review' : null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ test.describe('Create Workspace', () => {
|
|||
}) => {
|
||||
const title = `E2E smart URL resolution ${Date.now()}`
|
||||
const url = 'https://github.com/stablyai/orca/pull/2049'
|
||||
const linkedWorkspacePattern = /Review PR 2049/
|
||||
const linkedWorkspacePattern = new RegExp(title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
||||
|
||||
try {
|
||||
await orcaPage.getByRole('button', { name: 'New workspace', exact: true }).click()
|
||||
|
|
@ -298,6 +298,7 @@ test.describe('Create Workspace', () => {
|
|||
await expect(orcaPage.getByRole('option', { name: linkedWorkspacePattern })).toBeVisible({
|
||||
timeout: 10_000
|
||||
})
|
||||
await expect(orcaPage.getByRole('option', { name: url })).toHaveCount(0)
|
||||
await expect(orcaPage.getByText('Linked PR #2049')).toBeVisible()
|
||||
await expect
|
||||
.poll(() =>
|
||||
|
|
|
|||
Loading…
Reference in New Issue