Files
orca/src/shared/display-name-from-work.ts
T
b18ec39e1f fix(naming): lead workspace and tab names with the work-item identifier (#8238)
* fix(naming): lead workspace and tab names with the work-item identifier

Auto-generated workspace names, branch-rename display names, and tab
titles now lead with the referenced PR/MR/issue/ticket — e.g.
`PR 1033 - Review` instead of a paraphrase like "Review community pr
1094" that buried or dropped the number. Identifiers are the highest-
signal, most searchable token, so leading with them makes the sidebar
and tabs scannable and disambiguates same-verb work items.

- New shared `work-item-reference.ts` extracts the identifier from the
  raw prompt: URLs are validated by path structure (owner/repo/pull/N,
  GitLab's `/-/` marker) so GitHub Enterprise / self-hosted GitLab still
  resolve while stray `/pull/<n>` paths (CDN, docs) do not; a ticket-
  prefix denylist keeps `SHA-256` / `UTF-8` / `ISO-8601` from being read
  as Jira/Linear keys.
- Reconciles the existing create-from-work-item naming (was action-first
  `Review PR 1033`) with the first-work auto-rename onto one identifier-
  first format via a shared `formatIdentifierFirst`, so the two paths
  can't drift.
- Fixes a pre-existing tab-title bug where markdown punctuation was
  stripped before URLs, splitting a GitLab `merge_requests` URL at its
  underscore and leaking "requests" into the title.

* fix(naming): keep emphasis-wrapped URLs intact in generated names

The URL/markdown strip reorder left the tab-title URL strip anchored on
\b, which fails when a URL is wrapped in markdown emphasis (_...pull/5_)
and leaked URL fragments into the title. Drop the \b anchor and trim
trailing markdown emphasis (*_~) in the URL identifier parser, keeping
interior underscores (merge_requests) intact. Adds regression tests.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-11 14:14:12 -07:00

77 lines
2.7 KiB
TypeScript

import { humanizeBranchSlug } from './branch-name-from-work'
import { extractWorkIdentifier, formatIdentifierFirst } from './work-item-reference'
// Why: the git branch must stay lowercase kebab-case, but the sidebar display
// name is free-form. When the first prompt references a review target, that
// identifier is the highest-signal, most searchable token — so we lead with it
// (`PR 1033 - Review`) instead of a paraphrased slug like "Review community pr
// 1033". The identifier comes from the raw prompt (via work-item-reference),
// where the number still exists, not from the lossy branch slug.
// Type words that are never a useful action verb, skipped even if a slug leads
// with one (e.g. `pr-1094-review` → `Review`, not `Pr`).
const ACTION_STOPWORDS = new Set([
'pr',
'mr',
'pull',
'merge',
'request',
'requests',
'issue',
'issues',
'the',
'a',
'an',
'to',
'for',
'of'
])
// The first slug word that is neither a digit nor an identifier/type token,
// capitalized — the task verb ("Review", "Fix"). '' when none remains.
function leadingActionWord(slug: string, identifierTokens: string[]): string {
const skip = new Set([...identifierTokens, ...ACTION_STOPWORDS])
for (const word of slug.split('-').filter(Boolean)) {
if (/^\d+$/.test(word) || skip.has(word)) {
continue
}
return word.charAt(0).toUpperCase() + word.slice(1)
}
return ''
}
// A `-N` collision suffix (`fix-auth` → `fix-auth-2`) that resolveUniqueBranchName
// appended, so two worktrees on the same identifier stay distinguishable.
function collisionSuffixFromLeaf(
baseSlug: string,
resolvedLeaf: string | undefined
): number | null {
if (!resolvedLeaf || resolvedLeaf === baseSlug || !resolvedLeaf.startsWith(`${baseSlug}-`)) {
return null
}
const rest = resolvedLeaf.slice(baseSlug.length + 1)
return /^\d+$/.test(rest) ? Number(rest) : null
}
/**
* Build the sidebar display name for a freshly auto-renamed workspace. When the
* prompt names a review target, returns `<identifier> - <action>` (e.g.
* `PR 1033 - Review`); otherwise falls back to the plain humanized branch slug.
* `resolvedLeaf` is the final branch leaf (after any collision suffix); omit it
* for folder workspaces that have no git branch.
*/
export function deriveWorkspaceDisplayName(input: {
prompt: string
slug: string
resolvedLeaf?: string
}): string {
const identifier = extractWorkIdentifier(input.prompt)
if (!identifier) {
return humanizeBranchSlug(input.resolvedLeaf ?? input.slug)
}
const action = leadingActionWord(input.slug, identifier.tokens)
const base = formatIdentifierFirst(identifier.label, action)
const suffix = collisionSuffixFromLeaf(input.slug, input.resolvedLeaf)
return suffix ? `${base} (${suffix})` : base
}