From 72883c8fbceea8a13ec49ce7c60527a45e06169c Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Sat, 13 Jun 2026 02:29:39 -0700 Subject: [PATCH] Route typed GitHub lookups by source host Co-authored-by: Orca --- .../host-context-implementation-checklist.md | 1 + .../src/components/WorktreeJumpPalette.tsx | 41 +++++++--- .../new-workspace/SmartWorkspaceNameField.tsx | 22 ++++-- ...mp-palette-source-context-boundary.test.ts | 31 ++++++++ ...poserState-host-context-boundaries.test.ts | 18 +++++ src/renderer/src/hooks/useComposerState.ts | 60 ++++++++++++--- .../src/lib/github-work-item-source-lookup.ts | 76 +++++++++++++++++++ 7 files changed, 220 insertions(+), 29 deletions(-) create mode 100644 src/renderer/src/components/worktree-jump-palette-source-context-boundary.test.ts create mode 100644 src/renderer/src/lib/github-work-item-source-lookup.ts diff --git a/notes/host-context-implementation-checklist.md b/notes/host-context-implementation-checklist.md index 38294c25ba8..1fbd304426c 100644 --- a/notes/host-context-implementation-checklist.md +++ b/notes/host-context-implementation-checklist.md @@ -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. diff --git a/src/renderer/src/components/WorktreeJumpPalette.tsx b/src/renderer/src/components/WorktreeJumpPalette.tsx index ce7a3f5d9eb..5cc6aeb4de4 100644 --- a/src/renderer/src/components/WorktreeJumpPalette.tsx +++ b/src/renderer/src/components/WorktreeJumpPalette.tsx @@ -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 diff --git a/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.tsx b/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.tsx index d8cef3f1915..51740a51062 100644 --- a/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.tsx +++ b/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.tsx @@ -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, - workItemByOwnerRepo: (args) => - window.api.gh.workItemByOwnerRepo(args) as Promise + 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, - workItemByOwnerRepo: (args) => - window.api.gh.workItemByOwnerRepo(args) as Promise + 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, diff --git a/src/renderer/src/components/worktree-jump-palette-source-context-boundary.test.ts b/src/renderer/src/components/worktree-jump-palette-source-context-boundary.test.ts new file mode 100644 index 00000000000..ac60d86d2eb --- /dev/null +++ b/src/renderer/src/components/worktree-jump-palette-source-context-boundary.test.ts @@ -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') + }) +}) diff --git a/src/renderer/src/hooks/useComposerState-host-context-boundaries.test.ts b/src/renderer/src/hooks/useComposerState-host-context-boundaries.test.ts index 4fb71781705..75ce1b86d14 100644 --- a/src/renderer/src/hooks/useComposerState-host-context-boundaries.test.ts +++ b/src/renderer/src/hooks/useComposerState-host-context-boundaries.test.ts @@ -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') + }) }) diff --git a/src/renderer/src/hooks/useComposerState.ts b/src/renderer/src/hooks/useComposerState.ts index 5319df6119b..1c1ee1d861f 100644 --- a/src/renderer/src/hooks/useComposerState.ts +++ b/src/renderer/src/hooks/useComposerState.ts @@ -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(() => { 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, - workItemByOwnerRepo: (args) => - window.api.gh.workItemByOwnerRepo(args) as Promise + 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 diff --git a/src/renderer/src/lib/github-work-item-source-lookup.ts b/src/renderer/src/lib/github-work-item-source-lookup.ts new file mode 100644 index 00000000000..cfba24b672c --- /dev/null +++ b/src/renderer/src/lib/github-work-item-source-lookup.ts @@ -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): string { + return args.sourceContext?.repoId ?? args.repoId +} + +export async function lookupGitHubWorkItemForSource( + args: GitHubWorkItemLookupArgs +): Promise { + const target = getActiveRuntimeTarget(getTaskSourceRuntimeSettings(args.sourceContext)) + const item = + target.kind === 'environment' + ? await callRuntimeRpc | 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 { + const target = getActiveRuntimeTarget(getTaskSourceRuntimeSettings(args.sourceContext)) + const item = + target.kind === 'environment' + ? await callRuntimeRpc | 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 +}