mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 16:03:17 +00:00
Carry host on smart-submit link intents
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -50,6 +50,7 @@ import {
|
||||
GITHUB_PROJECT_REF_INPUT_TOO_LARGE_ERROR,
|
||||
isGitHubProjectRefInputTooLarge
|
||||
} from '../../shared/github-project-ref-input'
|
||||
import { githubProjectHost } from '../../shared/github-project-identity'
|
||||
|
||||
// Re-export the public API so existing `./project-view` call sites keep working; the split is internal-only.
|
||||
export { isValidOwnerSlug, isValidRepoSlug, isValidSlug } from './project-view/internals'
|
||||
@@ -1318,6 +1319,7 @@ export async function getProjectViewTable(
|
||||
const table: GitHubProjectTable = {
|
||||
project: {
|
||||
id: project.id,
|
||||
host: githubProjectHost(args.host),
|
||||
owner: args.owner,
|
||||
ownerType: args.ownerType,
|
||||
number: args.projectNumber,
|
||||
@@ -1363,7 +1365,7 @@ type RawViewerDiscovery = {
|
||||
export async function listAccessibleProjects(
|
||||
args?: ListAccessibleProjectsArgs
|
||||
): Promise<ListAccessibleProjectsResult> {
|
||||
const host = args?.host
|
||||
const host = githubProjectHost(args?.host)
|
||||
const viewerProjects: GitHubProjectSummary[] = []
|
||||
const orgProjects: GitHubProjectSummary[] = []
|
||||
// Why: collect per-org failures so the picker shows a "some orgs didn't load" banner instead of aborting discovery on the first 504.
|
||||
@@ -1416,6 +1418,7 @@ export async function listAccessibleProjects(
|
||||
n.owner?.__typename === 'Organization' ? 'organization' : 'user'
|
||||
viewerProjects.push({
|
||||
id: n.id,
|
||||
host,
|
||||
owner: ownerLogin,
|
||||
ownerType,
|
||||
number: n.number,
|
||||
@@ -1490,6 +1493,7 @@ export async function listAccessibleProjects(
|
||||
}
|
||||
orgProjects.push({
|
||||
id: n.id,
|
||||
host,
|
||||
owner: login,
|
||||
ownerType: 'organization',
|
||||
number: n.number,
|
||||
@@ -1710,6 +1714,7 @@ export async function resolveProjectRef(
|
||||
ownerType,
|
||||
number: parsed.number,
|
||||
title: p.title ?? '',
|
||||
host: githubProjectHost(args.host),
|
||||
// Why: forward the view number from /views/{n} URLs so the renderer can skip the view-pick step (bare owner/number shorthand carries none).
|
||||
...(parsed.kind !== 'bare' && parsed.viewNumber !== undefined
|
||||
? { viewNumber: parsed.viewNumber }
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
type RateLimitBucketKind
|
||||
} from '../rate-limit'
|
||||
import type { GitHubProjectViewError } from '../../../shared/github-project-types'
|
||||
import { githubProjectHost } from '../../../shared/github-project-identity'
|
||||
import {
|
||||
classifyProjectError,
|
||||
driftError,
|
||||
@@ -38,7 +39,7 @@ export type ProjectGhExecOptions = { cwd?: string; host?: string }
|
||||
// Why: owner/project-addressed calls carry only a host (no repo slug), so they
|
||||
// can't reuse githubHostExecOptions — mirror its host pinning for that shape.
|
||||
export function projectGhExecOptions(host: string | undefined): ProjectGhExecOptions {
|
||||
return host ? { host } : {}
|
||||
return { host: githubProjectHost(host) }
|
||||
}
|
||||
|
||||
// ─── Slug validation ──────────────────────────────────────────────────
|
||||
|
||||
@@ -111,7 +111,8 @@ const GitHubProjectRef = z
|
||||
.object({
|
||||
owner: z.string(),
|
||||
ownerType: z.enum(['organization', 'user']),
|
||||
number: z.number().int()
|
||||
number: z.number().int(),
|
||||
host: z.string().optional()
|
||||
})
|
||||
.strict()
|
||||
const GitHubProjectSettings = z
|
||||
|
||||
@@ -1477,7 +1477,7 @@ export type PreloadApi = {
|
||||
repoSlug: (args: {
|
||||
repoPath: string
|
||||
repoId?: string
|
||||
}) => Promise<{ owner: string; repo: string } | null>
|
||||
}) => Promise<{ owner: string; repo: string; host?: string } | null>
|
||||
repoUpstream: (args: {
|
||||
repoPath: string
|
||||
repoId?: string
|
||||
|
||||
@@ -36,11 +36,16 @@ import {
|
||||
rememberProjectPickerBrowseCacheEntry
|
||||
} from './project-picker-browse-cache'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import {
|
||||
githubProjectHost,
|
||||
githubProjectIdentityKey
|
||||
} from '../../../../shared/github-project-identity'
|
||||
|
||||
export type ResolvedProjectSelection = {
|
||||
owner: string
|
||||
ownerType: GitHubProjectOwnerType
|
||||
projectNumber: number
|
||||
host?: string
|
||||
viewId?: string
|
||||
}
|
||||
|
||||
@@ -49,6 +54,7 @@ type Props = {
|
||||
owner: string
|
||||
ownerType: GitHubProjectOwnerType
|
||||
number: number
|
||||
host?: string
|
||||
title?: string
|
||||
} | null
|
||||
onSelect: (selection: ResolvedProjectSelection) => void
|
||||
@@ -65,19 +71,22 @@ async function listAccessibleProjectsForRuntime(
|
||||
settings: Parameters<typeof getActiveRuntimeTarget>[0]
|
||||
): Promise<ListAccessibleProjectsResult> {
|
||||
const target = getActiveRuntimeTarget(settings)
|
||||
const args = { host: 'github.com' }
|
||||
return target.kind === 'environment'
|
||||
? callRuntimeRpc<ListAccessibleProjectsResult>(
|
||||
target,
|
||||
'github.project.listAccessible',
|
||||
{},
|
||||
{ timeoutMs: 60_000 }
|
||||
)
|
||||
: window.api.gh.listAccessibleProjects()
|
||||
? callRuntimeRpc<ListAccessibleProjectsResult>(target, 'github.project.listAccessible', args, {
|
||||
timeoutMs: 60_000
|
||||
})
|
||||
: window.api.gh.listAccessibleProjects(args)
|
||||
}
|
||||
|
||||
async function listProjectViewsForRuntime(
|
||||
settings: Parameters<typeof getActiveRuntimeTarget>[0],
|
||||
args: { owner: string; ownerType: GitHubProjectOwnerType; projectNumber: number }
|
||||
args: {
|
||||
owner: string
|
||||
ownerType: GitHubProjectOwnerType
|
||||
projectNumber: number
|
||||
host?: string
|
||||
}
|
||||
): Promise<ListProjectViewsResult> {
|
||||
const target = getActiveRuntimeTarget(settings)
|
||||
return target.kind === 'environment'
|
||||
@@ -89,17 +98,18 @@ async function listProjectViewsForRuntime(
|
||||
|
||||
async function resolveProjectRefForRuntime(
|
||||
settings: Parameters<typeof getActiveRuntimeTarget>[0],
|
||||
input: string
|
||||
input: string,
|
||||
host?: string
|
||||
): Promise<ResolveProjectRefResult> {
|
||||
const target = getActiveRuntimeTarget(settings)
|
||||
return target.kind === 'environment'
|
||||
? callRuntimeRpc<ResolveProjectRefResult>(
|
||||
target,
|
||||
'github.project.resolveRef',
|
||||
{ input },
|
||||
{ input, ...(host ? { host } : {}) },
|
||||
{ timeoutMs: 30_000 }
|
||||
)
|
||||
: window.api.gh.resolveProjectRef({ input })
|
||||
: window.api.gh.resolveProjectRef({ input, ...(host ? { host } : {}) })
|
||||
}
|
||||
|
||||
export default function ProjectPicker({ activeProject, onSelect }: Props): React.JSX.Element {
|
||||
@@ -203,16 +213,22 @@ export default function ProjectPicker({ activeProject, onSelect }: Props): React
|
||||
|
||||
const commitSelection = useCallback(
|
||||
async (selection: ResolvedProjectSelection, title: string | null) => {
|
||||
const key = `${selection.ownerType}:${selection.owner}:${selection.projectNumber}`
|
||||
const key = githubProjectIdentityKey({
|
||||
owner: selection.owner,
|
||||
ownerType: selection.ownerType,
|
||||
number: selection.projectNumber,
|
||||
host: selection.host
|
||||
})
|
||||
await updateProjectSettings((prev) => {
|
||||
const recent = [
|
||||
{
|
||||
owner: selection.owner,
|
||||
ownerType: selection.ownerType,
|
||||
number: selection.projectNumber,
|
||||
host: githubProjectHost(selection.host),
|
||||
lastOpenedAt: new Date().toISOString()
|
||||
},
|
||||
...prev.recent.filter((r) => `${r.ownerType}:${r.owner}:${r.number}` !== key)
|
||||
...prev.recent.filter((r) => githubProjectIdentityKey(r) !== key)
|
||||
].slice(0, 10)
|
||||
const lastViewByProject = { ...prev.lastViewByProject }
|
||||
if (selection.viewId) {
|
||||
@@ -225,7 +241,8 @@ export default function ProjectPicker({ activeProject, onSelect }: Props): React
|
||||
activeProject: {
|
||||
owner: selection.owner,
|
||||
ownerType: selection.ownerType,
|
||||
number: selection.projectNumber
|
||||
number: selection.projectNumber,
|
||||
host: githubProjectHost(selection.host)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -246,13 +263,14 @@ export default function ProjectPicker({ activeProject, onSelect }: Props): React
|
||||
owner: string
|
||||
ownerType: GitHubProjectOwnerType
|
||||
number: number
|
||||
host?: string
|
||||
title?: string
|
||||
// Why: when the paste resolver parsed a /views/{n} URL, the caller
|
||||
// passes the view number through so we can skip the view-pick step
|
||||
// and commit directly once listProjectViews returns the matching id.
|
||||
viewNumber?: number
|
||||
}) => {
|
||||
const key = `${selection.ownerType}:${selection.owner}:${selection.number}`
|
||||
const key = githubProjectIdentityKey(selection)
|
||||
const lastView = projectSettings.lastViewByProject[key]?.viewId
|
||||
// Why: an explicit viewNumber from the URL takes precedence over the
|
||||
// remembered last view — the user's intent (paste this exact view) wins
|
||||
@@ -263,6 +281,7 @@ export default function ProjectPicker({ activeProject, onSelect }: Props): React
|
||||
owner: selection.owner,
|
||||
ownerType: selection.ownerType,
|
||||
projectNumber: selection.number,
|
||||
host: githubProjectHost(selection.host),
|
||||
viewId: lastView
|
||||
},
|
||||
selection.title ?? null
|
||||
@@ -273,14 +292,16 @@ export default function ProjectPicker({ activeProject, onSelect }: Props): React
|
||||
setViewPickFor({
|
||||
owner: selection.owner,
|
||||
ownerType: selection.ownerType,
|
||||
projectNumber: selection.number
|
||||
projectNumber: selection.number,
|
||||
host: githubProjectHost(selection.host)
|
||||
})
|
||||
setViewLoading(true)
|
||||
try {
|
||||
const res = await listProjectViewsForRuntime(settings, {
|
||||
owner: selection.owner,
|
||||
ownerType: selection.ownerType,
|
||||
projectNumber: selection.number
|
||||
projectNumber: selection.number,
|
||||
host: githubProjectHost(selection.host)
|
||||
})
|
||||
if (!mountedRef.current) {
|
||||
return
|
||||
@@ -299,6 +320,7 @@ export default function ProjectPicker({ activeProject, onSelect }: Props): React
|
||||
owner: selection.owner,
|
||||
ownerType: selection.ownerType,
|
||||
projectNumber: selection.number,
|
||||
host: githubProjectHost(selection.host),
|
||||
viewId: match.id
|
||||
},
|
||||
selection.title ?? null
|
||||
@@ -348,7 +370,7 @@ export default function ProjectPicker({ activeProject, onSelect }: Props): React
|
||||
setPasteError(null)
|
||||
setPasteBusy(true)
|
||||
try {
|
||||
const res = await resolveProjectRefForRuntime(settings, input)
|
||||
const res = await resolveProjectRefForRuntime(settings, input, parsed.host)
|
||||
if (!mountedRef.current) {
|
||||
return
|
||||
}
|
||||
@@ -361,6 +383,7 @@ export default function ProjectPicker({ activeProject, onSelect }: Props): React
|
||||
owner: res.owner,
|
||||
ownerType: res.ownerType,
|
||||
number: res.number,
|
||||
host: githubProjectHost(res.host ?? parsed.host),
|
||||
title: res.title,
|
||||
// Why: forward the parsed view number from /views/{n} URLs so the
|
||||
// chooser can skip the view-pick step and commit directly.
|
||||
@@ -439,11 +462,9 @@ export default function ProjectPicker({ activeProject, onSelect }: Props): React
|
||||
)}
|
||||
>
|
||||
{projectSettings.pinned.map((p) => {
|
||||
const key = `${p.ownerType}:${p.owner}:${p.number}`
|
||||
const key = githubProjectIdentityKey(p)
|
||||
const knownGood = projectSettings.lastViewByProject[key]?.viewId != null
|
||||
const match = browseProjects.find(
|
||||
(bp) => `${bp.ownerType}:${bp.owner}:${bp.number}` === key
|
||||
)
|
||||
const match = browseProjects.find((bp) => githubProjectIdentityKey(bp) === key)
|
||||
return (
|
||||
<PickerRow
|
||||
key={key}
|
||||
@@ -455,15 +476,14 @@ export default function ProjectPicker({ activeProject, onSelect }: Props): React
|
||||
owner: p.owner,
|
||||
ownerType: p.ownerType,
|
||||
number: p.number,
|
||||
host: githubProjectHost(p.host),
|
||||
title: match?.title
|
||||
})
|
||||
}
|
||||
onRemovePin={async () => {
|
||||
await updateProjectSettings((prev) => ({
|
||||
...prev,
|
||||
pinned: prev.pinned.filter(
|
||||
(x) => `${x.ownerType}:${x.owner}:${x.number}` !== key
|
||||
)
|
||||
pinned: prev.pinned.filter((x) => githubProjectIdentityKey(x) !== key)
|
||||
}))
|
||||
}}
|
||||
/>
|
||||
@@ -482,16 +502,13 @@ export default function ProjectPicker({ activeProject, onSelect }: Props): React
|
||||
.filter(
|
||||
(r) =>
|
||||
!projectSettings.pinned.some(
|
||||
(p) =>
|
||||
p.ownerType === r.ownerType &&
|
||||
p.owner === r.owner &&
|
||||
p.number === r.number
|
||||
(p) => githubProjectIdentityKey(p) === githubProjectIdentityKey(r)
|
||||
)
|
||||
)
|
||||
.map((r) => {
|
||||
const key = `${r.ownerType}:${r.owner}:${r.number}`
|
||||
const key = githubProjectIdentityKey(r)
|
||||
const match = browseProjects.find(
|
||||
(bp) => `${bp.ownerType}:${bp.owner}:${bp.number}` === key
|
||||
(bp) => githubProjectIdentityKey(bp) === key
|
||||
)
|
||||
const pinnable = projectSettings.lastViewByProject[key]?.viewId != null
|
||||
return (
|
||||
@@ -505,7 +522,12 @@ export default function ProjectPicker({ activeProject, onSelect }: Props): React
|
||||
...prev,
|
||||
pinned: [
|
||||
...prev.pinned,
|
||||
{ owner: r.owner, ownerType: r.ownerType, number: r.number }
|
||||
{
|
||||
owner: r.owner,
|
||||
ownerType: r.ownerType,
|
||||
number: r.number,
|
||||
host: githubProjectHost(r.host)
|
||||
}
|
||||
].slice(0, 20)
|
||||
}))
|
||||
}}
|
||||
@@ -514,6 +536,7 @@ export default function ProjectPicker({ activeProject, onSelect }: Props): React
|
||||
owner: r.owner,
|
||||
ownerType: r.ownerType,
|
||||
number: r.number,
|
||||
host: githubProjectHost(r.host),
|
||||
title: match?.title
|
||||
})
|
||||
}
|
||||
@@ -546,7 +569,7 @@ export default function ProjectPicker({ activeProject, onSelect }: Props): React
|
||||
) : null}
|
||||
{filteredBrowse.map((p) => (
|
||||
<PickerRow
|
||||
key={`${p.ownerType}:${p.owner}:${p.number}`}
|
||||
key={githubProjectIdentityKey(p)}
|
||||
title={p.title}
|
||||
subtitle={p.owner}
|
||||
onClick={() =>
|
||||
@@ -554,6 +577,7 @@ export default function ProjectPicker({ activeProject, onSelect }: Props): React
|
||||
owner: p.owner,
|
||||
ownerType: p.ownerType,
|
||||
number: p.number,
|
||||
host: githubProjectHost(p.host),
|
||||
title: p.title
|
||||
})
|
||||
}
|
||||
@@ -803,7 +827,7 @@ function AuthErrorBanner({ error }: { error: GitHubProjectViewError }): React.JS
|
||||
|
||||
function parseProjectInput(
|
||||
input: string
|
||||
): { owner: string; number: number; viewNumber?: number } | null {
|
||||
): { owner: string; number: number; host?: string; viewNumber?: number } | null {
|
||||
if (!input) {
|
||||
return null
|
||||
}
|
||||
@@ -817,7 +841,7 @@ function parseProjectInput(
|
||||
}
|
||||
try {
|
||||
const url = new URL(input)
|
||||
if (url.hostname !== 'github.com') {
|
||||
if ((url.protocol !== 'https:' && url.protocol !== 'http:') || !url.host) {
|
||||
return null
|
||||
}
|
||||
const parts = url.pathname.split('/').filter(Boolean)
|
||||
@@ -835,7 +859,9 @@ function parseProjectInput(
|
||||
viewNumber = v
|
||||
}
|
||||
}
|
||||
return { owner, number, viewNumber }
|
||||
// Why: URL.host preserves non-default GHES ports; URL.hostname would
|
||||
// silently route a project on :8443 to the server's default port.
|
||||
return { owner, number, host: url.host, viewNumber }
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
|
||||
@@ -61,6 +61,10 @@ import {
|
||||
} from './project-visible-table-cache'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { buildTaskSourceContextFromRepo } from '../../../../shared/task-source-context'
|
||||
import {
|
||||
githubProjectHost,
|
||||
githubProjectIdentityKey
|
||||
} from '../../../../shared/github-project-identity'
|
||||
|
||||
type Props = {
|
||||
selectedRepoIds: ReadonlySet<string>
|
||||
@@ -70,7 +74,12 @@ const ORCA_FEATURE_REQUEST_URL = 'https://github.com/stablyai/orca/issues/new'
|
||||
|
||||
function listProjectViewsForRuntime(
|
||||
settings: Parameters<typeof getActiveRuntimeTarget>[0],
|
||||
args: { owner: string; ownerType: 'organization' | 'user'; projectNumber: number }
|
||||
args: {
|
||||
owner: string
|
||||
ownerType: 'organization' | 'user'
|
||||
projectNumber: number
|
||||
host?: string
|
||||
}
|
||||
): Promise<ListProjectViewsResult> {
|
||||
const target = getActiveRuntimeTarget(settings)
|
||||
return target.kind === 'environment'
|
||||
@@ -134,6 +143,7 @@ export default function ProjectViewWrapper({ selectedRepoIds }: Props): React.JS
|
||||
owner: selection.owner,
|
||||
ownerType: selection.ownerType,
|
||||
projectNumber: selection.projectNumber,
|
||||
host: githubProjectHost(selection.host),
|
||||
...(selection.viewId ? { viewId: selection.viewId } : {}),
|
||||
...(queryOverride !== undefined ? { queryOverride } : {})
|
||||
},
|
||||
@@ -167,7 +177,7 @@ export default function ProjectViewWrapper({ selectedRepoIds }: Props): React.JS
|
||||
if (!activeProject) {
|
||||
return
|
||||
}
|
||||
const key = `${activeProject.ownerType}:${activeProject.owner}:${activeProject.number}`
|
||||
const key = githubProjectIdentityKey(activeProject)
|
||||
const viewId = lastViewByProject[key]?.viewId
|
||||
if (!viewId) {
|
||||
return
|
||||
@@ -180,7 +190,8 @@ export default function ProjectViewWrapper({ selectedRepoIds }: Props): React.JS
|
||||
activeProject.number,
|
||||
viewId,
|
||||
queryOverride,
|
||||
projectViewSourceScope
|
||||
projectViewSourceScope,
|
||||
activeProject.host
|
||||
)
|
||||
if (projectViewCache[cacheKey]?.data) {
|
||||
return
|
||||
@@ -190,6 +201,7 @@ export default function ProjectViewWrapper({ selectedRepoIds }: Props): React.JS
|
||||
owner: activeProject.owner,
|
||||
ownerType: activeProject.ownerType,
|
||||
projectNumber: activeProject.number,
|
||||
host: githubProjectHost(activeProject.host),
|
||||
viewId
|
||||
},
|
||||
false,
|
||||
@@ -209,7 +221,7 @@ export default function ProjectViewWrapper({ selectedRepoIds }: Props): React.JS
|
||||
if (!activeProject) {
|
||||
return
|
||||
}
|
||||
const projectKey = `${projectViewSourceScope}:${activeProject.ownerType}:${activeProject.owner}:${activeProject.number}`
|
||||
const projectKey = `${projectViewSourceScope}:${githubProjectIdentityKey(activeProject)}`
|
||||
if (viewListByProject[projectKey]) {
|
||||
return
|
||||
}
|
||||
@@ -217,7 +229,8 @@ export default function ProjectViewWrapper({ selectedRepoIds }: Props): React.JS
|
||||
void listProjectViewsForRuntime(settings, {
|
||||
owner: activeProject.owner,
|
||||
ownerType: activeProject.ownerType,
|
||||
projectNumber: activeProject.number
|
||||
projectNumber: activeProject.number,
|
||||
host: githubProjectHost(activeProject.host)
|
||||
})
|
||||
.then((res) => {
|
||||
if (cancelled) {
|
||||
@@ -246,7 +259,7 @@ export default function ProjectViewWrapper({ selectedRepoIds }: Props): React.JS
|
||||
if (!activeProject) {
|
||||
return
|
||||
}
|
||||
const projectKey = `${activeProject.ownerType}:${activeProject.owner}:${activeProject.number}`
|
||||
const projectKey = githubProjectIdentityKey(activeProject)
|
||||
const current = lastViewByProject[projectKey]?.viewId
|
||||
if (current === viewId) {
|
||||
return
|
||||
@@ -272,6 +285,7 @@ export default function ProjectViewWrapper({ selectedRepoIds }: Props): React.JS
|
||||
owner: activeProject.owner,
|
||||
ownerType: activeProject.ownerType,
|
||||
projectNumber: activeProject.number,
|
||||
host: githubProjectHost(activeProject.host),
|
||||
viewId
|
||||
})
|
||||
},
|
||||
@@ -282,7 +296,7 @@ export default function ProjectViewWrapper({ selectedRepoIds }: Props): React.JS
|
||||
if (!activeProject) {
|
||||
return null
|
||||
}
|
||||
const key = `${activeProject.ownerType}:${activeProject.owner}:${activeProject.number}`
|
||||
const key = githubProjectIdentityKey(activeProject)
|
||||
const viewId = lastViewByProject[key]?.viewId
|
||||
if (!viewId) {
|
||||
return null
|
||||
@@ -298,7 +312,7 @@ export default function ProjectViewWrapper({ selectedRepoIds }: Props): React.JS
|
||||
if (!activeProject) {
|
||||
return null
|
||||
}
|
||||
const key = `${activeProject.ownerType}:${activeProject.owner}:${activeProject.number}`
|
||||
const key = githubProjectIdentityKey(activeProject)
|
||||
const viewId = lastViewByProject[key]?.viewId
|
||||
if (!viewId) {
|
||||
return null
|
||||
@@ -309,7 +323,8 @@ export default function ProjectViewWrapper({ selectedRepoIds }: Props): React.JS
|
||||
activeProject.number,
|
||||
viewId,
|
||||
currentAppliedOverride,
|
||||
projectViewSourceScope
|
||||
projectViewSourceScope,
|
||||
activeProject.host
|
||||
)
|
||||
}, [activeProject, lastViewByProject, currentAppliedOverride, projectViewSourceScope])
|
||||
|
||||
@@ -473,6 +488,7 @@ export default function ProjectViewWrapper({ selectedRepoIds }: Props): React.JS
|
||||
return {
|
||||
owner,
|
||||
repo,
|
||||
host: githubProjectHost(table.project.host),
|
||||
number: row.content.number,
|
||||
type: row.itemType === 'PULL_REQUEST' ? 'pr' : 'issue',
|
||||
projectId: table.project.id,
|
||||
@@ -506,6 +522,7 @@ export default function ProjectViewWrapper({ selectedRepoIds }: Props): React.JS
|
||||
const resolution = resolveSelectedProjectRowRepo({
|
||||
row,
|
||||
lookupSlug,
|
||||
host: table.project.host,
|
||||
slugIndexReady,
|
||||
selectedRepoIds
|
||||
})
|
||||
@@ -729,13 +746,15 @@ export default function ProjectViewWrapper({ selectedRepoIds }: Props): React.JS
|
||||
owner: activeProject.owner,
|
||||
ownerType: activeProject.ownerType,
|
||||
number: activeProject.number,
|
||||
host: githubProjectHost(activeProject.host),
|
||||
title: table.project.title
|
||||
}
|
||||
: activeProject
|
||||
? {
|
||||
owner: activeProject.owner,
|
||||
ownerType: activeProject.ownerType,
|
||||
number: activeProject.number
|
||||
number: activeProject.number,
|
||||
host: githubProjectHost(activeProject.host)
|
||||
}
|
||||
: null
|
||||
}
|
||||
@@ -751,7 +770,7 @@ export default function ProjectViewWrapper({ selectedRepoIds }: Props): React.JS
|
||||
if (!activeProject) {
|
||||
return
|
||||
}
|
||||
const key = `${activeProject.ownerType}:${activeProject.owner}:${activeProject.number}`
|
||||
const key = githubProjectIdentityKey(activeProject)
|
||||
const viewId = lastViewByProject[key]?.viewId
|
||||
if (!viewId) {
|
||||
return
|
||||
@@ -771,6 +790,7 @@ export default function ProjectViewWrapper({ selectedRepoIds }: Props): React.JS
|
||||
owner: activeProject.owner,
|
||||
ownerType: activeProject.ownerType,
|
||||
projectNumber: activeProject.number,
|
||||
host: githubProjectHost(activeProject.host),
|
||||
viewId
|
||||
},
|
||||
true,
|
||||
@@ -806,7 +826,7 @@ export default function ProjectViewWrapper({ selectedRepoIds }: Props): React.JS
|
||||
if (!activeProject || !currentCacheKey) {
|
||||
return
|
||||
}
|
||||
const key = `${activeProject.ownerType}:${activeProject.owner}:${activeProject.number}`
|
||||
const key = githubProjectIdentityKey(activeProject)
|
||||
const viewId = lastViewByProject[key]?.viewId
|
||||
if (!viewId) {
|
||||
return
|
||||
@@ -816,6 +836,7 @@ export default function ProjectViewWrapper({ selectedRepoIds }: Props): React.JS
|
||||
owner: activeProject.owner,
|
||||
ownerType: activeProject.ownerType,
|
||||
projectNumber: activeProject.number,
|
||||
host: githubProjectHost(activeProject.host),
|
||||
viewId
|
||||
},
|
||||
true,
|
||||
@@ -855,7 +876,7 @@ export default function ProjectViewWrapper({ selectedRepoIds }: Props): React.JS
|
||||
|
||||
{activeProject
|
||||
? (() => {
|
||||
const projectKey = `${activeProject.ownerType}:${activeProject.owner}:${activeProject.number}`
|
||||
const projectKey = githubProjectIdentityKey(activeProject)
|
||||
const scopedProjectKey = `${projectViewSourceScope}:${projectKey}`
|
||||
const views = viewListByProject[scopedProjectKey] ?? []
|
||||
const activeViewId = lastViewByProject[projectKey]?.viewId ?? null
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
GitHubProjectSettings,
|
||||
GitHubProjectSummary
|
||||
} from '../../../../shared/github-project-types'
|
||||
import { githubProjectIdentityKey } from '../../../../shared/github-project-identity'
|
||||
|
||||
export const GITHUB_PROJECT_PICKER_QUERY_MAX_BYTES = 2 * 1024
|
||||
|
||||
@@ -14,9 +15,9 @@ export function isGitHubProjectPickerQueryTooLarge(
|
||||
}
|
||||
|
||||
function getProjectKey(
|
||||
project: Pick<GitHubProjectSummary, 'ownerType' | 'owner' | 'number'>
|
||||
project: Pick<GitHubProjectSummary, 'ownerType' | 'owner' | 'number' | 'host'>
|
||||
): string {
|
||||
return `${project.ownerType}:${project.owner}:${project.number}`
|
||||
return githubProjectIdentityKey(project)
|
||||
}
|
||||
|
||||
export function filterGitHubProjectPickerProjects({
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { GitHubProjectRow, GitHubProjectTable } from '../../../../shared/github-project-types'
|
||||
import type { Repo } from '../../../../shared/types'
|
||||
|
||||
export type ProjectRowSlugLookup = (slug: string | null | undefined) => readonly Repo[]
|
||||
export type ProjectRowSlugLookup = (
|
||||
slug: string | null | undefined,
|
||||
host?: string
|
||||
) => readonly Repo[]
|
||||
|
||||
export type SelectedProjectRowResolution =
|
||||
| { status: 'loading' }
|
||||
@@ -18,6 +21,7 @@ export type SelectedProjectRowResolution =
|
||||
export function resolveSelectedProjectRowRepo(input: {
|
||||
row: GitHubProjectRow
|
||||
lookupSlug: ProjectRowSlugLookup
|
||||
host?: string
|
||||
slugIndexReady: boolean
|
||||
selectedRepoIds: ReadonlySet<string>
|
||||
}): SelectedProjectRowResolution {
|
||||
@@ -34,7 +38,7 @@ export function resolveSelectedProjectRowRepo(input: {
|
||||
return { status: 'invalid_slug' }
|
||||
}
|
||||
|
||||
const globalMatches = input.lookupSlug(repository)
|
||||
const globalMatches = input.lookupSlug(repository, input.host)
|
||||
if (globalMatches.length === 0) {
|
||||
return { status: 'no_global_match' }
|
||||
}
|
||||
@@ -51,16 +55,19 @@ export function resolveSelectedProjectRowRepo(input: {
|
||||
|
||||
export function projectRowHasOpenRepo(
|
||||
row: GitHubProjectRow,
|
||||
lookupSlug: ProjectRowSlugLookup
|
||||
lookupSlug: ProjectRowSlugLookup,
|
||||
host?: string
|
||||
): boolean {
|
||||
return lookupSlug(row.content.repository).length > 0
|
||||
return lookupSlug(row.content.repository, host).length > 0
|
||||
}
|
||||
|
||||
export function filterProjectTableRowsByOpenRepos(
|
||||
table: GitHubProjectTable,
|
||||
lookupSlug: ProjectRowSlugLookup
|
||||
): GitHubProjectTable {
|
||||
const rows = table.rows.filter((row) => projectRowHasOpenRepo(row, lookupSlug))
|
||||
const rows = table.rows.filter((row) =>
|
||||
projectRowHasOpenRepo(row, lookupSlug, table.project.host)
|
||||
)
|
||||
if (rows.length === table.rows.length && table.totalCount === rows.length) {
|
||||
return table
|
||||
}
|
||||
@@ -77,6 +84,7 @@ export function filterProjectTableRowsBySelectedRepos(
|
||||
const resolution = resolveSelectedProjectRowRepo({
|
||||
row,
|
||||
lookupSlug,
|
||||
host: table.project.host,
|
||||
slugIndexReady,
|
||||
selectedRepoIds
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import type { GlobalSettings, Repo } from '../../../shared/types'
|
||||
import { getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
|
||||
import { getSettingsForRepoRuntimeOwner } from './repo-runtime-owner'
|
||||
import { githubRepoIdentityKey } from '../../../shared/github-repository-identity-key'
|
||||
|
||||
/** Lowercased `owner/repo` → Repo[]. */
|
||||
export type SlugIndex = Map<string, Repo[]>
|
||||
@@ -34,16 +35,18 @@ export function settingsForRepoOwner(
|
||||
export function lookupReposBySlugFromCache(
|
||||
repos: readonly Repo[],
|
||||
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined,
|
||||
slug: string | null | undefined
|
||||
slug: string | null | undefined,
|
||||
host?: string
|
||||
): Repo[] {
|
||||
const target = slug?.toLowerCase()
|
||||
if (!target) {
|
||||
const [owner, repo] = slug?.split('/') ?? []
|
||||
if (!owner || !repo) {
|
||||
return []
|
||||
}
|
||||
const target = githubRepoIdentityKey({ owner, repo, host })
|
||||
const matched: Repo[] = []
|
||||
for (const repo of repos) {
|
||||
const cacheKey = slugCacheKey(repo.id, settingsForRepoOwner(repo, settings))
|
||||
if (slugByRepoId.get(cacheKey)?.toLowerCase() === target) {
|
||||
if (slugByRepoId.get(cacheKey) === target) {
|
||||
matched.push(repo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { Repo } from '../../../shared/types'
|
||||
import type { GlobalSettings } from '../../../shared/types'
|
||||
import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
|
||||
import { settingsForRepoOwner, slugByRepoId, slugCacheKey, type SlugIndex } from './repo-slug-cache'
|
||||
import { githubRepoIdentityKey } from '../../../shared/github-repository-identity-key'
|
||||
|
||||
export { lookupReposBySlugFromCache } from './repo-slug-cache'
|
||||
|
||||
@@ -50,7 +51,7 @@ async function resolveRepoSlug(
|
||||
const target = getActiveRuntimeTarget(settings)
|
||||
const result =
|
||||
target.kind === 'environment'
|
||||
? await callRuntimeRpc<{ owner: string; repo: string } | null>(
|
||||
? await callRuntimeRpc<{ owner: string; repo: string; host?: string } | null>(
|
||||
target,
|
||||
'github.repoSlug',
|
||||
{ repo: repo.id },
|
||||
@@ -61,7 +62,7 @@ async function resolveRepoSlug(
|
||||
slugByRepoId.set(cacheKey, null)
|
||||
return null
|
||||
}
|
||||
const slug = `${result.owner}/${result.repo}`.toLowerCase()
|
||||
const slug = githubRepoIdentityKey(result)
|
||||
slugByRepoId.set(cacheKey, slug)
|
||||
return slug
|
||||
} catch {
|
||||
@@ -104,7 +105,7 @@ async function buildIndex(
|
||||
}
|
||||
|
||||
export type RepoSlugIndexState = {
|
||||
lookupSlug: (slug: string | null | undefined) => Repo[]
|
||||
lookupSlug: (slug: string | null | undefined, host?: string) => Repo[]
|
||||
ready: boolean
|
||||
}
|
||||
|
||||
@@ -134,11 +135,12 @@ export function useRepoSlugIndex(): RepoSlugIndexState {
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
lookupSlug: (slug: string | null | undefined): Repo[] => {
|
||||
if (!slug) {
|
||||
lookupSlug: (slug: string | null | undefined, host?: string): Repo[] => {
|
||||
const [owner, repo] = slug?.split('/') ?? []
|
||||
if (!owner || !repo) {
|
||||
return []
|
||||
}
|
||||
return index.get(slug.toLowerCase()) ?? []
|
||||
return index.get(githubRepoIdentityKey({ owner, repo, host })) ?? []
|
||||
},
|
||||
ready
|
||||
}),
|
||||
|
||||
@@ -11,6 +11,7 @@ describe('getSmartGitHubSubmitIntent', () => {
|
||||
it('treats GitHub issue and pull URLs as submit-time source intent', () => {
|
||||
expect(getSmartGitHubSubmitIntent('https://github.com/stablyai/orca/pull/2049')).toEqual({
|
||||
kind: 'link',
|
||||
host: 'github.com',
|
||||
owner: 'stablyai',
|
||||
repo: 'orca',
|
||||
number: 2049,
|
||||
@@ -18,6 +19,7 @@ describe('getSmartGitHubSubmitIntent', () => {
|
||||
})
|
||||
expect(getSmartGitHubSubmitIntent('https://github.com/stablyai/orca/issues/2050')).toEqual({
|
||||
kind: 'link',
|
||||
host: 'github.com',
|
||||
owner: 'stablyai',
|
||||
repo: 'orca',
|
||||
number: 2050,
|
||||
@@ -32,6 +34,7 @@ describe('getSmartGitHubSubmitIntent', () => {
|
||||
)
|
||||
).toEqual({
|
||||
kind: 'link',
|
||||
host: 'github.com',
|
||||
owner: 'mvanhorn',
|
||||
repo: 'cli-printing-press',
|
||||
number: 2635,
|
||||
@@ -44,6 +47,7 @@ describe('getSmartGitHubSubmitIntent', () => {
|
||||
getSmartGitHubSubmitIntent('review (https://github.com/stablyai/orca/pull/2049), please')
|
||||
).toEqual({
|
||||
kind: 'link',
|
||||
host: 'github.com',
|
||||
owner: 'stablyai',
|
||||
repo: 'orca',
|
||||
number: 2049,
|
||||
@@ -53,6 +57,7 @@ describe('getSmartGitHubSubmitIntent', () => {
|
||||
expect(getSmartGitHubSubmitIntent('fix https://github.com/stablyai/orca/issues/2050.')).toEqual(
|
||||
{
|
||||
kind: 'link',
|
||||
host: 'github.com',
|
||||
owner: 'stablyai',
|
||||
repo: 'orca',
|
||||
number: 2050,
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { StateCreator } from 'zustand'
|
||||
import { toast } from 'sonner'
|
||||
import type { AppState } from '../types'
|
||||
import { githubRepoIdentityKey } from '../../../../shared/github-repository-identity-key'
|
||||
import { githubProjectIdentityKey } from '../../../../shared/github-project-identity'
|
||||
import type {
|
||||
ClassifiedError,
|
||||
GitHubOwnerRepo,
|
||||
@@ -419,9 +420,11 @@ export function projectViewCacheKey(
|
||||
projectNumber: number,
|
||||
resolvedViewId: string,
|
||||
queryOverride?: string,
|
||||
sourceScope = 'local'
|
||||
sourceScope = 'local',
|
||||
host?: string
|
||||
): string {
|
||||
return `github-project:${sourceScope}:${ownerType}:${owner}:${projectNumber}:${resolvedViewId}${queryOverrideKeyPart(queryOverride)}`
|
||||
const projectKey = githubProjectIdentityKey({ ownerType, owner, number: projectNumber, host })
|
||||
return `github-project:${sourceScope}:${projectKey}:${resolvedViewId}${queryOverrideKeyPart(queryOverride)}`
|
||||
}
|
||||
|
||||
function projectViewRequestKey(args: GetProjectViewTableArgs, sourceScope: string): string {
|
||||
@@ -433,7 +436,13 @@ function projectViewRequestKey(args: GetProjectViewTableArgs, sourceScope: strin
|
||||
: args.viewName
|
||||
? `name:${args.viewName}`
|
||||
: 'default'
|
||||
return `${sourceScope}:${args.ownerType}:${args.owner}:${args.projectNumber}:${selector}${queryOverrideKeyPart(args.queryOverride)}`
|
||||
const projectKey = githubProjectIdentityKey({
|
||||
ownerType: args.ownerType,
|
||||
owner: args.owner,
|
||||
number: args.projectNumber,
|
||||
host: args.host
|
||||
})
|
||||
return `${sourceScope}:${projectKey}:${selector}${queryOverrideKeyPart(args.queryOverride)}`
|
||||
}
|
||||
|
||||
function projectViewSourceScope(settings: AppState['settings']): string {
|
||||
@@ -2082,7 +2091,8 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
||||
args.projectNumber,
|
||||
args.viewId,
|
||||
args.queryOverride,
|
||||
sourceScope
|
||||
sourceScope,
|
||||
args.host
|
||||
)
|
||||
: null
|
||||
if (!options?.force && maybeKnownKey) {
|
||||
@@ -2122,7 +2132,8 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
||||
table.project.number,
|
||||
table.selectedView.id,
|
||||
args.queryOverride,
|
||||
sourceScope
|
||||
sourceScope,
|
||||
table.project.host
|
||||
)
|
||||
set((s) => ({
|
||||
projectViewCache: withBoundedCacheEntry(s.projectViewCache, key, {
|
||||
@@ -2208,6 +2219,7 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
||||
'github.project.updateItemField',
|
||||
{
|
||||
projectId: table.project.id,
|
||||
host: table.project.host,
|
||||
itemId: rowId,
|
||||
fieldId,
|
||||
value
|
||||
@@ -2216,6 +2228,7 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
||||
)
|
||||
: await window.api.gh.updateProjectItemField({
|
||||
projectId: table.project.id,
|
||||
host: table.project.host,
|
||||
itemId: rowId,
|
||||
fieldId,
|
||||
value
|
||||
@@ -2266,6 +2279,7 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
||||
'github.project.clearItemField',
|
||||
{
|
||||
projectId: table.project.id,
|
||||
host: table.project.host,
|
||||
itemId: rowId,
|
||||
fieldId
|
||||
},
|
||||
@@ -2273,6 +2287,7 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
||||
)
|
||||
: await window.api.gh.clearProjectItemField({
|
||||
projectId: table.project.id,
|
||||
host: table.project.host,
|
||||
itemId: rowId,
|
||||
fieldId
|
||||
})
|
||||
@@ -2372,6 +2387,7 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
||||
const args = {
|
||||
owner,
|
||||
repo,
|
||||
host: table.project.host,
|
||||
number,
|
||||
updates: {
|
||||
...(updates.title !== undefined ? { title: updates.title } : {}),
|
||||
@@ -2403,6 +2419,7 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
||||
const args = {
|
||||
owner,
|
||||
repo,
|
||||
host: table.project.host,
|
||||
number,
|
||||
updates: {
|
||||
...(updates.title !== undefined ? { title: updates.title } : {}),
|
||||
@@ -2495,6 +2512,7 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
||||
const args = {
|
||||
owner,
|
||||
repo,
|
||||
host: table.project.host,
|
||||
number,
|
||||
issueTypeId: issueType?.id ?? null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { isDefaultGitHubHost } from './github-repository-identity-key'
|
||||
|
||||
export type GitHubProjectIdentity = {
|
||||
owner: string
|
||||
ownerType: 'organization' | 'user'
|
||||
number: number
|
||||
host?: string
|
||||
}
|
||||
|
||||
/** Stable settings/cache identity. Default-host keys intentionally retain the
|
||||
* legacy shape so existing github.com project preferences survive upgrades. */
|
||||
export function githubProjectIdentityKey(project: GitHubProjectIdentity): string {
|
||||
const projectKey = `${project.ownerType}:${project.owner.toLowerCase()}:${project.number}`
|
||||
return project.host && !isDefaultGitHubHost(project.host)
|
||||
? `${project.host.toLowerCase()}:${projectKey}`
|
||||
: projectKey
|
||||
}
|
||||
|
||||
/** Project API calls must pin github.com too; otherwise process GH_HOST can
|
||||
* redirect a host-less persisted project to an Enterprise server. */
|
||||
export function githubProjectHost(host?: string | null): string {
|
||||
const trimmed = host?.trim()
|
||||
return trimmed || 'github.com'
|
||||
}
|
||||
@@ -184,6 +184,7 @@ export type GitHubProjectRow = {
|
||||
export type GitHubProjectTable = {
|
||||
project: {
|
||||
id: string
|
||||
host?: string
|
||||
owner: string
|
||||
ownerType: GitHubProjectOwnerType
|
||||
number: number
|
||||
@@ -201,6 +202,7 @@ export type GitHubProjectTable = {
|
||||
|
||||
export type GitHubProjectSummary = {
|
||||
id: string
|
||||
host?: string
|
||||
owner: string
|
||||
ownerType: GitHubProjectOwnerType
|
||||
number: number
|
||||
@@ -217,15 +219,21 @@ export type GitHubProjectViewSummary = {
|
||||
}
|
||||
|
||||
export type GitHubProjectSettings = {
|
||||
pinned: { owner: string; ownerType: GitHubProjectOwnerType; number: number }[]
|
||||
pinned: { owner: string; ownerType: GitHubProjectOwnerType; number: number; host?: string }[]
|
||||
recent: {
|
||||
owner: string
|
||||
ownerType: GitHubProjectOwnerType
|
||||
number: number
|
||||
host?: string
|
||||
lastOpenedAt: string
|
||||
}[]
|
||||
lastViewByProject: Record<string, { viewId: string }>
|
||||
activeProject: { owner: string; ownerType: GitHubProjectOwnerType; number: number } | null
|
||||
activeProject: {
|
||||
owner: string
|
||||
ownerType: GitHubProjectOwnerType
|
||||
number: number
|
||||
host?: string
|
||||
} | null
|
||||
}
|
||||
|
||||
// ─── Classified errors ─────────────────────────────────────────────────
|
||||
@@ -279,6 +287,7 @@ export type ResolveProjectRefResult =
|
||||
ownerType: GitHubProjectOwnerType
|
||||
number: number
|
||||
title: string
|
||||
host?: string
|
||||
// Why: when the input is a /views/{n} URL, surface the parsed view
|
||||
// number so the picker can skip the view-pick step and commit the
|
||||
// selection directly. Absent for owner/number shorthand and project
|
||||
|
||||
Reference in New Issue
Block a user