feat(workspaces): derive readable emoji identifiers

This commit is contained in:
Neil 2026-07-27 20:05:54 -07:00
parent 84b335f80c
commit 038fd7a50c
8 changed files with 104 additions and 15 deletions

View File

@ -195,6 +195,7 @@
"electron-builder-squirrel-windows": "^26.15.3",
"electron-vite": "^5.0.0",
"emoji-picker-react": "^4.19.1",
"emojibase-data": "17.0.0",
"happy-dom": "^20.9.0",
"html-to-image": "^1.11.13",
"husky": "^9.1.7",

View File

@ -252,6 +252,9 @@ importers:
emoji-picker-react:
specifier: ^4.19.1
version: 4.19.1(react@19.2.7)
emojibase-data:
specifier: 17.0.0
version: 17.0.0(emojibase@17.0.0)
happy-dom:
specifier: ^20.9.0
version: 20.9.0
@ -3986,6 +3989,15 @@ packages:
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
emojibase-data@17.0.0:
resolution: {integrity: sha512-Yvgb5AWoHViHV/gq1qr5ZAarcBip+B27/ZLRsUJkbgAEaLlZ/fof9g882LTpmEpyhBNEC0m2SEmItljHsTygjA==}
peerDependencies:
emojibase: '*'
emojibase@17.0.0:
resolution: {integrity: sha512-bXdpf4HPY3p41zK5swVKZdC/VynsMZ4LoLxdYDE+GucqkFwzcM1GVc4ODfYAlwoKaf2U2oNNUoOO78N96ovpBA==}
engines: {node: '>=18.12.0'}
encodeurl@2.0.0:
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
engines: {node: '>= 0.8'}
@ -10031,6 +10043,12 @@ snapshots:
emoji-regex@8.0.0: {}
emojibase-data@17.0.0(emojibase@17.0.0):
dependencies:
emojibase: 17.0.0
emojibase@17.0.0: {}
encodeurl@2.0.0: {}
end-of-stream@1.4.5:

View File

