mirror of
https://github.com/stablyai/orca.git
synced 2026-10-01 16:02:03 +00:00
fix(renderer): restore worktree palette behavior after split
(cherry picked from commit 0b78c1cbbc)
This commit is contained in:
@@ -11,6 +11,7 @@ import { useWorktreeJumpPaletteListEntries } from './use-worktree-jump-palette-l
|
||||
import { useWorktreeJumpPaletteSelectionLifecycle } from './use-worktree-jump-palette-selection-lifecycle'
|
||||
import { useWorktreeJumpPaletteSelectionActions } from './use-worktree-jump-palette-selection-actions'
|
||||
import { useWorktreeJumpPaletteCreateAction } from './use-worktree-jump-palette-create-action'
|
||||
import { useWorktreeJumpPaletteTaskUrl } from './use-worktree-jump-palette-task-url'
|
||||
import { useWorkspaceEmojiShortcodeInput } from '@/components/workspace-emoji/useWorkspaceEmojiShortcodeInput'
|
||||
|
||||
export function useWorktreeJumpPaletteController({
|
||||
@@ -23,7 +24,13 @@ export function useWorktreeJumpPaletteController({
|
||||
createLookupGuard: WorktreePaletteRequestGuard
|
||||
}) {
|
||||
const storeState = useWorktreeJumpPaletteStoreState({ visible, lingering })
|
||||
const localState = useWorktreeJumpPaletteLocalState({ createLookupGuard })
|
||||
const localState = useWorktreeJumpPaletteLocalState({ createLookupGuard, visible })
|
||||
const taskUrl = useWorktreeJumpPaletteTaskUrl({
|
||||
visible,
|
||||
createWorktreeName: localState.createWorktreeName,
|
||||
taskSourceUrl: localState.taskSourceUrl,
|
||||
createLookupGuard
|
||||
})
|
||||
const filter = useWorktreeJumpPaletteFilter({ ...storeState, ...localState })
|
||||
const worktrees = useWorktreeJumpPaletteWorktrees({
|
||||
...storeState,
|
||||
@@ -63,13 +70,15 @@ export function useWorktreeJumpPaletteController({
|
||||
...openTabs,
|
||||
...recentTabs,
|
||||
...projectTargets,
|
||||
...quickActions
|
||||
...quickActions,
|
||||
...taskUrl
|
||||
})
|
||||
const listEntries = useWorktreeJumpPaletteListEntries({
|
||||
...localState,
|
||||
...worktrees,
|
||||
...openTabs,
|
||||
...sections
|
||||
...sections,
|
||||
...taskUrl
|
||||
})
|
||||
const selectionLifecycle = useWorktreeJumpPaletteSelectionLifecycle({
|
||||
...storeState,
|
||||
@@ -81,7 +90,8 @@ export function useWorktreeJumpPaletteController({
|
||||
...projectTargets,
|
||||
...quickActions,
|
||||
...sections,
|
||||
...listEntries
|
||||
...listEntries,
|
||||
...taskUrl
|
||||
})
|
||||
const selectionActions = useWorktreeJumpPaletteSelectionActions({
|
||||
...storeState,
|
||||
@@ -102,12 +112,14 @@ export function useWorktreeJumpPaletteController({
|
||||
...quickActions,
|
||||
...sections,
|
||||
...selectionLifecycle,
|
||||
...selectionActions
|
||||
...selectionActions,
|
||||
...taskUrl
|
||||
})
|
||||
|
||||
return {
|
||||
...storeState,
|
||||
...localState,
|
||||
...taskUrl,
|
||||
...filter,
|
||||
...worktrees,
|
||||
...openTabs,
|
||||
|
||||
@@ -12,7 +12,13 @@ import { subscribeCmdJRowIndexJump } from '@/lib/cmd-j-row-index-jump'
|
||||
import { getRepoMapFromState } from '@/store/selectors'
|
||||
import { useAppStore } from '@/store'
|
||||
import { isGitRepoKind } from '../../../shared/repo-kind'
|
||||
import { buildTaskSourceContextFromRepo } from '../../../shared/task-source-context'
|
||||
import {
|
||||
buildTaskSourceContextFromRepo,
|
||||
normalizeTaskSourceContext
|
||||
} from '../../../shared/task-source-context'
|
||||
import { getLinearIssueWorkspaceName } from '../../../shared/workspace-name'
|
||||
import { buildLinearIssueLinkedWorkItem } from '@/lib/linear-linked-work-item'
|
||||
import { isWorktreePaletteCreateActivationAllowed } from '@/lib/worktree-palette-create-action'
|
||||
import type { WorktreeJumpPaletteFilter } from './use-worktree-jump-palette-filter'
|
||||
import type { WorktreeJumpPaletteLocalState } from './use-worktree-jump-palette-local-state'
|
||||
import type { WorktreeJumpPaletteQuickActions } from './use-worktree-jump-palette-quick-actions'
|
||||
@@ -21,6 +27,7 @@ import type { WorktreeJumpPaletteSelectionActions } from './use-worktree-jump-pa
|
||||
import type { WorktreeJumpPaletteSelectionLifecycle } from './use-worktree-jump-palette-selection-lifecycle'
|
||||
import type { WorktreeJumpPaletteStoreState } from './use-worktree-jump-palette-store-state'
|
||||
import type { WorktreeJumpPaletteWorktrees } from './use-worktree-jump-palette-worktrees'
|
||||
import type { WorktreeJumpPaletteTaskUrl } from './use-worktree-jump-palette-task-url'
|
||||
|
||||
type WorktreeJumpPaletteCreateActionInput = WorktreeJumpPaletteStoreState &
|
||||
WorktreeJumpPaletteLocalState &
|
||||
@@ -29,6 +36,7 @@ type WorktreeJumpPaletteCreateActionInput = WorktreeJumpPaletteStoreState &
|
||||
WorktreeJumpPaletteSections &
|
||||
WorktreeJumpPaletteSelectionActions &
|
||||
WorktreeJumpPaletteSelectionLifecycle &
|
||||
WorktreeJumpPaletteTaskUrl &
|
||||
Pick<WorktreeJumpPaletteWorktrees, 'hasQuery'>
|
||||
|
||||
export function useWorktreeJumpPaletteCreateAction({
|
||||
@@ -50,30 +58,41 @@ export function useWorktreeJumpPaletteCreateAction({
|
||||
createLookupGuard,
|
||||
preserveCreateLookupOnCloseRef,
|
||||
inputRef,
|
||||
setDialogElement
|
||||
setDialogElement,
|
||||
taskSourceUrl,
|
||||
linearIssueUrlIntent,
|
||||
currentLinearIssuePreview,
|
||||
currentGitHubWorkItemPreview,
|
||||
linearLookupRef,
|
||||
githubLookupRef,
|
||||
taskUrlCreatePreview,
|
||||
selectionMovedByUserRef
|
||||
}: WorktreeJumpPaletteCreateActionInput) {
|
||||
useLayoutEffect(() => {
|
||||
digitShortcutItemsRef.current = paletteSections.visibleOpenTabItems
|
||||
// oxlint-disable-next-line react-hooks/exhaustive-deps -- the controller ref preserves its original stable identity.
|
||||
}, [paletteSections])
|
||||
}, [digitShortcutItemsRef, paletteSections])
|
||||
useEffect(() => {
|
||||
if (!visible || hasQuery || query.length > 0) {
|
||||
return
|
||||
}
|
||||
if (!visible || hasQuery || query.length > 0) return
|
||||
return subscribeCmdJRowIndexJump((index) => {
|
||||
const item = digitShortcutItemsRef.current[index]
|
||||
if (item) {
|
||||
handleSelectItem(item)
|
||||
}
|
||||
if (item) handleSelectItem(item)
|
||||
})
|
||||
// oxlint-disable-next-line react-hooks/exhaustive-deps -- the controller ref preserves its original stable identity.
|
||||
}, [handleSelectItem, hasQuery, query.length, visible])
|
||||
}, [digitShortcutItemsRef, handleSelectItem, hasQuery, query.length, visible])
|
||||
|
||||
const handleCreateWorktree = useCallback(() => {
|
||||
skipRestoreFocusRef.current = true
|
||||
const trimmed = createWorktreeName.trim()
|
||||
if (query.trim() !== trimmed) return
|
||||
if (
|
||||
!isWorktreePaletteCreateActivationAllowed({
|
||||
hasTaskUrlIntent: taskSourceUrl !== null,
|
||||
hasCreateName: trimmed.length > 0,
|
||||
selectionMovedByUser: selectionMovedByUserRef.current
|
||||
})
|
||||
) return
|
||||
const ghLink = parseGitHubIssueOrPRLink(trimmed)
|
||||
const ghNumber = parseGitHubIssueOrPRNumber(trimmed)
|
||||
const openComposer = (data: Record<string, unknown>): void => {
|
||||
skipRestoreFocusRef.current = true
|
||||
prefetchCreateWorkspaceBaseForComposer(
|
||||
typeof data.initialRepoId === 'string' ? data.initialRepoId : undefined
|
||||
)
|
||||
@@ -83,35 +102,108 @@ export function useWorktreeJumpPaletteCreateAction({
|
||||
openModal('new-workspace-composer', { ...data, telemetrySource: 'command_palette' })
|
||||
)
|
||||
}
|
||||
if (ghLink) {
|
||||
const { number } = ghLink
|
||||
const state = useAppStore.getState()
|
||||
const matches = allWorktrees.filter(
|
||||
(worktree) =>
|
||||
!worktree.isArchived && (worktree.linkedIssue === number || worktree.linkedPR === number)
|
||||
)
|
||||
const activeMatch =
|
||||
matches.find((worktree) => worktree.repoId === state.activeRepoId) ?? matches[0]
|
||||
if (activeMatch) {
|
||||
closeModal()
|
||||
const activation = activateAndRevealWorktree(activeMatch.id)
|
||||
if (!queueWorkspaceActivationTerminalFocus(activeMatch.id, activation)) {
|
||||
focusFallbackSurface()
|
||||
|
||||
if (linearIssueUrlIntent) {
|
||||
const lookup = linearLookupRef.current
|
||||
const resolve = async (): Promise<void> => {
|
||||
const preview = currentLinearIssuePreview?.loading
|
||||
? await lookup?.promise
|
||||
: (currentLinearIssuePreview ?? (await lookup?.promise))
|
||||
if (
|
||||
!preview ||
|
||||
lookup?.query !== trimmed ||
|
||||
linearLookupRef.current !== lookup ||
|
||||
query.trim() !== trimmed ||
|
||||
useAppStore.getState().activeModal !== 'worktree-palette'
|
||||
) {
|
||||
return
|
||||
}
|
||||
recordFeatureInteraction('cmd-j-workspace-open')
|
||||
return
|
||||
const data = preview.issue
|
||||
? (() => {
|
||||
const sourceContext = preview.sourceContext
|
||||
? normalizeTaskSourceContext({
|
||||
...preview.sourceContext,
|
||||
providerIdentity: {
|
||||
provider: 'linear',
|
||||
workspaceId: preview.issue.workspaceId ?? null,
|
||||
workspaceName: preview.issue.workspaceName ?? null,
|
||||
teamId: preview.issue.team.id,
|
||||
teamKey: preview.issue.team.key
|
||||
},
|
||||
accountLabel: preview.issue.workspaceName ?? null
|
||||
})
|
||||
: null
|
||||
return {
|
||||
prefilledName: getLinearIssueWorkspaceName(preview.issue),
|
||||
linkedWorkItem: buildLinearIssueLinkedWorkItem(preview.issue),
|
||||
...(preview.initialRepoId ? { initialRepoId: preview.initialRepoId } : {}),
|
||||
...(sourceContext ? { taskSourceContext: sourceContext } : {})
|
||||
}
|
||||
})()
|
||||
: preview.initialRepoId
|
||||
? { prefilledName: trimmed, initialRepoId: preview.initialRepoId }
|
||||
: { prefilledName: trimmed }
|
||||
openComposer(data)
|
||||
}
|
||||
const eligibleRepos = state.repos.filter((repo) => isGitRepoKind(repo))
|
||||
const repoForLookup =
|
||||
(state.activeRepoId && eligibleRepos.find((repo) => repo.id === state.activeRepoId)) ||
|
||||
eligibleRepos[0]
|
||||
openComposer(
|
||||
repoForLookup
|
||||
? { prefilledName: trimmed, initialRepoId: repoForLookup.id }
|
||||
: { prefilledName: trimmed }
|
||||
)
|
||||
void resolve()
|
||||
return
|
||||
}
|
||||
|
||||
if (ghLink) {
|
||||
const lookup = githubLookupRef.current
|
||||
const resolve = async (): Promise<void> => {
|
||||
const preview = currentGitHubWorkItemPreview?.loading
|
||||
? await lookup?.promise
|
||||
: (currentGitHubWorkItemPreview ?? (await lookup?.promise))
|
||||
if (
|
||||
!preview ||
|
||||
lookup?.query !== trimmed ||
|
||||
githubLookupRef.current !== lookup ||
|
||||
query.trim() !== trimmed ||
|
||||
useAppStore.getState().activeModal !== 'worktree-palette'
|
||||
) {
|
||||
return
|
||||
}
|
||||
const item = preview.item
|
||||
if (item) {
|
||||
const linkedWorkItem: LinkedWorkItemSummary = {
|
||||
provider: 'github',
|
||||
type: item.type,
|
||||
number: item.number,
|
||||
title: item.title,
|
||||
url: item.url,
|
||||
...(item.repoId ? { repoId: item.repoId } : {})
|
||||
}
|
||||
openComposer({
|
||||
prefilledName:
|
||||
getLinkedWorkItemWorkspaceName(linkedWorkItem)?.seedName ??
|
||||
getLinkedWorkItemSuggestedName({ title: item.title }),
|
||||
linkedWorkItem,
|
||||
initialGitHubWorkItem: item,
|
||||
...(preview.initialRepoId ? { initialRepoId: preview.initialRepoId } : {}),
|
||||
...(preview.sourceContext ? { taskSourceContext: preview.sourceContext } : {})
|
||||
})
|
||||
} else {
|
||||
openComposer({
|
||||
prefilledName: trimmed,
|
||||
...(preview.initialRepoId ? { initialRepoId: preview.initialRepoId } : {})
|
||||
})
|
||||
}
|
||||
}
|
||||
void resolve()
|
||||
return
|
||||
}
|
||||
|
||||
if (taskUrlCreatePreview) {
|
||||
const state = useAppStore.getState()
|
||||
const eligibleRepos = state.repos.filter((repo) => isGitRepoKind(repo))
|
||||
const repo =
|
||||
(state.activeRepoId && eligibleRepos.find((candidate) => candidate.id === state.activeRepoId)) ||
|
||||
eligibleRepos[0]
|
||||
openComposer(repo ? { prefilledName: trimmed, initialRepoId: repo.id } : { prefilledName: trimmed })
|
||||
return
|
||||
}
|
||||
|
||||
if (ghNumber !== null) {
|
||||
const state = useAppStore.getState()
|
||||
const matches = allWorktrees.filter(
|
||||
@@ -122,97 +214,104 @@ export function useWorktreeJumpPaletteCreateAction({
|
||||
const activeMatch =
|
||||
matches.find((worktree) => worktree.repoId === state.activeRepoId) ?? matches[0]
|
||||
if (activeMatch) {
|
||||
skipRestoreFocusRef.current = true
|
||||
closeModal()
|
||||
const activation = activateAndRevealWorktree(activeMatch.id)
|
||||
const activation = activateAndRevealWorktree(
|
||||
activeMatch.id,
|
||||
activeMatch.hostId ? { executionHostId: activeMatch.hostId } : {}
|
||||
)
|
||||
if (!queueWorkspaceActivationTerminalFocus(activeMatch.id, activation)) {
|
||||
focusFallbackSurface()
|
||||
}
|
||||
recordFeatureInteraction('cmd-j-workspace-open')
|
||||
return
|
||||
}
|
||||
const repoForLookup =
|
||||
const repo =
|
||||
(state.activeRepoId ? (repoMap.get(state.activeRepoId) ?? null) : null) ||
|
||||
[...getRepoMapFromState(state).values()].find((repo) => isGitRepoKind(repo))
|
||||
if (!repoForLookup || !isGitRepoKind(repoForLookup)) {
|
||||
[...getRepoMapFromState(state).values()].find((candidate) => isGitRepoKind(candidate))
|
||||
if (!repo || !isGitRepoKind(repo)) {
|
||||
openComposer({ prefilledName: trimmed })
|
||||
return
|
||||
}
|
||||
prefetchCreateWorkspaceBaseForComposer(repoForLookup.id)
|
||||
const sourceContext = buildTaskSourceContextFromRepo({
|
||||
provider: 'github',
|
||||
projectId: repoForLookup.id,
|
||||
repo: repoForLookup
|
||||
projectId: repo.id,
|
||||
repo
|
||||
})
|
||||
const lookupToken = createLookupGuard.start()
|
||||
const token = createLookupGuard.start()
|
||||
preserveCreateLookupOnCloseRef.current = true
|
||||
recordFeatureInteraction('cmd-j-create-workspace')
|
||||
skipRestoreFocusRef.current = true
|
||||
closeModal()
|
||||
void lookupGitHubWorkItemForSource({
|
||||
repoPath: repoForLookup.path,
|
||||
repoId: repoForLookup.id,
|
||||
repoPath: repo.path,
|
||||
repoId: repo.id,
|
||||
sourceContext,
|
||||
number: ghNumber
|
||||
})
|
||||
.then((item) => {
|
||||
if (!createLookupGuard.isCurrent(lookupToken)) {
|
||||
return
|
||||
}
|
||||
const data: Record<string, unknown> = { initialRepoId: repoForLookup.id }
|
||||
if (item) {
|
||||
const linkedWorkItem: LinkedWorkItemSummary = {
|
||||
type: item.type,
|
||||
number: item.number,
|
||||
title: item.title,
|
||||
url: item.url
|
||||
}
|
||||
data.linkedWorkItem = linkedWorkItem
|
||||
data.prefilledName =
|
||||
getLinkedWorkItemWorkspaceName(linkedWorkItem)?.seedName ??
|
||||
getLinkedWorkItemSuggestedName({ title: item.title })
|
||||
} else {
|
||||
data.prefilledName = trimmed
|
||||
}
|
||||
queueMicrotask(() =>
|
||||
openModal('new-workspace-composer', { ...data, telemetrySource: 'command_palette' })
|
||||
)
|
||||
})
|
||||
.catch(() => {
|
||||
if (!createLookupGuard.isCurrent(lookupToken)) {
|
||||
return
|
||||
}
|
||||
if (!createLookupGuard.isCurrent(token)) return
|
||||
const linkedWorkItem = item
|
||||
? { type: item.type, number: item.number, title: item.title, url: item.url }
|
||||
: null
|
||||
queueMicrotask(() =>
|
||||
openModal('new-workspace-composer', {
|
||||
initialRepoId: repoForLookup.id,
|
||||
prefilledName: trimmed,
|
||||
initialRepoId: repo.id,
|
||||
...(linkedWorkItem
|
||||
? {
|
||||
linkedWorkItem,
|
||||
prefilledName:
|
||||
getLinkedWorkItemWorkspaceName(linkedWorkItem)?.seedName ??
|
||||
getLinkedWorkItemSuggestedName({ title: linkedWorkItem.title })
|
||||
}
|
||||
: { prefilledName: trimmed }),
|
||||
telemetrySource: 'command_palette'
|
||||
})
|
||||
)
|
||||
})
|
||||
.catch(() => {
|
||||
if (createLookupGuard.isCurrent(token)) {
|
||||
queueMicrotask(() =>
|
||||
openModal('new-workspace-composer', {
|
||||
initialRepoId: repo.id,
|
||||
prefilledName: trimmed,
|
||||
telemetrySource: 'command_palette'
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
openComposer(trimmed ? { prefilledName: trimmed } : {})
|
||||
// oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs preserve their original stable identities.
|
||||
}, [
|
||||
allWorktrees,
|
||||
closeModal,
|
||||
createLookupGuard,
|
||||
createWorktreeName,
|
||||
currentGitHubWorkItemPreview,
|
||||
currentLinearIssuePreview,
|
||||
focusFallbackSurface,
|
||||
githubLookupRef,
|
||||
linearIssueUrlIntent,
|
||||
linearLookupRef,
|
||||
openModal,
|
||||
prefetchCreateWorkspaceBaseForComposer,
|
||||
query,
|
||||
recordFeatureInteraction,
|
||||
repoMap
|
||||
repoMap,
|
||||
selectionMovedByUserRef,
|
||||
skipRestoreFocusRef,
|
||||
taskSourceUrl,
|
||||
taskUrlCreatePreview
|
||||
])
|
||||
|
||||
const handleCloseAutoFocus = useCallback((event: Event) => event.preventDefault(), [])
|
||||
// oxlint-disable-next-line react-hooks/exhaustive-deps -- the controller ref preserves its original stable identity.
|
||||
const focusPaletteInput = useCallback(() => inputRef.current?.focus(), [])
|
||||
const setDialogElementFromNode = useCallback((node: HTMLDivElement | null) => {
|
||||
setDialogElement(node?.closest<HTMLElement>('[role="dialog"]') ?? null)
|
||||
// oxlint-disable-next-line react-hooks/exhaustive-deps -- local-state setter identity is stable across extraction.
|
||||
}, [])
|
||||
const handleOpenAutoFocus = useCallback((_event: Event) => {
|
||||
// No-op: focus is captured before Radix moves it into the dialog.
|
||||
}, [])
|
||||
const focusPaletteInput = useCallback(() => inputRef.current?.focus(), [inputRef])
|
||||
const setDialogElementFromNode = useCallback(
|
||||
(node: HTMLDivElement | null) =>
|
||||
setDialogElement(node?.closest<HTMLElement>('[role="dialog"]') ?? null),
|
||||
[setDialogElement]
|
||||
)
|
||||
const handleOpenAutoFocus = useCallback((_event: Event) => {}, [])
|
||||
return {
|
||||
handleCreateWorktree,
|
||||
handleCloseAutoFocus,
|
||||
@@ -222,4 +321,6 @@ export function useWorktreeJumpPaletteCreateAction({
|
||||
}
|
||||
}
|
||||
|
||||
export type WorktreeJumpPaletteCreateAction = ReturnType<typeof useWorktreeJumpPaletteCreateAction>
|
||||
export type WorktreeJumpPaletteCreateAction = ReturnType<
|
||||
typeof useWorktreeJumpPaletteCreateAction
|
||||
>
|
||||
|
||||
@@ -10,23 +10,25 @@ import {
|
||||
type PaletteItem,
|
||||
type PaletteListEntry
|
||||
} from './worktree-jump-palette-model'
|
||||
import type { WorktreeJumpPaletteLocalState } from './use-worktree-jump-palette-local-state'
|
||||
import type { WorktreeJumpPaletteOpenTabs } from './use-worktree-jump-palette-open-tabs'
|
||||
import { buildPaletteListEntryRenderKeys } from '@/components/cmd-j/palette-list-entry-render-keys'
|
||||
import type { WorktreeJumpPaletteSections } from './use-worktree-jump-palette-sections'
|
||||
import type { WorktreeJumpPaletteWorktrees } from './use-worktree-jump-palette-worktrees'
|
||||
import type { WorktreeJumpPaletteLocalState } from './use-worktree-jump-palette-local-state'
|
||||
|
||||
type WorktreeJumpPaletteListEntriesInput = WorktreeJumpPaletteSections &
|
||||
Pick<WorktreeJumpPaletteWorktrees, 'hasQuery'> &
|
||||
Pick<WorktreeJumpPaletteOpenTabs, 'worktreeItems'> &
|
||||
Pick<WorktreeJumpPaletteLocalState, 'autoSelectedItemIdRef'>
|
||||
Pick<WorktreeJumpPaletteLocalState, 'autoSelectedItemIdRef' | 'taskSourceUrl'> &
|
||||
Pick<WorktreeJumpPaletteSections, 'middleLeadsSections' | 'handleExpandSection'>
|
||||
|
||||
export function useWorktreeJumpPaletteListEntries({
|
||||
hasQuery,
|
||||
openTabsLeadSections,
|
||||
paletteSections,
|
||||
showCreateAction,
|
||||
worktreeItems,
|
||||
autoSelectedItemIdRef
|
||||
autoSelectedItemIdRef,
|
||||
taskSourceUrl,
|
||||
handleExpandSection,
|
||||
middleLeadsSections
|
||||
}: WorktreeJumpPaletteListEntriesInput) {
|
||||
const listEntries = useMemo<PaletteListEntry[]>(() => {
|
||||
const entries: PaletteListEntry[] = []
|
||||
@@ -35,7 +37,6 @@ export function useWorktreeJumpPaletteListEntries({
|
||||
visibleProjectTargetItems,
|
||||
visibleMiddleItems,
|
||||
visibleOpenTabItems,
|
||||
showWorktreeHint,
|
||||
worktreeOverflowCount,
|
||||
projectTargetOverflowCount,
|
||||
middleOverflowCount,
|
||||
@@ -43,16 +44,19 @@ export function useWorktreeJumpPaletteListEntries({
|
||||
multiPrimaryFirstScreen,
|
||||
multiPrimaryLayout
|
||||
} = paletteSections
|
||||
const pushOverflowHint = (id: string, overflowCount: number): void => {
|
||||
const pushOverflowHint = (
|
||||
id: string,
|
||||
overflowCount: number,
|
||||
onSeeMore?: () => void
|
||||
): void => {
|
||||
if (overflowCount > 0) {
|
||||
entries.push({
|
||||
id,
|
||||
type: 'hint',
|
||||
label: translate(
|
||||
'worktreeJumpPalette.renderCapOverflow',
|
||||
'{{value0}} more — scroll or keep typing to narrow',
|
||||
{ value0: overflowCount }
|
||||
)
|
||||
label: translate('worktreeJumpPalette.renderCapOverflow', '{{value0}} more', {
|
||||
value0: overflowCount
|
||||
}),
|
||||
onSeeMore
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -96,18 +100,9 @@ export function useWorktreeJumpPaletteListEntries({
|
||||
}
|
||||
pushWorktreesHeader()
|
||||
appendPaletteListEntries(entries, visibleWorktreeItems)
|
||||
if (showWorktreeHint) {
|
||||
entries.push({
|
||||
id: '__hint_worktree_cap__',
|
||||
type: 'hint',
|
||||
label: translate(
|
||||
'auto.components.WorktreeJumpPalette.dabd819ca1',
|
||||
'Type to see all {{value0}} worktrees',
|
||||
{ value0: worktreeItems.length }
|
||||
)
|
||||
})
|
||||
}
|
||||
pushOverflowHint('__hint_worktree_overflow__', worktreeOverflowCount)
|
||||
pushOverflowHint('__hint_worktree_overflow__', worktreeOverflowCount, () =>
|
||||
handleExpandSection('worktrees')
|
||||
)
|
||||
}
|
||||
const pushOpenTabSection = (): void => {
|
||||
if (visibleOpenTabItems.length === 0) {
|
||||
@@ -115,7 +110,9 @@ export function useWorktreeJumpPaletteListEntries({
|
||||
}
|
||||
pushOpenTabsHeader()
|
||||
appendPaletteListEntries(entries, visibleOpenTabItems)
|
||||
pushOverflowHint('__hint_open_tab_overflow__', openTabOverflowCount)
|
||||
pushOverflowHint('__hint_open_tab_overflow__', openTabOverflowCount, () =>
|
||||
handleExpandSection('open-tabs')
|
||||
)
|
||||
}
|
||||
const pushProjectAndMiddleSections = (): void => {
|
||||
if (visibleProjectTargetItems.length > 0) {
|
||||
@@ -130,7 +127,9 @@ export function useWorktreeJumpPaletteListEntries({
|
||||
})
|
||||
}
|
||||
appendPaletteListEntries(entries, visibleProjectTargetItems)
|
||||
pushOverflowHint('__hint_project_overflow__', projectTargetOverflowCount)
|
||||
pushOverflowHint('__hint_project_overflow__', projectTargetOverflowCount, () =>
|
||||
handleExpandSection('projects')
|
||||
)
|
||||
}
|
||||
if (visibleMiddleItems.length > 0) {
|
||||
if (showMiddleHeader) {
|
||||
@@ -141,15 +140,30 @@ export function useWorktreeJumpPaletteListEntries({
|
||||
})
|
||||
}
|
||||
appendPaletteListEntries(entries, visibleMiddleItems)
|
||||
pushOverflowHint('__hint_middle_overflow__', middleOverflowCount)
|
||||
pushOverflowHint('__hint_middle_overflow__', middleOverflowCount, () =>
|
||||
handleExpandSection('middle')
|
||||
)
|
||||
}
|
||||
}
|
||||
// Why: a pasted issue/PR URL is decisive. Show linked worktrees first so
|
||||
// Enter jumps; keep create available underneath when the user wants a new one.
|
||||
if (taskSourceUrl) {
|
||||
if (visibleWorktreeItems.length > 0) {
|
||||
pushWorktreeSection()
|
||||
}
|
||||
if (showCreateAction) {
|
||||
entries.push({ id: CREATE_WORKTREE_ITEM_ID, type: 'create-worktree' })
|
||||
}
|
||||
return entries
|
||||
}
|
||||
if (!hasQuery) {
|
||||
pushOpenTabSection()
|
||||
pushWorktreeSection()
|
||||
return entries
|
||||
}
|
||||
if (multiPrimaryFirstScreen && multiPrimaryLayout) {
|
||||
const leadingSectionKey = openTabsLeadSections ? 'open-tabs' : 'worktrees'
|
||||
const trailingSectionKey = openTabsLeadSections ? 'worktrees' : 'open-tabs'
|
||||
const leadingHintId = openTabsLeadSections
|
||||
? '__hint_open_tab_overflow__'
|
||||
: '__hint_worktree_overflow__'
|
||||
@@ -172,13 +186,18 @@ export function useWorktreeJumpPaletteListEntries({
|
||||
}
|
||||
pushLeadingHeader()
|
||||
appendPaletteListEntries(entries, multiPrimaryLayout.leadingPreview as PaletteItem[])
|
||||
pushOverflowHint(leadingHintId, multiPrimaryLayout.leadingMoreCount)
|
||||
pushOverflowHint(leadingHintId, multiPrimaryLayout.leadingMoreCount, () =>
|
||||
handleExpandSection(openTabsLeadSections ? 'open-tabs' : 'worktrees')
|
||||
)
|
||||
pushTrailingHeader()
|
||||
appendPaletteListEntries(entries, multiPrimaryLayout.trailingFloor as PaletteItem[])
|
||||
const hasLeadingRest = multiPrimaryLayout.leadingRest.length > 0
|
||||
if (hasLeadingRest) {
|
||||
pushLeadingHeader(CONTINUED_SECTION_HEADER_ID_SUFFIX)
|
||||
appendPaletteListEntries(entries, multiPrimaryLayout.leadingRest as PaletteItem[])
|
||||
pushOverflowHint(`${leadingHintId}_tail`, multiPrimaryLayout.leadingHardOverflowCount, () =>
|
||||
handleExpandSection(leadingSectionKey)
|
||||
)
|
||||
}
|
||||
if (multiPrimaryLayout.trailingRest.length > 0) {
|
||||
if (hasLeadingRest) {
|
||||
@@ -186,18 +205,25 @@ export function useWorktreeJumpPaletteListEntries({
|
||||
}
|
||||
appendPaletteListEntries(entries, multiPrimaryLayout.trailingRest as PaletteItem[])
|
||||
}
|
||||
pushOverflowHint(trailingHintId, multiPrimaryLayout.trailingHardOverflowCount)
|
||||
pushOverflowHint(trailingHintId, multiPrimaryLayout.trailingHardOverflowCount, () =>
|
||||
handleExpandSection(trailingSectionKey)
|
||||
)
|
||||
pushProjectAndMiddleSections()
|
||||
if (showCreateAction) {
|
||||
entries.push({ id: CREATE_WORKTREE_ITEM_ID, type: 'create-worktree' })
|
||||
}
|
||||
return entries
|
||||
}
|
||||
if (middleLeadsSections) {
|
||||
pushProjectAndMiddleSections()
|
||||
}
|
||||
if (openTabsLeadSections) {
|
||||
pushOpenTabSection()
|
||||
}
|
||||
pushWorktreeSection()
|
||||
pushProjectAndMiddleSections()
|
||||
if (!middleLeadsSections) {
|
||||
pushProjectAndMiddleSections()
|
||||
}
|
||||
if (!openTabsLeadSections) {
|
||||
pushOpenTabSection()
|
||||
}
|
||||
@@ -205,7 +231,16 @@ export function useWorktreeJumpPaletteListEntries({
|
||||
entries.push({ id: CREATE_WORKTREE_ITEM_ID, type: 'create-worktree' })
|
||||
}
|
||||
return entries
|
||||
}, [hasQuery, openTabsLeadSections, paletteSections, showCreateAction, worktreeItems.length])
|
||||
}, [
|
||||
handleExpandSection,
|
||||
hasQuery,
|
||||
middleLeadsSections,
|
||||
openTabsLeadSections,
|
||||
paletteSections,
|
||||
showCreateAction,
|
||||
taskSourceUrl,
|
||||
|
||||
])
|
||||
const selectableItems = useMemo<PaletteItem[]>(
|
||||
() =>
|
||||
listEntries.filter(
|
||||
@@ -216,15 +251,19 @@ export function useWorktreeJumpPaletteListEntries({
|
||||
),
|
||||
[listEntries]
|
||||
)
|
||||
const selectionItemIds = useMemo(
|
||||
() => getWorktreePaletteSelectionItemIds(listEntries),
|
||||
const listEntryRenderKeys = useMemo(
|
||||
() => buildPaletteListEntryRenderKeys(listEntries.map((entry) => entry.id)),
|
||||
[listEntries]
|
||||
)
|
||||
const selectionItemIds = useMemo(
|
||||
() => getWorktreePaletteSelectionItemIds(listEntries, listEntryRenderKeys),
|
||||
[listEntries, listEntryRenderKeys]
|
||||
)
|
||||
useEffect(() => {
|
||||
autoSelectedItemIdRef.current = selectionItemIds[0] ?? null
|
||||
// oxlint-disable-next-line react-hooks/exhaustive-deps -- the controller ref preserves its original stable identity.
|
||||
}, [selectionItemIds])
|
||||
return { listEntries, selectableItems, selectionItemIds }
|
||||
return { listEntries, listEntryRenderKeys, selectableItems, selectionItemIds }
|
||||
}
|
||||
|
||||
export type WorktreeJumpPaletteListEntries = ReturnType<typeof useWorktreeJumpPaletteListEntries>
|
||||
|
||||
@@ -1,20 +1,39 @@
|
||||
import { useDeferredValue, useRef, useState } from 'react'
|
||||
import { useDeferredValue, useMemo, useRef, useState } from 'react'
|
||||
import type { WorktreePaletteRequestGuard } from '@/lib/worktree-palette-create-action'
|
||||
import { EMPTY_PALETTE_FILTER, type PaletteFilterState } from '@/components/cmd-j/palette-filter'
|
||||
import {
|
||||
EMPTY_PALETTE_FILTER,
|
||||
type PaletteFilterState
|
||||
} from '@/components/cmd-j/palette-filter'
|
||||
import { parseCmdJTaskSourceUrl } from '@/lib/worktree-palette-task-url-match'
|
||||
import { getWorktreePaletteCreateActionState } from '@/lib/worktree-palette-create-action'
|
||||
import type { CmdJActiveGroupSnapshot } from '@/components/cmd-j/quick-action-context'
|
||||
import type { WorkspaceVisibleTabType } from '../../../shared/tab-types'
|
||||
import type { PaletteItem } from './worktree-jump-palette-model'
|
||||
|
||||
export function useWorktreeJumpPaletteLocalState({
|
||||
createLookupGuard
|
||||
createLookupGuard,
|
||||
visible
|
||||
}: {
|
||||
createLookupGuard: WorktreePaletteRequestGuard
|
||||
visible: boolean
|
||||
}) {
|
||||
const [query, setQuery] = useState('')
|
||||
const deferredQuery = useDeferredValue(query)
|
||||
const liveQueryRef = useRef(query)
|
||||
liveQueryRef.current = query
|
||||
const taskSourceUrl = useMemo(() => parseCmdJTaskSourceUrl(query), [query])
|
||||
const paletteSearchQuery = taskSourceUrl ? query.trim() : deferredQuery.trim()
|
||||
const deferredCreateAction = useMemo(
|
||||
() => getWorktreePaletteCreateActionState({ query: deferredQuery }),
|
||||
[deferredQuery]
|
||||
)
|
||||
const createWorktreeName = taskSourceUrl ? query.trim() : deferredCreateAction.createWorktreeName
|
||||
const showCreateAction = deferredCreateAction.showCreateAction || taskSourceUrl !== null
|
||||
const [selectedItemId, setSelectedItemId] = useState('')
|
||||
const latestQueryRef = useRef('')
|
||||
const autoSelectedItemIdRef = useRef<string | null>(null)
|
||||
// Create is armed by an explicit keyboard/pointer move, except for task URLs.
|
||||
const selectionMovedByUserRef = useRef(false)
|
||||
const digitShortcutItemsRef = useRef<readonly PaletteItem[]>([])
|
||||
const [rawFilter, setRawFilter] = useState<PaletteFilterState>(EMPTY_PALETTE_FILTER)
|
||||
const [dialogElement, setDialogElement] = useState<HTMLElement | null>(null)
|
||||
@@ -31,15 +50,31 @@ export function useWorktreeJumpPaletteLocalState({
|
||||
const fallbackFocusOuterFrameRef = useRef<number | null>(null)
|
||||
const fallbackFocusInnerFrameRef = useRef<number | null>(null)
|
||||
const preserveCreateLookupOnCloseRef = useRef(false)
|
||||
const [expandedSectionCaps, setExpandedSectionCaps] = useState<Record<string, number>>({})
|
||||
|
||||
// Reset expansion after a new query or a fresh open without adding an extra effect render.
|
||||
const [previousQuery, setPreviousQuery] = useState(query)
|
||||
const [previousVisible, setPreviousVisible] = useState(visible)
|
||||
if (previousQuery !== query || previousVisible !== visible) {
|
||||
setPreviousQuery(query)
|
||||
setPreviousVisible(visible)
|
||||
setExpandedSectionCaps({})
|
||||
}
|
||||
|
||||
return {
|
||||
query,
|
||||
setQuery,
|
||||
deferredQuery,
|
||||
liveQueryRef,
|
||||
taskSourceUrl,
|
||||
paletteSearchQuery,
|
||||
createWorktreeName,
|
||||
showCreateAction,
|
||||
selectedItemId,
|
||||
setSelectedItemId,
|
||||
latestQueryRef,
|
||||
autoSelectedItemIdRef,
|
||||
selectionMovedByUserRef,
|
||||
digitShortcutItemsRef,
|
||||
rawFilter,
|
||||
setRawFilter,
|
||||
@@ -58,7 +93,10 @@ export function useWorktreeJumpPaletteLocalState({
|
||||
fallbackFocusOuterFrameRef,
|
||||
fallbackFocusInnerFrameRef,
|
||||
createLookupGuard,
|
||||
preserveCreateLookupOnCloseRef
|
||||
preserveCreateLookupOnCloseRef,
|
||||
expandedSectionCaps,
|
||||
setExpandedSectionCaps,
|
||||
previousVisible
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,11 +11,8 @@ import {
|
||||
searchWorkspaceTabs,
|
||||
type SearchableWorkspaceTab
|
||||
} from '@/lib/workspace-tab-palette-search'
|
||||
import {
|
||||
getOpenTabMatchRelevance,
|
||||
getWorktreeMatchRelevance,
|
||||
NO_MATCH_RELEVANCE
|
||||
} from '@/lib/cmd-j-match-relevance'
|
||||
import { comparePaletteRankedItems } from '@/lib/cmd-j-section-leadership'
|
||||
import { getWorktreeHostIdentity } from '../../../shared/worktree/host-qualified-identity'
|
||||
import type {
|
||||
BrowserPaletteItem,
|
||||
OpenTabPaletteItem,
|
||||
@@ -34,18 +31,21 @@ const EMPTY_WORKSPACE_TAB_ENTRIES: SearchableWorkspaceTab[] = []
|
||||
|
||||
type WorktreeJumpPaletteOpenTabsInput = WorktreeJumpPaletteStoreState &
|
||||
WorktreeJumpPaletteWorktrees &
|
||||
Pick<WorktreeJumpPaletteFilter, 'repoMap'> &
|
||||
Pick<WorktreeJumpPaletteFilter, 'repoMap' | 'repoByHostIdentity'> &
|
||||
Pick<WorktreeJumpPaletteLocalState, 'deferredQuery'>
|
||||
|
||||
export function useWorktreeJumpPaletteOpenTabs({
|
||||
paletteStatusInputsActive,
|
||||
browserSortedWorktrees,
|
||||
allWorktrees,
|
||||
repoMap,
|
||||
repoByHostIdentity,
|
||||
worktreeOrder,
|
||||
browserTabsByWorktree,
|
||||
browserPagesByWorkspace,
|
||||
activeBrowserTabId,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
activeTabType,
|
||||
unifiedTabsByWorktree,
|
||||
activeGroupIdByWorktree,
|
||||
@@ -61,10 +61,12 @@ export function useWorktreeJumpPaletteOpenTabs({
|
||||
activeFileIdByWorktree,
|
||||
activeTabTypeByWorktree,
|
||||
settings,
|
||||
terminalLayoutsByTabId,
|
||||
paneForegroundAgentByPaneKey,
|
||||
deferredQuery,
|
||||
hasQuery,
|
||||
worktreeMatches,
|
||||
worktreeMap
|
||||
resolveWorktree
|
||||
}: WorktreeJumpPaletteOpenTabsInput) {
|
||||
const browserPageEntries = useMemo<SearchableBrowserPage[]>(() => {
|
||||
if (!paletteStatusInputsActive) {
|
||||
@@ -72,12 +74,15 @@ export function useWorktreeJumpPaletteOpenTabs({
|
||||
}
|
||||
return buildSearchableBrowserPages({
|
||||
worktrees: browserSortedWorktrees,
|
||||
ownershipWorktrees: allWorktrees,
|
||||
repoMap,
|
||||
repoMapByHostIdentity: repoByHostIdentity,
|
||||
worktreeOrder,
|
||||
browserTabsByWorktree,
|
||||
browserPagesByWorkspace,
|
||||
activeBrowserTabId,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
activeTabType
|
||||
})
|
||||
}, [
|
||||
@@ -85,9 +90,12 @@ export function useWorktreeJumpPaletteOpenTabs({
|
||||
activeBrowserTabId,
|
||||
activeTabType,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
allWorktrees,
|
||||
browserPagesByWorkspace,
|
||||
browserTabsByWorktree,
|
||||
browserSortedWorktrees,
|
||||
repoByHostIdentity,
|
||||
repoMap,
|
||||
worktreeOrder
|
||||
])
|
||||
@@ -101,12 +109,15 @@ export function useWorktreeJumpPaletteOpenTabs({
|
||||
}
|
||||
return buildSearchableSimulatorTabs({
|
||||
worktrees: browserSortedWorktrees,
|
||||
ownershipWorktrees: allWorktrees,
|
||||
repoMap,
|
||||
repoMapByHostIdentity: repoByHostIdentity,
|
||||
worktreeOrder,
|
||||
unifiedTabsByWorktree,
|
||||
activeGroupIdByWorktree,
|
||||
groupsByWorktree,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
activeTabType
|
||||
})
|
||||
}, [
|
||||
@@ -114,8 +125,11 @@ export function useWorktreeJumpPaletteOpenTabs({
|
||||
activeGroupIdByWorktree,
|
||||
activeTabType,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
allWorktrees,
|
||||
browserSortedWorktrees,
|
||||
groupsByWorktree,
|
||||
repoByHostIdentity,
|
||||
repoMap,
|
||||
unifiedTabsByWorktree,
|
||||
worktreeOrder
|
||||
@@ -130,7 +144,9 @@ export function useWorktreeJumpPaletteOpenTabs({
|
||||
}
|
||||
return buildSearchableWorkspaceTabs({
|
||||
worktrees: browserSortedWorktrees,
|
||||
ownershipWorktrees: allWorktrees,
|
||||
repoMap,
|
||||
repoMapByHostIdentity: repoByHostIdentity,
|
||||
worktreeOrder,
|
||||
unifiedTabsByWorktree,
|
||||
tabsByWorktree,
|
||||
@@ -141,13 +157,16 @@ export function useWorktreeJumpPaletteOpenTabs({
|
||||
activeGroupIdByWorktree,
|
||||
groupsByWorktree,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
activeTabType,
|
||||
activeTabId,
|
||||
activeTabIdByWorktree,
|
||||
activeFileId,
|
||||
activeFileIdByWorktree,
|
||||
activeTabTypeByWorktree,
|
||||
generatedTitlesEnabled: settings?.tabAutoGenerateTitle === true
|
||||
generatedTitlesEnabled: settings?.tabAutoGenerateTitle === true,
|
||||
terminalLayoutsByTabId,
|
||||
paneForegroundAgentByPaneKey
|
||||
})
|
||||
}, [
|
||||
paletteStatusInputsActive,
|
||||
@@ -159,15 +178,20 @@ export function useWorktreeJumpPaletteOpenTabs({
|
||||
activeTabType,
|
||||
activeTabTypeByWorktree,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
allWorktrees,
|
||||
agentStatusByPaneKey,
|
||||
browserSortedWorktrees,
|
||||
groupsByWorktree,
|
||||
openFiles,
|
||||
repoMap,
|
||||
repoByHostIdentity,
|
||||
retainedAgentsByPaneKey,
|
||||
settings?.tabAutoGenerateTitle,
|
||||
sleepingAgentSessionsByPaneKey,
|
||||
paneForegroundAgentByPaneKey,
|
||||
tabsByWorktree,
|
||||
terminalLayoutsByTabId,
|
||||
unifiedTabsByWorktree,
|
||||
worktreeOrder
|
||||
])
|
||||
@@ -175,30 +199,10 @@ export function useWorktreeJumpPaletteOpenTabs({
|
||||
() => searchWorkspaceTabs(workspaceTabEntries, deferredQuery.trim()),
|
||||
[workspaceTabEntries, deferredQuery]
|
||||
)
|
||||
const worktreeRelevanceById = useMemo(() => {
|
||||
const relevanceById = new Map<string, number>()
|
||||
if (!hasQuery) {
|
||||
return relevanceById
|
||||
}
|
||||
for (const match of worktreeMatches) {
|
||||
const worktree = worktreeMap.get(match.worktreeId)
|
||||
if (worktree) {
|
||||
relevanceById.set(
|
||||
match.worktreeId,
|
||||
getWorktreeMatchRelevance(
|
||||
match,
|
||||
worktree,
|
||||
repoMap.get(worktree.repoId)?.displayName ?? ''
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
return relevanceById
|
||||
}, [hasQuery, repoMap, worktreeMap, worktreeMatches])
|
||||
const worktreeItems = useMemo<WorktreePaletteItem[]>(() => {
|
||||
const items = worktreeMatches
|
||||
.map((match) => {
|
||||
const worktree = worktreeMap.get(match.worktreeId)
|
||||
const worktree = resolveWorktree(match.worktreeId, match.worktreeHostId)
|
||||
return worktree
|
||||
? { id: `worktree:${worktree.id}`, type: 'worktree' as const, match, worktree }
|
||||
: null
|
||||
@@ -207,12 +211,24 @@ export function useWorktreeJumpPaletteOpenTabs({
|
||||
if (!hasQuery) {
|
||||
return items
|
||||
}
|
||||
return items.sort(
|
||||
(left, right) =>
|
||||
(worktreeRelevanceById.get(left.worktree.id) ?? NO_MATCH_RELEVANCE) -
|
||||
(worktreeRelevanceById.get(right.worktree.id) ?? NO_MATCH_RELEVANCE)
|
||||
const orderByIdentity = new Map(
|
||||
items.map((item, index) => [getWorktreeHostIdentity(item.worktree), index])
|
||||
)
|
||||
}, [hasQuery, worktreeMap, worktreeMatches, worktreeRelevanceById])
|
||||
return items.sort((left, right) =>
|
||||
comparePaletteRankedItems(
|
||||
{
|
||||
rank: left.match.rank,
|
||||
order: orderByIdentity.get(getWorktreeHostIdentity(left.worktree)) ?? 0,
|
||||
id: left.id
|
||||
},
|
||||
{
|
||||
rank: right.match.rank,
|
||||
order: orderByIdentity.get(getWorktreeHostIdentity(right.worktree)) ?? 0,
|
||||
id: right.id
|
||||
}
|
||||
)
|
||||
)
|
||||
}, [hasQuery, resolveWorktree, worktreeMatches])
|
||||
const browserItems = useMemo<BrowserPaletteItem[]>(
|
||||
() =>
|
||||
browserMatches.map((result) => ({
|
||||
@@ -242,28 +258,28 @@ export function useWorktreeJumpPaletteOpenTabs({
|
||||
)
|
||||
const openTabItems = useMemo<OpenTabPaletteItem[]>(() => {
|
||||
const items = [...browserItems, ...simulatorItems, ...workspaceTabItems]
|
||||
const relevanceById = new Map(
|
||||
items.map((item) => [item.id, getOpenTabMatchRelevance(item.result)])
|
||||
return items.sort((left, right) =>
|
||||
comparePaletteRankedItems(
|
||||
{
|
||||
rank: left.result.rank,
|
||||
order: left.result.score,
|
||||
id: left.id,
|
||||
lastActiveAt: left.result.lastActiveAt ?? undefined
|
||||
},
|
||||
{
|
||||
rank: right.result.rank,
|
||||
order: right.result.score,
|
||||
id: right.id,
|
||||
lastActiveAt: right.result.lastActiveAt ?? undefined
|
||||
}
|
||||
)
|
||||
)
|
||||
return items.sort((left, right) => {
|
||||
const relevance =
|
||||
(relevanceById.get(left.id) ?? NO_MATCH_RELEVANCE) -
|
||||
(relevanceById.get(right.id) ?? NO_MATCH_RELEVANCE)
|
||||
if (relevance !== 0) {
|
||||
return relevance
|
||||
}
|
||||
if (left.result.score !== right.result.score) {
|
||||
return left.result.score - right.result.score
|
||||
}
|
||||
return left.id.localeCompare(right.id)
|
||||
})
|
||||
}, [browserItems, simulatorItems, workspaceTabItems])
|
||||
|
||||
return {
|
||||
browserPageEntries,
|
||||
simulatorTabEntries,
|
||||
workspaceTabEntries,
|
||||
worktreeRelevanceById,
|
||||
worktreeItems,
|
||||
browserItems,
|
||||
simulatorItems,
|
||||
|
||||
@@ -28,6 +28,7 @@ export function useWorktreeJumpPaletteProjectTargets({
|
||||
repos,
|
||||
worktreesByRepo,
|
||||
detectedWorktreesByRepo,
|
||||
worktreeVisibilityDefaultsByHost,
|
||||
pendingWorktreeCreations,
|
||||
projectGroups,
|
||||
projects,
|
||||
@@ -63,8 +64,9 @@ export function useWorktreeJumpPaletteProjectTargets({
|
||||
}
|
||||
}
|
||||
for (const repoId of buildImportedWorktreesCardCandidates({
|
||||
repos,
|
||||
detectedWorktreesByRepo
|
||||
repos,
|
||||
detectedWorktreesByRepo,
|
||||
visibilityDefaultsByHost: worktreeVisibilityDefaultsByHost
|
||||
}).keys()) {
|
||||
ids.add(repoId)
|
||||
}
|
||||
@@ -72,7 +74,14 @@ export function useWorktreeJumpPaletteProjectTargets({
|
||||
ids.add(creation.request.repoId)
|
||||
}
|
||||
return ids
|
||||
}, [allWorktrees, detectedWorktreesByRepo, pendingWorktreeCreations, repos, worktreesByRepo])
|
||||
}, [
|
||||
allWorktrees,
|
||||
detectedWorktreesByRepo,
|
||||
pendingWorktreeCreations,
|
||||
repos,
|
||||
worktreeVisibilityDefaultsByHost,
|
||||
worktreesByRepo
|
||||
])
|
||||
const hasAnyProjectSearchCandidates = useMemo(
|
||||
() =>
|
||||
hasCmdJProjectSearchCandidates({
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { rankCmdJMiddleResults } from '@/components/cmd-j/palette-results'
|
||||
import {
|
||||
getComposerEligibleRepos,
|
||||
resolveComposerActiveRepoId,
|
||||
resolveComposerGitRepoId
|
||||
} from '@/lib/new-workspace-composer-repo'
|
||||
import type { QuickActionPaletteItem, SettingsPaletteItem } from './worktree-jump-palette-model'
|
||||
@@ -24,7 +25,7 @@ function getComposerPrefetchRepoId(
|
||||
return resolveComposerGitRepoId({
|
||||
eligibleRepos: getComposerEligibleRepos(state.repos),
|
||||
initialRepoId,
|
||||
activeRepoId: state.activeRepoId,
|
||||
activeRepoId: resolveComposerActiveRepoId(state.repos, getComposerEligibleRepos(state.repos), state.activeRepoId),
|
||||
focusedHostScope: state.workspaceHostScope
|
||||
})
|
||||
}
|
||||
@@ -71,11 +72,20 @@ export function useWorktreeJumpPaletteQuickActions({
|
||||
)
|
||||
}, [openModal, prefetchCreateWorkspaceBaseForComposer])
|
||||
const deleteActiveWorkspaceAction = useCallback(() => {
|
||||
const { activeView: currentView, activeWorktreeId: currentWorktreeId } = useAppStore.getState()
|
||||
const {
|
||||
activeView: currentView,
|
||||
activeWorktreeId: currentWorktreeId,
|
||||
activeWorkspaceExecutionHostId
|
||||
} = useAppStore.getState()
|
||||
if (currentView !== 'terminal' || !currentWorktreeId) {
|
||||
return
|
||||
}
|
||||
queueMicrotask(() => runWorktreeDelete(currentWorktreeId))
|
||||
queueMicrotask(() =>
|
||||
runWorktreeDelete(
|
||||
currentWorktreeId,
|
||||
activeWorkspaceExecutionHostId ? { expectedHostId: activeWorkspaceExecutionHostId } : {}
|
||||
)
|
||||
)
|
||||
}, [])
|
||||
const openAddQuickCommandAction = useCallback(() => {
|
||||
openSettingsTarget({ pane: 'quick-commands', repoId: null, intent: 'add-quick-command' })
|
||||
|
||||
@@ -23,10 +23,43 @@ import type { WorktreeJumpPaletteWorktrees } from './use-worktree-jump-palette-w
|
||||
|
||||
type WorktreeJumpPaletteRecentTabsInput = WorktreeJumpPaletteStoreState &
|
||||
WorktreeJumpPaletteOpenTabs &
|
||||
Pick<WorktreeJumpPaletteWorktrees, 'worktreeMap' | 'hasQuery'> &
|
||||
Pick<WorktreeJumpPaletteWorktrees, 'resolveWorktree' | 'hasQuery'> &
|
||||
Pick<WorktreeJumpPaletteFilter, 'filterActive'> &
|
||||
Pick<WorktreeJumpPaletteLocalState, 'query' | 'autoSelectedItemIdRef' | 'setSelectedItemId'>
|
||||
|
||||
function getRecentTabOccurrenceBase(item: OpenTabRecentRow['item']): string {
|
||||
if (item.type === 'browser-page') {
|
||||
const result = item.result
|
||||
return JSON.stringify([
|
||||
item.type,
|
||||
item.id,
|
||||
result.executionHostId ?? '',
|
||||
result.worktreeId,
|
||||
result.workspaceId,
|
||||
result.pageId
|
||||
])
|
||||
}
|
||||
if (item.type === 'simulator-tab') {
|
||||
const result = item.result
|
||||
return JSON.stringify([
|
||||
item.type,
|
||||
item.id,
|
||||
result.executionHostId ?? '',
|
||||
result.worktreeId,
|
||||
result.tabId
|
||||
])
|
||||
}
|
||||
const result = item.result
|
||||
return JSON.stringify([
|
||||
item.type,
|
||||
item.id,
|
||||
result.executionHostId ?? '',
|
||||
result.worktreeId,
|
||||
result.tabId,
|
||||
result.entityId
|
||||
])
|
||||
}
|
||||
|
||||
export function useWorktreeJumpPaletteRecentTabs({
|
||||
tabsByWorktree,
|
||||
agentStatusByPaneKey,
|
||||
@@ -35,7 +68,7 @@ export function useWorktreeJumpPaletteRecentTabs({
|
||||
runtimePaneTitlesByTabId,
|
||||
terminalLayoutsByTabId,
|
||||
openTabItems,
|
||||
worktreeMap,
|
||||
resolveWorktree,
|
||||
unreadTerminalTabs,
|
||||
unreadAgentCompletionPanes,
|
||||
visible,
|
||||
@@ -48,6 +81,15 @@ export function useWorktreeJumpPaletteRecentTabs({
|
||||
autoSelectedItemIdRef,
|
||||
setSelectedItemId
|
||||
}: WorktreeJumpPaletteRecentTabsInput) {
|
||||
const occurrenceIds = useMemo(() => {
|
||||
const counts = new Map<string, number>()
|
||||
return openTabItems.map((item) => {
|
||||
const base = getRecentTabOccurrenceBase(item)
|
||||
const ordinal = counts.get(base) ?? 0
|
||||
counts.set(base, ordinal + 1)
|
||||
return `recent-tab:${base}:${ordinal}`
|
||||
})
|
||||
}, [openTabItems])
|
||||
const terminalTabsById = useMemo(() => {
|
||||
const byId = new Map<string, TerminalTab>()
|
||||
for (const tabs of Object.values(tabsByWorktree)) {
|
||||
@@ -77,17 +119,21 @@ export function useWorktreeJumpPaletteRecentTabs({
|
||||
)
|
||||
const openTabRecentRows = useMemo<OpenTabRecentRow[]>(() => {
|
||||
const entries: OpenTabRecentRow[] = []
|
||||
for (const item of openTabItems) {
|
||||
const worktree = worktreeMap.get(item.result.worktreeId)
|
||||
for (const [index, item] of openTabItems.entries()) {
|
||||
const worktree = resolveWorktree(item.result.worktreeId, item.result.executionHostId)
|
||||
if (!worktree) {
|
||||
continue
|
||||
}
|
||||
const occurrenceId = occurrenceIds[index]!
|
||||
entries.push({
|
||||
item,
|
||||
occurrenceId,
|
||||
worktree,
|
||||
row: {
|
||||
id: item.id,
|
||||
occurrenceId,
|
||||
worktreeId: worktree.id,
|
||||
worktreeHostId: worktree.hostId,
|
||||
unifiedTabId: item.type === 'browser-page' ? null : item.result.tabId,
|
||||
terminalTab:
|
||||
item.type === 'workspace-tab' && item.result.contentType === 'terminal'
|
||||
@@ -98,9 +144,9 @@ export function useWorktreeJumpPaletteRecentTabs({
|
||||
})
|
||||
}
|
||||
return entries
|
||||
}, [openTabItems, terminalTabsById, worktreeMap])
|
||||
const recentTabRowById = useMemo(
|
||||
() => new Map(openTabRecentRows.map(({ row }) => [row.id, row])),
|
||||
}, [occurrenceIds, openTabItems, resolveWorktree, terminalTabsById])
|
||||
const recentTabRowByItem = useMemo(
|
||||
() => new Map(openTabRecentRows.map(({ item, row }) => [item, row])),
|
||||
[openTabRecentRows]
|
||||
)
|
||||
const recentTabRows = useMemo<RecentWorkspaceTabRow[]>(() => {
|
||||
@@ -190,11 +236,13 @@ export function useWorktreeJumpPaletteRecentTabs({
|
||||
visible
|
||||
])
|
||||
const recentTabItems = useMemo<PaletteItem[]>(() => {
|
||||
const itemById = new Map(openTabItems.map((item) => [item.id, item]))
|
||||
return recentTabOrder.flatMap((id) => itemById.get(id) ?? [])
|
||||
}, [openTabItems, recentTabOrder])
|
||||
const itemByOccurrenceId = new Map(
|
||||
openTabRecentRows.map(({ occurrenceId, item }) => [occurrenceId, item])
|
||||
)
|
||||
return recentTabOrder.flatMap((occurrenceId) => itemByOccurrenceId.get(occurrenceId) ?? [])
|
||||
}, [openTabRecentRows, recentTabOrder])
|
||||
|
||||
return { recentTabPaneSources, recentTabRowById, recentTabItems }
|
||||
return { recentTabPaneSources, recentTabRowByItem, recentTabItems, openTabRecentRows }
|
||||
}
|
||||
|
||||
export type WorktreeJumpPaletteRecentTabs = ReturnType<typeof useWorktreeJumpPaletteRecentTabs>
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useShortcutKeyComboDetails } from '@/hooks/useShortcutLabel'
|
||||
import { getOpenTabMatchRelevance, NO_MATCH_RELEVANCE } from '@/lib/cmd-j-match-relevance'
|
||||
import { getWorktreePaletteCreateActionState } from '@/lib/worktree-palette-create-action'
|
||||
import { bestCmdJPaletteSectionQualityClass } from '@/components/cmd-j/palette-results'
|
||||
import {
|
||||
bestPaletteQualityRank,
|
||||
NO_PALETTE_QUALITY_RANK,
|
||||
shouldIntentSectionLeadPaletteSections,
|
||||
shouldOpenTabsLeadPaletteSections
|
||||
} from '@/lib/cmd-j-section-leadership'
|
||||
import {
|
||||
capPaletteSection,
|
||||
layoutMultiPrimaryPaletteSections
|
||||
layoutMultiPrimaryPaletteSections,
|
||||
PALETTE_SECTION_EXPAND_STEP,
|
||||
PALETTE_SECTION_RENDER_CAP,
|
||||
TYPED_QUERY_LEADING_PREVIEW
|
||||
} from '@/components/cmd-j/palette-section-render-cap'
|
||||
import {
|
||||
DIGIT_INDEX_ACTION_ID,
|
||||
@@ -28,56 +36,108 @@ type WorktreeJumpPaletteSectionsInput = WorktreeJumpPaletteOpenTabs &
|
||||
WorktreeJumpPaletteProjectTargets &
|
||||
Pick<WorktreeJumpPaletteQuickActions, 'middleItems'> &
|
||||
Pick<WorktreeJumpPaletteWorktrees, 'hasQuery'> &
|
||||
Pick<WorktreeJumpPaletteLocalState, 'deferredQuery'>
|
||||
Pick<
|
||||
WorktreeJumpPaletteLocalState,
|
||||
'createWorktreeName' | 'showCreateAction' | 'expandedSectionCaps' | 'setExpandedSectionCaps'
|
||||
>
|
||||
|
||||
export function useWorktreeJumpPaletteSections({
|
||||
hasQuery,
|
||||
worktreeItems,
|
||||
worktreeRelevanceById,
|
||||
openTabItems,
|
||||
recentTabItems,
|
||||
projectTargetItems,
|
||||
middleItems,
|
||||
deferredQuery
|
||||
createWorktreeName,
|
||||
showCreateAction,
|
||||
expandedSectionCaps,
|
||||
setExpandedSectionCaps
|
||||
}: WorktreeJumpPaletteSectionsInput) {
|
||||
const openTabsLeadSections = useMemo(() => {
|
||||
if (!hasQuery) {
|
||||
return true
|
||||
}
|
||||
const bestWorktree = worktreeItems[0]
|
||||
const bestWorktreeRelevance = bestWorktree
|
||||
? (worktreeRelevanceById.get(bestWorktree.worktree.id) ?? NO_MATCH_RELEVANCE)
|
||||
: NO_MATCH_RELEVANCE
|
||||
const bestOpenTab = openTabItems[0]
|
||||
const bestOpenTabRelevance = bestOpenTab
|
||||
? getOpenTabMatchRelevance(bestOpenTab.result)
|
||||
: NO_MATCH_RELEVANCE
|
||||
return bestOpenTabRelevance <= bestWorktreeRelevance
|
||||
}, [hasQuery, openTabItems, worktreeItems, worktreeRelevanceById])
|
||||
if (!hasQuery) return true
|
||||
return shouldOpenTabsLeadPaletteSections({
|
||||
bestWorktreeQualityRank: worktreeItems[0]
|
||||
? bestPaletteQualityRank([worktreeItems[0].match.qualityClass])
|
||||
: NO_PALETTE_QUALITY_RANK,
|
||||
bestOpenTabQualityRank: openTabItems[0]
|
||||
? bestPaletteQualityRank([openTabItems[0].result.qualityClass])
|
||||
: NO_PALETTE_QUALITY_RANK
|
||||
})
|
||||
}, [hasQuery, openTabItems, worktreeItems])
|
||||
|
||||
const middleLeadsSections = useMemo(() => {
|
||||
if (!hasQuery) return false
|
||||
const bestEntityQualityRank = Math.min(
|
||||
worktreeItems[0]
|
||||
? bestPaletteQualityRank([worktreeItems[0].match.qualityClass])
|
||||
: NO_PALETTE_QUALITY_RANK,
|
||||
openTabItems[0]
|
||||
? bestPaletteQualityRank([openTabItems[0].result.qualityClass])
|
||||
: NO_PALETTE_QUALITY_RANK
|
||||
)
|
||||
return shouldIntentSectionLeadPaletteSections({
|
||||
bestEntityQualityRank,
|
||||
bestIntentQualityRank: bestPaletteQualityRank([
|
||||
bestCmdJPaletteSectionQualityClass(middleItems.map((item) => item.result)),
|
||||
bestCmdJPaletteSectionQualityClass(projectTargetItems.map((item) => item.result))
|
||||
])
|
||||
})
|
||||
}, [hasQuery, middleItems, openTabItems, projectTargetItems, worktreeItems])
|
||||
|
||||
const handleExpandSection = useCallback(
|
||||
(sectionKey: string) => {
|
||||
setExpandedSectionCaps((previous) => ({
|
||||
...previous,
|
||||
[sectionKey]: (previous[sectionKey] ?? 0) + PALETTE_SECTION_EXPAND_STEP
|
||||
}))
|
||||
},
|
||||
[setExpandedSectionCaps]
|
||||
)
|
||||
|
||||
const paletteSections = useMemo(() => {
|
||||
// Why: the empty-query trim lives here, not in `recentTabItems`, so the trimmed tail becomes an
|
||||
// overflow count the section can offer as "See more" instead of silently vanishing.
|
||||
const openTabsCap = PALETTE_SECTION_RENDER_CAP + (expandedSectionCaps['open-tabs'] ?? 0)
|
||||
// Why: "See more" drops the above-the-fold trim outright instead of stepping 20 at a time, so one
|
||||
// click reveals the whole recent history the shared render cap allows.
|
||||
const recentTabsCap = expandedSectionCaps['open-tabs']
|
||||
? openTabsCap
|
||||
: EMPTY_QUERY_RECENT_TAB_CAP
|
||||
const openTabs = hasQuery
|
||||
? capPaletteSection(openTabItems)
|
||||
: capPaletteSection(recentTabItems, EMPTY_QUERY_RECENT_TAB_CAP)
|
||||
const worktreeCap = hasQuery
|
||||
? capPaletteSection(openTabItems, openTabsCap)
|
||||
: capPaletteSection(recentTabItems, recentTabsCap)
|
||||
const baseWorktreeCap = hasQuery
|
||||
? Infinity
|
||||
: Math.min(
|
||||
openTabs.visible.length === 0 ? EMPTY_QUERY_ROW_BUDGET : EMPTY_QUERY_WORKTREE_CAP,
|
||||
Math.max(1, EMPTY_QUERY_ROW_BUDGET - openTabs.visible.length)
|
||||
)
|
||||
const worktreeCap = hasQuery
|
||||
? PALETTE_SECTION_RENDER_CAP + (expandedSectionCaps.worktrees ?? 0)
|
||||
: baseWorktreeCap + (expandedSectionCaps.worktrees ?? 0)
|
||||
const worktrees = hasQuery
|
||||
? capPaletteSection(worktreeItems)
|
||||
: { visible: worktreeItems.slice(0, worktreeCap), overflowCount: 0 }
|
||||
const projectTargets = capPaletteSection(hasQuery ? projectTargetItems : [])
|
||||
const middle = capPaletteSection(hasQuery ? middleItems : [])
|
||||
const showWorktreeHint = !hasQuery && worktreeItems.length > worktreeCap
|
||||
? capPaletteSection(worktreeItems, worktreeCap)
|
||||
: {
|
||||
visible: worktreeItems.slice(0, worktreeCap),
|
||||
overflowCount: Math.max(0, worktreeItems.length - worktreeCap)
|
||||
}
|
||||
const projectTargets = capPaletteSection(
|
||||
hasQuery ? projectTargetItems : [],
|
||||
PALETTE_SECTION_RENDER_CAP + (expandedSectionCaps.projects ?? 0)
|
||||
)
|
||||
const middle = capPaletteSection(
|
||||
hasQuery ? middleItems : [],
|
||||
PALETTE_SECTION_RENDER_CAP + (expandedSectionCaps.middle ?? 0)
|
||||
)
|
||||
const multiPrimaryFirstScreen =
|
||||
hasQuery && openTabs.visible.length > 0 && worktrees.visible.length > 0
|
||||
const multiPrimaryLayout = multiPrimaryFirstScreen
|
||||
? layoutMultiPrimaryPaletteSections<WorktreePaletteItem | OpenTabPaletteItem>({
|
||||
leadingItems: openTabsLeadSections ? openTabItems : worktreeItems,
|
||||
trailingItems: openTabsLeadSections ? worktreeItems : openTabItems
|
||||
trailingItems: openTabsLeadSections ? worktreeItems : openTabItems,
|
||||
leadingPreviewCount:
|
||||
TYPED_QUERY_LEADING_PREVIEW +
|
||||
(expandedSectionCaps[openTabsLeadSections ? 'open-tabs' : 'worktrees'] ?? 0),
|
||||
leadingHardCap: openTabsLeadSections ? openTabsCap : worktreeCap,
|
||||
trailingHardCap: openTabsLeadSections ? worktreeCap : openTabsCap
|
||||
})
|
||||
: null
|
||||
return {
|
||||
@@ -89,21 +149,33 @@ export function useWorktreeJumpPaletteSections({
|
||||
middleOverflowCount: middle.overflowCount,
|
||||
visibleOpenTabItems: openTabs.visible as PaletteItem[],
|
||||
openTabOverflowCount: openTabs.overflowCount,
|
||||
showWorktreeHint,
|
||||
multiPrimaryFirstScreen,
|
||||
multiPrimaryLayout
|
||||
}
|
||||
}, [
|
||||
worktreeItems,
|
||||
projectTargetItems,
|
||||
expandedSectionCaps,
|
||||
hasQuery,
|
||||
middleItems,
|
||||
openTabItems,
|
||||
openTabsLeadSections,
|
||||
projectTargetItems,
|
||||
recentTabItems,
|
||||
hasQuery,
|
||||
openTabsLeadSections
|
||||
worktreeItems
|
||||
])
|
||||
|
||||
// Why: badges number the snapshotted recent rows only — ⌘N is meaningless on a typed query, and an
|
||||
// expanded section leaves its unaddressable rows unbadged rather than advertising ⌘10.
|
||||
const recentTabShortcutIndexByItem = useMemo(
|
||||
() =>
|
||||
new Map(
|
||||
hasQuery
|
||||
? []
|
||||
: paletteSections.visibleOpenTabItems
|
||||
.slice(0, DIGIT_INDEX_ADDRESSABLE_ROWS)
|
||||
.map((item, index) => [item, index])
|
||||
),
|
||||
[hasQuery, paletteSections]
|
||||
)
|
||||
const recentTabShortcutIndexById = useMemo(
|
||||
() =>
|
||||
new Map(
|
||||
@@ -117,17 +189,17 @@ export function useWorktreeJumpPaletteSections({
|
||||
)
|
||||
const digitShortcutModifiers =
|
||||
useShortcutKeyComboDetails(DIGIT_INDEX_ACTION_ID)[0]?.keys.slice(0, -1) ?? []
|
||||
const { createWorktreeName, showCreateAction } = useMemo(
|
||||
() => getWorktreePaletteCreateActionState({ query: deferredQuery }),
|
||||
[deferredQuery]
|
||||
)
|
||||
|
||||
return {
|
||||
openTabsLeadSections,
|
||||
middleLeadsSections,
|
||||
paletteSections,
|
||||
recentTabShortcutIndexByItem,
|
||||
recentTabShortcutIndexById,
|
||||
digitShortcutModifiers,
|
||||
createWorktreeName,
|
||||
showCreateAction
|
||||
showCreateAction,
|
||||
handleExpandSection
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useCallback, useEffect, useLayoutEffect } from 'react'
|
||||
import {
|
||||
ORCA_BROWSER_FOCUS_REQUEST_EVENT,
|
||||
queueBrowserFocusRequest
|
||||
@@ -57,6 +57,8 @@ export function useWorktreeJumpPaletteSelectionLifecycle({
|
||||
setQuery,
|
||||
setSelectedItemId,
|
||||
setRawFilter,
|
||||
selectionMovedByUserRef,
|
||||
taskSourceUrl,
|
||||
listRef,
|
||||
preserveCreateLookupOnCloseRef,
|
||||
selectedItemId,
|
||||
@@ -105,6 +107,7 @@ export function useWorktreeJumpPaletteSelectionLifecycle({
|
||||
latestQueryRef.current = ''
|
||||
setQuery('')
|
||||
setSelectedItemId('')
|
||||
selectionMovedByUserRef.current = false
|
||||
setRawFilter(EMPTY_PALETTE_FILTER)
|
||||
listRef.current?.scrollTo(0, 0)
|
||||
}
|
||||
@@ -131,18 +134,29 @@ export function useWorktreeJumpPaletteSelectionLifecycle({
|
||||
currentSelectedItemId: selectedItemId,
|
||||
queryChanged: false,
|
||||
selectableItemIds: selectionItemIds,
|
||||
showCreateAction
|
||||
showCreateAction,
|
||||
autoSelectCreateAction: taskSourceUrl !== null
|
||||
})
|
||||
const handleCommandSelectionChange = useCallback(
|
||||
(nextItemId: string) => {
|
||||
if (latestQueryRef.current !== deferredQuery) {
|
||||
return
|
||||
}
|
||||
setSelectedItemId(nextItemId)
|
||||
},
|
||||
// oxlint-disable-next-line react-hooks/exhaustive-deps -- controller refs and setters preserve their original stable identities.
|
||||
[deferredQuery]
|
||||
[setSelectedItemId]
|
||||
)
|
||||
// A late cmdk callback can restore the old cursor after handleQueryChange clears it.
|
||||
// Commit the new list head explicitly when the deferred query changes.
|
||||
useLayoutEffect(() => {
|
||||
setSelectedItemId(
|
||||
getNextWorktreePaletteSelection({
|
||||
currentSelectedItemId: '',
|
||||
queryChanged: true,
|
||||
selectableItemIds: selectionItemIds,
|
||||
showCreateAction,
|
||||
autoSelectCreateAction: taskSourceUrl !== null
|
||||
})
|
||||
)
|
||||
// oxlint-disable-next-line react-hooks/exhaustive-deps -- selection resets only when the deferred query commits.
|
||||
}, [deferredQuery])
|
||||
useEffect(() => {
|
||||
const isCreateWorkspaceHighlighted =
|
||||
commandSelectedItemId === CREATE_WORKTREE_ITEM_ID ||
|
||||
|
||||
@@ -19,6 +19,9 @@ export function useWorktreeJumpPaletteStoreState({
|
||||
lingering: boolean
|
||||
}) {
|
||||
useTranslation()
|
||||
// Freeze age labels for one palette session; live status dots own their clock separately.
|
||||
// oxlint-disable-next-line react/purity
|
||||
const paletteNowMs = useMemo(() => Date.now(), [visible])
|
||||
const closeModal = useAppStore((state) => state.closeModal)
|
||||
const openModal = useAppStore((state) => state.openModal)
|
||||
const openSettingsPage = useAppStore((state) => state.openSettingsPage)
|
||||
@@ -44,6 +47,9 @@ export function useWorktreeJumpPaletteStoreState({
|
||||
const migrationUnsupportedByPtyId = useAppStore((state) => state.migrationUnsupportedByPtyId)
|
||||
const activeView = useAppStore((state) => state.activeView)
|
||||
const activeWorktreeId = useAppStore((state) => state.activeWorktreeId)
|
||||
const activeWorkspaceExecutionHostId = useAppStore(
|
||||
(state) => state.activeWorkspaceExecutionHostId
|
||||
)
|
||||
const activeTabType = useAppStore((state) => state.activeTabType)
|
||||
const activeTabId = useAppStore((state) => state.activeTabId)
|
||||
const activeTabIdByWorktree = useAppStore((state) => state.activeTabIdByWorktree)
|
||||
@@ -72,7 +78,13 @@ export function useWorktreeJumpPaletteStoreState({
|
||||
const sleepingAgentSessionsByPaneKey = useAppStore(
|
||||
(state) => state.sleepingAgentSessionsByPaneKey
|
||||
)
|
||||
const paneForegroundAgentByPaneKey = useAppStore(
|
||||
(state) => state.paneForegroundAgentByPaneKey
|
||||
)
|
||||
const settings = useAppStore((state) => state.settings)
|
||||
const worktreeVisibilityDefaultsByHost = useAppStore(
|
||||
(state) => state.worktreeVisibilityDefaultsByHost
|
||||
)
|
||||
const sshTargetLabels = useAppStore((state) => state.sshTargetLabels)
|
||||
const sshConnectionStates = useAppStore((state) => state.sshConnectionStates)
|
||||
const runtimeEnvironments = useAppStore((state) => state.runtimeEnvironments)
|
||||
@@ -83,6 +95,9 @@ export function useWorktreeJumpPaletteStoreState({
|
||||
)
|
||||
const hideCliCreatedWorkspaces = useAppStore((state) => state.hideCliCreatedWorkspaces)
|
||||
const hideDetachedHeadWorkspaces = useAppStore((state) => state.hideDetachedHeadWorkspaces)
|
||||
const hideWorkspacesFromOtherDevices = useAppStore(
|
||||
(state) => state.hideWorkspacesFromOtherDevices
|
||||
)
|
||||
const showSleepingWorkspaces = useAppStore((state) => state.showSleepingWorkspaces)
|
||||
const alwaysShowDefaultBranchWorkspace = useAppStore(
|
||||
(state) => state.alwaysShowDefaultBranchWorkspace
|
||||
@@ -102,6 +117,7 @@ export function useWorktreeJumpPaletteStoreState({
|
||||
|
||||
return {
|
||||
visible,
|
||||
paletteNowMs,
|
||||
closeModal,
|
||||
openModal,
|
||||
openSettingsPage,
|
||||
@@ -127,6 +143,7 @@ export function useWorktreeJumpPaletteStoreState({
|
||||
migrationUnsupportedByPtyId,
|
||||
activeView,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
activeTabType,
|
||||
activeTabId,
|
||||
activeTabIdByWorktree,
|
||||
@@ -146,7 +163,9 @@ export function useWorktreeJumpPaletteStoreState({
|
||||
groupsByWorktree,
|
||||
retainedAgentsByPaneKey,
|
||||
sleepingAgentSessionsByPaneKey,
|
||||
paneForegroundAgentByPaneKey,
|
||||
settings,
|
||||
worktreeVisibilityDefaultsByHost,
|
||||
sshTargetLabels,
|
||||
sshConnectionStates,
|
||||
runtimeEnvironments,
|
||||
@@ -155,6 +174,7 @@ export function useWorktreeJumpPaletteStoreState({
|
||||
hideAutomationGeneratedWorkspaces,
|
||||
hideCliCreatedWorkspaces,
|
||||
hideDetachedHeadWorkspaces,
|
||||
hideWorkspacesFromOtherDevices,
|
||||
showSleepingWorkspaces,
|
||||
alwaysShowDefaultBranchWorkspace,
|
||||
lastVisitedAtByWorktreeId,
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useAppStore } from '@/store'
|
||||
import { getComposerEligibleRepos, resolveComposerActiveRepoId } from '@/lib/new-workspace-composer-repo'
|
||||
import { resolveWorkspaceCreationTarget } from '@/lib/project-host-workspace-target'
|
||||
import { lookupCmdJGitHubUrlWorkItem } from '@/lib/cmd-j-github-url-lookup'
|
||||
import { lookupLinearIssueUrl } from '@/lib/linear-issue-url-lookup'
|
||||
import { withResolvedCmdJGitHubPreview, type CmdJTaskSourceUrl, getCmdJTaskUrlCreatePreview } from '@/lib/worktree-palette-task-url-match'
|
||||
import type { GitHubWorkItem } from '../../../shared/github/work-item-types'
|
||||
import type { LinearIssue } from '../../../shared/linear/issue-types'
|
||||
import type { TaskSourceContext } from '../../../shared/task-source-context'
|
||||
import { buildTaskSourceContextFromRepo } from '../../../shared/task-source-context'
|
||||
import type { WorktreePaletteRequestGuard } from '@/lib/worktree-palette-create-action'
|
||||
|
||||
export type CmdJLinearIssuePreview = {
|
||||
query: string
|
||||
issue: LinearIssue | null
|
||||
loading: boolean
|
||||
initialRepoId: string | null
|
||||
sourceContext: TaskSourceContext | null
|
||||
}
|
||||
|
||||
export type CmdJGitHubWorkItemPreview = {
|
||||
query: string
|
||||
item: GitHubWorkItem | null
|
||||
loading: boolean
|
||||
initialRepoId: string | null
|
||||
sourceContext: TaskSourceContext | null
|
||||
}
|
||||
|
||||
function getComposerDefaultWorkspaceTarget(state: ReturnType<typeof useAppStore.getState>) {
|
||||
const eligibleRepos = getComposerEligibleRepos(state.repos)
|
||||
const activeRepoId = resolveComposerActiveRepoId(state.repos, eligibleRepos, state.activeRepoId)
|
||||
const resolution = resolveWorkspaceCreationTarget({
|
||||
eligibleRepos,
|
||||
projects: state.projects,
|
||||
projectHostSetups: state.projectHostSetups,
|
||||
activeRepoId,
|
||||
focusedHostScope: state.workspaceHostScope
|
||||
})
|
||||
return resolution.status === 'ready' ? resolution.target : null
|
||||
}
|
||||
|
||||
export function useWorktreeJumpPaletteTaskUrl({
|
||||
visible,
|
||||
createWorktreeName,
|
||||
taskSourceUrl,
|
||||
createLookupGuard
|
||||
}: {
|
||||
visible: boolean
|
||||
createWorktreeName: string
|
||||
taskSourceUrl: CmdJTaskSourceUrl | null
|
||||
createLookupGuard: WorktreePaletteRequestGuard
|
||||
}) {
|
||||
const linearIssueUrlIntent = taskSourceUrl?.provider === 'linear' ? taskSourceUrl.intent : null
|
||||
const githubUrlLink = taskSourceUrl?.provider === 'github' ? taskSourceUrl.link : null
|
||||
const parsedTaskUrlCreatePreview = useMemo(
|
||||
() => (taskSourceUrl ? getCmdJTaskUrlCreatePreview(taskSourceUrl) : null),
|
||||
[taskSourceUrl]
|
||||
)
|
||||
const [linearIssuePreview, setLinearIssuePreview] = useState<CmdJLinearIssuePreview | null>(null)
|
||||
const [githubWorkItemPreview, setGithubWorkItemPreview] =
|
||||
useState<CmdJGitHubWorkItemPreview | null>(null)
|
||||
const linearLookupRef = useRef<{ query: string; promise: Promise<CmdJLinearIssuePreview> } | null>(null)
|
||||
const githubLookupRef = useRef<{ query: string; promise: Promise<CmdJGitHubWorkItemPreview> } | null>(null)
|
||||
const linearGenerationRef = useRef(0)
|
||||
const githubGenerationRef = useRef(0)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const generation = ++linearGenerationRef.current
|
||||
linearLookupRef.current = null
|
||||
if (!visible || !linearIssueUrlIntent) {
|
||||
setLinearIssuePreview(null)
|
||||
return
|
||||
}
|
||||
const state = useAppStore.getState()
|
||||
const target = getComposerDefaultWorkspaceTarget(state)
|
||||
const sourceContext = target
|
||||
? buildTaskSourceContextFromRepo({
|
||||
provider: 'linear',
|
||||
projectId: target.projectId,
|
||||
repo: target.repo,
|
||||
projectHostSetupId: target.projectHostSetupId
|
||||
})
|
||||
: null
|
||||
const pending: CmdJLinearIssuePreview = {
|
||||
query: createWorktreeName,
|
||||
issue: null,
|
||||
loading: true,
|
||||
initialRepoId: target?.repoId ?? null,
|
||||
sourceContext
|
||||
}
|
||||
setLinearIssuePreview(pending)
|
||||
const promise = lookupLinearIssueUrl({
|
||||
intent: linearIssueUrlIntent,
|
||||
knownStatus: state.linearStatus,
|
||||
sourceContext,
|
||||
fetchLinearIssue: state.fetchLinearIssue
|
||||
})
|
||||
.catch(() => null)
|
||||
.then((issue): CmdJLinearIssuePreview => ({ ...pending, issue, loading: false }))
|
||||
linearLookupRef.current = { query: createWorktreeName, promise }
|
||||
void promise.then((preview) => {
|
||||
if (linearGenerationRef.current === generation) {
|
||||
setLinearIssuePreview(preview)
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
if (linearGenerationRef.current === generation) {
|
||||
linearGenerationRef.current += 1
|
||||
}
|
||||
}
|
||||
}, [createWorktreeName, linearIssueUrlIntent, visible])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const generation = ++githubGenerationRef.current
|
||||
githubLookupRef.current = null
|
||||
if (!visible || !githubUrlLink) {
|
||||
setGithubWorkItemPreview(null)
|
||||
return
|
||||
}
|
||||
const state = useAppStore.getState()
|
||||
const target = getComposerDefaultWorkspaceTarget(state)
|
||||
const sourceContext = target
|
||||
? buildTaskSourceContextFromRepo({
|
||||
provider: 'github',
|
||||
projectId: target.projectId,
|
||||
repo: target.repo,
|
||||
projectHostSetupId: target.projectHostSetupId
|
||||
})
|
||||
: null
|
||||
const pending: CmdJGitHubWorkItemPreview = {
|
||||
query: createWorktreeName,
|
||||
item: null,
|
||||
loading: true,
|
||||
initialRepoId: target?.repoId ?? null,
|
||||
sourceContext
|
||||
}
|
||||
setGithubWorkItemPreview(pending)
|
||||
const promise = lookupCmdJGitHubUrlWorkItem({
|
||||
link: githubUrlLink,
|
||||
repo: target?.repo ?? null,
|
||||
sourceContext
|
||||
})
|
||||
.catch(() => null)
|
||||
.then((item): CmdJGitHubWorkItemPreview => ({ ...pending, item, loading: false }))
|
||||
githubLookupRef.current = { query: createWorktreeName, promise }
|
||||
void promise.then((preview) => {
|
||||
if (githubGenerationRef.current === generation) {
|
||||
setGithubWorkItemPreview(preview)
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
if (githubGenerationRef.current === generation) {
|
||||
githubGenerationRef.current += 1
|
||||
}
|
||||
}
|
||||
}, [createWorktreeName, githubUrlLink, visible])
|
||||
|
||||
const currentLinearIssuePreview =
|
||||
linearIssuePreview?.query === createWorktreeName ? linearIssuePreview : null
|
||||
const currentGitHubWorkItemPreview =
|
||||
githubWorkItemPreview?.query === createWorktreeName ? githubWorkItemPreview : null
|
||||
const taskUrlCreatePreview = useMemo(() => {
|
||||
if (!parsedTaskUrlCreatePreview) {
|
||||
return null
|
||||
}
|
||||
return withResolvedCmdJGitHubPreview(
|
||||
parsedTaskUrlCreatePreview,
|
||||
currentGitHubWorkItemPreview?.item?.title ?? null,
|
||||
currentGitHubWorkItemPreview?.loading === true
|
||||
)
|
||||
}, [currentGitHubWorkItemPreview, parsedTaskUrlCreatePreview])
|
||||
const [linearLoadingFeedbackQuery, setLinearLoadingFeedbackQuery] = useState<string | null>(null)
|
||||
useEffect(() => {
|
||||
if (!currentLinearIssuePreview?.loading) {
|
||||
setLinearLoadingFeedbackQuery(null)
|
||||
return
|
||||
}
|
||||
setLinearLoadingFeedbackQuery(null)
|
||||
const timer = window.setTimeout(
|
||||
() => setLinearLoadingFeedbackQuery(currentLinearIssuePreview.query),
|
||||
200
|
||||
)
|
||||
return () => window.clearTimeout(timer)
|
||||
}, [currentLinearIssuePreview?.loading, currentLinearIssuePreview?.query])
|
||||
|
||||
return {
|
||||
linearIssueUrlIntent,
|
||||
githubUrlLink,
|
||||
taskUrlCreatePreview,
|
||||
currentLinearIssuePreview,
|
||||
currentGitHubWorkItemPreview,
|
||||
linearLookupRef,
|
||||
githubLookupRef,
|
||||
showLinearLoadingFeedback:
|
||||
currentLinearIssuePreview?.loading === true &&
|
||||
linearLoadingFeedbackQuery === currentLinearIssuePreview.query,
|
||||
createLookupGuard
|
||||
}
|
||||
}
|
||||
|
||||
export type WorktreeJumpPaletteTaskUrl = ReturnType<typeof useWorktreeJumpPaletteTaskUrl>
|
||||
@@ -10,8 +10,24 @@ import { sortWorktreesSmart } from '@/components/sidebar/smart-sort'
|
||||
import { buildWorktreeChecksReviewIndex } from '@/components/cmd-j/worktree-checks-review-index'
|
||||
import { getLiveAgentStatusByWorktreeId, isInactiveWorkspace } from '@/lib/worktree-activity-state'
|
||||
import { orderEmptyQueryWorktrees } from '@/lib/order-empty-query-worktrees'
|
||||
import { getWorktreePaletteSearchScope, searchWorktrees } from '@/lib/worktree-palette-search'
|
||||
import {
|
||||
getWorktreePaletteSearchScope,
|
||||
searchWorktreeDocuments
|
||||
} from '@/lib/worktree-palette-search'
|
||||
import { buildWorktreePaletteDocuments } from '@/lib/worktree-palette-document'
|
||||
import { getWorkspacePortsByWorktreeId } from '@/lib/workspace-port-groups'
|
||||
import {
|
||||
buildPaletteWorktreeIndex,
|
||||
resolvePaletteWorktree,
|
||||
resolvePaletteRepoForWorktree
|
||||
} from '@/lib/palette-repo-resolution'
|
||||
import { getWorktreeHostIdentity } from '../../../shared/worktree/host-qualified-identity'
|
||||
import { getPaletteHostBadge } from '@/components/cmd-j/palette-host-badge'
|
||||
import {
|
||||
EMPTY_PAIRED_DEVICE_IDS_BY_ENVIRONMENT,
|
||||
getPairedDeviceIdsByEnvironment,
|
||||
isWorkspaceFromOtherDevice
|
||||
} from '@/components/sidebar/workspace-creator-visibility'
|
||||
import type { Worktree } from '../../../shared/worktree/types'
|
||||
import { EMPTY_SORTED_WORKTREES } from './worktree-jump-palette-model'
|
||||
import type { WorktreeJumpPaletteFilter } from './use-worktree-jump-palette-filter'
|
||||
@@ -19,11 +35,14 @@ import type { WorktreeJumpPaletteLocalState } from './use-worktree-jump-palette-
|
||||
import type { WorktreeJumpPaletteStoreState } from './use-worktree-jump-palette-store-state'
|
||||
|
||||
type WorktreeJumpPaletteWorktreesInput = WorktreeJumpPaletteStoreState &
|
||||
Pick<WorktreeJumpPaletteFilter, 'filterPredicate' | 'repoMap' | 'repoByHostIdentity'> &
|
||||
Pick<WorktreeJumpPaletteLocalState, 'deferredQuery'>
|
||||
Pick<
|
||||
WorktreeJumpPaletteFilter,
|
||||
'filterPredicate' | 'repoMap' | 'repoByHostIdentity' | 'hostOptions' | 'hostFilterActive'
|
||||
> &
|
||||
Pick<WorktreeJumpPaletteLocalState, 'paletteSearchQuery'>
|
||||
|
||||
export function useWorktreeJumpPaletteWorktrees({
|
||||
deferredQuery,
|
||||
paletteSearchQuery,
|
||||
repos,
|
||||
worktreesByRepo,
|
||||
agentStatusByPaneKey,
|
||||
@@ -34,11 +53,15 @@ export function useWorktreeJumpPaletteWorktrees({
|
||||
hideAutomationGeneratedWorkspaces,
|
||||
hideCliCreatedWorkspaces,
|
||||
hideDetachedHeadWorkspaces,
|
||||
hideWorkspacesFromOtherDevices,
|
||||
showSleepingWorkspaces,
|
||||
alwaysShowDefaultBranchWorkspace,
|
||||
ptyIdsByTabId,
|
||||
browserTabsByWorktree,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
runtimeEnvironments,
|
||||
runtimeStatusByEnvironmentId,
|
||||
lastVisitedAtByWorktreeId,
|
||||
paletteStatusInputsActive,
|
||||
repoMap,
|
||||
@@ -46,13 +69,15 @@ export function useWorktreeJumpPaletteWorktrees({
|
||||
migrationUnsupportedByPtyId,
|
||||
terminalLayoutsByTabId,
|
||||
repoByHostIdentity,
|
||||
hostOptions,
|
||||
hostFilterActive,
|
||||
prCache,
|
||||
hostedReviewCache,
|
||||
settings,
|
||||
issueCache,
|
||||
workspacePortScan
|
||||
}: WorktreeJumpPaletteWorktreesInput) {
|
||||
const hasQuery = deferredQuery.trim().length > 0
|
||||
const hasQuery = paletteSearchQuery.length > 0
|
||||
const isLoading = repos.length > 0 && Object.keys(worktreesByRepo).length === 0
|
||||
const worktreeIdsWithLiveAgent = useMemo(
|
||||
() =>
|
||||
@@ -64,6 +89,13 @@ export function useWorktreeJumpPaletteWorktrees({
|
||||
),
|
||||
[agentStatusByPaneKey, tabsByWorktree]
|
||||
)
|
||||
const pairedDeviceIdsByEnvironment = useMemo(
|
||||
() =>
|
||||
hideWorkspacesFromOtherDevices
|
||||
? getPairedDeviceIdsByEnvironment(runtimeEnvironments, runtimeStatusByEnvironmentId)
|
||||
: EMPTY_PAIRED_DEVICE_IDS_BY_ENVIRONMENT,
|
||||
[hideWorkspacesFromOtherDevices, runtimeEnvironments, runtimeStatusByEnvironmentId]
|
||||
)
|
||||
const emptyQueryVisibleWorktrees = useMemo(
|
||||
() =>
|
||||
allWorktrees.filter((worktree) => {
|
||||
@@ -85,6 +117,12 @@ export function useWorktreeJumpPaletteWorktrees({
|
||||
if (hideDetachedHeadWorkspaces && isDetachedHeadWorkspace(worktree)) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
hideWorkspacesFromOtherDevices &&
|
||||
isWorkspaceFromOtherDevice(worktree, pairedDeviceIdsByEnvironment)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
!showSleepingWorkspaces &&
|
||||
!isSleepingSweepExemptWorkspace(worktree, alwaysShowDefaultBranchWorkspace) &&
|
||||
@@ -109,6 +147,8 @@ export function useWorktreeJumpPaletteWorktrees({
|
||||
hideCliCreatedWorkspaces,
|
||||
hideDefaultBranchWorkspace,
|
||||
hideDetachedHeadWorkspaces,
|
||||
hideWorkspacesFromOtherDevices,
|
||||
pairedDeviceIdsByEnvironment,
|
||||
ptyIdsByTabId,
|
||||
showSleepingWorkspaces,
|
||||
tabsByWorktree,
|
||||
@@ -120,9 +160,15 @@ export function useWorktreeJumpPaletteWorktrees({
|
||||
orderEmptyQueryWorktrees({
|
||||
visibleWorktrees: emptyQueryVisibleWorktrees,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
lastVisitedAtByWorktreeId
|
||||
}),
|
||||
[emptyQueryVisibleWorktrees, activeWorktreeId, lastVisitedAtByWorktreeId]
|
||||
[
|
||||
emptyQueryVisibleWorktrees,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
lastVisitedAtByWorktreeId
|
||||
]
|
||||
)
|
||||
const searchScopeWorktrees = useMemo(() => {
|
||||
const scope = getWorktreePaletteSearchScope({
|
||||
@@ -168,17 +214,45 @@ export function useWorktreeJumpPaletteWorktrees({
|
||||
: searchScopeWorktrees,
|
||||
[hasQuery, browserSortedWorktrees, searchScopeWorktrees]
|
||||
)
|
||||
const paletteWorktreeIndex = useMemo(
|
||||
() => buildPaletteWorktreeIndex(browserSortedWorktrees),
|
||||
[browserSortedWorktrees]
|
||||
)
|
||||
const resolveWorktree = useMemo(
|
||||
() =>
|
||||
(worktreeId: string, hostId: Worktree['hostId'] | undefined): Worktree | undefined =>
|
||||
resolvePaletteWorktree(paletteWorktreeIndex, worktreeId, hostId),
|
||||
[paletteWorktreeIndex]
|
||||
)
|
||||
// Keep a host-qualified map for consumers that only have an identity key.
|
||||
const worktreeMap = useMemo(() => {
|
||||
const map = new Map<string, Worktree>()
|
||||
for (const worktree of browserSortedWorktrees) {
|
||||
map.set(worktree.id, worktree)
|
||||
map.set(getWorktreeHostIdentity(worktree), worktree)
|
||||
if (!map.has(worktree.id)) {
|
||||
map.set(worktree.id, worktree)
|
||||
}
|
||||
}
|
||||
return map
|
||||
}, [browserSortedWorktrees])
|
||||
const worktreeOrder = useMemo(
|
||||
() => new Map(browserSortedWorktrees.map((worktree, index) => [worktree.id, index])),
|
||||
() =>
|
||||
new Map(
|
||||
browserSortedWorktrees.map((worktree, index) => [getWorktreeHostIdentity(worktree), index])
|
||||
),
|
||||
[browserSortedWorktrees]
|
||||
)
|
||||
const hostLabelByWorktreeId = useMemo(() => {
|
||||
const labels = new Map<string, string>()
|
||||
for (const worktree of allWorktrees) {
|
||||
const repo = resolvePaletteRepoForWorktree(worktree, repoMap, repoByHostIdentity)
|
||||
const badge = getPaletteHostBadge(repo, hostOptions, hostFilterActive)
|
||||
if (badge) {
|
||||
labels.set(getWorktreeHostIdentity(worktree), badge.label)
|
||||
}
|
||||
}
|
||||
return labels
|
||||
}, [allWorktrees, hostFilterActive, hostOptions, repoByHostIdentity, repoMap])
|
||||
const checksReviewByWorktree = useMemo(
|
||||
() =>
|
||||
buildWorktreeChecksReviewIndex({
|
||||
@@ -190,24 +264,48 @@ export function useWorktreeJumpPaletteWorktrees({
|
||||
}),
|
||||
[allWorktrees, hostedReviewCache, prCache, repoByHostIdentity, settings]
|
||||
)
|
||||
const worktreeDocuments = useMemo(
|
||||
() =>
|
||||
buildWorktreePaletteDocuments(
|
||||
allWorktrees.filter((worktree) => !worktree.isArchived),
|
||||
{
|
||||
repoMap,
|
||||
repoMapByHostIdentity: repoByHostIdentity,
|
||||
prCache,
|
||||
issueCache,
|
||||
workspacePortsByWorktreeId: getWorkspacePortsByWorktreeId(workspacePortScan),
|
||||
checksReviewByWorktree,
|
||||
hostLabelByWorktreeId
|
||||
}
|
||||
),
|
||||
[
|
||||
allWorktrees,
|
||||
checksReviewByWorktree,
|
||||
issueCache,
|
||||
prCache,
|
||||
repoByHostIdentity,
|
||||
repoMap,
|
||||
workspacePortScan,
|
||||
hostLabelByWorktreeId
|
||||
]
|
||||
)
|
||||
const worktreeMatches = useMemo(
|
||||
() =>
|
||||
searchWorktrees(sortedWorktrees, deferredQuery.trim(), repoMap, {
|
||||
searchWorktreeDocuments({
|
||||
worktrees: sortedWorktrees,
|
||||
query: paletteSearchQuery,
|
||||
documents: worktreeDocuments,
|
||||
repoMap,
|
||||
repoMapByHostIdentity: repoByHostIdentity,
|
||||
prCache,
|
||||
issueCache,
|
||||
workspacePortsByWorktreeId: getWorkspacePortsByWorktreeId(workspacePortScan),
|
||||
checksReviewByWorktree
|
||||
}),
|
||||
[
|
||||
sortedWorktrees,
|
||||
deferredQuery,
|
||||
repoMap,
|
||||
checksReviewByWorktree,
|
||||
paletteSearchQuery,
|
||||
repoByHostIdentity,
|
||||
prCache,
|
||||
issueCache,
|
||||
workspacePortScan,
|
||||
checksReviewByWorktree
|
||||
repoMap,
|
||||
sortedWorktrees,
|
||||
worktreeDocuments
|
||||
]
|
||||
)
|
||||
return {
|
||||
@@ -218,6 +316,8 @@ export function useWorktreeJumpPaletteWorktrees({
|
||||
searchScopeWorktrees,
|
||||
browserSortedWorktrees,
|
||||
worktreeMap,
|
||||
resolveWorktree,
|
||||
paletteWorktreeIndex,
|
||||
worktreeOrder,
|
||||
worktreeMatches
|
||||
}
|
||||
|
||||
@@ -13,18 +13,26 @@ import {
|
||||
PaletteOpenTabPrimaryLine,
|
||||
PaletteRowShortcutBadge
|
||||
} from './worktree-jump-palette-primitives'
|
||||
import { formatPaletteSessionAge } from '@/components/cmd-j/palette-session-age'
|
||||
import { resolvePaletteRepoForWorktree } from '@/lib/palette-repo-resolution'
|
||||
|
||||
export function WorktreeJumpPaletteSimulatorRow({
|
||||
entry,
|
||||
renderKey,
|
||||
controller
|
||||
}: {
|
||||
entry: SimulatorPaletteItem
|
||||
renderKey: string
|
||||
controller: WorktreeJumpPaletteController
|
||||
}): React.JSX.Element {
|
||||
const result = entry.result
|
||||
const simulatorWorktree = controller.worktreeMap.get(result.worktreeId)
|
||||
const simulatorWorktree = controller.resolveWorktree(result.worktreeId, result.executionHostId)
|
||||
const simulatorRepo = simulatorWorktree
|
||||
? controller.repoMap.get(simulatorWorktree.repoId)
|
||||
? resolvePaletteRepoForWorktree(
|
||||
simulatorWorktree,
|
||||
controller.repoMap,
|
||||
controller.repoByHostIdentity
|
||||
)
|
||||
: undefined
|
||||
const simulatorRepoName = simulatorRepo?.displayName ?? result.repoName
|
||||
const simulatorHostBadge = getPaletteHostBadge(
|
||||
@@ -32,11 +40,14 @@ export function WorktreeJumpPaletteSimulatorRow({
|
||||
controller.hostOptions,
|
||||
controller.hostFilterActive
|
||||
)
|
||||
const simulatorSessionAge = formatPaletteSessionAge(
|
||||
result.lastActiveAt ?? null,
|
||||
controller.paletteNowMs
|
||||
)
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
key={entry.id}
|
||||
value={entry.id}
|
||||
value={renderKey}
|
||||
onSelect={() => controller.handleSelectItem(entry)}
|
||||
className={cn(
|
||||
'group mx-0.5 flex cursor-pointer items-center gap-3 rounded-lg border border-transparent px-3 py-2.5 text-left outline-none transition-[background-color,border-color,box-shadow]',
|
||||
@@ -56,6 +67,7 @@ export function WorktreeJumpPaletteSimulatorRow({
|
||||
secondaryRanges={result.secondaryRanges}
|
||||
worktreeName={result.worktreeName}
|
||||
worktreeRanges={result.worktreeRanges}
|
||||
sessionAge={simulatorSessionAge}
|
||||
leadingBadges={
|
||||
<>
|
||||
{result.isCurrentTab && (
|
||||
@@ -86,7 +98,7 @@ export function WorktreeJumpPaletteSimulatorRow({
|
||||
</span>
|
||||
)}
|
||||
<PaletteRowShortcutBadge
|
||||
index={controller.recentTabShortcutIndexById.get(entry.id)}
|
||||
index={controller.recentTabShortcutIndexByItem.get(entry)}
|
||||
modifierKeys={controller.digitShortcutModifiers}
|
||||
/>
|
||||
</div>
|
||||
@@ -98,25 +110,36 @@ export function WorktreeJumpPaletteSimulatorRow({
|
||||
|
||||
export function WorktreeJumpPaletteBrowserRow({
|
||||
entry,
|
||||
renderKey,
|
||||
controller
|
||||
}: {
|
||||
entry: BrowserPaletteItem
|
||||
renderKey: string
|
||||
controller: WorktreeJumpPaletteController
|
||||
}): React.JSX.Element {
|
||||
const result = entry.result
|
||||
const browserWorktree = controller.worktreeMap.get(result.worktreeId)
|
||||
const browserRepo = browserWorktree ? controller.repoMap.get(browserWorktree.repoId) : undefined
|
||||
const browserWorktree = controller.resolveWorktree(result.worktreeId, result.executionHostId)
|
||||
const browserRepo = browserWorktree
|
||||
? resolvePaletteRepoForWorktree(
|
||||
browserWorktree,
|
||||
controller.repoMap,
|
||||
controller.repoByHostIdentity
|
||||
)
|
||||
: undefined
|
||||
const browserRepoName = browserRepo?.displayName ?? result.repoName
|
||||
const browserHostBadge = getPaletteHostBadge(
|
||||
browserRepo,
|
||||
controller.hostOptions,
|
||||
controller.hostFilterActive
|
||||
)
|
||||
const browserSessionAge = formatPaletteSessionAge(
|
||||
result.lastActiveAt ?? null,
|
||||
controller.paletteNowMs
|
||||
)
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
key={entry.id}
|
||||
value={entry.id}
|
||||
value={renderKey}
|
||||
onSelect={() => controller.handleSelectItem(entry)}
|
||||
className={cn(
|
||||
'group mx-0.5 flex cursor-pointer items-center gap-3 rounded-lg border border-transparent px-3 py-2.5 text-left outline-none transition-[background-color,border-color,box-shadow]',
|
||||
@@ -136,6 +159,7 @@ export function WorktreeJumpPaletteBrowserRow({
|
||||
secondaryRanges={result.secondaryRanges}
|
||||
worktreeName={result.worktreeName}
|
||||
worktreeRanges={result.worktreeRanges}
|
||||
sessionAge={browserSessionAge}
|
||||
leadingBadges={
|
||||
<>
|
||||
{result.isCurrentPage && (
|
||||
@@ -166,7 +190,7 @@ export function WorktreeJumpPaletteBrowserRow({
|
||||
</span>
|
||||
)}
|
||||
<PaletteRowShortcutBadge
|
||||
index={controller.recentTabShortcutIndexById.get(entry.id)}
|
||||
index={controller.recentTabShortcutIndexByItem.get(entry)}
|
||||
modifierKeys={controller.digitShortcutModifiers}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type React from 'react'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { flushSync } from 'react-dom'
|
||||
import { CommandItem } from '@/components/ui/command'
|
||||
import { CREATE_WORKTREE_ITEM_ID } from '@/lib/worktree-palette-create-action'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { PaletteCreateWorktreeRow } from '@/components/cmd-j/PaletteCreateWorktreeRow'
|
||||
import type { PaletteListEntry } from './worktree-jump-palette-model'
|
||||
import type { WorktreeJumpPaletteController } from './use-worktree-jump-palette-controller'
|
||||
import { WorktreeJumpPaletteWorktreeRow } from './worktree-jump-palette-worktree-row'
|
||||
@@ -15,12 +15,15 @@ import {
|
||||
WorktreeJumpPaletteBrowserRow,
|
||||
WorktreeJumpPaletteSimulatorRow
|
||||
} from './worktree-jump-palette-browser-simulator-rows'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
export function WorktreeJumpPaletteEntry({
|
||||
entry,
|
||||
renderKey,
|
||||
controller
|
||||
}: {
|
||||
entry: PaletteListEntry
|
||||
renderKey: string
|
||||
controller: WorktreeJumpPaletteController
|
||||
}): React.JSX.Element {
|
||||
if (entry.type === 'section-header') {
|
||||
@@ -31,48 +34,63 @@ export function WorktreeJumpPaletteEntry({
|
||||
)
|
||||
}
|
||||
if (entry.type === 'hint') {
|
||||
return (
|
||||
<div className="mx-0.5 mt-1 px-3 py-1.5 text-[12px] italic text-muted-foreground/70">
|
||||
{entry.label}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (entry.type === 'create-worktree') {
|
||||
return (
|
||||
<CommandItem
|
||||
value={CREATE_WORKTREE_ITEM_ID}
|
||||
onSelect={controller.handleCreateWorktree}
|
||||
className="group mx-0.5 mt-1 flex cursor-pointer items-center gap-3 rounded-lg border border-transparent px-3 py-1.5 text-left outline-none transition-[background-color,border-color,box-shadow] data-[selected=true]:border-border data-[selected=true]:bg-accent data-[selected=true]:text-foreground"
|
||||
value={renderKey}
|
||||
onSelect={() => {
|
||||
const previousIndex = controller.selectionItemIds.indexOf(renderKey)
|
||||
flushSync(() => entry.onSeeMore?.())
|
||||
const expandedItemId = Array.from(
|
||||
controller.listRef.current?.querySelectorAll<HTMLElement>('[cmdk-item]') ?? []
|
||||
)[previousIndex]?.getAttribute('data-value')
|
||||
if (expandedItemId) {
|
||||
controller.setSelectedItemId(expandedItemId)
|
||||
}
|
||||
controller.inputRef.current?.focus()
|
||||
}}
|
||||
className={cn(
|
||||
'group mx-0.5 mt-1 min-h-0 gap-2 py-1.5 text-[12px] text-muted-foreground',
|
||||
'data-[selected=true]:bg-accent/60'
|
||||
)}
|
||||
>
|
||||
<div className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-dashed border-border/60 bg-muted/25 text-muted-foreground/70">
|
||||
<Plus size={13} aria-hidden="true" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-[14px] font-semibold tracking-[-0.01em] text-foreground">
|
||||
{translate(
|
||||
'auto.components.WorktreeJumpPalette.95be6587d3',
|
||||
'Create worktree "{{value0}}"',
|
||||
{ value0: controller.createWorktreeName }
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<span className="truncate">{entry.label}</span>
|
||||
{entry.onSeeMore ? (
|
||||
<span className="h-6 shrink-0 rounded-md border border-input bg-background px-2 text-xs font-medium leading-6 text-foreground">
|
||||
{translate('worktreeJumpPalette.seeMore', 'See more')}
|
||||
</span>
|
||||
) : null}
|
||||
</CommandItem>
|
||||
)
|
||||
}
|
||||
if (entry.type === 'create-worktree') {
|
||||
const linearPreview = controller.currentLinearIssuePreview
|
||||
return (
|
||||
<PaletteCreateWorktreeRow
|
||||
className="group mx-0.5 mt-1 flex cursor-pointer items-center gap-3 rounded-lg border border-transparent px-3 py-1.5 text-left outline-none transition-[background-color,border-color,box-shadow] data-[selected=true]:border-border data-[selected=true]:bg-accent data-[selected=true]:text-foreground"
|
||||
createWorktreeName={controller.createWorktreeName}
|
||||
linearIdentifier={controller.linearIssueUrlIntent?.identifier ?? null}
|
||||
linearIssue={linearPreview?.issue ?? null}
|
||||
linearPending={controller.linearIssueUrlIntent !== null && linearPreview?.loading !== false}
|
||||
showLinearLoadingFeedback={controller.showLinearLoadingFeedback}
|
||||
taskUrlPreview={controller.taskUrlCreatePreview}
|
||||
onSelect={controller.handleCreateWorktree}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (entry.type === 'worktree') {
|
||||
return <WorktreeJumpPaletteWorktreeRow entry={entry} controller={controller} />
|
||||
return <WorktreeJumpPaletteWorktreeRow entry={entry} renderKey={renderKey} controller={controller} />
|
||||
}
|
||||
if (entry.type === 'project-target') {
|
||||
return <WorktreeJumpPaletteProjectRow entry={entry} controller={controller} />
|
||||
return <WorktreeJumpPaletteProjectRow entry={entry} renderKey={renderKey} controller={controller} />
|
||||
}
|
||||
if (entry.type === 'settings' || entry.type === 'quick-action') {
|
||||
return <WorktreeJumpPaletteActionRow entry={entry} controller={controller} />
|
||||
return <WorktreeJumpPaletteActionRow entry={entry} renderKey={renderKey} controller={controller} />
|
||||
}
|
||||
if (entry.type === 'workspace-tab') {
|
||||
return <WorktreeJumpPaletteWorkspaceTabRow entry={entry} controller={controller} />
|
||||
return <WorktreeJumpPaletteWorkspaceTabRow entry={entry} renderKey={renderKey} controller={controller} />
|
||||
}
|
||||
if (entry.type === 'simulator-tab') {
|
||||
return <WorktreeJumpPaletteSimulatorRow entry={entry} controller={controller} />
|
||||
return <WorktreeJumpPaletteSimulatorRow entry={entry} renderKey={renderKey} controller={controller} />
|
||||
}
|
||||
return <WorktreeJumpPaletteBrowserRow entry={entry} controller={controller} />
|
||||
return <WorktreeJumpPaletteBrowserRow entry={entry} renderKey={renderKey} controller={controller} />
|
||||
}
|
||||
|
||||
@@ -2,8 +2,15 @@ import type { BrowserPaletteSearchResult } from '@/lib/browser-palette-search'
|
||||
import type { PaletteSearchResult } from '@/lib/worktree-palette-search'
|
||||
import type { SimulatorPaletteSearchResult } from '@/lib/simulator-palette-search'
|
||||
import type { WorkspaceTabPaletteSearchResult } from '@/lib/workspace-tab-palette-search'
|
||||
import type { CmdJActionResult, CmdJSettingsResult } from '@/components/cmd-j/palette-results'
|
||||
import type { CmdJProjectSearchResult } from '@/components/cmd-j/palette-project-results'
|
||||
import type {
|
||||
CmdJActionResult,
|
||||
CmdJRankedMiddleResult,
|
||||
CmdJSettingsResult
|
||||
} from '@/components/cmd-j/palette-results'
|
||||
import type {
|
||||
CmdJProjectSearchResult,
|
||||
CmdJRankedProjectSearchResult
|
||||
} from '@/components/cmd-j/palette-project-results'
|
||||
import type { RecentWorkspaceTabRow } from '@/lib/recent-workspace-tab-rows'
|
||||
import type { Worktree } from '../../../shared/worktree/types'
|
||||
import { CREATE_WORKSPACE_QUICK_ACTION_ID } from '@/components/cmd-j/quick-actions'
|
||||
@@ -37,27 +44,28 @@ export type WorkspaceTabPaletteItem = {
|
||||
export type SettingsPaletteItem = {
|
||||
id: string
|
||||
type: 'settings'
|
||||
result: CmdJSettingsResult
|
||||
result: CmdJSettingsResult & Pick<CmdJRankedMiddleResult, 'qualityClass'>
|
||||
}
|
||||
|
||||
export type QuickActionPaletteItem = {
|
||||
id: string
|
||||
type: 'quick-action'
|
||||
result: CmdJActionResult
|
||||
result: CmdJActionResult & Pick<CmdJRankedMiddleResult, 'qualityClass'>
|
||||
}
|
||||
|
||||
export type ProjectTargetPaletteItem = {
|
||||
id: string
|
||||
type: 'project-target'
|
||||
result: CmdJProjectSearchResult
|
||||
result: CmdJProjectSearchResult & Pick<CmdJRankedProjectSearchResult, 'qualityClass'>
|
||||
}
|
||||
|
||||
export type SectionHeader = {
|
||||
export type SectionHeader = { id: string; type: 'section-header'; label: string }
|
||||
export type HintRow = {
|
||||
id: string
|
||||
type: 'section-header'
|
||||
type: 'hint'
|
||||
label: string
|
||||
onSeeMore?: () => void
|
||||
}
|
||||
export type HintRow = { id: string; type: 'hint'; label: string }
|
||||
export type CreateWorktreePaletteItem = {
|
||||
id: typeof CREATE_WORKTREE_ITEM_ID
|
||||
type: 'create-worktree'
|
||||
@@ -77,6 +85,8 @@ export type OpenTabPaletteItem = BrowserPaletteItem | SimulatorPaletteItem | Wor
|
||||
|
||||
export type OpenTabRecentRow = {
|
||||
item: OpenTabPaletteItem
|
||||
/** Stable per-occurrence key; persisted tab ids can collide across hosts/snapshots. */
|
||||
occurrenceId: string
|
||||
worktree: Worktree
|
||||
row: RecentWorkspaceTabRow
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { useLayoutEffect, useRef, useState } from 'react'
|
||||
import type React from 'react'
|
||||
import { ShortcutKeyCombo } from '@/components/ShortcutKeyCombo'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { PaletteHostBadge } from '@/components/cmd-j/palette-host-badge'
|
||||
import type { MatchRange, PaletteSearchResult } from '@/lib/worktree-palette-search'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import type { Worktree } from '../../../shared/worktree/types'
|
||||
import { resolveWorktreeBranchLabel } from '@/lib/worktree-default-display-name'
|
||||
|
||||
export function PaletteRowShortcutBadge({
|
||||
index,
|
||||
@@ -67,6 +71,7 @@ export function PaletteOpenTabPrimaryLine({
|
||||
secondaryRanges,
|
||||
worktreeName,
|
||||
worktreeRanges,
|
||||
sessionAge,
|
||||
leadingBadges
|
||||
}: {
|
||||
title: string
|
||||
@@ -75,6 +80,7 @@ export function PaletteOpenTabPrimaryLine({
|
||||
secondaryRanges: readonly MatchRange[]
|
||||
worktreeName: string
|
||||
worktreeRanges: readonly MatchRange[]
|
||||
sessionAge?: string
|
||||
leadingBadges?: React.ReactNode
|
||||
}): React.JSX.Element {
|
||||
const showSecondary = secondaryText.trim().length > 0
|
||||
@@ -82,9 +88,24 @@ export function PaletteOpenTabPrimaryLine({
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 items-center gap-2 overflow-hidden">
|
||||
<span className="min-w-0 max-w-[42%] shrink-0 truncate text-[14px] font-semibold tracking-[-0.01em] text-foreground">
|
||||
<span
|
||||
data-slot="palette-open-tab-title"
|
||||
className="min-w-0 flex-1 truncate text-[14px] font-semibold tracking-[-0.01em] text-foreground"
|
||||
>
|
||||
<HighlightedText text={title} matchRanges={titleRanges} />
|
||||
</span>
|
||||
{sessionAge ? (
|
||||
<span
|
||||
aria-label={translate(
|
||||
'auto.components.WorktreeJumpPalette.lastActiveTime',
|
||||
'Last active {{value0}} ago',
|
||||
{ value0: sessionAge }
|
||||
)}
|
||||
className="shrink-0 text-[11px] font-medium tabular-nums text-muted-foreground/70"
|
||||
>
|
||||
{sessionAge}
|
||||
</span>
|
||||
) : null}
|
||||
{leadingBadges}
|
||||
{showSecondary ? (
|
||||
<>
|
||||
@@ -97,7 +118,10 @@ export function PaletteOpenTabPrimaryLine({
|
||||
{showWorktree ? (
|
||||
<>
|
||||
<span className="shrink-0 text-muted-foreground/45">·</span>
|
||||
<span className="min-w-0 truncate text-[12px] font-medium text-muted-foreground/92">
|
||||
<span
|
||||
data-slot="palette-open-tab-worktree"
|
||||
className="min-w-0 truncate text-[12px] font-medium text-muted-foreground/92"
|
||||
>
|
||||
<HighlightedText text={worktreeName} matchRanges={worktreeRanges} />
|
||||
</span>
|
||||
</>
|
||||
@@ -106,6 +130,69 @@ export function PaletteOpenTabPrimaryLine({
|
||||
)
|
||||
}
|
||||
|
||||
function resolveOpenTabWorktreeRailTooltip({
|
||||
isBranch,
|
||||
truncated,
|
||||
name
|
||||
}: {
|
||||
isBranch: boolean
|
||||
truncated: boolean
|
||||
name: string
|
||||
}): string {
|
||||
if (truncated) return name
|
||||
return isBranch
|
||||
? translate('auto.components.WorktreeJumpPalette.paletteOpenTabBranch', 'Branch name')
|
||||
: translate('auto.components.WorktreeJumpPalette.paletteOpenTabWorkspace', 'Workspace name')
|
||||
}
|
||||
|
||||
export function PaletteOpenTabWorktreeRailLabel({
|
||||
name,
|
||||
matchRanges,
|
||||
worktree,
|
||||
className,
|
||||
slot = 'palette-open-tab-worktree'
|
||||
}: {
|
||||
name: string
|
||||
matchRanges: readonly MatchRange[]
|
||||
worktree?: Pick<Worktree, 'branch'> | null
|
||||
className?: string
|
||||
slot?: string
|
||||
}): React.JSX.Element | null {
|
||||
const [truncated, setTruncated] = useState(false)
|
||||
const labelRef = useRef<HTMLSpanElement | null>(null)
|
||||
useLayoutEffect(() => {
|
||||
const node = labelRef.current
|
||||
if (!node) {
|
||||
setTruncated(false)
|
||||
return
|
||||
}
|
||||
const updateTruncated = (): void => {
|
||||
const next = node.scrollWidth > node.clientWidth
|
||||
setTruncated((current) => (current === next ? current : next))
|
||||
}
|
||||
updateTruncated()
|
||||
if (typeof ResizeObserver === 'undefined') return
|
||||
const observer = new ResizeObserver(updateTruncated)
|
||||
observer.observe(node)
|
||||
return () => observer.disconnect()
|
||||
}, [name])
|
||||
if (name.trim().length === 0) return null
|
||||
const isBranch = worktree != null && name === resolveWorktreeBranchLabel(worktree)
|
||||
const tooltip = resolveOpenTabWorktreeRailTooltip({ isBranch, truncated, name })
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span ref={labelRef} data-slot={slot} tabIndex={-1} className={className}>
|
||||
<HighlightedText text={name} matchRanges={matchRanges} />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={6} className="max-w-80 break-all">
|
||||
{tooltip}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export function PaletteState({
|
||||
title,
|
||||
subtitle
|
||||
|
||||
@@ -15,9 +15,11 @@ import { PaletteHostBadgeChip } from './worktree-jump-palette-primitives'
|
||||
|
||||
export function WorktreeJumpPaletteProjectRow({
|
||||
entry,
|
||||
renderKey,
|
||||
controller
|
||||
}: {
|
||||
entry: ProjectTargetPaletteItem
|
||||
renderKey: string
|
||||
controller: WorktreeJumpPaletteController
|
||||
}): React.JSX.Element {
|
||||
const result = entry.result
|
||||
@@ -31,8 +33,7 @@ export function WorktreeJumpPaletteProjectRow({
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
key={entry.id}
|
||||
value={entry.id}
|
||||
value={renderKey}
|
||||
onSelect={() => controller.handleSelectItem(entry)}
|
||||
className={cn(
|
||||
'group mx-0.5 flex cursor-pointer items-center gap-3 rounded-lg border border-transparent px-3 py-2.5 text-left outline-none transition-[background-color,border-color,box-shadow]',
|
||||
@@ -71,9 +72,11 @@ export function WorktreeJumpPaletteProjectRow({
|
||||
|
||||
export function WorktreeJumpPaletteActionRow({
|
||||
entry,
|
||||
renderKey,
|
||||
controller
|
||||
}: {
|
||||
entry: SettingsPaletteItem | QuickActionPaletteItem
|
||||
renderKey: string
|
||||
controller: WorktreeJumpPaletteController
|
||||
}): React.JSX.Element {
|
||||
const result = entry.result
|
||||
@@ -85,8 +88,7 @@ export function WorktreeJumpPaletteActionRow({
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
key={entry.id}
|
||||
value={entry.id}
|
||||
value={renderKey}
|
||||
onSelect={() => controller.handleSelectItem(entry)}
|
||||
className={cn(
|
||||
'group mx-0.5 flex cursor-pointer items-center gap-3 rounded-lg border border-transparent px-3 py-2.5 text-left outline-none transition-[background-color,border-color,box-shadow]',
|
||||
|
||||
@@ -45,5 +45,5 @@ export function shouldIncludeOpenTabInRecentSection({
|
||||
unreadAgentCompletionPanes
|
||||
})
|
||||
})
|
||||
return badge != null && badge !== 'done'
|
||||
return badge != null && badge !== 'done' && badge !== 'interrupted'
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type React from 'react'
|
||||
import { CommandDialog, CommandEmpty, CommandInput, CommandList } from '@/components/ui/command'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import PaletteFilterMenu from '@/components/cmd-j/PaletteFilterMenu'
|
||||
import PaletteFilterChips from '@/components/cmd-j/PaletteFilterChips'
|
||||
import { PaletteLiveStatusProvider } from '@/components/cmd-j/palette-live-status'
|
||||
@@ -39,7 +40,13 @@ export function WorktreeJumpPaletteSurface({
|
||||
loop: true,
|
||||
value: controller.commandSelectedItemId,
|
||||
onValueChange: controller.handleCommandSelectionChange,
|
||||
className: 'bg-transparent'
|
||||
className: 'bg-transparent',
|
||||
onKeyDownCapture: (event: React.KeyboardEvent) => {
|
||||
if (controller.selectionMovedByUserRef &&
|
||||
['ArrowDown', 'ArrowUp', 'Home', 'End', 'PageDown', 'PageUp'].includes(event.key)) {
|
||||
controller.selectionMovedByUserRef.current = true
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CommandInput
|
||||
@@ -88,6 +95,9 @@ export function WorktreeJumpPaletteSurface({
|
||||
/>
|
||||
<CommandList
|
||||
ref={controller.listRef}
|
||||
onPointerDownCapture={() => {
|
||||
controller.selectionMovedByUserRef.current = true
|
||||
}}
|
||||
className="max-h-[min(600px,calc(100vh-14rem))] px-2.5 pb-2.5 pt-2"
|
||||
>
|
||||
{controller.isLoading &&
|
||||
@@ -109,8 +119,13 @@ export function WorktreeJumpPaletteSurface({
|
||||
</CommandEmpty>
|
||||
) : (
|
||||
<>
|
||||
{controller.listEntries.map((entry) => (
|
||||
<WorktreeJumpPaletteEntry key={entry.id} entry={entry} controller={controller} />
|
||||
{controller.listEntries.map((entry, index) => (
|
||||
<WorktreeJumpPaletteEntry
|
||||
key={controller.listEntryRenderKeys[index] ?? entry.id}
|
||||
entry={entry}
|
||||
renderKey={controller.listEntryRenderKeys[index] ?? entry.id}
|
||||
controller={controller}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
@@ -159,8 +174,10 @@ export function WorktreeJumpPaletteSurface({
|
||||
)
|
||||
|
||||
return (
|
||||
<PaletteLiveStatusProvider active={controller.paletteStatusInputsActive}>
|
||||
{paletteDialog}
|
||||
</PaletteLiveStatusProvider>
|
||||
<TooltipProvider delayDuration={400}>
|
||||
<PaletteLiveStatusProvider active={controller.paletteStatusInputsActive}>
|
||||
{paletteDialog}
|
||||
</PaletteLiveStatusProvider>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type React from 'react'
|
||||
import { FileText, SquareTerminal } from 'lucide-react'
|
||||
import { AgentIcon } from '@/lib/agent-catalog'
|
||||
import { CommandItem } from '@/components/ui/command'
|
||||
import { PaletteRecentTabStatusDot } from '@/components/cmd-j/palette-live-status'
|
||||
import { RepoBadgeMark } from '@/components/repo/RepoBadgeLabel'
|
||||
@@ -14,18 +15,29 @@ import {
|
||||
PaletteOpenTabPrimaryLine,
|
||||
PaletteRowShortcutBadge
|
||||
} from './worktree-jump-palette-primitives'
|
||||
import { formatPaletteSessionAge } from '@/components/cmd-j/palette-session-age'
|
||||
import { resolvePaletteRepoForWorktree } from '@/lib/palette-repo-resolution'
|
||||
|
||||
export function WorktreeJumpPaletteWorkspaceTabRow({
|
||||
entry,
|
||||
renderKey,
|
||||
controller
|
||||
}: {
|
||||
entry: WorkspaceTabPaletteItem
|
||||
renderKey: string
|
||||
controller: WorktreeJumpPaletteController
|
||||
}): React.JSX.Element {
|
||||
const result = entry.result
|
||||
const workspaceTabWorktree = controller.worktreeMap.get(result.worktreeId)
|
||||
const workspaceTabWorktree = controller.resolveWorktree(
|
||||
result.worktreeId,
|
||||
result.executionHostId
|
||||
)
|
||||
const workspaceTabRepo = workspaceTabWorktree
|
||||
? controller.repoMap.get(workspaceTabWorktree.repoId)
|
||||
? resolvePaletteRepoForWorktree(
|
||||
workspaceTabWorktree,
|
||||
controller.repoMap,
|
||||
controller.repoByHostIdentity
|
||||
)
|
||||
: undefined
|
||||
const workspaceTabRepoName = workspaceTabRepo?.displayName ?? result.repoName
|
||||
const workspaceTabHostBadge = getPaletteHostBadge(
|
||||
@@ -33,13 +45,22 @@ export function WorktreeJumpPaletteWorkspaceTabRow({
|
||||
controller.hostOptions,
|
||||
controller.hostFilterActive
|
||||
)
|
||||
const WorkspaceTabIcon = result.contentType === 'terminal' ? SquareTerminal : FileText
|
||||
const recentRow = controller.hasQuery ? null : (controller.recentTabRowById.get(entry.id) ?? null)
|
||||
const recentRow = controller.recentTabRowByItem.get(entry) ?? null
|
||||
const fallback =
|
||||
result.contentType === 'terminal' && result.occupantAgent ? (
|
||||
<span className="inline-flex" data-agent-icon={result.occupantAgent} aria-hidden="true">
|
||||
<AgentIcon agent={result.occupantAgent} size={14} />
|
||||
</span>
|
||||
) : result.contentType === 'terminal' ? (
|
||||
<SquareTerminal className="size-3.5" aria-hidden="true" />
|
||||
) : (
|
||||
<FileText className="size-3.5" aria-hidden="true" />
|
||||
)
|
||||
const sessionAge = formatPaletteSessionAge(result.lastActiveAt ?? null, controller.paletteNowMs)
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
key={entry.id}
|
||||
value={entry.id}
|
||||
value={renderKey}
|
||||
onSelect={() => controller.handleSelectItem(entry)}
|
||||
className={cn(
|
||||
'group mx-0.5 flex cursor-pointer items-center gap-3 rounded-lg border border-transparent px-3 py-2.5 text-left outline-none transition-[background-color,border-color,box-shadow]',
|
||||
@@ -49,7 +70,7 @@ export function WorktreeJumpPaletteWorkspaceTabRow({
|
||||
<div className="flex h-5 w-4 shrink-0 items-center justify-center self-start text-muted-foreground/85">
|
||||
<PaletteRecentTabStatusDot
|
||||
row={recentRow}
|
||||
fallback={<WorkspaceTabIcon className="size-3.5" aria-hidden="true" />}
|
||||
fallback={fallback}
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 overflow-hidden">
|
||||
@@ -62,6 +83,7 @@ export function WorktreeJumpPaletteWorkspaceTabRow({
|
||||
secondaryRanges={result.secondaryRanges}
|
||||
worktreeName={result.worktreeName}
|
||||
worktreeRanges={result.worktreeRanges}
|
||||
sessionAge={sessionAge}
|
||||
leadingBadges={
|
||||
<>
|
||||
{result.isCurrentTab && (
|
||||
@@ -92,7 +114,7 @@ export function WorktreeJumpPaletteWorkspaceTabRow({
|
||||
</span>
|
||||
)}
|
||||
<PaletteRowShortcutBadge
|
||||
index={controller.recentTabShortcutIndexById.get(entry.id)}
|
||||
index={controller.recentTabShortcutIndexByItem.get(entry)}
|
||||
modifierKeys={controller.digitShortcutModifiers}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
import { cn } from '@/lib/utils'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { isRuntimeOwnedSshTargetId } from '../../../shared/execution-host'
|
||||
import { isPaletteCurrentWorktree, resolvePaletteRepoForWorktree } from '@/lib/palette-repo-resolution'
|
||||
import { formatPaletteSessionAge } from '@/components/cmd-j/palette-session-age'
|
||||
import type { WorktreePaletteItem } from './worktree-jump-palette-model'
|
||||
import type { WorktreeJumpPaletteController } from './use-worktree-jump-palette-controller'
|
||||
import {
|
||||
@@ -21,9 +23,11 @@ import {
|
||||
|
||||
export function WorktreeJumpPaletteWorktreeRow({
|
||||
entry,
|
||||
renderKey,
|
||||
controller
|
||||
}: {
|
||||
entry: WorktreePaletteItem
|
||||
renderKey: string
|
||||
controller: WorktreeJumpPaletteController
|
||||
}): React.JSX.Element {
|
||||
const {
|
||||
@@ -35,11 +39,20 @@ export function WorktreeJumpPaletteWorktreeRow({
|
||||
handleSelectItem
|
||||
} = controller
|
||||
const worktree = entry.worktree
|
||||
const repo = repoMap.get(worktree.repoId)
|
||||
const repo = resolvePaletteRepoForWorktree(
|
||||
worktree,
|
||||
repoMap,
|
||||
controller.repoByHostIdentity
|
||||
)
|
||||
const repoName = repo?.displayName ?? ''
|
||||
const branch = resolveWorktreeBranchLabel(worktree)
|
||||
const worktreeLabel = resolveWorktreeDisplayName(worktree)
|
||||
const isCurrentWorktree = activeWorktreeId === worktree.id
|
||||
const isCurrentWorktree = isPaletteCurrentWorktree(
|
||||
worktree,
|
||||
activeWorktreeId,
|
||||
controller.activeWorkspaceExecutionHostId
|
||||
)
|
||||
const sessionAge = formatPaletteSessionAge(worktree.lastActivityAt, controller.paletteNowMs)
|
||||
const sshConnectionId =
|
||||
repo?.connectionId && !isRuntimeOwnedSshTargetId(repo.connectionId) ? repo.connectionId : null
|
||||
const sshStatus = sshConnectionId
|
||||
@@ -50,8 +63,7 @@ export function WorktreeJumpPaletteWorktreeRow({
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
key={entry.id}
|
||||
value={entry.id}
|
||||
value={renderKey}
|
||||
onSelect={() => handleSelectItem(entry)}
|
||||
data-current={isCurrentWorktree ? 'true' : undefined}
|
||||
className={cn(
|
||||
@@ -88,6 +100,18 @@ export function WorktreeJumpPaletteWorktreeRow({
|
||||
<span className="truncate text-[14px] font-semibold text-foreground">
|
||||
<HighlightedText text={worktreeLabel} matchRanges={entry.match.displayNameRanges} />
|
||||
</span>
|
||||
{sessionAge ? (
|
||||
<span
|
||||
aria-label={translate(
|
||||
'auto.components.WorktreeJumpPalette.lastActiveTime',
|
||||
'Last active {{value0}} ago',
|
||||
{ value0: sessionAge }
|
||||
)}
|
||||
className="shrink-0 text-[11px] font-medium tabular-nums text-muted-foreground/70"
|
||||
>
|
||||
{sessionAge}
|
||||
</span>
|
||||
) : null}
|
||||
{isCurrentWorktree && (
|
||||
<span className="shrink-0 self-center rounded-[6px] border border-border/60 bg-background/45 px-1.5 py-px text-[9px] font-medium leading-normal text-muted-foreground/88">
|
||||
{translate('auto.components.WorktreeJumpPalette.556e7232ca', 'Current')}
|
||||
@@ -98,10 +122,14 @@ export function WorktreeJumpPaletteWorktreeRow({
|
||||
{translate('auto.components.WorktreeJumpPalette.739bda980c', 'primary')}
|
||||
</span>
|
||||
)}
|
||||
<span className="shrink-0 text-muted-foreground/45">·</span>
|
||||
<span className="truncate text-[12px] font-medium text-muted-foreground/92">
|
||||
<HighlightedText text={branch} matchRanges={entry.match.branchRanges} />
|
||||
</span>
|
||||
{branch.trim().length > 0 ? (
|
||||
<>
|
||||
<span className="shrink-0 text-muted-foreground/45">·</span>
|
||||
<span className="truncate text-[12px] font-medium text-muted-foreground/92">
|
||||
<HighlightedText text={branch} matchRanges={entry.match.branchRanges} />
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
{entry.match.supportingText && (
|
||||
<div className="mt-1.5 flex min-w-0 items-center gap-2 text-[12px] leading-5 text-muted-foreground/88">
|
||||
|
||||
Reference in New Issue
Block a user