create-interview-cockpit 0.28.0 → 0.29.0
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/package.json +1 -1
- package/template/client/src/codeowners.ts +120 -0
- package/template/client/src/components/ChatView.tsx +18 -7
- package/template/client/src/components/GithubActionsLabModal.tsx +80 -39
- package/template/client/src/components/LabsPanel.tsx +7 -0
- package/template/client/src/components/PullRequestPanel.tsx +125 -0
- package/template/client/src/components/SettingsPanel.tsx +4 -1
- package/template/client/src/githubActionsLab.ts +1693 -0
- package/template/client/src/index.css +71 -0
- package/template/client/src/types.ts +6 -0
- package/template/cockpit.json +1 -1
package/package.json
CHANGED
|
@@ -790,3 +790,123 @@ export function findCodeOwnersPath(
|
|
|
790
790
|
export function isCodeOwnersPath(path: string): boolean {
|
|
791
791
|
return (CODEOWNERS_LOCATIONS as readonly string[]).includes(path);
|
|
792
792
|
}
|
|
793
|
+
|
|
794
|
+
// ─── Pull request templates ───────────────────────────────────────────
|
|
795
|
+
//
|
|
796
|
+
// GitHub doesn't have a settings UI for PR templates — they're picked
|
|
797
|
+
// up purely from files in the repo. The lookup rules we mirror:
|
|
798
|
+
//
|
|
799
|
+
// 1. Single template: the first match wins, in priority order:
|
|
800
|
+
// .github/pull_request_template.md
|
|
801
|
+
// .github/PULL_REQUEST_TEMPLATE.md
|
|
802
|
+
// pull_request_template.md (repo root)
|
|
803
|
+
// PULL_REQUEST_TEMPLATE.md (repo root)
|
|
804
|
+
// docs/pull_request_template.md
|
|
805
|
+
// docs/PULL_REQUEST_TEMPLATE.md
|
|
806
|
+
// Lookup is also case-insensitive on the filename.
|
|
807
|
+
//
|
|
808
|
+
// 2. Multiple templates: any *.md (or *.txt) file inside
|
|
809
|
+
// .github/PULL_REQUEST_TEMPLATE/
|
|
810
|
+
// PULL_REQUEST_TEMPLATE/
|
|
811
|
+
// docs/PULL_REQUEST_TEMPLATE/
|
|
812
|
+
// Each becomes a pick-able template; on github.com you'd append
|
|
813
|
+
// ?template=foo.md to the compare URL. We just show a picker.
|
|
814
|
+
//
|
|
815
|
+
// If both kinds are present, github.com shows the directory picker but
|
|
816
|
+
// also still lists the single template; we follow the same behaviour.
|
|
817
|
+
|
|
818
|
+
export interface PullRequestTemplate {
|
|
819
|
+
/** Workspace path of the file. */
|
|
820
|
+
path: string;
|
|
821
|
+
/** Display name (file basename minus extension, prettified). */
|
|
822
|
+
name: string;
|
|
823
|
+
/** Raw markdown body of the template. */
|
|
824
|
+
body: string;
|
|
825
|
+
/** True if this is the repo's default single template. */
|
|
826
|
+
isDefault: boolean;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const SINGLE_PR_TEMPLATE_DIRS: readonly string[] = [".github/", "", "docs/"];
|
|
830
|
+
const MULTI_PR_TEMPLATE_DIRS: readonly string[] = [
|
|
831
|
+
".github/PULL_REQUEST_TEMPLATE/",
|
|
832
|
+
"PULL_REQUEST_TEMPLATE/",
|
|
833
|
+
"docs/PULL_REQUEST_TEMPLATE/",
|
|
834
|
+
];
|
|
835
|
+
|
|
836
|
+
function isPullRequestTemplateFilename(filename: string): boolean {
|
|
837
|
+
// GitHub matches pull_request_template.md case-insensitively, with .md
|
|
838
|
+
// or .markdown extensions. We accept both.
|
|
839
|
+
return /^pull_request_template\.(md|markdown)$/i.test(filename);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function prettifyTemplateName(filename: string): string {
|
|
843
|
+
const base = filename.replace(/\.(md|markdown|txt)$/i, "");
|
|
844
|
+
return base.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Discover all PR templates in the workspace, in github.com priority
|
|
849
|
+
* order. Returns an empty array if no templates exist.
|
|
850
|
+
*/
|
|
851
|
+
export function findPullRequestTemplates(
|
|
852
|
+
files: Record<string, string>,
|
|
853
|
+
): PullRequestTemplate[] {
|
|
854
|
+
const out: PullRequestTemplate[] = [];
|
|
855
|
+
const seen = new Set<string>();
|
|
856
|
+
|
|
857
|
+
// 1. Single-template lookup
|
|
858
|
+
for (const dir of SINGLE_PR_TEMPLATE_DIRS) {
|
|
859
|
+
for (const path of Object.keys(files)) {
|
|
860
|
+
if (seen.has(path)) continue;
|
|
861
|
+
if (!path.startsWith(dir)) continue;
|
|
862
|
+
const rest = path.slice(dir.length);
|
|
863
|
+
if (rest.includes("/")) continue; // not directly in this dir
|
|
864
|
+
if (!isPullRequestTemplateFilename(rest)) continue;
|
|
865
|
+
out.push({
|
|
866
|
+
path,
|
|
867
|
+
name: "Default",
|
|
868
|
+
body: files[path] ?? "",
|
|
869
|
+
isDefault: true,
|
|
870
|
+
});
|
|
871
|
+
seen.add(path);
|
|
872
|
+
break; // first match per dir; priority order handles the rest
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// 2. Multi-template directory lookup
|
|
877
|
+
for (const dir of MULTI_PR_TEMPLATE_DIRS) {
|
|
878
|
+
for (const path of Object.keys(files)) {
|
|
879
|
+
if (seen.has(path)) continue;
|
|
880
|
+
if (!path.startsWith(dir)) continue;
|
|
881
|
+
const rest = path.slice(dir.length);
|
|
882
|
+
if (!rest || rest.includes("/")) continue;
|
|
883
|
+
if (!/\.(md|markdown|txt)$/i.test(rest)) continue;
|
|
884
|
+
out.push({
|
|
885
|
+
path,
|
|
886
|
+
name: prettifyTemplateName(rest),
|
|
887
|
+
body: files[path] ?? "",
|
|
888
|
+
isDefault: false,
|
|
889
|
+
});
|
|
890
|
+
seen.add(path);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
return out;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/** True if the path is a recognised PR-template location. */
|
|
898
|
+
export function isPullRequestTemplatePath(path: string): boolean {
|
|
899
|
+
for (const dir of SINGLE_PR_TEMPLATE_DIRS) {
|
|
900
|
+
if (!path.startsWith(dir)) continue;
|
|
901
|
+
const rest = path.slice(dir.length);
|
|
902
|
+
if (!rest.includes("/") && isPullRequestTemplateFilename(rest)) return true;
|
|
903
|
+
}
|
|
904
|
+
for (const dir of MULTI_PR_TEMPLATE_DIRS) {
|
|
905
|
+
if (!path.startsWith(dir)) continue;
|
|
906
|
+
const rest = path.slice(dir.length);
|
|
907
|
+
if (rest && !rest.includes("/") && /\.(md|markdown|txt)$/i.test(rest)) {
|
|
908
|
+
return true;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
return false;
|
|
912
|
+
}
|
|
@@ -482,13 +482,24 @@ export default function ChatView({ question }: Props) {
|
|
|
482
482
|
// and triggering an update loop.
|
|
483
483
|
const initialMessages = useMemo(
|
|
484
484
|
() =>
|
|
485
|
-
(question.messages ?? []).map((m) =>
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
485
|
+
(question.messages ?? []).map((m) => {
|
|
486
|
+
// Older saved messages persist as `{ content, parts: [] }` — the empty
|
|
487
|
+
// array is truthy so a plain `m.parts ?? fallback` keeps `parts`
|
|
488
|
+
// empty, which makes `getTextContent` return "" and breaks any feature
|
|
489
|
+
// that reads message text (annotations, copy-to-clipboard, etc.).
|
|
490
|
+
// Treat empty `parts` the same as missing and rebuild from `content`.
|
|
491
|
+
const hasParts = Array.isArray(m.parts) && m.parts.length > 0;
|
|
492
|
+
const parts = hasParts
|
|
493
|
+
? (m.parts as UIMessage["parts"])
|
|
494
|
+
: ([
|
|
495
|
+
{ type: "text" as const, text: m.content ?? "" },
|
|
496
|
+
] as UIMessage["parts"]);
|
|
497
|
+
return {
|
|
498
|
+
id: m.id,
|
|
499
|
+
role: m.role as "user" | "assistant",
|
|
500
|
+
parts,
|
|
501
|
+
};
|
|
502
|
+
}),
|
|
492
503
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
493
504
|
[question.id],
|
|
494
505
|
);
|
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
ChevronDown,
|
|
4
4
|
ChevronRight,
|
|
5
5
|
Copy,
|
|
6
|
+
Eye,
|
|
7
|
+
EyeOff,
|
|
6
8
|
FilePlus,
|
|
7
9
|
Folder,
|
|
8
10
|
GitPullRequest,
|
|
@@ -24,6 +26,8 @@ import {
|
|
|
24
26
|
X,
|
|
25
27
|
} from "lucide-react";
|
|
26
28
|
import MonacoEditorLib from "@monaco-editor/react";
|
|
29
|
+
import ReactMarkdown from "react-markdown";
|
|
30
|
+
import remarkGfm from "remark-gfm";
|
|
27
31
|
import type {
|
|
28
32
|
BeforeMount,
|
|
29
33
|
Monaco,
|
|
@@ -346,6 +350,11 @@ export default function GithubActionsLabModal() {
|
|
|
346
350
|
const [historyNonce, setHistoryNonce] = useState(0);
|
|
347
351
|
const [leftCollapsed, setLeftCollapsed] = useState(false);
|
|
348
352
|
const [rightCollapsed, setRightCollapsed] = useState(false);
|
|
353
|
+
// When viewing a markdown file, let the user toggle between the raw
|
|
354
|
+
// editor and a rendered preview. We default to preview because most
|
|
355
|
+
// of the .md files in the governance template are explanatory docs
|
|
356
|
+
// — readers want them to look like real GitHub markdown, not source.
|
|
357
|
+
const [mdPreview, setMdPreview] = useState(true);
|
|
349
358
|
const abortRef = useRef<AbortController | null>(null);
|
|
350
359
|
// ── Concurrency engine state ───────────────────────────────────────
|
|
351
360
|
// The concurrency tab is no longer a simulator — these records track
|
|
@@ -2188,48 +2197,80 @@ interface ImportMeta {
|
|
|
2188
2197
|
{activeFile || "(no file selected)"}
|
|
2189
2198
|
</span>
|
|
2190
2199
|
</div>
|
|
2191
|
-
<
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
:
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2200
|
+
<div className="flex items-center gap-1">
|
|
2201
|
+
{/* Markdown preview toggle. Only meaningful for .md / .markdown
|
|
2202
|
+
files; hidden otherwise so it doesn't clutter the header. */}
|
|
2203
|
+
{activeFile && /\.(md|markdown)$/i.test(activeFile) && (
|
|
2204
|
+
<button
|
|
2205
|
+
onClick={() => setMdPreview((v) => !v)}
|
|
2206
|
+
className="flex items-center gap-1 p-1 rounded text-slate-500 hover:text-amber-300 hover:bg-slate-800/60"
|
|
2207
|
+
title={
|
|
2208
|
+
mdPreview
|
|
2209
|
+
? "Show raw markdown source"
|
|
2210
|
+
: "Render markdown preview"
|
|
2211
|
+
}
|
|
2212
|
+
>
|
|
2213
|
+
{mdPreview ? (
|
|
2214
|
+
<EyeOff className="w-3.5 h-3.5" />
|
|
2215
|
+
) : (
|
|
2216
|
+
<Eye className="w-3.5 h-3.5" />
|
|
2217
|
+
)}
|
|
2218
|
+
<span className="text-[10px] uppercase tracking-wide">
|
|
2219
|
+
{mdPreview ? "Source" : "Preview"}
|
|
2220
|
+
</span>
|
|
2221
|
+
</button>
|
|
2204
2222
|
)}
|
|
2205
|
-
|
|
2223
|
+
<button
|
|
2224
|
+
onClick={() => setRightCollapsed((v) => !v)}
|
|
2225
|
+
className="p-1 rounded text-slate-500 hover:text-amber-300 hover:bg-slate-800/60 shrink-0"
|
|
2226
|
+
title={
|
|
2227
|
+
rightCollapsed
|
|
2228
|
+
? "Show console/jobs panel"
|
|
2229
|
+
: "Hide console/jobs panel"
|
|
2230
|
+
}
|
|
2231
|
+
>
|
|
2232
|
+
{rightCollapsed ? (
|
|
2233
|
+
<PanelRightOpen className="w-3.5 h-3.5" />
|
|
2234
|
+
) : (
|
|
2235
|
+
<PanelRightClose className="w-3.5 h-3.5" />
|
|
2236
|
+
)}
|
|
2237
|
+
</button>
|
|
2238
|
+
</div>
|
|
2206
2239
|
</div>
|
|
2207
2240
|
<div className="flex-1 min-h-0">
|
|
2208
|
-
{activeFile &&
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2241
|
+
{activeFile &&
|
|
2242
|
+
workspace.files[activeFile] !== undefined &&
|
|
2243
|
+
(mdPreview && /\.(md|markdown)$/i.test(activeFile) ? (
|
|
2244
|
+
<div className="h-full w-full overflow-auto px-6 py-4 text-slate-200 gha-md-preview">
|
|
2245
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
|
2246
|
+
{workspace.files[activeFile]}
|
|
2247
|
+
</ReactMarkdown>
|
|
2248
|
+
</div>
|
|
2249
|
+
) : (
|
|
2250
|
+
<MonacoEditorLib
|
|
2251
|
+
key={activeFile}
|
|
2252
|
+
height="100%"
|
|
2253
|
+
width="100%"
|
|
2254
|
+
language={getEditorLanguage(activeFile)}
|
|
2255
|
+
theme="gha-lab-dark"
|
|
2256
|
+
path={getGhaLabModelPath(activeFile)}
|
|
2257
|
+
value={workspace.files[activeFile]}
|
|
2258
|
+
beforeMount={handleBeforeMount}
|
|
2259
|
+
onMount={handleMount}
|
|
2260
|
+
onChange={handleEditorChange}
|
|
2261
|
+
options={{
|
|
2262
|
+
fontFamily: EDITOR_FONT,
|
|
2263
|
+
fontSize: 13,
|
|
2264
|
+
lineHeight: 22,
|
|
2265
|
+
minimap: { enabled: false },
|
|
2266
|
+
automaticLayout: true,
|
|
2267
|
+
scrollBeyondLastLine: false,
|
|
2268
|
+
wordWrap: "off",
|
|
2269
|
+
tabSize: 2,
|
|
2270
|
+
insertSpaces: true,
|
|
2271
|
+
}}
|
|
2272
|
+
/>
|
|
2273
|
+
))}
|
|
2233
2274
|
</div>
|
|
2234
2275
|
</div>
|
|
2235
2276
|
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "../infraLab";
|
|
10
10
|
import {
|
|
11
11
|
DEFAULT_GHA_LAB,
|
|
12
|
+
GOVERNANCE_GHA_LAB,
|
|
12
13
|
parseGhaLabWorkspace,
|
|
13
14
|
REACT_VITE_TYPESCRIPT_GHA_LAB,
|
|
14
15
|
} from "../githubActionsLab";
|
|
@@ -696,6 +697,12 @@ export default function LabsPanel() {
|
|
|
696
697
|
"Multi-job CI workflow with a local composite action and a matrix build",
|
|
697
698
|
onClick: () => openGhaLab(DEFAULT_GHA_LAB),
|
|
698
699
|
},
|
|
700
|
+
{
|
|
701
|
+
label: "Platform Governance Template",
|
|
702
|
+
description:
|
|
703
|
+
"PLF-style mono-repo: CODEOWNERS, PR template, Azure PIM/Policy + AWS IAM deploy workflows, offboarding",
|
|
704
|
+
onClick: () => openGhaLab(GOVERNANCE_GHA_LAB),
|
|
705
|
+
},
|
|
699
706
|
]}
|
|
700
707
|
onOpen={openGhaFile}
|
|
701
708
|
openTitle="Open in GitHub Lab"
|
|
@@ -26,6 +26,7 @@ import { useMemo, useState } from "react";
|
|
|
26
26
|
import {
|
|
27
27
|
Check,
|
|
28
28
|
CircleDot,
|
|
29
|
+
FileText,
|
|
29
30
|
GitBranch,
|
|
30
31
|
GitPullRequest,
|
|
31
32
|
Info,
|
|
@@ -52,10 +53,12 @@ import {
|
|
|
52
53
|
evaluateCodeOwners,
|
|
53
54
|
evaluateMergeability,
|
|
54
55
|
findCodeOwnersPath,
|
|
56
|
+
findPullRequestTemplates,
|
|
55
57
|
latestReviewByAuthor,
|
|
56
58
|
parseCodeOwners,
|
|
57
59
|
type CheckStatus,
|
|
58
60
|
type MergeabilityResult,
|
|
61
|
+
type PullRequestTemplate,
|
|
59
62
|
} from "../codeowners";
|
|
60
63
|
|
|
61
64
|
interface PullRequestPanelProps {
|
|
@@ -119,6 +122,13 @@ export default function PullRequestPanel({
|
|
|
119
122
|
[evaluation, pr, workspace.rulesets, org, baseBranch, defaultBranch],
|
|
120
123
|
);
|
|
121
124
|
|
|
125
|
+
// PR templates discovered from .github/PULL_REQUEST_TEMPLATE[.md]
|
|
126
|
+
// (just like real github.com — no Settings UI, file-based only).
|
|
127
|
+
const templates = useMemo(
|
|
128
|
+
() => findPullRequestTemplates(workspace.files),
|
|
129
|
+
[workspace.files],
|
|
130
|
+
);
|
|
131
|
+
|
|
122
132
|
// ── Mutators ────────────────────────────────────────────────────
|
|
123
133
|
const updatePR = (next: Partial<GithubLabPullRequest>) =>
|
|
124
134
|
onChange((prev) => ({
|
|
@@ -206,6 +216,13 @@ export default function PullRequestPanel({
|
|
|
206
216
|
</div>
|
|
207
217
|
</header>
|
|
208
218
|
|
|
219
|
+
{/* ── Description (PR body + template picker) ── */}
|
|
220
|
+
<PullRequestBody
|
|
221
|
+
body={pr.body ?? ""}
|
|
222
|
+
templates={templates}
|
|
223
|
+
onChange={(body) => updatePR({ body })}
|
|
224
|
+
/>
|
|
225
|
+
|
|
209
226
|
{!codeownersPath && (
|
|
210
227
|
<div className="mb-3 rounded-lg border border-amber-500/30 bg-amber-500/10 p-2 text-amber-200">
|
|
211
228
|
No CODEOWNERS file found. Add one at <code>.github/CODEOWNERS</code>{" "}
|
|
@@ -578,6 +595,114 @@ function labelForStatus(s: string): string {
|
|
|
578
595
|
}
|
|
579
596
|
}
|
|
580
597
|
|
|
598
|
+
// ─── PR description body + template picker ────────────────────────────
|
|
599
|
+
//
|
|
600
|
+
// On github.com the description box is auto-prefilled from a PR
|
|
601
|
+
// template when you open a new PR, and you can choose between
|
|
602
|
+
// templates via ?template= when there's a directory of them. Here we
|
|
603
|
+
// surface the same mechanic explicitly: a textarea for the body, plus
|
|
604
|
+
// "Apply template" buttons that overwrite the body with the chosen
|
|
605
|
+
// template (with a confirm if the body has been edited away from any
|
|
606
|
+
// template's contents).
|
|
607
|
+
|
|
608
|
+
function PullRequestBody({
|
|
609
|
+
body,
|
|
610
|
+
templates,
|
|
611
|
+
onChange,
|
|
612
|
+
}: {
|
|
613
|
+
body: string;
|
|
614
|
+
templates: PullRequestTemplate[];
|
|
615
|
+
onChange: (next: string) => void;
|
|
616
|
+
}) {
|
|
617
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
618
|
+
// Map body → matching template name (so we can mark the active chip).
|
|
619
|
+
const matchingTemplate = templates.find((t) => t.body === body);
|
|
620
|
+
|
|
621
|
+
const apply = (t: PullRequestTemplate) => {
|
|
622
|
+
if (
|
|
623
|
+
body.trim().length > 0 &&
|
|
624
|
+
!templates.some((tpl) => tpl.body === body) &&
|
|
625
|
+
!window.confirm(
|
|
626
|
+
`Replace the current description with template "${t.name}"?`,
|
|
627
|
+
)
|
|
628
|
+
) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
onChange(t.body);
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
return (
|
|
635
|
+
<section className="mb-3 rounded-xl border border-slate-800/60 bg-slate-900/40">
|
|
636
|
+
<header className="flex items-center justify-between gap-2 px-3 py-2">
|
|
637
|
+
<button
|
|
638
|
+
onClick={() => setCollapsed((c) => !c)}
|
|
639
|
+
className="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-slate-400 hover:text-amber-200"
|
|
640
|
+
>
|
|
641
|
+
<FileText className="h-3 w-3" />
|
|
642
|
+
Description
|
|
643
|
+
</button>
|
|
644
|
+
{templates.length > 0 && (
|
|
645
|
+
<div className="flex flex-wrap items-center gap-1">
|
|
646
|
+
<span className="text-[10px] text-slate-500">Templates:</span>
|
|
647
|
+
{templates.map((t) => {
|
|
648
|
+
const active = matchingTemplate?.path === t.path;
|
|
649
|
+
return (
|
|
650
|
+
<button
|
|
651
|
+
key={t.path}
|
|
652
|
+
onClick={() => apply(t)}
|
|
653
|
+
title={t.path}
|
|
654
|
+
className={`rounded px-1.5 py-0.5 text-[10px] ${
|
|
655
|
+
active
|
|
656
|
+
? "bg-amber-500/25 text-amber-100"
|
|
657
|
+
: "bg-slate-800 text-slate-300 hover:bg-slate-700"
|
|
658
|
+
}`}
|
|
659
|
+
>
|
|
660
|
+
{t.name}
|
|
661
|
+
</button>
|
|
662
|
+
);
|
|
663
|
+
})}
|
|
664
|
+
{body.length > 0 && (
|
|
665
|
+
<button
|
|
666
|
+
onClick={() => onChange("")}
|
|
667
|
+
className="rounded px-1.5 py-0.5 text-[10px] text-slate-500 hover:text-rose-300"
|
|
668
|
+
title="Clear the description"
|
|
669
|
+
>
|
|
670
|
+
Clear
|
|
671
|
+
</button>
|
|
672
|
+
)}
|
|
673
|
+
</div>
|
|
674
|
+
)}
|
|
675
|
+
</header>
|
|
676
|
+
{!collapsed && (
|
|
677
|
+
<div className="px-3 pb-3">
|
|
678
|
+
{templates.length === 0 && body.length === 0 && (
|
|
679
|
+
<p className="mb-1.5 text-[10px] text-slate-500">
|
|
680
|
+
Tip: add{" "}
|
|
681
|
+
<code className="rounded bg-slate-800 px-1 py-px text-[10px] text-amber-200">
|
|
682
|
+
.github/pull_request_template.md
|
|
683
|
+
</code>{" "}
|
|
684
|
+
to the workspace and a one-click "Default" template will appear
|
|
685
|
+
here — same as github.com.
|
|
686
|
+
</p>
|
|
687
|
+
)}
|
|
688
|
+
<textarea
|
|
689
|
+
value={body}
|
|
690
|
+
onChange={(e) => onChange(e.target.value)}
|
|
691
|
+
placeholder="Leave a description (Markdown supported)…"
|
|
692
|
+
className="h-32 w-full resize-y rounded border border-slate-700 bg-slate-950/60 p-2 font-mono text-[11px] text-slate-200 outline-none focus:border-emerald-500/40"
|
|
693
|
+
/>
|
|
694
|
+
{matchingTemplate && (
|
|
695
|
+
<p className="mt-1 text-[10px] text-slate-500">
|
|
696
|
+
Using template{" "}
|
|
697
|
+
<code className="text-slate-400">{matchingTemplate.path}</code>
|
|
698
|
+
</p>
|
|
699
|
+
)}
|
|
700
|
+
</div>
|
|
701
|
+
)}
|
|
702
|
+
</section>
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
581
706
|
// ─── Rulesets summary (read-only) ──────────────────────────────────────
|
|
582
707
|
//
|
|
583
708
|
// On real github.com the PR sidebar shows a tiny summary of which
|
|
@@ -1379,17 +1379,20 @@ function EnvironmentsSection({
|
|
|
1379
1379
|
|
|
1380
1380
|
function Field({
|
|
1381
1381
|
label,
|
|
1382
|
+
help,
|
|
1382
1383
|
children,
|
|
1383
1384
|
}: {
|
|
1384
1385
|
label: string;
|
|
1386
|
+
help?: string;
|
|
1385
1387
|
children: React.ReactNode;
|
|
1386
1388
|
}) {
|
|
1387
1389
|
return (
|
|
1388
|
-
<div>
|
|
1390
|
+
<div className="mt-3">
|
|
1389
1391
|
<div className="mb-1 text-[11px] font-semibold text-slate-200">
|
|
1390
1392
|
{label}
|
|
1391
1393
|
</div>
|
|
1392
1394
|
{children}
|
|
1395
|
+
{help && <p className="mt-1 text-[11px] text-slate-500">{help}</p>}
|
|
1393
1396
|
</div>
|
|
1394
1397
|
);
|
|
1395
1398
|
}
|