Slice 10: create_pr (open a pull request)

forge CreatePullRequest (Gitea; empty base resolves to the repo default branch). Service CreatePR records a pr-created event. HTTP POST /api/repo/pr/create; MCP tool create_pr; <pr-list> New pull request button (head = selected repo current branch, base = default). Live-tested end-to-end: app create_pr opened a real PR (base auto-resolved), listed, then cleaned up; main untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-20 17:34:13 -04:00
parent e1999bcf21
commit ad00654487
10 changed files with 149 additions and 7 deletions
+32 -3
View File
@@ -10,6 +10,7 @@ class PRList extends HTMLElement {
#controller = null;
#onSelect = null;
#path = '';
#repo = null;
constructor() {
super();
@@ -18,7 +19,7 @@ class PRList extends HTMLElement {
connectedCallback() {
this.#renderShell();
this.#onSelect = (e) => this.#load(e.detail?.path);
this.#onSelect = (e) => { this.#repo = e.detail; this.#load(e.detail?.path); };
document.addEventListener('repo:select', this.#onSelect);
}
@@ -64,6 +65,25 @@ class PRList extends HTMLElement {
}
}
async #create() {
const branch = this.#repo?.branch;
if (!branch) return;
const title = window.prompt(`Open a pull request from "${branch}" (into the default branch).\nTitle:`, branch);
if (title === null) return;
try {
const res = await fetch('/api/repo/pr/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: this.#path, head: branch, base: '', title: title.trim() || branch }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
this.#load(this.#path); // refresh so the new PR appears
} catch (err) {
window.alert(`Create PR failed: ${err.message}`);
}
}
#render(prs) {
const body = this.shadowRoot.getElementById('body');
if (prs.length === 0) {
@@ -102,8 +122,13 @@ class PRList extends HTMLElement {
:host { display: block; }
.box { background: var(--surface-1); border: 1px solid var(--border);
border-radius: var(--radius); padding: 14px 16px; }
.hd { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
h3 { font-size: 12px; text-transform: uppercase; letter-spacing: .04em;
color: var(--color-fg-muted); margin: 0 0 8px; }
color: var(--color-fg-muted); margin: 0; }
#new { margin-left: auto; font: inherit; cursor: pointer; padding: 3px 10px;
border-radius: var(--radius-sm); border: 1px solid var(--fill-accent);
color: var(--fill-accent); background: transparent; }
#new:hover { background: var(--surface-2); }
ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
li { border-top: 1px solid var(--border); padding-top: 8px; }
li:first-child { border-top: none; padding-top: 0; }
@@ -124,9 +149,13 @@ class PRList extends HTMLElement {
.error { color: var(--color-danger); }
</style>
<div class="box">
<h3>Pull requests</h3>
<div class="hd">
<h3>Pull requests</h3>
<button id="new" title="Open a PR from the current branch">New pull request…</button>
</div>
<div id="body"><p class="muted">Select a repository.</p></div>
</div>`;
this.shadowRoot.getElementById('new').addEventListener('click', () => this.#create());
}
#esc(s) {
+5 -1
View File
@@ -13,10 +13,14 @@ naming the PR, base, and branch to be deleted.
configured or the repo isn't on the forge host.
- **Listens:** `repo:select` on `document` — loads PRs for `event.detail.path`.
- **Fetches:** `GET /api/repo/prs?path=…` (`{supported:false}` → hidden).
- **Writes:** `POST /api/repo/pr/merge {path, number}` after a `confirm()`.
- **Writes:** `POST /api/repo/pr/merge {path, number}` after a `confirm()`;
`POST /api/repo/pr/create {path, head, base, title}` via the "New pull request…"
button (head = the selected repo's current branch, base = the repo default).
## History
- 2026-09-20: created — slice 5 (forge); list open PRs + "Merge & clean up".
- 2026-09-20: added "New pull request…" (create_pr) — opens a PR from the
selected repo's current branch into the default branch (slice 10).
## Notes / gotchas
- Requires `GITEA_URL` + `GITEA_TOKEN` on the server; otherwise the component