@volter/twin-github 0.1.2 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -36
- package/client/github-mirror.css +277 -319
- package/client/github-mirror.d.ts +418 -0
- package/client/github-mirror.js +485 -0
- package/client/github-mirror.tsx +153 -357
- package/client/pulls-rest.ts +159 -0
- package/client/pulls-workspace.tsx +841 -0
- package/dist/client/github-mirror.bundle.js +239 -0
- package/dist/client/github-mirror.css +916 -0
- package/dist/client/github-mirror.d.ts +418 -0
- package/dist/client/github-mirror.js +485 -0
- package/dist/client/github-mirror.tsx +1315 -0
- package/dist/client/pulls-rest.d.ts +42 -0
- package/dist/client/pulls-rest.js +140 -0
- package/dist/client/pulls-rest.ts +159 -0
- package/dist/client/pulls-workspace.bundle.js +22 -0
- package/dist/client/pulls-workspace.d.ts +114 -0
- package/dist/client/pulls-workspace.js +418 -0
- package/dist/client/pulls-workspace.tsx +841 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +40 -0
- package/dist/src/generated/graphql-sdl.gen.json +1 -0
- package/dist/src/generated/graphql.gen.json +1 -0
- package/dist/src/generated/surface.gen.json +1 -0
- package/dist/src/generated/ui.gen.json +1 -0
- package/dist/src/github-budget.d.ts +69 -0
- package/dist/src/github-budget.js +172 -0
- package/dist/src/github-capabilities.d.ts +5 -0
- package/dist/src/github-capabilities.js +4468 -0
- package/dist/src/github-conformance.d.ts +43 -0
- package/dist/src/github-conformance.js +76 -0
- package/dist/src/github-connector.d.ts +307 -0
- package/dist/src/github-connector.js +1398 -0
- package/dist/src/github-events.d.ts +41 -0
- package/dist/src/github-events.js +232 -0
- package/dist/src/github-git-http.d.ts +49 -0
- package/dist/src/github-git-http.js +185 -0
- package/dist/src/github-git-plane.d.ts +114 -0
- package/dist/src/github-git-plane.js +407 -0
- package/dist/src/github-mirror-state.d.ts +2 -0
- package/dist/src/github-mirror-state.js +335 -0
- package/dist/src/github-mirror-ui.d.ts +20 -0
- package/dist/src/github-mirror-ui.js +101 -0
- package/dist/src/github-server.d.ts +14 -0
- package/dist/src/github-server.js +240 -0
- package/dist/src/github-shared.d.ts +12 -0
- package/dist/src/github-shared.js +21 -0
- package/dist/src/github-twin.d.ts +1411 -0
- package/dist/src/github-twin.js +4084 -0
- package/dist/src/github-ui-conformance.d.ts +4 -0
- package/dist/src/github-ui-conformance.js +105 -0
- package/dist/src/github-ui-structure.d.ts +18 -0
- package/dist/src/github-ui-structure.js +251 -0
- package/dist/src/graphql-wire.d.ts +22 -0
- package/dist/src/graphql-wire.js +89 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.js +104 -0
- package/dist/src/manifest.d.ts +8 -0
- package/dist/src/manifest.js +597 -0
- package/dist/src/npm-registry.d.ts +7 -0
- package/dist/src/npm-registry.js +47 -0
- package/dist/src/screens/app-installation.d.ts +3 -0
- package/dist/src/screens/app-installation.js +166 -0
- package/dist/src/screens/app-manifest.d.ts +3 -0
- package/dist/src/screens/app-manifest.js +81 -0
- package/dist/src/screens/oauth.d.ts +15 -0
- package/dist/src/screens/oauth.js +257 -0
- package/dist/src/screens/session.d.ts +8 -0
- package/dist/src/screens/session.js +170 -0
- package/dist/src/semantics/actions.d.ts +2 -0
- package/dist/src/semantics/actions.js +413 -0
- package/dist/src/semantics/activity.d.ts +4 -0
- package/dist/src/semantics/activity.js +161 -0
- package/dist/src/semantics/apps.d.ts +2 -0
- package/dist/src/semantics/apps.js +144 -0
- package/dist/src/semantics/branches.d.ts +2 -0
- package/dist/src/semantics/branches.js +136 -0
- package/dist/src/semantics/checks.d.ts +2 -0
- package/dist/src/semantics/checks.js +176 -0
- package/dist/src/semantics/code-scanning-upload.d.ts +2 -0
- package/dist/src/semantics/code-scanning-upload.js +97 -0
- package/dist/src/semantics/codespaces.d.ts +2 -0
- package/dist/src/semantics/codespaces.js +58 -0
- package/dist/src/semantics/commits.d.ts +2 -0
- package/dist/src/semantics/commits.js +109 -0
- package/dist/src/semantics/contents.d.ts +2 -0
- package/dist/src/semantics/contents.js +131 -0
- package/dist/src/semantics/deployments.d.ts +2 -0
- package/dist/src/semantics/deployments.js +127 -0
- package/dist/src/semantics/gists.d.ts +2 -0
- package/dist/src/semantics/gists.js +86 -0
- package/dist/src/semantics/git.d.ts +2 -0
- package/dist/src/semantics/git.js +235 -0
- package/dist/src/semantics/graphql.d.ts +7 -0
- package/dist/src/semantics/graphql.js +512 -0
- package/dist/src/semantics/index.d.ts +5 -0
- package/dist/src/semantics/index.js +62 -0
- package/dist/src/semantics/issues.d.ts +2 -0
- package/dist/src/semantics/issues.js +456 -0
- package/dist/src/semantics/keys.d.ts +2 -0
- package/dist/src/semantics/keys.js +66 -0
- package/dist/src/semantics/labels.d.ts +2 -0
- package/dist/src/semantics/labels.js +100 -0
- package/dist/src/semantics/meta.d.ts +12 -0
- package/dist/src/semantics/meta.js +144 -0
- package/dist/src/semantics/notifications.d.ts +4 -0
- package/dist/src/semantics/notifications.js +62 -0
- package/dist/src/semantics/orgs.d.ts +2 -0
- package/dist/src/semantics/orgs.js +420 -0
- package/dist/src/semantics/packages.d.ts +2 -0
- package/dist/src/semantics/packages.js +55 -0
- package/dist/src/semantics/pages.d.ts +2 -0
- package/dist/src/semantics/pages.js +176 -0
- package/dist/src/semantics/projects.d.ts +2 -0
- package/dist/src/semantics/projects.js +239 -0
- package/dist/src/semantics/pulls.d.ts +2 -0
- package/dist/src/semantics/pulls.js +462 -0
- package/dist/src/semantics/push-reactions.d.ts +101 -0
- package/dist/src/semantics/push-reactions.js +513 -0
- package/dist/src/semantics/releases.d.ts +19 -0
- package/dist/src/semantics/releases.js +231 -0
- package/dist/src/semantics/repo-invitations.d.ts +2 -0
- package/dist/src/semantics/repo-invitations.js +98 -0
- package/dist/src/semantics/repos.d.ts +17 -0
- package/dist/src/semantics/repos.js +390 -0
- package/dist/src/semantics/rulesets.d.ts +2 -0
- package/dist/src/semantics/rulesets.js +111 -0
- package/dist/src/semantics/search.d.ts +2 -0
- package/dist/src/semantics/search.js +84 -0
- package/dist/src/semantics/security.d.ts +2 -0
- package/dist/src/semantics/security.js +177 -0
- package/dist/src/semantics/shared.d.ts +61 -0
- package/dist/src/semantics/shared.js +151 -0
- package/dist/src/semantics/users.d.ts +2 -0
- package/dist/src/semantics/users.js +255 -0
- package/dist/src/semantics/webhooks.d.ts +2 -0
- package/dist/src/semantics/webhooks.js +107 -0
- package/dist/test-fixtures/github-a11y-reference.pr-list.SOURCE.md +56 -0
- package/dist/test-fixtures/github-a11y-reference.pr-list.json +1447 -0
- package/dist/test-fixtures/github-comment-schema.SOURCE.md +11 -0
- package/dist/test-fixtures/github-comment-schema.json +702 -0
- package/dist/test-fixtures/github-known-deviations.json +94 -0
- package/dist/test-fixtures/github-openapi-operations.SOURCE.md +76 -0
- package/dist/test-fixtures/github-openapi-operations.json +362 -0
- package/dist/test-fixtures/github-pull-schema.SOURCE.md +37 -0
- package/dist/test-fixtures/github-pull-schema.json +3601 -0
- package/dist/test-fixtures/github-review-schema.SOURCE.md +12 -0
- package/dist/test-fixtures/github-review-schema.json +236 -0
- package/package.json +20 -11
- package/src/cli.ts +7 -6
- package/src/generated/graphql-sdl.gen.json +1 -0
- package/src/generated/graphql.gen.json +1 -0
- package/src/generated/surface.gen.json +1 -0
- package/src/generated/ui.gen.json +1 -0
- package/src/github-a11y-snapshot.uitest.ts +5 -5
- package/src/github-budget.ts +4 -4
- package/src/github-capabilities.ts +1993 -556
- package/src/github-conformance.ts +12 -7
- package/src/github-connector.ts +108 -96
- package/src/github-events.ts +225 -95
- package/src/github-git-http.ts +48 -84
- package/src/github-git-plane.ts +247 -385
- package/src/github-journey.uitest.ts +28 -44
- package/src/github-mirror-state.ts +23 -61
- package/src/github-mirror-ui.ts +16 -10
- package/src/github-server.ts +158 -60
- package/src/github-shared.ts +1 -1
- package/src/github-twin.ts +1525 -4494
- package/src/github-ui-conformance.ts +7 -8
- package/src/github-ui-structure.ts +19 -18
- package/src/graphql-wire.ts +115 -0
- package/src/index.ts +50 -7
- package/src/manifest.ts +605 -0
- package/src/npm-registry.ts +43 -0
- package/src/screens/app-installation.tsx +217 -0
- package/src/screens/app-manifest.tsx +97 -0
- package/src/screens/oauth.tsx +258 -0
- package/src/screens/session.tsx +193 -0
- package/src/semantics/actions.ts +395 -0
- package/src/semantics/activity.ts +171 -0
- package/src/semantics/apps.ts +133 -0
- package/src/semantics/branches.ts +133 -0
- package/src/semantics/checks.ts +163 -0
- package/src/semantics/code-scanning-upload.ts +92 -0
- package/src/semantics/codespaces.ts +58 -0
- package/src/semantics/commits.ts +109 -0
- package/src/semantics/contents.ts +112 -0
- package/src/semantics/deployments.ts +116 -0
- package/src/semantics/gists.ts +85 -0
- package/src/semantics/git.ts +226 -0
- package/src/semantics/graphql.ts +505 -0
- package/src/semantics/index.ts +68 -0
- package/src/semantics/issues.ts +434 -0
- package/src/semantics/keys.ts +66 -0
- package/src/semantics/labels.ts +91 -0
- package/src/semantics/meta.ts +139 -0
- package/src/semantics/notifications.ts +58 -0
- package/src/semantics/orgs.ts +383 -0
- package/src/semantics/packages.ts +59 -0
- package/src/semantics/pages.ts +154 -0
- package/src/semantics/projects.ts +246 -0
- package/src/semantics/pulls.ts +421 -0
- package/src/semantics/push-reactions.ts +471 -0
- package/src/semantics/releases.ts +213 -0
- package/src/semantics/repo-invitations.ts +112 -0
- package/src/semantics/repos.ts +373 -0
- package/src/semantics/rulesets.ts +107 -0
- package/src/semantics/search.ts +83 -0
- package/src/semantics/security.ts +153 -0
- package/src/semantics/shared.ts +181 -0
- package/src/semantics/users.ts +252 -0
- package/src/semantics/webhooks.ts +101 -0
- package/test-fixtures/github-known-deviations.json +7 -8
- package/test-fixtures/github-openapi-operations.json +46 -227
- package/src/github-graphql.ts +0 -398
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { GITHUB_MIRROR_STORE_PATH, githubIssuePath, githubPullPath } from "../src/github-shared.js";
|
|
5
|
+
function shortSha(value) {
|
|
6
|
+
return value ? value.slice(0, 7) : "unknown";
|
|
7
|
+
}
|
|
8
|
+
// CI rollup glyph per PR head_sha (✓ success, ✗ failure, ● pending). Undefined for
|
|
9
|
+
// observed-only PRs with no statuses/check-runs — they show no CI indicator (honesty).
|
|
10
|
+
const CI_GLYPH = { success: "✓", failure: "✗", pending: "●" };
|
|
11
|
+
export function CiStatus({ status }) {
|
|
12
|
+
if (!status)
|
|
13
|
+
return null;
|
|
14
|
+
return _jsx("span", { className: `ci-status ${status}`, title: `checks: ${status}`, children: CI_GLYPH[status] ?? "●" });
|
|
15
|
+
}
|
|
16
|
+
function prKey(pr) {
|
|
17
|
+
return `${pr.repo}#${pr.number}`;
|
|
18
|
+
}
|
|
19
|
+
// A workflow-run status/conclusion badge (✓ success, ✗ failure, ⊘ cancelled, ● queued/
|
|
20
|
+
// in_progress) — mirrors GitHub's run-list status glyphs. A completed run shows its
|
|
21
|
+
// conclusion; an in-flight one shows its status.
|
|
22
|
+
const RUN_GLYPH = { success: "✓", failure: "✗", cancelled: "⊘", skipped: "–", queued: "●", in_progress: "●", completed: "✓" };
|
|
23
|
+
export function RunStatusBadge({ status, conclusion }) {
|
|
24
|
+
const effective = status === "completed" ? (conclusion || "completed") : (status || "queued");
|
|
25
|
+
return _jsxs("span", { className: `run-badge ${effective}`, title: `run: ${effective}`, children: [RUN_GLYPH[effective] ?? "●", " ", effective] });
|
|
26
|
+
}
|
|
27
|
+
function initials(name) {
|
|
28
|
+
return (name || "?").split(/[-_\s]+/).map((part) => part[0]).join("").slice(0, 2).toUpperCase() || "?";
|
|
29
|
+
}
|
|
30
|
+
function Avatar({ src, label }) {
|
|
31
|
+
return (_jsx("div", { className: "avatar", children: src ? _jsx("img", { src: src, alt: "", loading: "lazy", referrerPolicy: "no-referrer" }) : initials(label) }));
|
|
32
|
+
}
|
|
33
|
+
const VIEW_NAMES = ["pulls", "issues", "actions", "releases", "discussions", "code", "search", "notifications", "deployments", "projects", "insights", "settings"];
|
|
34
|
+
const DEFAULT_ROUTE = { repo: "all", view: "pulls", selectedKey: "", selectedIssueKey: "" };
|
|
35
|
+
/** `selectedKey`/`selectedIssueKey` are `${repo}#${number}` — split on the LAST `#` (repo
|
|
36
|
+
* names never contain one) to recover the PR/issue's OWN repo, independent of whichever
|
|
37
|
+
* repo tab is currently selected (opening a PR from the "all repos" list still routes to
|
|
38
|
+
* that PR's own `/{owner}/{repo}/pull/{n}`, matching real GitHub). */
|
|
39
|
+
function splitRepoScopedKey(key) {
|
|
40
|
+
const hashIndex = key.lastIndexOf("#");
|
|
41
|
+
return { repo: key.slice(0, hashIndex), number: key.slice(hashIndex + 1) };
|
|
42
|
+
}
|
|
43
|
+
function pathFor(route) {
|
|
44
|
+
const { repo, view, selectedKey, selectedIssueKey } = route;
|
|
45
|
+
// An opened PR/issue always routes to ITS OWN repo's path, regardless of the "all repos"
|
|
46
|
+
// vs single-repo tab currently selected (mirrors real GitHub: /pulls can link out to any
|
|
47
|
+
// repo's /owner/repo/pull/n).
|
|
48
|
+
// The route grammar is real GitHub's — the same one the twin's mirror store stamps on
|
|
49
|
+
// each PR/issue `url` (github-shared.ts), so a rendered link and the address bar agree.
|
|
50
|
+
if (view === "pulls" && selectedKey) {
|
|
51
|
+
const { repo: prRepo, number } = splitRepoScopedKey(selectedKey);
|
|
52
|
+
return githubPullPath(prRepo, number);
|
|
53
|
+
}
|
|
54
|
+
if (view === "issues" && selectedIssueKey) {
|
|
55
|
+
const { repo: issueRepo, number } = splitRepoScopedKey(selectedIssueKey);
|
|
56
|
+
return githubIssuePath(issueRepo, number);
|
|
57
|
+
}
|
|
58
|
+
if (repo === "all" || !repo)
|
|
59
|
+
return `/${view}`;
|
|
60
|
+
return `/${repo}/${view}`;
|
|
61
|
+
}
|
|
62
|
+
function stateFor(pathname) {
|
|
63
|
+
const segments = pathname.split("/").filter(Boolean).map((segment) => decodeURIComponent(segment));
|
|
64
|
+
if (segments.length === 0)
|
|
65
|
+
return DEFAULT_ROUTE;
|
|
66
|
+
if (segments.length === 1 && VIEW_NAMES.includes(segments[0])) {
|
|
67
|
+
return { ...DEFAULT_ROUTE, view: segments[0] };
|
|
68
|
+
}
|
|
69
|
+
if (segments.length >= 2) {
|
|
70
|
+
const repo = `${segments[0]}/${segments[1]}`;
|
|
71
|
+
if (segments.length === 2)
|
|
72
|
+
return { ...DEFAULT_ROUTE, repo, view: "pulls" };
|
|
73
|
+
const tab = segments[2];
|
|
74
|
+
if (tab === "pull" && segments[3])
|
|
75
|
+
return { ...DEFAULT_ROUTE, repo, view: "pulls", selectedKey: `${repo}#${segments[3]}` };
|
|
76
|
+
if (tab === "pulls")
|
|
77
|
+
return { ...DEFAULT_ROUTE, repo, view: "pulls" };
|
|
78
|
+
if (tab === "issues" && segments[3])
|
|
79
|
+
return { ...DEFAULT_ROUTE, repo, view: "issues", selectedIssueKey: `${repo}#${segments[3]}` };
|
|
80
|
+
if (tab === "issues")
|
|
81
|
+
return { ...DEFAULT_ROUTE, repo, view: "issues" };
|
|
82
|
+
if (VIEW_NAMES.includes(tab))
|
|
83
|
+
return { ...DEFAULT_ROUTE, repo, view: tab };
|
|
84
|
+
return { ...DEFAULT_ROUTE, repo, view: "pulls" };
|
|
85
|
+
}
|
|
86
|
+
return DEFAULT_ROUTE;
|
|
87
|
+
}
|
|
88
|
+
function App() {
|
|
89
|
+
const [payload, setPayload] = useState(null);
|
|
90
|
+
const initialRoute = typeof window !== "undefined" ? stateFor(window.location.pathname) : DEFAULT_ROUTE;
|
|
91
|
+
const [repo, setRepo] = useState(initialRoute.repo);
|
|
92
|
+
const [selectedKey, setSelectedKey] = useState(initialRoute.selectedKey);
|
|
93
|
+
const [query, setQuery] = useState("");
|
|
94
|
+
const [view, setView] = useState(initialRoute.view);
|
|
95
|
+
const [selectedProjectId, setSelectedProjectId] = useState(undefined);
|
|
96
|
+
const [selectedRunId, setSelectedRunId] = useState(undefined);
|
|
97
|
+
const [selectedReleaseId, setSelectedReleaseId] = useState(undefined);
|
|
98
|
+
const [selectedDiscussionKey, setSelectedDiscussionKey] = useState(undefined);
|
|
99
|
+
const [selectedIssueKey, setSelectedIssueKey] = useState(initialRoute.selectedIssueKey);
|
|
100
|
+
const [showFilesDiff, setShowFilesDiff] = useState(false);
|
|
101
|
+
const [error, setError] = useState("");
|
|
102
|
+
// Set while applying a popstate-derived route, so the pushState effect below (which reacts
|
|
103
|
+
// to the same state) skips re-pushing the entry the browser just navigated to.
|
|
104
|
+
const skipNextPush = React.useRef(false);
|
|
105
|
+
// The twin's store door (`GET /twin/store/mirror` — the ONE named projection the twin
|
|
106
|
+
// declares for this console) is the only read of twin state; it is the twin's own
|
|
107
|
+
// surface on this origin, never a mirror-private endpoint.
|
|
108
|
+
async function refresh() {
|
|
109
|
+
try {
|
|
110
|
+
const response = await fetch(GITHUB_MIRROR_STORE_PATH);
|
|
111
|
+
const json = await response.json();
|
|
112
|
+
if (!response.ok)
|
|
113
|
+
throw new Error(json.error ?? "Failed to load GitHub state");
|
|
114
|
+
setPayload(json);
|
|
115
|
+
setError("");
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
void refresh();
|
|
123
|
+
const timer = window.setInterval(() => void refresh(), 1500);
|
|
124
|
+
return () => window.clearInterval(timer);
|
|
125
|
+
}, []);
|
|
126
|
+
// pushState on navigation: any click that changes repo/view/selection updates the address
|
|
127
|
+
// bar (no reload) to the vendor-faithful path for the new state.
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
const path = pathFor({ repo, view, selectedKey, selectedIssueKey });
|
|
130
|
+
if (skipNextPush.current) {
|
|
131
|
+
skipNextPush.current = false;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (window.location.pathname !== path)
|
|
135
|
+
window.history.pushState(null, "", path);
|
|
136
|
+
}, [repo, view, selectedKey, selectedIssueKey]);
|
|
137
|
+
// popstate (back/forward): re-derive the view + selection from the restored pathname.
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
function onPopState() {
|
|
140
|
+
const route = stateFor(window.location.pathname);
|
|
141
|
+
skipNextPush.current = true;
|
|
142
|
+
setRepo(route.repo);
|
|
143
|
+
setView(route.view);
|
|
144
|
+
setSelectedKey(route.selectedKey);
|
|
145
|
+
setSelectedIssueKey(route.selectedIssueKey);
|
|
146
|
+
}
|
|
147
|
+
window.addEventListener("popstate", onPopState);
|
|
148
|
+
return () => window.removeEventListener("popstate", onPopState);
|
|
149
|
+
}, []);
|
|
150
|
+
const repos = useMemo(() => Array.from(new Set((payload?.github.pullRequests ?? []).map((pr) => pr.repo))).sort(), [payload]);
|
|
151
|
+
const prs = useMemo(() => {
|
|
152
|
+
const search = query.trim().toLowerCase();
|
|
153
|
+
return (payload?.github.pullRequests ?? [])
|
|
154
|
+
.filter((pr) => repo === "all" || !repo || pr.repo === repo)
|
|
155
|
+
.filter((pr) => {
|
|
156
|
+
if (!search)
|
|
157
|
+
return true;
|
|
158
|
+
return [
|
|
159
|
+
pr.repo,
|
|
160
|
+
String(pr.number),
|
|
161
|
+
pr.title,
|
|
162
|
+
pr.author,
|
|
163
|
+
pr.sourceIssue,
|
|
164
|
+
pr.sourceIssueTitle,
|
|
165
|
+
pr.state,
|
|
166
|
+
pr.url,
|
|
167
|
+
].filter(Boolean).join(" ").toLowerCase().includes(search);
|
|
168
|
+
})
|
|
169
|
+
.sort((a, b) => {
|
|
170
|
+
const aDate = Date.parse(a.updatedAt || a.mergedAt || a.createdAt || "");
|
|
171
|
+
const bDate = Date.parse(b.updatedAt || b.mergedAt || b.createdAt || "");
|
|
172
|
+
if (Number.isFinite(aDate) && Number.isFinite(bDate) && aDate !== bDate)
|
|
173
|
+
return bDate - aDate;
|
|
174
|
+
return Number(b.number) - Number(a.number);
|
|
175
|
+
});
|
|
176
|
+
}, [payload, query, repo]);
|
|
177
|
+
const issues = useMemo(() => {
|
|
178
|
+
const search = query.trim().toLowerCase();
|
|
179
|
+
return (payload?.github.issues ?? [])
|
|
180
|
+
.filter((issue) => repo === "all" || !repo || issue.repo === repo)
|
|
181
|
+
.filter((issue) => {
|
|
182
|
+
if (!search)
|
|
183
|
+
return true;
|
|
184
|
+
return [issue.repo, String(issue.number), issue.title, issue.state, issue.url]
|
|
185
|
+
.filter(Boolean).join(" ").toLowerCase().includes(search);
|
|
186
|
+
})
|
|
187
|
+
.sort((a, b) => {
|
|
188
|
+
const aDate = Date.parse(a.updatedAt || a.createdAt || "");
|
|
189
|
+
const bDate = Date.parse(b.updatedAt || b.createdAt || "");
|
|
190
|
+
if (Number.isFinite(aDate) && Number.isFinite(bDate) && aDate !== bDate)
|
|
191
|
+
return bDate - aDate;
|
|
192
|
+
return Number(b.number) - Number(a.number);
|
|
193
|
+
});
|
|
194
|
+
}, [payload, query, repo]);
|
|
195
|
+
const runs = useMemo(() => {
|
|
196
|
+
const search = query.trim().toLowerCase();
|
|
197
|
+
return (payload?.github.runs ?? [])
|
|
198
|
+
.filter((run) => repo === "all" || !repo || run.repo === repo)
|
|
199
|
+
.filter((run) => {
|
|
200
|
+
if (!search)
|
|
201
|
+
return true;
|
|
202
|
+
return [run.repo, String(run.runNumber), run.workflowName, run.name, run.event, run.headBranch, run.status, run.conclusion]
|
|
203
|
+
.filter(Boolean).join(" ").toLowerCase().includes(search);
|
|
204
|
+
})
|
|
205
|
+
.sort((a, b) => {
|
|
206
|
+
const aDate = Date.parse(a.updatedAt || a.createdAt || "");
|
|
207
|
+
const bDate = Date.parse(b.updatedAt || b.createdAt || "");
|
|
208
|
+
if (Number.isFinite(aDate) && Number.isFinite(bDate) && aDate !== bDate)
|
|
209
|
+
return bDate - aDate;
|
|
210
|
+
return Number(b.id) - Number(a.id);
|
|
211
|
+
});
|
|
212
|
+
}, [payload, query, repo]);
|
|
213
|
+
const jobs = payload?.github.jobs ?? [];
|
|
214
|
+
const releases = useMemo(() => {
|
|
215
|
+
const search = query.trim().toLowerCase();
|
|
216
|
+
return (payload?.github.releases ?? [])
|
|
217
|
+
.filter((rel) => repo === "all" || !repo || rel.repo === repo)
|
|
218
|
+
.filter((rel) => {
|
|
219
|
+
if (!search)
|
|
220
|
+
return true;
|
|
221
|
+
return [rel.repo, rel.tagName, rel.name, rel.body, rel.draft ? "draft" : "", rel.prerelease ? "prerelease" : ""]
|
|
222
|
+
.filter(Boolean).join(" ").toLowerCase().includes(search);
|
|
223
|
+
})
|
|
224
|
+
.sort((a, b) => {
|
|
225
|
+
const aDate = Date.parse(a.publishedAt || a.createdAt || "");
|
|
226
|
+
const bDate = Date.parse(b.publishedAt || b.createdAt || "");
|
|
227
|
+
if (Number.isFinite(aDate) && Number.isFinite(bDate) && aDate !== bDate)
|
|
228
|
+
return bDate - aDate;
|
|
229
|
+
return Number(b.id) - Number(a.id);
|
|
230
|
+
});
|
|
231
|
+
}, [payload, query, repo]);
|
|
232
|
+
const discussions = useMemo(() => {
|
|
233
|
+
const search = query.trim().toLowerCase();
|
|
234
|
+
return (payload?.github.discussions ?? [])
|
|
235
|
+
.filter((d) => repo === "all" || !repo || d.repo === repo)
|
|
236
|
+
.filter((d) => {
|
|
237
|
+
if (!search)
|
|
238
|
+
return true;
|
|
239
|
+
return [d.repo, String(d.number), d.title, d.body, d.categoryName, d.state, d.isAnswered ? "answered" : ""]
|
|
240
|
+
.filter(Boolean).join(" ").toLowerCase().includes(search);
|
|
241
|
+
})
|
|
242
|
+
.sort((a, b) => {
|
|
243
|
+
const aDate = Date.parse(a.updatedAt || a.createdAt || "");
|
|
244
|
+
const bDate = Date.parse(b.updatedAt || b.createdAt || "");
|
|
245
|
+
if (Number.isFinite(aDate) && Number.isFinite(bDate) && aDate !== bDate)
|
|
246
|
+
return bDate - aDate;
|
|
247
|
+
return Number(b.number) - Number(a.number);
|
|
248
|
+
});
|
|
249
|
+
}, [payload, query, repo]);
|
|
250
|
+
const deployments = useMemo(() => {
|
|
251
|
+
const search = query.trim().toLowerCase();
|
|
252
|
+
return (payload?.github.deployments ?? [])
|
|
253
|
+
.filter((d) => repo === "all" || !repo || d.repo === repo)
|
|
254
|
+
.filter((d) => {
|
|
255
|
+
if (!search)
|
|
256
|
+
return true;
|
|
257
|
+
return [d.repo, d.environment, d.ref, d.state, d.description].filter(Boolean).join(" ").toLowerCase().includes(search);
|
|
258
|
+
})
|
|
259
|
+
.sort((a, b) => Number(b.id) - Number(a.id));
|
|
260
|
+
}, [payload, query, repo]);
|
|
261
|
+
const environments = useMemo(() => (payload?.github.environments ?? []).filter((e) => repo === "all" || !repo || e.repo === repo), [payload, repo]);
|
|
262
|
+
// Projects v2 boards are owner-scoped (not repo-scoped) — show all when "all", else those
|
|
263
|
+
// whose owner matches the selected repo's owner. Insights/Settings are repo-scoped.
|
|
264
|
+
const projectBoards = useMemo(() => (payload?.github.projectBoards ?? []).filter((p) => repo === "all" || !repo || p.owner === repo.split("/")[0]), [payload, repo]);
|
|
265
|
+
const insights = useMemo(() => (payload?.github.insights ?? []).filter((i) => repo === "all" || !repo || i.repo === repo), [payload, repo]);
|
|
266
|
+
const settings = useMemo(() => (payload?.github.settings ?? []).filter((s) => repo === "all" || !repo || s.repo === repo), [payload, repo]);
|
|
267
|
+
const current = prs.find((pr) => prKey(pr) === selectedKey) ?? prs[0];
|
|
268
|
+
const currentReviews = current
|
|
269
|
+
? (payload?.github.reviews ?? []).filter((review) => review.repo === current.repo && Number(review.number) === Number(current.number))
|
|
270
|
+
: [];
|
|
271
|
+
const currentCommits = current
|
|
272
|
+
? (payload?.github.commits ?? []).filter((commit) => commit.repo === current.repo && Number(commit.number) === Number(current.number))
|
|
273
|
+
: [];
|
|
274
|
+
const currentComments = current
|
|
275
|
+
? (payload?.github.comments ?? []).filter((comment) => comment.repo === current.repo && Number(comment.number) === Number(current.number))
|
|
276
|
+
: [];
|
|
277
|
+
const currentReviewComments = current
|
|
278
|
+
? (payload?.github.reviewComments ?? []).filter((comment) => comment.repo === current.repo && Number(comment.number) === Number(current.number))
|
|
279
|
+
: [];
|
|
280
|
+
const currentFiles = current
|
|
281
|
+
? (payload?.github.files ?? []).filter((file) => file.repo === current.repo && Number(file.number) === Number(current.number))
|
|
282
|
+
: [];
|
|
283
|
+
// NOTE: `selectedKey` is intentionally NOT auto-synced to `current` here (a prior version
|
|
284
|
+
// did) — `current` falling back to `prs[0]` is purely a display default for the split-pane
|
|
285
|
+
// preview; auto-writing that fallback into `selectedKey` would immediately pushState the
|
|
286
|
+
// address bar to `/pull/{n}` on every list view, which is not vendor-faithful (GitHub's
|
|
287
|
+
// `/pulls` list never auto-navigates into a PR). `selectedKey` only changes on an explicit
|
|
288
|
+
// row click or a deep link (`stateFor`), so the URL only reflects a PR the user actually
|
|
289
|
+
// opened. The legacy `?repo=&pr=|number=|url=` query-param deep-link mechanism this effect
|
|
290
|
+
// used to implement is REPLACED by real path routing (see pathFor/stateFor above) — no
|
|
291
|
+
// dual/legacy path.
|
|
292
|
+
return (_jsxs("main", { className: "browser-shell", children: [_jsxs("section", { className: "browser-tabs", "aria-label": "Repository browser tabs", children: [_jsxs("button", { className: `browser-tab ${repo === "all" ? "active" : ""}`, onClick: () => {
|
|
293
|
+
setRepo("all");
|
|
294
|
+
setSelectedKey("");
|
|
295
|
+
}, children: [_jsx("span", { children: "All repositories" }), _jsx("strong", { children: payload?.github.pullRequests?.length ?? 0 })] }), repos.map((name) => {
|
|
296
|
+
const count = (payload?.github.pullRequests ?? []).filter((pr) => pr.repo === name).length;
|
|
297
|
+
return (_jsxs("button", { className: `browser-tab ${name === repo ? "active" : ""}`, onClick: () => {
|
|
298
|
+
setRepo(name);
|
|
299
|
+
setSelectedKey("");
|
|
300
|
+
}, children: [_jsx("span", { children: name }), _jsx("strong", { children: count })] }, name));
|
|
301
|
+
})] }), _jsxs("section", { className: "browser-address", children: [_jsx("span", { className: "browser-dot red" }), _jsx("span", { className: "browser-dot yellow" }), _jsx("span", { className: "browser-dot green" }), _jsxs("div", { className: "address-bar", children: ["github.local", pathFor({ repo, view, selectedKey, selectedIssueKey })] })] }), _jsxs("section", { className: "github-shell", children: [_jsxs("header", { className: "global-header", children: [_jsx("div", { className: "octo", children: "GH" }), _jsx("input", { className: "search", value: "Search or jump to...", readOnly: true }), _jsxs("nav", { className: "global-nav", children: [_jsx("span", { children: "Pull requests" }), _jsx("span", { children: "Issues" }), _jsx("span", { children: "Marketplace" }), _jsx("span", { children: "Explore" })] }), _jsx("div", { className: "global-spacer" }), _jsx("span", { className: "pill-dark", children: "mock github" })] }), _jsxs("section", { className: "repo-head", children: [_jsx("div", { className: "repo-line", children: repo === "all" ? (_jsxs(_Fragment, { children: [_jsx("button", { children: "all" }), _jsx("span", { children: "/" }), _jsx("strong", { children: "repositories" })] })) : (_jsxs(_Fragment, { children: [_jsx("button", { children: repo.split("/")[0] || "owner" }), _jsx("span", { children: "/" }), _jsx("strong", { children: repo.split("/")[1] || "repository" })] })) }), _jsxs("nav", { className: "repo-tabs", children: [_jsx("button", { children: "Code" }), _jsxs("button", { className: view === "issues" ? "active" : "", onClick: () => setView("issues"), children: ["Issues ", _jsx("span", { className: "count", children: issues.length })] }), _jsxs("button", { className: view === "pulls" ? "active" : "", onClick: () => setView("pulls"), children: ["Pull requests ", _jsx("span", { className: "count", children: prs.length })] }), _jsxs("button", { className: view === "actions" ? "active" : "", onClick: () => setView("actions"), children: ["Actions ", _jsx("span", { className: "count", children: runs.length })] }), _jsxs("button", { className: view === "releases" ? "active" : "", onClick: () => setView("releases"), children: ["Releases ", _jsx("span", { className: "count", children: releases.length })] }), _jsxs("button", { className: view === "discussions" ? "active" : "", onClick: () => setView("discussions"), children: ["Discussions ", _jsx("span", { className: "count", children: discussions.length })] }), _jsxs("button", { className: view === "code" ? "active" : "", onClick: () => setView("code"), children: ["Code ", _jsx("span", { className: "count", children: (payload?.github.contents ?? []).length })] }), _jsx("button", { className: view === "search" ? "active" : "", onClick: () => setView("search"), children: "Search" }), _jsxs("button", { className: view === "notifications" ? "active" : "", onClick: () => setView("notifications"), children: ["Notifications ", _jsx("span", { className: "count", children: (payload?.github.notifications ?? []).filter((n) => n.unread).length })] }), _jsxs("button", { className: view === "deployments" ? "active" : "", onClick: () => setView("deployments"), children: ["Deployments ", _jsx("span", { className: "count", children: deployments.length })] }), _jsxs("button", { className: view === "projects" ? "active" : "", onClick: () => setView("projects"), children: ["Projects ", _jsx("span", { className: "count", children: projectBoards.length })] }), _jsx("button", { children: "Security" }), _jsx("button", { className: view === "insights" ? "active" : "", onClick: () => setView("insights"), children: "Insights" }), _jsxs("button", { className: view === "settings" ? "active" : "", onClick: () => setView("settings"), children: ["Settings ", _jsx("span", { className: "count", children: settings.length })] })] })] }), _jsxs("div", { className: "content", children: [error ? _jsx("div", { className: "error", children: error }) : null, _jsxs("div", { className: "toolbar", children: [_jsx("input", { className: "pr-filter", value: query, onChange: (event) => setQuery(event.target.value), placeholder: view === "issues" ? "Search issues by title, number, repo" : view === "actions" ? "Search runs by workflow, number, branch, status" : view === "releases" ? "Search releases by tag, name, draft/prerelease" : view === "discussions" ? "Search discussions by title, category, answered" : "Search PRs by title, number, repo, issue, author" }), _jsx("button", { className: "new-pr", children: view === "issues" ? "New issue" : view === "actions" ? "Run workflow" : view === "releases" ? "Draft a new release" : view === "discussions" ? "New discussion" : "New pull request" })] }), view === "issues" ? (_jsxs("div", { className: "split", children: [_jsx("main", { children: _jsx(IssuesTable, { issues: issues, onSelect: setSelectedIssueKey }) }), _jsx(IssueDetail, { issue: issues.find((i) => `${i.repo}#${i.number}` === selectedIssueKey) ?? issues[0] })] })) : view === "actions" ? (_jsx(ActionsView, { runs: runs, jobs: jobs, selectedRunId: selectedRunId, onSelect: setSelectedRunId })) : view === "releases" ? (_jsx(ReleasesView, { releases: releases, selectedReleaseId: selectedReleaseId, onSelect: setSelectedReleaseId })) : view === "discussions" ? (_jsx(DiscussionsView, { discussions: discussions, selectedKey: selectedDiscussionKey, onSelect: setSelectedDiscussionKey })) : view === "code" ? (_jsx(CodeBrowser, { contents: (payload?.github.contents ?? []).filter((c) => repo === "all" || !repo || c.repo === repo) })) : view === "search" ? (_jsx(SearchView, { query: query, prs: payload?.github.pullRequests ?? [], issues: payload?.github.issues ?? [], contents: payload?.github.contents ?? [] })) : view === "notifications" ? (_jsx(NotificationsView, { notifications: (payload?.github.notifications ?? []).filter((n) => repo === "all" || !repo || n.repo === repo) })) : view === "deployments" ? (_jsx(DeploymentsView, { deployments: deployments, environments: environments })) : view === "projects" ? (_jsx(ProjectBoardView, { boards: projectBoards, selectedId: selectedProjectId, onSelect: setSelectedProjectId })) : view === "insights" ? (_jsx(InsightsView, { insights: insights })) : view === "settings" ? (_jsx(SettingsView, { settings: settings })) : (_jsxs("div", { className: "split", children: [_jsxs("main", { children: [_jsx(PrList, { prs: prs, reviews: payload?.github.reviews ?? [], selectedKey: selectedKey, onSelect: setSelectedKey }), _jsxs("div", { className: "pr-tabs", children: [_jsx("button", { className: showFilesDiff ? "" : "active", onClick: () => setShowFilesDiff(false), children: "Conversation" }), _jsxs("button", { className: showFilesDiff ? "active" : "", onClick: () => setShowFilesDiff(true), children: ["Files changed ", _jsx("span", { className: "count", children: current?.changedFiles ?? currentFiles.length })] })] }), showFilesDiff ? (_jsx(FilesDiff, { files: currentFiles })) : (_jsx(Conversation, { pr: current, reviews: currentReviews, commits: currentCommits, comments: currentComments, reviewComments: currentReviewComments, files: currentFiles }))] }), _jsx(Aside, { pr: current, reviews: currentReviews, commits: currentCommits, files: currentFiles })] }))] })] })] }));
|
|
302
|
+
}
|
|
303
|
+
export function PrList({ prs, reviews, selectedKey, onSelect }) {
|
|
304
|
+
return (_jsxs("section", { className: "list-box", children: [_jsxs("div", { className: "list-head", children: [_jsx("span", { children: _jsxs("strong", { children: [prs.length, " pull requests"] }) }), _jsx("span", { children: "Updated" })] }), prs.length ? prs.map((pr) => {
|
|
305
|
+
const approvedCount = reviews.filter((review) => review.repo === pr.repo && Number(review.number) === Number(pr.number) && review.state === "approved").length;
|
|
306
|
+
const state = pr.merged ? "merged" : (pr.state ?? "open");
|
|
307
|
+
return (_jsxs("button", { className: `pr-row ${prKey(pr) === selectedKey ? "active" : ""}`, onClick: () => onSelect?.(prKey(pr)), children: [_jsx("span", { className: `status-icon ${state}`, children: "o" }), _jsxs("span", { children: [_jsx("span", { className: "pr-title", children: pr.title || pr.url || `${pr.repo}#${pr.number}` }), _jsxs("div", { className: "pr-meta", children: [pr.repo, " #", pr.number, " \u00B7 ", state, pr.author ? ` · opened by ${pr.author}` : "", pr.updatedAt ? ` · updated ${new Date(pr.updatedAt).toLocaleString()}` : pr.createdAt ? ` · ${new Date(pr.createdAt).toLocaleString()}` : "", pr.sourceIssue ? ` · ${pr.sourceIssue}` : "", pr.changedFiles != null ? ` · ${pr.changedFiles} files` : "", pr.commitsCount != null ? ` · ${pr.commitsCount} commits` : ""] })] }), _jsxs("span", { className: "labels", children: [_jsx(CiStatus, { status: pr.ciStatus }), _jsx("span", { className: `state-badge ${state}`, children: state }), approvedCount ? _jsxs("span", { className: "label approved", children: [approvedCount, " approved"] }) : null, pr.milestone ? _jsxs("span", { className: "label milestone", title: "Milestone", children: ["\uD83C\uDFF7 ", pr.milestone] }) : null, (pr.labels ?? []).slice(0, 2).map((label) => label.name ? _jsx("span", { className: "label", children: label.name }, label.name) : null), (pr.assignees ?? []).slice(0, 2).map((login) => _jsxs("span", { className: "label assignee", children: ["@", login] }, login))] })] }, prKey(pr)));
|
|
308
|
+
}) : _jsx("div", { className: "empty", children: "No pull requests match this view." })] }));
|
|
309
|
+
}
|
|
310
|
+
export function IssuesTable({ issues, onSelect }) {
|
|
311
|
+
return (_jsxs("section", { className: "list-box issues-box", children: [_jsxs("div", { className: "list-head", children: [_jsx("span", { children: _jsxs("strong", { children: [issues.length, " issues"] }) }), _jsx("span", { children: "Updated" })] }), issues.length ? issues.map((issue) => {
|
|
312
|
+
const state = issue.state ?? "open";
|
|
313
|
+
return (_jsxs("button", { className: "pr-row issue-row", onClick: () => onSelect?.(`${issue.repo}#${issue.number}`), children: [_jsx("span", { className: `status-icon ${state === "closed" ? "closed" : "open"}`, children: "!" }), _jsxs("span", { children: [_jsx("span", { className: "pr-title", children: issue.title || issue.url || `${issue.repo}#${issue.number}` }), _jsxs("div", { className: "pr-meta", children: [issue.repo, " #", issue.number, " \u00B7 ", state, issue.updatedAt ? ` · updated ${new Date(issue.updatedAt).toLocaleString()}` : issue.createdAt ? ` · ${new Date(issue.createdAt).toLocaleString()}` : ""] }), (issue.linked ?? []).length ? (_jsxs("div", { className: "linked-issues", children: ["linked:", " ", (issue.linked ?? []).map((link, idx) => (_jsxs("a", { className: "linked-ref", href: link.url, target: "_blank", rel: "noreferrer", children: [link.type === "pull_request" ? "PR" : "issue", " #", link.number] }, `linked-${idx}`)))] })) : null] }), _jsxs("span", { className: "labels", children: [_jsx("span", { className: `state-badge ${state}`, children: state }), issue.milestone ? _jsxs("span", { className: "label milestone", title: "Milestone", children: ["\uD83C\uDFF7 ", issue.milestone] }) : null, (issue.labels ?? []).slice(0, 3).map((label) => label.name ? _jsx("span", { className: "label", children: label.name }, label.name) : null), (issue.assignees ?? []).slice(0, 2).map((login) => _jsxs("span", { className: "label assignee", children: ["@", login] }, login))] })] }, `${issue.repo}#${issue.number}`));
|
|
314
|
+
}) : _jsx("div", { className: "empty", children: "No issues match this view." })] }));
|
|
315
|
+
}
|
|
316
|
+
// The PR Files-changed DIFF view: renders each changed file's unified-diff hunks as
|
|
317
|
+
// +/- lines (the GitHub "Files changed" tab). Hunk text comes from the LOCAL PR write's
|
|
318
|
+
// `patch`; observed PRs carry no per-file diff, so a file with no patch shows a summary row
|
|
319
|
+
// only (no fabricated hunks). Each diff line is classed add/del/context so it renders like GitHub.
|
|
320
|
+
export function FilesDiff({ files }) {
|
|
321
|
+
if (!files.length)
|
|
322
|
+
return _jsx("div", { className: "empty", children: "No file diffs in this snapshot." });
|
|
323
|
+
return (_jsxs("section", { className: "files-diff", "data-testid": "files-diff", children: [_jsxs("h3", { children: ["Files changed ", _jsx("span", { className: "count", children: files.length })] }), files.map((file) => (_jsxs("article", { className: "diff-file", children: [_jsxs("div", { className: "diff-file-head", children: [_jsx("span", { className: `diff-status ${file.status ?? "modified"}`, children: file.status ?? "modified" }), _jsx("strong", { className: "diff-filename", children: file.filename }), _jsxs("span", { className: "diff-stat", children: ["+", file.additions ?? 0, " \u2212", file.deletions ?? 0] })] }), file.patch ? (_jsx("pre", { className: "diff-hunk", "data-testid": "diff-hunk", children: file.patch.split("\n").map((line, idx) => {
|
|
324
|
+
const cls = line.startsWith("+") && !line.startsWith("+++") ? "diff-line-add"
|
|
325
|
+
: line.startsWith("-") && !line.startsWith("---") ? "diff-line-del"
|
|
326
|
+
: line.startsWith("@@") ? "diff-line-hunk" : "diff-line-ctx";
|
|
327
|
+
return _jsx("div", { className: cls, children: line || " " }, idx);
|
|
328
|
+
}) })) : (_jsx("div", { className: "sha diff-no-patch", children: "No diff hunk in snapshot (observed PRs carry counts only)." }))] }, `${file.repo}#${file.number}#${file.filename}`)))] }));
|
|
329
|
+
}
|
|
330
|
+
// The Issue DETAIL page: the selected issue's header (state/labels/assignees/milestone),
|
|
331
|
+
// its derived state-change Timeline (labeled/assigned/locked/closed events), and its
|
|
332
|
+
// comments feed. Mirrors GitHub's single-issue screen. Observed comments are labelled.
|
|
333
|
+
export function IssueDetail({ issue }) {
|
|
334
|
+
if (!issue)
|
|
335
|
+
return _jsx("div", { className: "empty", children: "Select an issue." });
|
|
336
|
+
const state = issue.state ?? "open";
|
|
337
|
+
return (_jsxs("section", { className: "issue-detail", "data-testid": "issue-detail", children: [_jsxs("h2", { children: [issue.title || `${issue.repo}#${issue.number}`, " ", _jsxs("span", { children: ["#", issue.number] })] }), _jsxs("div", { className: "state-line", children: [_jsxs("span", { className: `state-badge ${state}`, children: [state, issue.stateReason ? ` (${issue.stateReason})` : ""] }), issue.locked ? _jsx("span", { className: "label", title: "Locked", children: "\uD83D\uDD12 locked" }) : null, _jsx("strong", { children: issue.repo }), issue.milestone ? _jsxs("span", { className: "label milestone", title: "Milestone", children: ["\uD83C\uDFF7 ", issue.milestone] }) : null, issue.url ? _jsx("a", { className: "branch", href: issue.url, target: "_blank", rel: "noreferrer", children: "View on GitHub" }) : null] }), _jsxs("div", { className: "meta-row", children: [(issue.labels ?? []).map((l) => l.name ? _jsx("span", { className: "label", children: l.name }, l.name) : null), (issue.assignees ?? []).map((login) => _jsxs("span", { className: "label assignee", children: ["@", login] }, login))] }), _jsxs("article", { className: "comment", children: [_jsx("div", { className: "comment-head", children: _jsx("strong", { children: "opened this issue" }) }), _jsx("div", { className: "comment-body", children: issue.body || "No issue body in snapshot." })] }), _jsxs("section", { className: "snapshot-section issue-timeline", "data-testid": "issue-timeline", children: [_jsx("h3", { children: "Timeline" }), (issue.timeline ?? []).length ? (issue.timeline ?? []).map((ev, idx) => (_jsxs("div", { className: "timeline-event", children: [_jsx("span", { className: "event-kind", children: ev.event }), ev.detail ? _jsxs("span", { className: "event-detail", children: [" ", ev.detail] }) : null] }, `tl-${idx}`))) : _jsx("p", { className: "sha", children: "No timeline events." })] }), _jsxs("section", { className: "snapshot-section", children: [_jsxs("h3", { children: ["Comments ", _jsx("span", { className: "count", children: (issue.comments ?? []).length })] }), (issue.comments ?? []).length ? (issue.comments ?? []).map((c) => (_jsxs("article", { className: "comment issue-detail-comment", "data-testid": "issue-detail-comment", children: [_jsxs("div", { className: "comment-head", children: [_jsx("strong", { children: c.author || "GitHub user" }), " commented ", c.createdAt ? new Date(c.createdAt).toLocaleString() : ""] }), _jsx("div", { className: "comment-body", children: c.body || "" })] }, `c-${c.id}`))) : _jsx("p", { className: "sha", children: "No comments." })] })] }));
|
|
338
|
+
}
|
|
339
|
+
export function CodeBrowser({ contents }) {
|
|
340
|
+
if (!contents.length)
|
|
341
|
+
return _jsx("div", { className: "empty", "data-testid": "code-browser", children: "No files in this snapshot." });
|
|
342
|
+
const dirs = new Set();
|
|
343
|
+
for (const c of contents) {
|
|
344
|
+
const parts = c.path.split("/");
|
|
345
|
+
for (let i = 1; i < parts.length; i++)
|
|
346
|
+
dirs.add(parts.slice(0, i).join("/"));
|
|
347
|
+
}
|
|
348
|
+
return (_jsxs("section", { className: "code-browser", "data-testid": "code-browser", children: [_jsxs("h3", { children: ["Code ", _jsxs("span", { className: "count", children: [contents.length, " files"] })] }), _jsxs("div", { className: "file-tree", "data-testid": "file-tree", children: [[...dirs].sort().map((d) => (_jsxs("div", { className: "tree-row tree-dir", "data-testid": "tree-dir", children: ["\uD83D\uDCC1 ", d] }, `d-${d}`))), contents.map((c) => (_jsxs("div", { className: "tree-row tree-file", "data-testid": "tree-file", children: [_jsxs("span", { children: ["\uD83D\uDCC4 ", c.path] }), _jsxs("span", { className: "sha", children: [c.size ?? 0, " bytes"] })] }, `${c.repo}#${c.path}`)))] })] }));
|
|
349
|
+
}
|
|
350
|
+
// The global Search results screen — filters PRs/issues/repos files by the query. Mirrors
|
|
351
|
+
// GitHub's search page (a single box over multiple result kinds). Local, over the snapshot.
|
|
352
|
+
export function SearchView({ query, prs, issues, contents }) {
|
|
353
|
+
const q = query.trim().toLowerCase();
|
|
354
|
+
const match = (...vals) => q === "" || vals.some((v) => (v ?? "").toLowerCase().includes(q));
|
|
355
|
+
const prHits = prs.filter((p) => match(p.title, p.repo));
|
|
356
|
+
const issueHits = issues.filter((i) => match(i.title, i.repo));
|
|
357
|
+
const codeHits = contents.filter((c) => match(c.path, c.repo));
|
|
358
|
+
const total = prHits.length + issueHits.length + codeHits.length;
|
|
359
|
+
return (_jsxs("section", { className: "search-view", "data-testid": "search-view", children: [_jsxs("h3", { children: ["Search results ", _jsx("span", { className: "count", children: total })] }), _jsxs("div", { className: "search-group", "data-testid": "search-prs", children: [_jsxs("h4", { children: ["Pull requests (", prHits.length, ")"] }), prHits.map((p) => _jsxs("div", { className: "search-hit", children: [p.repo, " #", p.number, " \u00B7 ", p.title] }, `p-${p.repo}#${p.number}`))] }), _jsxs("div", { className: "search-group", "data-testid": "search-issues", children: [_jsxs("h4", { children: ["Issues (", issueHits.length, ")"] }), issueHits.map((i) => _jsxs("div", { className: "search-hit", children: [i.repo, " #", i.number, " \u00B7 ", i.title] }, `i-${i.repo}#${i.number}`))] }), _jsxs("div", { className: "search-group", "data-testid": "search-code", children: [_jsxs("h4", { children: ["Code (", codeHits.length, ")"] }), codeHits.map((c) => _jsxs("div", { className: "search-hit", children: [c.repo, " \u00B7 ", c.path] }, `c-${c.repo}#${c.path}`))] })] }));
|
|
360
|
+
}
|
|
361
|
+
export function NotificationsView({ notifications }) {
|
|
362
|
+
if (!notifications.length)
|
|
363
|
+
return _jsx("div", { className: "empty", "data-testid": "notifications-view", children: "No notifications." });
|
|
364
|
+
return (_jsxs("section", { className: "notifications-view", "data-testid": "notifications-view", children: [_jsxs("h3", { children: ["Notifications ", _jsxs("span", { className: "count", children: [notifications.filter((n) => n.unread).length, " unread"] })] }), notifications.map((n) => (_jsxs("div", { className: `notification-row ${n.unread ? "unread" : "read"}`, "data-testid": "notification-row", children: [n.unread ? _jsx("span", { className: "unread-dot", "data-testid": "unread-dot", children: "\u25CF" }) : _jsx("span", { className: "read-dot", children: "\u25CB" }), _jsxs("span", { className: "notif-subject", children: [_jsx("strong", { children: n.title }), " ", _jsx("span", { className: "sha", children: n.type })] }), _jsx("span", { className: "notif-reason label", children: n.reason }), _jsx("span", { className: "sha", children: n.repo })] }, n.id)))] }));
|
|
365
|
+
}
|
|
366
|
+
// The Deployments view (the repo's Deployments/Environments screen). Models the Deployments
|
|
367
|
+
// API objects — each deployment carries its environment, ref, and LATEST status (the
|
|
368
|
+
// deployment's state), plus a per-environment protection summary. Maps GitHub's Environments
|
|
369
|
+
// view: most-recent deployments with their state badge + the environment list.
|
|
370
|
+
export function DeploymentState({ state }) {
|
|
371
|
+
const s = (state || "pending").toLowerCase();
|
|
372
|
+
const cls = s === "success" ? "success" : (s === "failure" || s === "error" ? "failure" : (s === "inactive" ? "inactive" : "pending"));
|
|
373
|
+
return _jsx("span", { className: `deployment-state ${cls}`, "data-testid": "deployment-state", title: `deployment: ${s}`, children: s });
|
|
374
|
+
}
|
|
375
|
+
export function DeploymentsView({ deployments, environments }) {
|
|
376
|
+
if (!deployments.length && !environments.length)
|
|
377
|
+
return _jsx("div", { className: "empty", "data-testid": "deployments-view", children: "No deployments." });
|
|
378
|
+
return (_jsxs("section", { className: "deployments-view", "data-testid": "deployments-view", children: [environments.length ? (_jsxs("div", { className: "environments-list", "data-testid": "environments-list", children: [_jsxs("h3", { children: ["Environments ", _jsx("span", { className: "count", children: environments.length })] }), environments.map((env) => (_jsxs("div", { className: "environment-row", "data-testid": "environment-row", children: [_jsx("span", { className: "environment-name", children: _jsx("strong", { children: env.name }) }), _jsx("span", { className: "sha", children: env.repo }), env.protected ? (_jsxs("span", { className: "environment-protected label", "data-testid": "environment-protected", children: ["protected", env.waitTimer ? ` · ${env.waitTimer}m wait` : "", env.reviewers && env.reviewers.length ? ` · ${env.reviewers.length} reviewers` : ""] })) : _jsx("span", { className: "label", children: "no protection rules" })] }, `${env.repo}#${env.name}`)))] })) : null, _jsxs("div", { className: "deployments-list", children: [_jsxs("h3", { children: ["Deployments ", _jsx("span", { className: "count", children: deployments.length })] }), deployments.length ? deployments.map((d) => (_jsxs("div", { className: "deployment-row", "data-testid": "deployment-row", children: [_jsx(DeploymentState, { state: d.state }), _jsxs("span", { className: "deployment-env label", "data-testid": "deployment-env", children: [d.environment, d.production ? " · production" : ""] }), _jsxs("span", { className: "deployment-ref", children: [_jsx("strong", { children: d.ref }), _jsxs("div", { className: "deployment-meta", children: [d.repo, " \u00B7 deploy #", d.id, d.sha ? ` · ${shortSha(d.sha)}` : "", d.description ? ` · ${d.description}` : ""] })] }), d.environmentUrl ? _jsx("a", { className: "branch", href: d.environmentUrl, target: "_blank", rel: "noreferrer", children: "View deployment" }) : null] }, d.id))) : _jsx("div", { className: "empty", children: "No deployments yet." })] })] }));
|
|
379
|
+
}
|
|
380
|
+
// The Projects v2 board view (GitHub Projects). Renders the selected project's BOARD: its
|
|
381
|
+
// Status field's options as COLUMNS (swim-lanes) with the items grouped into them by their
|
|
382
|
+
// Status value, plus a "No Status" lane. Data-coupled: the column card counts come straight
|
|
383
|
+
// from the modeled project items + their field values (seeding items moves the counts).
|
|
384
|
+
export function ProjectBoardView({ boards, selectedId, onSelect }) {
|
|
385
|
+
if (!boards.length)
|
|
386
|
+
return _jsx("div", { className: "empty", "data-testid": "project-board-view", children: "No projects." });
|
|
387
|
+
const board = boards.find((b) => b.id === selectedId) ?? boards[0];
|
|
388
|
+
const columns = [...board.columns, board.noStatus];
|
|
389
|
+
return (_jsxs("section", { className: "project-board-view", "data-testid": "project-board-view", children: [_jsxs("div", { className: "project-list", children: [_jsxs("h3", { children: ["Projects ", _jsx("span", { className: "count", children: boards.length })] }), boards.map((b) => (_jsxs("div", { className: `project-row${b.id === board.id ? " active" : ""}`, "data-testid": "project-row", onClick: () => onSelect(b.id), children: [_jsxs("span", { className: "project-title", children: [_jsx("strong", { children: b.title }), " #", b.number] }), _jsxs("span", { className: "project-item-count label", "data-testid": "project-item-count", children: [b.itemCount, " items"] }), b.closed ? _jsx("span", { className: "label", children: "closed" }) : null, _jsx("span", { className: "sha", children: b.owner })] }, b.id)))] }), _jsxs("div", { className: "project-board", "data-testid": "project-board", children: [_jsx("div", { className: "project-views", children: board.views.map((v) => _jsxs("span", { className: "project-view label", "data-testid": "project-view", children: [v.name, " (", v.layout, ")"] }, v.number)) }), _jsx("div", { className: "board-columns", "data-testid": "board-columns", children: columns.map((col) => (_jsxs("div", { className: "board-column", "data-testid": "board-column", children: [_jsxs("div", { className: "board-column-head", children: [_jsx("strong", { children: col.name }), " ", _jsx("span", { className: "count", "data-testid": "board-column-count", children: col.cards.length })] }), col.cards.map((card) => (_jsxs("div", { className: "board-card", "data-testid": "board-card", children: [_jsx("span", { className: "board-card-title", children: card.title }), _jsx("span", { className: "board-card-type label", children: card.contentType })] }, card.id)))] }, col.name))) })] })] }));
|
|
390
|
+
}
|
|
391
|
+
// The Insights / Pulse / Contributors screen. Pulse summarizes activity (merged/open PRs,
|
|
392
|
+
// opened/closed issues); Contributors is a commit leaderboard. Data-coupled: every count is
|
|
393
|
+
// derived from the modeled PRs/issues/commits the twin tracks (new objects move the numbers).
|
|
394
|
+
export function InsightsView({ insights }) {
|
|
395
|
+
if (!insights.length)
|
|
396
|
+
return _jsx("div", { className: "empty", "data-testid": "insights-view", children: "No insights." });
|
|
397
|
+
return (_jsx("section", { className: "insights-view", "data-testid": "insights-view", children: insights.map((ins) => (_jsxs("div", { className: "insights-repo", "data-testid": "insights-repo", children: [_jsx("h3", { children: ins.repo }), _jsxs("div", { className: "pulse", "data-testid": "pulse", children: [_jsxs("span", { className: "pulse-stat", "data-testid": "pulse-merged-prs", children: [ins.pulse.mergedPrs, " Merged pull requests"] }), _jsxs("span", { className: "pulse-stat", "data-testid": "pulse-open-prs", children: [ins.pulse.openPrs, " Open pull requests"] }), _jsxs("span", { className: "pulse-stat", "data-testid": "pulse-closed-issues", children: [ins.pulse.closedIssues, " Closed issues"] }), _jsxs("span", { className: "pulse-stat", "data-testid": "pulse-open-issues", children: [ins.pulse.openedIssues, " New issues"] })] }), _jsxs("div", { className: "contributors", "data-testid": "contributors", children: [_jsx("h4", { children: "Contributors" }), ins.contributors.length ? ins.contributors.map((c) => (_jsxs("div", { className: "contributor-row", "data-testid": "contributor-row", children: [_jsx("span", { className: "contributor-login", children: _jsx("strong", { children: c.login }) }), _jsxs("span", { className: "contributor-commits label", "data-testid": "contributor-commits", children: [c.commits, " commits"] })] }, c.login))) : _jsx("div", { className: "empty", children: "No commit data." })] })] }, ins.repo))) }));
|
|
398
|
+
}
|
|
399
|
+
// The repo Settings screens (General / Collaborators / Webhooks / Branches). Data-coupled:
|
|
400
|
+
// every row + flag is read from the modeled repo config + collaborators/webhooks/branches
|
|
401
|
+
// (adding a collaborator or webhook moves the rows/counts this screen renders).
|
|
402
|
+
export function SettingsView({ settings }) {
|
|
403
|
+
if (!settings.length)
|
|
404
|
+
return _jsx("div", { className: "empty", "data-testid": "settings-view", children: "No repositories." });
|
|
405
|
+
return (_jsx("section", { className: "settings-view", "data-testid": "settings-view", children: settings.map((s) => (_jsxs("div", { className: "settings-repo", "data-testid": "settings-repo", children: [_jsxs("h3", { children: [s.repo, " settings"] }), _jsxs("div", { className: "settings-general", "data-testid": "settings-general", children: [_jsxs("span", { className: "label", "data-testid": "settings-default-branch", children: ["default branch: ", s.general.defaultBranch ?? "main"] }), _jsx("span", { className: "label", children: s.general.private ? "private" : "public" }), s.general.hasIssues ? _jsx("span", { className: "label", children: "Issues" }) : null, s.general.hasProjects ? _jsx("span", { className: "label", children: "Projects" }) : null, s.general.hasWiki ? _jsx("span", { className: "label", children: "Wiki" }) : null, s.general.hasDiscussions ? _jsx("span", { className: "label", children: "Discussions" }) : null] }), _jsxs("div", { className: "settings-section", "data-testid": "settings-collaborators", children: [_jsxs("h4", { children: ["Manage access ", _jsx("span", { className: "count", "data-testid": "collaborators-count", children: s.collaborators.length })] }), s.collaborators.map((c) => (_jsxs("div", { className: "collaborator-row", "data-testid": "collaborator-row", children: [_jsx("strong", { children: c.login }), " ", _jsx("span", { className: "label", children: c.permission })] }, c.login)))] }), _jsxs("div", { className: "settings-section", "data-testid": "settings-webhooks", children: [_jsxs("h4", { children: ["Webhooks ", _jsx("span", { className: "count", "data-testid": "webhooks-count", children: s.webhooks.length })] }), s.webhooks.map((h) => (_jsxs("div", { className: "webhook-row", "data-testid": "webhook-row", children: [_jsx("span", { className: "webhook-url", children: h.url ?? "(no url)" }), _jsx("span", { className: "label", children: h.active ? "active" : "inactive" }), _jsx("span", { className: "label", children: h.events.join(", ") })] }, h.id)))] }), _jsxs("div", { className: "settings-section", "data-testid": "settings-branches", children: [_jsxs("h4", { children: ["Branches ", _jsx("span", { className: "count", "data-testid": "branches-count", children: s.branches.length })] }), s.branches.map((b) => (_jsxs("div", { className: "branch-row", "data-testid": "branch-row", children: [_jsx("strong", { children: b.name }), " ", b.protected ? _jsx("span", { className: "label", "data-testid": "branch-protected", children: "protected" }) : null] }, b.name)))] })] }, s.repo))) }));
|
|
406
|
+
}
|
|
407
|
+
// The Actions view: a workflow-runs list (status/conclusion badge per run, run number,
|
|
408
|
+
// workflow name, branch, sha) and, on selecting a run, a detail pane with its jobs +
|
|
409
|
+
// each job's steps. Models the Actions API objects — the only Actions non-goal is running
|
|
410
|
+
// real CI, so runs/jobs render with the status/conclusion the twin tracks (no live logs).
|
|
411
|
+
export function ActionsView({ runs, jobs, selectedRunId, onSelect }) {
|
|
412
|
+
const selected = runs.find((r) => r.id === selectedRunId) ?? runs[0];
|
|
413
|
+
const selectedJobs = selected ? jobs.filter((j) => j.repo === selected.repo && Number(j.runId) === Number(selected.id)) : [];
|
|
414
|
+
return (_jsxs("div", { className: "actions-view split", children: [_jsxs("section", { className: "list-box actions-box", children: [_jsxs("div", { className: "list-head", children: [_jsx("span", { children: _jsxs("strong", { children: [runs.length, " workflow runs"] }) }), _jsx("span", { children: "Status" })] }), runs.length ? runs.map((run) => {
|
|
415
|
+
const effective = run.status === "completed" ? (run.conclusion || "completed") : (run.status || "queued");
|
|
416
|
+
return (_jsxs("button", { className: `run-row ${run.id === (selected?.id) ? "active" : ""}`, onClick: () => onSelect?.(run.id), children: [_jsx(RunStatusBadge, { status: run.status, conclusion: run.conclusion }), _jsxs("span", { children: [_jsx("span", { className: "run-title", children: run.workflowName || run.name || `Run #${run.runNumber}` }), _jsxs("div", { className: "run-meta", children: [run.repo, " \u00B7 run #", run.runNumber, " \u00B7 ", run.event || "workflow_dispatch", run.headBranch ? ` · ${run.headBranch}` : "", run.headSha ? ` · ${shortSha(run.headSha)}` : "", ` · ${effective}`, run.jobCount != null ? ` · ${run.jobCount} jobs` : ""] })] })] }, run.id));
|
|
417
|
+
}) : _jsx("div", { className: "empty", children: "No workflow runs in this view." })] }), _jsx("section", { className: "conversation run-detail", children: selected ? (_jsxs(_Fragment, { children: [_jsxs("h2", { children: [selected.workflowName || selected.name || `Run #${selected.runNumber}`, " ", _jsxs("span", { children: ["#", selected.runNumber] })] }), _jsxs("div", { className: "state-line", children: [_jsx(RunStatusBadge, { status: selected.status, conclusion: selected.conclusion }), _jsx("strong", { children: selected.repo }), _jsxs("span", { className: "sha", children: [selected.event || "workflow_dispatch", selected.headBranch ? ` · ${selected.headBranch}` : "", selected.headSha ? ` · ${shortSha(selected.headSha)}` : ""] }), selected.url ? _jsx("a", { className: "branch", href: selected.url, target: "_blank", rel: "noreferrer", children: "View on GitHub" }) : null] }), _jsxs("section", { className: "snapshot-section", children: [_jsx("h3", { children: "Jobs" }), _jsx("div", { className: "job-list", children: selectedJobs.length ? selectedJobs.map((job) => (_jsxs("div", { className: "job-row", children: [_jsxs("div", { className: "job-head", children: [_jsx(RunStatusBadge, { status: job.status, conclusion: job.conclusion }), _jsx("strong", { children: job.name })] }), (job.steps ?? []).length ? (_jsx("ol", { className: "step-list", children: (job.steps ?? []).map((step) => (_jsxs("li", { className: "step-row", children: [_jsx(RunStatusBadge, { status: step.status, conclusion: step.conclusion }), " ", step.name] }, step.number))) })) : null] }, job.id))) : _jsx("p", { className: "sha", children: "No jobs in this run." }) })] })] })) : _jsx("div", { className: "empty", children: "Select a workflow run." }) })] }));
|
|
418
|
+
}
|
|
419
|
+
function fmtBytes(size) {
|
|
420
|
+
if (!size)
|
|
421
|
+
return "0 B";
|
|
422
|
+
if (size < 1024)
|
|
423
|
+
return `${size} B`;
|
|
424
|
+
if (size < 1024 * 1024)
|
|
425
|
+
return `${(size / 1024).toFixed(1)} KB`;
|
|
426
|
+
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
|
427
|
+
}
|
|
428
|
+
// A release-channel badge: a draft release, a prerelease, or the published "Latest"/release.
|
|
429
|
+
export function ReleaseBadge({ draft, prerelease }) {
|
|
430
|
+
const kind = draft ? "draft" : prerelease ? "prerelease" : "release";
|
|
431
|
+
const label = draft ? "Draft" : prerelease ? "Pre-release" : "Release";
|
|
432
|
+
return _jsx("span", { className: `release-badge ${kind}`, title: `release channel: ${kind}`, children: label });
|
|
433
|
+
}
|
|
434
|
+
// The Releases view: a list of releases (tag + name + draft/prerelease badge) and, on
|
|
435
|
+
// selecting one, a detail pane with the release body + its ASSET catalog (name, type,
|
|
436
|
+
// size, downloads). Models the Releases API objects — the only Non-goal is the asset
|
|
437
|
+
// binary BYTES (declared), so assets render as metadata rows, never downloadable content.
|
|
438
|
+
export function ReleasesView({ releases, selectedReleaseId, onSelect }) {
|
|
439
|
+
const selected = releases.find((r) => r.id === selectedReleaseId) ?? releases[0];
|
|
440
|
+
const assets = selected?.assets ?? [];
|
|
441
|
+
return (_jsxs("div", { className: "releases-view split", children: [_jsxs("section", { className: "list-box releases-box", children: [_jsxs("div", { className: "list-head", children: [_jsx("span", { children: _jsxs("strong", { children: [releases.length, " releases"] }) }), _jsx("span", { children: "Published" })] }), releases.length ? releases.map((rel) => (_jsxs("button", { className: `release-row ${rel.id === (selected?.id) ? "active" : ""}`, onClick: () => onSelect?.(rel.id), children: [_jsxs("span", { className: "release-tag", children: ["\uD83C\uDFF7 ", rel.tagName] }), _jsxs("span", { children: [_jsx("span", { className: "release-title", children: rel.name || rel.tagName }), _jsxs("div", { className: "release-meta", children: [rel.repo, rel.publishedAt ? ` · published ${new Date(rel.publishedAt).toLocaleString()}` : rel.createdAt ? ` · created ${new Date(rel.createdAt).toLocaleString()}` : "", (rel.assets ?? []).length ? ` · ${(rel.assets ?? []).length} assets` : ""] })] }), _jsx("span", { className: "labels", children: _jsx(ReleaseBadge, { draft: rel.draft, prerelease: rel.prerelease }) })] }, rel.id))) : _jsx("div", { className: "empty", children: "No releases in this view." })] }), _jsx("section", { className: "conversation release-detail", children: selected ? (_jsxs(_Fragment, { children: [_jsxs("h2", { children: [selected.name || selected.tagName, " ", _jsx("span", { children: selected.tagName })] }), _jsxs("div", { className: "state-line", children: [_jsx(ReleaseBadge, { draft: selected.draft, prerelease: selected.prerelease }), _jsx("strong", { children: selected.repo }), _jsxs("span", { className: "sha", children: ["target: ", selected.targetCommitish || "main", selected.publishedAt ? ` · published ${new Date(selected.publishedAt).toLocaleString()}` : selected.draft ? " · unpublished draft" : ""] }), selected.url ? _jsx("a", { className: "branch", href: selected.url, target: "_blank", rel: "noreferrer", children: "View on GitHub" }) : null] }), _jsx("div", { className: "timeline-item", children: _jsxs("article", { className: "comment", children: [_jsx("div", { className: "comment-head", children: _jsx("strong", { children: "Release notes" }) }), _jsx("div", { className: "comment-body", children: selected.body || "No release notes." })] }) }), _jsxs("section", { className: "snapshot-section", children: [_jsx("h3", { children: "Assets" }), _jsx("div", { className: "asset-list", children: assets.length ? assets.map((asset) => (_jsxs("div", { className: "asset-row", children: [_jsx("strong", { className: "asset-name", children: asset.name }), _jsx("span", { className: "asset-type", children: asset.contentType || "application/octet-stream" }), _jsx("span", { className: "asset-size", children: fmtBytes(asset.size) }), _jsxs("span", { className: "asset-downloads", children: [asset.downloadCount ?? 0, " downloads"] })] }, asset.id))) : _jsx("p", { className: "sha", children: "No assets on this release (asset bytes are a declared Non-goal \u2014 metadata only)." }) })] })] })) : _jsx("div", { className: "empty", children: "Select a release." }) })] }));
|
|
442
|
+
}
|
|
443
|
+
// A discussion category chip (emoji + name). Q&A categories are answerable.
|
|
444
|
+
export function DiscussionCategoryBadge({ name, emoji, answerable }) {
|
|
445
|
+
if (!name)
|
|
446
|
+
return null;
|
|
447
|
+
return _jsxs("span", { className: `discussion-category ${answerable ? "answerable" : ""}`, title: answerable ? "answerable (Q&A) category" : "category", children: [emoji ? `${emoji} ` : "", name] });
|
|
448
|
+
}
|
|
449
|
+
// An "answered"/"unanswered" status badge for a Q&A discussion (only shown for answerable
|
|
450
|
+
// categories). Mirrors the green check GitHub shows once a discussion has an accepted answer.
|
|
451
|
+
export function DiscussionAnswerBadge({ answerable, answered }) {
|
|
452
|
+
if (!answerable)
|
|
453
|
+
return null;
|
|
454
|
+
return _jsx("span", { className: `answer-badge ${answered ? "answered" : "unanswered"}`, title: answered ? "marked answered" : "awaiting an answer", children: answered ? "✓ Answered" : "Unanswered" });
|
|
455
|
+
}
|
|
456
|
+
// The Discussions view: a list of discussions (category + title + answered badge) and, on
|
|
457
|
+
// selecting one, a detail pane with the opening post + the comment/reply thread, marking
|
|
458
|
+
// the accepted answer. Models the Discussions objects (modeled over REST in the twin — a
|
|
459
|
+
// declared deviation from GitHub's GraphQL Discussions API).
|
|
460
|
+
export function DiscussionsView({ discussions, selectedKey, onSelect }) {
|
|
461
|
+
const keyOf = (d) => `${d.repo}#${d.number}`;
|
|
462
|
+
const selected = discussions.find((d) => keyOf(d) === selectedKey) ?? discussions[0];
|
|
463
|
+
const comments = selected?.comments ?? [];
|
|
464
|
+
const answer = comments.find((c) => c.isAnswer);
|
|
465
|
+
return (_jsxs("div", { className: "discussions-view split", children: [_jsxs("section", { className: "list-box discussions-box", children: [_jsxs("div", { className: "list-head", children: [_jsx("span", { children: _jsxs("strong", { children: [discussions.length, " discussions"] }) }), _jsx("span", { children: "Updated" })] }), discussions.length ? discussions.map((d) => (_jsxs("button", { className: `discussion-row ${keyOf(d) === (selected ? keyOf(selected) : "") ? "active" : ""}`, onClick: () => onSelect?.(keyOf(d)), children: [_jsx("span", { className: `status-icon ${d.state === "closed" ? "closed" : "open"}`, children: "\uD83D\uDCAC" }), _jsxs("span", { children: [_jsx("span", { className: "discussion-title", children: d.title || `${d.repo}#${d.number}` }), _jsxs("div", { className: "discussion-meta", children: [d.repo, " #", d.number, d.categoryName ? ` · ${d.categoryName}` : "", d.commentsCount != null ? ` · ${d.commentsCount} comments` : "", d.updatedAt ? ` · updated ${new Date(d.updatedAt).toLocaleString()}` : d.createdAt ? ` · ${new Date(d.createdAt).toLocaleString()}` : ""] })] }), _jsxs("span", { className: "labels", children: [_jsx(DiscussionCategoryBadge, { name: d.categoryName, emoji: d.categoryEmoji, answerable: d.isAnswerable }), _jsx(DiscussionAnswerBadge, { answerable: d.isAnswerable, answered: d.isAnswered }), d.locked ? _jsx("span", { className: "label locked", title: "locked", children: "\uD83D\uDD12" }) : null] })] }, keyOf(d)))) : _jsx("div", { className: "empty", children: "No discussions in this view." })] }), _jsx("section", { className: "conversation discussion-detail", children: selected ? (_jsxs(_Fragment, { children: [_jsxs("h2", { children: [selected.title || `${selected.repo}#${selected.number}`, " ", _jsxs("span", { children: ["#", selected.number] })] }), _jsxs("div", { className: "state-line", children: [_jsx(DiscussionCategoryBadge, { name: selected.categoryName, emoji: selected.categoryEmoji, answerable: selected.isAnswerable }), _jsx(DiscussionAnswerBadge, { answerable: selected.isAnswerable, answered: selected.isAnswered }), _jsx("strong", { children: selected.repo }), _jsxs("span", { className: "sha", children: [selected.state, selected.locked ? " · locked" : ""] }), selected.url ? _jsx("a", { className: "branch", href: selected.url, target: "_blank", rel: "noreferrer", children: "View on GitHub" }) : null] }), _jsx("div", { className: "timeline-item", children: _jsxs("article", { className: "comment", children: [_jsx("div", { className: "comment-head", children: _jsx("strong", { children: "Opening post" }) }), _jsx("div", { className: "comment-body", children: selected.body || "No description." })] }) }), answer ? (_jsx("div", { className: "timeline-item answer-highlight", children: _jsxs("article", { className: "comment accepted-answer", children: [_jsxs("div", { className: "comment-head", children: [_jsx("strong", { children: "\u2713 Marked as answer" }), " ", _jsxs("span", { className: "sha", children: ["comment #", answer.id] })] }), _jsx("div", { className: "comment-body", children: answer.body || "" })] }) })) : null, _jsxs("section", { className: "snapshot-section", children: [_jsx("h3", { children: "Comments" }), _jsx("div", { className: "comment-thread", children: comments.length ? comments.map((c) => (_jsxs("div", { className: `thread-comment ${c.parentId != null ? "reply" : ""} ${c.isAnswer ? "is-answer" : ""}`, children: [_jsxs("div", { className: "comment-head", children: [_jsxs("strong", { children: ["comment #", c.id] }), c.parentId != null ? _jsxs("span", { className: "sha", children: [" \u00B7 reply to #", c.parentId] }) : null, c.isAnswer ? _jsx("span", { className: "answer-badge answered", children: " \u2713 answer" }) : null, c.createdAt ? _jsxs("span", { className: "sha", children: [" \u00B7 ", new Date(c.createdAt).toLocaleString()] }) : null] }), _jsx("div", { className: "comment-body", children: c.body || "" })] }, c.id))) : _jsx("p", { className: "sha", children: "No comments yet." }) })] })] })) : _jsx("div", { className: "empty", children: "Select a discussion." }) })] }));
|
|
466
|
+
}
|
|
467
|
+
export function Conversation({ pr, reviews, commits, comments, reviewComments, files }) {
|
|
468
|
+
if (!pr)
|
|
469
|
+
return _jsx("div", { className: "empty", children: "Select a pull request." });
|
|
470
|
+
const state = pr.merged ? "merged" : (pr.state ?? "open");
|
|
471
|
+
const labels = pr.labels ?? [];
|
|
472
|
+
const assignees = pr.assignees ?? [];
|
|
473
|
+
return (_jsxs("section", { className: "conversation", children: [_jsxs("h2", { children: [pr.title || pr.url || `${pr.repo}#${pr.number}`, " ", _jsxs("span", { children: ["#", pr.number] })] }), _jsxs("div", { className: "state-line", children: [_jsx("span", { className: `state-badge ${state}`, children: state }), _jsx(CiStatus, { status: pr.ciStatus }), _jsx("strong", { children: pr.repo }), _jsxs("span", { className: "sha", children: [pr.changedFiles ?? 0, " files changed \u00B7 ", pr.commitsCount ?? 0, " commits \u00B7 ", pr.reviewCount ?? 0, " reviews \u00B7 ", pr.commentsCount ?? 0, " comments \u00B7 ", pr.reviewCommentsCount ?? 0, " review comments"] }), pr.milestone ? _jsxs("span", { className: "label milestone", title: "Milestone", children: ["\uD83C\uDFF7 ", pr.milestone] }) : null, pr.mergedAt ? _jsxs("span", { className: "sha", children: ["merged ", new Date(pr.mergedAt).toLocaleString()] }) : null, pr.url ? _jsx("a", { className: "branch", href: pr.url, target: "_blank", rel: "noreferrer", children: "View on GitHub" }) : null] }), labels.length || assignees.length ? (_jsxs("div", { className: "meta-row", children: [labels.map((label) => label.name ? _jsx("span", { className: "label", children: label.name }, `l-${label.name}`) : null), assignees.map((login) => _jsxs("span", { className: "label assignee", children: ["@", login] }, `a-${login}`))] })) : null, _jsxs("div", { className: "timeline-item", children: [_jsx(Avatar, { src: pr.authorAvatarUrl, label: pr.author || "PR" }), _jsxs("article", { className: "comment", children: [_jsxs("div", { className: "comment-head", children: [_jsx("strong", { children: pr.author || "GitHub" }), " opened this pull request"] }), _jsxs("div", { className: "comment-body", children: [pr.body || "No real PR body was included in the synced seed.", pr.sourceIssue ? _jsxs("div", { className: "sha", children: ["source issue: ", pr.sourceIssue, pr.sourceIssueTitle ? ` ${pr.sourceIssueTitle}` : ""] }) : null, pr.mergeCommit ? _jsxs("div", { className: "sha", children: ["merge commit: ", pr.mergeCommit] }) : null] })] })] }), commits.length ? (_jsxs("section", { className: "snapshot-section", children: [_jsx("h3", { children: "Commits" }), commits.map((commit) => (_jsxs("div", { className: "timeline-item", children: [_jsx(Avatar, { src: commit.authorAvatarUrl, label: commit.authorLogin || commit.author }), _jsxs("article", { className: "comment", children: [_jsxs("div", { className: "comment-head", children: [_jsx("strong", { children: commit.authorLogin || commit.author || "Git author" }), " committed ", commit.date ? new Date(commit.date).toLocaleString() : ""] }), _jsxs("div", { className: "comment-body", children: [commit.message || commit.sha, _jsx("div", { className: "sha", children: shortSha(commit.sha) })] })] })] }, commit.sha)))] })) : null, comments.map((comment, index) => (_jsxs("div", { className: "timeline-item", children: [_jsx(Avatar, { src: comment.authorAvatarUrl, label: comment.author }), _jsxs("article", { className: "comment", children: [_jsxs("div", { className: "comment-head", children: [_jsx("strong", { children: comment.author || "GitHub user" }), " commented ", comment.createdAt ? new Date(comment.createdAt).toLocaleString() : ""] }), _jsx("div", { className: "comment-body", children: comment.body || "" })] })] }, `comment-${comment.url || index}`))), reviews.map((review, index) => (_jsxs("div", { className: "timeline-item", children: [_jsx(Avatar, { src: review.reviewerAvatarUrl, label: review.reviewer }), _jsxs("article", { className: "review-card", children: [_jsxs("div", { className: "review-head", children: [review.reviewer || "GitHub user", " ", review.state || "reviewed", " ", review.submittedAt ? new Date(review.submittedAt).toLocaleString() : ""] }), _jsx("div", { className: "review-body", children: review.body || "No review body." })] })] }, `${review.reviewer}-${index}`))), reviewComments.map((comment, index) => (_jsxs("div", { className: "timeline-item", children: [_jsx(Avatar, { src: comment.authorAvatarUrl, label: comment.author }), _jsxs("article", { className: "comment", children: [_jsxs("div", { className: "comment-head", children: [_jsx("strong", { children: comment.author || "GitHub user" }), " commented on ", comment.path || "a file"] }), _jsx("div", { className: "comment-body", children: comment.body || "" })] })] }, `review-comment-${comment.url || index}`))), files.length ? (_jsxs("section", { className: "snapshot-section", children: [_jsx("h3", { children: "Files changed" }), _jsx("div", { className: "file-list", children: files.map((file) => (_jsxs("div", { className: "file-row", children: [_jsx("span", { children: file.status || "modified" }), _jsx("strong", { children: file.filename }), _jsxs("span", { children: ["+", file.additions ?? 0, " -", file.deletions ?? 0] })] }, file.filename))) })] })) : null] }));
|
|
474
|
+
}
|
|
475
|
+
export function Aside({ pr, reviews, commits, files }) {
|
|
476
|
+
if (!pr)
|
|
477
|
+
return _jsx("aside", { className: "sidebar" });
|
|
478
|
+
return (_jsxs("aside", { className: "sidebar", children: [_jsxs("section", { className: "side-section", children: [_jsx("h3", { children: "Reviewers" }), reviews.length ? reviews.map((review, index) => _jsxs("div", { className: `label ${review.observed ? "" : "approved"}`, children: [review.state, " ", review.reviewer] }, `${review.reviewer}-${index}`)) : _jsx("p", { className: "sha", children: "No reviews in snapshot." }), (pr.requestedReviewers ?? []).length ? (_jsx("div", { className: "requested-reviewers", children: (pr.requestedReviewers ?? []).map((login) => _jsxs("span", { className: "label requested", title: "Requested reviewer", children: ["@", login, " (requested)"] }, `req-${login}`)) })) : null] }), _jsxs("section", { className: "side-section", children: [_jsx("h3", { children: "Checks" }), pr.ciStatus ? _jsxs("div", { className: `ci-line ${pr.ciStatus}`, children: [_jsx(CiStatus, { status: pr.ciStatus }), " ", pr.ciStatus] }) : _jsx("p", { className: "sha", children: "No CI checks in snapshot." })] }), _jsxs("section", { className: "side-section", children: [_jsx("h3", { children: "Milestone" }), pr.milestone ? _jsxs("span", { className: "label milestone", children: ["\uD83C\uDFF7 ", pr.milestone] }) : _jsx("p", { className: "sha", children: "No milestone." })] }), _jsxs("section", { className: "side-section", children: [_jsx("h3", { children: "Labels" }), (pr.labels ?? []).length ? (pr.labels ?? []).map((label) => label.name ? _jsx("span", { className: "label", children: label.name }, label.name) : null) : _jsx("p", { className: "sha", children: "None." })] }), _jsxs("section", { className: "side-section", children: [_jsx("h3", { children: "Assignees" }), (pr.assignees ?? []).length ? (pr.assignees ?? []).map((login) => _jsxs("div", { className: "label assignee", children: ["@", login] }, login)) : _jsx("p", { className: "sha", children: "No one assigned." })] }), _jsxs("section", { className: "side-section", children: [_jsx("h3", { children: "Development" }), pr.baseBranch ? _jsxs("div", { className: "branch", children: ["base: ", pr.baseBranch] }) : null, pr.headBranch ? _jsxs("div", { className: "branch", children: ["head: ", pr.headBranch] }) : null, pr.headSha ? _jsx("p", { className: "sha", children: shortSha(pr.headSha) }) : _jsx("p", { className: "sha", children: "No branch or commit list in synced seed." })] }), _jsxs("section", { className: "side-section", children: [_jsx("h3", { children: "Repository" }), _jsx("p", { children: pr.repo })] }), _jsxs("section", { className: "side-section", children: [_jsx("h3", { children: "Snapshot Counts" }), _jsxs("p", { className: "sha", children: [commits.length, " commits"] }), _jsxs("p", { className: "sha", children: [files.length, " files"] }), _jsxs("p", { className: "sha", children: [pr.additions ?? 0, " additions / ", pr.deletions ?? 0, " deletions"] })] })] }));
|
|
479
|
+
}
|
|
480
|
+
// Guard the top-level mount: the browser bundle has a DOM and mounts exactly as before,
|
|
481
|
+
// but importing this module in a non-DOM test (for renderToStaticMarkup of the exported
|
|
482
|
+
// presentational components) must not touch `document`.
|
|
483
|
+
if (typeof document !== "undefined") {
|
|
484
|
+
createRoot(document.getElementById("root")).render(_jsx(App, {}));
|
|
485
|
+
}
|