@oddessentials/agent-guild 0.31.0 → 0.32.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/README.md +5 -2
- package/package.json +1 -1
- package/src/manager/github-views.mjs +37 -1
- package/src/manager/server.mjs +2 -1
- package/web/app.js +195 -4
- package/web/environment.js +2 -0
- package/web/index.html +22 -0
- package/web/styles.css +18 -1
package/README.md
CHANGED
|
@@ -195,8 +195,11 @@ of them from the box at its top; it starts on the repository the focused
|
|
|
195
195
|
terminal's folder is a clone of. **Issues** lists, opens, edits and closes
|
|
196
196
|
issues. **Actions** shows the latest workflow runs and whether one is still
|
|
197
197
|
going, refreshing while it is on screen. **Pull requests** lists the open
|
|
198
|
-
ones. **
|
|
199
|
-
|
|
198
|
+
ones. **Branches** lists every remote branch of the selected repository, with
|
|
199
|
+
its default and protected branches marked. Filter by name, copy a branch name,
|
|
200
|
+
or open it on GitHub. Larger lists load progressively; **Refresh** checks for
|
|
201
|
+
changes while keeping your place in the list. **Repositories** lists and creates
|
|
202
|
+
repositories and clones them over SSH with a key Agent Guild keeps for each account. On a wide window the side panel
|
|
200
203
|
makes room for itself; on a smaller one it lies over the cards, and over the
|
|
201
204
|
terminals when they would get too narrow. Drag its edge to resize it. Escape
|
|
202
205
|
or **Close** puts it away.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oddessentials/agent-guild",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "Launch and watch AI coding-assistant terminal sessions from one local web page. A bundled session manager owns the terminals so the page can close and reconnect.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Issues, Actions runs and open pull requests of one repository, read with a
|
|
1
|
+
// Branches, Issues, Actions runs and open pull requests of one repository, read with a
|
|
2
2
|
// signed-in account. Reading and editing stay shallow: GitHub's page is the full view.
|
|
3
3
|
|
|
4
4
|
import { nextLink, parseRepo } from './github.mjs';
|
|
@@ -114,6 +114,42 @@ export function createViews(github) {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
return {
|
|
117
|
+
async branches(accountId, owner, name, { page = '1' } = {}) {
|
|
118
|
+
if (!/^[1-9]\d*$/.test(String(page)) || !Number.isSafeInteger(Number(page))) {
|
|
119
|
+
throw refusal(400, 'bad_page', 'page must be a positive integer');
|
|
120
|
+
}
|
|
121
|
+
page = Number(page);
|
|
122
|
+
// One metadata read per listing, never a protection request per branch.
|
|
123
|
+
const [listing, metadata] = await Promise.all([
|
|
124
|
+
call(accountId, owner, name, `/branches?per_page=100&page=${page}`),
|
|
125
|
+
page === 1 ? call(accountId, owner, name, '').then(({ body }) => ({ body }), (error) => ({ error: error.message })) : null,
|
|
126
|
+
]);
|
|
127
|
+
const { body, res, repo } = listing;
|
|
128
|
+
if (!Array.isArray(body)) throw refusal(502, 'github_error', 'GitHub did not return a branch list');
|
|
129
|
+
let nextPage = null;
|
|
130
|
+
const next = nextLink(res.headers.get('link'));
|
|
131
|
+
if (next) {
|
|
132
|
+
// Reconstruct our own route; never send credentials to a supplied Link URL.
|
|
133
|
+
try { nextPage = Number(new URL(next).searchParams.get('page')); } catch { /* rejected below */ }
|
|
134
|
+
if (!Number.isSafeInteger(nextPage) || nextPage <= page) {
|
|
135
|
+
throw refusal(502, 'github_error', 'GitHub returned an invalid next page for branches');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const branches = body.filter((item) => typeof item?.name === 'string' && item.name.length > 0).map((item) => ({
|
|
139
|
+
name: item.name,
|
|
140
|
+
sha: typeof item.commit?.sha === 'string' && /^[a-f0-9]{40,64}$/i.test(item.commit.sha) ? item.commit.sha : null,
|
|
141
|
+
protected: item.protected === true,
|
|
142
|
+
url: `${WEB}/${repo.fullName}/tree/${encodeURIComponent(item.name)}`,
|
|
143
|
+
}));
|
|
144
|
+
return {
|
|
145
|
+
branches, nextPage,
|
|
146
|
+
defaultBranch: typeof metadata?.body?.default_branch === 'string' ? metadata.body.default_branch : null,
|
|
147
|
+
metadataError: metadata?.error ?? null,
|
|
148
|
+
fetchedAt: new Date().toISOString(),
|
|
149
|
+
url: `${WEB}/${repo.fullName}/branches`,
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
|
|
117
153
|
async issues(accountId, owner, name, { state = 'open' } = {}) {
|
|
118
154
|
if (state !== 'open' && state !== 'closed' && state !== 'all') throw refusal(400, 'bad_state', 'state must be open, closed or all');
|
|
119
155
|
const { body, res, repo } = await call(accountId, owner, name, `/issues?state=${state}&per_page=${PAGE}&sort=updated&direction=desc`);
|
package/src/manager/server.mjs
CHANGED
|
@@ -458,7 +458,7 @@ export function createManagerServer({
|
|
|
458
458
|
const folder = manager.resolveCwd(url.searchParams.get('cwd'));
|
|
459
459
|
return sendJson(res, 200, { folder, repo: folderOrigin(folder) });
|
|
460
460
|
}
|
|
461
|
-
const view = route.match(/^\/github\/accounts\/([^/]+)\/repos\/([^/]+)\/([^/]+)\/(issues|actions|pulls)(?:\/([^/]+))?$/);
|
|
461
|
+
const view = route.match(/^\/github\/accounts\/([^/]+)\/repos\/([^/]+)\/([^/]+)\/(issues|actions|pulls|branches)(?:\/([^/]+))?$/);
|
|
462
462
|
if (view) {
|
|
463
463
|
let parts;
|
|
464
464
|
try { parts = view.map((part) => (part === undefined ? part : decodeURIComponent(part))); } catch { throw new HttpError(400, 'repo must be a GitHub repository written as owner/name', 'bad_repo'); }
|
|
@@ -472,6 +472,7 @@ export function createManagerServer({
|
|
|
472
472
|
if (kind === 'issues' && number !== undefined && method === 'PATCH') {
|
|
473
473
|
return sendJson(res, 200, { issue: await views.updateIssue(id, owner, name, number, await readJsonBody(req)) });
|
|
474
474
|
}
|
|
475
|
+
if (kind === 'branches' && number === undefined && method === 'GET') return sendJson(res, 200, await views.branches(id, owner, name, { page: url.searchParams.get('page') ?? '1' }));
|
|
475
476
|
if (kind === 'actions' && number === undefined && method === 'GET') return sendJson(res, 200, await views.actions(id, owner, name));
|
|
476
477
|
if (kind === 'pulls' && number === undefined && method === 'GET') return sendJson(res, 200, await views.pulls(id, owner, name));
|
|
477
478
|
}
|
package/web/app.js
CHANGED
|
@@ -2967,6 +2967,13 @@ function loadGitHub() {
|
|
|
2967
2967
|
function setGitHub(github) {
|
|
2968
2968
|
const before = state.github?.signIn;
|
|
2969
2969
|
state.github = github;
|
|
2970
|
+
const branches = githubPick.data.branches;
|
|
2971
|
+
if (branches && !github.accounts.some((a) => a.id === branches.repo.accountId && !a.needsSignIn)) {
|
|
2972
|
+
// Replace the generation even while the dock is closed; signing in again must
|
|
2973
|
+
// never make an earlier account's pending answer current.
|
|
2974
|
+
githubPick.data.branches = { ...branches, value: null, loading: false, nextPage: null,
|
|
2975
|
+
error: `Sign in as @${branches.repo.login} in Repositories to see its branches.` };
|
|
2976
|
+
}
|
|
2970
2977
|
const done = github.signIn?.status === 'done' && before?.status === 'pending' ? github.signIn : null;
|
|
2971
2978
|
if (done) {
|
|
2972
2979
|
selectGitHubAccount(done.accountId);
|
|
@@ -3086,7 +3093,7 @@ function openGitHub({ focus = true } = {}) {
|
|
|
3086
3093
|
loadGitHub();
|
|
3087
3094
|
if (githubPick.repo) {
|
|
3088
3095
|
loadView('actions');
|
|
3089
|
-
if (
|
|
3096
|
+
if (['issues', 'pulls', 'branches'].includes(githubShownView())) loadView(githubShownView());
|
|
3090
3097
|
}
|
|
3091
3098
|
followTerminal();
|
|
3092
3099
|
if (focus) ($('github-card').querySelector('.btn.primary') ?? $('dock-close')).focus();
|
|
@@ -3497,7 +3504,7 @@ function useFolder(dir) {
|
|
|
3497
3504
|
|
|
3498
3505
|
// ---- GitHub repository views ------------------------------------------------
|
|
3499
3506
|
|
|
3500
|
-
const GITHUB_VIEWS = ['repos', 'issues', 'actions', 'pulls'];
|
|
3507
|
+
const GITHUB_VIEWS = ['repos', 'issues', 'actions', 'pulls', 'branches'];
|
|
3501
3508
|
const RUNS_POLL_MS = 6000;
|
|
3502
3509
|
const RUNS_IDLE_POLL_MS = 30000;
|
|
3503
3510
|
const BODY_LIMIT = 48000;
|
|
@@ -3592,7 +3599,11 @@ function pickRepo(repo, { chosen = false } = {}) {
|
|
|
3592
3599
|
githubPick.recent = remember(githubPick.recent, repo);
|
|
3593
3600
|
save(GITHUB_RECENT_KEY, JSON.stringify(githubPick.recent));
|
|
3594
3601
|
}
|
|
3595
|
-
if (!same)
|
|
3602
|
+
if (!same) {
|
|
3603
|
+
Object.assign(githubPick, { data: {}, editing: null });
|
|
3604
|
+
$('github-branches-filter').value = '';
|
|
3605
|
+
$('github-branches').scrollTop = 0;
|
|
3606
|
+
}
|
|
3596
3607
|
if ((!same || chosen) && githubView.accountId !== repo.accountId && state.github?.accounts.some((a) => a.id === repo.accountId)) {
|
|
3597
3608
|
selectGitHubAccount(repo.accountId);
|
|
3598
3609
|
githubView.card = null;
|
|
@@ -3605,7 +3616,7 @@ function pickRepo(repo, { chosen = false } = {}) {
|
|
|
3605
3616
|
renderGitHubViews();
|
|
3606
3617
|
if (!dockShows('github')) return;
|
|
3607
3618
|
loadView('actions');
|
|
3608
|
-
if (
|
|
3619
|
+
if (['issues', 'pulls', 'branches'].includes(githubShownView())) loadView(githubShownView());
|
|
3609
3620
|
}
|
|
3610
3621
|
|
|
3611
3622
|
/** Picks the repository whose clone holds the focused terminal's folder, once for each terminal focused. */
|
|
@@ -3628,6 +3639,7 @@ async function followTerminal() {
|
|
|
3628
3639
|
}
|
|
3629
3640
|
|
|
3630
3641
|
async function loadView(view) {
|
|
3642
|
+
if (view === 'branches') return loadBranches();
|
|
3631
3643
|
const repo = githubPick.repo;
|
|
3632
3644
|
if (!repo || view === 'repos') return;
|
|
3633
3645
|
const query = view === 'issues' ? `?state=${githubPick.issueState}` : '';
|
|
@@ -3652,6 +3664,170 @@ async function loadView(view) {
|
|
|
3652
3664
|
if (view === 'actions') scheduleRuns();
|
|
3653
3665
|
}
|
|
3654
3666
|
|
|
3667
|
+
// Each object is a listing generation, scoped to the selected account/repository.
|
|
3668
|
+
// A retry continues that generation; Refresh replaces it and starts at page one.
|
|
3669
|
+
function branchesCurrent(request) {
|
|
3670
|
+
return githubPick.data.branches === request && githubPick.repo && repoKey(githubPick.repo) === request.key
|
|
3671
|
+
&& state.github?.accounts.some((a) => a.id === request.repo.accountId && !a.needsSignIn);
|
|
3672
|
+
}
|
|
3673
|
+
|
|
3674
|
+
function branchesVisible() {
|
|
3675
|
+
return dockShows('github') && githubShownView() === 'branches' && document.visibilityState === 'visible';
|
|
3676
|
+
}
|
|
3677
|
+
|
|
3678
|
+
async function loadBranches({ resume = false } = {}) {
|
|
3679
|
+
const repo = githubPick.repo;
|
|
3680
|
+
if (!repo || !state.github?.accounts.some((a) => a.id === repo.accountId && !a.needsSignIn)) return;
|
|
3681
|
+
const before = viewData('branches');
|
|
3682
|
+
if (resume && (!before || before.loading || before.nextPage === null)) return;
|
|
3683
|
+
const request = resume ? before : {
|
|
3684
|
+
key: repoKey(repo), repo, value: before?.value ?? null, loading: false, error: null,
|
|
3685
|
+
nextPage: 1, received: false, rows: new Map(), defaultBranch: null, metadataError: null,
|
|
3686
|
+
};
|
|
3687
|
+
githubPick.data.branches = request;
|
|
3688
|
+
request.loading = true;
|
|
3689
|
+
request.error = null;
|
|
3690
|
+
renderGitHubViews();
|
|
3691
|
+
try {
|
|
3692
|
+
while (request.nextPage !== null && branchesVisible() && branchesCurrent(request)) {
|
|
3693
|
+
const page = request.nextPage;
|
|
3694
|
+
const result = await api('GET', `${repoPath(repo)}/branches?page=${page}`);
|
|
3695
|
+
if (!branchesCurrent(request)) return;
|
|
3696
|
+
// A malformed response must not masquerade as a complete, empty list.
|
|
3697
|
+
if (!Array.isArray(result.branches) || (result.nextPage !== null
|
|
3698
|
+
&& (!Number.isSafeInteger(result.nextPage) || result.nextPage <= page))) {
|
|
3699
|
+
throw new Error('Could not read the next page of branches. Try again.');
|
|
3700
|
+
}
|
|
3701
|
+
if (page === 1) {
|
|
3702
|
+
request.defaultBranch = result.defaultBranch;
|
|
3703
|
+
request.metadataError = result.metadataError;
|
|
3704
|
+
}
|
|
3705
|
+
for (const branch of result.branches) request.rows.set(branch.name, branch);
|
|
3706
|
+
request.nextPage = result.nextPage;
|
|
3707
|
+
request.received = true;
|
|
3708
|
+
request.value = { ...result, branches: [...request.rows.values()],
|
|
3709
|
+
defaultBranch: request.defaultBranch, metadataError: request.metadataError };
|
|
3710
|
+
renderGitHubViews();
|
|
3711
|
+
}
|
|
3712
|
+
} catch (err) {
|
|
3713
|
+
if (!branchesCurrent(request)) return;
|
|
3714
|
+
if (err instanceof AuthError) { showAuth(err.message); return; }
|
|
3715
|
+
request.error = err.message;
|
|
3716
|
+
} finally {
|
|
3717
|
+
if (branchesCurrent(request)) {
|
|
3718
|
+
request.loading = false;
|
|
3719
|
+
renderGitHubViews();
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3724
|
+
/** Keep the first visible branch at the same pixel offset when earlier rows arrive. */
|
|
3725
|
+
function branchAnchor(panel, list) {
|
|
3726
|
+
const { top, bottom } = panel.getBoundingClientRect();
|
|
3727
|
+
const row = [...list.children].find((node) => {
|
|
3728
|
+
const rect = node.getBoundingClientRect();
|
|
3729
|
+
return rect.bottom > top && rect.top < bottom;
|
|
3730
|
+
});
|
|
3731
|
+
return row ? { name: row.dataset.branch, offset: row.getBoundingClientRect().top - top } : null;
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3734
|
+
async function copyBranch(name, control) {
|
|
3735
|
+
const request = viewData('branches');
|
|
3736
|
+
control.disabled = true;
|
|
3737
|
+
let message;
|
|
3738
|
+
try {
|
|
3739
|
+
if (!navigator.clipboard?.writeText) throw new Error('Clipboard unavailable');
|
|
3740
|
+
await navigator.clipboard.writeText(name);
|
|
3741
|
+
message = `Copied branch name: ${name}`;
|
|
3742
|
+
} catch {
|
|
3743
|
+
message = 'Could not copy. Select the branch name and copy it using your browser.';
|
|
3744
|
+
} finally {
|
|
3745
|
+
control.disabled = false;
|
|
3746
|
+
}
|
|
3747
|
+
if (request === viewData('branches') && branchesVisible() && control.isConnected) toast(message);
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3750
|
+
function renderBranches({ preserveAnchor = true } = {}) {
|
|
3751
|
+
const panel = $('github-branches');
|
|
3752
|
+
const list = $('github-branches-list');
|
|
3753
|
+
const repo = githubPick.repo;
|
|
3754
|
+
const key = repo ? repoKey(repo) : '';
|
|
3755
|
+
const same = panel.dataset.repo === key;
|
|
3756
|
+
const anchor = same && preserveAnchor ? branchAnchor(panel, list) : null;
|
|
3757
|
+
const focused = list.contains(document.activeElement) ? document.activeElement : null;
|
|
3758
|
+
const oldScroll = same ? panel.scrollTop : 0;
|
|
3759
|
+
if (!same) { list.replaceChildren(); panel.dataset.repo = key; }
|
|
3760
|
+
$('github-branches-pick').hidden = Boolean(repo);
|
|
3761
|
+
$('github-branches-content').hidden = !repo;
|
|
3762
|
+
if (!repo) return;
|
|
3763
|
+
const slot = viewData('branches');
|
|
3764
|
+
const value = slot?.value;
|
|
3765
|
+
const query = $('github-branches-filter').value.trim().toLocaleLowerCase();
|
|
3766
|
+
$('github-branches-clear').disabled = !$('github-branches-filter').value;
|
|
3767
|
+
const refresh = $('github-branches-refresh');
|
|
3768
|
+
const usable = state.github?.accounts.some((a) => a.id === repo.accountId && !a.needsSignIn);
|
|
3769
|
+
refresh.disabled = Boolean(slot?.loading) || !usable;
|
|
3770
|
+
refresh.textContent = slot?.loading ? (value ? 'Refreshing…' : 'Loading…') : 'Refresh';
|
|
3771
|
+
const out = $('github-branches-out');
|
|
3772
|
+
out.href = value?.url ?? `https://github.com/${repo.fullName}/branches`;
|
|
3773
|
+
out.title = `Open ${repo.fullName} branches on GitHub`;
|
|
3774
|
+
const branches = (value?.branches ?? []).filter((b) => b.name.toLocaleLowerCase().includes(query)).sort((a, b) =>
|
|
3775
|
+
Number(b.name === value.defaultBranch) - Number(a.name === value.defaultBranch)
|
|
3776
|
+
|| a.name.localeCompare(b.name) || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
3777
|
+
const complete = Boolean(value && value.nextPage === null && (!slot.error || slot.received));
|
|
3778
|
+
const count = value?.branches.length ?? 0;
|
|
3779
|
+
let status = !value ? (slot?.error ? 'Branches could not be loaded.' : 'Loading branches…')
|
|
3780
|
+
: query ? `${branches.length} of ${count} loaded branches match.`
|
|
3781
|
+
: `${count} ${count === 1 ? 'branch' : 'branches'}${complete ? '' : ' loaded'}.`;
|
|
3782
|
+
if (value && !slot.received) status += slot.loading ? ' Refreshing…' : ' Showing previous results.';
|
|
3783
|
+
else if (!complete && value) status += slot.loading ? ' Loading more…' : ' Listing incomplete.';
|
|
3784
|
+
if (complete && !branches.length) status = query ? `No branches match “${$('github-branches-filter').value.trim()}”.` : 'No remote branches yet.';
|
|
3785
|
+
const statusNode = $('github-branches-status');
|
|
3786
|
+
if (statusNode.textContent !== status) statusNode.textContent = status;
|
|
3787
|
+
const failure = $('github-branches-error');
|
|
3788
|
+
failure.hidden = !slot?.error;
|
|
3789
|
+
failure.querySelector('span').textContent = slot?.error ?? '';
|
|
3790
|
+
$('github-branches-retry').disabled = Boolean(slot?.loading) || slot?.nextPage === null || !usable;
|
|
3791
|
+
const metadata = $('github-branches-metadata');
|
|
3792
|
+
metadata.hidden = !value?.metadataError;
|
|
3793
|
+
metadata.textContent = value?.metadataError ? 'Branches loaded, but the default branch could not be identified. Refresh to try again.' : '';
|
|
3794
|
+
const existing = new Map([...list.children].map((row) => [row.dataset.branch, row]));
|
|
3795
|
+
const wanted = new Set(branches.map((branch) => branch.name));
|
|
3796
|
+
let cursor = list.firstElementChild;
|
|
3797
|
+
for (const branch of branches) {
|
|
3798
|
+
let row = existing.get(branch.name);
|
|
3799
|
+
if (!row) {
|
|
3800
|
+
const copy = button('Copy name', () => copyBranch(branch.name, copy));
|
|
3801
|
+
copy.setAttribute('aria-label', `Copy branch name ${branch.name}`);
|
|
3802
|
+
row = recordRow(`branch:${branch.name}`, branch.name, '', copy, branch.url);
|
|
3803
|
+
row.classList.add('github-branch');
|
|
3804
|
+
row.dataset.branch = branch.name;
|
|
3805
|
+
row.setAttribute('role', 'listitem');
|
|
3806
|
+
row.querySelector('.history-title').append(el('span', 'github-branch-badges'));
|
|
3807
|
+
}
|
|
3808
|
+
const badges = row.querySelector('.github-branch-badges');
|
|
3809
|
+
const badgeState = `${branch.name === value.defaultBranch}:${branch.protected}`;
|
|
3810
|
+
if (badges.dataset.state !== badgeState) {
|
|
3811
|
+
badges.dataset.state = badgeState;
|
|
3812
|
+
badges.replaceChildren(...[
|
|
3813
|
+
branch.name === value.defaultBranch ? badge('', 'Default', 'Default branch') : null,
|
|
3814
|
+
branch.protected ? badge('', 'Protected', 'Protected by branch protection or rulesets') : null,
|
|
3815
|
+
].filter(Boolean));
|
|
3816
|
+
}
|
|
3817
|
+
row.querySelector('.history-meta').textContent = branch.sha ? branch.sha.slice(0, 7) : '';
|
|
3818
|
+
row.querySelector('.history-meta').title = branch.sha ?? '';
|
|
3819
|
+
if (row === cursor) cursor = cursor.nextElementSibling;
|
|
3820
|
+
else list.insertBefore(row, cursor);
|
|
3821
|
+
}
|
|
3822
|
+
for (const row of [...list.children]) if (!wanted.has(row.dataset.branch)) row.remove();
|
|
3823
|
+
if (focused?.isConnected && document.activeElement !== focused) focused.focus({ preventScroll: true });
|
|
3824
|
+
panel.scrollTop = oldScroll;
|
|
3825
|
+
if (anchor) {
|
|
3826
|
+
const row = [...list.children].find((node) => node.dataset.branch === anchor.name);
|
|
3827
|
+
if (row) panel.scrollTop += row.getBoundingClientRect().top - panel.getBoundingClientRect().top - anchor.offset;
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3655
3831
|
function viewData(view) {
|
|
3656
3832
|
const slot = githubPick.data[view];
|
|
3657
3833
|
const repo = githubPick.repo;
|
|
@@ -3824,6 +4000,7 @@ function renderGitHubViews() {
|
|
|
3824
4000
|
$('github-issues').hidden = shown !== 'issues';
|
|
3825
4001
|
$('github-runs').hidden = shown !== 'actions';
|
|
3826
4002
|
$('github-pulls').hidden = shown !== 'pulls';
|
|
4003
|
+
$('github-branches').hidden = shown !== 'branches';
|
|
3827
4004
|
const ready = Boolean(state.github && githubAccount() && !githubAccount().needsSignIn);
|
|
3828
4005
|
$('github-clone-into').hidden = !ready || shown !== 'repos';
|
|
3829
4006
|
const running = Boolean(viewData('actions')?.value?.running);
|
|
@@ -3833,6 +4010,7 @@ function renderGitHubViews() {
|
|
|
3833
4010
|
if (shown === 'issues') renderIssues();
|
|
3834
4011
|
else if (shown === 'actions') renderRuns();
|
|
3835
4012
|
else if (shown === 'pulls') renderPulls();
|
|
4013
|
+
else if (shown === 'branches') renderBranches();
|
|
3836
4014
|
}
|
|
3837
4015
|
|
|
3838
4016
|
/** Rebuilds a panel and puts the focus back on the control with the same `data-key`. */
|
|
@@ -5650,6 +5828,18 @@ $('github-views').addEventListener('click', (e) => {
|
|
|
5650
5828
|
if (view) switchGitHubView(view);
|
|
5651
5829
|
});
|
|
5652
5830
|
$('github-views').addEventListener('keydown', moveGitHubView);
|
|
5831
|
+
$('github-branches-refresh').addEventListener('click', () => loadView('branches'));
|
|
5832
|
+
$('github-branches-retry').addEventListener('click', () => loadBranches({ resume: true }));
|
|
5833
|
+
$('github-branches-filter').addEventListener('input', () => {
|
|
5834
|
+
$('github-branches').scrollTop = 0;
|
|
5835
|
+
renderBranches({ preserveAnchor: false });
|
|
5836
|
+
});
|
|
5837
|
+
$('github-branches-clear').addEventListener('click', () => {
|
|
5838
|
+
$('github-branches-filter').value = '';
|
|
5839
|
+
$('github-branches').scrollTop = 0;
|
|
5840
|
+
renderBranches({ preserveAnchor: false });
|
|
5841
|
+
$('github-branches-filter').focus();
|
|
5842
|
+
});
|
|
5653
5843
|
$('github-parent').addEventListener('change', () => setCloneParent(cloneParent()));
|
|
5654
5844
|
$('github-parent-pick').addEventListener('click', firstClick(() => chooseFolder('clone')));
|
|
5655
5845
|
$('history-here').addEventListener('change', renderHistory);
|
|
@@ -5710,6 +5900,7 @@ document.addEventListener('visibilitychange', () => {
|
|
|
5710
5900
|
if (document.visibilityState === 'visible' && state.connected && Date.now() - newsLoadedAt > 60000) loadNews();
|
|
5711
5901
|
if (document.visibilityState === 'visible' && dockShows('github') && githubShownView() === 'actions') loadView('actions');
|
|
5712
5902
|
else scheduleRuns();
|
|
5903
|
+
if (branchesVisible() && !viewData('branches')?.error) loadBranches({ resume: true });
|
|
5713
5904
|
});
|
|
5714
5905
|
addEventListener('pagehide', () => {
|
|
5715
5906
|
flushNotes();
|
package/web/environment.js
CHANGED
|
@@ -92,6 +92,8 @@ export function createEnvironmentUI({ api, onAuthError, isAuthError = () => fals
|
|
|
92
92
|
$('environment-refresh').addEventListener('click', () => load(true));
|
|
93
93
|
dialog.addEventListener('click', (event) => { if (event.target === dialog) dialog.close(); });
|
|
94
94
|
dialog.addEventListener('close', () => {
|
|
95
|
+
// A queued close from an earlier opening must not clear the current opener.
|
|
96
|
+
if (dialog.open) return;
|
|
95
97
|
if (openerId) document.querySelector(`.provider[data-id="${openerId}"] .environment-open`)?.focus();
|
|
96
98
|
openerId = null;
|
|
97
99
|
});
|
package/web/index.html
CHANGED
|
@@ -208,6 +208,7 @@
|
|
|
208
208
|
<button id="github-view-issues" class="github-view" type="button" role="tab" aria-controls="github-issues" aria-selected="false" data-view="issues" tabindex="-1">Issues</button>
|
|
209
209
|
<button id="github-view-actions" class="github-view" type="button" role="tab" aria-controls="github-runs" aria-selected="false" data-view="actions" tabindex="-1" aria-label="Actions">Actions<span id="github-running" class="github-running" hidden></span></button>
|
|
210
210
|
<button id="github-view-pulls" class="github-view" type="button" role="tab" aria-controls="github-pulls" aria-selected="false" data-view="pulls" tabindex="-1" aria-label="Pull requests"><span class="long">Pull requests</span><span class="short" aria-hidden="true">PRs</span></button>
|
|
211
|
+
<button id="github-view-branches" class="github-view" type="button" role="tab" aria-controls="github-branches" aria-selected="false" data-view="branches" tabindex="-1">Branches</button>
|
|
211
212
|
</div>
|
|
212
213
|
<div id="github-repos" class="models-body github-body" role="tabpanel" aria-labelledby="github-view-repos">
|
|
213
214
|
<section id="github-card" class="github-card" aria-live="polite" hidden></section>
|
|
@@ -241,6 +242,27 @@
|
|
|
241
242
|
<div id="github-issues" class="models-body github-body" role="tabpanel" aria-labelledby="github-view-issues" hidden></div>
|
|
242
243
|
<div id="github-runs" class="models-body github-body" role="tabpanel" aria-labelledby="github-view-actions" hidden></div>
|
|
243
244
|
<div id="github-pulls" class="models-body github-body" role="tabpanel" aria-labelledby="github-view-pulls" hidden></div>
|
|
245
|
+
<div id="github-branches" class="models-body github-body" role="tabpanel" aria-labelledby="github-view-branches" hidden>
|
|
246
|
+
<p id="github-branches-pick" class="github-view-note">Pick a repository above to see its branches.</p>
|
|
247
|
+
<div id="github-branches-content" hidden>
|
|
248
|
+
<div class="github-view-tools">
|
|
249
|
+
<button id="github-branches-refresh" class="btn" type="button">Refresh</button>
|
|
250
|
+
<span class="github-spacer"></span>
|
|
251
|
+
<a id="github-branches-out" class="btn github-out" target="_blank" rel="noopener noreferrer" aria-description="Opens in a new tab">GitHub</a>
|
|
252
|
+
</div>
|
|
253
|
+
<div class="github-branch-filter">
|
|
254
|
+
<input id="github-branches-filter" type="search" placeholder="Filter branches" aria-label="Filter branches" spellcheck="false" autocomplete="off">
|
|
255
|
+
<button id="github-branches-clear" class="btn" type="button" disabled>Clear</button>
|
|
256
|
+
</div>
|
|
257
|
+
<p id="github-branches-status" class="github-view-note" role="status" aria-live="polite" aria-atomic="true"></p>
|
|
258
|
+
<div id="github-branches-error" class="github-error" hidden>
|
|
259
|
+
<span role="alert"></span>
|
|
260
|
+
<button id="github-branches-retry" class="btn" type="button">Retry</button>
|
|
261
|
+
</div>
|
|
262
|
+
<p id="github-branches-metadata" class="github-view-note" hidden></p>
|
|
263
|
+
<div id="github-branches-list" class="history-list" role="list" aria-label="Remote branches"></div>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
244
266
|
<div id="github-clone-into" class="history-form github-form">
|
|
245
267
|
<label for="github-parent">Clone into</label>
|
|
246
268
|
<div class="cwd-field single">
|
package/web/styles.css
CHANGED
|
@@ -898,10 +898,27 @@ a.news-link:visited { color: var(--muted); }
|
|
|
898
898
|
.github-view[aria-selected="true"] { color: var(--heading); background: var(--surface-2); border-color: var(--border); border-bottom-color: var(--surface-2); }
|
|
899
899
|
.github-view:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
|
|
900
900
|
.github-view .short { display: none; }
|
|
901
|
-
@container (max-width:
|
|
901
|
+
@container (max-width: 440px) {
|
|
902
902
|
.github-view .long { display: none; }
|
|
903
903
|
.github-view .short { display: inline; }
|
|
904
904
|
}
|
|
905
|
+
#github-branches { overflow-anchor: none; }
|
|
906
|
+
.github-branch-filter { display: flex; align-items: center; gap: 6px; }
|
|
907
|
+
.github-branch-filter input {
|
|
908
|
+
flex: 1; min-width: 0; padding: 7px 10px; border: 1px solid var(--border); border-radius: var(--radius);
|
|
909
|
+
background: var(--surface); color: var(--text); font: inherit; font-size: 0.85rem;
|
|
910
|
+
}
|
|
911
|
+
.github-branch-filter input:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
|
|
912
|
+
.github-branch .history-title { display: flex; flex-wrap: wrap; gap: 4px 8px; align-items: baseline; }
|
|
913
|
+
.github-branch .github-record-title { min-width: 0; overflow-wrap: anywhere; }
|
|
914
|
+
.github-branch-badges { display: inline-flex; flex-wrap: wrap; gap: 4px; }
|
|
915
|
+
.github-branch .history-meta { font-family: var(--font-mono, monospace); }
|
|
916
|
+
.github-branch > .btn { align-self: start; }
|
|
917
|
+
#github-branches-error .btn { margin-left: 6px; }
|
|
918
|
+
@media (pointer: coarse) {
|
|
919
|
+
.github-branch-filter input { font-size: 16px; }
|
|
920
|
+
.github-branch > .btn, #github-branches .github-view-tools .btn, .github-branch-filter .btn { min-height: 36px; }
|
|
921
|
+
}
|
|
905
922
|
.github-running { width: 8px; height: 8px; border-radius: 50%; background: var(--warn); box-shadow: 0 0 0 3px color-mix(in srgb, var(--warn) 25%, transparent); animation: pulse 1.4s ease-in-out infinite; }
|
|
906
923
|
.github-body[hidden] { display: none; }
|
|
907
924
|
.github-view-tools { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding-bottom: 6px; }
|