* Add design spec: OpenCode readiness loading indicator * Add implementation plan: OpenCode readiness loading indicator * feat: add useOpenCodeReadiness hook * feat: add i18n keys for common.loading * feat: add loading state to ModelSelector * feat: add loading state to AgentSelector * feat: add loading state to ModelControls chat selectors * update package-lock * feat(github): add shared fork detection utility * feat(github): make issue listing fork-aware * feat(github): make PR listing fork-aware * feat(types): add sourceRepo to issue/PR summary types * feat(ui): add source badges to GitHub integration dialog * feat(ui): add source badges to issue/PR picker dialogs * feat(github): pass headRemote in PR creation for fork support * feat(ui): add source→target label in PR tab for fork workflows * fix(github): allow PR section on base branch when upstream remote exists * fix(github): show PR section on any branch including main for fork→upstream PRs * fix(github): allow PullRequestSection to render on base branch when upstream remote exists * feat(github): auto-detect upstream repo for fork→upstream PR creation - Add GET /api/github/repo/upstream endpoint to discover fork's upstream - Fix PullRequestSection canShow to allow PR creation on base branch when repo is a fork - Add virtual upstream target in remote dropdown (no explicit upstream remote needed) - Add targetRepo parameter to /api/github/pr/create for direct upstream targeting - Add repoUpstream() API client method and GitHubRepoUpstreamResult type * feat(github): auto-detect upstream repo for fork→upstream PR creation - Add GET /api/github/repo/upstream endpoint to discover fork's upstream - Fix PullRequestSection canShow to allow PR creation on base branch when repo is a fork - Add virtual upstream target in remote dropdown (no explicit upstream remote needed) - Add targetRepo parameter to /api/github/pr/create for direct upstream targeting - Add repoUpstream() API client method and GitHubRepoUpstreamResult type * fix: complete fork→upstream PR workflow - Server: return defaultBranch from /api/github/repo/upstream endpoint - Server: fix cross-repo head ref construction (compare repos, not remote names) - Server: filterActiveRemoteBranches checks all remotes, not just origin - UI: set targetBaseBranch to upstream's default branch when using detected upstream - UI: include all remote branches in base branch dropdown when using detected upstream - UI: skip base===head check for cross-repo PRs (same branch name on different repos is valid) - Types: add defaultBranch to GitHubRepoUpstreamResult * chore: delete superpowers folder * feat: add (local)/(remote) labels to PR branch display and adapt Repository button to selected remote * feat: Repository button adapts to selected remote (upstream vs origin) * fix: complete fork→upstream PR feature gaps Server: - Extend /api/github/repo/upstream to return defaultBranchSha and remoteName - Reuse headRepo result instead of redundant resolveGitHubRepoFromDirectory call - Return clear error when headRepo is null (invalid GitHub URL) UI: - Add upstream's default branch to availableBaseBranches when using detected upstream - Use upstream's default branch SHA in git log for generate description (fixes 'No commits found in range main...main') - Show qualified names (owner/repo · branch) in base branch dropdown when using detected upstream Types: - Add defaultBranchSha and remoteName to GitHubRepoUpstreamResult * fix: move detectedUpstream state before availableBaseBranches to fix temporal dead zone * fix: fetch upstream branches from GitHub API for base branch dropdown - Add GET /api/github/repo/branches endpoint to fetch branches via Octokit - Add repoBranches() to GitHub API client and interface - Fetch upstream branches on detection and store in upstreamBranches state - Include upstreamBranches in availableBaseBranches when using detected upstream - Re-add availableBaseBranches memo and auto-correction effect that were lost - Remove unnecessary qualified names from dropdown (upstream is already selected) * fix: restore prStatusKey and statusEntry declarations lost during refactor * fix: cleanly re-apply all fork→upstream PR UI changes Restored PullRequestSection.tsx from clean base and re-applied: - Expand detectedUpstream type with defaultBranch, defaultBranchSha, remoteName - Add upstreamBranches state and fetch on upstream detection - Include upstream branches in availableBaseBranches when using detected upstream - Use upstream default branch SHA in generate description (fixes 'No commits found') - Adapt Repository button URL to selected remote - Add (local)/(remote)/(upstream) labels to branch display * fix: move detectedUpstream/upstreamBranches before availableBaseBranches to fix TDZ * style: add pill badge styling to upstream repo source labels * fix: don't cache error PR status responses, allow force-bypass of server cache * fix: resolve PR status cache bugs, stale directory fallback, and upstream re-detection * fix: keep collapse button visible when scrolling long user messages - Collapse button now sticks to top of scrollable user message content instead of scrolling away * fix: checkbox focus ring blends into sidebar background * fix: polish fork PR follow-ups * fix: remove user message collapse artifact * fix: tighten fork PR internals * fix: check all remotes for fork PR status * fix: recover sidebar PR status misses --------- Signed-off-by: Islam Nofl <islamnofl.official@gmail.com> Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
103 lines
3.3 KiB
JavaScript
103 lines
3.3 KiB
JavaScript
import { resolveGitHubRepoFromDirectory } from './index.js';
|
|
|
|
const REPO_METADATA_TTL_MS = 5 * 60_000;
|
|
const REPO_METADATA_CACHE_MAX_ENTRIES = 200;
|
|
const repoMetadataCache = new Map();
|
|
|
|
const setRepoMetadataCache = (repoKey, data) => {
|
|
if (repoMetadataCache.size >= REPO_METADATA_CACHE_MAX_ENTRIES && !repoMetadataCache.has(repoKey)) {
|
|
const oldest = repoMetadataCache.entries().next().value;
|
|
if (oldest) {
|
|
repoMetadataCache.delete(oldest[0]);
|
|
}
|
|
}
|
|
repoMetadataCache.set(repoKey, { data, fetchedAt: Date.now() });
|
|
};
|
|
|
|
const normalizeRepoKey = (owner, repo) => {
|
|
const o = typeof owner === 'string' ? owner.trim().toLowerCase() : '';
|
|
const r = typeof repo === 'string' ? repo.trim().toLowerCase() : '';
|
|
if (!o || !r) return '';
|
|
return `${o}/${r}`;
|
|
};
|
|
|
|
const getRepoMetadata = async (octokit, repo) => {
|
|
const repoKey = normalizeRepoKey(repo?.owner, repo?.repo);
|
|
if (!repoKey) return null;
|
|
|
|
const cached = repoMetadataCache.get(repoKey);
|
|
if (cached && Date.now() - cached.fetchedAt < REPO_METADATA_TTL_MS) {
|
|
return cached.data;
|
|
}
|
|
|
|
try {
|
|
const response = await octokit.rest.repos.get({
|
|
owner: repo.owner,
|
|
repo: repo.repo,
|
|
});
|
|
const data = response?.data ?? null;
|
|
setRepoMetadataCache(repoKey, data);
|
|
return data;
|
|
} catch (error) {
|
|
if (error?.status === 403 || error?.status === 404) {
|
|
setRepoMetadataCache(repoKey, null);
|
|
return null;
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Resolve the repo network for a directory. If the origin repo is a fork,
|
|
* includes the parent/source (upstream) repo in the result.
|
|
*
|
|
* @param {import('@octokit/rest').Octokit} octokit
|
|
* @param {string} directory
|
|
* @param {string} [remoteName='origin']
|
|
* @returns {Promise<Array<{ owner: string, repo: string, url: string, source: string }> | null>}
|
|
* Array of repos to query (origin first, then upstream), or null if not a fork.
|
|
*/
|
|
export async function resolveRepoNetwork(octokit, directory, remoteName = 'origin') {
|
|
const { repo } = await resolveGitHubRepoFromDirectory(directory, remoteName).catch(() => ({ repo: null }));
|
|
if (!repo) return null;
|
|
|
|
const metadata = await getRepoMetadata(octokit, repo);
|
|
if (!metadata) return [{ ...repo, source: 'origin' }];
|
|
|
|
const result = [{ ...repo, source: 'origin' }];
|
|
const seenKeys = new Set([normalizeRepoKey(repo.owner, repo.repo)]);
|
|
|
|
const parent = metadata?.parent;
|
|
if (parent?.owner?.login && parent?.name) {
|
|
const key = normalizeRepoKey(parent.owner.login, parent.name);
|
|
if (!seenKeys.has(key)) {
|
|
seenKeys.add(key);
|
|
result.push({
|
|
owner: parent.owner.login,
|
|
repo: parent.name,
|
|
url: parent.html_url || `https://github.com/${parent.owner.login}/${parent.name}`,
|
|
source: 'upstream',
|
|
});
|
|
}
|
|
}
|
|
|
|
const source = metadata?.source;
|
|
if (source?.owner?.login && source?.name) {
|
|
const key = normalizeRepoKey(source.owner.login, source.name);
|
|
if (!seenKeys.has(key)) {
|
|
seenKeys.add(key);
|
|
result.push({
|
|
owner: source.owner.login,
|
|
repo: source.name,
|
|
url: source.html_url || `https://github.com/${source.owner.login}/${source.name}`,
|
|
source: 'upstream',
|
|
});
|
|
}
|
|
}
|
|
|
|
// If no parent/source found, repo is not a fork
|
|
if (result.length === 1) return null;
|
|
|
|
return result;
|
|
}
|