diff --git a/notes/host-context-implementation-checklist.md b/notes/host-context-implementation-checklist.md index 87f87b7f0bb..61ea1abd1bf 100644 --- a/notes/host-context-implementation-checklist.md +++ b/notes/host-context-implementation-checklist.md @@ -268,3 +268,4 @@ Done means: reviewers and future agents can understand the vision, implementatio - [x] 2026-06-12: Completed Tasks account/host metadata by showing repo-backed account labels in source summaries when GitHub/GitLab accounts differ by Host, while preserving host labels, provider identities, Linear workspace names, Jira site names, and unavailable-host status in labels/titles. Verified with source-summary and repo picker tests, targeted oxlint, full `pnpm run typecheck`, and `git diff --check`. - [x] 2026-06-12: Continued the automation project-id migration by changing CLI automation show output to foreground explicit `runContext` fields (`runProjectId`, `runHostId`, `projectHostSetupId`, `runRepoId`, `runPath`) and label the old repo id as `legacyRepoId`. The checklist row remains partial because the shared `Automation.projectId` compatibility field still exists. Verified with CLI format/index tests, targeted oxlint, full `pnpm run typecheck`, and `git diff --check`. - [x] 2026-06-12: Aligned Automations manual-run disabled states with the backend run-target resolver so saved run contexts now disable Run Now when the project-host setup is missing or not ready, instead of failing after dispatch. Verified with focused automation availability tests, targeted oxlint, full `pnpm run typecheck`, and `git diff --check`. +- [x] 2026-06-12: Routed GitHub drawer label/assignee/reviewer metadata reads through the same task source context as issue/PR mutations, including slug-addressed Project view rows and reviewer request/remove actions, so same-repo multi-host drawers do not fall back to the repo owner or focused runtime. Verified with a focused source-boundary test, targeted oxlint, full `pnpm run typecheck`, and `git diff --check`. diff --git a/src/renderer/src/components/GitHubItemDialog.tsx b/src/renderer/src/components/GitHubItemDialog.tsx index 3c619723294..c5773669e44 100644 --- a/src/renderer/src/components/GitHubItemDialog.tsx +++ b/src/renderer/src/components/GitHubItemDialog.tsx @@ -167,7 +167,10 @@ import type { PRCheckDetail, PRComment } from '../../../shared/types' -import type { TaskSourceContext } from '../../../shared/task-source-context' +import { + getTaskSourceRuntimeSettings, + type TaskSourceContext +} from '../../../shared/task-source-context' import { PER_REPO_FETCH_LIMIT } from '../../../shared/work-items' import { translate } from '@/i18n/i18n' import { getSettingsForRepoRuntimeOwner } from '@/lib/repo-runtime-owner' @@ -456,10 +459,19 @@ function PRReviewersPanel({ reviewRequests: item.reviewRequests })) const patchWorkItem = useAppStore((s) => s.patchWorkItem) - const settings = useAppStore((s) => s.settings) const repoOwnerSettings = useAppStore( useShallow((s) => getSettingsForRepoRuntimeOwner(s, item.repoId ?? null)) ) + const sourceSettings = useMemo( + () => + sourceContext?.provider === 'github' + ? ({ + ...repoOwnerSettings, + ...getTaskSourceRuntimeSettings(sourceContext) + } as typeof repoOwnerSettings) + : repoOwnerSettings, + [repoOwnerSettings, sourceContext] + ) const reviewerInputRef = useRef(null) const reviewerInputFocusFrameRef = useRef(null) const reviewerPanelMountedRef = useRef(true) @@ -534,11 +546,12 @@ function PRReviewersPanel({ open && reviewSlug ? reviewSlug.owner : null, open && reviewSlug ? reviewSlug.repo : null, reviewerSeedUsers.map((user) => user.login), - settings + sourceSettings ) const reviewerMetadataByPath = useRepoAssignees( open && !reviewSlug ? repoPath : null, - open && !reviewSlug ? item.repoId : null + open && !reviewSlug ? item.repoId : null, + sourceSettings ) const reviewerMetadata = reviewSlug ? reviewerMetadataBySlug : reviewerMetadataByPath const displayItem = { ...item, reviewRequests: localReviewRequests } @@ -638,7 +651,7 @@ function PRReviewersPanel({ item.reviewRequests !== undefined || item.latestReviews !== undefined const canRequestReview = - !!repoPath || getActiveRuntimeTarget(repoOwnerSettings).kind === 'environment' + !!repoPath || getActiveRuntimeTarget(sourceSettings).kind === 'environment' const measureReviewerPickerPlacement = useCallback(() => { const rect = reviewerInputRef.current?.getBoundingClientRect() @@ -681,7 +694,7 @@ function PRReviewersPanel({ ) return } - const target = getActiveRuntimeTarget(repoOwnerSettings) + const target = getActiveRuntimeTarget(sourceSettings) if (target.kind !== 'environment' && !repoPath) { toast.error( translate( @@ -759,7 +772,7 @@ function PRReviewersPanel({ if (logins.length === 0) { return } - const target = getActiveRuntimeTarget(repoOwnerSettings) + const target = getActiveRuntimeTarget(sourceSettings) if (target.kind !== 'environment' && !repoPath) { toast.error( translate( @@ -4545,6 +4558,19 @@ function GHEditSection({ const assigneesItemKey = `${item.repoId}\0${item.id}` const patchWorkItem = useAppStore((s) => s.patchWorkItem) const patchProjectRowContent = useAppStore((s) => s.patchProjectRowContent) + const repoOwnerSettings = useAppStore( + useShallow((s) => getSettingsForRepoRuntimeOwner(s, item.repoId ?? null)) + ) + const sourceSettings = useMemo( + () => + sourceContext?.provider === 'github' + ? ({ + ...repoOwnerSettings, + ...getTaskSourceRuntimeSettings(sourceContext) + } as typeof repoOwnerSettings) + : repoOwnerSettings, + [repoOwnerSettings, sourceContext] + ) const { isPending, run } = useImmediateMutation() // Why: when the dialog opens from a Project view, mutations route through // *BySlug IPCs and we must keep `projectViewCache` in sync alongside @@ -4568,16 +4594,18 @@ function GHEditSection({ const slugRepo = projectOrigin?.repo ?? null const repoLabelsByPath = useRepoLabels( projectOrigin ? null : repoPath, - projectOrigin ? null : repoId + projectOrigin ? null : repoId, + sourceSettings ) - const repoLabelsBySlug = useRepoLabelsBySlug(slugOwner, slugRepo) + const repoLabelsBySlug = useRepoLabelsBySlug(slugOwner, slugRepo, sourceSettings) const repoLabels = projectOrigin ? repoLabelsBySlug : repoLabelsByPath const repositoryLabelsUrl = useMemo(() => getGitHubRepositoryLabelsUrl(item.url), [item.url]) const repoAssigneesByPath = useRepoAssignees( projectOrigin ? null : repoPath, - projectOrigin ? null : repoId + projectOrigin ? null : repoId, + sourceSettings ) - const repoAssigneesBySlug = useRepoAssigneesBySlug(slugOwner, slugRepo, assignees) + const repoAssigneesBySlug = useRepoAssigneesBySlug(slugOwner, slugRepo, assignees, sourceSettings) const repoAssignees = projectOrigin ? repoAssigneesBySlug : repoAssigneesByPath const hasAttachedWorkspace = attachedWorkspaceLabel !== null && attachedWorkspaceLabel !== undefined diff --git a/src/renderer/src/components/github-item-dialog-source-boundary.test.ts b/src/renderer/src/components/github-item-dialog-source-boundary.test.ts new file mode 100644 index 00000000000..1e70364f09c --- /dev/null +++ b/src/renderer/src/components/github-item-dialog-source-boundary.test.ts @@ -0,0 +1,43 @@ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' + +const COMPONENT_ROOT = __dirname + +function componentSource(relativePath: string): string { + return readFileSync(join(COMPONENT_ROOT, relativePath), 'utf8') +} + +function sourceBetween(source: string, 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('GitHubItemDialog source host boundaries', () => { + it('routes reviewer metadata and reviewer mutations through the task source context', () => { + const source = componentSource('GitHubItemDialog.tsx') + const section = sourceBetween(source, 'function PRReviewersPanel', 'function isPRFileViewed') + + expect(section).toContain('getTaskSourceRuntimeSettings(sourceContext)') + expect(section).toContain('useRepoAssigneesBySlug(') + expect(section).toContain('sourceSettings') + expect(section).toContain('useRepoAssignees(') + expect(section).toContain('sourceSettings') + expect(section).toContain('getActiveRuntimeTarget(sourceSettings)') + }) + + it('routes edit metadata through the same task source as issue mutations', () => { + const source = componentSource('GitHubItemDialog.tsx') + const section = sourceBetween(source, 'function GHEditSection', 'const hasAttachedWorkspace') + + expect(section).toContain('getTaskSourceRuntimeSettings(sourceContext)') + expect(section).toContain('useRepoLabels(') + expect(section).toContain('useRepoLabelsBySlug(slugOwner, slugRepo, sourceSettings)') + expect(section).toContain('useRepoAssignees(') + expect(section).toContain('useRepoAssigneesBySlug(') + expect(section).toContain('sourceSettings') + }) +}) diff --git a/src/renderer/src/hooks/useIssueMetadata.ts b/src/renderer/src/hooks/useIssueMetadata.ts index 0ba6f1bd6d6..ea1e306ca13 100644 --- a/src/renderer/src/hooks/useIssueMetadata.ts +++ b/src/renderer/src/hooks/useIssueMetadata.ts @@ -31,6 +31,7 @@ type MetadataState = { type GitHubMetadataOptions = { runtimeEnvironmentId?: string | null + activeRuntimeEnvironmentId?: string | null } // ─── GitHub ──────────────────────────────────────────────── @@ -54,7 +55,8 @@ export function useRepoLabels( if (!repoPath && !repoId) { return } - const runtimeEnvironmentId = options?.runtimeEnvironmentId?.trim() || null + const runtimeEnvironmentId = + options?.runtimeEnvironmentId?.trim() || options?.activeRuntimeEnvironmentId?.trim() || null const repoSelector = repoId ?? repoPath ?? '' // Why: SSH/runtime metadata must not reuse host-path cache entries; the same // repo id may resolve through a different credential/runtime boundary. @@ -107,7 +109,7 @@ export function useRepoLabels( error: err instanceof Error ? err.message : 'Failed to load labels' })) }) - }, [repoPath, repoId, options?.runtimeEnvironmentId]) + }, [repoPath, repoId, options?.runtimeEnvironmentId, options?.activeRuntimeEnvironmentId]) return state } @@ -128,7 +130,8 @@ export function useRepoAssignees( if (!repoPath && !repoId) { return } - const runtimeEnvironmentId = options?.runtimeEnvironmentId?.trim() || null + const runtimeEnvironmentId = + options?.runtimeEnvironmentId?.trim() || options?.activeRuntimeEnvironmentId?.trim() || null const repoSelector = repoId ?? repoPath ?? '' // Why: SSH/runtime metadata must not reuse host-path cache entries; the same // repo id may resolve through a different credential/runtime boundary. @@ -181,7 +184,7 @@ export function useRepoAssignees( error: err instanceof Error ? err.message : 'Failed to load assignees' })) }) - }, [repoPath, repoId, options?.runtimeEnvironmentId]) + }, [repoPath, repoId, options?.runtimeEnvironmentId, options?.activeRuntimeEnvironmentId]) return state }