Route typed GitHub lookups by source host

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-06-13 02:29:39 -07:00
co-authored by Orca
parent 83e5f43872
commit 72883c8fbc
7 changed files with 220 additions and 29 deletions
@@ -294,5 +294,6 @@ Done means: reviewers and future agents can understand the vision, implementatio
- [x] 2026-06-13: Fed saved automation source contexts into Run Now availability so internal Orca automations now disable before dispatch when the saved source host is unavailable, still checking capabilities, missing task-source support, missing GitHub/GitLab auth, or missing provider tooling. Verified with focused automation availability tests, provider availability tests, targeted oxlint, `git diff --check`, and full `pnpm run typecheck`.
- [x] 2026-06-13: Closed another request/cache ownership slice by routing right-sidebar search, create-review field generation, browser tab default profile selection, Smart Workspace GitHub/GitLab/Linear lookups, GitHub Project browse cache, Project slug comments, and standalone GitHub issue comments through explicit worktree/repo/source ownership instead of ambient focused runtime state. Verified with focused ownership/browser/smart-submit tests, targeted oxlint, `git diff --check`, and full `pnpm run typecheck`.
- [x] 2026-06-13: Routed Quick Open file listing through the selected worktree runtime owner instead of ambient focused settings, matching File Explorer and sidebar search ownership. Verified with focused Quick Open and file-owner boundary tests, targeted oxlint, `git diff --check`, and full `pnpm run typecheck`.
- [x] 2026-06-13: Routed typed GitHub issue/PR lookup entry points in Cmd-J, Smart Workspace cross-repo accept, and Composer submit fallback through source-aware local/SSH/runtime lookup helpers instead of direct preload calls. Verified with focused composer/palette/smart-submit tests, targeted oxlint, `git diff --check`, and full `pnpm run typecheck`.
- [~] 2026-06-13: Remaining source-error gap: unavailable hosts, version-skewed runtimes, and GitHub/GitLab missing auth/tooling are surfaced for desktop-owned and runtime-owned source hosts, but unsupported-provider still has no production emitter per source host.
- [~] 2026-06-13: Remaining Automation gap: run-host/source-host availability and missing project-host setup are handled for manual/internal/external actions, but `Automation.projectId` remains the legacy repo-id compatibility field.
@@ -81,9 +81,14 @@ import {
getComposerEligibleRepos,
resolveComposerGitRepoId
} from '@/lib/new-workspace-composer-repo'
import {
lookupGitHubWorkItemByOwnerRepoForSource,
lookupGitHubWorkItemForSource
} from '@/lib/github-work-item-source-lookup'
import type { SettingsNavTarget } from '@/lib/settings-navigation-types'
import type { BrowserPage, BrowserWorkspace, Worktree } from '../../../shared/types'
import { isGitRepoKind } from '../../../shared/repo-kind'
import { buildTaskSourceContextFromRepo } from '../../../shared/task-source-context'
import { translate } from '@/i18n/i18n'
type WorktreePaletteItem = {
@@ -1183,6 +1188,11 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
}
prefetchCreateWorkspaceBaseForComposer(repoForLookup.id)
const sourceContext = buildTaskSourceContextFromRepo({
provider: 'github',
projectId: repoForLookup.id,
repo: repoForLookup
})
// Why: awaiting inside the user gesture would leave the palette open
// indefinitely on slow networks. Close immediately and populate the
// composer once the lookup returns.
@@ -1190,15 +1200,15 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
preserveCreateLookupOnCloseRef.current = true
recordFeatureInteraction('cmd-j-create-workspace')
closeModal()
void window.api.gh
.workItemByOwnerRepo({
repoPath: repoForLookup.path,
repoId: repoForLookup.id,
owner: slug.owner,
repo: slug.repo,
number,
type: ghLink.type
})
void lookupGitHubWorkItemByOwnerRepoForSource({
repoPath: repoForLookup.path,
repoId: repoForLookup.id,
sourceContext,
owner: slug.owner,
repo: slug.repo,
number,
type: ghLink.type
})
.then((item) => {
if (!createLookupGuard.isCurrent(lookupToken)) {
return
@@ -1260,12 +1270,21 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
}
prefetchCreateWorkspaceBaseForComposer(repoForLookup.id)
const sourceContext = buildTaskSourceContextFromRepo({
provider: 'github',
projectId: repoForLookup.id,
repo: repoForLookup
})
const lookupToken = createLookupGuard.start()
preserveCreateLookupOnCloseRef.current = true
recordFeatureInteraction('cmd-j-create-workspace')
closeModal()
void window.api.gh
.workItem({ repoPath: repoForLookup.path, repoId: repoForLookup.id, number: ghNumber })
void lookupGitHubWorkItemForSource({
repoPath: repoForLookup.path,
repoId: repoForLookup.id,
sourceContext,
number: ghNumber
})
.then((item) => {
if (!createLookupGuard.isCurrent(lookupToken)) {
return
@@ -37,6 +37,10 @@ import {
parseGitHubIssueOrPRLink,
type RepoSlug
} from '@/lib/github-links'
import {
lookupGitHubWorkItemByOwnerRepoForSource,
lookupGitHubWorkItemForSource
} from '@/lib/github-work-item-source-lookup'
import { lookupSmartGitHubSubmitItem } from '@/lib/smart-github-submit'
import { parseGitLabIssueOrMRLink } from '@/lib/gitlab-links'
import { getLocalPreflightContext, localPreflightContextKey } from '@/lib/local-preflight-context'
@@ -401,9 +405,8 @@ export default function SmartWorkspaceNameField({
number: directLink.number,
type: directLink.type
},
workItem: (args) => window.api.gh.workItem(args) as Promise<GitHubWorkItem | null>,
workItemByOwnerRepo: (args) =>
window.api.gh.workItemByOwnerRepo(args) as Promise<GitHubWorkItem | null>
workItem: lookupGitHubWorkItemForSource,
workItemByOwnerRepo: lookupGitHubWorkItemByOwnerRepoForSource
})
if (!stale) {
setGithubItems(item ? [item] : [])
@@ -447,9 +450,8 @@ export default function SmartWorkspaceNameField({
repoId: selectedRepo.id,
sourceContext: githubSourceContext,
intent,
workItem: (args) => window.api.gh.workItem(args) as Promise<GitHubWorkItem | null>,
workItemByOwnerRepo: (args) =>
window.api.gh.workItemByOwnerRepo(args) as Promise<GitHubWorkItem | null>
workItem: lookupGitHubWorkItemForSource,
workItemByOwnerRepo: lookupGitHubWorkItemByOwnerRepoForSource
})
void request
.then((item) => {
@@ -869,9 +871,15 @@ export default function SmartWorkspaceNameField({
handledCrossRepoUrlRef.current = debouncedQuery.trim()
setGithubLoading(true)
try {
const item = await window.api.gh.workItemByOwnerRepo({
const sourceContext = buildTaskSourceContextFromRepo({
provider: 'github',
projectId: targetRepo.id,
repo: targetRepo
})
const item = await lookupGitHubWorkItemByOwnerRepoForSource({
repoPath: targetRepo.path,
repoId: targetRepo.id,
sourceContext,
owner: crossRepoPrompt.link.slug.owner,
repo: crossRepoPrompt.link.slug.repo,
number: crossRepoPrompt.link.number,
@@ -0,0 +1,31 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
const source = readFileSync(join(__dirname, 'WorktreeJumpPalette.tsx'), 'utf8')
function sourceBetween(startPattern: string, endPattern: string): string {
const start = source.indexOf(startPattern)
expect(start).toBeGreaterThanOrEqual(0)
const end = source.indexOf(endPattern, start + startPattern.length)
expect(end).toBeGreaterThan(start)
return source.slice(start, end)
}
describe('WorktreeJumpPalette source-context boundaries', () => {
it('resolves typed GitHub issue/PR entries through the lookup repo source host', () => {
expect(source).toContain('buildTaskSourceContextFromRepo')
const githubLinkSection = sourceBetween(
'void lookupGitHubWorkItemByOwnerRepoForSource({',
'// Case 2: user typed a raw issue number.'
)
expect(githubLinkSection).toContain('sourceContext')
const rawNumberSection = sourceBetween(
'void lookupGitHubWorkItemForSource({',
'.then((item) => {'
)
expect(rawNumberSection).toContain('sourceContext')
})
})
@@ -80,4 +80,22 @@ describe('useComposerState host-context boundaries', () => {
expect(section).toContain('hostId: initialRunSeed.hostId')
expect(section).toContain('projectHostSetupId: initialRunSeed.projectHostSetupId')
})
it('resolves typed GitHub issue/PR input through the selected repo source context', () => {
expect(HOOK_SOURCE).toContain('const selectedRepoGitHubSourceContext = useMemo')
const directLookup = sourceBetween(
HOOK_SOURCE,
'void window.api.gh',
'const applyLinkedWorkItem = useCallback'
)
expect(directLookup).toContain('sourceContext: selectedRepoGitHubSourceContext')
const submitLookup = sourceBetween(
HOOK_SOURCE,
'const resolvePendingSmartGitHubSubmit',
'const resolution = getSmartGitHubSubmitResolution(item)'
)
expect(submitLookup).toContain('sourceContext: selectedRepoGitHubSourceContext')
})
})
+49 -11
View File
@@ -83,6 +83,10 @@ import {
getSmartGitHubSubmitResolution,
type SmartGitHubSubmitResolution
} from '@/lib/smart-github-submit'
import {
lookupGitHubWorkItemByOwnerRepoForSource,
lookupGitHubWorkItemForSource
} from '@/lib/github-work-item-source-lookup'
import { isWorkItemLookupText } from '@/lib/work-item-lookup-text'
import {
canUseRepoBackedComposerSources,
@@ -566,6 +570,34 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
selectedRepo,
selectedWorkspaceTarget
])
const selectedRepoGitHubSourceContext = useMemo(() => {
if (!selectedRepo || !selectedRepoIsGit) {
return null
}
if (taskSourceContext?.provider === 'github') {
return taskSourceContext
}
if (selectedWorkspaceTarget.status === 'ready') {
const selectedProject = projects.find(
(project) => project.id === selectedWorkspaceTarget.target.projectId
)
return buildTaskSourceContextFromRepo({
provider: 'github',
projectId: selectedWorkspaceTarget.target.projectId,
repo: selectedRepo,
projectHostSetupId: selectedWorkspaceTarget.target.projectHostSetupId,
providerIdentity:
selectedProject?.providerIdentity?.provider === 'github'
? selectedProject.providerIdentity
: null
})
}
return buildTaskSourceContextFromRepo({
provider: 'github',
projectId: selectedRepo.id,
repo: selectedRepo
})
}, [projects, selectedRepo, selectedRepoIsGit, selectedWorkspaceTarget, taskSourceContext])
const [linkedIssue, setLinkedIssue] = useState<string>(() => {
if (persistDraft && newWorkspaceDraft?.linkedIssue) {
return newWorkspaceDraft.linkedIssue
@@ -1326,12 +1358,12 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
// resolving direct lookups against the selected repo instead of requiring a
// text match in the recent-items list.
const lookupRepoId = selectedRepo.id
void window.api.gh
.workItem({
repoPath: selectedRepo.path,
repoId: selectedRepo.id,
number: normalizedLinkQuery.directNumber
})
void lookupGitHubWorkItemForSource({
repoPath: selectedRepo.path,
repoId: selectedRepo.id,
sourceContext: selectedRepoGitHubSourceContext,
number: normalizedLinkQuery.directNumber
})
.then((item) => {
if (!cancelled) {
setLinkDirectItem(
@@ -1353,7 +1385,13 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
return () => {
cancelled = true
}
}, [linkPopoverOpen, normalizedLinkQuery.directNumber, selectedRepo, selectedRepoIsGit])
}, [
linkPopoverOpen,
normalizedLinkQuery.directNumber,
selectedRepo,
selectedRepoGitHubSourceContext,
selectedRepoIsGit
])
const applyLinkedWorkItem = useCallback(
(item: GitHubWorkItem, options: { preserveBranchNameOverride?: boolean } = {}): void => {
@@ -1404,10 +1442,10 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
const item = await lookupSmartGitHubSubmitItem({
repoPath: selectedRepo.path,
repoId: selectedRepo.id,
sourceContext: selectedRepoGitHubSourceContext,
intent,
workItem: (args) => window.api.gh.workItem(args) as Promise<GitHubWorkItem | null>,
workItemByOwnerRepo: (args) =>
window.api.gh.workItemByOwnerRepo(args) as Promise<GitHubWorkItem | null>
workItem: lookupGitHubWorkItemForSource,
workItemByOwnerRepo: lookupGitHubWorkItemByOwnerRepoForSource
})
if (!item) {
throw new Error('Could not resolve the GitHub item before creating the workspace.')
@@ -1430,7 +1468,7 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
branchAutoNameRef.current = ''
setStartFromResetHint(null)
return resolution
}, [linkedWorkItem, name, selectedRepo, selectedRepoIsGit])
}, [linkedWorkItem, name, selectedRepo, selectedRepoGitHubSourceContext, selectedRepoIsGit])
// Why: parallel of applyLinkedWorkItem for GitLab. Touches the GitLab
// state slots only — the GitHub linkedIssue/linkedPR remain unchanged
@@ -0,0 +1,76 @@
import type { GitHubWorkItem } from '../../../shared/types'
import type { TaskSourceContext } from '../../../shared/task-source-context'
import { getTaskSourceRuntimeSettings } from '../../../shared/task-source-context'
import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
type GitHubWorkItemLookupArgs = {
repoPath: string
repoId: string
sourceContext?: TaskSourceContext | null
number: number
type?: 'issue' | 'pr'
}
type GitHubWorkItemByOwnerRepoLookupArgs = GitHubWorkItemLookupArgs & {
owner: string
repo: string
type: 'issue' | 'pr'
}
function runtimeRepoId(args: Pick<GitHubWorkItemLookupArgs, 'repoId' | 'sourceContext'>): string {
return args.sourceContext?.repoId ?? args.repoId
}
export async function lookupGitHubWorkItemForSource(
args: GitHubWorkItemLookupArgs
): Promise<GitHubWorkItem | null> {
const target = getActiveRuntimeTarget(getTaskSourceRuntimeSettings(args.sourceContext))
const item =
target.kind === 'environment'
? await callRuntimeRpc<Omit<GitHubWorkItem, 'repoId'> | null>(
target,
'github.workItem',
{
repo: runtimeRepoId(args),
number: args.number,
type: args.type
},
{ timeoutMs: 30_000 }
)
: await window.api.gh.workItem({
repoPath: args.repoPath,
repoId: args.repoId,
number: args.number,
type: args.type
})
return item ? ({ ...item, repoId: args.repoId } as GitHubWorkItem) : null
}
export async function lookupGitHubWorkItemByOwnerRepoForSource(
args: GitHubWorkItemByOwnerRepoLookupArgs
): Promise<GitHubWorkItem | null> {
const target = getActiveRuntimeTarget(getTaskSourceRuntimeSettings(args.sourceContext))
const item =
target.kind === 'environment'
? await callRuntimeRpc<Omit<GitHubWorkItem, 'repoId'> | null>(
target,
'github.workItemByOwnerRepo',
{
repo: runtimeRepoId(args),
owner: args.owner,
ownerRepo: args.repo,
number: args.number,
type: args.type
},
{ timeoutMs: 30_000 }
)
: await window.api.gh.workItemByOwnerRepo({
repoPath: args.repoPath,
repoId: args.repoId,
owner: args.owner,
repo: args.repo,
number: args.number,
type: args.type
})
return item ? ({ ...item, repoId: args.repoId } as GitHubWorkItem) : null
}