feat(KB-022): add GitHub issue status badges to task cards

- Extend core types with GitHub issue tracking fields (issueNumber, issueStatus, lastIssueSync)
- Add TaskStore.getTaskGitHubIssueInfo() to fetch issue details from GitHub API
- Create server-side API endpoint for issue status lookup with caching
- Build GitHubBadge component with color-coded status indicators (open/closed)
- Integrate badges into TaskCard with hover state linking to GitHub issues
- Add CSS styles for badge positioning and visual polish
- Include comprehensive tests for GitHubBadge and TaskCard badge rendering
This commit is contained in:
gsxdsm
2026-03-29 20:12:13 -07:00
parent 52b8205220
commit 6c59560a0a
12 changed files with 930 additions and 30 deletions

View File

@@ -220,6 +220,56 @@ export class GitHubClient {
private mapPrState(state: string): "open" | "closed" {
return state === "open" ? "open" : "closed";
}
/**
* Fetch current issue status from GitHub API.
* Returns null if the issue is not found or is a pull request.
*/
async getIssueStatus(
owner: string,
repo: string,
number: number,
): Promise<Omit<import("@kb/core").IssueInfo, "lastCheckedAt"> | null> {
const url = `${this.baseUrl}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${number}`;
const headers = this.buildHeaders();
const response = await fetch(url, { headers });
if (!response.ok) {
if (response.status === 404) {
return null;
}
const error = await response.json().catch(() => ({ message: response.statusText }));
throw new Error(`GitHub API error: ${response.status} ${error.message || response.statusText}`);
}
const data = (await response.json()) as {
number: number;
html_url: string;
title: string;
state: string;
state_reason?: "completed" | "not_planned" | "reopened";
pull_request?: unknown;
};
// Filter out pull requests - this endpoint returns both issues and PRs
if (data.pull_request) {
return null;
}
return {
url: data.html_url,
number: data.number,
state: this.mapIssueState(data.state),
title: data.title,
stateReason: data.state_reason,
};
}
private mapIssueState(state: string): "open" | "closed" {
return state === "open" ? "open" : "closed";
}
}
/**