@ -74,15 +74,19 @@ describe('sanitizeWorktreeName', () => {
expect(sanitizeWorktreeName('feat: 中文 (v2)')).toBe('feat-中文-v2')
})
it('uses a git-safe fallback when a name contains only emoji', () => {
expect(sanitizeWorktreeName('🚀')).toBe('workspace')
expect(sanitizeWorktreeName('👩‍💻✨')).toBe('workspace')
expect(sanitizeWorktreeName('🇯🇵')).toBe('workspace')
expect(sanitizeWorktreeName('1⃣')).toBe('1')
it('uses readable git-safe shortcodes for known emoji', () => {
expect(sanitizeWorktreeName('🚀')).toBe('rocket')
expect(sanitizeWorktreeName('👩‍💻✨')).toBe('woman-technologist-sparkles')
expect(sanitizeWorktreeName('🇯🇵')).toBe('jp')
expect(sanitizeWorktreeName('1⃣')).toBe('one')
})
it('keeps readable text while removing emoji from branch and path names', () => {
expect(sanitizeWorktreeName('Ship it 🚀')).toBe('Ship-it')
it('keeps readable text and emoji shortcodes in branch and path names', () => {
expect(sanitizeWorktreeName('Ship it 🚀')).toBe('Ship-it-rocket')
})
it('uses a git-safe fallback for emoji newer than the shortcode catalog', () => {
expect(sanitizeWorktreeName('\u{1fae9}')).toBe('workspace')
})
it('does not treat arbitrary punctuation as a workspace name', () => {

View File

@ -3,6 +3,7 @@ import type { GlobalSettings, OrcaWorkspaceLayout, Repo } from '../../shared/typ
import { isWindowsAbsolutePathLike, resolveRuntimePath } from '../../shared/cross-platform-path'
import { isWslUncPath } from '../../shared/wsl-paths'
import { splitWorktreeId } from '../../shared/worktree-id'
import { replaceKnownEmojiWithShortcodes } from '../../shared/emoji-shortcode-catalog'
import { getWslHome, parseWslPath } from '../wsl'
type WorktreePathSettings = Pick<GlobalSettings, 'nestWorkspaces' | 'workspaceDir'>
@ -25,7 +26,7 @@ export function sanitizeWorktreeName(input: string): string {
// name workspaces in their own language. Git ref-format permits non-ASCII
// bytes, and modern filesystems handle UTF-8 paths. Only strip characters
// git or the filesystem actually rejects.
const sanitized = input
const sanitized = replaceKnownEmojiWithShortcodes(input)
.trim()
.replace(/[^\p{L}\p{N}._-]+/gu, '-')
.replace(/-+/g, '-')

View File

@ -899,9 +899,9 @@ describe('registerWorktreeHandlers', () => {
it('keeps an emoji-only display name while using safe branch and path names', async () => {
listWorktreesMock.mockResolvedValue([
{
path: '/workspace/workspace',
path: '/workspace/rocket',
head: 'abc123',
branch: 'workspace',
branch: 'rocket',
isBare: false,
isMainWorktree: false
}
@ -914,13 +914,13 @@ describe('registerWorktreeHandlers', () => {
expect(addWorktreeMock).toHaveBeenCalledWith(
'/workspace/repo',
'/workspace/workspace',
'workspace',
'/workspace/rocket',
'rocket',
'origin/main',
false
)
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/workspace',
'repo-1::/workspace/rocket',
expect.objectContaining({ displayName: '🚀' })
)
})

View File

@ -86,6 +86,23 @@ describe('worktree-palette-search', () => {
])
})
it('finds an emoji-named workspace by its readable branch shortcode', () => {
const results = searchWorktrees(
[makeWorktree({ displayName: '🚀', branch: 'refs/heads/rocket' })],
'rocket',
repoMap,
null,
null
)
expect(results).toHaveLength(1)
expect(results[0]).toMatchObject({
worktreeId: 'wt-1',
matchedField: 'branch',
branchRange: { start: 0, end: 6 }
})
})
it('rejects oversized pasted queries before reading worktree metadata', () => {
const oversizedQuery = 'secret-worktree-palette-search'.repeat(WORKTREE_PALETTE_QUERY_MAX_BYTES)
const worktree = {

View File

@ -0,0 +1,49 @@
import emojiShortcodes from 'emojibase-data/en/shortcodes/github.json'
export type StandardEmojiShortcodeEntry = {
emoji: string
shortcode: string
}
export const STANDARD_EMOJI_SHORTCODE_ENTRIES: readonly StandardEmojiShortcodeEntry[] =
Object.entries(emojiShortcodes).flatMap(([hexcode, value]) => {
const shortcodes = typeof value === 'string' ? [value] : value
const emoji = hexcodeToEmoji(hexcode)
return shortcodes.map((shortcode) => ({ emoji, shortcode }))
})
const PRIMARY_SHORTCODE_BY_EMOJI = new Map(
Object.entries(emojiShortcodes).map(([hexcode, value]) => {
const shortcodes = typeof value === 'string' ? [value] : value
const shortcode = shortcodes.find((candidate) => /^[a-z]/i.test(candidate)) ?? shortcodes[0]
return [normalizeEmojiLookup(hexcodeToEmoji(hexcode)), shortcode]
})
)
const EMOJI_SEGMENTER = new Intl.Segmenter('en', { granularity: 'grapheme' })
export function replaceKnownEmojiWithShortcodes(input: string): string {
return Array.from(EMOJI_SEGMENTER.segment(input), ({ segment }) => {
const shortcode = PRIMARY_SHORTCODE_BY_EMOJI.get(normalizeEmojiLookup(segment))
return shortcode ? ` ${shortcode.replaceAll('_', '-')} ` : segment
}).join('')
}
function normalizeEmojiLookup(emoji: string): string {
return Array.from(emoji)
.filter((character) => {
const codepoint = character.codePointAt(0)
return (
character !== '\ufe0f' &&
(codepoint === undefined || codepoint < 0x1f3fb || codepoint > 0x1f3ff)
)
})
.join('')
}
function hexcodeToEmoji(hexcode: string): string {
return hexcode
.split('-')
.map((codepoint) => String.fromCodePoint(Number.parseInt(codepoint, 16)))
.join('')
}

View File

@ -178,8 +178,7 @@ test.describe('Create Workspace', () => {
const worktrees = Object.values(window.__store!.getState().worktreesByRepo).flat()
return worktrees.find((worktree) => worktree.displayName === displayName)?.branch ?? null
}, workspaceName)
expect(branch).not.toBeNull()
expect(branch).not.toMatch(/[\p{Emoji_Presentation}\p{Extended_Pictographic}]/u)
expect(branch).toBe('refs/heads/rocket-test-tube-sparkles')
} finally {
await orcaPage
.evaluate(() => {